defmodule Odinsea.Game.ReactorFactory do @moduledoc """ Reactor Factory - loads and caches reactor template data. This module loads reactor metadata (states, types, items, timeouts) from cached JSON files. The JSON files should be exported from the Java server's WZ data providers. Reactor data is cached in ETS for fast lookups. Ported from Java: src/server/maps/MapleReactorFactory.java """ use GenServer require Logger alias Odinsea.Game.{Reactor, ReactorStats} # ETS table name @reactor_stats :odinsea_reactor_stats # Data file path @reactor_data_file "data/reactors.json" ## Public API @doc "Starts the ReactorFactory GenServer" def start_link(opts \\ []) do GenServer.start_link(__MODULE__, opts, name: __MODULE__) end @doc """ Gets reactor stats by reactor ID. Returns nil if not found. """ @spec get_reactor_stats(integer()) :: ReactorStats.t() | nil def get_reactor_stats(reactor_id) do case :ets.lookup(@reactor_stats, reactor_id) do [{^reactor_id, stats}] -> stats [] -> nil end end @doc """ Gets a reactor instance by ID. Returns nil if stats not found. """ @spec get_reactor(integer()) :: Reactor.t() | nil def get_reactor(reactor_id) do case get_reactor_stats(reactor_id) do nil -> nil stats -> Reactor.new(reactor_id, stats) end end @doc """ Creates a reactor instance with position and properties. """ @spec create_reactor(integer(), integer(), integer(), integer(), String.t(), integer()) :: Reactor.t() | nil def create_reactor(reactor_id, x, y, facing_direction \\ 0, name \\ "", delay \\ -1) do case get_reactor_stats(reactor_id) do nil -> Logger.warning("Reactor stats not found for reactor_id=#{reactor_id}") nil stats -> %Reactor{ reactor_id: reactor_id, stats: stats, x: x, y: y, facing_direction: facing_direction, name: name, delay: delay, state: 0, alive: true, timer_active: false, custom: false } end end @doc """ Checks if reactor stats exist. """ @spec reactor_exists?(integer()) :: boolean() def reactor_exists?(reactor_id) do :ets.member(@reactor_stats, reactor_id) end @doc """ Gets all loaded reactor IDs. """ @spec get_all_reactor_ids() :: [integer()] def get_all_reactor_ids do :ets.select(@reactor_stats, [{{:"$1", :_}, [], [:"$1"]}]) end @doc """ Gets the number of loaded reactors. """ @spec get_reactor_count() :: integer() def get_reactor_count do :ets.info(@reactor_stats, :size) end @doc """ Reloads reactor data from files. """ def reload do GenServer.call(__MODULE__, :reload, :infinity) end ## GenServer Callbacks @impl true def init(_opts) do # Create ETS table :ets.new(@reactor_stats, [:set, :public, :named_table, read_concurrency: true]) # Load data load_reactor_data() {:ok, %{}} end @impl true def handle_call(:reload, _from, state) do Logger.info("Reloading reactor data...") load_reactor_data() {:reply, :ok, state} end ## Private Functions defp load_reactor_data do priv_dir = :code.priv_dir(:odinsea) |> to_string() file_path = Path.join(priv_dir, @reactor_data_file) load_reactors_from_file(file_path) count = :ets.info(@reactor_stats, :size) Logger.info("Loaded #{count} reactor templates") end defp load_reactors_from_file(file_path) do case File.read(file_path) do {:ok, content} -> case Jason.decode(content) do {:ok, reactors} when is_map(reactors) -> # Clear existing data :ets.delete_all_objects(@reactor_stats) # Load reactors and handle links links = process_reactors(reactors, %{}) # Resolve links resolve_links(links) :ok {:error, reason} -> Logger.warning("Failed to parse reactors JSON: #{inspect(reason)}") create_fallback_reactors() end {:error, :enoent} -> Logger.warning("Reactors file not found: #{file_path}, using fallback data") create_fallback_reactors() {:error, reason} -> Logger.error("Failed to read reactors: #{inspect(reason)}") create_fallback_reactors() end end defp process_reactors(reactors, links) do Enum.reduce(reactors, links, fn {reactor_id_str, reactor_data}, acc_links -> reactor_id = String.to_integer(reactor_id_str) # Check if this is a link to another reactor link_target = reactor_data["link"] if link_target && link_target > 0 do # Store link for later resolution Map.put(acc_links, reactor_id, link_target) else # Build stats from data stats = ReactorStats.from_json(reactor_data) :ets.insert(@reactor_stats, {reactor_id, stats}) acc_links end end) end defp resolve_links(links) do Enum.each(links, fn {reactor_id, target_id} -> case :ets.lookup(@reactor_stats, target_id) do [{^target_id, target_stats}] -> # Copy target stats for linked reactor :ets.insert(@reactor_stats, {reactor_id, target_stats}) [] -> Logger.warning("Link target not found: #{target_id} for reactor #{reactor_id}") end end) end # Fallback data for basic testing defp create_fallback_reactors do # Common reactors from MapleStory fallback_reactors = [ %{ reactor_id: 100000, # Normal box states: %{ "0" => %{type: 999, next_state: -1, timeout: -1, can_touch: 0}, "1" => %{type: 999, next_state: -1, timeout: -1, can_touch: 0} } }, %{ reactor_id: 200000, # Herb activate_by_touch: true, states: %{ "0" => %{type: 2, next_state: 1, timeout: -1, can_touch: 2}, "1" => %{type: 999, next_state: -1, timeout: -1, can_touch: 0} } }, %{ reactor_id: 200100, # Vein activate_by_touch: true, states: %{ "0" => %{type: 2, next_state: 1, timeout: -1, can_touch: 2}, "1" => %{type: 999, next_state: -1, timeout: -1, can_touch: 0} } }, %{ reactor_id: 200200, # Gold Flower activate_by_touch: true, states: %{ "0" => %{type: 2, next_state: 1, timeout: -1, can_touch: 2}, "1" => %{type: 999, next_state: -1, timeout: -1, can_touch: 0} } }, %{ reactor_id: 200300, # Silver Flower activate_by_touch: true, states: %{ "0" => %{type: 2, next_state: 1, timeout: -1, can_touch: 2}, "1" => %{type: 999, next_state: -1, timeout: -1, can_touch: 0} } }, %{ reactor_id: 100011, # Mysterious Herb activate_by_touch: true, states: %{ "0" => %{type: 2, next_state: 1, timeout: -1, can_touch: 2}, "1" => %{type: 999, next_state: -1, timeout: -1, can_touch: 0} } }, %{ reactor_id: 200011, # Mysterious Vein activate_by_touch: true, states: %{ "0" => %{type: 2, next_state: 1, timeout: -1, can_touch: 2}, "1" => %{type: 999, next_state: -1, timeout: -1, can_touch: 0} } } ] Enum.each(fallback_reactors, fn reactor_data -> stats = ReactorStats.from_json(reactor_data) :ets.insert(@reactor_stats, {reactor_data.reactor_id, stats}) end) Logger.info("Created #{length(fallback_reactors)} fallback reactor templates") end end