Add disassembly support for storage items
- 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 <noreply@anthropic.com>
This commit is contained in:
parent
30285644d6
commit
8c7a131869
@ -1,10 +1,10 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://claude.ai/code/settings-schema.json",
|
"$schema": "https://json.schemastore.org/claude-code-settings.json",
|
||||||
"permissions": {
|
"permissions": {
|
||||||
"allow": [
|
"allow": [
|
||||||
"Bash(dotnet build*)",
|
"Bash(dotnet build:*)",
|
||||||
"Bash(dotnet clean*)",
|
"Bash(dotnet clean:*)",
|
||||||
"Bash(dotnet restore*)",
|
"Bash(dotnet restore:*)",
|
||||||
"WebFetch(domain:code.claude.com)"
|
"WebFetch(domain:code.claude.com)"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
41
CLAUDE.md
41
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.
|
- **`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
|
### Dependencies
|
||||||
|
|
||||||
- **HarmonyLoadMod** (Workshop ID: 3589088839): Required mod dependency providing Harmony 2.4.1. Referenced at build time but not bundled to avoid version conflicts.
|
- **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 storage items (Card Binder, Binder Sheet)
|
||||||
- Spawn random cards by rarity
|
- Spawn random cards by rarity
|
||||||
- Draggable OnGUI window interface
|
- 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
|
- Deploy/remove scripts for quick iteration
|
||||||
- Unit tests for parsing logic and pack system
|
- Unit tests for parsing logic and pack system
|
||||||
|
|
||||||
|
|||||||
293
src/DisassemblyHelper.cs
Normal file
293
src/DisassemblyHelper.cs
Normal file
@ -0,0 +1,293 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
|
using UnityEngine;
|
||||||
|
using Duckov.Economy;
|
||||||
|
|
||||||
|
namespace TradingCardMod
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
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<int> _addedItemIds = new List<int>();
|
||||||
|
|
||||||
|
// Cached reflection info for ItemEntry type
|
||||||
|
private static Type? _itemEntryType;
|
||||||
|
private static FieldInfo? _itemEntryIdField;
|
||||||
|
private static FieldInfo? _itemEntryAmountField;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes the ItemEntry type info via reflection.
|
||||||
|
/// Gets the type from Cost.items field to find the correct ItemEntry type.
|
||||||
|
/// </summary>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates an ItemEntry array using reflection.
|
||||||
|
/// </summary>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds a disassembly formula for an item.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="itemId">The TypeID of the item that can be disassembled</param>
|
||||||
|
/// <param name="money">Money reward when disassembled (0 for none)</param>
|
||||||
|
/// <param name="resultItems">Array of (itemId, amount) tuples for materials received</param>
|
||||||
|
/// <returns>True if formula was added successfully</returns>
|
||||||
|
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<DecomposeFormula> formulaList = new List<DecomposeFormula>(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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds a simple money-only disassembly formula.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="itemId">The TypeID of the item</param>
|
||||||
|
/// <param name="money">Money reward when disassembled</param>
|
||||||
|
/// <returns>True if formula was added successfully</returns>
|
||||||
|
public static bool AddMoneyOnlyFormula(int itemId, long money)
|
||||||
|
{
|
||||||
|
return AddFormula(itemId, money);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removes all disassembly formulas added by this mod.
|
||||||
|
/// Should be called during mod cleanup/unload.
|
||||||
|
/// </summary>
|
||||||
|
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<DecomposeFormula> formulaList = new List<DecomposeFormula>(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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the count of disassembly formulas added by this mod.
|
||||||
|
/// </summary>
|
||||||
|
public static int GetAddedFormulaCount()
|
||||||
|
{
|
||||||
|
return _addedItemIds.Count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -126,6 +126,9 @@ namespace TradingCardMod
|
|||||||
// Create card packs
|
// Create card packs
|
||||||
CreateCardPacks();
|
CreateCardPacks();
|
||||||
|
|
||||||
|
// Set up disassembly formulas for all items
|
||||||
|
SetupDisassembly();
|
||||||
|
|
||||||
Debug.Log("[TradingCardMod] Mod initialized successfully!");
|
Debug.Log("[TradingCardMod] Mod initialized successfully!");
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@ -448,6 +451,40 @@ namespace TradingCardMod
|
|||||||
Debug.Log($"[TradingCardMod] Created {_registeredPacks.Count} card packs");
|
Debug.Log($"[TradingCardMod] Created {_registeredPacks.Count} card packs");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets up disassembly formulas for storage items only.
|
||||||
|
/// Cards intentionally have no disassembly (they're collectibles).
|
||||||
|
/// </summary>
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Update is called every frame. Used for debug input handling.
|
/// Update is called every frame. Used for debug input handling.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -919,6 +956,9 @@ namespace TradingCardMod
|
|||||||
// Clean up packs
|
// Clean up packs
|
||||||
PackHelper.Cleanup();
|
PackHelper.Cleanup();
|
||||||
|
|
||||||
|
// Clean up disassembly formulas
|
||||||
|
DisassemblyHelper.Cleanup();
|
||||||
|
|
||||||
// Clean up tags
|
// Clean up tags
|
||||||
TagHelper.Cleanup();
|
TagHelper.Cleanup();
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user