301 lines
8.1 KiB
Elixir
301 lines
8.1 KiB
Elixir
defmodule Odinsea.Game.Map do
|
|
@moduledoc """
|
|
Represents a game map instance.
|
|
|
|
Each map is a GenServer that manages all objects on the map:
|
|
- Players
|
|
- Monsters (mobs)
|
|
- NPCs
|
|
- Items (drops)
|
|
- Reactors
|
|
- Portals
|
|
|
|
Maps are registered by map_id and belong to a specific channel.
|
|
"""
|
|
use GenServer
|
|
require Logger
|
|
|
|
alias Odinsea.Game.Character
|
|
alias Odinsea.Channel.Packets, as: ChannelPackets
|
|
|
|
# ============================================================================
|
|
# Data Structures
|
|
# ============================================================================
|
|
|
|
defmodule State do
|
|
@moduledoc "Map instance state"
|
|
defstruct [
|
|
# Map identity
|
|
:map_id,
|
|
:channel_id,
|
|
# Objects on map (by type)
|
|
:players,
|
|
# Map stores character_id => %{oid: integer(), character: Character.State}
|
|
:monsters,
|
|
# Map stores oid => Monster
|
|
:npcs,
|
|
# Map stores oid => NPC
|
|
:items,
|
|
# Map stores oid => Item
|
|
:reactors,
|
|
# Map stores oid => Reactor
|
|
# Object ID counter
|
|
:next_oid,
|
|
# Map properties (TODO: load from WZ data)
|
|
:return_map,
|
|
:forced_return,
|
|
:time_limit,
|
|
:field_limit,
|
|
:mob_rate,
|
|
:drop_rate,
|
|
# Timestamps
|
|
:created_at
|
|
]
|
|
|
|
@type t :: %__MODULE__{
|
|
map_id: non_neg_integer(),
|
|
channel_id: byte(),
|
|
players: %{pos_integer() => map()},
|
|
monsters: %{pos_integer() => any()},
|
|
npcs: %{pos_integer() => any()},
|
|
items: %{pos_integer() => any()},
|
|
reactors: %{pos_integer() => any()},
|
|
next_oid: pos_integer(),
|
|
return_map: non_neg_integer() | nil,
|
|
forced_return: non_neg_integer() | nil,
|
|
time_limit: non_neg_integer() | nil,
|
|
field_limit: non_neg_integer() | nil,
|
|
mob_rate: float(),
|
|
drop_rate: float(),
|
|
created_at: DateTime.t()
|
|
}
|
|
end
|
|
|
|
# ============================================================================
|
|
# Client API
|
|
# ============================================================================
|
|
|
|
@doc """
|
|
Starts a map GenServer.
|
|
"""
|
|
def start_link(opts) do
|
|
map_id = Keyword.fetch!(opts, :map_id)
|
|
channel_id = Keyword.fetch!(opts, :channel_id)
|
|
|
|
GenServer.start_link(__MODULE__, opts, name: via_tuple(map_id, channel_id))
|
|
end
|
|
|
|
@doc """
|
|
Ensures a map is loaded for the given channel.
|
|
"""
|
|
def ensure_map(map_id, channel_id) do
|
|
case Registry.lookup(Odinsea.MapRegistry, {map_id, channel_id}) do
|
|
[{pid, _}] ->
|
|
{:ok, pid}
|
|
|
|
[] ->
|
|
# Start map via DynamicSupervisor
|
|
spec = {__MODULE__, map_id: map_id, channel_id: channel_id}
|
|
|
|
case DynamicSupervisor.start_child(Odinsea.MapSupervisor, spec) do
|
|
{:ok, pid} -> {:ok, pid}
|
|
{:error, {:already_started, pid}} -> {:ok, pid}
|
|
error -> error
|
|
end
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Adds a player to the map.
|
|
"""
|
|
def add_player(map_id, character_id) do
|
|
# TODO: Get channel_id from somewhere
|
|
channel_id = 1
|
|
{:ok, _pid} = ensure_map(map_id, channel_id)
|
|
GenServer.call(via_tuple(map_id, channel_id), {:add_player, character_id})
|
|
end
|
|
|
|
@doc """
|
|
Removes a player from the map.
|
|
"""
|
|
def remove_player(map_id, character_id) do
|
|
# TODO: Get channel_id from somewhere
|
|
channel_id = 1
|
|
|
|
case Registry.lookup(Odinsea.MapRegistry, {map_id, channel_id}) do
|
|
[{_pid, _}] ->
|
|
GenServer.call(via_tuple(map_id, channel_id), {:remove_player, character_id})
|
|
|
|
[] ->
|
|
:ok
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Broadcasts a packet to all players on the map.
|
|
"""
|
|
def broadcast(map_id, channel_id, packet) do
|
|
GenServer.cast(via_tuple(map_id, channel_id), {:broadcast, packet})
|
|
end
|
|
|
|
@doc """
|
|
Broadcasts a packet to all players except the specified character.
|
|
"""
|
|
def broadcast_except(map_id, channel_id, except_character_id, packet) do
|
|
GenServer.cast(via_tuple(map_id, channel_id), {:broadcast_except, except_character_id, packet})
|
|
end
|
|
|
|
@doc """
|
|
Gets all players on the map.
|
|
"""
|
|
def get_players(map_id, channel_id) do
|
|
GenServer.call(via_tuple(map_id, channel_id), :get_players)
|
|
end
|
|
|
|
# ============================================================================
|
|
# GenServer Callbacks
|
|
# ============================================================================
|
|
|
|
@impl true
|
|
def init(opts) do
|
|
map_id = Keyword.fetch!(opts, :map_id)
|
|
channel_id = Keyword.fetch!(opts, :channel_id)
|
|
|
|
state = %State{
|
|
map_id: map_id,
|
|
channel_id: channel_id,
|
|
players: %{},
|
|
monsters: %{},
|
|
npcs: %{},
|
|
items: %{},
|
|
reactors: %{},
|
|
next_oid: 500_000,
|
|
return_map: nil,
|
|
forced_return: nil,
|
|
time_limit: nil,
|
|
field_limit: 0,
|
|
mob_rate: 1.0,
|
|
drop_rate: 1.0,
|
|
created_at: DateTime.utc_now()
|
|
}
|
|
|
|
Logger.debug("Map loaded: #{map_id} (channel #{channel_id})")
|
|
{:ok, state}
|
|
end
|
|
|
|
@impl true
|
|
def handle_call({:add_player, character_id}, _from, state) do
|
|
# Allocate OID for this player
|
|
oid = state.next_oid
|
|
|
|
# Get character state
|
|
case Character.get_state(character_id) do
|
|
%Character.State{} = char_state ->
|
|
# Add player to map
|
|
player_entry = %{
|
|
oid: oid,
|
|
character: char_state
|
|
}
|
|
|
|
new_players = Map.put(state.players, character_id, player_entry)
|
|
|
|
# Broadcast spawn packet to other players
|
|
spawn_packet = ChannelPackets.spawn_player(oid, char_state)
|
|
broadcast_to_players(new_players, spawn_packet, except: character_id)
|
|
|
|
# Send existing players to new player
|
|
client_pid = char_state.client_pid
|
|
|
|
if client_pid do
|
|
send_existing_players(client_pid, new_players, except: character_id)
|
|
end
|
|
|
|
new_state = %{
|
|
state
|
|
| players: new_players,
|
|
next_oid: oid + 1
|
|
}
|
|
|
|
{:reply, {:ok, oid}, new_state}
|
|
|
|
nil ->
|
|
{:reply, {:error, :character_not_found}, state}
|
|
end
|
|
end
|
|
|
|
@impl true
|
|
def handle_call({:remove_player, character_id}, _from, state) do
|
|
case Map.get(state.players, character_id) do
|
|
nil ->
|
|
{:reply, :ok, state}
|
|
|
|
player_entry ->
|
|
# Broadcast despawn packet
|
|
despawn_packet = ChannelPackets.remove_player(player_entry.oid)
|
|
broadcast_to_players(state.players, despawn_packet, except: character_id)
|
|
|
|
# Remove from map
|
|
new_players = Map.delete(state.players, character_id)
|
|
new_state = %{state | players: new_players}
|
|
|
|
{:reply, :ok, new_state}
|
|
end
|
|
end
|
|
|
|
@impl true
|
|
def handle_call(:get_players, _from, state) do
|
|
{:reply, state.players, state}
|
|
end
|
|
|
|
@impl true
|
|
def handle_cast({:broadcast, packet}, state) do
|
|
broadcast_to_players(state.players, packet)
|
|
{:noreply, state}
|
|
end
|
|
|
|
@impl true
|
|
def handle_cast({:broadcast_except, except_character_id, packet}, state) do
|
|
broadcast_to_players(state.players, packet, except: except_character_id)
|
|
{:noreply, state}
|
|
end
|
|
|
|
# ============================================================================
|
|
# Helper Functions
|
|
# ============================================================================
|
|
|
|
defp via_tuple(map_id, channel_id) do
|
|
{:via, Registry, {Odinsea.MapRegistry, {map_id, channel_id}}}
|
|
end
|
|
|
|
defp broadcast_to_players(players, packet, opts \\ []) do
|
|
except_char_id = Keyword.get(opts, :except)
|
|
|
|
Enum.each(players, fn
|
|
{char_id, %{character: char_state}} when char_id != except_char_id ->
|
|
if char_state.client_pid do
|
|
send_packet(char_state.client_pid, packet)
|
|
end
|
|
|
|
_ ->
|
|
:ok
|
|
end)
|
|
end
|
|
|
|
defp send_existing_players(client_pid, players, opts) do
|
|
except_char_id = Keyword.get(opts, :except)
|
|
|
|
Enum.each(players, fn
|
|
{char_id, %{oid: oid, character: char_state}} when char_id != except_char_id ->
|
|
spawn_packet = ChannelPackets.spawn_player(oid, char_state)
|
|
send_packet(client_pid, spawn_packet)
|
|
|
|
_ ->
|
|
:ok
|
|
end)
|
|
end
|
|
|
|
defp send_packet(client_pid, packet) do
|
|
send(client_pid, {:send_packet, packet})
|
|
end
|
|
end
|