diff --git a/apps/amethyst/lib/amethyst.ex b/apps/amethyst/lib/amethyst.ex index bcd6314..bde95b6 100644 --- a/apps/amethyst/lib/amethyst.ex +++ b/apps/amethyst/lib/amethyst.ex @@ -26,6 +26,7 @@ defmodule Amethyst.Application do children = [ {Task.Supervisor, name: Amethyst.ConnectionSupervisor}, {Amethyst.Keys, 1024}, + {Amethyst.GameRegistry, []} ] children = case Application.fetch_env!(:amethyst, :port) do diff --git a/apps/amethyst/lib/api/game.ex b/apps/amethyst/lib/api/game.ex new file mode 100644 index 0000000..b2e1f59 --- /dev/null +++ b/apps/amethyst/lib/api/game.ex @@ -0,0 +1,30 @@ +defmodule Amethyst.API.Game do + @moduledoc """ + This module includes the interface for defining and registering + a game with Amethyst. + """ + @callback instantiate() :: {:ok, new_state :: term} | {:error, reason :: term} + + defmacro __using__(opts) do + meta = Keyword.get(opts, :meta, []) + quote do + @behaviour Amethyst.API.Game + def child_spec(state) do + %{ + id: __MODULE__, + start: {__MODULE__, :start_link, []} + } + end + def start_link() do + register_self() + _loop() + end + defp _loop() do + _loop() + end + def register_self() do + Amethyst.GameRegistry.register(__MODULE__, unquote(meta)) + end + end + end +end diff --git a/apps/amethyst/lib/apps/game_coordinator.ex b/apps/amethyst/lib/apps/game_coordinator.ex new file mode 100644 index 0000000..b71ee44 --- /dev/null +++ b/apps/amethyst/lib/apps/game_coordinator.ex @@ -0,0 +1,36 @@ +defmodule Amethyst.GameCoordinator do + use GenServer + + @moduledoc """ + The game coordinator is responsible for keeping track of active + instances of each game and can create new ones on demand. + """ + + @impl true + def init(initial) do + {:ok, initial} + end + + @impl true + def handle_call({:get, type}, _from, state) do + {:reply, find_or_create(type, state), state} + end + + defp create(mod) do + state = mod.initialize() + spawn(mod, :listen, [state]) + end + + defp find(type, games) do + existing = games |> Enum.filter(fn {mod, _pid, _opts} -> mod == type end) + [{_mod, pid, _} | _] = existing + pid + end + + defp find_or_create(type, games) do + case find(type, games) do + nil -> create(type) + some -> some + end + end +end diff --git a/apps/amethyst/lib/apps/game_registry.ex b/apps/amethyst/lib/apps/game_registry.ex new file mode 100644 index 0000000..c1465b6 --- /dev/null +++ b/apps/amethyst/lib/apps/game_registry.ex @@ -0,0 +1,53 @@ +defmodule Amethyst.GameRegistry do + use GenServer + + @moduledoc """ + The game registry stores information about which games are + available. + """ + + @impl true + def init(initial) do + {:ok, initial} + end + + @impl true + def handle_call({:register, module, meta}, _, state) do + if Keyword.get(meta, :default, false) && find_default(state) != nil do + {:reply, :error, :default_exists} + end + {:reply, :ok, [{module, meta} | state]} + end + + @impl true + def handle_call({:list}, _, state) do + {:reply, state, state} + end + + @impl true + def handle_call({:get_default}, _, state) do + {:reply, find_default(state), state} + end + + defp find_default(state) do + state |> Enum.filter(fn {_mod, meta} -> + Keyword.get(meta, :default, false) + end) |> List.first(nil) + end + + def start_link(initial) when is_list(initial) do + GenServer.start_link(__MODULE__, initial, name: {:global, __MODULE__}) + end + + def register(module, meta) do + GenServer.call({:global, __MODULE__}, {:register, module, meta}) + end + + def list() do + GenServer.call({:global, __MODULE__}, {:list}) + end + + def get_default() do + GenServer.call({:global, __MODULE__}, {:get_default}) + end +end diff --git a/apps/amethyst/lib/servers/configuration.ex b/apps/amethyst/lib/servers/configuration.ex index e9752c9..71f2db2 100644 --- a/apps/amethyst/lib/servers/configuration.ex +++ b/apps/amethyst/lib/servers/configuration.ex @@ -332,6 +332,11 @@ defmodule Amethyst.Server.Configuration do 0, false, ["minecraft:overworld"], 0, 16, 16, false, true, true, 0, "minecraft:overworld", <<0::64>>, :spectator, nil, false, true, nil, 0, false }, client) + game = Amethyst.GameRegistry.get_default() + if game == nil do + raise RuntimeError, "No game is set as a default! A single game must be defined as default for players to be able to join." + end + state = Keyword.put(state, :game, game) Amethyst.Server.Play.serve(client, state) end # Serverbound Plugin Message https://wiki.vg/Protocol#Serverbound_Plugin_Message_(configuration) diff --git a/apps/example_game/.credo.exs b/apps/example_game/.credo.exs new file mode 100644 index 0000000..060eb8c --- /dev/null +++ b/apps/example_game/.credo.exs @@ -0,0 +1,188 @@ +%{ + # + # You can have as many configs as you like in the `configs:` field. + configs: [ + %{ + # + # Run any config using `mix credo -C `. If no config name is given + # "default" is used. + # + name: "default", + # + # These are the files included in the analysis: + files: %{ + # + # You can give explicit globs or simply directories. + # In the latter case `**/*.{ex,exs}` will be used. + # + included: [ + "lib/", + "src/", + "test/", + ], + excluded: [~r"/_build/", ~r"/deps/"] + }, + plugins: [], + requires: [], + strict: false, + parse_timeout: 5000, + color: true, + # + # You can customize the parameters of any check by adding a second element + # to the tuple. + # + # To disable a check put `false` as second element: + # + # {Credo.Check.Design.DuplicatedCode, false} + # + checks: %{ + enabled: [ + # + ## Consistency Checks + # + {Credo.Check.Consistency.ExceptionNames, []}, + {Credo.Check.Consistency.LineEndings, []}, + {Credo.Check.Consistency.ParameterPatternMatching, []}, + {Credo.Check.Consistency.SpaceAroundOperators, []}, + {Credo.Check.Consistency.SpaceInParentheses, []}, + {Credo.Check.Consistency.TabsOrSpaces, []}, + + # + ## Design Checks + # + # You can customize the priority of any check + # Priority values are: `low, normal, high, higher` + # + {Credo.Check.Design.AliasUsage, + [priority: :low, if_nested_deeper_than: 2, if_called_more_often_than: 0]}, + {Credo.Check.Design.TagFIXME, []}, + # You can also customize the exit_status of each check. + # If you don't want TODO comments to cause `mix credo` to fail, just + # set this value to 0 (zero). + # + {Credo.Check.Design.TagTODO, [exit_status: 0]}, + + # + ## Readability Checks + # + {Credo.Check.Readability.AliasOrder, []}, + {Credo.Check.Readability.FunctionNames, []}, + {Credo.Check.Readability.LargeNumbers, []}, + {Credo.Check.Readability.MaxLineLength, [priority: :low, max_length: 120]}, + {Credo.Check.Readability.ModuleAttributeNames, []}, + {Credo.Check.Readability.ModuleDoc, []}, + {Credo.Check.Readability.ModuleNames, []}, + {Credo.Check.Readability.ParenthesesInCondition, []}, + {Credo.Check.Readability.ParenthesesOnZeroArityDefs, []}, + {Credo.Check.Readability.PipeIntoAnonymousFunctions, []}, + {Credo.Check.Readability.PredicateFunctionNames, []}, + {Credo.Check.Readability.PreferImplicitTry, []}, + {Credo.Check.Readability.RedundantBlankLines, []}, + {Credo.Check.Readability.Semicolons, []}, + {Credo.Check.Readability.SpaceAfterCommas, []}, + {Credo.Check.Readability.StringSigils, []}, + {Credo.Check.Readability.TrailingBlankLine, []}, + {Credo.Check.Readability.TrailingWhiteSpace, []}, + {Credo.Check.Readability.UnnecessaryAliasExpansion, []}, + {Credo.Check.Readability.VariableNames, []}, + {Credo.Check.Readability.WithSingleClause, []}, + + # + ## Refactoring Opportunities + # + {Credo.Check.Refactor.Apply, []}, + {Credo.Check.Refactor.CondStatements, []}, + {Credo.Check.Refactor.CyclomaticComplexity, []}, + {Credo.Check.Refactor.FilterCount, []}, + {Credo.Check.Refactor.FilterFilter, []}, + {Credo.Check.Refactor.FunctionArity, []}, + {Credo.Check.Refactor.LongQuoteBlocks, []}, + {Credo.Check.Refactor.MapJoin, []}, + {Credo.Check.Refactor.MatchInCondition, []}, + {Credo.Check.Refactor.NegatedConditionsInUnless, []}, + {Credo.Check.Refactor.NegatedConditionsWithElse, []}, + {Credo.Check.Refactor.Nesting, []}, + {Credo.Check.Refactor.RedundantWithClauseResult, []}, + {Credo.Check.Refactor.RejectReject, []}, + {Credo.Check.Refactor.UnlessWithElse, []}, + {Credo.Check.Refactor.WithClauses, []}, + + # + ## Warnings + # + {Credo.Check.Warning.ApplicationConfigInModuleAttribute, []}, + {Credo.Check.Warning.BoolOperationOnSameValues, []}, + {Credo.Check.Warning.Dbg, []}, + {Credo.Check.Warning.ExpensiveEmptyEnumCheck, []}, + {Credo.Check.Warning.IExPry, []}, + {Credo.Check.Warning.IoInspect, []}, + {Credo.Check.Warning.MissedMetadataKeyInLoggerConfig, []}, + {Credo.Check.Warning.OperationOnSameValues, []}, + {Credo.Check.Warning.OperationWithConstantResult, []}, + {Credo.Check.Warning.RaiseInsideRescue, []}, + {Credo.Check.Warning.SpecWithStruct, []}, + {Credo.Check.Warning.UnsafeExec, []}, + {Credo.Check.Warning.UnusedEnumOperation, []}, + {Credo.Check.Warning.UnusedFileOperation, []}, + {Credo.Check.Warning.UnusedKeywordOperation, []}, + {Credo.Check.Warning.UnusedListOperation, []}, + {Credo.Check.Warning.UnusedPathOperation, []}, + {Credo.Check.Warning.UnusedRegexOperation, []}, + {Credo.Check.Warning.UnusedStringOperation, []}, + {Credo.Check.Warning.UnusedTupleOperation, []}, + {Credo.Check.Warning.WrongTestFileExtension, []} + ], + disabled: [ + # + # Checks scheduled for next check update (opt-in for now) + {Credo.Check.Refactor.UtcNowTruncate, []}, + + # + # Controversial and experimental checks (opt-in, just move the check to `:enabled` + # and be sure to use `mix credo --strict` to see low priority checks) + # + {Credo.Check.Consistency.MultiAliasImportRequireUse, []}, + {Credo.Check.Consistency.UnusedVariableNames, []}, + {Credo.Check.Design.DuplicatedCode, []}, + {Credo.Check.Design.SkipTestWithoutComment, []}, + {Credo.Check.Readability.AliasAs, []}, + {Credo.Check.Readability.BlockPipe, []}, + {Credo.Check.Readability.ImplTrue, []}, + {Credo.Check.Readability.MultiAlias, []}, + {Credo.Check.Readability.NestedFunctionCalls, []}, + {Credo.Check.Readability.OneArityFunctionInPipe, []}, + {Credo.Check.Readability.OnePipePerLine, []}, + {Credo.Check.Readability.SeparateAliasRequire, []}, + {Credo.Check.Readability.SingleFunctionToBlockPipe, []}, + {Credo.Check.Readability.SinglePipe, []}, + {Credo.Check.Readability.Specs, []}, + {Credo.Check.Readability.StrictModuleLayout, []}, + {Credo.Check.Readability.WithCustomTaggedTuple, []}, + {Credo.Check.Refactor.ABCSize, []}, + {Credo.Check.Refactor.AppendSingleItem, []}, + {Credo.Check.Refactor.DoubleBooleanNegation, []}, + {Credo.Check.Refactor.FilterReject, []}, + {Credo.Check.Refactor.IoPuts, []}, + {Credo.Check.Refactor.MapMap, []}, + {Credo.Check.Refactor.ModuleDependencies, []}, + {Credo.Check.Refactor.NegatedIsNil, []}, + {Credo.Check.Refactor.PassAsyncInTestCases, []}, + {Credo.Check.Refactor.PipeChainStart, []}, + {Credo.Check.Refactor.RejectFilter, []}, + {Credo.Check.Refactor.VariableRebinding, []}, + {Credo.Check.Warning.LazyLogging, []}, + {Credo.Check.Warning.LeakyEnvironment, []}, + {Credo.Check.Warning.MapGetUnsafePass, []}, + {Credo.Check.Warning.MixEnv, []}, + {Credo.Check.Warning.UnsafeToAtom, []} + + # {Credo.Check.Refactor.MapInto, []}, + + # + # Custom checks can be created using `mix credo.gen.check`. + # + ] + } + } + ] +} diff --git a/apps/example_game/.formatter.exs b/apps/example_game/.formatter.exs new file mode 100644 index 0000000..d2cda26 --- /dev/null +++ b/apps/example_game/.formatter.exs @@ -0,0 +1,4 @@ +# Used by "mix format" +[ + inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] +] diff --git a/apps/example_game/.gitignore b/apps/example_game/.gitignore new file mode 100644 index 0000000..9dab323 --- /dev/null +++ b/apps/example_game/.gitignore @@ -0,0 +1,26 @@ +# The directory Mix will write compiled artifacts to. +/_build/ + +# If you run "mix test --cover", coverage assets end up here. +/cover/ + +# The directory Mix downloads your dependencies sources to. +/deps/ + +# Where third-party dependencies like ExDoc output generated docs. +/doc/ + +# Ignore .fetch files in case you like to edit your project deps locally. +/.fetch + +# If the VM crashes, it generates a dump, let's ignore it too. +erl_crash.dump + +# Also ignore archive artifacts (built via "mix archive.build"). +*.ez + +# Ignore package tarball (built via "mix hex.build"). +example_game-*.tar + +# Temporary files, for example, from tests. +/tmp/ diff --git a/apps/example_game/README.md b/apps/example_game/README.md new file mode 100644 index 0000000..86222e6 --- /dev/null +++ b/apps/example_game/README.md @@ -0,0 +1,3 @@ +# Example Game + +This module is a simple game which is made to aid in designing and testing Amethyst's APIs. diff --git a/apps/example_game/lib/example/application.ex b/apps/example_game/lib/example/application.ex new file mode 100644 index 0000000..c9eaf22 --- /dev/null +++ b/apps/example_game/lib/example/application.ex @@ -0,0 +1,19 @@ +defmodule Example.Application do + # See https://hexdocs.pm/elixir/Application.html + # for more information on OTP Applications + @moduledoc false + + use Application + + @impl true + def start(_type, _args) do + children = [ + {Example.Game, {}} + ] + + # See https://hexdocs.pm/elixir/Supervisor.html + # for other strategies and supported options + opts = [strategy: :one_for_one, name: Example.Supervisor] + Supervisor.start_link(children, opts) + end +end diff --git a/apps/example_game/lib/example/game.ex b/apps/example_game/lib/example/game.ex new file mode 100644 index 0000000..0ead295 --- /dev/null +++ b/apps/example_game/lib/example/game.ex @@ -0,0 +1,8 @@ +defmodule Example.Game do + use Amethyst.API.Game, meta: [default: true] + + @impl true + def instantiate() do + {:ok, [:hello]} + end +end diff --git a/apps/example_game/mix.exs b/apps/example_game/mix.exs new file mode 100644 index 0000000..e6bb9eb --- /dev/null +++ b/apps/example_game/mix.exs @@ -0,0 +1,32 @@ +defmodule Example.MixProject do + use Mix.Project + + def project do + [ + app: :example_game, + version: "0.1.0", + build_path: "../../_build", + config_path: "../../config/config.exs", + deps_path: "../../deps", + lockfile: "../../mix.lock", + elixir: "~> 1.17", + start_permanent: Mix.env() == :prod, + deps: deps() + ] + end + + # Run "mix help compile.app" to learn about applications. + def application do + [ + extra_applications: [:logger], + mod: {Example.Application, []} + ] + end + + # Run "mix help deps" to learn about dependencies. + defp deps do + [ + {:amethyst, in_umbrella: true} + ] + end +end diff --git a/apps/example_game/test/test_helper.exs b/apps/example_game/test/test_helper.exs new file mode 100644 index 0000000..869559e --- /dev/null +++ b/apps/example_game/test/test_helper.exs @@ -0,0 +1 @@ +ExUnit.start()