diff --git a/src/ModBehaviour.cs b/src/ModBehaviour.cs index d183470..fb5a60b 100644 --- a/src/ModBehaviour.cs +++ b/src/ModBehaviour.cs @@ -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 _loadedCards = new List(); private List _registeredItems = new List(); private List _createdGameObjects = new List(); 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 _allSpawnableItems = new List(); /// - /// 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. /// - 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)"); + } + + /// + /// Creates storage items (binder and card box) for holding trading cards. + /// + 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 + ); } /// @@ -92,37 +143,37 @@ namespace TradingCardMod /// void Update() { - // Debug: Press F9 to spawn a card + // Debug: Press F9 to spawn an item if (Input.GetKeyDown(KeyCode.F9)) { - SpawnDebugCard(); + SpawnDebugItem(); } } /// - /// Spawns a trading card for testing purposes. + /// Spawns items for testing - cycles through cards, then storage items. /// - 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."); } diff --git a/src/Patches.cs b/src/Patches.cs index dc3c31c..9e649fd 100644 --- a/src/Patches.cs +++ b/src/Patches.cs @@ -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 // ========================================================================== - /* /// - /// 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. /// - [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."); + } } } - - /// - /// Example: Prefix patch that runs before a method. - /// Use case: Modify parameters, skip original method, validate inputs. - /// - [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; - } - } - - /// - /// Example: Transpiler patch that modifies IL instructions. - /// Use case: Complex modifications, inserting code mid-method. - /// Note: Advanced technique - prefer Prefix/Postfix when possible. - /// - [HarmonyPatch(typeof(SomeClass), "SomeMethod")] - public static class SomeMethod_Transpiler - { - [HarmonyTranspiler] - public static IEnumerable Transpiler(IEnumerable instructions) - { - // Modify and return the IL instructions - return instructions; - } - } - */ } diff --git a/src/StorageHelper.cs b/src/StorageHelper.cs new file mode 100644 index 0000000..3a6c5b0 --- /dev/null +++ b/src/StorageHelper.cs @@ -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 +{ + /// + /// Helper class for creating storage items (binders, card boxes) with slot filtering. + /// + 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 _createdStorageItems = new List(); + private static readonly List _createdGameObjects = new List(); + + /// + /// Creates a card binder item with slots that only accept TradingCard tagged items. + /// + /// Unique ID for this storage item + /// Display name shown to player + /// Item description + /// Number of card slots + /// Item weight + /// Item value + /// The TradingCard tag for filtering + /// Optional custom icon sprite + /// The created Item, or null on failure + 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(); + 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; + } + } + + /// + /// Configures an item's slots to only accept items with a specific tag. + /// + 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"); + } + + /// + /// Gets all storage items created by this helper. + /// + public static IReadOnlyList GetCreatedStorageItems() + { + return _createdStorageItems.AsReadOnly(); + } + + /// + /// Cleans up all storage items created by this helper. + /// + 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."); + } + } +}