commit 9704516dd762d3bbb0acc70ccfb7e7ef4128844d Author: Cal Corum Date: Tue Nov 18 18:55:04 2025 -0600 Initial commit: Trading Card Mod scaffold for Escape from Duckov Set up project structure with: - .NET Standard 2.1 project targeting Duckov modding API - ModBehaviour entry point with card set loading system - Harmony patching infrastructure (depends on HarmonyLoadMod) - Pipe-separated card definition format for user-generated content - Example card set and documentation 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude diff --git a/.claude/commands/build.md b/.claude/commands/build.md new file mode 100644 index 0000000..63a1efe --- /dev/null +++ b/.claude/commands/build.md @@ -0,0 +1,7 @@ +# Build Mod + +Build the TradingCardMod and report any errors. + +1. Run `dotnet build` in the project root +2. If successful, show the output DLL path +3. If errors occur, analyze and suggest fixes diff --git a/.claude/commands/deploy.md b/.claude/commands/deploy.md new file mode 100644 index 0000000..e5fb823 --- /dev/null +++ b/.claude/commands/deploy.md @@ -0,0 +1,11 @@ +# Deploy to Game + +Build and deploy the mod to the game's Mods folder. + +1. Run `dotnet build -c Release` +2. Copy the following to `{DuckovPath}/Duckov_Data/Mods/TradingCardMod/`: + - `TradingCardMod.dll` + - `info.ini` + - `preview.png` (if exists) + - `CardSets/` folder +3. Report deployment status diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..f7ac691 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,10 @@ +{ + "$schema": "https://claude.ai/code/settings-schema.json", + "permissions": { + "allow": [ + "Bash(dotnet build*)", + "Bash(dotnet clean*)", + "Bash(dotnet restore*)" + ] + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9767a9b --- /dev/null +++ b/.gitignore @@ -0,0 +1,30 @@ +# Build outputs +bin/ +obj/ + +# IDE +.vs/ +.vscode/ +*.user +*.suo + +# Claude Code private notes +.claude/scratchpad/ + +# OS files +.DS_Store +Thumbs.db + +# NuGet +*.nupkg +packages/ + +# Debug logs +*.log + +# Card set images (user-generated content) +# Uncomment if you don't want to track example images +# CardSets/*/images/ + +# Preview image (generate your own) +# preview.png diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..cada07c --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,80 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +A Unity mod for Escape from Duckov that adds a customizable trading card system with storage solutions. The framework supports user-generated content through simple pipe-separated text files, allowing non-programmers to add their own card sets. + +## Build Commands + +```bash +# Build the mod +dotnet build + +# Build release version +dotnet build -c Release + +# Output location: bin/Debug/netstandard2.1/TradingCardMod.dll +``` + +**Important**: Before building, update `DuckovPath` in `TradingCardMod.csproj` (line 10) to your actual game installation path. + +## Architecture + +### Mod Loading System + +The game loads mods from `Duckov_Data/Mods/`. Each mod requires: +- A DLL with namespace matching `info.ini`'s `name` field +- A `ModBehaviour` class inheriting from `Duckov.Modding.ModBehaviour` +- The `info.ini` and `preview.png` files + +### Key Classes + +- **`ModBehaviour`** (`src/ModBehaviour.cs`): Main entry point. Inherits from `Duckov.Modding.ModBehaviour` (which extends `MonoBehaviour`). Handles card set loading in `Start()` and cleanup in `OnDestroy()`. + +- **`Patches`** (`src/Patches.cs`): Harmony patch definitions. Uses `HarmonyLib` for runtime method patching. Patches are applied in `Start()` and removed in `OnDestroy()`. + +- **`TradingCard`**: Data class representing card properties. Contains `GenerateTypeID()` for creating unique item IDs (100000+ range to avoid game conflicts). + +### Dependencies + +- **HarmonyLoadMod** (Workshop ID: 3589088839): Required mod dependency providing Harmony 2.4.1. Referenced at build time but not bundled to avoid version conflicts. + +### Card Definition Format + +Cards are defined in `CardSets/{SetName}/cards.txt` using pipe-separated values: +``` +CardName | SetName | SetNumber | ImageFile | Rarity | Weight | Value +``` + +Images go in `CardSets/{SetName}/images/`. + +## Game API Reference + +Key namespaces and APIs from the game: +- `ItemStatsSystem.ItemAssetsCollection.AddDynamicEntry(Item prefab)` - Register custom items +- `ItemStatsSystem.ItemAssetsCollection.RemoveDynamicEntry(Item prefab)` - Remove on unload +- `SodaCraft.Localizations.LocalizationManager.SetOverrideText(string key, string value)` - Localization +- `ItemStatsSystem.ItemUtilities.SendToPlayer(Item item)` - Give items to player + +## Development Notes + +- Target framework: .NET Standard 2.1 +- C# language version: 8.0 +- All logging uses `Debug.Log()` with `[TradingCardMod]` prefix +- Custom items need unique TypeIDs to avoid conflicts with base game and other mods + +## Testing + +1. Copy build output to `{GamePath}/Duckov_Data/Mods/TradingCardMod/` +2. Include: `TradingCardMod.dll`, `info.ini`, `preview.png`, `CardSets/` folder +3. Launch game and enable mod in Mods menu +4. Check game logs for `[TradingCardMod]` messages + +## TODO Items in Code + +The following features need implementation (marked with TODO comments): +- `RegisterCardWithGame()` - Create Unity prefabs and register with ItemAssetsCollection +- Item cleanup in `OnDestroy()` - Remove registered items on mod unload +- `TradingCard.ItemPrefab` property - Store Unity prefab reference diff --git a/CardSets/ExampleSet/cards.txt b/CardSets/ExampleSet/cards.txt new file mode 100644 index 0000000..9489c0f --- /dev/null +++ b/CardSets/ExampleSet/cards.txt @@ -0,0 +1,20 @@ +# Example Card Set - Trading Card Mod for Escape from Duckov +# Format: CardName | SetName | SetNumber | ImageFile | Rarity | Weight | Value +# +# Fields: +# CardName - Display name of the card +# SetName - Name of the card collection +# SetNumber - Number within the set (for sorting) +# ImageFile - Filename of the card image (must be in images/ subfolder) +# Rarity - Card rarity (Common, Uncommon, Rare, Ultra Rare) +# Weight - Physical weight in game units +# Value - In-game currency value +# +# Add your own cards below! Just follow the format above. +# Place corresponding images in the images/ subfolder. + +Duck Hero | Example Set | 001 | duck_hero.png | Rare | 0.01 | 100 +Golden Quacker | Example Set | 002 | golden_quacker.png | Ultra Rare | 0.01 | 500 +Pond Guardian | Example Set | 003 | pond_guardian.png | Uncommon | 0.01 | 25 +Bread Seeker | Example Set | 004 | bread_seeker.png | Common | 0.01 | 10 +Feathered Fury | Example Set | 005 | feathered_fury.png | Rare | 0.01 | 75 diff --git a/README.md b/README.md new file mode 100644 index 0000000..a894f3a --- /dev/null +++ b/README.md @@ -0,0 +1,136 @@ +# Trading Card Mod for Escape from Duckov + +A customizable trading card system that lets you add your own card sets to the game. + +## Requirements + +**Required Mod Dependency:** +- [HarmonyLib (HarmonyLoadMod)](https://steamcommunity.com/sharedfiles/filedetails/?id=3589088839) - Subscribe on Steam Workshop + +This mod requires the HarmonyLoadMod to be installed. It provides the Harmony library that many mods share to avoid version conflicts. + +## Installation + +1. Subscribe to [HarmonyLib](https://steamcommunity.com/sharedfiles/filedetails/?id=3589088839) on Steam Workshop +2. Build the mod (see Development section) +3. Copy the `TradingCardMod` folder to your game's `Duckov_Data/Mods` directory +4. Launch the game and enable both HarmonyLib and this mod in the Mods menu + +## Adding Card Sets + +### Creating a New Card Set + +1. Create a new folder in `CardSets/` with your set name (e.g., `CardSets/MyCards/`) +2. Create a `cards.txt` file in your folder +3. Create an `images/` subfolder for card artwork + +### Card Definition Format + +Cards are defined in `cards.txt` using pipe-separated values: + +``` +CardName | SetName | SetNumber | ImageFile | Rarity | Weight | Value +``` + +Example: +``` +Blue Dragon | Fantasy Set | 001 | blue_dragon.png | Ultra Rare | 0.01 | 500 +Fire Sprite | Fantasy Set | 002 | fire_sprite.png | Rare | 0.01 | 100 +``` + +### Field Descriptions + +| Field | Description | Example | +|-------|-------------|---------| +| CardName | Display name of the card | "Blue Dragon" | +| SetName | Name of the collection | "Fantasy Set" | +| SetNumber | Number for sorting (as integer) | 001 | +| ImageFile | Image filename in images/ folder | "blue_dragon.png" | +| Rarity | Card rarity tier | Common, Uncommon, Rare, Ultra Rare | +| Weight | Physical weight in game units | 0.01 | +| Value | In-game currency value | 500 | + +### Image Requirements + +- Place images in your card set's `images/` subfolder +- Recommended format: PNG +- Recommended size: 256x256 or similar aspect ratio + +### Comments + +Lines starting with `#` are treated as comments and ignored: + +``` +# This is a comment +# CardName | SetName | SetNumber | ImageFile | Rarity | Weight | Value +Blue Dragon | Fantasy Set | 001 | blue_dragon.png | Ultra Rare | 0.01 | 500 +``` + +## Folder Structure + +``` +TradingCardMod/ +├── TradingCardMod.dll +├── info.ini +├── preview.png +├── CardSets/ +│ ├── ExampleSet/ +│ │ ├── cards.txt +│ │ └── images/ +│ │ └── (card images here) +│ └── YourCustomSet/ +│ ├── cards.txt +│ └── images/ +└── README.md +``` + +## Development + +### Requirements + +- .NET SDK (for .NET Standard 2.1) +- Escape from Duckov installed + +### Building + +1. Update `DuckovPath` in `TradingCardMod.csproj` to point to your game installation +2. Build the project: + ```bash + dotnet build + ``` +3. The compiled DLL will be in `bin/Debug/netstandard2.1/` + +### Linux Steam Paths + +Common Steam library locations on Linux: +- `~/.steam/steam/steamapps/common/Escape from Duckov` +- `~/.local/share/Steam/steamapps/common/Escape from Duckov` + +### Testing + +1. Copy the build output to `Duckov_Data/Mods/TradingCardMod/` +2. Launch the game through Steam +3. Enable the mod in the Mods menu +4. Check the game's log for `[TradingCardMod]` messages + +## Troubleshooting + +### "CardSets directory not found" +The mod will create this directory automatically. Add your card sets there. + +### Cards not appearing +- Check that `cards.txt` follows the exact format (7 pipe-separated fields) +- Ensure image files exist in the `images/` subfolder +- Check the game log for parsing errors + +### Build errors +- Verify `DuckovPath` in the .csproj points to your actual game installation +- Ensure you have .NET SDK installed with `dotnet --version` + +## License + +This mod is provided as-is for personal use. Do not distribute copyrighted card artwork. + +## Credits + +Built using the official Duckov modding framework. diff --git a/TradingCardMod.csproj b/TradingCardMod.csproj new file mode 100644 index 0000000..bc00d08 --- /dev/null +++ b/TradingCardMod.csproj @@ -0,0 +1,40 @@ + + + netstandard2.1 + enable + 8.0 + TradingCardMod + TradingCardMod + + + /mnt/NV2/SteamLibrary/steamapps/common/Escape from Duckov + + /Duckov_Data/Managed/ + + + /mnt/NV2/SteamLibrary/steamapps/workshop/content/3167020 + + + 3589088839 + + + + + + + + + + + + + + + + + + + + + + diff --git a/info.ini b/info.ini new file mode 100644 index 0000000..d405ccf --- /dev/null +++ b/info.ini @@ -0,0 +1,3 @@ +name=TradingCardMod +displayName=Trading Card Mod +description=A customizable trading card system with storage solutions. Add your own card sets! diff --git a/src/ModBehaviour.cs b/src/ModBehaviour.cs new file mode 100644 index 0000000..aa81de8 --- /dev/null +++ b/src/ModBehaviour.cs @@ -0,0 +1,203 @@ +using System; +using System.IO; +using System.Collections.Generic; +using UnityEngine; +using ItemStatsSystem; +using SodaCraft.Localizations; + +namespace TradingCardMod +{ + /// + /// Main entry point for the Trading Card Mod. + /// Inherits from Duckov.Modding.ModBehaviour as required by the mod system. + /// + public class ModBehaviour : Duckov.Modding.ModBehaviour + { + private static ModBehaviour? _instance; + public static ModBehaviour Instance => _instance!; + + private string _modPath = string.Empty; + private List _loadedCards = new List(); + + /// + /// Called when the mod is loaded. Initialize the card system here. + /// + void Start() + { + _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 path: {_modPath}"); + + // Apply Harmony patches + Patches.ApplyPatches(); + + try + { + LoadCardSets(); + } + catch (Exception ex) + { + Debug.LogError($"[TradingCardMod] Failed to load card sets: {ex.Message}"); + Debug.LogException(ex); + } + } + + /// + /// Scans the CardSets directory and loads all card definitions. + /// + private void LoadCardSets() + { + string cardSetsPath = Path.Combine(_modPath, "CardSets"); + + if (!Directory.Exists(cardSetsPath)) + { + Debug.LogWarning($"[TradingCardMod] CardSets directory not found at: {cardSetsPath}"); + Directory.CreateDirectory(cardSetsPath); + return; + } + + string[] setDirectories = Directory.GetDirectories(cardSetsPath); + Debug.Log($"[TradingCardMod] Found {setDirectories.Length} card set directories"); + + foreach (string setDir in setDirectories) + { + LoadCardSet(setDir); + } + + Debug.Log($"[TradingCardMod] Total cards loaded: {_loadedCards.Count}"); + } + + /// + /// Loads a single card set from a directory. + /// Expects a cards.txt file with pipe-separated values. + /// + /// Path to the card set directory + private void LoadCardSet(string setDirectory) + { + string setName = Path.GetFileName(setDirectory); + string cardsFile = Path.Combine(setDirectory, "cards.txt"); + + if (!File.Exists(cardsFile)) + { + Debug.LogWarning($"[TradingCardMod] No cards.txt found in {setName}"); + return; + } + + Debug.Log($"[TradingCardMod] Loading card set: {setName}"); + + try + { + string[] lines = File.ReadAllLines(cardsFile); + int cardCount = 0; + + foreach (string line in lines) + { + // Skip empty lines and comments + if (string.IsNullOrWhiteSpace(line) || line.TrimStart().StartsWith("#")) + continue; + + TradingCard? card = ParseCardLine(line, setDirectory); + if (card != null) + { + _loadedCards.Add(card); + cardCount++; + // TODO: Register card with game's item system + // RegisterCardWithGame(card); + } + } + + Debug.Log($"[TradingCardMod] Loaded {cardCount} cards from {setName}"); + } + catch (Exception ex) + { + Debug.LogError($"[TradingCardMod] Error loading card set {setName}: {ex.Message}"); + } + } + + /// + /// Parses a single line from cards.txt into a TradingCard object. + /// Format: CardName | SetName | SetNumber | ImageFile | Rarity | Weight | Value + /// + private TradingCard? ParseCardLine(string line, string setDirectory) + { + string[] parts = line.Split('|'); + + if (parts.Length < 7) + { + Debug.LogWarning($"[TradingCardMod] Invalid card line (expected 7 fields): {line}"); + return null; + } + + try + { + return new TradingCard + { + CardName = parts[0].Trim(), + SetName = parts[1].Trim(), + SetNumber = int.Parse(parts[2].Trim()), + ImagePath = Path.Combine(setDirectory, "images", parts[3].Trim()), + Rarity = parts[4].Trim(), + Weight = float.Parse(parts[5].Trim()), + Value = int.Parse(parts[6].Trim()) + }; + } + catch (Exception ex) + { + Debug.LogWarning($"[TradingCardMod] Failed to parse card line: {line} - {ex.Message}"); + return null; + } + } + + /// + /// Called when the mod is unloaded. Clean up registered items. + /// + void OnDestroy() + { + Debug.Log("[TradingCardMod] Mod unloading, cleaning up..."); + + // Remove Harmony patches + Patches.RemovePatches(); + + // TODO: Remove registered items from game + // foreach (var card in _loadedCards) + // { + // ItemAssetsCollection.RemoveDynamicEntry(card.ItemPrefab); + // } + + _loadedCards.Clear(); + } + } + + /// + /// Represents a trading card's data loaded from a card set file. + /// + public class TradingCard + { + public string CardName { get; set; } = string.Empty; + public string SetName { get; set; } = string.Empty; + public int SetNumber { get; set; } + public string ImagePath { get; set; } = string.Empty; + public string Rarity { get; set; } = string.Empty; + public float Weight { get; set; } + public int Value { get; set; } + + // TODO: Add Unity prefab reference once we understand the item system better + // public Item? ItemPrefab { get; set; } + + /// + /// Generates a unique TypeID for this card to avoid conflicts. + /// Uses hash of set name + card name for uniqueness. + /// + public int GenerateTypeID() + { + // Start from a high number to avoid conflicts with base game items + // Use hash to ensure consistency across loads + string uniqueKey = $"TradingCard_{SetName}_{CardName}"; + return 100000 + Math.Abs(uniqueKey.GetHashCode() % 900000); + } + } +} diff --git a/src/Patches.cs b/src/Patches.cs new file mode 100644 index 0000000..d9bc91b --- /dev/null +++ b/src/Patches.cs @@ -0,0 +1,118 @@ +using System; +using HarmonyLib; +using UnityEngine; + +namespace TradingCardMod +{ + /// + /// Contains all Harmony patches for the Trading Card Mod. + /// Patches are applied in ModBehaviour.Start() via Harmony.PatchAll(). + /// + public static class Patches + { + /// + /// Unique Harmony ID for this mod. Used to identify and unpatch if needed. + /// + public const string HarmonyId = "com.manticorum.tradingcardgame.duckov"; + + private static Harmony? _harmony; + + /// + /// Apply all Harmony patches defined in this assembly. + /// + public static void ApplyPatches() + { + try + { + _harmony = new Harmony(HarmonyId); + _harmony.PatchAll(); + Debug.Log($"[TradingCardMod] Harmony patches applied successfully"); + } + catch (Exception ex) + { + Debug.LogError($"[TradingCardMod] Failed to apply Harmony patches: {ex.Message}"); + Debug.LogException(ex); + } + } + + /// + /// Remove all Harmony patches applied by this mod. + /// Called during mod unload to clean up. + /// + public static void RemovePatches() + { + try + { + _harmony?.UnpatchSelf(); + Debug.Log($"[TradingCardMod] Harmony patches removed"); + } + catch (Exception ex) + { + Debug.LogError($"[TradingCardMod] Failed to remove Harmony patches: {ex.Message}"); + } + } + } + + // ========================================================================== + // 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/ + // ========================================================================== + + /* + /// + /// Example: Postfix patch that runs after a method completes. + /// Use case: Log when items are added to inventory, modify return values. + /// + [HarmonyPatch(typeof(ItemStatsSystem.ItemUtilities), "SendToPlayer")] + public static class SendToPlayer_Patch + { + [HarmonyPostfix] + public static void Postfix(ItemStatsSystem.Item item) + { + // 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}"); + } + } + + /// + /// 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; + } + } + */ +}