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": {
|
||||
"allow": [
|
||||
"Bash(dotnet build*)",
|
||||
"Bash(dotnet clean*)",
|
||||
"Bash(dotnet restore*)",
|
||||
"Bash(dotnet build:*)",
|
||||
"Bash(dotnet clean:*)",
|
||||
"Bash(dotnet restore:*)",
|
||||
"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.
|
||||
|
||||
- **`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
|
||||
|
||||
|
||||
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
|
||||
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");
|
||||
}
|
||||
|
||||
/// <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>
|
||||
/// Update is called every frame. Used for debug input handling.
|
||||
/// </summary>
|
||||
@ -919,6 +956,9 @@ namespace TradingCardMod
|
||||
// Clean up packs
|
||||
PackHelper.Cleanup();
|
||||
|
||||
// Clean up disassembly formulas
|
||||
DisassemblyHelper.Cleanup();
|
||||
|
||||
// Clean up tags
|
||||
TagHelper.Cleanup();
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user