476 lines
13 KiB
Elixir
476 lines
13 KiB
Elixir
defmodule Odinsea.Scripting.EventManager do
|
|
@moduledoc """
|
|
Event Script Manager for handling game events and party quests.
|
|
|
|
Event scripts run continuously on channel servers and manage instances
|
|
of party quests, special events, and scheduled activities.
|
|
|
|
## Script Interface
|
|
|
|
Event scripts receive an `em` (event manager) object with these callbacks:
|
|
|
|
- `init/1` - Called when event is loaded
|
|
- `schedule/3` - Schedule a method callback after delay
|
|
- `setup/2` - Called to create a new event instance
|
|
- `player_entry/2` - Player enters instance
|
|
- `player_dead/2` - Player dies
|
|
- `player_revive/2` - Player revives
|
|
- `player_disconnected/2` - Player disconnects
|
|
- `monster_value/2` - Monster killed, returns points
|
|
- `all_monsters_dead/1` - All monsters killed
|
|
- `scheduled_timeout/1` - Event timer expired
|
|
- `left_party/2` - Player left party
|
|
- `disband_party/1` - Party disbanded
|
|
- `clear_pq/1` - Party quest cleared
|
|
- `player_exit/2` - Player exits event
|
|
- `cancel_schedule/1` - Event cancelled
|
|
|
|
## Example Event Script
|
|
|
|
defmodule Odinsea.Scripting.Event.Boats do
|
|
@behaviour Odinsea.Scripting.Behavior
|
|
|
|
alias Odinsea.Scripting.EventManager
|
|
|
|
@impl true
|
|
def init(em) do
|
|
schedule_new(em)
|
|
end
|
|
|
|
@impl true
|
|
def schedule(em, "stopentry", _delay) do
|
|
set_property(em, "entry", "false")
|
|
end
|
|
|
|
def schedule(em, "takeoff", _delay) do
|
|
warp_all_player(em, 200000112, 200090000)
|
|
schedule(em, "arrived", 420_000)
|
|
end
|
|
|
|
def schedule(em, "arrived", _delay) do
|
|
warp_all_player(em, 200090000, 101000300)
|
|
schedule_new(em)
|
|
end
|
|
|
|
defp schedule_new(em) do
|
|
set_property(em, "docked", "true")
|
|
set_property(em, "entry", "true")
|
|
schedule(em, "stopentry", 240_000)
|
|
schedule(em, "takeoff", 300_000)
|
|
end
|
|
end
|
|
"""
|
|
|
|
use GenServer
|
|
require Logger
|
|
|
|
alias Odinsea.Scripting.{Manager, EventInstance}
|
|
|
|
# ETS tables
|
|
@event_scripts :event_scripts
|
|
@event_instances :event_instances
|
|
@event_properties :event_properties
|
|
|
|
# ============================================================================
|
|
# Types
|
|
# ============================================================================
|
|
|
|
@type t :: %__MODULE__{
|
|
name: String.t(),
|
|
channel: integer(),
|
|
script_module: module() | nil,
|
|
properties: map()
|
|
}
|
|
|
|
defstruct [
|
|
:name,
|
|
:channel,
|
|
:script_module,
|
|
:properties
|
|
]
|
|
|
|
# ============================================================================
|
|
# Client API
|
|
# ============================================================================
|
|
|
|
@doc """
|
|
Starts the event script manager.
|
|
"""
|
|
@spec start_link(keyword()) :: GenServer.on_start()
|
|
def start_link(opts \\ []) do
|
|
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
|
|
end
|
|
|
|
@doc """
|
|
Loads and initializes event scripts.
|
|
|
|
## Parameters
|
|
- `scripts` - List of event script names (without .js extension)
|
|
- `channel` - Channel server number
|
|
|
|
## Returns
|
|
- `{:ok, count}` - Number of events loaded
|
|
"""
|
|
@spec load_events([String.t()], integer()) :: {:ok, integer()} | {:error, term()}
|
|
def load_events(scripts, channel) do
|
|
GenServer.call(__MODULE__, {:load_events, scripts, channel})
|
|
end
|
|
|
|
@doc """
|
|
Gets an event manager for a specific event.
|
|
|
|
## Parameters
|
|
- `event_name` - Name of the event
|
|
|
|
## Returns
|
|
- `{:ok, em}` - Event manager struct
|
|
- `{:error, :not_found}` - Event not loaded
|
|
"""
|
|
@spec get_event(String.t()) :: {:ok, t()} | {:error, term()}
|
|
def get_event(event_name) do
|
|
case :ets.lookup(@event_scripts, event_name) do
|
|
[{^event_name, em}] -> {:ok, em}
|
|
[] -> {:error, :not_found}
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Creates a new event instance.
|
|
|
|
## Parameters
|
|
- `event_name` - Name of the event
|
|
- `instance_name` - Unique name for this instance
|
|
- `args` - Arguments to pass to setup
|
|
|
|
## Returns
|
|
- `{:ok, eim}` - Event instance created
|
|
- `{:error, reason}` - Failed to create
|
|
"""
|
|
@spec new_instance(String.t(), String.t(), term()) ::
|
|
{:ok, EventInstance.t()} | {:error, term()}
|
|
def new_instance(event_name, instance_name, args \\ nil) do
|
|
GenServer.call(__MODULE__, {:new_instance, event_name, instance_name, args})
|
|
end
|
|
|
|
@doc """
|
|
Gets an existing event instance.
|
|
"""
|
|
@spec get_instance(String.t()) :: {:ok, EventInstance.t()} | {:error, term()}
|
|
def get_instance(instance_name) do
|
|
case :ets.lookup(@event_instances, instance_name) do
|
|
[{^instance_name, eim}] -> {:ok, eim}
|
|
[] -> {:error, :not_found}
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Disposes of an event instance.
|
|
"""
|
|
@spec dispose_instance(String.t()) :: :ok
|
|
def dispose_instance(instance_name) do
|
|
GenServer.call(__MODULE__, {:dispose_instance, instance_name})
|
|
end
|
|
|
|
@doc """
|
|
Schedules a method callback on an event.
|
|
|
|
## Parameters
|
|
- `em` - Event manager or instance
|
|
- `method_name` - Name of the method to call
|
|
- `delay_ms` - Delay in milliseconds
|
|
"""
|
|
@spec schedule(t() | EventInstance.t(), String.t(), integer()) :: reference()
|
|
def schedule(em_or_eim, method_name, delay_ms) do
|
|
GenServer.call(__MODULE__, {:schedule, em_or_eim, method_name, delay_ms})
|
|
end
|
|
|
|
@doc """
|
|
Cancels all scheduled tasks for an event.
|
|
"""
|
|
@spec cancel(t()) :: :ok
|
|
def cancel(em) do
|
|
GenServer.call(__MODULE__, {:cancel, em.name})
|
|
end
|
|
|
|
@doc """
|
|
Sets a property on an event manager.
|
|
"""
|
|
@spec set_property(t(), String.t(), String.t()) :: :ok
|
|
def set_property(em, key, value) do
|
|
GenServer.call(__MODULE__, {:set_property, em.name, key, value})
|
|
end
|
|
|
|
@doc """
|
|
Gets a property from an event manager.
|
|
"""
|
|
@spec get_property(t(), String.t()) :: String.t() | nil
|
|
def get_property(em, key) do
|
|
case :ets.lookup(@event_properties, {em.name, key}) do
|
|
[{_, value}] -> value
|
|
[] -> nil
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Warps all players from one map to another.
|
|
|
|
## Parameters
|
|
- `em` - Event manager
|
|
- `from_map` - Source map ID
|
|
- `to_map` - Destination map ID
|
|
"""
|
|
@spec warp_all_player(t(), integer(), integer()) :: :ok
|
|
def warp_all_player(em, from_map, to_map) do
|
|
Logger.debug("Event #{em.name}: Warp all from #{from_map} to #{to_map}")
|
|
# TODO: Implement warp all
|
|
:ok
|
|
end
|
|
|
|
@doc """
|
|
Broadcasts a ship effect to a map.
|
|
"""
|
|
@spec broadcast_ship(t(), integer(), integer()) :: :ok
|
|
def broadcast_ship(em, map_id, effect) do
|
|
Logger.debug("Event #{em.name}: Broadcast ship effect #{effect} to map #{map_id}")
|
|
# TODO: Send boat packet
|
|
:ok
|
|
end
|
|
|
|
@doc """
|
|
Broadcasts a yellow message to the channel.
|
|
"""
|
|
@spec broadcast_yellow_msg(t(), String.t()) :: :ok
|
|
def broadcast_yellow_msg(em, message) do
|
|
Logger.debug("Event #{em.name}: Yellow message: #{message}")
|
|
# TODO: Broadcast to channel
|
|
:ok
|
|
end
|
|
|
|
@doc """
|
|
Broadcasts a server message.
|
|
"""
|
|
@spec broadcast_server_msg(t(), integer(), String.t(), boolean()) :: :ok
|
|
def broadcast_server_msg(em, type, message, weather \\ false) do
|
|
Logger.debug("Event #{em.name}: Server message (#{type}): #{message}")
|
|
# TODO: Broadcast
|
|
:ok
|
|
end
|
|
|
|
@doc """
|
|
Gets the map factory for creating map instances.
|
|
"""
|
|
@spec get_map_factory(t()) :: term()
|
|
def get_map_factory(_em) do
|
|
# TODO: Return map factory
|
|
nil
|
|
end
|
|
|
|
@doc """
|
|
Gets a monster by ID.
|
|
"""
|
|
@spec get_monster(t(), integer()) :: term()
|
|
def get_monster(_em, mob_id) do
|
|
# TODO: Return monster data
|
|
%{id: mob_id}
|
|
end
|
|
|
|
@doc """
|
|
Gets a reactor by ID.
|
|
"""
|
|
@spec get_reactor(t(), integer()) :: term()
|
|
def get_reactor(_em, reactor_id) do
|
|
# TODO: Return reactor data
|
|
%{id: reactor_id}
|
|
end
|
|
|
|
@doc """
|
|
Creates new monster stats for overriding.
|
|
"""
|
|
@spec new_monster_stats() :: map()
|
|
def new_monster_stats() do
|
|
%{}
|
|
end
|
|
|
|
@doc """
|
|
Creates a new character list.
|
|
"""
|
|
@spec new_char_list() :: list()
|
|
def new_char_list() do
|
|
[]
|
|
end
|
|
|
|
@doc """
|
|
Gets the EXP rate for the channel.
|
|
"""
|
|
@spec get_exp_rate(t()) :: integer()
|
|
def get_exp_rate(_em) do
|
|
# TODO: Get from channel config
|
|
1
|
|
end
|
|
|
|
@doc """
|
|
Gets the channel server.
|
|
"""
|
|
@spec get_channel_server(t()) :: term()
|
|
def get_channel_server(em) do
|
|
# TODO: Return channel server
|
|
%{channel: em.channel}
|
|
end
|
|
|
|
@doc """
|
|
Gets the channel number.
|
|
"""
|
|
@spec get_channel(t()) :: integer()
|
|
def get_channel(em) do
|
|
em.channel
|
|
end
|
|
|
|
# ============================================================================
|
|
# Server Callbacks
|
|
# ============================================================================
|
|
|
|
@impl true
|
|
def init(_opts) do
|
|
# Create ETS tables
|
|
:ets.new(@event_scripts, [:named_table, :set, :public,
|
|
read_concurrency: true, write_concurrency: true])
|
|
:ets.new(@event_instances, [:named_table, :set, :public,
|
|
read_concurrency: true, write_concurrency: true])
|
|
:ets.new(@event_properties, [:named_table, :set, :public,
|
|
read_concurrency: true, write_concurrency: true])
|
|
|
|
Logger.info("Event Script Manager initialized")
|
|
|
|
{:ok, %{timers: %{}}}
|
|
end
|
|
|
|
@impl true
|
|
def handle_call({:load_events, scripts, channel}, _from, state) do
|
|
count = Enum.count(scripts, fn script_name ->
|
|
case Manager.get_script(:event, script_name) do
|
|
{:ok, module} ->
|
|
em = %__MODULE__{
|
|
name: script_name,
|
|
channel: channel,
|
|
script_module: module,
|
|
properties: %{}
|
|
}
|
|
|
|
:ets.insert(@event_scripts, {script_name, em})
|
|
|
|
# Call init if available
|
|
if function_exported?(module, :init, 1) do
|
|
Task.start(fn ->
|
|
try do
|
|
module.init(em)
|
|
rescue
|
|
e -> Logger.error("Event #{script_name} init error: #{inspect(e)}")
|
|
end
|
|
end)
|
|
end
|
|
|
|
true
|
|
|
|
{:error, reason} ->
|
|
Logger.warning("Failed to load event #{script_name}: #{inspect(reason)}")
|
|
false
|
|
end
|
|
end)
|
|
|
|
{:reply, {:ok, count}, state}
|
|
end
|
|
|
|
@impl true
|
|
def handle_call({:new_instance, event_name, instance_name, args}, _from, state) do
|
|
case :ets.lookup(@event_scripts, event_name) do
|
|
[{^event_name, em}] ->
|
|
# Create event instance
|
|
eim = EventInstance.new(em, instance_name, em.channel)
|
|
:ets.insert(@event_instances, {instance_name, eim})
|
|
|
|
# Call setup
|
|
if em.script_module && function_exported?(em.script_module, :setup, 2) do
|
|
Task.start(fn ->
|
|
try do
|
|
em.script_module.setup(eim, args)
|
|
rescue
|
|
e -> Logger.error("Event #{event_name} setup error: #{inspect(e)}")
|
|
end
|
|
end)
|
|
end
|
|
|
|
{:reply, {:ok, eim}, state}
|
|
|
|
[] ->
|
|
{:reply, {:error, :event_not_found}, state}
|
|
end
|
|
end
|
|
|
|
@impl true
|
|
def handle_call({:dispose_instance, instance_name}, _from, state) do
|
|
:ets.delete(@event_instances, instance_name)
|
|
|
|
# Cancel any timers for this instance
|
|
timers = Map.get(state.timers, instance_name, [])
|
|
Enum.each(timers, fn ref ->
|
|
Process.cancel_timer(ref)
|
|
end)
|
|
|
|
new_timers = Map.delete(state.timers, instance_name)
|
|
{:reply, :ok, %{state | timers: new_timers}}
|
|
end
|
|
|
|
@impl true
|
|
def handle_call({:schedule, em_or_eim, method_name, delay_ms}, _from, state) do
|
|
instance_name = case em_or_eim do
|
|
%{name: name} -> name
|
|
_ -> "unknown"
|
|
end
|
|
|
|
ref = Process.send_after(self(), {:scheduled, em_or_eim, method_name}, delay_ms)
|
|
|
|
timers = Map.update(state.timers, instance_name, [ref], &[ref | &1])
|
|
|
|
{:reply, ref, %{state | timers: timers}}
|
|
end
|
|
|
|
@impl true
|
|
def handle_call({:cancel, event_name}, _from, state) do
|
|
timers = Map.get(state.timers, event_name, [])
|
|
Enum.each(timers, fn ref ->
|
|
Process.cancel_timer(ref)
|
|
end)
|
|
|
|
new_timers = Map.delete(state.timers, event_name)
|
|
{:reply, :ok, %{state | timers: new_timers}}
|
|
end
|
|
|
|
@impl true
|
|
def handle_call({:set_property, event_name, key, value}, _from, state) do
|
|
:ets.insert(@event_properties, {{event_name, key}, value})
|
|
{:reply, :ok, state}
|
|
end
|
|
|
|
@impl true
|
|
def handle_info({:scheduled, em_or_eim, method_name}, state) do
|
|
# Find script module
|
|
script_module = case em_or_eim do
|
|
%{__struct__: Odinsea.Scripting.EventManager, script_module: mod} -> mod
|
|
%{__struct__: Odinsea.Scripting.EventInstance, event_manager: em} -> em.script_module
|
|
_ -> nil
|
|
end
|
|
|
|
if script_module && function_exported?(script_module, :schedule, 3) do
|
|
Task.start(fn ->
|
|
try do
|
|
script_module.schedule(em_or_eim, method_name, 0)
|
|
rescue
|
|
e -> Logger.error("Scheduled event error: #{inspect(e)}")
|
|
end
|
|
end)
|
|
end
|
|
|
|
{:noreply, state}
|
|
end
|
|
end
|