277 lines
7.6 KiB
Elixir
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
|