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

412 lines
12 KiB
Elixir

defmodule Odinsea.Game.Timer do
@moduledoc """
Timer system for scheduling game events.
Ported from Java `server.Timer`.
Provides multiple timer types for different purposes:
- WorldTimer - Global world events
- MapTimer - Map-specific events
- BuffTimer - Character buffs
- EventTimer - Game events
- CloneTimer - Character clones
- EtcTimer - Miscellaneous
- CheatTimer - Anti-cheat monitoring
- PingTimer - Connection keep-alive
- RedisTimer - Redis updates
- EMTimer - Event manager
- GlobalTimer - Global scheduled tasks
Each timer is a GenServer that manages scheduled tasks using
`Process.send_after` for efficient Erlang VM scheduling.
"""
require Logger
# ============================================================================
# Task Struct (defined first for use in Base)
# ============================================================================
defmodule Task do
@moduledoc """
Represents a scheduled task.
Fields:
- id: Unique task identifier
- type: :one_shot or :recurring
- fun: The function to execute (arity 0)
- repeat_time: For recurring tasks, interval in milliseconds
- timer_ref: Reference to the Erlang timer
"""
defstruct [
:id,
:type,
:fun,
:repeat_time,
:timer_ref
]
@type t :: %__MODULE__{
id: pos_integer(),
type: :one_shot | :recurring,
fun: function(),
repeat_time: non_neg_integer() | nil,
timer_ref: reference()
}
end
# ============================================================================
# Base Timer Implementation (GenServer) - Must be defined before timer types
# ============================================================================
defmodule Base do
@moduledoc """
Base implementation for all timer types.
Uses GenServer with Process.send_after for scheduling.
"""
defmacro __using__(opts) do
timer_name = Keyword.fetch!(opts, :name)
quote do
use GenServer
require Logger
alias Odinsea.Game.Timer.Task
# ============================================================================
# Client API
# ============================================================================
@doc """
Starts the timer GenServer.
"""
def start_link(_opts \\ []) do
GenServer.start_link(__MODULE__, %{}, name: __MODULE__)
end
@doc """
Registers a recurring task that executes at fixed intervals.
## Parameters
- `fun`: Function to execute (arity 0)
- `repeat_time`: Interval in milliseconds between executions
- `delay`: Initial delay in milliseconds before first execution (default: 0)
## Returns
- `{:ok, task_id}` on success
- `{:error, reason}` on failure
"""
def register(fun, repeat_time, delay \\ 0) when is_function(fun, 0) and is_integer(repeat_time) and repeat_time > 0 do
GenServer.call(__MODULE__, {:register, fun, repeat_time, delay})
end
@doc """
Schedules a one-shot task to execute after a delay.
## Parameters
- `fun`: Function to execute (arity 0)
- `delay`: Delay in milliseconds before execution
## Returns
- `{:ok, task_id}` on success
- `{:error, reason}` on failure
"""
def schedule(fun, delay) when is_function(fun, 0) and is_integer(delay) and delay >= 0 do
GenServer.call(__MODULE__, {:schedule, fun, delay})
end
@doc """
Schedules a one-shot task to execute at a specific timestamp.
## Parameters
- `fun`: Function to execute (arity 0)
- `timestamp`: Unix timestamp in milliseconds
## Returns
- `{:ok, task_id}` on success
- `{:error, reason}` on failure
"""
def schedule_at_timestamp(fun, timestamp) when is_function(fun, 0) and is_integer(timestamp) do
delay = timestamp - System.system_time(:millisecond)
schedule(fun, max(0, delay))
end
@doc """
Cancels a scheduled or recurring task.
## Parameters
- `task_id`: The task ID returned from register/schedule
## Returns
- `:ok` on success
- `{:error, :not_found}` if task doesn't exist
"""
def cancel(task_id) do
GenServer.call(__MODULE__, {:cancel, task_id})
end
@doc """
Stops the timer and cancels all pending tasks.
"""
def stop do
GenServer.stop(__MODULE__, :normal)
end
@doc """
Gets information about all active tasks.
"""
def info do
GenServer.call(__MODULE__, :info)
end
# ============================================================================
# GenServer Callbacks
# ============================================================================
@impl true
def init(_) do
Logger.debug("#{__MODULE__} started")
{:ok, %{tasks: %{}, next_id: 1}}
end
@impl true
def handle_call({:register, fun, repeat_time, delay}, _from, state) do
task_id = state.next_id
# Schedule initial execution
timer_ref = Process.send_after(self(), {:execute_recurring, task_id}, delay)
task = %Task{
id: task_id,
type: :recurring,
fun: fun,
repeat_time: repeat_time,
timer_ref: timer_ref
}
new_state = %{
state
| tasks: Map.put(state.tasks, task_id, task),
next_id: task_id + 1
}
{:reply, {:ok, task_id}, new_state}
end
@impl true
def handle_call({:schedule, fun, delay}, _from, state) do
task_id = state.next_id
timer_ref = Process.send_after(self(), {:execute_once, task_id}, delay)
task = %Task{
id: task_id,
type: :one_shot,
fun: fun,
timer_ref: timer_ref
}
new_state = %{
state
| tasks: Map.put(state.tasks, task_id, task),
next_id: task_id + 1
}
{:reply, {:ok, task_id}, new_state}
end
@impl true
def handle_call({:cancel, task_id}, _from, state) do
case Map.pop(state.tasks, task_id) do
{nil, _} ->
{:reply, {:error, :not_found}, state}
{task, remaining_tasks} ->
# Cancel the timer if it hasn't fired yet
Process.cancel_timer(task.timer_ref)
{:reply, :ok, %{state | tasks: remaining_tasks}}
end
end
@impl true
def handle_call(:info, _from, state) do
info = %{
module: __MODULE__,
task_count: map_size(state.tasks),
tasks: state.tasks
}
{:reply, info, state}
end
@impl true
def handle_info({:execute_once, task_id}, state) do
case Map.pop(state.tasks, task_id) do
{nil, _} ->
# Task was already cancelled
{:noreply, state}
{task, remaining_tasks} ->
# Execute the task with error handling
execute_task(task)
{:noreply, %{state | tasks: remaining_tasks}}
end
end
@impl true
def handle_info({:execute_recurring, task_id}, state) do
case Map.get(state.tasks, task_id) do
nil ->
# Task was cancelled
{:noreply, state}
task ->
# Execute the task with error handling
execute_task(task)
# Reschedule the next execution
new_timer_ref = Process.send_after(self(), {:execute_recurring, task_id}, task.repeat_time)
updated_task = %{task | timer_ref: new_timer_ref}
new_tasks = Map.put(state.tasks, task_id, updated_task)
{:noreply, %{state | tasks: new_tasks}}
end
end
@impl true
def terminate(_reason, state) do
# Cancel all pending timers
Enum.each(state.tasks, fn {_id, task} ->
Process.cancel_timer(task.timer_ref)
end)
Logger.debug("#{__MODULE__} stopped, cancelled #{map_size(state.tasks)} tasks")
:ok
end
# ============================================================================
# Private Functions
# ============================================================================
defp execute_task(task) do
try do
task.fun.()
rescue
exception ->
Logger.error("#{__MODULE__} task #{task.id} failed: #{Exception.message(exception)}")
Logger.debug("#{__MODULE__} task #{task.id} stacktrace: #{Exception.format_stacktrace()}")
catch
kind, reason ->
Logger.error("#{__MODULE__} task #{task.id} crashed: #{kind} - #{inspect(reason)}")
end
end
end
end
end
# ============================================================================
# Timer Types - Individual GenServer Modules (defined AFTER Base)
# ============================================================================
defmodule WorldTimer do
@moduledoc "Timer for global world events."
use Odinsea.Game.Timer.Base, name: :world_timer
end
defmodule MapTimer do
@moduledoc "Timer for map-specific events."
use Odinsea.Game.Timer.Base, name: :map_timer
end
defmodule BuffTimer do
@moduledoc "Timer for character buffs."
use Odinsea.Game.Timer.Base, name: :buff_timer
end
defmodule EventTimer do
@moduledoc "Timer for game events."
use Odinsea.Game.Timer.Base, name: :event_timer
end
defmodule CloneTimer do
@moduledoc "Timer for character clones."
use Odinsea.Game.Timer.Base, name: :clone_timer
end
defmodule EtcTimer do
@moduledoc "Timer for miscellaneous tasks."
use Odinsea.Game.Timer.Base, name: :etc_timer
end
defmodule CheatTimer do
@moduledoc "Timer for anti-cheat monitoring."
use Odinsea.Game.Timer.Base, name: :cheat_timer
end
defmodule PingTimer do
@moduledoc "Timer for connection keep-alive pings."
use Odinsea.Game.Timer.Base, name: :ping_timer
end
defmodule RedisTimer do
@moduledoc "Timer for Redis updates."
use Odinsea.Game.Timer.Base, name: :redis_timer
end
defmodule EMTimer do
@moduledoc "Timer for event manager scheduling."
use Odinsea.Game.Timer.Base, name: :em_timer
end
defmodule GlobalTimer do
@moduledoc "Timer for global scheduled tasks."
use Odinsea.Game.Timer.Base, name: :global_timer
end
# ============================================================================
# Convenience Functions (Delegating to specific timers)
# ============================================================================
@doc """
Schedules a one-shot task on the EtcTimer (for general use).
"""
def schedule(fun, delay) when is_function(fun, 0) do
EtcTimer.schedule(fun, delay)
end
@doc """
Registers a recurring task on the EtcTimer (for general use).
"""
def register(fun, repeat_time, delay \\ 0) when is_function(fun, 0) do
EtcTimer.register(fun, repeat_time, delay)
end
@doc """
Cancels a task by ID on the EtcTimer.
Note: For other timers, use TimerType.cancel(task_id) directly.
"""
def cancel(task_id) do
EtcTimer.cancel(task_id)
end
@doc """
Returns a list of all timer modules for supervision.
"""
def all_timer_modules do
[
WorldTimer,
MapTimer,
BuffTimer,
EventTimer,
CloneTimer,
EtcTimer,
CheatTimer,
PingTimer,
RedisTimer,
EMTimer,
GlobalTimer
]
end
end