Files
odinsea-elixir/lib/odinsea/game/reactor_factory.ex
2026-02-14 23:12:33 -07:00

277 lines
7.6 KiB
Elixir

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