amethyst/apps/amethyst/lib/apps/game_coordinator.ex
Kodi Craft 8ae0c08e8d
Some checks failed
Build & Test / nix-build (push) Failing after 28s
Complete refactoring
2024-09-03 19:29:19 +02:00

115 lines
3.9 KiB
Elixir

# 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.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.
"""
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: [], gid: 0
end
@spec start_link(State) :: {:ok, pid()}
def start_link(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}
end
@impl true
def handle_call({:create, type}, _from, state) do
{game, state} = _create(type, state)
{:reply, game, state}
end
@impl true
def handle_call({:find_or_create, type}, from, state) do
{game, state} = _find(type, state)
case game 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
# Create a DynamicSupervisor for this game
{:ok, game_supervisor_pid} = DynamicSupervisor.start_child(
{:via, PartitionSupervisor, {Amethyst.GameMetaSupervisor, type}},
DynamicSupervisor
)
{:ok, task_supervisor_pid} = DynamicSupervisor.start_child(
game_supervisor_pid,
Task.Supervisor
)
# TODO: Instantiation can fail (including with an exception), and if it does the entire GameCoordinator goes down
# 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) |> Map.put(:game_coordinator, self())
game = %Game{
mod: type, refs: refs, opts: [], gid: state.gid
}
games = state.games |> Map.put(state.gid, game)
{game, %State{gid: state.gid + 1, games: games}}
end
defp _find(type, state) do
case state.games |> Enum.find(fn {_, game} -> game.mod == type end) do
nil -> {nil, state}
{_, game} -> {game, state}
end
end
def create(type) when is_atom(type) do
GenServer.call({:global, __MODULE__}, {:create, type})
end
def find(type) when is_atom(type) do
GenServer.call({:global, __MODULE__}, {:find, type})
end
def find_or_create(type) when is_atom(type) do
GenServer.call({:global, __MODULE__}, {:find_or_create, type})
end
end