Compare commits
No commits in common. "4a5ccc719d935e0e3c961ce0d45f5ba99944adb5" and "dc9a2f2b5fc6c632731b82233e5ef7361748d61d" have entirely different histories.
4a5ccc719d
...
dc9a2f2b5f
@ -19,62 +19,31 @@ defmodule Amethyst.API.Game do
|
||||
This module includes the interface for defining and registering
|
||||
a game with Amethyst.
|
||||
"""
|
||||
|
||||
@doc """
|
||||
`instantiate/1` is called when a new instance of your game is created. You may start any additional
|
||||
processes you want under the `DynamicSupervisor` 'supervisor' and return a map with the PIDs to those
|
||||
processes, known as your references. Other callbacks in your game will receive these references and
|
||||
may use them to access and update state.
|
||||
"""
|
||||
@callback instantiate(supervisor :: pid()) :: {:ok, state_refs :: map()} | {:error, reason :: term}
|
||||
@doc """
|
||||
`login/3` is called when a new player logs into a game instance.
|
||||
You may either :accept or :reject the player for whatever reason, avoid
|
||||
rejecting the player in your default game as that will disconnect the player.
|
||||
The PID received in 'from' will be the one that calls all callbacks
|
||||
caused directly by this player, such as their movement or interactions --
|
||||
You can expect that PID to never be used by any other player and that this same
|
||||
player will not use another PID until they disconnect from the server.
|
||||
|
||||
- 'from' is the PID of the player's connection process.
|
||||
- 'player_cfg' is a keyword list containing the configuration passed by the game client
|
||||
- 'state_refs' are your references (see `instantiate/1`)
|
||||
"""
|
||||
@callback login(from :: pid(), player_cfg :: keyword(), state_refs :: map()) :: :accept | :reject
|
||||
@doc """
|
||||
`player_position/3` is called when a player moves. This function is called with the absolute coordinates
|
||||
that the player client expects. TODO: Teleport Player API.
|
||||
|
||||
- 'from' is the PID of the player's connection process (see `login/3`).
|
||||
- 'x', 'y' and 'z' are the absolute coordinates that the player wants to move to.
|
||||
- `state_refs` are your references (see `instantiate/1`)
|
||||
"""
|
||||
@callback login(from :: pid(), player_cfg :: keyword(), state_refs :: map()) :: :ok
|
||||
@callback player_position(from :: pid(), {x :: float(), y :: float(), z :: float()}, state_refs :: map()) :: :ok
|
||||
@doc """
|
||||
Whether or not this game instance can be joined by a new player. This should include basic logic such as
|
||||
if joining makes sense, for instance
|
||||
"""
|
||||
@callback joinable?(state_refs :: map()) :: boolean()
|
||||
|
||||
def child_spec(mod, refs) do
|
||||
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, [mod, refs]}
|
||||
start: {__MODULE__, :register_self, []}
|
||||
}
|
||||
end
|
||||
def register_self() do
|
||||
Amethyst.GameRegistry.register(__MODULE__, unquote(meta))
|
||||
Process.sleep(:infinity)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@spec start(Amethyst.API.Game, term()) :: {:ok, pid}
|
||||
@spec start(Amethyst.API.Game, term()) :: no_return()
|
||||
def start(mod, refs) do
|
||||
pid = spawn(fn ->
|
||||
true = refs |> Enum.all?(fn {_key, pid } -> Process.link(pid) end)
|
||||
loop(mod, refs)
|
||||
end)
|
||||
{:ok, pid}
|
||||
end
|
||||
def start_link(mod, refs) do
|
||||
{:ok, pid} = start(mod, refs)
|
||||
Process.link(pid)
|
||||
{:ok, pid}
|
||||
end
|
||||
defp loop(mod, refs) do
|
||||
receive do
|
||||
|
@ -22,59 +22,37 @@ defmodule Amethyst.GameCoordinator do
|
||||
instances of each game and can create new ones on demand.
|
||||
"""
|
||||
|
||||
defmodule State do
|
||||
@moduledoc """
|
||||
This struct represents the state tracked by a Amethyst.GameCoordinator.State
|
||||
|
||||
It contains two fields: `gid` which represents the latest gid (game id) and must be incremented every
|
||||
time a game is created and `games` which is a map linking each gid to a Amethyst.GameCoordinator.Game
|
||||
"""
|
||||
alias Amethyst.GameCoordinator.Game
|
||||
defstruct gid: 0, games: %{0 => Game}
|
||||
end
|
||||
defmodule Game do
|
||||
@moduledoc """
|
||||
This module represents an individual game instance that is currently active. Each game has a module
|
||||
which defines callbacks for various events, a map containing pids to the game's state (called references
|
||||
or refs) and optional metadata.
|
||||
"""
|
||||
defstruct mod: :none, refs: %{}, opts: []
|
||||
end
|
||||
|
||||
@spec start_link(State) :: {:ok, pid()}
|
||||
def start_link(initial) do
|
||||
def start_link(initial) when is_list(initial) do
|
||||
GenServer.start_link(__MODULE__, initial, name: {:global, __MODULE__})
|
||||
end
|
||||
|
||||
@impl true
|
||||
@spec init(State) :: {:ok, State}
|
||||
def init(initial) do
|
||||
{:ok, initial}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call({:find, type}, _from, state) do
|
||||
{game, state} = _find(type, state)
|
||||
{:reply, game, state}
|
||||
{pid, state} = _find(type, state)
|
||||
{:reply, pid, state}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call({:create, type}, _from, state) do
|
||||
{game, state} = _create(type, state)
|
||||
{:reply, game, state}
|
||||
{pid, state} = _create(type, state)
|
||||
{:reply, pid, state}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call({:find_or_create, type}, from, state) do
|
||||
{game, state} = _find(type, state)
|
||||
case game do
|
||||
{pid, state} = _find(type, state)
|
||||
case pid do
|
||||
nil -> handle_call({:create, type}, from, state)
|
||||
some -> {:reply, some, state}
|
||||
end
|
||||
end
|
||||
|
||||
@spec _create(atom(), State.t()) :: {Game.t(), State.t()}
|
||||
defp _create(type, state) do
|
||||
defp _create(type, games) do
|
||||
# Create a DynamicSupervisor for this game
|
||||
{:ok, game_supervisor_pid} = DynamicSupervisor.start_child(
|
||||
{:via, PartitionSupervisor, {Amethyst.GameMetaSupervisor, type}},
|
||||
@ -88,19 +66,20 @@ defmodule Amethyst.GameCoordinator do
|
||||
# We should gracefully handle situations where we cannot create a game.
|
||||
{:ok, refs} = type.instantiate(game_supervisor_pid)
|
||||
refs = refs |> Map.put(:game_supervisor, game_supervisor_pid) |> Map.put(:task_supervisor, task_supervisor_pid)
|
||||
game = %Game{
|
||||
mod: type, refs: refs, opts: []
|
||||
}
|
||||
games = state.games |> Map.put(state.gid, game)
|
||||
{game, %State{gid: state.gid + 1, games: games}}
|
||||
pid = spawn(Amethyst.API.Game, :start, [type, refs])
|
||||
games = [{type, pid, []} | games]
|
||||
{pid, games}
|
||||
end
|
||||
|
||||
defp _find(type, state) do
|
||||
defp _find(type, games) do
|
||||
alive_games = games |> Enum.filter(fn {_mod, pid, _opts} -> Process.alive?(pid) end)
|
||||
# TODO: Here we should have some additional filtering for specifically joinable games
|
||||
existing = alive_games |> Enum.filter(fn {mod, _pid, _opts} -> mod == type end)
|
||||
if length(existing) > 0 do
|
||||
[{_mod, pid, _} | _] = existing
|
||||
{pid, games}
|
||||
{pid, alive_games}
|
||||
else
|
||||
{nil, games}
|
||||
{nil, alive_games}
|
||||
end
|
||||
end
|
||||
|
||||
|
70
apps/amethyst/lib/apps/game_registry.ex
Normal file
70
apps/amethyst/lib/apps/game_registry.ex
Normal file
@ -0,0 +1,70 @@
|
||||
# Amethyst - An experimental Minecraft server written in Elixir.
|
||||
# Copyright (C) 2024 KodiCraft
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
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}, state}
|
||||
else
|
||||
{:reply, :ok, [{module, meta} | state]}
|
||||
end
|
||||
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
|
@ -327,7 +327,7 @@ defmodule Amethyst.Server.Configuration do
|
||||
end
|
||||
# Acknowledge Finish Configuration https://wiki.vg/Protocol#Acknowledge_Finish_Configuration
|
||||
def handle({:acknowledge_finish_configuration}, client, state) do
|
||||
game = Application.fetch_env!(:amethyst, :default_game) |> Amethyst.GameCoordinator.find_or_create()
|
||||
game = Amethyst.GameRegistry.get_default() |> elem(0) |> Amethyst.GameCoordinator.find_or_create()
|
||||
state = Keyword.put(state, :game, game)
|
||||
Amethyst.API.Game.login(game, state)
|
||||
# TODO: All of this stuff should obviously not be hardcoded here
|
||||
|
@ -20,7 +20,7 @@ defmodule Amethyst.MixProject do
|
||||
source_url: "https://git.colon-three.com/kodi/amethyst",
|
||||
docs: [
|
||||
main: "readme",
|
||||
extras: ["../../README.md", "../../LICENSE.md"]
|
||||
extras: ["../README.md", "../LICENSE.md"]
|
||||
]
|
||||
]
|
||||
end
|
||||
|
@ -8,6 +8,7 @@ defmodule Example.Application do
|
||||
@impl true
|
||||
def start(_type, _args) do
|
||||
children = [
|
||||
{Example.Game, {}}
|
||||
]
|
||||
|
||||
# See https://hexdocs.pm/elixir/Supervisor.html
|
||||
|
@ -1,6 +1,6 @@
|
||||
defmodule Example.Game do
|
||||
require Logger
|
||||
@behaviour Amethyst.API.Game
|
||||
use Amethyst.API.Game, meta: [default: true]
|
||||
|
||||
@impl true
|
||||
def instantiate(supervisor) do
|
||||
@ -9,9 +9,8 @@ defmodule Example.Game do
|
||||
end
|
||||
|
||||
@impl true
|
||||
def login(from, cfg, refs) do
|
||||
def login(from, cfg, _state) do
|
||||
Logger.info("Player logged in from #{inspect(from)}: #{inspect(cfg)}")
|
||||
Logger.info("The refs for this game are #{inspect(refs)}")
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
@ -3,5 +3,4 @@ import Config
|
||||
config :amethyst,
|
||||
port: 25599, # Bogus port for testing, avoids unexpected conflicts
|
||||
encryption: false, # Whether or not to request encryption from clients.
|
||||
auth: false, # Whether or not users should be authenticated with Mojang.
|
||||
default_game: Example.Game # Which game new players should be sent to
|
||||
auth: false # Whether or not users should be authenticated with Mojang.
|
||||
|
Loading…
Reference in New Issue
Block a user