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

368 lines
10 KiB
Elixir

defmodule Odinsea.Shop.CashItemFactory do
@moduledoc """
Cash Item Factory - loads and caches cash shop item data.
This module loads cash shop item data from JSON files and caches it in ETS
for fast lookups. Ported from server/CashItemFactory.java.
Data sources:
- cash_items.json: Base item definitions (from WZ Commodity.img)
- cash_packages.json: Package item definitions
- cash_mods.json: Modified item info (from database)
"""
use GenServer
require Logger
alias Odinsea.Shop.CashItem
# ETS table names
@item_cache :odinsea_cash_items
@package_cache :odinsea_cash_packages
@category_cache :odinsea_cash_categories
# Data file paths (relative to priv directory)
@items_file "data/cash_items.json"
@packages_file "data/cash_packages.json"
@categories_file "data/cash_categories.json"
@mods_file "data/cash_mods.json"
# Best items (featured items for the main page)
@best_items [
100_030_55,
100_030_90,
101_034_64,
100_029_60,
101_033_63
]
## Public API
@doc "Starts the CashItemFactory GenServer"
def start_link(opts \\ []) do
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
end
@doc "Gets a cash item by SN (serial number)"
@spec get_item(integer()) :: CashItem.t() | nil
def get_item(sn) do
case :ets.lookup(@item_cache, sn) do
[{^sn, item}] -> item
[] -> nil
end
end
@doc "Gets a simple item by SN (without modification check)"
@spec get_simple_item(integer()) :: CashItem.t() | nil
def get_simple_item(sn) do
get_item(sn)
end
@doc "Gets all items in a package by item ID"
@spec get_package_items(integer()) :: [integer()] | nil
def get_package_items(item_id) do
case :ets.lookup(@package_cache, item_id) do
[{^item_id, items}] -> items
[] -> nil
end
end
@doc "Gets all cash items"
@spec get_all_items() :: [CashItem.t()]
def get_all_items do
:ets.select(@item_cache, [{{:_, :"$1"}, [], [:"$1"]}])
end
@doc "Gets items that are currently on sale"
@spec get_sale_items() :: [CashItem.t()]
def get_sale_items do
get_all_items()
|> Enum.filter(& &1.on_sale)
end
@doc "Gets items by category"
@spec get_items_by_category(integer()) :: [CashItem.t()]
def get_items_by_category(category_id) do
# Filter items by category - simplified implementation
# Full implementation would check category mappings
get_all_items()
|> Enum.filter(fn item ->
# Check if item belongs to category based on item_id
# This is a simplified check
case category_id do
1 -> item.item_id >= 5_000_000 && item.item_id < 5_010_000
2 -> item.item_id >= 5_100_000 && item.item_id < 5_110_000
3 -> item.item_id >= 1_700_000 && item.item_id < 1_800_000
_ -> true
end
end)
end
@doc "Gets the best/featured items"
@spec get_best_items() :: [integer()]
def get_best_items do
@best_items
end
@doc "Gets all categories"
@spec get_categories() :: [map()]
def get_categories do
:ets.select(@category_cache, [{{:_, :"$1"}, [], [:"$1"]}])
end
@doc "Gets a category by ID"
@spec get_category(integer()) :: map() | nil
def get_category(category_id) do
case :ets.lookup(@category_cache, category_id) do
[{^category_id, cat}] -> cat
[] -> nil
end
end
@doc "Checks if an item is blocked from cash shop purchase"
@spec blocked?(integer()) :: boolean()
def blocked?(item_id) do
# List of blocked item IDs (hacks, exploits, etc.)
blocked_ids = [
# Add specific blocked item IDs here
]
item_id in blocked_ids
end
@doc "Checks if an item should be ignored (weapon skins, etc.)"
@spec ignore_weapon?(integer()) :: boolean()
def ignore_weapon?(item_id) do
# Ignore certain weapon skin items
false
end
@doc "Reloads cash item data from files"
@spec reload() :: :ok
def reload do
GenServer.call(__MODULE__, :reload, :infinity)
end
@doc "Generates random featured items"
@spec generate_featured() :: [integer()]
def generate_featured do
# Get all on-sale items
sale_items =
get_all_items()
|> Enum.filter(& &1.on_sale)
|> Enum.map(& &1.item_id)
# Return random selection or defaults
if length(sale_items) > 10 do
sale_items
|> Enum.shuffle()
|> Enum.take(10)
else
@best_items
end
end
## GenServer Callbacks
@impl true
def init(_opts) do
# Create ETS tables
:ets.new(@item_cache, [:set, :public, :named_table, read_concurrency: true])
:ets.new(@package_cache, [:set, :public, :named_table, read_concurrency: true])
:ets.new(@category_cache, [:set, :public, :named_table, read_concurrency: true])
# Load data
load_cash_data()
{:ok, %{}}
end
@impl true
def handle_call(:reload, _from, state) do
Logger.info("Reloading cash shop data...")
load_cash_data()
{:reply, :ok, state}
end
## Private Functions
defp load_cash_data do
priv_dir = :code.priv_dir(:odinsea) |> to_string()
load_categories(Path.join(priv_dir, @categories_file))
load_items(Path.join(priv_dir, @items_file))
load_packages(Path.join(priv_dir, @packages_file))
load_modifications(Path.join(priv_dir, @mods_file))
item_count = :ets.info(@item_cache, :size)
Logger.info("Loaded #{item_count} cash shop items")
end
defp load_categories(file_path) do
case File.read(file_path) do
{:ok, content} ->
case Jason.decode(content, keys: :atoms) do
{:ok, categories} when is_list(categories) ->
Enum.each(categories, fn cat ->
id = Map.get(cat, :id)
if id, do: :ets.insert(@category_cache, {id, cat})
end)
{:error, reason} ->
Logger.warn("Failed to parse categories JSON: #{inspect(reason)}")
create_fallback_categories()
end
{:error, :enoent} ->
Logger.debug("Categories file not found: #{file_path}, using fallback")
create_fallback_categories()
{:error, reason} ->
Logger.error("Failed to read categories: #{inspect(reason)}")
create_fallback_categories()
end
end
defp load_items(file_path) do
case File.read(file_path) do
{:ok, content} ->
case Jason.decode(content, keys: :atoms) do
{:ok, items} when is_list(items) ->
Enum.each(items, fn item_data ->
item = CashItem.new(item_data)
if item.sn > 0 do
:ets.insert(@item_cache, {item.sn, item})
end
end)
{:error, reason} ->
Logger.warn("Failed to parse cash items JSON: #{inspect(reason)}")
create_fallback_items()
end
{:error, :enoent} ->
Logger.warn("Cash items file not found: #{file_path}, using fallback data")
create_fallback_items()
{:error, reason} ->
Logger.error("Failed to read cash items: #{inspect(reason)}")
create_fallback_items()
end
end
defp load_packages(file_path) do
case File.read(file_path) do
{:ok, content} ->
case Jason.decode(content, keys: :atoms) do
{:ok, packages} when is_list(packages) ->
Enum.each(packages, fn pkg ->
item_id = Map.get(pkg, :item_id)
items = Map.get(pkg, :items, [])
if item_id do
:ets.insert(@package_cache, {item_id, items})
end
end)
{:error, reason} ->
Logger.warn("Failed to parse packages JSON: #{inspect(reason)}")
end
{:error, :enoent} ->
Logger.debug("Packages file not found: #{file_path}")
{:error, reason} ->
Logger.error("Failed to read packages: #{inspect(reason)}")
end
end
defp load_modifications(file_path) do
case File.read(file_path) do
{:ok, content} ->
case Jason.decode(content, keys: :atoms) do
{:ok, mods} when is_list(mods) ->
Enum.each(mods, fn mod ->
sn = Map.get(mod, :sn)
if sn do
# Get existing item and apply modifications
case :ets.lookup(@item_cache, sn) do
[{^sn, item}] ->
modified = CashItem.apply_mods(item, mod)
:ets.insert(@item_cache, {sn, modified})
[] ->
# Create new item from modification data
item = CashItem.new(mod)
:ets.insert(@item_cache, {sn, item})
end
end
end)
{:error, reason} ->
Logger.warn("Failed to parse mods JSON: #{inspect(reason)}")
end
{:error, :enoent} ->
Logger.debug("Modifications file not found: #{file_path}")
{:error, reason} ->
Logger.error("Failed to read modifications: #{inspect(reason)}")
end
end
# Fallback data for basic testing
defp create_fallback_categories do
categories = [
%{id: 1, name: "Pets", category: 1, sub_category: 0, discount_rate: 0},
%{id: 2, name: "Pet Food", category: 2, sub_category: 0, discount_rate: 0},
%{id: 3, name: "Weapons", category: 3, sub_category: 0, discount_rate: 0},
%{id: 4, name: "Equipment", category: 4, sub_category: 0, discount_rate: 0},
%{id: 5, name: "Effects", category: 5, sub_category: 0, discount_rate: 0}
]
Enum.each(categories, fn cat ->
:ets.insert(@category_cache, {cat.id, cat})
end)
end
defp create_fallback_items do
# Basic cash items for testing
items = [
%{
sn: 1_000_000,
item_id: 5_000_000,
price: 9_000,
count: 1,
period: 90,
gender: 2,
on_sale: true
},
%{
sn: 1_000_001,
item_id: 5_000_001,
price: 9_000,
count: 1,
period: 90,
gender: 2,
on_sale: true
},
%{
sn: 1_000_002,
item_id: 5_001_000,
price: 2_400,
count: 1,
period: 0,
gender: 2,
on_sale: true
}
]
Enum.each(items, fn item_data ->
item = CashItem.new(item_data)
:ets.insert(@item_cache, {item.sn, item})
end)
end
end