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