From 8c7a131869ce089cc64cef88179a0e78b6e6a55c Mon Sep 17 00:00:00 2001 From: Cal Corum Date: Mon, 29 Dec 2025 08:58:07 -0600 Subject: [PATCH] Add disassembly support for storage items MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add DisassemblyHelper class for runtime disassembly formula management - Binder Sheet disassembles into 2x Polyethylene Sheet - Card Binder disassembles into 4x Polyethylene Sheet - Cards and packs intentionally have no disassembly (collectibles) - Document disassembly pattern in CLAUDE.md (struct boxing for reflection) - Fix .claude/settings.json permission patterns 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .claude/settings.json | 8 +- CLAUDE.md | 41 ++++++ src/DisassemblyHelper.cs | 293 +++++++++++++++++++++++++++++++++++++++ src/ModBehaviour.cs | 40 ++++++ 4 files changed, 378 insertions(+), 4 deletions(-) create mode 100644 src/DisassemblyHelper.cs diff --git a/.claude/settings.json b/.claude/settings.json index 9f61922..6f834ea 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -1,10 +1,10 @@ { - "$schema": "https://claude.ai/code/settings-schema.json", + "$schema": "https://json.schemastore.org/claude-code-settings.json", "permissions": { "allow": [ - "Bash(dotnet build*)", - "Bash(dotnet clean*)", - "Bash(dotnet restore*)", + "Bash(dotnet build:*)", + "Bash(dotnet clean:*)", + "Bash(dotnet restore:*)", "WebFetch(domain:code.claude.com)" ] } diff --git a/CLAUDE.md b/CLAUDE.md index e0b679f..38ee892 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -83,6 +83,43 @@ The game loads mods from `Duckov_Data/Mods/`. Each mod requires: - **`StorageHelper`** (`src/StorageHelper.cs`): Helper class for creating storage items with multi-tag slot filtering, enabling hierarchical storage systems. +- **`DisassemblyHelper`** (`src/DisassemblyHelper.cs`): Helper class for adding disassembly (deconstruction) formulas to custom items at runtime. + +### Disassembly System Pattern + +The game's disassembly system uses `DecomposeDatabase` (singleton in `Duckov.Economy` namespace) to store formulas. Key implementation details: + +**Types:** +- `DecomposeFormula`: Contains `item` (TypeID), `valid` (bool), and `result` (Cost) +- `Cost`: A **struct** with `money` (long) and `items` (ItemEntry[]) +- `Cost+ItemEntry`: **Nested struct** inside Cost with `id` (int) and `amount` (long) + +**Critical Reflection Pattern (Cost is a struct):** +```csharp +// Cost is a VALUE TYPE - must box before reflection, then unbox +Cost result = new Cost { money = 0L }; +object boxedResult = result; // Box the struct +itemsField.SetValue(boxedResult, itemEntries); // Modify boxed copy +result = (Cost)boxedResult; // Unbox back +formula.result = result; +``` + +**Adding a Formula:** +1. Get `DecomposeDatabase.Instance` +2. Access private `entries` field via reflection +3. Create `DecomposeFormula` with item TypeID and `valid = true` +4. Create `Cost` with money and/or items array +5. For items: Create `Cost+ItemEntry[]` via reflection (type found from `Cost.items` field type) +6. Add formula to list, write back via reflection +7. Call private `RebuildDictionary()` method + +**Known Material TypeIDs:** +- Polyethylene Sheet: `764` + +**Cleanup:** Track added item IDs and remove formulas in `OnDestroy()`. + +See `DisassemblyHelper.cs` for complete implementation. + ### Dependencies - **HarmonyLoadMod** (Workshop ID: 3589088839): Required mod dependency providing Harmony 2.4.1. Referenced at build time but not bundled to avoid version conflicts. @@ -153,6 +190,10 @@ Key namespaces and APIs from the game: - Spawn storage items (Card Binder, Binder Sheet) - Spawn random cards by rarity - Draggable OnGUI window interface +- Disassembly support for storage items: + - Binder Sheet → 2x Polyethylene Sheet + - Card Binder → 4x Polyethylene Sheet + - Cards/packs intentionally have no disassembly (collectibles) - Deploy/remove scripts for quick iteration - Unit tests for parsing logic and pack system diff --git a/src/DisassemblyHelper.cs b/src/DisassemblyHelper.cs new file mode 100644 index 0000000..954cb5a --- /dev/null +++ b/src/DisassemblyHelper.cs @@ -0,0 +1,293 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using UnityEngine; +using Duckov.Economy; + +namespace TradingCardMod +{ + /// + /// Helper class for adding disassembly formulas to custom items. + /// Uses reflection to modify the game's DecomposeDatabase at runtime. + /// Based on the approach from AdditionalCollectibles mod. + /// + public static class DisassemblyHelper + { + // Common material IDs from the game + public static readonly int Metal01 = 91043; + public static readonly int Metal02 = 91044; + public static readonly int Metal03 = 91045; + public static readonly int Plastic01 = 91046; + public static readonly int Plastic02 = 91047; + public static readonly int Plastic03 = 91048; + + // Track added formulas for cleanup + private static readonly List _addedItemIds = new List(); + + // Cached reflection info for ItemEntry type + private static Type? _itemEntryType; + private static FieldInfo? _itemEntryIdField; + private static FieldInfo? _itemEntryAmountField; + + /// + /// Initializes the ItemEntry type info via reflection. + /// Gets the type from Cost.items field to find the correct ItemEntry type. + /// + private static bool InitializeItemEntryType() + { + if (_itemEntryType != null) + return true; + + try + { + // Get ItemEntry type from Cost.items field (more reliable than searching by name) + FieldInfo? itemsField = typeof(Cost).GetField("items", BindingFlags.Instance | BindingFlags.Public); + if (itemsField == null) + { + Debug.LogError("[TradingCardMod] Could not find Cost.items field!"); + return false; + } + + // items is ItemEntry[], so get the element type + Type itemsType = itemsField.FieldType; + if (!itemsType.IsArray) + { + Debug.LogError("[TradingCardMod] Cost.items is not an array!"); + return false; + } + + _itemEntryType = itemsType.GetElementType(); + if (_itemEntryType == null) + { + Debug.LogError("[TradingCardMod] Could not get ItemEntry element type!"); + return false; + } + + Debug.Log($"[TradingCardMod] Found ItemEntry type: {_itemEntryType.FullName}"); + + _itemEntryIdField = _itemEntryType.GetField("id", BindingFlags.Instance | BindingFlags.Public); + _itemEntryAmountField = _itemEntryType.GetField("amount", BindingFlags.Instance | BindingFlags.Public); + + if (_itemEntryIdField == null || _itemEntryAmountField == null) + { + // Try non-public fields + _itemEntryIdField = _itemEntryType.GetField("id", BindingFlags.Instance | BindingFlags.NonPublic); + _itemEntryAmountField = _itemEntryType.GetField("amount", BindingFlags.Instance | BindingFlags.NonPublic); + } + + if (_itemEntryIdField == null || _itemEntryAmountField == null) + { + // Log available fields for debugging + var fields = _itemEntryType.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); + Debug.LogError($"[TradingCardMod] Could not find ItemEntry id/amount fields! Available: {string.Join(", ", fields.Select(f => f.Name))}"); + return false; + } + + Debug.Log($"[TradingCardMod] ItemEntry fields: id={_itemEntryIdField.Name}, amount={_itemEntryAmountField.Name}"); + return true; + } + catch (Exception ex) + { + Debug.LogError($"[TradingCardMod] Error initializing ItemEntry type: {ex.Message}"); + return false; + } + } + + /// + /// Creates an ItemEntry array using reflection. + /// + private static Array? CreateItemEntryArray((int id, long amount)[] items) + { + if (!InitializeItemEntryType() || _itemEntryType == null) + return null; + + Array array = Array.CreateInstance(_itemEntryType, items.Length); + for (int i = 0; i < items.Length; i++) + { + object entry = Activator.CreateInstance(_itemEntryType)!; + _itemEntryIdField!.SetValue(entry, items[i].id); + _itemEntryAmountField!.SetValue(entry, items[i].amount); + array.SetValue(entry, i); + } + return array; + } + + /// + /// Adds a disassembly formula for an item. + /// + /// The TypeID of the item that can be disassembled + /// Money reward when disassembled (0 for none) + /// Array of (itemId, amount) tuples for materials received + /// True if formula was added successfully + public static bool AddFormula(int itemId, long money, params (int id, long amount)[] resultItems) + { + try + { + DecomposeDatabase instance = DecomposeDatabase.Instance; + if (instance == null) + { + Debug.LogError("[TradingCardMod] DecomposeDatabase.Instance is null!"); + return false; + } + + // Get the private entries field via reflection + FieldInfo? entriesField = typeof(DecomposeDatabase).GetField("entries", + BindingFlags.Instance | BindingFlags.NonPublic); + if (entriesField == null) + { + Debug.LogError("[TradingCardMod] Could not find 'entries' field on DecomposeDatabase!"); + return false; + } + + DecomposeFormula[] currentEntries = (DecomposeFormula[])entriesField.GetValue(instance); + List formulaList = new List(currentEntries); + + // Check if formula already exists for this item + foreach (DecomposeFormula existing in formulaList) + { + if (existing.item == itemId) + { + Debug.LogWarning($"[TradingCardMod] Disassembly formula already exists for item {itemId}, skipping"); + return false; + } + } + + // Create new formula + DecomposeFormula newFormula = new DecomposeFormula + { + item = itemId, + valid = true + }; + + // Create cost (result items) + // Cost is a struct, so we need to box it for reflection to work + Cost result = new Cost + { + money = money + }; + + // Build item entries array using reflection + if (resultItems.Length > 0) + { + Array? itemEntries = CreateItemEntryArray(resultItems); + if (itemEntries == null) + { + Debug.LogError("[TradingCardMod] Failed to create ItemEntry array!"); + return false; + } + + // Set items field on Cost using reflection (it expects Cost+ItemEntry[]) + // Must box the struct first, modify, then unbox + FieldInfo? itemsField = typeof(Cost).GetField("items", BindingFlags.Instance | BindingFlags.Public); + if (itemsField != null) + { + object boxedResult = result; // Box the struct + itemsField.SetValue(boxedResult, itemEntries); + result = (Cost)boxedResult; // Unbox back + Debug.Log($"[TradingCardMod] Set {itemEntries.Length} items on Cost for item {itemId}"); + } + } + + newFormula.result = result; + + // Add to list and write back + formulaList.Add(newFormula); + entriesField.SetValue(instance, formulaList.ToArray()); + + // Track for cleanup + if (!_addedItemIds.Contains(itemId)) + { + _addedItemIds.Add(itemId); + } + + // Rebuild internal dictionary for lookup performance + MethodInfo? rebuildMethod = typeof(DecomposeDatabase).GetMethod("RebuildDictionary", + BindingFlags.Instance | BindingFlags.NonPublic); + rebuildMethod?.Invoke(instance, null); + + Debug.Log($"[TradingCardMod] Added disassembly formula for item {itemId}"); + return true; + } + catch (Exception ex) + { + Debug.LogError($"[TradingCardMod] Error adding disassembly formula: {ex.Message}"); + return false; + } + } + + /// + /// Adds a simple money-only disassembly formula. + /// + /// The TypeID of the item + /// Money reward when disassembled + /// True if formula was added successfully + public static bool AddMoneyOnlyFormula(int itemId, long money) + { + return AddFormula(itemId, money); + } + + /// + /// Removes all disassembly formulas added by this mod. + /// Should be called during mod cleanup/unload. + /// + public static void Cleanup() + { + try + { + DecomposeDatabase instance = DecomposeDatabase.Instance; + if (instance == null) + { + Debug.LogWarning("[TradingCardMod] DecomposeDatabase.Instance is null during cleanup"); + _addedItemIds.Clear(); + return; + } + + FieldInfo entriesField = typeof(DecomposeDatabase).GetField("entries", + BindingFlags.Instance | BindingFlags.NonPublic); + if (entriesField == null) + { + Debug.LogWarning("[TradingCardMod] Could not find 'entries' field during cleanup"); + _addedItemIds.Clear(); + return; + } + + DecomposeFormula[] currentEntries = (DecomposeFormula[])entriesField.GetValue(instance); + List formulaList = new List(currentEntries); + + int removedCount = 0; + for (int i = formulaList.Count - 1; i >= 0; i--) + { + if (_addedItemIds.Contains(formulaList[i].item)) + { + formulaList.RemoveAt(i); + removedCount++; + } + } + + entriesField.SetValue(instance, formulaList.ToArray()); + + // Rebuild dictionary + MethodInfo rebuildMethod = typeof(DecomposeDatabase).GetMethod("RebuildDictionary", + BindingFlags.Instance | BindingFlags.NonPublic); + rebuildMethod?.Invoke(instance, null); + + Debug.Log($"[TradingCardMod] Removed {removedCount} disassembly formulas"); + _addedItemIds.Clear(); + } + catch (Exception ex) + { + Debug.LogError($"[TradingCardMod] Error during disassembly cleanup: {ex.Message}"); + _addedItemIds.Clear(); + } + } + + /// + /// Gets the count of disassembly formulas added by this mod. + /// + public static int GetAddedFormulaCount() + { + return _addedItemIds.Count; + } + } +} diff --git a/src/ModBehaviour.cs b/src/ModBehaviour.cs index 5d1a079..4e1b0ac 100644 --- a/src/ModBehaviour.cs +++ b/src/ModBehaviour.cs @@ -126,6 +126,9 @@ namespace TradingCardMod // Create card packs CreateCardPacks(); + // Set up disassembly formulas for all items + SetupDisassembly(); + Debug.Log("[TradingCardMod] Mod initialized successfully!"); } catch (Exception ex) @@ -448,6 +451,40 @@ namespace TradingCardMod Debug.Log($"[TradingCardMod] Created {_registeredPacks.Count} card packs"); } + /// + /// Sets up disassembly formulas for storage items only. + /// Cards intentionally have no disassembly (they're collectibles). + /// + private void SetupDisassembly() + { + int formulasAdded = 0; + + // Polyethylene Sheet TypeID (base game material) + const int POLYETHYLENE_SHEET_ID = 764; + + // Binder Sheet: 2x polyethylene sheet + if (_binderItem != null) + { + if (DisassemblyHelper.AddFormula(BINDER_SHEET_ITEM_ID, 0L, (POLYETHYLENE_SHEET_ID, 2L))) + { + formulasAdded++; + } + } + + // Card Binder: 4x polyethylene sheet + if (_cardBoxItem != null) + { + if (DisassemblyHelper.AddFormula(CARD_BINDER_ITEM_ID, 0L, (POLYETHYLENE_SHEET_ID, 4L))) + { + formulasAdded++; + } + } + + // No disassembly for cards or packs (collectibles) + + Debug.Log($"[TradingCardMod] Added {formulasAdded} disassembly formulas"); + } + /// /// Update is called every frame. Used for debug input handling. /// @@ -919,6 +956,9 @@ namespace TradingCardMod // Clean up packs PackHelper.Cleanup(); + // Clean up disassembly formulas + DisassemblyHelper.Cleanup(); + // Clean up tags TagHelper.Cleanup();