diff --git a/shell.nix b/shell.nix index e8139d5..f0142e6 100644 --- a/shell.nix +++ b/shell.nix @@ -14,7 +14,10 @@ pkgs.mkShell { coreutils findutils bash + + # Extra buildtools graphviz + scc # Elixir elixir_1_14 @@ -24,6 +27,11 @@ pkgs.mkShell { glew # An OpenGL extension loading library for C/C++ pkg-config gcc + + # For Membrane + portaudio + libmad + ffmpeg_6-headless ] ++ lib.optionals stdenv.isDarwin [ darwin.apple_sdk.frameworks.Cocoa ]; diff --git a/snake-ecs/.formatter.exs b/snake-ecs/.formatter.exs index d2cda26..84f690c 100644 --- a/snake-ecs/.formatter.exs +++ b/snake-ecs/.formatter.exs @@ -1,4 +1,8 @@ # Used by "mix format" +locals_without_parens = [member: 2, member: 3, name: 1, component: 1, component: 2, wants: 1] + [ - inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] + inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"], + locals_without_parens: locals_without_parens, + import_deps: [:typed_struct] ] diff --git a/snake-ecs/.gradient_ignore.exs b/snake-ecs/.gradient_ignore.exs new file mode 100644 index 0000000..579c3da --- /dev/null +++ b/snake-ecs/.gradient_ignore.exs @@ -0,0 +1,4 @@ +[ + # Ignore the spec error from using Membrane + "lib/engine/sounds.ex:1" +] diff --git a/snake-ecs/Makefile b/snake-ecs/Makefile new file mode 100644 index 0000000..6aa6075 --- /dev/null +++ b/snake-ecs/Makefile @@ -0,0 +1,32 @@ +include modules/make/boilerplate.mk +include modules/make/elixir-build.mk + +.PHONY: interactive-server is +## Shorthand for interactive-server +is: interactive-server +## Start Athena in interactive mode with: +## - Phoenix running on http://localhost:3000 +interactive-server: deps + @${source} + iex -S mix phx.server + +.PHONY: test +## Run test suite +test: deps + export ERL_FLAGS=-kernel start_pg true + mix test --cover + +test-ci: deps + mix test + +.PHONY: tc typecheck +tc: typecheck +typecheck: + mix gradient && mix dialyzer + +.PHONY: test-watch +## Run test suite with code watcher +## Use --stale option (https://github.com/lpil/mix-test.watch#running-tests-of-modules-that-changed) +test-watch: deps + export ERL_FLAGS=-kernel start_pg true + mix test.watch --stale diff --git a/snake-ecs/lib/components/stats.ex b/snake-ecs/lib/components/stats.ex new file mode 100644 index 0000000..3477f95 --- /dev/null +++ b/snake-ecs/lib/components/stats.ex @@ -0,0 +1,5 @@ +use Engine.DSL.Component + +defcomponent Stats do + member :hp, 100 +end diff --git a/snake-ecs/lib/engine/DSL/component.ex b/snake-ecs/lib/engine/DSL/component.ex new file mode 100644 index 0000000..1d920bf --- /dev/null +++ b/snake-ecs/lib/engine/DSL/component.ex @@ -0,0 +1,60 @@ +defmodule Engine.DSL.Component do + defmacro __using__(_options) do + quote do + use TypedStruct + import Engine.DSL.Component + end + end + + defmacro defcomponent(name, do: block) do + quote do + defmodule Engine.ComponentTypes.unquote(name) do + typedstruct do + unquote(block) + end + end + end + end + + defmacro member(name, default_value) when is_atom(default_value) do + quote do + field(unquote(name), atom(), default: unquote(default_value)) + end + end + + defmacro member(name, default_value) when is_boolean(default_value) do + quote do + field(unquote(name), boolean(), default: unquote(default_value)) + end + end + + defmacro member(name, default_value) when is_integer(default_value) do + quote do + field(unquote(name), integer(), default: unquote(default_value)) + end + end + + defmacro member(name, default_value) when is_float(default_value) do + quote do + field(unquote(name), float(), default: unquote(default_value)) + end + end + + defmacro member(name, default_value) when is_binary(default_value) do + quote do + field(unquote(name), String.t(), default: unquote(default_value)) + end + end + + defmacro member(name, default_value) when is_list(default_value) do + quote do + field(unquote(name), list(), default: unquote(default_value)) + end + end + + defmacro member(name, default_value) when is_map(default_value) do + quote do + field(unquote(name), %{}, default: unquote(default_value)) + end + end +end diff --git a/snake-ecs/lib/engine/DSL/entity.ex b/snake-ecs/lib/engine/DSL/entity.ex new file mode 100644 index 0000000..98527ce --- /dev/null +++ b/snake-ecs/lib/engine/DSL/entity.ex @@ -0,0 +1,64 @@ +defmodule Engine.DSL.Entity do + defmacro __using__(_options) do + quote do + import Engine.DSL.Entity + end + end + + defmacro defentity(name, do: block) do + quote do + # Maybe it should Game.Entity instead of Engine.EntityTypes + defmodule Engine.EntityTypes.unquote(name) do + alias Engine.Entity + + Module.register_attribute(__MODULE__, :components, accumulate: true, persist: true) + Module.register_attribute(__MODULE__, :entity_name, persist: true) + + unquote(block) + + defp __full_type(type) do + Module.concat(Engine.ComponentTypes, type) + end + + def create do + components = + Enum.reduce(@components, %{}, fn {type, default_data}, acc -> + full_type = __full_type(type) + Map.put_new(acc, full_type, struct(full_type, default_data)) + end) + + %Entity.Data{ + components: components + } + end + end + end + end + + defmacro component(component_type) do + quote do + @components {unquote(component_type), %{}} + end + end + + defmacro component(component_type, default_data) do + {:__aliases__, _, [type]} = component_type + full_type = Module.concat(Engine.ComponentTypes, type) + + # Some person on the Internet[1] say this function is unsafe and can + # deadlock, and that the recommended function is actually ensure_loaded? + # + # [1]: https://github.com/elixirmoney/money/pull/131 + if !Code.ensure_compiled(full_type) do + raise(CompileError, + description: "Component #{type} does not exist!", + file: __CALLER__.file, + line: __CALLER__.line + ) + end + + quote do + @components {unquote(component_type), unquote(default_data)} + end + end +end diff --git a/snake-ecs/lib/engine/DSL/system.ex b/snake-ecs/lib/engine/DSL/system.ex new file mode 100644 index 0000000..fdca783 --- /dev/null +++ b/snake-ecs/lib/engine/DSL/system.ex @@ -0,0 +1,146 @@ +defmodule Engine.DSL.System do + defmacro __using__(_options) do + quote do + import Engine.DSL.System + end + end + + defmacro defsystem(name, do: block) do + quote do + defmodule RuntimeSystems.unquote(name) do + alias Engine.Entity + + require Logger + + Module.register_attribute(__MODULE__, :wants, accumulate: true, persist: true) + Module.register_attribute(__MODULE__, :system_name, persist: true) + + defp __get_component_data(entity_pid, component_type, key) do + case GenServer.call(entity_pid, {:get_component, component_type}) do + %{} = component -> + get_in(component, [Access.key!(key)]) + + _ -> + Logger.warn( + "Could not fetch component #{inspect(component_type)} on #{inspect(entity_pid)}" + ) + + nil + end + end + + defp __get_all_components(entity_pid) do + GenServer.call(entity_pid, :get_all_components) + end + + defp __set_component_data(entity_pid, component_type, key, new_value) do + GenServer.call(entity_pid, {:set_component_data, component_type, key, new_value}) + end + + defp __add_component(entity_pid, component_type, default_data) do + GenServer.call(entity_pid, {:add_component, component_type, default_data}) + end + + defp __remove_component(entity_pid, component_type) do + GenServer.call(entity_pid, {:remove_component, component_type}) + end + + unquote(block) + + def wants do + @wants + end + + def name do + @system_name + end + end + end + end + + defmacro log(item) do + quote do + Engine.Util.SystemLog.debug(unquote(item)) + end + end + + defmacro warn(item) do + quote do + Engine.Util.SystemLog.warn(unquote(item)) + end + end + + defmacro get_component_data(component_type, key) do + quote do + __get_component_data(var!(entity), unquote(component_type), unquote(key)) + end + end + + defmacro get_all_components() do + quote do + __get_all_components(var!(entity)) + end + end + + defmacro set_component_data(component_type, key, new_data) do + quote do + __set_component_data(var!(entity), unquote(component_type), unquote(key), unquote(new_data)) + end + end + + defmacro add_component(component_type) do + quote do + __add_component(var!(entity), unquote(component_type)) + end + end + + defmacro remove_component(component_type) do + quote do + __remove_component(var!(entity), unquote(component_type)) + end + end + + defmacro name(name) do + quote do + @system_name unquote(name) + end + end + + defmacro wants(component_name) do + {:__aliases__, _, [type]} = component_name + full_type = Module.concat(ComponentTypes, type) + + # Some person on the Internet[1] say this function is unsafe and can + # deadlock, and that the recommended function is actually ensure_loaded? + # + # [1]: https://github.com/elixirmoney/money/pull/131 + if !Code.ensure_compiled(full_type) do + raise(CompileError, + description: "Component #{type} does not exist!", + file: __CALLER__.file, + line: __CALLER__.line + ) + end + + quote do + @wants unquote(component_name) + end + end + + defmacro on_tick(do: block) do + quote do + alias Engine.Util.Performance + + def __process_entity(var!(entity), var!(world_name), var!(frontend_pid), var!(delta_time)) do + unquote(block) + end + + def __tick(entity_list, world_name, frontend_pid, delta_time) + when is_list(entity_list) and is_atom(world_name) do + Engine.Util.Performance.parallel_map(entity_list, fn ent -> + __process_entity(ent, world_name, frontend_pid, delta_time) + end) + end + end + end +end diff --git a/snake-ecs/lib/engine/action.ex b/snake-ecs/lib/engine/action.ex new file mode 100644 index 0000000..07a885f --- /dev/null +++ b/snake-ecs/lib/engine/action.ex @@ -0,0 +1,30 @@ +defmodule Engine.Action do + use TypedStruct + + require Logger + + alias __MODULE__ + + typedstruct do + field(:action_type, atom(), enforce: true) + field(:target_entity, pid(), enforce: true) + field(:payload, %{}, enforce: true) + end + + def create(type, target, extra_data \\ %{}) do + %Action{ + action_type: type, + target_entity: target, + payload: extra_data + } + end + + def execute(%Action{} = action) do + if Process.alive?(action.target_entity) do + Logger.debug("Action enqueued from: #{inspect(action)}") + GenServer.call(action.target_entity, {:action_recv, action}) + else + Logger.warn("Action was dropped because target PID was dead: #{inspect(action)}") + end + end +end diff --git a/snake-ecs/lib/engine/entity.ex b/snake-ecs/lib/engine/entity.ex new file mode 100644 index 0000000..5b7e306 --- /dev/null +++ b/snake-ecs/lib/engine/entity.ex @@ -0,0 +1,148 @@ +defmodule Engine.Entity do + use GenServer + + require Logger + + alias Engine.Action + alias Engine.Entity + + @spec create(atom(), atom()) :: GenServer.on_start() + def create(type, world_name \\ :global) when is_atom(type) do + full_type = Module.concat(Engine.EntityTypes, type) + data = full_type.create() + data = %Entity.Data{data | world_name: world_name, action_queue: Qex.new()} + start_link(data) + end + + @spec pop_action(pid()) :: Engine.Action.t() | :empty + def pop_action(entity) do + GenServer.call(entity, :pop_action) + end + + # TODO: Can't type Component, because there are many ComponentTypes + # Not sure how to type that more generically + # @spec pop_action(pid()) :: Engine.Component.Data.t() | :empty + @spec get_component(pid(), atom()) :: any() + def(get_component(entity, type)) do + GenServer.call(entity, {:get_component, type}) + end + + @spec set_component_data(pid(), atom(), atom(), any()) :: :ok | :error + def set_component_data(entity, type, key, value) do + GenServer.call(entity, {:set_component_data, type, key, value}) + end + + def start_link(%Entity.Data{} = entity_data) do + GenServer.start_link(__MODULE__, data: entity_data) + end + + @spec destroy(pid()) :: :ok + def destroy(entity) do + GenServer.call(entity, :destroy) + end + + @impl GenServer + def init(data: entity_data) do + Enum.each(entity_data.components, fn {k, _} -> + register_with_component_group(k, entity_data.world_name) + end) + + {:ok, entity_data} + end + + # Calls for component manipulation + + @impl GenServer + def handle_call({:add_component, type, data}, _from, entity_data) + when is_atom(type) and is_map(data) do + full_type = Module.concat(Engine.ComponentTypes, type) + + if Map.has_key?(entity_data.components, type) do + {:reply, :error, entity_data} + else + new_component = struct(full_type, data) + + register_with_component_group(type, entity_data.world_name) + + {:reply, :ok, + %Entity.Data{ + entity_data + | components: Map.put_new(entity_data.components, type, new_component) + }} + end + end + + def handle_call({:remove_component, type}, _from, entity_data) when is_atom(type) do + unregister_with_component_group(type, entity_data.world_name) + + {:reply, :ok, + %Entity.Data{entity_data | components: Map.delete(entity_data.components, type)}} + end + + def handle_call({:get_component, type}, _from, entity_data) when is_atom(type) do + {:reply, Map.get(entity_data.components, type), entity_data} + end + + def handle_call(:get_all_components, _from, entity_data) do + {:reply, entity_data.components, entity_data} + end + + def handle_call({:set_world_name, world_name}, _from, entity_data) when is_pid(world_name) do + {:reply, :ok, %Entity.Data{entity_data | world_name: world_name}} + end + + def handle_call({:set_component_data, type, key, value}, _from, entity_data) do + case Map.get(entity_data.components, type) do + %{} = component -> + updated_component = %{component | key => value} + + {:reply, :ok, + %Entity.Data{ + entity_data + | components: %{entity_data.components | type => updated_component} + }} + + _ -> + {:reply, :error, entity_data} + end + end + + # Calls for action queue management + + def handle_call({:action_recv, %Action{} = action}, _from, entity_data) do + {:reply, :ok, + %Entity.Data{entity_data | action_queue: Qex.push(entity_data.action_queue, action)}} + end + + def handle_call(:pop_action, _from, entity_data) do + case Qex.pop(entity_data.action_queue) do + {:empty, _} -> {:reply, :empty, entity_data} + {{:value, action}, rest} -> {:reply, action, %Entity.Data{entity_data | action_queue: rest}} + end + end + + def handle_call(:destroy, _from, state) do + # TODO: remove the pid from the groups is part of? Feels pg may not automagically detect that the pid is dead + {:stop, :normal, :ok, state} + end + + # Catch-all Call because sometimes it helps :D + + def handle_call(unknown_message, from, entity_data) do + Logger.warn( + "#{inspect(self())} got an unknown message from #{inspect(from)}: #{inspect(unknown_message)}" + ) + + {:reply, :ok, entity_data} + end + + # Private helper functions + + defp register_with_component_group(type, world_name) do + Entity.Store.add_to_group(type, world_name, self()) + end + + defp unregister_with_component_group(type, world_name) do + Entity.Store.remove_from_group(type, world_name, self()) + end +end diff --git a/snake-ecs/lib/engine/entity/data.ex b/snake-ecs/lib/engine/entity/data.ex new file mode 100644 index 0000000..d2c2e18 --- /dev/null +++ b/snake-ecs/lib/engine/entity/data.ex @@ -0,0 +1,9 @@ +defmodule Engine.Entity.Data do + use TypedStruct + + typedstruct do + field(:world_name, atom(), default: :global) + field(:components, map(), default: %{}) + field(:action_queue, Qex.t(), default: nil) + end +end diff --git a/snake-ecs/lib/engine/entity/store.ex b/snake-ecs/lib/engine/entity/store.ex new file mode 100644 index 0000000..ed09b5c --- /dev/null +++ b/snake-ecs/lib/engine/entity/store.ex @@ -0,0 +1,37 @@ +defmodule Engine.Entity.Store do + # The fact that all this use Module.concat feels wrong + + @spec add_to_group(atom(), atom() | binary(), pid()) :: :ok + def add_to_group(group, world_name, entity) when is_atom(group) and is_pid(entity) do + full_name = Module.concat(world_name, group) + :pg.join(full_name, entity) + end + + @spec remove_from_group(atom(), atom() | binary(), pid()) :: :ok + def remove_from_group(group, world_name, entity) + when is_atom(group) and is_pid(entity) do + full_name = Module.concat(world_name, group) + :pg.leave(full_name, entity) + end + + @spec get_with(list(atom()) | atom(), atom()) :: list(pid()) + def get_with(single_want_list, world_name) when is_atom(single_want_list) do + full_name = Module.concat(world_name, single_want_list) + + :pg.get_members(full_name) + end + + # TODO: Replace :sets with MapSet, maybe add more "complex" tests before this refactoring + def get_with(want_list, world_name) when is_list(want_list) do + Enum.map(want_list, fn want -> + full_name = Module.concat(world_name, want) + + :pg.get_members(full_name) + |> :sets.from_list() + end) + # With one args, it expect a list of set + # https://www.erlang.org/doc/man/sets.html#intersection-1 + |> :sets.intersection() + |> :sets.to_list() + end +end diff --git a/snake-ecs/lib/engine/sounds.ex b/snake-ecs/lib/engine/sounds.ex new file mode 100644 index 0000000..9046caa --- /dev/null +++ b/snake-ecs/lib/engine/sounds.ex @@ -0,0 +1,30 @@ +defmodule Engine.Sounds do + use Membrane.Pipeline + + @impl true + def handle_init(path_to_mp3) do + children = %{ + file: %Membrane.File.Source{location: path_to_mp3}, + decoder: Membrane.MP3.MAD.Decoder, + converter: %Membrane.FFmpeg.SWResample.Converter{ + output_caps: %Membrane.RawAudio{ + sample_format: :s16le, + sample_rate: 48000, + channels: 2 + } + }, + portaudio: Membrane.PortAudio.Sink + } + + links = [ + link(:file) + |> to(:decoder) + |> to(:converter) + |> to(:portaudio) + ] + + spec = %ParentSpec{children: children, links: links} + + {{:ok, spec: spec}, %{}} + end +end diff --git a/snake-ecs/lib/engine/util/module.ex b/snake-ecs/lib/engine/util/module.ex new file mode 100644 index 0000000..8e4e988 --- /dev/null +++ b/snake-ecs/lib/engine/util/module.ex @@ -0,0 +1,39 @@ +defmodule Engine.Util.Module do + @spec get_entity_types() :: list(atom()) + def get_entity_types do + {:ok, modules} = :application.get_key(:snake, :modules) + + modules + |> Enum.filter(fn module -> + parts = Module.split(module) + + # TODO: convert the module beginning name to string instead of using string (for compiling error) + # TODO: Also, consider making this a configuration. It should not be Engine.EntityTypes by default also, maybe just Entity.xxx, because it's not the entities of the engine. + match?(["Engine", "EntityTypes" | _], parts) + end) + end + + @spec get_component_types() :: list(atom()) + def get_component_types do + {:ok, modules} = :application.get_key(:snake, :modules) + + modules + |> Enum.filter(fn module -> + parts = Module.split(module) + # TODO: convert the moduleule name to string instead of using string (for compiling error) + match?(["Engine", "ComponentTypes" | _], parts) + end) + end + + @spec get_system_types() :: list(atom()) + def get_system_types do + {:ok, modules} = :application.get_key(:snake, :modules) + + modules + |> Enum.filter(fn module -> + parts = Module.split(module) + # TODO: convert the moduleule name to string instead of using string (for compiling error) + match?(["Engine", "RuntimeSystems" | _], parts) + end) + end +end diff --git a/snake-ecs/lib/engine/util/performance.ex b/snake-ecs/lib/engine/util/performance.ex new file mode 100644 index 0000000..6afe82c --- /dev/null +++ b/snake-ecs/lib/engine/util/performance.ex @@ -0,0 +1,8 @@ +defmodule Engine.Util.Performance do + # TODO: pretty sure we don't need that. + def parallel_map(collection, func) do + collection + |> Enum.map(&Task.async(fn -> func.(&1) end)) + |> Enum.each(&Task.await/1) + end +end diff --git a/snake-ecs/lib/game.ex b/snake-ecs/lib/game.ex index af8eac4..f5b6bc9 100644 --- a/snake-ecs/lib/game.ex +++ b/snake-ecs/lib/game.ex @@ -9,6 +9,13 @@ defmodule Game do # start the application with the viewport children = [ + # Process Group + # To manage groups of entities in the store + # Or, one can export ERL_FLAGS="-kernel start_pg true" + %{ + id: :pg, + start: {:pg, :start_link, []} + }, {Scenic, [main_viewport_config]}, PubSub.Supervisor ] diff --git a/snake-ecs/mix.exs b/snake-ecs/mix.exs index b6f8d03..5401819 100644 --- a/snake-ecs/mix.exs +++ b/snake-ecs/mix.exs @@ -8,7 +8,8 @@ defmodule Snake.MixProject do elixir: "~> 1.14", build_embedded: true, start_permanent: Mix.env() == :prod, - deps: deps() + deps: deps(), + test_coverage: [tool: Coverex.Task] ] end @@ -26,7 +27,18 @@ defmodule Snake.MixProject do {:scenic, "~> 0.11.0"}, {:scenic_driver_local, "~> 0.11.0"}, {:typed_struct, "~> 0.3.0"}, - {:qex, "~> 0.5"} + {:qex, "~> 0.5"}, + {:membrane_core, "~> 0.10"}, + {:membrane_file_plugin, "~> 0.12"}, + {:membrane_portaudio_plugin, "~> 0.13"}, + {:membrane_ffmpeg_swresample_plugin, "~> 0.15"}, + {:membrane_mp3_mad_plugin, "~> 0.13.0"}, + {:coverex, "~> 1.4.15", only: :test}, + {:dialyxir, "~> 1.3", only: [:dev], runtime: false}, + {:credo, "~> 1.7", only: [:dev, :test], runtime: false}, + {:gradient, github: "esl/gradient", only: [:dev], runtime: false}, + {:mix_test_watch, "~> 1.1", only: [:dev, :test], runtime: false}, + {:ex_unit_notifier, "~> 1.3", only: :test} ] end end diff --git a/snake-ecs/mix.lock b/snake-ecs/mix.lock index e38c582..8e61e19 100644 --- a/snake-ecs/mix.lock +++ b/snake-ecs/mix.lock @@ -1,12 +1,55 @@ %{ + "bimap": {:hex, :bimap, "1.3.0", "3ea4832e58dc83a9b5b407c6731e7bae87458aa618e6d11d8e12114a17afa4b3", [:mix], [], "hexpm", "bf5a2b078528465aa705f405a5c638becd63e41d280ada41e0f77e6d255a10b4"}, + "bunch": {:hex, :bunch, "1.3.2", "9a3647e8bf8859482206c554d907b13d60aa8e40a3b82057a34bf71c0e23a0ae", [:mix], [], "hexpm", "dd89f2df6e6284c06cf9c44be5aae622f2a5a0804098167a0cb5370542c8c22b"}, + "bunch_native": {:hex, :bunch_native, "0.5.0", "8ac1536789a597599c10b652e0b526d8833348c19e4739a0759a2bedfd924e63", [:mix], [{:bundlex, "~> 1.0", [hex: :bundlex, repo: "hexpm", optional: false]}], "hexpm", "24190c760e32b23b36edeb2dc4852515c7c5b3b8675b1a864e0715bdd1c8f80d"}, + "bundlex": {:hex, :bundlex, "1.1.1", "e637b79a1eaab1bf019de4100b6db262aa3b660beff0cd2f3617949b1618eeda", [:mix], [{:bunch, "~> 1.0", [hex: :bunch, repo: "hexpm", optional: false]}, {:qex, "~> 0.5", [hex: :qex, repo: "hexpm", optional: false]}, {:secure_random, "~> 0.5", [hex: :secure_random, repo: "hexpm", optional: false]}], "hexpm", "1fdfa3d6240baa5a2d5496a86e2e43116f80105e93d9adfd4f1fc75be487ea30"}, + "bunt": {:hex, :bunt, "0.2.1", "e2d4792f7bc0ced7583ab54922808919518d0e57ee162901a16a1b6664ef3b14", [:mix], [], "hexpm", "a330bfb4245239787b15005e66ae6845c9cd524a288f0d141c148b02603777a5"}, + "certifi": {:hex, :certifi, "2.9.0", "6f2a475689dd47f19fb74334859d460a2dc4e3252a3324bd2111b8f0429e7e21", [:rebar3], [], "hexpm", "266da46bdb06d6c6d35fde799bcb28d36d985d424ad7c08b5bb48f5b5cdd4641"}, + "coerce": {:hex, :coerce, "1.0.1", "211c27386315dc2894ac11bc1f413a0e38505d808153367bd5c6e75a4003d096", [:mix], [], "hexpm", "b44a691700f7a1a15b4b7e2ff1fa30bebd669929ac8aa43cffe9e2f8bf051cf1"}, + "coverex": {:hex, :coverex, "1.4.15", "60fadf825a6c0439b79d1f98cdb54b6733cdd5cb1b35d15d56026c44ed15a5a8", [:mix], [{:hackney, "~> 1.5", [hex: :hackney, repo: "hexpm", optional: false]}, {:poison, "~> 1.5 or ~> 2.0 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm", "bfde7ad7ecb83e0199902d087caf570057047aa004cfd2eafef3b42a5428103c"}, + "credo": {:hex, :credo, "1.7.0", "6119bee47272e85995598ee04f2ebbed3e947678dee048d10b5feca139435f75", [:mix], [{:bunt, "~> 0.2.1", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "6839fcf63d1f0d1c0f450abc8564a57c43d644077ab96f2934563e68b8a769d7"}, + "dialyxir": {:hex, :dialyxir, "1.3.0", "fd1672f0922b7648ff9ce7b1b26fcf0ef56dda964a459892ad15f6b4410b5284", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "00b2a4bcd6aa8db9dcb0b38c1225b7277dca9bc370b6438715667071a304696f"}, "elixir_make": {:hex, :elixir_make, "0.6.3", "bc07d53221216838d79e03a8019d0839786703129599e9619f4ab74c8c096eac", [:mix], [], "hexpm", "f5cbd651c5678bcaabdbb7857658ee106b12509cd976c2c2fca99688e1daf716"}, + "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, "ex_image_info": {:hex, :ex_image_info, "0.2.4", "610002acba43520a9b1cf1421d55812bde5b8a8aeaf1fe7b1f8823e84e762adb", [:mix], [], "hexpm", "fd1a7e02664e3b14dfd3b231d22fdd48bd3dd694c4773e6272b3a6228f1106bc"}, + "ex_unit_notifier": {:hex, :ex_unit_notifier, "1.3.0", "1d82aa6d2fb44e6f0f219142661a46e13dcba833e150e1395190d2e0fb721990", [:mix], [], "hexpm", "55fffd6062e8d962fc44e8b06fa30a87dc7251ee2a69f520781a3bb29858c365"}, + "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"}, "font_metrics": {:hex, :font_metrics, "0.5.1", "10ce0b8b1bf092a2d3d307e05a7c433787ae8ca7cd1f3cf959995a628809a994", [:mix], [{:nimble_options, "~> 0.3", [hex: :nimble_options, repo: "hexpm", optional: false]}], "hexpm", "192e4288772839ae4dadccb0f5b1d5c89b73a0c3961ccea14b6181fdcd535e54"}, + "gradient": {:git, "https://github.com/esl/gradient.git", "3a795ed8b2949b9fb5d3826b57bba948ed0cf049", []}, + "gradient_macros": {:git, "https://github.com/esl/gradient_macros.git", "3bce2146bf0cdf380f773c40e2b7bd6558ab6de8", [ref: "3bce214"]}, + "gradualixir": {:git, "https://github.com/overminddl1/gradualixir.git", "60caa8049f3e1b18551642543e7255073f52d366", [ref: "master"]}, + "gradualizer": {:git, "https://github.com/josefs/Gradualizer.git", "1498d1792155010950c86dc3e92ccb111b706e80", [ref: "1498d17"]}, + "hackney": {:hex, :hackney, "1.18.1", "f48bf88f521f2a229fc7bae88cf4f85adc9cd9bcf23b5dc8eb6a1788c662c4f6", [:rebar3], [{:certifi, "~> 2.9.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "a4ecdaff44297e9b5894ae499e9a070ea1888c84afdd1fd9b7b2bc384950128e"}, + "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, "input_event": {:hex, :input_event, "1.2.0", "18297c9572ace3b7f7f9e586c8cd5e9425e3cb6270d8d35322242c351d6ae5e0", [:make, :mix], [{:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "1dc96fd4fb08b595bf1a93dd6c9d13f8aa5cfc52cff9e1179c3f994aaa9deeac"}, + "jason": {:hex, :jason, "1.4.0", "e855647bc964a44e2f67df589ccf49105ae039d4179db7f6271dfd3843dc27e6", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "79a3791085b2a0f743ca04cec0f7be26443738779d09302e01318f97bdb82121"}, + "membrane_caps_audio_mpeg": {:hex, :membrane_caps_audio_mpeg, "0.2.0", "9cf9a63f03e25b31cf31445325aa68e60a07d36ee1e759caa1422fa45df49367", [:mix], [], "hexpm", "f7a80e4841d46164c148be880932ac7425329f4bcc32eb36ad2e47eafe5f23e4"}, + "membrane_common_c": {:hex, :membrane_common_c, "0.13.0", "c314623f93209eb2fa092379954c686f6e50ac89baa48360f836d24f4d53f5ee", [:mix], [{:membrane_core, "~> 0.10.0", [hex: :membrane_core, repo: "hexpm", optional: false]}, {:shmex, "~> 0.5.0", [hex: :shmex, repo: "hexpm", optional: false]}, {:unifex, "~> 1.0", [hex: :unifex, repo: "hexpm", optional: false]}], "hexpm", "90181fbbe481ccd0a4a76daf0300f8ad1b5b0bf0ebd8b42c133904f8839663ca"}, + "membrane_core": {:hex, :membrane_core, "0.10.2", "d2d17039f6df746e4a3c47da32f51867fbafe528272cdd9b226d16b1032bc337", [:mix], [{:bunch, "~> 1.3", [hex: :bunch, repo: "hexpm", optional: false]}, {:qex, "~> 0.3", [hex: :qex, repo: "hexpm", optional: false]}, {:ratio, "~> 2.0", [hex: :ratio, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "6a4f290f919ada66c772807d64d5830be2962b7c13a2f2bc9ace416a1cd19ee1"}, + "membrane_ffmpeg_swresample_plugin": {:hex, :membrane_ffmpeg_swresample_plugin, "0.15.0", "a3774b1a5ab1722c81857bb5db9b24c7812772c035c76ffb6f5c93b2f0c9a039", [:mix], [{:bunch, "~> 1.3.0", [hex: :bunch, repo: "hexpm", optional: false]}, {:bundlex, "~> 1.0", [hex: :bundlex, repo: "hexpm", optional: false]}, {:membrane_common_c, "~> 0.13.0", [hex: :membrane_common_c, repo: "hexpm", optional: false]}, {:membrane_core, "~> 0.10.0", [hex: :membrane_core, repo: "hexpm", optional: false]}, {:membrane_raw_audio_format, "~> 0.9.0", [hex: :membrane_raw_audio_format, repo: "hexpm", optional: false]}, {:mockery, "~> 2.1", [hex: :mockery, repo: "hexpm", optional: false]}, {:unifex, "~> 1.0", [hex: :unifex, repo: "hexpm", optional: false]}], "hexpm", "62058ddc3b11e53c31c12a71d7317cb518c8f87d7fcf836ff0b590cb4c9b7283"}, + "membrane_file_plugin": {:hex, :membrane_file_plugin, "0.12.0", "eb940e7a2f2abf30e048bd0b7c2bef9c17c18aa58875b9a833c0bc7e7b1fd709", [:mix], [{:membrane_core, "~> 0.10.0", [hex: :membrane_core, repo: "hexpm", optional: false]}], "hexpm", "281b9bf9467beead3f973adce55b9844bc4206bb3f3f60f0db8320a4af4fc5ca"}, + "membrane_mp3_mad_plugin": {:hex, :membrane_mp3_mad_plugin, "0.13.0", "b02d69c78cbf5c691d838212090d2ada6c7030d91c3fc9b755f7b2277b50d9e8", [:mix], [{:membrane_caps_audio_mpeg, "~> 0.2.0", [hex: :membrane_caps_audio_mpeg, repo: "hexpm", optional: false]}, {:membrane_common_c, "~> 0.13.0", [hex: :membrane_common_c, repo: "hexpm", optional: false]}, {:membrane_core, "~> 0.10.0", [hex: :membrane_core, repo: "hexpm", optional: false]}, {:membrane_raw_audio_format, "~> 0.9.0", [hex: :membrane_raw_audio_format, repo: "hexpm", optional: false]}, {:unifex, "~> 1.0", [hex: :unifex, repo: "hexpm", optional: false]}], "hexpm", "23cf358843f182596d0f20456fea7388cba886d80eb8bbb5bcb5a43132052fde"}, + "membrane_portaudio_plugin": {:hex, :membrane_portaudio_plugin, "0.13.0", "85cc9bc9fb20cdd8ef097d8c8c8956aaff7523a46223b019b27d50a1361caf86", [:mix], [{:bunch, "~> 1.3.0", [hex: :bunch, repo: "hexpm", optional: false]}, {:bundlex, "~> 1.0", [hex: :bundlex, repo: "hexpm", optional: false]}, {:membrane_common_c, "~> 0.13.0", [hex: :membrane_common_c, repo: "hexpm", optional: false]}, {:membrane_core, "~> 0.10.0", [hex: :membrane_core, repo: "hexpm", optional: false]}, {:membrane_raw_audio_format, "~> 0.9.0", [hex: :membrane_raw_audio_format, repo: "hexpm", optional: false]}, {:mockery, "~> 2.1", [hex: :mockery, repo: "hexpm", optional: false]}, {:unifex, "~> 1.0", [hex: :unifex, repo: "hexpm", optional: false]}], "hexpm", "f5dafea43f84f8765955cadad9c5fcc35ce878b737da0420a45bb1b744958005"}, + "membrane_raw_audio_format": {:hex, :membrane_raw_audio_format, "0.9.0", "c404a6eb38600dd85ad69dcf974b3c82fe0ef07c92e602cd438763dcdaf3462d", [:mix], [{:bimap, "~> 1.1", [hex: :bimap, repo: "hexpm", optional: false]}, {:bunch, "~> 1.0", [hex: :bunch, repo: "hexpm", optional: false]}, {:membrane_core, "~> 0.10.0", [hex: :membrane_core, repo: "hexpm", optional: false]}], "hexpm", "7b346bd3be6bcc7ceb9fe28ca6a9dfd8b072e9f6960fe5766ea99582319c05df"}, + "membrane_wav_plugin": {:hex, :membrane_wav_plugin, "0.7.0", "052b521d36d28b36f4d8b79040d82c7f20b2dc717188d597343e6819e8f045e8", [:mix], [{:membrane_core, "~> 0.10.0", [hex: :membrane_core, repo: "hexpm", optional: false]}, {:membrane_file_plugin, "~> 0.12.0", [hex: :membrane_file_plugin, repo: "hexpm", optional: true]}, {:membrane_raw_audio_format, "~> 0.9.0", [hex: :membrane_raw_audio_format, repo: "hexpm", optional: false]}], "hexpm", "6d9adfa9209b61deda7d0efc680ee322df42d439380edd156e44bc3b606cc9d7"}, + "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, + "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"}, + "mix_test_watch": {:hex, :mix_test_watch, "1.1.0", "330bb91c8ed271fe408c42d07e0773340a7938d8a0d281d57a14243eae9dc8c3", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}], "hexpm", "52b6b1c476cbb70fd899ca5394506482f12e5f6b0d6acff9df95c7f1e0812ec3"}, + "mockery": {:hex, :mockery, "2.3.1", "a02fd60b10ac9ed37a7a2ecf6786c1f1dd5c75d2b079a60594b089fba32dc087", [:mix], [], "hexpm", "1d0971d88ebf084e962da3f2cfee16f0ea8e04ff73a7710428500d4500b947fa"}, "nimble_options": {:hex, :nimble_options, "0.4.0", "c89babbab52221a24b8d1ff9e7d838be70f0d871be823165c94dd3418eea728f", [:mix], [], "hexpm", "e6701c1af326a11eea9634a3b1c62b475339ace9456c1a23ec3bc9a847bca02d"}, + "numbers": {:hex, :numbers, "5.2.4", "f123d5bb7f6acc366f8f445e10a32bd403c8469bdbce8ce049e1f0972b607080", [:mix], [{:coerce, "~> 1.0", [hex: :coerce, repo: "hexpm", optional: false]}, {:decimal, "~> 1.9 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "eeccf5c61d5f4922198395bf87a465b6f980b8b862dd22d28198c5e6fab38582"}, + "parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"}, + "poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm", "fec8660eb7733ee4117b85f55799fd3833eb769a6df71ccf8903e8dc5447cfce"}, "qex": {:hex, :qex, "0.5.1", "0d82c0f008551d24fffb99d97f8299afcb8ea9cf99582b770bd004ed5af63fd6", [:mix], [], "hexpm", "935a39fdaf2445834b95951456559e9dc2063d0a055742c558a99987b38d6bab"}, + "ratio": {:hex, :ratio, "2.4.2", "c8518f3536d49b1b00d88dd20d49f8b11abb7819638093314a6348139f14f9f9", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}, {:numbers, "~> 5.2.0", [hex: :numbers, repo: "hexpm", optional: false]}], "hexpm", "441ef6f73172a3503de65ccf1769030997b0d533b1039422f1e5e0e0b4cbf89e"}, "scenic": {:hex, :scenic, "0.11.1", "9cabc40a1362de76b25c9b243503251c9e9287816fab88b539c55e8debdb6513", [:make, :mix], [{:elixir_make, "~> 0.6.2", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:ex_image_info, "~> 0.2.4", [hex: :ex_image_info, repo: "hexpm", optional: false]}, {:font_metrics, "~> 0.5.0", [hex: :font_metrics, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.3.4 or ~> 0.4.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:truetype_metrics, "~> 0.6", [hex: :truetype_metrics, repo: "hexpm", optional: false]}], "hexpm", "86845290002bb61ac34ab24573dfd0f15f1d17bb24fa68767af65601eaae5153"}, "scenic_driver_local": {:hex, :scenic_driver_local, "0.11.0", "c26f7665c3d4aa634a0f8873bd958cb3bfcc99cb96e2381422de3e78d244357c", [:make, :mix], [{:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:input_event, "~> 0.4 or ~> 1.0", [hex: :input_event, repo: "hexpm", optional: false]}, {:scenic, "~> 0.11.0", [hex: :scenic, repo: "hexpm", optional: false]}], "hexpm", "77b27b82a8fe41d5fade5c88cf413af098d3f3d56717c988097e7902ab9b9d03"}, + "secure_random": {:hex, :secure_random, "0.5.1", "c5532b37c89d175c328f5196a0c2a5680b15ebce3e654da37129a9fe40ebf51b", [:mix], [], "hexpm", "1b9754f15e3940a143baafd19da12293f100044df69ea12db5d72878312ae6ab"}, + "shmex": {:hex, :shmex, "0.5.0", "7dc4fb1a8bd851085a652605d690bdd070628717864b442f53d3447326bcd3e8", [:mix], [{:bunch_native, "~> 0.5.0", [hex: :bunch_native, repo: "hexpm", optional: false]}, {:bundlex, "~> 1.0", [hex: :bundlex, repo: "hexpm", optional: false]}], "hexpm", "b67bb1e22734758397c84458dbb746519e28eac210423c267c7248e59fc97bdc"}, + "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"}, + "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"}, "truetype_metrics": {:hex, :truetype_metrics, "0.6.1", "9119a04dc269dd8f63e85e12e4098f711cb7c5204a420f4896f40667b9e064f6", [:mix], [{:font_metrics, "~> 0.5", [hex: :font_metrics, repo: "hexpm", optional: false]}], "hexpm", "5711d4a3e4fc92eb073326fbe54208925d35168dc9b288c331ee666a8a84759b"}, "typed_struct": {:hex, :typed_struct, "0.3.0", "939789e3c1dca39d7170c87f729127469d1315dcf99fee8e152bb774b17e7ff7", [:mix], [], "hexpm", "c50bd5c3a61fe4e198a8504f939be3d3c85903b382bde4865579bc23111d1b6d"}, + "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, + "unifex": {:hex, :unifex, "1.1.0", "26b1bcb6c3b3454e1ea15f85b2e570aaa5b5c609566aa9f5c2e0a8b213379d6b", [:mix], [{:bunch, "~> 1.0", [hex: :bunch, repo: "hexpm", optional: false]}, {:bundlex, "~> 1.0", [hex: :bundlex, repo: "hexpm", optional: false]}, {:shmex, "~> 0.5.0", [hex: :shmex, repo: "hexpm", optional: false]}], "hexpm", "d8f47e9e3240301f5b20eec5792d1d4341e1a3a268d94f7204703b48da4aaa06"}, } diff --git a/snake-ecs/modules/make/boilerplate.mk b/snake-ecs/modules/make/boilerplate.mk new file mode 100644 index 0000000..be355d6 --- /dev/null +++ b/snake-ecs/modules/make/boilerplate.mk @@ -0,0 +1,63 @@ +# This boilerplate provides: +# - sound default settings for make +# - usefull variables: +# - root: the root of your project +# - modules: path to the module folder +# - makefile_path: path to the makefile being executed +# - source: source the local env.sh if any +# - the help target being run by default +# - help generation provided by pretty-make (https://github.com/Awea/pretty-make) + +# Use one shell for the whole recipe, instead of per-line +.ONESHELL: +# Use bash in strict mode +SHELL := bash +.SHELLFLAGS = -eu -o pipefail -c + +# Delete target on error +# If the recipe fails, make will delete the non-phony target. +# +# This avoid case were the target is being created in the middle of a run and +# thus make will think everything is OK on the next run. +.DELETE_ON_ERROR: + +# Sane makefile settings to avoid the unexpected +MAKEFLAGS += --warn-undefined-variables +MAKEFLAGS += --no-builtin-rules + +# Absolute path to the project root +root := $(shell git rev-parse --show-toplevel) +# We give access to this to every targets +export root + +# Absolute path to modules +modules := $(root)/modules +export modules + +# Absolute path to the executed Makefile, and its directory +# NOT the makefile we are in +makefile_path := $(abspath $(firstword $(MAKEFILE_LIST))) +makefile_dir := $(dir makefile_path) + +# Provide a command to source the env.sh file if any +source ?= source env.sh || true + +.PHONY: hide-secret +hide-secret: + @echo "๐Ÿ” Hide any known unencrypted secret" + git-secret hide -mdF 2>/dev/null + +# Everything needed to generate helps +.DEFAULT_GOAL := help + +.PHONY: help +## List available commands +help: $(root)/bin/pretty-make + @"$<" pretty-help "$(makefile_path)" + +$(root)/bin: + @mkdir -p $@ + +$(root)/bin/pretty-make: + @cd $(root) + curl -Ls https://raw.githubusercontent.com/awea/pretty-make/master/scripts/install.sh | bash -sx diff --git a/snake-ecs/modules/make/elixir-build.mk b/snake-ecs/modules/make/elixir-build.mk new file mode 100644 index 0000000..a0cf8bb --- /dev/null +++ b/snake-ecs/modules/make/elixir-build.mk @@ -0,0 +1,190 @@ +# Our app details +APP_NAME ?= $(shell grep 'app:' mix.exs | sed -e 's/\[//g' -e 's/ //g' -e 's/app://' -e 's/[:,]//g') +APP_VSN ?= $(shell grep 'version:' mix.exs | cut -d '"' -f2 ) +BUILD ?= $(shell git rev-parse --short HEAD ) +MIX_ENV ?= prod + +EX_FILE = $(shell find . -name '*.ex') +SLIME_FILE = $(shell find . -name '*.slime') +POT_FILE = $(shell find . -name '*.pot') +PO_FILE = $(shell find . -name '*.po') + +node-deps: assets/node_modules +assets/node_modules: assets/package.json assets/yarn.lock deps + @echo "๐Ÿ“ฆ Install Javascript dependencies" + ${source} + yarn install --cwd $(shell dirname $@) --frozen-lockfile + touch $@ + +deps: mix.exs mix.lock + @echo "๐Ÿ“ฆ Install Elixir dependencies" + ${source} + @mix deps.get + @touch $@ + +#.PHONY: build +#build: _build +_build: deps $(EX_FILE) $(SLIME_FILE) + @echo "๐Ÿ”จ Build Project Saturn" + ${source} + mix compile + touch $@ + +# This is only called on demand to avoid having dirty data in the POT & PO +.PHONY: pot +pot: $(POT_FILE) +$(POT_FILE) &: $(EX_FILE) + ${source} + mix gettext.extract + +.PHONY: po +po: $(PO_FILE) +$(PO_FILE) &: $(POT_FILE) + ${source} + mix gettext.merge priv/gettext + +.PHONY: s serve +s: server +serve: server + +.DEFAULT_GOAL := serve +## Serve site at http://localhost:3000 with live reloading +.PHONY: server +server: _build uploads + @echo "๐Ÿ Start the server" + ${source} + mix phx.server + +.PHONY: si +si: server-i + +.PHONY: server-i +## โœจ Serve site at http://localhost:3000 with live reloading +## in interactive mode โœจ +server-i: _build uploads + @echo "๐Ÿ Start the server in interactive mode โœจ" + ${source} + iex -S mix phx.server + +uploads: + @echo "๐Ÿ“ Create the required uploads folder" + ${source} + @mkdir -p $@ + + +.PHONY: test +## Run the tests +test: _build + @echo "๐Ÿงช Run the tests" + ${source} + PROJECT_SATURN_UPLOAD_DIR=/tmp + mix test + +.PHONY: test-watch t +t: test-watch +## Run the tests on file change +test-watch: _build + @echo "๐Ÿงช Run the tests" + ${source} + PROJECT_SATURN_UPLOAD_DIR=/tmp + mix test.watch + + +## Create a release of the project +RELEASE_ARCHIVE = "_build/$(MIX_ENV)/rel/$(APP_NAME)/releases/$(APP_VSN)/$(APP_NAME).tar.gz" +RELEASE_PATH = "_build/$(MIX_ENV)/rel/$(APP_NAME)/releases/$(APP_VSN)/" + +.PHONY: release +release: $(RELEASE_PATH) +$(RELEASE_PATH): rel _build config/config.exs config/runtime.exs config/prod.exs + @echo "๐Ÿ“ฆ Create a project release" + MIX_ENV=prod mix release --path $(RELEASE_PATH) + touch $@ + +.PHONY: run-release +run-release: $(RELEASE_PATH) + @echo "๐Ÿ Run Project Saturn" + ${source} + #_build/prod/rel/project_saturn/bin/project_saturn foreground + $^/bin/project_saturn start + +.PHONY: clean +## Clean all the artifacts: assets/node_modules, deps, _build, etc. +clean: + @echo "๐Ÿ—‘ Delete artifacts" + @rm -rf deps + @rm -rf _build + @rm -rf assets/node_modules + +.PHONY: install-deps +## Install dependencies +install-deps: assets/node_modules deps + +# Docker Section +# -------------- +# +# This is dedicated to target that are docker related + +.PHONY: docker-build +## Build the Docker image +TAG = $(APP_VSN)-$(BUILD) +BUILDER_IMG = $(APP_NAME)-builder +TESTER_IMG = $(APP_NAME)-tester + +docker-build: + @echo "๐Ÿณ Build the docker image" + ${source} + docker build \ + --rm=false \ + --build-arg APP_NAME=$(APP_NAME) --build-arg APP_VSN=$(APP_VSN) \ + -t $(BUILDER_IMG):$(TAG) \ + -t $(BUILDER_IMG):latest \ + --target builder . + +.PHONY: docker-test +docker-test: #docker-build + @echo "๐Ÿณ Test the docker image" + ${source} + #docker build \ + # --build-arg APP_NAME=$(APP_NAME) --build-arg APP_VSN=$(APP_VSN) \ + # -t $(TESTER_IMG):$(TAG) \ + # -t $(TESTER_IMG):latest \ + # --target tester . + docker run -e MIX_ENV=test $(BUILDER_IMG) make test + +.PHONY: docker-release +docker-release: + @echo "๐Ÿณ Create a production docker image" + ${source} + docker build \ + --build-arg APP_NAME=$(APP_NAME) --build-arg APP_VSN=$(APP_VSN) \ + -t $(APP_NAME):$(APP_VSN)-$(BUILD) \ + -t $(APP_NAME):latest \ + --target production . + +.PHONY: docker-serve +## Run the app in Docker +docker-serve: # docker-release + @echo "๐Ÿณ Run Project Saturn in docker" + ${source} + docker run \ + -e BASIC_AUTH_USERNAME="$${BASIC_AUTH_USERNAME}" \ + -e BASIC_AUTH_PASSWORD="$${BASIC_AUTH_PASSWORD}" \ + -e REPLACE_HOST_VARS="$${REPLACE_HOST_VARS}" \ + -e ERLANG_COOKIE="$${ERLANG_COOKIE}" \ + -e NODE_NAME="$${NODE_NAME}" \ + -e PORT="$${PORT}" \ + -e URL_PORT="$${URL_PORT}" \ + -e URL_SCHEME="$${URL_SCHEME}" \ + -e URL_HOST="$${URL_HOST}" \ + -e SECRET_KEY_BASE="$${SECRET_KEY_BASE}"\ + -e MAILGUN_BASE_URI="$${MAILGUN_BASE_URI}" \ + -e MAILGUN_API_KEY="$${MAILGUN_API_KEY}" \ + -e MAILGUN_DOMAIN="$${MAILGUN_DOMAIN}" \ + -e PROJECT_SATURN_EMAIL_SITE_NAME="$${PROJECT_SATURN_EMAIL_SITE_NAME}" \ + -e PROJECT_SATURN_EMAIL_FROM_NAME="$${PROJECT_SATURN_EMAIL_FROM_NAME}" \ + -e PROJECT_SATURN_EMAIL_FROM="$${PROJECT_SATURN_EMAIL_FROM}" \ + -e PROJECT_SATURN_UPLOAD_DIR="/opt/app/uploads" \ + -v $${PROJECT_SATURN_UPLOAD_DIR}:/opt/app/uploads \ + --expose $${PORT} -p $${PORT}:$${PORT} \ + --rm -it $(APP_NAME):latest diff --git a/snake-ecs/test/engine/action_test.exs b/snake-ecs/test/engine/action_test.exs new file mode 100644 index 0000000..8a56b68 --- /dev/null +++ b/snake-ecs/test/engine/action_test.exs @@ -0,0 +1,17 @@ +defmodule Engine.ActionTest do + use ExUnit.Case, async: true + + alias Engine.Action + + test "#{__MODULE__}.create: should work" do + assert %Action{} = Action.create(:a_type, System.pid(), %{pay: "load"}) + end + + test "#{__MODULE__}.execute: should work" do + {:ok, ent} = Engine.Entity.create(:Goblin) + action = Action.create(:attack, ent, %{hp: -10_000}) + assert :ok = Action.execute(action) + # TODO: how to check for the goblin action queue? + # assert %Entity.Data{} + end +end diff --git a/snake-ecs/test/engine/entity/data_test.exs b/snake-ecs/test/engine/entity/data_test.exs new file mode 100644 index 0000000..a54f384 --- /dev/null +++ b/snake-ecs/test/engine/entity/data_test.exs @@ -0,0 +1,13 @@ +defmodule Engine.Entity.DataTest do + use ExUnit.Case, async: true + + alias Engine.Entity.Data + + test "validate Entity default data" do + data = %Data{} + + assert data.world_name == :global + assert data.components == %{} + assert data.action_queue == nil + end +end diff --git a/snake-ecs/test/engine/entity/store_test.exs b/snake-ecs/test/engine/entity/store_test.exs new file mode 100644 index 0000000..8bd7324 --- /dev/null +++ b/snake-ecs/test/engine/entity/store_test.exs @@ -0,0 +1,51 @@ +defmodule Engine.Entity.StoreTest do + use ExUnit.Case, async: true + + alias Engine.Entity + alias Entity.Store + + test "it should be possible to add entity to a group" do + group = :group1 + world = :world + {:ok, entity} = Entity.create(:Goblin) + + :ok = Store.add_to_group(group, world, entity) + + assert [entity] == :pg.get_members(Module.concat(world, group)) + + Entity.destroy(entity) + end + + test "it should be possible to remove an entity from a group" do + group = :group2 + world = :world + {:ok, entity} = Entity.create(:Goblin) + + :ok = Store.add_to_group(group, world, entity) + :ok = Store.remove_from_group(group, world, entity) + + assert [] == :pg.get_members(Module.concat(world, group)) + end + + test "it should be possible to get an entity matching a group" do + group = :group3 + group2 = :group4 + world = :default + {:ok, entity} = Entity.create(:Goblin) + + :ok = Store.add_to_group(group, world, entity) + :ok = Store.add_to_group(group2, world, entity) + + assert [entity] == Store.get_with(group, world) + assert [entity] == Store.get_with([group], world) + + # Matching a non existing group should return empty + assert [] == Store.get_with(:group5, world) + + # Matching a list of group, both exist for this entity + assert [entity] == Store.get_with([group, group2], world) + + # Matching a list of group, of which only one match the entity in the store + assert [] == Store.get_with([group2, :group5], world) + end +end diff --git a/snake-ecs/test/engine/entity_test.exs b/snake-ecs/test/engine/entity_test.exs new file mode 100644 index 0000000..7513fef --- /dev/null +++ b/snake-ecs/test/engine/entity_test.exs @@ -0,0 +1,46 @@ +defmodule Engine.EntityTest do + use ExUnit.Case, async: true + + alias Engine.Entity + alias Engine.Action + alias Engine.ComponentTypes + + test "it should be possible to create and destroy an entity without error" do + assert {:ok, ent} = Entity.create(:Goblin) + + assert :ok = Entity.destroy(ent) + end + + test "it should be possible to pop an action from the entity" do + {:ok, ent} = Entity.create(:Goblin) + + # Without action, we should get :empty + assert :empty = Entity.pop_action(ent) + + # Add an action + action = Action.create(:attack, ent, %{hp: -10_000}) + assert action = Entity.pop_action(ent) + + # Now it should be empty again + assert :empty = Entity.pop_action(ent) + + Entity.destroy(ent) + end + + test "it should be possible to get the componant of an entity" do + {:ok, ent} = Entity.create(:Goblin) + + assert %ComponentTypes.Stats{hp: _} = Entity.get_component(ent, ComponentTypes.Stats) + + Entity.destroy(ent) + end + + test "it should be possible to set the data of a component throught the entity" do + {:ok, ent} = Entity.create(:Goblin) + + assert :ok = Entity.set_component_data(ent, ComponentTypes.Stats, :hp, 100) + assert %ComponentTypes.Stats{hp: 100} = Entity.get_component(ent, ComponentTypes.Stats) + + Entity.destroy(ent) + end +end diff --git a/snake-ecs/test/engine/util/module_test.exs b/snake-ecs/test/engine/util/module_test.exs new file mode 100644 index 0000000..57d85e8 --- /dev/null +++ b/snake-ecs/test/engine/util/module_test.exs @@ -0,0 +1,17 @@ +defmodule Engine.Util.ModuleTest do + use ExUnit.Case, async: true + + alias Engine.Util.Module + + test "get_entity_types should return the entities defined in the code" do + assert [Engine.EntityTypes.Goblin] == Module.get_entity_types() + end + + test "get_component_types should return the components defined in the code" do + assert [Engine.ComponentTypes.Stats] == Module.get_component_types() + end + + test "get_system_types should return the systems definde in the code" do + assert [] == Module.get_system_types() + end +end diff --git a/snake-ecs/test/helpers/components/stats.exs b/snake-ecs/test/helpers/components/stats.exs new file mode 100644 index 0000000..3477f95 --- /dev/null +++ b/snake-ecs/test/helpers/components/stats.exs @@ -0,0 +1,5 @@ +use Engine.DSL.Component + +defcomponent Stats do + member :hp, 100 +end diff --git a/snake-ecs/test/helpers/entities/goblin.exs b/snake-ecs/test/helpers/entities/goblin.exs new file mode 100644 index 0000000..397b825 --- /dev/null +++ b/snake-ecs/test/helpers/entities/goblin.exs @@ -0,0 +1,7 @@ +use Engine.DSL.Entity + +defentity Goblin do + component Stats, %{ + hp: 50 + } +end diff --git a/snake-ecs/test/test_helper.exs b/snake-ecs/test/test_helper.exs new file mode 100644 index 0000000..f761b96 --- /dev/null +++ b/snake-ecs/test/test_helper.exs @@ -0,0 +1,2 @@ +ExUnit.configure(formatters: [ExUnit.CLIFormatter, ExUnitNotifier]) +ExUnit.start()