Files
odinsea-elixir/lib/odinsea/scripting/event_manager.ex
2026-02-14 23:12:33 -07:00

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