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
|
This module includes the interface for defining and registering
|
||||||
a game with Amethyst.
|
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}
|
@callback instantiate(supervisor :: pid()) :: {:ok, state_refs :: map()} | {:error, reason :: term}
|
||||||
@doc """
|
@callback login(from :: pid(), player_cfg :: keyword(), state_refs :: map()) :: :ok
|
||||||
`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 player_position(from :: pid(), {x :: float(), y :: float(), z :: float()}, 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__,
|
id: __MODULE__,
|
||||||
start: {__MODULE__, :start_link, [mod, refs]}
|
start: {__MODULE__, :register_self, []}
|
||||||
}
|
}
|
||||||
end
|
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
|
def start(mod, refs) do
|
||||||
pid = spawn(fn ->
|
|
||||||
true = refs |> Enum.all?(fn {_key, pid } -> Process.link(pid) end)
|
true = refs |> Enum.all?(fn {_key, pid } -> Process.link(pid) end)
|
||||||
loop(mod, refs)
|
loop(mod, refs)
|
||||||
end)
|
|
||||||
{:ok, pid}
|
|
||||||
end
|
|
||||||
def start_link(mod, refs) do
|
|
||||||
{:ok, pid} = start(mod, refs)
|
|
||||||
Process.link(pid)
|
|
||||||
{:ok, pid}
|
|
||||||
end
|
end
|
||||||
defp loop(mod, refs) do
|
defp loop(mod, refs) do
|
||||||
receive do
|
receive do
|
||||||
|
@ -22,59 +22,37 @@ defmodule Amethyst.GameCoordinator do
|
|||||||
instances of each game and can create new ones on demand.
|
instances of each game and can create new ones on demand.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
defmodule State do
|
def start_link(initial) when is_list(initial) 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
|
|
||||||
GenServer.start_link(__MODULE__, initial, name: {:global, __MODULE__})
|
GenServer.start_link(__MODULE__, initial, name: {:global, __MODULE__})
|
||||||
end
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
@spec init(State) :: {:ok, State}
|
|
||||||
def init(initial) do
|
def init(initial) do
|
||||||
{:ok, initial}
|
{:ok, initial}
|
||||||
end
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def handle_call({:find, type}, _from, state) do
|
def handle_call({:find, type}, _from, state) do
|
||||||
{game, state} = _find(type, state)
|
{pid, state} = _find(type, state)
|
||||||
{:reply, game, state}
|
{:reply, pid, state}
|
||||||
end
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def handle_call({:create, type}, _from, state) do
|
def handle_call({:create, type}, _from, state) do
|
||||||
{game, state} = _create(type, state)
|
{pid, state} = _create(type, state)
|
||||||
{:reply, game, state}
|
{:reply, pid, state}
|
||||||
end
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def handle_call({:find_or_create, type}, from, state) do
|
def handle_call({:find_or_create, type}, from, state) do
|
||||||
{game, state} = _find(type, state)
|
{pid, state} = _find(type, state)
|
||||||
case game do
|
case pid do
|
||||||
nil -> handle_call({:create, type}, from, state)
|
nil -> handle_call({:create, type}, from, state)
|
||||||
some -> {:reply, some, state}
|
some -> {:reply, some, state}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@spec _create(atom(), State.t()) :: {Game.t(), State.t()}
|
defp _create(type, games) do
|
||||||
defp _create(type, state) do
|
|
||||||
# Create a DynamicSupervisor for this game
|
# Create a DynamicSupervisor for this game
|
||||||
{:ok, game_supervisor_pid} = DynamicSupervisor.start_child(
|
{:ok, game_supervisor_pid} = DynamicSupervisor.start_child(
|
||||||
{:via, PartitionSupervisor, {Amethyst.GameMetaSupervisor, type}},
|
{:via, PartitionSupervisor, {Amethyst.GameMetaSupervisor, type}},
|
||||||
@ -88,19 +66,20 @@ defmodule Amethyst.GameCoordinator do
|
|||||||
# We should gracefully handle situations where we cannot create a game.
|
# We should gracefully handle situations where we cannot create a game.
|
||||||
{:ok, refs} = type.instantiate(game_supervisor_pid)
|
{:ok, refs} = type.instantiate(game_supervisor_pid)
|
||||||
refs = refs |> Map.put(:game_supervisor, game_supervisor_pid) |> Map.put(:task_supervisor, task_supervisor_pid)
|
refs = refs |> Map.put(:game_supervisor, game_supervisor_pid) |> Map.put(:task_supervisor, task_supervisor_pid)
|
||||||
game = %Game{
|
pid = spawn(Amethyst.API.Game, :start, [type, refs])
|
||||||
mod: type, refs: refs, opts: []
|
games = [{type, pid, []} | games]
|
||||||
}
|
{pid, games}
|
||||||
games = state.games |> Map.put(state.gid, game)
|
|
||||||
{game, %State{gid: state.gid + 1, games: games}}
|
|
||||||
end
|
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
|
if length(existing) > 0 do
|
||||||
[{_mod, pid, _} | _] = existing
|
[{_mod, pid, _} | _] = existing
|
||||||
{pid, games}
|
{pid, alive_games}
|
||||||
else
|
else
|
||||||
{nil, games}
|
{nil, alive_games}
|
||||||
end
|
end
|
||||||
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
|
end
|
||||||
# Acknowledge Finish Configuration https://wiki.vg/Protocol#Acknowledge_Finish_Configuration
|
# Acknowledge Finish Configuration https://wiki.vg/Protocol#Acknowledge_Finish_Configuration
|
||||||
def handle({:acknowledge_finish_configuration}, client, state) do
|
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)
|
state = Keyword.put(state, :game, game)
|
||||||
Amethyst.API.Game.login(game, state)
|
Amethyst.API.Game.login(game, state)
|
||||||
# TODO: All of this stuff should obviously not be hardcoded here
|
# 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",
|
source_url: "https://git.colon-three.com/kodi/amethyst",
|
||||||
docs: [
|
docs: [
|
||||||
main: "readme",
|
main: "readme",
|
||||||
extras: ["../../README.md", "../../LICENSE.md"]
|
extras: ["../README.md", "../LICENSE.md"]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
@ -8,6 +8,7 @@ defmodule Example.Application do
|
|||||||
@impl true
|
@impl true
|
||||||
def start(_type, _args) do
|
def start(_type, _args) do
|
||||||
children = [
|
children = [
|
||||||
|
{Example.Game, {}}
|
||||||
]
|
]
|
||||||
|
|
||||||
# See https://hexdocs.pm/elixir/Supervisor.html
|
# See https://hexdocs.pm/elixir/Supervisor.html
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
defmodule Example.Game do
|
defmodule Example.Game do
|
||||||
require Logger
|
require Logger
|
||||||
@behaviour Amethyst.API.Game
|
use Amethyst.API.Game, meta: [default: true]
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def instantiate(supervisor) do
|
def instantiate(supervisor) do
|
||||||
@ -9,9 +9,8 @@ defmodule Example.Game do
|
|||||||
end
|
end
|
||||||
|
|
||||||
@impl true
|
@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("Player logged in from #{inspect(from)}: #{inspect(cfg)}")
|
||||||
Logger.info("The refs for this game are #{inspect(refs)}")
|
|
||||||
:ok
|
:ok
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -3,5 +3,4 @@ import Config
|
|||||||
config :amethyst,
|
config :amethyst,
|
||||||
port: 25599, # Bogus port for testing, avoids unexpected conflicts
|
port: 25599, # Bogus port for testing, avoids unexpected conflicts
|
||||||
encryption: false, # Whether or not to request encryption from clients.
|
encryption: false, # Whether or not to request encryption from clients.
|
||||||
auth: false, # Whether or not users should be authenticated with Mojang.
|
auth: false # Whether or not users should be authenticated with Mojang.
|
||||||
default_game: Example.Game # Which game new players should be sent to
|
|
||||||
|
Loading…
Reference in New Issue
Block a user