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