412 lines
12 KiB
Elixir
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
|