Implement Phase 3: Storage system with slot filtering

- Add StorageHelper.cs for creating filtered storage items
- Create Card Binder (9 slots) and Card Box (36 slots)
- Slots use requireTags to only accept TradingCard tagged items
- Update debug spawn (F9) to cycle through cards and storage items
- Move initialization from Start() to Awake() to fix save/load crash
- Add safety patch for missing item warnings
- Items now persist correctly across game saves

Storage items clone base item 1255 which has slot support, then
configure slots with TradingCard tag requirement.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Cal Corum 2025-11-19 17:12:03 -06:00
parent 6536a7126a
commit 0d9ab65422
3 changed files with 277 additions and 70 deletions

View File

@ -20,29 +20,36 @@ namespace TradingCardMod
// Base game item ID to clone (135 is commonly used for collectibles)
private const int BASE_ITEM_ID = 135;
// Storage item IDs (high range to avoid conflicts)
private const int BINDER_ITEM_ID = 200001;
private const int CARD_BOX_ITEM_ID = 200002;
private string _modPath = string.Empty;
private List<TradingCard> _loadedCards = new List<TradingCard>();
private List<Item> _registeredItems = new List<Item>();
private List<GameObject> _createdGameObjects = new List<GameObject>();
private Tag? _tradingCardTag;
private Item? _binderItem;
private Item? _cardBoxItem;
// Debug: track if we've spawned test items
// Debug: track spawn cycling
private int _debugSpawnIndex = 0;
private List<Item> _allSpawnableItems = new List<Item>();
/// <summary>
/// Called when the mod is loaded. Initialize the card system here.
/// Called when the GameObject is created. Initialize early to register items before saves load.
/// </summary>
void Start()
void Awake()
{
_instance = this;
// Get the mod's directory path
_modPath = Path.GetDirectoryName(GetType().Assembly.Location) ?? string.Empty;
Debug.Log("[TradingCardMod] Mod initialized!");
Debug.Log("[TradingCardMod] Mod awakening (early init)...");
Debug.Log($"[TradingCardMod] Mod path: {_modPath}");
// Apply Harmony patches
// Apply Harmony patches FIRST - before anything else
Patches.ApplyPatches();
try
@ -50,8 +57,18 @@ namespace TradingCardMod
// Create our custom tag first
_tradingCardTag = TagHelper.GetOrCreateTradingCardTag();
// Load and register cards
// Load and register cards - do this early so saves can load them
LoadCardSets();
// Create storage items
CreateStorageItems();
// Build spawnable items list (cards + storage)
_allSpawnableItems.AddRange(_registeredItems);
if (_binderItem != null) _allSpawnableItems.Add(_binderItem);
if (_cardBoxItem != null) _allSpawnableItems.Add(_cardBoxItem);
Debug.Log("[TradingCardMod] Mod initialized successfully!");
}
catch (Exception ex)
{
@ -84,7 +101,41 @@ namespace TradingCardMod
Debug.Log($"[TradingCardMod] Total cards loaded: {_loadedCards.Count}");
Debug.Log($"[TradingCardMod] Total items registered: {_registeredItems.Count}");
Debug.Log("[TradingCardMod] DEBUG: Press F9 to spawn a trading card!");
Debug.Log("[TradingCardMod] DEBUG: Press F9 to spawn items (cycles through cards, then binder, then box)");
}
/// <summary>
/// Creates storage items (binder and card box) for holding trading cards.
/// </summary>
private void CreateStorageItems()
{
if (_tradingCardTag == null)
{
Debug.LogError("[TradingCardMod] Cannot create storage items - TradingCard tag not created!");
return;
}
// Create Card Binder (9 slots = 3x3 grid)
_binderItem = StorageHelper.CreateCardStorage(
BINDER_ITEM_ID,
"Card Binder",
"A binder for storing and organizing trading cards. Holds 9 cards.",
9,
0.5f, // weight
500, // value
_tradingCardTag
);
// Create Card Box (36 slots = bulk storage)
_cardBoxItem = StorageHelper.CreateCardStorage(
CARD_BOX_ITEM_ID,
"Card Box",
"A large box for bulk storage of trading cards. Holds 36 cards.",
36,
2.0f, // weight
1500, // value
_tradingCardTag
);
}
/// <summary>
@ -92,37 +143,37 @@ namespace TradingCardMod
/// </summary>
void Update()
{
// Debug: Press F9 to spawn a card
// Debug: Press F9 to spawn an item
if (Input.GetKeyDown(KeyCode.F9))
{
SpawnDebugCard();
SpawnDebugItem();
}
}
/// <summary>
/// Spawns a trading card for testing purposes.
/// Spawns items for testing - cycles through cards, then storage items.
/// </summary>
private void SpawnDebugCard()
private void SpawnDebugItem()
{
if (_registeredItems.Count == 0)
if (_allSpawnableItems.Count == 0)
{
Debug.LogWarning("[TradingCardMod] No cards registered to spawn!");
Debug.LogWarning("[TradingCardMod] No items registered to spawn!");
return;
}
// Cycle through registered cards
Item cardToSpawn = _registeredItems[_debugSpawnIndex % _registeredItems.Count];
// Cycle through all spawnable items
Item itemToSpawn = _allSpawnableItems[_debugSpawnIndex % _allSpawnableItems.Count];
_debugSpawnIndex++;
try
{
// Use game's utility to give item to player
ItemUtilities.SendToPlayer(cardToSpawn);
Debug.Log($"[TradingCardMod] Spawned card: {cardToSpawn.DisplayName} (ID: {cardToSpawn.TypeID})");
ItemUtilities.SendToPlayer(itemToSpawn);
Debug.Log($"[TradingCardMod] Spawned: {itemToSpawn.DisplayName} (ID: {itemToSpawn.TypeID})");
}
catch (Exception ex)
{
Debug.LogError($"[TradingCardMod] Failed to spawn card: {ex.Message}");
Debug.LogError($"[TradingCardMod] Failed to spawn item: {ex.Message}");
}
}
@ -386,10 +437,14 @@ namespace TradingCardMod
}
_createdGameObjects.Clear();
// Clean up storage items
StorageHelper.Cleanup();
// Clean up tags
TagHelper.Cleanup();
_loadedCards.Clear();
_allSpawnableItems.Clear();
Debug.Log("[TradingCardMod] Cleanup complete.");
}

