115 lines
3.9 KiB
Elixir
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
|