kimi gone wild
This commit is contained in:
367
lib/odinsea/shop/cash_item_factory.ex
Normal file
367
lib/odinsea/shop/cash_item_factory.ex
Normal file
@@ -0,0 +1,367 @@
|
||||
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
|
||||
Reference in New Issue
Block a user