Begin implementing GameCoordinator

This commit is contained in:
Kodi Craft 2024-08-17 19:43:08 +02:00
parent 4e629fb8e4
commit 4a56f9b954
Signed by: kodi
GPG Key ID: 69D9EED60B242822
13 changed files with 406 additions and 0 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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 <name>`. 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`.
#
]
}
}
]
}

View File

@ -0,0 +1,4 @@
# Used by "mix format"
[
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
]

26
apps/example_game/.gitignore vendored Normal file
View File

@ -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/

View File

@ -0,0 +1,3 @@
# Example Game
This module is a simple game which is made to aid in designing and testing Amethyst's APIs.

View File

@ -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

View File

@ -0,0 +1,8 @@
defmodule Example.Game do
use Amethyst.API.Game, meta: [default: true]
@impl true
def instantiate() do
{:ok, [:hello]}
end
end

32
apps/example_game/mix.exs Normal file
View File

@ -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

View File

@ -0,0 +1 @@
ExUnit.start()