View File

@ -54,65 +54,24 @@ namespace TradingCardMod
}
// ==========================================================================
// Example Harmony Patches
// ==========================================================================
//
// Below are example patches showing common patterns. Uncomment and modify
// as needed once you've identified the game methods to patch.
//
// To find methods to patch, use a decompiler (ILSpy) on the game DLLs in:
// Duckov_Data/Managed/
// Safety Patches - Prevent crashes from missing mod items
// ==========================================================================
/*
/// <summary>
/// Example: Postfix patch that runs after a method completes.
/// Use case: Log when items are added to inventory, modify return values.
/// Patch to prevent crashes when loading saves with mod items that aren't registered yet.
/// Logs a warning for missing mod items instead of letting the game crash.
/// </summary>
[HarmonyPatch(typeof(ItemStatsSystem.ItemUtilities), "SendToPlayer")]
public static class SendToPlayer_Patch
[HarmonyPatch(typeof(ItemStatsSystem.ItemAssetsCollection), "GetPrefab", new Type[] { typeof(int) })]
public static class GetPrefab_SafetyPatch
{
[HarmonyPostfix]
public static void Postfix(ItemStatsSystem.Item item)
public static void Postfix(int typeID, ItemStatsSystem.Item __result)
{
// Check if the item is one of our trading cards
// This runs after the original method completes
Debug.Log($"[TradingCardMod] Item sent to player: {item.name}");
// Check if this TypeID is in our mod's range and wasn't found
if (typeID >= 100000 && __result == null)
{
Debug.LogWarning($"[TradingCardMod] Item TypeID {typeID} not found. Item was likely saved before mod loaded. It will be lost.");
}
}
}
/// <summary>
/// Example: Prefix patch that runs before a method.
/// Use case: Modify parameters, skip original method, validate inputs.
/// </summary>
[HarmonyPatch(typeof(SomeClass), "SomeMethod")]
public static class SomeMethod_Patch
{
[HarmonyPrefix]
public static bool Prefix(ref int someParameter)
{
// Modify parameter before original method runs
someParameter = someParameter * 2;
// Return true to run original method, false to skip it
return true;
}
}
/// <summary>
/// Example: Transpiler patch that modifies IL instructions.
/// Use case: Complex modifications, inserting code mid-method.
/// Note: Advanced technique - prefer Prefix/Postfix when possible.
/// </summary>
[HarmonyPatch(typeof(SomeClass), "SomeMethod")]
public static class SomeMethod_Transpiler
{
[HarmonyTranspiler]
public static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions)
{
// Modify and return the IL instructions
return instructions;
}
}
*/
}

193
src/StorageHelper.cs Normal file
View File

