kimi gone wild
This commit is contained in:
475
lib/odinsea/scripting/event_manager.ex
Normal file
475
lib/odinsea/scripting/event_manager.ex
Normal file
@@ -0,0 +1,475 @@
|
||||
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
|
||||
Reference in New Issue
Block a user