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