@ -0,0 +1,193 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using UnityEngine;
using ItemStatsSystem;
using ItemStatsSystem.Items;
using Duckov.Utilities;
using SodaCraft.Localizations;
namespace TradingCardMod
{
/// <summary>
/// Helper class for creating storage items (binders, card boxes) with slot filtering.
/// </summary>
public static class StorageHelper
{
// Base item ID that has slots (used as template for storage items)
private const int SLOTTED_ITEM_BASE_ID = 1255;
// Track created storage items for cleanup
private static readonly List<Item> _createdStorageItems = new List<Item>();
private static readonly List<GameObject> _createdGameObjects = new List<GameObject>();
/// <summary>
/// Creates a card binder item with slots that only accept TradingCard tagged items.
/// </summary>
/// <param name="itemId">Unique ID for this storage item</param>
/// <param name="displayName">Display name shown to player</param>
/// <param name="description">Item description</param>
/// <param name="slotCount">Number of card slots</param>
/// <param name="weight">Item weight</param>
/// <param name="value">Item value</param>
/// <param name="tradingCardTag">The TradingCard tag for filtering</param>
/// <param name="icon">Optional custom icon sprite</param>
/// <returns>The created Item, or null on failure</returns>
public static Item? CreateCardStorage(
int itemId,
string displayName,
string description,
int slotCount,
float weight,
int value,
Tag tradingCardTag,
Sprite? icon = null)
{
try
{
// Get base item with slots
Item original = ItemAssetsCollection.GetPrefab(SLOTTED_ITEM_BASE_ID);
if (original == null)
{
Debug.LogError($"[TradingCardMod] Base slotted item ID {SLOTTED_ITEM_BASE_ID} not found!");
return null;
}
// Clone the item
GameObject clone = UnityEngine.Object.Instantiate(original.gameObject);
clone.name = $"CardStorage_{itemId}";
UnityEngine.Object.DontDestroyOnLoad(clone);
_createdGameObjects.Add(clone);
Item item = clone.GetComponent<Item>();
if (item == null)
{
Debug.LogError("[TradingCardMod] Cloned storage object has no Item component!");
return null;
}
// Set basic properties
string locKey = $"TC_Storage_{itemId}";
item.SetPrivateField("typeID", itemId);
item.SetPrivateField("weight", weight);
item.SetPrivateField("value", value);
item.SetPrivateField("displayName", locKey);
item.SetPrivateField("quality", 3); // Uncommon quality
item.SetPrivateField("order", 0);
item.SetPrivateField("maxStackCount", 1);
// Set display quality
item.DisplayQuality = (DisplayQuality)3;
// Set tags - storage items should be tools
item.Tags.Clear();
Tag? toolTag = TagHelper.GetTargetTag("Tool");
if (toolTag != null)
{
item.Tags.Add(toolTag);
}
// Configure slots to only accept TradingCard tagged items
ConfigureCardSlots(item, tradingCardTag, slotCount);
// Set icon if provided
if (icon != null)
{
item.SetPrivateField("icon", icon);
}
// Set localization
LocalizationManager.SetOverrideText(locKey, displayName);
LocalizationManager.SetOverrideText($"{locKey}_Desc", description);
// Register with game
if (ItemAssetsCollection.AddDynamicEntry(item))
{
_createdStorageItems.Add(item);
Debug.Log($"[TradingCardMod] Registered storage: {displayName} (ID: {itemId}, Slots: {slotCount})");
return item;
}
else
{
Debug.LogError($"[TradingCardMod] Failed to register storage {displayName}!");
UnityEngine.Object.Destroy(clone);
_createdGameObjects.Remove(clone);
return null;
}
}
catch (Exception ex)
{
Debug.LogError($"[TradingCardMod] Error creating storage {displayName}: {ex.Message}");
return null;
}
}
/// <summary>
/// Configures an item's slots to only accept items with a specific tag.
/// </summary>
private static void ConfigureCardSlots(Item item, Tag requiredTag, int slotCount)
{
// Get template slot info if available
Slot templateSlot = new Slot();
if (item.Slots.Count > 0)
{
templateSlot = item.Slots[0];
}
// Clear existing slots
item.Slots.Clear();
// Create new slots with tag filtering
for (int i = 0; i < slotCount; i++)
{
Slot newSlot = new Slot(templateSlot.Key);
newSlot.SlotIcon = templateSlot.SlotIcon;
// Set unique key for each slot
typeof(Slot).GetField("key", BindingFlags.Instance | BindingFlags.NonPublic)
?.SetValue(newSlot, $"CardSlot{i}");
// Add tag requirement - only TradingCard items can go in
newSlot.requireTags.Add(requiredTag);
item.Slots.Add(newSlot);
}
Debug.Log($"[TradingCardMod] Configured {slotCount} slots with TradingCard filter");
}
/// <summary>
/// Gets all storage items created by this helper.
/// </summary>
public static IReadOnlyList<Item> GetCreatedStorageItems()
{
return _createdStorageItems.AsReadOnly();
}
/// <summary>
/// Cleans up all storage items created by this helper.
/// </summary>
public static void Cleanup()
{
foreach (var item in _createdStorageItems)
{
if (item != null)
{
ItemAssetsCollection.RemoveDynamicEntry(item);
}
}
_createdStorageItems.Clear();
foreach (var go in _createdGameObjects)
{
if (go != null)
{
UnityEngine.Object.Destroy(go);
}
}
_createdGameObjects.Clear();
Debug.Log("[TradingCardMod] StorageHelper cleaned up.");
}
}
}