defmodule Odinsea.Game.EventManager do @moduledoc """ Event Manager for scheduling and managing in-game events. Ported from Java `server.events` scheduling functionality. ## Responsibilities - Event scheduling per channel - Player registration for events - Event state management - Event coordination across channels ## Event Types - Coconut - Team coconut hitting - Fitness - Obstacle course - OlaOla - Portal guessing - OxQuiz - True/False quiz - Snowball - Team snowball rolling - Survival - Last man standing ## Usage Event scheduling is typically done by GM commands or automated system: # Schedule an event EventManager.schedule_event(channel_id, :coconut) # Player joins event EventManager.join_event(channel_id, character_id, :coconut) # Start the event EventManager.start_event(channel_id, :coconut) """ use GenServer alias Odinsea.Game.Events alias Odinsea.Game.Event alias Odinsea.Game.Timer.EventTimer require Logger # ============================================================================ # Types # ============================================================================ @typedoc "Channel event state" @type channel_events :: %{ optional(Events.t()) => Event.t() | struct() } @typedoc "Manager state" @type state :: %{ channels: %{optional(non_neg_integer()) => channel_events()}, schedules: %{optional(reference()) => {:auto_start, non_neg_integer(), Events.t()}} } # ============================================================================ # Client API # ============================================================================ @doc """ Starts the Event Manager. """ def start_link(_opts) do GenServer.start_link(__MODULE__, [], name: __MODULE__) end @doc """ Schedules an event to run on a specific channel. ## Parameters - channel_id: Channel to run event on - event_type: Type of event (:coconut, :fitness, etc.) ## Returns - :ok on success - {:error, reason} on failure """ def schedule_event(channel_id, event_type) do GenServer.call(__MODULE__, {:schedule_event, channel_id, event_type}) end @doc """ Starts a scheduled event immediately. ## Parameters - channel_id: Channel running the event - event_type: Type of event """ def start_event(channel_id, event_type) do GenServer.call(__MODULE__, {:start_event, channel_id, event_type}) end @doc """ Cancels a scheduled event. ## Parameters - channel_id: Channel with the event - event_type: Type of event """ def cancel_event(channel_id, event_type) do GenServer.call(__MODULE__, {:cancel_event, channel_id, event_type}) end @doc """ Registers a player for an event. ## Parameters - channel_id: Channel with the event - character_id: Character joining - event_type: Type of event """ def join_event(channel_id, character_id, event_type) do GenServer.call(__MODULE__, {:join_event, channel_id, character_id, event_type}) end @doc """ Unregisters a player from an event. ## Parameters - channel_id: Channel with the event - character_id: Character leaving - event_type: Type of event """ def leave_event(channel_id, character_id, event_type) do GenServer.call(__MODULE__, {:leave_event, channel_id, character_id, event_type}) end @doc """ Handles a player loading into an event map. Called by map load handlers. ## Parameters - channel_id: Channel the player is on - character: Character struct - map_id: Map ID player loaded into """ def on_map_load(channel_id, character, map_id) do GenServer.cast(__MODULE__, {:on_map_load, channel_id, character, map_id}) end @doc """ Handles a GM manually starting an event. """ def on_start_event(channel_id, character, map_id) do GenServer.cast(__MODULE__, {:on_start_event, channel_id, character, map_id}) end @doc """ Gets the active event on a channel. """ def get_active_event(channel_id) do GenServer.call(__MODULE__, {:get_active_event, channel_id}) end @doc """ Gets all events for a channel. """ def get_channel_events(channel_id) do GenServer.call(__MODULE__, {:get_channel_events, channel_id}) end @doc """ Checks if an event is running on a channel. """ def event_running?(channel_id, event_type) do GenServer.call(__MODULE__, {:event_running?, channel_id, event_type}) end @doc """ Sets the active event map for a channel. This is the map where players should go to join. """ def set_event_map(channel_id, map_id) do GenServer.cast(__MODULE__, {:set_event_map, channel_id, map_id}) end @doc """ Gets the event map for a channel (where players join). """ def get_event_map(channel_id) do GenServer.call(__MODULE__, {:get_event_map, channel_id}) end @doc """ Lists all available event types. """ def list_event_types do Events.all() end @doc """ Gets event info for a type. """ def event_info(event_type) do %{ type: event_type, name: Events.display_name(event_type), map_ids: Events.map_ids(event_type), entry_map: Events.entry_map_id(event_type), stages: Events.stage_count(event_type), is_race: Events.race_event?(event_type), is_team: Events.team_event?(event_type) } end # ============================================================================ # Server Callbacks # ============================================================================ @impl true def init(_) do Logger.info("EventManager started") state = %{ channels: %{}, schedules: %{}, event_maps: %{} # channel_id => map_id } {:ok, state} end @impl true def handle_call({:schedule_event, channel_id, event_type}, _from, state) do case do_schedule_event(state, channel_id, event_type) do {:ok, new_state} -> broadcast_event_notice(channel_id, event_type) {:reply, :ok, new_state} {:error, reason} -> {:reply, {:error, reason}, state} end end @impl true def handle_call({:start_event, channel_id, event_type}, _from, state) do case do_start_event(state, channel_id, event_type) do {:ok, new_state} -> {:reply, :ok, new_state} {:error, reason} -> {:reply, {:error, reason}, state} end end @impl true def handle_call({:cancel_event, channel_id, event_type}, _from, state) do new_state = do_cancel_event(state, channel_id, event_type) {:reply, :ok, new_state} end @impl true def handle_call({:join_event, channel_id, character_id, event_type}, _from, state) do {reply, new_state} = do_join_event(state, channel_id, character_id, event_type) {:reply, reply, new_state} end @impl true def handle_call({:leave_event, channel_id, character_id, event_type}, _from, state) do {reply, new_state} = do_leave_event(state, channel_id, character_id, event_type) {:reply, reply, new_state} end @impl true def handle_call({:get_active_event, channel_id}, _from, state) do event = get_active_event_impl(state, channel_id) {:reply, event, state} end @impl true def handle_call({:get_channel_events, channel_id}, _from, state) do events = Map.get(state.channels, channel_id, %{}) {:reply, events, state} end @impl true def handle_call({:event_running?, channel_id, event_type}, _from, state) do running = event_running_impl?(state, channel_id, event_type) {:reply, running, state} end @impl true def handle_call({:get_event_map, channel_id}, _from, state) do map_id = Map.get(state.event_maps, channel_id) {:reply, map_id, state} end @impl true def handle_cast({:on_map_load, channel_id, character, map_id}, state) do new_state = do_on_map_load(state, channel_id, character, map_id) {:noreply, new_state} end @impl true def handle_cast({:on_start_event, channel_id, character, map_id}, state) do new_state = do_on_start_event(state, channel_id, character, map_id) {:noreply, new_state} end @impl true def handle_cast({:set_event_map, channel_id, map_id}, state) do new_maps = Map.put(state.event_maps, channel_id, map_id) {:noreply, %{state | event_maps: new_maps}} end @impl true def handle_info({:auto_start, channel_id, event_type}, state) do Logger.info("Auto-starting event #{event_type} on channel #{channel_id}") # Start the event case do_start_event(state, channel_id, event_type) do {:ok, new_state} -> # Clear event map new_maps = Map.delete(new_state.event_maps, channel_id) {:noreply, %{new_state | event_maps: new_maps}} {:error, _} -> {:noreply, state} end end # ============================================================================ # Private Functions # ============================================================================ defp do_schedule_event(state, channel_id, event_type) do # Check if event type is valid if event_type not in Events.all() do {:error, "Invalid event type"} else # Check if event is already running if event_running_impl?(state, channel_id, event_type) do {:error, "Event already running"} else # Create event instance event = create_event(event_type, channel_id) # Reset event event = reset_event(event, event_type) # Store event channel_events = Map.get(state.channels, channel_id, %{}) channel_events = Map.put(channel_events, event_type, event) channels = Map.put(state.channels, channel_id, channel_events) # Set event map (entry map) entry_map = Events.entry_map_id(event_type) event_maps = Map.put(state.event_maps, channel_id, entry_map) {:ok, %{state | channels: channels, event_maps: event_maps}} end end end defp do_start_event(state, channel_id, event_type) do with {:ok, event} <- get_event(state, channel_id, event_type) do # Start the event new_event = start_event_impl(event, event_type) # Update state channel_events = Map.get(state.channels, channel_id, %{}) channel_events = Map.put(channel_events, event_type, new_event) channels = Map.put(state.channels, channel_id, channel_events) # Clear event map event_maps = Map.delete(state.event_maps, channel_id) {:ok, %{state | channels: channels, event_maps: event_maps}} else nil -> {:error, "Event not found"} end end defp do_cancel_event(state, channel_id, event_type) do with {:ok, event} <- get_event(state, channel_id, event_type) do # Unreset event (cleanup) unreset_event(event, event_type) # Remove from state channel_events = Map.get(state.channels, channel_id, %{}) channel_events = Map.delete(channel_events, event_type) channels = Map.put(state.channels, channel_id, channel_events) # Clear event map event_maps = Map.delete(state.event_maps, channel_id) %{state | channels: channels, event_maps: event_maps} else nil -> state end end defp do_join_event(state, channel_id, character_id, event_type) do with {:ok, event} <- get_event(state, channel_id, event_type) do if event.base.is_running do # Register player new_event = Event.register_player(event.base, character_id) new_event = %{event | base: new_event} # Check if we should auto-start (250 players) new_event = Event.increment_player_count(new_event.base) new_event = %{event | base: new_event} # Update state channel_events = Map.get(state.channels, channel_id, %{}) channel_events = Map.put(channel_events, event_type, new_event) channels = Map.put(state.channels, channel_id, channel_events) {{:ok, :joined}, %{state | channels: channels}} else {{:error, "Event not running"}, state} end else nil -> {{:error, "Event not found"}, state} end end defp do_leave_event(state, channel_id, character_id, event_type) do with {:ok, event} <- get_event(state, channel_id, event_type) do # Unregister player new_base = Event.unregister_player(event.base, character_id) new_event = %{event | base: new_base} # Update state channel_events = Map.get(state.channels, channel_id, %{}) channel_events = Map.put(channel_events, event_type, new_event) channels = Map.put(state.channels, channel_id, channel_events) {{:ok, :left}, %{state | channels: channels}} else nil -> {{:error, "Event not found"}, state} end end defp do_on_map_load(state, channel_id, character, map_id) do # Check if any event is running on this channel channel_events = Map.get(state.channels, channel_id, %{}) Enum.reduce(channel_events, state, fn {event_type, event}, acc_state -> if event.base.is_running do # Check if this is the finish map if map_id == 109050000 do # Call finished callback finished_event(event, event_type, character) end # Check if this is an event map if Event.event_map?(event.base, map_id) do # Call on_map_load callback on_map_load_event(event, event_type, character) # If first map, increment player count if Event.map_index(event.base, map_id) == 0 do new_base = Event.increment_player_count(event.base) # Check if we hit 250 players if new_base.player_count >= 250 do # Auto-start schedule_auto_start(channel_id, event_type) end # Update event in state new_event = put_event_base(event, event_type, new_base) channel_events = Map.put(acc_state.channels[channel_id], event_type, new_event) channels = Map.put(acc_state.channels, channel_id, channel_events) %{acc_state | channels: channels} else acc_state end else acc_state end else acc_state end end) end defp do_on_start_event(state, channel_id, character, map_id) do channel_events = Map.get(state.channels, channel_id, %{}) Enum.find_value(channel_events, state, fn {event_type, event} -> if event.base.is_running and Event.event_map?(event.base, map_id) do # Start the event new_event = start_event_impl(event, event_type) # Update state channel_events = Map.put(channel_events, event_type, new_event) channels = Map.put(state.channels, channel_id, channel_events) # Clear event map event_maps = Map.delete(state.event_maps, channel_id) %{state | channels: channels, event_maps: event_maps} end end) || state end defp get_event(state, channel_id, event_type) do channel_events = Map.get(state.channels, channel_id, %{}) case Map.get(channel_events, event_type) do nil -> nil event -> {:ok, event} end end defp get_active_event_impl(state, channel_id) do channel_events = Map.get(state.channels, channel_id, %{}) Enum.find_value(channel_events, fn {event_type, event} -> if event.base.is_running do {event_type, event} end end) end defp event_running_impl?(state, channel_id, event_type) do case get_event(state, channel_id, event_type) do {:ok, event} -> event.base.is_running nil -> false end end defp create_event(:coconut, channel_id), do: Odinsea.Game.Events.Coconut.new(channel_id) defp create_event(:fitness, channel_id), do: Odinsea.Game.Events.Fitness.new(channel_id) defp create_event(:ola_ola, channel_id), do: Odinsea.Game.Events.OlaOla.new(channel_id) defp create_event(:ox_quiz, channel_id), do: Odinsea.Game.Events.OxQuiz.new(channel_id) defp create_event(:snowball, channel_id), do: Odinsea.Game.Events.Snowball.new(channel_id) defp create_event(:survival, channel_id), do: Odinsea.Game.Events.Survival.new(channel_id) defp create_event(_, _), do: nil defp reset_event(event, :coconut), do: Odinsea.Game.Events.Coconut.reset(event) defp reset_event(event, :fitness), do: Odinsea.Game.Events.Fitness.reset(event) defp reset_event(event, :ola_ola), do: Odinsea.Game.Events.OlaOla.reset(event) defp reset_event(event, :ox_quiz), do: Odinsea.Game.Events.OxQuiz.reset(event) defp reset_event(event, :snowball), do: Odinsea.Game.Events.Snowball.reset(event) defp reset_event(event, :survival), do: Odinsea.Game.Events.Survival.reset(event) defp reset_event(event, _), do: event defp unreset_event(event, :coconut), do: Odinsea.Game.Events.Coconut.unreset(event) defp unreset_event(event, :fitness), do: Odinsea.Game.Events.Fitness.unreset(event) defp unreset_event(event, :ola_ola), do: Odinsea.Game.Events.OlaOla.unreset(event) defp unreset_event(event, :ox_quiz), do: Odinsea.Game.Events.OxQuiz.unreset(event) defp unreset_event(event, :snowball), do: Odinsea.Game.Events.Snowball.unreset(event) defp unreset_event(event, :survival), do: Odinsea.Game.Events.Survival.unreset(event) defp unreset_event(event, _), do: event defp start_event_impl(event, :coconut), do: Odinsea.Game.Events.Coconut.start_event(event) defp start_event_impl(event, :fitness), do: Odinsea.Game.Events.Fitness.start_event(event) defp start_event_impl(event, :ola_ola), do: Odinsea.Game.Events.OlaOla.start_event(event) defp start_event_impl(event, :ox_quiz), do: Odinsea.Game.Events.OxQuiz.start_event(event) defp start_event_impl(event, :snowball), do: Odinsea.Game.Events.Snowball.start_event(event) defp start_event_impl(event, :survival), do: Odinsea.Game.Events.Survival.start_event(event) defp start_event_impl(event, _), do: event defp finished_event(event, :coconut, character), do: Odinsea.Game.Events.Coconut.finished(event, character) defp finished_event(event, :fitness, character), do: Odinsea.Game.Events.Fitness.finished(event, character) defp finished_event(event, :ola_ola, character), do: Odinsea.Game.Events.OlaOla.finished(event, character) defp finished_event(event, :ox_quiz, character), do: Odinsea.Game.Events.OxQuiz.finished(event, character) defp finished_event(event, :snowball, character), do: Odinsea.Game.Events.Snowball.finished(event, character) defp finished_event(event, :survival, character), do: Odinsea.Game.Events.Survival.finished(event, character) defp finished_event(_, _, _), do: :ok defp on_map_load_event(event, :coconut, character), do: Odinsea.Game.Events.Coconut.on_map_load(event, character) defp on_map_load_event(event, :fitness, character), do: Odinsea.Game.Events.Fitness.on_map_load(event, character) defp on_map_load_event(event, :ola_ola, character), do: Odinsea.Game.Events.OlaOla.on_map_load(event, character) defp on_map_load_event(event, :ox_quiz, character), do: Odinsea.Game.Events.OxQuiz.on_map_load(event, character) defp on_map_load_event(event, :snowball, character), do: Odinsea.Game.Events.Snowball.on_map_load(event, character) defp on_map_load_event(event, :survival, character), do: Odinsea.Game.Events.Survival.on_map_load(event, character) defp on_map_load_event(_, _, _), do: :ok defp put_event_base(event, :coconut, base), do: %{event | base: base} defp put_event_base(event, :fitness, base), do: %{event | base: base} defp put_event_base(event, :ola_ola, base), do: %{event | base: base} defp put_event_base(event, :ox_quiz, base), do: %{event | base: base} defp put_event_base(event, :snowball, base), do: %{event | base: base} defp put_event_base(event, :survival, base), do: %{event | base: base} defp put_event_base(event, _, _), do: event defp schedule_auto_start(channel_id, event_type) do EventTimer.schedule( fn -> send(__MODULE__, {:auto_start, channel_id, event_type}) end, 30_000 # 30 seconds ) broadcast_server_notice(channel_id, "The event will start in 30 seconds!") end defp broadcast_event_notice(channel_id, event_type) do event_name = Events.display_name(event_type) broadcast_server_notice( channel_id, "Hello! Let's play a #{event_name} event in channel #{channel_id}! " <> "Change to channel #{channel_id} and use @event command!" ) end defp broadcast_server_notice(channel_id, message) do # In real implementation, broadcast to channel Logger.info("[Channel #{channel_id}] #{message}") end end