defmodule Odinsea.Game.MapFactory do @moduledoc """ Map Factory - loads and caches map templates and data. This module loads map metadata (portals, footholds, spawns, properties) from cached JSON files. The JSON files should be exported from the Java server's WZ data providers. Map templates are cached in ETS for fast lookups. """ use GenServer require Logger # ETS table names @map_templates :odinsea_map_templates @portal_cache :odinsea_portal_cache @foothold_cache :odinsea_foothold_cache # Data file paths @map_data_file "data/maps.json" @portal_data_file "data/portals.json" @foothold_data_file "data/footholds.json" defmodule Portal do @moduledoc "Represents a portal on a map" @type portal_type :: :spawn | :invisible | :visible | :collision | :changeable | :changeable_invisible | :town_portal_point | :script | :sp | :pi | :pv | :tp | :ps | :psi | :hidden @type t :: %__MODULE__{ id: integer(), name: String.t(), type: portal_type(), x: integer(), y: integer(), target_map: integer(), target_portal: String.t(), script: String.t() | nil } defstruct [ :id, :name, :type, :x, :y, :target_map, :target_portal, :script ] @doc "Converts portal type integer to atom" def type_from_int(type_int) do case type_int do 0 -> :spawn 1 -> :invisible 2 -> :visible 3 -> :collision 4 -> :changeable 5 -> :changeable_invisible 6 -> :town_portal_point 7 -> :script 8 -> :sp 9 -> :pi 10 -> :pv 11 -> :tp 12 -> :ps 13 -> :psi 14 -> :hidden _ -> :invisible end end @doc "Converts portal type string to atom" def type_from_string(type_str) do case type_str do "sp" -> :spawn "pi" -> :invisible "pv" -> :visible "pc" -> :collision "pg" -> :changeable "pgi" -> :changeable_invisible "tp" -> :town_portal_point "ps" -> :script "psi" -> :script "hidden" -> :hidden _ -> :invisible end end end defmodule Foothold do @moduledoc "Represents a foothold (platform) on a map" @type t :: %__MODULE__{ id: integer(), x1: integer(), y1: integer(), x2: integer(), y2: integer(), prev: integer(), next: integer() } defstruct [ :id, :x1, :y1, :x2, :y2, :prev, :next ] end defmodule SpawnPoint do @moduledoc "Represents a monster spawn point on a map" @type t :: %__MODULE__{ mob_id: integer(), x: integer(), y: integer(), fh: integer(), cy: integer(), f: integer(), mob_time: integer() } defstruct [ :mob_id, :x, :y, :fh, :cy, :f, :mob_time ] end defmodule ReactorSpawn do @moduledoc "Represents a reactor spawn point on a map" @type t :: %__MODULE__{ reactor_id: integer(), x: integer(), y: integer(), facing_direction: integer(), name: String.t(), delay: integer() } defstruct [ :reactor_id, :x, :y, facing_direction: 0, name: "", delay: 0 ] end defmodule FieldTemplate do @moduledoc "Map field template containing all map data" @type t :: %__MODULE__{ map_id: integer(), map_name: String.t(), street_name: String.t(), return_map: integer(), forced_return: integer(), mob_rate: float(), field_limit: integer(), time_limit: integer(), dec_hp: integer(), dec_hp_interval: integer(), portal_map: %{String.t() => Portal.t()}, portals: [Portal.t()], spawn_points: [SpawnPoint.t()], reactor_spawns: [ReactorSpawn.t()], footholds: [Foothold.t()], top: integer(), bottom: integer(), left: integer(), right: integer(), bgm: String.t(), first_user_enter: String.t(), user_enter: String.t(), clock: boolean(), everlast: boolean(), town: boolean(), mount_allowed: boolean(), recovery_rate: float(), create_mob_interval: integer(), fixed_mob_capacity: integer() } defstruct [ :map_id, :map_name, :street_name, :return_map, :forced_return, :mob_rate, :field_limit, :time_limit, :dec_hp, :dec_hp_interval, :portal_map, :portals, :spawn_points, :reactor_spawns, :footholds, :top, :bottom, :left, :right, :bgm, :first_user_enter, :user_enter, :clock, :everlast, :town, :mount_allowed, :recovery_rate, :create_mob_interval, :fixed_mob_capacity ] end ## Public API @doc "Starts the MapFactory GenServer" def start_link(opts \\ []) do GenServer.start_link(__MODULE__, opts, name: __MODULE__) end @doc "Gets map template by map ID" @spec get_map_template(integer()) :: FieldTemplate.t() | nil def get_map_template(map_id) do case :ets.lookup(@map_templates, map_id) do [{^map_id, template}] -> template [] -> nil end end @doc "Gets portal by name on a map" @spec get_portal(integer(), String.t()) :: Portal.t() | nil def get_portal(map_id, portal_name) do case get_map_template(map_id) do nil -> nil template -> Map.get(template.portal_map, portal_name) end end @doc "Gets random spawn portal on a map" @spec get_random_spawn_portal(integer()) :: Portal.t() | nil def get_random_spawn_portal(map_id) do case get_map_template(map_id) do nil -> nil template -> spawn_points = template.spawn_points if Enum.empty?(spawn_points) do nil else Enum.random(spawn_points) end end end @doc "Gets map name" @spec get_map_name(integer()) :: String.t() def get_map_name(map_id) do case get_map_template(map_id) do nil -> "UNKNOWN" template -> template.map_name || "UNKNOWN" end end @doc "Gets return map ID" @spec get_return_map(integer()) :: integer() def get_return_map(map_id) do case get_map_template(map_id) do nil -> 999_999_999 template -> template.return_map || 999_999_999 end end @doc "Checks if map exists" @spec map_exists?(integer()) :: boolean() def map_exists?(map_id) do :ets.member(@map_templates, map_id) end @doc "Gets all loaded map IDs" @spec get_all_map_ids() :: [integer()] def get_all_map_ids do :ets.select(@map_templates, [{{:"$1", :_}, [], [:"$1"]}]) end @doc "Reloads map data from files" def reload do GenServer.call(__MODULE__, :reload, :infinity) end ## GenServer Callbacks @impl true def init(_opts) do # Create ETS tables :ets.new(@map_templates, [:set, :public, :named_table, read_concurrency: true]) :ets.new(@portal_cache, [:set, :public, :named_table, read_concurrency: true]) :ets.new(@foothold_cache, [:set, :public, :named_table, read_concurrency: true]) # Load data load_map_data() {:ok, %{}} end @impl true def handle_call(:reload, _from, state) do Logger.info("Reloading map data...") load_map_data() {:reply, :ok, state} end ## Private Functions defp load_map_data do priv_dir = :code.priv_dir(:odinsea) |> to_string() # Load maps load_maps(Path.join(priv_dir, @map_data_file)) map_count = :ets.info(@map_templates, :size) Logger.info("Loaded #{map_count} map templates") end defp load_maps(file_path) do case File.read(file_path) do {:ok, content} -> case Jason.decode(content, keys: :atoms) do {:ok, maps} when is_list(maps) -> Enum.each(maps, fn map_data -> template = build_field_template(map_data) :ets.insert(@map_templates, {template.map_id, template}) end) {:error, reason} -> Logger.warn("Failed to parse maps JSON: #{inspect(reason)}") create_fallback_maps() end {:error, :enoent} -> Logger.warn("Maps file not found: #{file_path}, using fallback data") create_fallback_maps() {:error, reason} -> Logger.error("Failed to read maps: #{inspect(reason)}") create_fallback_maps() end end defp build_field_template(map_data) do # Parse portals portals = (map_data[:portals] || []) |> Enum.map(&build_portal/1) portal_map = portals |> Enum.map(fn portal -> {portal.name, portal} end) |> Enum.into(%{}) # Parse spawn points spawn_points = (map_data[:spawns] || []) |> Enum.map(&build_spawn_point/1) # Parse reactor spawns reactor_spawns = (map_data[:reactors] || []) |> Enum.map(&build_reactor_spawn/1) # Parse footholds footholds = (map_data[:footholds] || []) |> Enum.map(&build_foothold/1) %FieldTemplate{ map_id: map_data[:map_id], map_name: map_data[:map_name] || "", street_name: map_data[:street_name] || "", return_map: map_data[:return_map] || 999_999_999, forced_return: map_data[:forced_return] || 999_999_999, mob_rate: map_data[:mob_rate] || 1.0, field_limit: map_data[:field_limit] || 0, time_limit: map_data[:time_limit] || -1, dec_hp: map_data[:dec_hp] || 0, dec_hp_interval: map_data[:dec_hp_interval] || 10000, portal_map: portal_map, portals: portals, spawn_points: spawn_points, reactor_spawns: reactor_spawns, footholds: footholds, top: map_data[:top] || 0, bottom: map_data[:bottom] || 0, left: map_data[:left] || 0, right: map_data[:right] || 0, bgm: map_data[:bgm] || "", first_user_enter: map_data[:first_user_enter] || "", user_enter: map_data[:user_enter] || "", clock: map_data[:clock] || false, everlast: map_data[:everlast] || false, town: map_data[:town] || false, mount_allowed: map_data[:mount_allowed] || true, recovery_rate: map_data[:recovery_rate] || 1.0, create_mob_interval: map_data[:create_mob_interval] || 4000, fixed_mob_capacity: map_data[:fixed_mob_capacity] || 0 } end defp build_portal(portal_data) do type = cond do is_integer(portal_data[:type]) -> Portal.type_from_int(portal_data[:type]) is_binary(portal_data[:type]) -> Portal.type_from_string(portal_data[:type]) true -> :invisible end %Portal{ id: portal_data[:id] || 0, name: portal_data[:name] || "sp", type: type, x: portal_data[:x] || 0, y: portal_data[:y] || 0, target_map: portal_data[:target_map] || 999_999_999, target_portal: portal_data[:target_portal] || "", script: portal_data[:script] } end defp build_foothold(foothold_data) do %Foothold{ id: foothold_data[:id] || 0, x1: foothold_data[:x1] || 0, y1: foothold_data[:y1] || 0, x2: foothold_data[:x2] || 0, y2: foothold_data[:y2] || 0, prev: foothold_data[:prev] || 0, next: foothold_data[:next] || 0 } end defp build_spawn_point(spawn_data) do %SpawnPoint{ mob_id: spawn_data[:mob_id] || 0, x: spawn_data[:x] || 0, y: spawn_data[:y] || 0, fh: spawn_data[:fh] || 0, cy: spawn_data[:cy] || 0, f: spawn_data[:f] || 0, mob_time: spawn_data[:mob_time] || 10_000 } end defp build_reactor_spawn(reactor_data) do %ReactorSpawn{ reactor_id: reactor_data[:reactor_id] || reactor_data[:id] || 0, x: reactor_data[:x] || 0, y: reactor_data[:y] || 0, facing_direction: reactor_data[:f] || reactor_data[:facing_direction] || 0, name: reactor_data[:name] || "", delay: reactor_data[:reactor_time] || reactor_data[:delay] || 0 } end # Fallback data for basic testing defp create_fallback_maps do # Common beginner maps fallback_maps = [ # Maple Island - Southperry %{ map_id: 60000, map_name: "Southperry", street_name: "Maple Island", return_map: 60000, forced_return: 60000, portals: [ %{id: 0, name: "sp", type: "sp", x: 0, y: 0, target_map: 60000, target_portal: ""} ] }, # Victoria Island - Henesys %{ map_id: 100000000, map_name: "Henesys", street_name: "Victoria Island", return_map: 100000000, forced_return: 100000000, portals: [ %{id: 0, name: "sp", type: "sp", x: -1283, y: 86, target_map: 100000000, target_portal: ""} ] }, # Henesys Hunting Ground I - with monsters! %{ map_id: 100010000, map_name: "Henesys Hunting Ground I", street_name: "Victoria Island", return_map: 100000000, forced_return: 100000000, portals: [ %{id: 0, name: "sp", type: "sp", x: 0, y: 0, target_map: 100010000, target_portal: ""} ], spawns: [ # Blue Snails (mob_id: 100001) %{mob_id: 100001, x: -500, y: 100, fh: 0, cy: 0, f: 0, mob_time: 8000}, %{mob_id: 100001, x: -200, y: 100, fh: 0, cy: 0, f: 1, mob_time: 8000}, %{mob_id: 100001, x: 200, y: 100, fh: 0, cy: 0, f: 0, mob_time: 8000}, # Orange Mushrooms (mob_id: 1210102) %{mob_id: 1210102, x: 500, y: 100, fh: 0, cy: 0, f: 1, mob_time: 10000}, %{mob_id: 1210102, x: 800, y: 100, fh: 0, cy: 0, f: 0, mob_time: 10000} ] }, # Hidden Street - FM Entrance %{ map_id: 910000000, map_name: "Free Market Entrance", street_name: "Free Market", return_map: 100000000, forced_return: 100000000, portals: [ %{id: 0, name: "sp", type: "sp", x: 0, y: 0, target_map: 910000000, target_portal: ""} ] } ] Enum.each(fallback_maps, fn map_data -> template = build_field_template(map_data) :ets.insert(@map_templates, {template.map_id, template}) end) end end