368 lines
10 KiB
Elixir
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
|