From d05ba64700f1f8482467d57c853c822decc22118 Mon Sep 17 00:00:00 2001 From: Cal Corum Date: Wed, 19 Nov 2025 16:56:04 -0600 Subject: [PATCH] Implement Phase 2: Core card framework with clone + reflection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add ItemExtensions.cs with reflection helpers for private field access - Add TagHelper.cs for game tag operations and custom TradingCard tag - Rewrite ModBehaviour.cs with complete item creation pipeline: - Clone base game item (ID 135) as template - Set properties via reflection (typeID, weight, value, quality) - Load PNG sprites from CardSets/*/images/ - Register items with ItemAssetsCollection - Proper cleanup on mod unload - Add F9 debug key to spawn cards for testing - Add deploy.sh and remove.sh scripts for quick mod installation - Add placeholder card images for ExampleSet - Add Unity module references (ImageConversion, InputLegacy) Cards now appear in-game with custom icons and can be spawned to inventory. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- CardSets/ExampleSet/images/bread_seeker.png | Bin 0 -> 3335 bytes CardSets/ExampleSet/images/duck_hero.png | Bin 0 -> 3403 bytes CardSets/ExampleSet/images/feathered_fury.png | Bin 0 -> 3023 bytes CardSets/ExampleSet/images/golden_quacker.png | Bin 0 -> 3226 bytes CardSets/ExampleSet/images/pond_guardian.png | Bin 0 -> 3571 bytes TradingCardMod.csproj | 11 + deploy.sh | 61 ++++ remove.sh | 46 +++ src/ItemExtensions.cs | 128 +++++++ src/ModBehaviour.cs | 333 ++++++++++++++---- src/TagHelper.cs | 124 +++++++ 11 files changed, 638 insertions(+), 65 deletions(-) create mode 100644 CardSets/ExampleSet/images/bread_seeker.png create mode 100644 CardSets/ExampleSet/images/duck_hero.png create mode 100644 CardSets/ExampleSet/images/feathered_fury.png create mode 100644 CardSets/ExampleSet/images/golden_quacker.png create mode 100644 CardSets/ExampleSet/images/pond_guardian.png create mode 100755 deploy.sh create mode 100755 remove.sh create mode 100644 src/ItemExtensions.cs create mode 100644 src/TagHelper.cs diff --git a/CardSets/ExampleSet/images/bread_seeker.png b/CardSets/ExampleSet/images/bread_seeker.png new file mode 100644 index 0000000000000000000000000000000000000000..e2548c57ac09397cd9b3047d0f2a9f37633a6be9 GIT binary patch literal 3335 zcmd5$VxwilSTOg{*M1f3DKhf9L zxcui_vnKu`XJ|rn4K-<2=~-@zgSqC?GXMbk6dsfb4wC%`Ye+jSt#FjiiT zByk&oAsw+|)1FC*Y*jNMgQVnILo`8)p0mq99qpiQVq+Rd&@;a`9a7`glTNa2A`-h- zi3$qk=N@_XxRw0~(kL`?Kz0Udw4JxzuP7-u-KM4$&qKuwtQL%60=WQaB^dzE;wg9r z!2l{w>KK)F0Fx&>fRgcwO2NO9q;NEjA;t-J(D38o7nyG?WWT(czXfHumird!-av-} zq-gD9ETUKs-(~O`DczOIMsCvimbFNCI!Fsuvd!?d=mjZB<%Pdyu-*SMhL77C;p`X3 zQs!{MVuLYyVK3_WWfoW2MW?q%aEliyv8Bh2Cfr@vYGst}Nu_+76>GJVQprN5csSPM z%lV*tJsQtFr~nLut#2pNn9*mrwm-)K6+GT9Ajt1Sv#imO_By@TwseN!sUDo|mdWUQ&rf|yiJU7N3 zZYIUcl9uKm#g#tweuhSV$FC8@{@T!f{_#Fx-;`0hyB>?F3pidZZQ2`4$+xB+(xd%n zfdudOPW*+-op*K*F*$AP@`{$T%!HyxUfbN%t-`|1L4?ABaE$rKHoA+(=M`!+$}MaN zRfuzrYG)^k)Z?&Vwm6{&y6oqsdZEGJv0OgE6%G>BXJrV)H(Pn1@e4|U%D3P2j##BE z()x)U8l$H|ocI)BkKNAPOFx<_U6!TvY0} z2@1SAUu%+|8I#Pt1?NywPp}CM*wc}E&@FcF^qO4c|ZM+ zbPeeTE<{l|EF6ke{iqA6bU&{$Z?-pIrce^Bu>?8qyWe$Ps%MuXi3gN?PS zY%ISJmmE6kuVr5wFfNwx=<4uZ7yZ>L0O8kWQ4sOH*Ay9iW+zBk$w2e+tLik9N#!e++DCqO$M)PEo$5bWst>}q`S9|13 zXQh5+*sz^^)Lh*_6f-+S5D`$=TwF^$7$kIY42&xmj^|iNX4o$sM%@t(ye8R^Y4~ZS zm&hb1wxT!8>7^8DAYFOylavH98qcS)T(>vOrupQatkL?Plc&8p>etTr1}4ELRhQu~cZ&qm)AK&ZOshpn_(V(ull-D0f08+~XPpkWn8Q(9GVetRKwc{%lK zK9Tsd_8X?Em~3&0S?EBf9*z~M_XJYbq7u{GWmK-CH6o(O;uZdDH&Fm)Q;#+S%~J)` zI!am;z-+Tr&ZxYT57diaa|)#BK6B|&#;+m?j~;(vc5$2Dn)r3o><~6P%LbzcHsX-e z_}r%+3u~I*lt9E#0^FoTrqRbM=QL^Qs!u?b@eRe?`3&Wc^~Oa+$?E(8&nve8m1)cB zu2qS$a0Pf!kbio>ajeh7=oT(((*2fFxGCf5#-zfYAMwrTC3)Vg3}AiDQj@Ivlt-!7z@2C%K(B z^lml+%fwW_Wp)Z_L1zLFWPul!u(uF+iC3iEhLArRKz0jsvNTQuC+NF=gJQ?S(p{t304(Dl_C@qU`wd({yg9l9+~BZY|A1 zZR>ON;`};nB!c7?e#IUTrHx>~3T;6&10GofIo;1j(cQex0!$5;$Myubn#C3kCHA?M z&diVkRh3)(qS{xz>AkCst5y>l6E4?}6Rtg#y145gF0s@NeddFt)^(JT>V=vTGkaTZ za?Hu}SwzlDeA$|-lH(uuD5WlPLMql_W0;t=ziM0&X>W9|N04BhR$|*rhK7s<_{iDA zeGb2vOcen6PXnXyTo!9$Qy7r6mcrLABh6i zG3cM|c5vLFS7cdd;W`xT@*G1#%B0A|BP*aC9bvl7 zyqJd+I=Y&HCzxQhsX=vj1iohlV#K6qS+11_e^T^3q-nb@fQ2k%R zW%>SSqX}~sP4*6yeBbT>*0Tf4%5{*2|w*VW8?Caz$;C_gvb%>iwh`XF?usaz5;$U$p5iv0l zFHdwG5`Po literal 0 HcmV?d00001 diff --git a/CardSets/ExampleSet/images/duck_hero.png b/CardSets/ExampleSet/images/duck_hero.png new file mode 100644 index 0000000000000000000000000000000000000000..2b05e33b1cba81cdb216b9f77d09be4edb0ba979 GIT binary patch literal 3403 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 literal 0 HcmV?d00001 diff --git a/CardSets/ExampleSet/images/feathered_fury.png b/CardSets/ExampleSet/images/feathered_fury.png new file mode 100644 index 0000000000000000000000000000000000000000..9fc5e6d5a6626dbee97094f25a4bb3494f9768af GIT binary patch literal 3023 zcmdUxcTf{b9>)XnC{h&x6%@e&f=G=tkw-6qP{cqG=?F+~hR~uYDBVJlNP>a_5-A~+ z1bCr5N@xKAi9iAd=@7a^0>a_l%+1`*d;j0<%W={c*01_3R?Y7}RK|p&9nh zb??-1SCUtxs4TP98C@VNZI_C9_corTa#4A`RjT-D;$1mp@H2em@bKLdCv)LQU+9bY zBI)>KpLf4IgeZqm-3(^GaQbT_Qls>&*|;3@=63f%+q>(_I{_7f`)K<4!qRRkm4*UX zn}LRe0RtB}0Com|-+})R7#i8)NbCq~7i_GU7lldLYNuv@Pz_Hk&9ITa{hA)} zV#mrz%~bs8n7JtO&ZI=O?c^3YaTkP(0?#N{sLnl;6ET?qUzL@)td5Zk+~Vg=hWfZJ zzy5l6Aa;IkfW=KGznia~pXhJ>>Ps6y`XPwxU3->-H^NnNWf*8^#n6=ZrhkZeQQ3Cj zYo>zec9SviVRI+!vmBfX!`|wHLY@{rPZS*Z+dpOTP#o3fdVK=eii+juUw}9-B-^ zHyL4MGV@lOecVpSdG^T|Le7B7Et+PA3UD)>vBg=^*t84Dsh2&j0DyW#=2X_g27c8Y z^CB*+q+~Tmx=`n;q@>Wsgre4IrLWdTM|4QFsLoB=`G&}_=mp!tH}LC|e+(AHZ8!&YT}lniKy9ngqs;dy(J@mjcTIkMKy1qwOWL`dJ+WeNb?4 z(K-LI)zto#pYNBa`WWv6Y8pq4{GvK2wfW~}7Q+OBtOPNYPNek&jpC1w17p1fzVR}`8PP~IZ9ulKZCRkAvv){2$EoJb zrRlKm#`pJDkd7I~&>>-ZL`c9`L)kY4XYVXD3A*20B#FD+-;2YGnl)|tEtBKc@=sJFb|^)ArgdH}+nPen4&AaB9#c=wH(cSjvX5Kipkt_ZF59|}Hv5)EX_2aI;Q+|kbLyys)VgGidfbWBF2KGk z|C%_Wp7YgykEBBo!AK)Zwf}aE%8l@; z6>?|cQGVHcKx7HG;QB3EC|H2RP#G&-m@-U zpM^D05l)(?`wN`UV9jZFP%b|Cp;SffTSF8bHHRZY9f((>7OLAV1WmNf@qTKu{_FiY zQ^<^{Q#1*}v9fk~WGm*0F#&X9cs~!Ke7paRpQp)4+-twikh7AFivGB&-PS*M_!Z4j zk2oYdlYG1*tICnEY1c( z1S(TsM$SM^j5(au0mcRuj@-&Fo)GmyZY9UG%=b|@zTN!BoUm1}>5$`k1@D=Gb=~OP zzT8<@Rs3^Z?K%=VlDjos23u>-m$AZn^GsdkS0dUD2g)b0%n+e# zRLAe)M6dwiifE4&QtyR(?Oj4wpT?mcSG&NWKxTzN*m*WXfC?D0~U3AL+WMAc%>@2J{r8i>f5i%Y(b*r4n@`mF=JWXwz@d;9jT+|^Qx@^^G>5Y8V%UbY0&2=o_DsFud#fK!u zW7*jDEI%L$#21MjdE~Oy;NI{$=O_(6LgXROFiL?0f&kM;R9NT~T=>4wR#&NOt`T~ff7 z&ySJ|{OHd-B3?!-(gG>G9qaP-a|zdvT8f}nMxPe)y%;u2NpNeJw8-~k+2Ui8z|qh| zCV|7&QiB6DA=Ruf@YLpRZJ$s|?zn|&2aSQq%)NJi;8jKGOBopCLxGmLSM+{lHMwERO}R03xoT{$%{^vJ^*Xm}TZuYaoW`Aw3QN=JY#KB8 znC0fv75bT6Gq2Lr3>6b5j2H~U!8U7e7h(JA==~E7hI$OJW1VrFHgAd4gJVPVY%zz% z!ia-(9j>k-%P_y?w^T1d27+S$kU0rgatEHf^N ziFqoWu9V^|No->Kix>Zw-{5~j+;-`26pZLmjZHhOW-o>tIfQ$>SKr9%(E%m{2)uJ0QE4=^Kp2xU2t8|azGza87jIB%ofFRGQKLCKezxgEq2!#W{iYovhG5|meuBpGsj1n@UvM?IJNFxuOhEh(Uj{=@>oOVd{5E~sHGU1nmM=7_;L(4 zcr8B_6)%ytvPWm_{0yw@%gj8n_#J-`1cQ}K@8;wN6LP+@b6TehEN#O!Pm7%%2Ee)JW1iKan0e@T+BtCifzes-Iei`cfX`GldSy(}giOG>2 zDC*8qA&%CP^#K2=<`$#T3S97aUt+wlu01FRA&?8npdj`@W@hUf;KhEIsk{buJ+{+^ z_ThV`}ZjZ6WVbiWb<8yHOA;P&xd&7S|6mS(dC2nZ-C zQHvUE-gZ+cV}`WR+Qt6vwWle9whG#$(AD~A-Uh15&%5h)7CX*}(Ds;{X-MM_dskeC z%M7)NDJeY$NGq$MZDM-*t@H4Ej2@AhO*%y!Umm=u%HSdK2j%enAvvE6^~9{HL8A5) zmZ7c_4Q|E60=oVNxWo(-uv7kiVO9Iw$>)`Qn>=M@xL-j>kCqF8Ber2R!2W*gb?bZO zDJeIg_;M22-ky?=Gr_K(aKy z)|T9H+VbEiA92Lxl`@pTTv>TW=w_~x--B=6nmiQPKVTK4(c*>m(}7yAmauEgigxq< z?9NhrtIHtiMMcDY?mFLXO0_geHz_WzgPXR{+Y|tdj#33pzge{5j3LRn(FH25etscV zn|m%%R03#Vc+IvrT!DlhsXU1RDjZgQ0TPJ^UAA3SQbOme3z+^Xn4IhKrM^08y1j7H zE3aYavth@oc8ECP`smX1JFhDZHS7$Eqyxynz?ndMFMEN{LZ>M&9tF4qfxBDGXa(m0 z@6le#@E!QLu`>a0x^cQ6)YF3?fwnjFpM%#YS-n=w16kcNN#0cQbOMO*1D@c^pkGi? zk>bywQ0x?(t5&4t&Yj!U7>;1g36GQ`vDgaXV=^+i?AhET^3iyJN2p!~iw4fN zCuyc(&p7i94w`>HToD&vVhe=R6Gyf|K|%3GH#hWE{kPp{pFek*w|ZO@RbxnkfOH3PrCNsFcZSWXB5!oo&|N(A2inO0FT>^}3mP_`}j_;Ck@%a7o#n!6t-pxvPXc518nS^k?hvMc63wiS=?5ERN! z0nB;AH-O;aK`mOHx0wkjI(j=8AhtY;Sen3x%ObB{rKi-eWf3B_+;u)hY-keNBAKk3 zoE+#X3dl*%-5nZsYW{MB<;F-uxgTlIQ2dVuNg3n$HW`K%#lhP0^IJq+vAPGmFI``g z^%b*(H)hp0%Cf^JEYO`f7v^hDRR1!Mj`%6P*R=0_cd59tEa|#3TH@joTPH7c6{Jd} z?tX|QRt03hES%Vvt^Fm0YrA3M+V>Vf7}n^^udEG_M(uHOjFK&{UaHZ-+#3psT z{H!6`X57K<54`n3jzdeS8_=CzfjNKpwR@G^Qw9W+{qaY^-_V~7uHa&-)0n! zBW-M$YwsG~@9ab>5#8Q0%)pzQEZg(rUfb(+Ojr=fd!7_of^dwK=5=K$xAzwm{^CXUv>dN$cjv8BbU$iB z*z4r(4@*L$AC`lRj94nfSVJ<7Hj2Lqdl`JE>mt?E**OMmVHq@yK3LK8^ceKL9LV?* z1_lWTBszBF78k=V@Hje&`9V`sTG7DXkL6vAi@!T!5SKbAOd~jEyX>KvKiX3Cl zEjRU|w^zFtKbG#)bWd_hJYZe>9>WCi>d9DAt;$r#G`C5M9NpcGz1HRR{ZN9eI?sem zOoBsWS|cOEfRv|6`!5KrmI$Fx+epE0%#k32N8_?O8Tgw)YRRpwSm|$HALbfyZEk)A z&uOPMT9uFH&5qbBo9=sX+)oCqtc*y&Qw;l~T-*_L;aI(>eLrAk9VX5@G&HmV%+GI& za%pL`NdgNC#kCxda5$Z!fBDy-PrK_)0g0(2ahS}~QZ+2V`BJQ7Kwxb5fl-T zbr^Bbu7z};wcpU+ZVO+n$Uj3&O-*DbAu$*mwgNL^15X7T8cOux?iNN3t;oVzFqtW%R9>`5D^ zUx^r3QPDY*xwZ}+*3J(5HM0LtCpO^Zlw&8SvWBXwVc)?xQ9$7Y9wROSp|N#~psl?c zHs3Y#r*1EM4zeJjp{IY|_SvP%TNQO1A8b5kqa#`OsFoV~)uzT3T*vsPbahJ-dEO-* z?(hfh?@$eJj!M_QvRURYfJcw~hr76we-m#qGF)4dhn|>)yvmy?)|{aWuPC}?b%;Ch zbIPnlT{3}a6!6`V}CGMvV_A`c$)Bth=q_gV(@^zy9%5Bez?B8?UrL(Cd4>?1z; z==*qUFvNf#){Z5sI_ZkS)g*9Y;=lU&oZH2BkNtJvF+UA?3}c%M!rk6{XzhAzaNlKs zx!NH$Z);6$e4NYwAa+-kY&2Y%^VW!_z0or%um9NbFCWYO!Go~V`kUTXAwT;3WVZtx3`cN4m!*x9DJ^G`%BoR5$9{+`9s!Xi*&?Qzwi1_sq(gZDE&IG;p;U2j;t zJ$1|@8utH_A~~6N><~!rQYfm;=EZY@`PmRdyAV&e5DdZthv5LA4p+aVqNb*zrgc+Y z9dQYcxOiC!4oAS@ONqF+e-mH>JiWc}|6Pzd&~=d`IRCF5LcFn<;1D-#;D6Wjb*!#( PGyvMr3{`o>^|yZjccjyS literal 0 HcmV?d00001 diff --git a/CardSets/ExampleSet/images/pond_guardian.png b/CardSets/ExampleSet/images/pond_guardian.png new file mode 100644 index 0000000000000000000000000000000000000000..082ac9f0d9a7e963e2766120bcf36ca9b399c746 GIT binary patch literal 3571 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 literal 0 HcmV?d00001 diff --git a/TradingCardMod.csproj b/TradingCardMod.csproj index bbbed72..76f4029 100644 --- a/TradingCardMod.csproj +++ b/TradingCardMod.csproj @@ -44,6 +44,12 @@ $(DuckovPath)$(SubPath)UnityEngine.CoreModule.dll + + $(DuckovPath)$(SubPath)UnityEngine.ImageConversionModule.dll + + + $(DuckovPath)$(SubPath)UnityEngine.InputLegacyModule.dll + @@ -57,4 +63,9 @@ + + + + + diff --git a/deploy.sh b/deploy.sh new file mode 100755 index 0000000..e4247c2 --- /dev/null +++ b/deploy.sh @@ -0,0 +1,61 @@ +#!/bin/bash +# Deploy Trading Card Mod to Escape from Duckov +# Usage: ./deploy.sh [--release] + +set -e + +# Configuration +GAME_PATH="/mnt/NV2/SteamLibrary/steamapps/common/Escape from Duckov" +MOD_NAME="TradingCardMod" +MOD_DIR="$GAME_PATH/Duckov_Data/Mods/$MOD_NAME" + +# Build configuration +BUILD_CONFIG="Debug" +if [[ "$1" == "--release" ]]; then + BUILD_CONFIG="Release" +fi + +echo "=== Trading Card Mod Deployment ===" +echo "Build config: $BUILD_CONFIG" +echo "Target: $MOD_DIR" +echo "" + +# Build the project +echo "[1/4] Building project..." +dotnet build TradingCardMod.csproj -c "$BUILD_CONFIG" --verbosity quiet +if [[ $? -ne 0 ]]; then + echo "ERROR: Build failed!" + exit 1 +fi +echo " Build successful" + +# Create mod directory if it doesn't exist +echo "[2/4] Creating mod directory..." +mkdir -p "$MOD_DIR" +mkdir -p "$MOD_DIR/CardSets" + +# Copy mod files +echo "[3/4] Copying mod files..." +cp "bin/$BUILD_CONFIG/netstandard2.1/$MOD_NAME.dll" "$MOD_DIR/" +cp "info.ini" "$MOD_DIR/" + +# Copy preview if it exists +if [[ -f "preview.png" ]]; then + cp "preview.png" "$MOD_DIR/" +fi + +# Copy card sets +echo "[4/4] Copying card sets..." +if [[ -d "CardSets" ]]; then + cp -r CardSets/* "$MOD_DIR/CardSets/" 2>/dev/null || true +fi + +echo "" +echo "=== Deployment Complete ===" +echo "Mod installed to: $MOD_DIR" +echo "" +echo "Contents:" +ls -la "$MOD_DIR/" +echo "" +echo "Card sets:" +ls -la "$MOD_DIR/CardSets/" 2>/dev/null || echo " (none)" diff --git a/remove.sh b/remove.sh new file mode 100755 index 0000000..4ceac6d --- /dev/null +++ b/remove.sh @@ -0,0 +1,46 @@ +#!/bin/bash +# Remove Trading Card Mod from Escape from Duckov +# Usage: ./remove.sh [--backup] + +set -e + +# Configuration +GAME_PATH="/mnt/NV2/SteamLibrary/steamapps/common/Escape from Duckov" +MOD_NAME="TradingCardMod" +MOD_DIR="$GAME_PATH/Duckov_Data/Mods/$MOD_NAME" +BACKUP_DIR="$HOME/.local/share/TradingCardMod_backup" + +echo "=== Trading Card Mod Removal ===" +echo "Target: $MOD_DIR" +echo "" + +# Check if mod exists +if [[ ! -d "$MOD_DIR" ]]; then + echo "Mod not installed at: $MOD_DIR" + exit 0 +fi + +# Backup option +if [[ "$1" == "--backup" ]]; then + echo "[1/2] Creating backup..." + mkdir -p "$BACKUP_DIR" + TIMESTAMP=$(date +%Y%m%d_%H%M%S) + BACKUP_PATH="$BACKUP_DIR/${MOD_NAME}_$TIMESTAMP" + cp -r "$MOD_DIR" "$BACKUP_PATH" + echo " Backup saved to: $BACKUP_PATH" + echo "[2/2] Removing mod..." +else + echo "[1/1] Removing mod..." +fi + +# Remove the mod +rm -rf "$MOD_DIR" + +echo "" +echo "=== Removal Complete ===" +echo "Mod removed from: $MOD_DIR" + +# Show backup location if created +if [[ "$1" == "--backup" ]]; then + echo "Backup location: $BACKUP_PATH" +fi diff --git a/src/ItemExtensions.cs b/src/ItemExtensions.cs new file mode 100644 index 0000000..f436bba --- /dev/null +++ b/src/ItemExtensions.cs @@ -0,0 +1,128 @@ +using System; +using System.Reflection; +using UnityEngine; + +namespace TradingCardMod +{ + /// + /// Extension methods for reflection-based field access on Unity objects. + /// Used to set private fields on cloned game items since we can't use + /// constructors or public setters for internal game types. + /// + public static class ItemExtensions + { + /// + /// Sets a private field value on an object using reflection. + /// + /// The type of the value to set. + /// The object containing the field. + /// The name of the private field. + /// The value to set. + /// Thrown when obj is null. + /// Thrown when field is not found. + public static void SetPrivateField(this object obj, string fieldName, T value) + { + if (obj == null) + throw new ArgumentNullException(nameof(obj)); + + Type type = obj.GetType(); + FieldInfo field = null; + + // Search up the inheritance hierarchy for the field + while (type != null && field == null) + { + field = type.GetField(fieldName, + BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public); + type = type.BaseType; + } + + if (field == null) + { + throw new ArgumentException( + $"Field '{fieldName}' not found on type '{obj.GetType().Name}' or its base types."); + } + + field.SetValue(obj, value); + } + + /// + /// Gets a private field value from an object using reflection. + /// + /// The expected type of the field value. + /// The object containing the field. + /// The name of the private field. + /// The field value cast to type T. + /// Thrown when obj is null. + /// Thrown when field is not found. + /// Thrown when field value cannot be cast to T. + public static T GetPrivateField(this object obj, string fieldName) + { + if (obj == null) + throw new ArgumentNullException(nameof(obj)); + + Type type = obj.GetType(); + FieldInfo field = null; + + // Search up the inheritance hierarchy for the field + while (type != null && field == null) + { + field = type.GetField(fieldName, + BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public); + type = type.BaseType; + } + + if (field == null) + { + throw new ArgumentException( + $"Field '{fieldName}' not found on type '{obj.GetType().Name}' or its base types."); + } + + return (T)field.GetValue(obj); + } + + /// + /// Attempts to set a private field value, logging errors instead of throwing. + /// Useful for non-critical field assignments where failure shouldn't halt execution. + /// + /// The type of the value to set. + /// The object containing the field. + /// The name of the private field. + /// The value to set. + /// True if successful, false otherwise. + public static bool TrySetPrivateField(this object obj, string fieldName, T value) + { + try + { + obj.SetPrivateField(fieldName, value); + return true; + } + catch (Exception ex) + { + Debug.LogWarning($"[TradingCardMod] Failed to set field '{fieldName}': {ex.Message}"); + return false; + } + } + + /// + /// Attempts to get a private field value, returning default on failure. + /// Useful for optional field access where failure shouldn't halt execution. + /// + /// The expected type of the field value. + /// The object containing the field. + /// The name of the private field. + /// The default value to return on failure. + /// The field value if successful, otherwise defaultValue. + public static T TryGetPrivateField(this object obj, string fieldName, T defaultValue = default) + { + try + { + return obj.GetPrivateField(fieldName); + } + catch (Exception ex) + { + Debug.LogWarning($"[TradingCardMod] Failed to get field '{fieldName}': {ex.Message}"); + return defaultValue; + } + } + } +} diff --git a/src/ModBehaviour.cs b/src/ModBehaviour.cs index aa81de8..d183470 100644 --- a/src/ModBehaviour.cs +++ b/src/ModBehaviour.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using UnityEngine; using ItemStatsSystem; using SodaCraft.Localizations; +using Duckov.Utilities; namespace TradingCardMod { @@ -16,8 +17,17 @@ namespace TradingCardMod private static ModBehaviour? _instance; public static ModBehaviour Instance => _instance!; + // Base game item ID to clone (135 is commonly used for collectibles) + private const int BASE_ITEM_ID = 135; + private string _modPath = string.Empty; private List _loadedCards = new List(); + private List _registeredItems = new List(); + private List _createdGameObjects = new List(); + private Tag? _tradingCardTag; + + // Debug: track if we've spawned test items + private int _debugSpawnIndex = 0; /// /// Called when the mod is loaded. Initialize the card system here. @@ -37,6 +47,10 @@ namespace TradingCardMod try { + // Create our custom tag first + _tradingCardTag = TagHelper.GetOrCreateTradingCardTag(); + + // Load and register cards LoadCardSets(); } catch (Exception ex) @@ -69,13 +83,53 @@ 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!"); + } + + /// + /// Update is called every frame. Used for debug input handling. + /// + void Update() + { + // Debug: Press F9 to spawn a card + if (Input.GetKeyDown(KeyCode.F9)) + { + SpawnDebugCard(); + } + } + + /// + /// Spawns a trading card for testing purposes. + /// + private void SpawnDebugCard() + { + if (_registeredItems.Count == 0) + { + Debug.LogWarning("[TradingCardMod] No cards registered to spawn!"); + return; + } + + // Cycle through registered cards + Item cardToSpawn = _registeredItems[_debugSpawnIndex % _registeredItems.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})"); + } + catch (Exception ex) + { + Debug.LogError($"[TradingCardMod] Failed to spawn card: {ex.Message}"); + } } /// /// 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); @@ -91,26 +145,30 @@ namespace TradingCardMod try { - string[] lines = File.ReadAllLines(cardsFile); - int cardCount = 0; + // Use CardParser to load cards + string imagesDirectory = Path.Combine(setDirectory, "images"); + var cards = CardParser.ParseFile(cardsFile, imagesDirectory); - foreach (string line in lines) + foreach (var card in cards) { - // Skip empty lines and comments - if (string.IsNullOrWhiteSpace(line) || line.TrimStart().StartsWith("#")) - continue; - - TradingCard? card = ParseCardLine(line, setDirectory); - if (card != null) + // Validate card + var errors = CardParser.ValidateCard(card); + if (errors.Count > 0) { - _loadedCards.Add(card); - cardCount++; - // TODO: Register card with game's item system - // RegisterCardWithGame(card); + foreach (var error in errors) + { + Debug.LogWarning($"[TradingCardMod] {card.CardName}: {error}"); + } + continue; } + + _loadedCards.Add(card); + + // Register card as game item + RegisterCardWithGame(card); } - Debug.Log($"[TradingCardMod] Loaded {cardCount} cards from {setName}"); + Debug.Log($"[TradingCardMod] Loaded {cards.Count} cards from {setName}"); } catch (Exception ex) { @@ -119,35 +177,181 @@ namespace TradingCardMod } /// - /// Parses a single line from cards.txt into a TradingCard object. - /// Format: CardName | SetName | SetNumber | ImageFile | Rarity | Weight | Value + /// Creates a game item for a trading card using clone + reflection. /// - private TradingCard? ParseCardLine(string line, string setDirectory) + private void RegisterCardWithGame(TradingCard card) { - string[] parts = line.Split('|'); - - if (parts.Length < 7) - { - Debug.LogWarning($"[TradingCardMod] Invalid card line (expected 7 fields): {line}"); - return null; - } - try { - return new TradingCard + // Get base item to clone + Item original = ItemAssetsCollection.GetPrefab(BASE_ITEM_ID); + if (original == null) { - 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()) - }; + Debug.LogError($"[TradingCardMod] Base item ID {BASE_ITEM_ID} not found!"); + return; + } + + // Clone the item + GameObject clone = UnityEngine.Object.Instantiate(original.gameObject); + clone.name = $"TradingCard_{card.SetName}_{card.CardName}"; + UnityEngine.Object.DontDestroyOnLoad(clone); + _createdGameObjects.Add(clone); + + Item item = clone.GetComponent(); + if (item == null) + { + Debug.LogError($"[TradingCardMod] Cloned object has no Item component!"); + return; + } + + // Set item properties via reflection + int typeId = card.GenerateTypeID(); + string locKey = $"TC_{card.SetName}_{card.CardName}".Replace(" ", "_"); + + item.SetPrivateField("typeID", typeId); + item.SetPrivateField("weight", card.Weight); + item.SetPrivateField("value", card.Value); + item.SetPrivateField("displayName", locKey); + item.SetPrivateField("quality", card.GetQuality()); + item.SetPrivateField("order", 0); + item.SetPrivateField("maxStackCount", 1); + + // Set display quality based on rarity + SetDisplayQuality(item, card.GetQuality()); + + // Set tags + item.Tags.Clear(); + + // Add Luxury tag (for selling at shops) + Tag? luxuryTag = TagHelper.GetTargetTag("Luxury"); + if (luxuryTag != null) + { + item.Tags.Add(luxuryTag); + } + + // Add our custom TradingCard tag + if (_tradingCardTag != null) + { + item.Tags.Add(_tradingCardTag); + } + + // Load and set icon + Sprite? cardSprite = LoadSpriteFromFile(card.ImagePath, typeId); + if (cardSprite != null) + { + item.SetPrivateField("icon", cardSprite); + } + else + { + Debug.LogWarning($"[TradingCardMod] Using default icon for {card.CardName}"); + } + + // Set localization + LocalizationManager.SetOverrideText(locKey, card.CardName); + LocalizationManager.SetOverrideText($"{locKey}_Desc", card.GetDescription()); + + // Register with game's item system + if (ItemAssetsCollection.AddDynamicEntry(item)) + { + _registeredItems.Add(item); + Debug.Log($"[TradingCardMod] Registered: {card.CardName} (ID: {typeId})"); + } + else + { + Debug.LogError($"[TradingCardMod] Failed to register {card.CardName}!"); + UnityEngine.Object.Destroy(clone); + _createdGameObjects.Remove(clone); + } } catch (Exception ex) { - Debug.LogWarning($"[TradingCardMod] Failed to parse card line: {line} - {ex.Message}"); + Debug.LogError($"[TradingCardMod] Error registering {card.CardName}: {ex.Message}"); + } + } + + /// + /// Sets the DisplayQuality enum based on quality level. + /// + private void SetDisplayQuality(Item item, int quality) + { + // DisplayQuality is cast from int - matching AdditionalCollectibles pattern + // Values: 0=Common, 2=Uncommon, 3=Rare, 4=Epic, 5=Legendary, 6=Mythic + int displayValue; + switch (quality) + { + case 2: + displayValue = 2; + break; + case 3: + displayValue = 3; + break; + case 4: + displayValue = 4; + break; + case 5: + displayValue = 5; + break; + case 6: + case 7: + displayValue = 6; + break; + default: + displayValue = 0; + break; + } + item.DisplayQuality = (DisplayQuality)displayValue; + } + + /// + /// Loads a sprite from an image file (PNG/JPG). + /// + private Sprite? LoadSpriteFromFile(string imagePath, int itemId) + { + try + { + if (!File.Exists(imagePath)) + { + Debug.LogWarning($"[TradingCardMod] Image not found: {imagePath}"); + return null; + } + + // Read image bytes + byte[] imageData = File.ReadAllBytes(imagePath); + + // Create texture and load image + Texture2D texture = new Texture2D(2, 2, TextureFormat.RGBA32, false); + if (!ImageConversion.LoadImage(texture, imageData)) + { + Debug.LogError($"[TradingCardMod] Failed to load image (not PNG/JPG?): {imagePath}"); + return null; + } + + texture.filterMode = FilterMode.Bilinear; + texture.Apply(); + + // Create sprite from texture + Sprite sprite = Sprite.Create( + texture, + new Rect(0f, 0f, texture.width, texture.height), + new Vector2(0.5f, 0.5f), + 100f + ); + + // Create holder to keep texture/sprite alive + GameObject holder = new GameObject($"CardIcon_{itemId}"); + UnityEngine.Object.DontDestroyOnLoad(holder); + _createdGameObjects.Add(holder); + + // Store references on the holder to prevent GC + var resourceHolder = holder.AddComponent(); + resourceHolder.Texture = texture; + resourceHolder.Sprite = sprite; + + return sprite; + } + catch (Exception ex) + { + Debug.LogError($"[TradingCardMod] Error loading sprite: {ex.Message}"); return null; } } @@ -162,42 +366,41 @@ namespace TradingCardMod // Remove Harmony patches Patches.RemovePatches(); - // TODO: Remove registered items from game - // foreach (var card in _loadedCards) - // { - // ItemAssetsCollection.RemoveDynamicEntry(card.ItemPrefab); - // } + // Remove registered items from game + foreach (var item in _registeredItems) + { + if (item != null) + { + ItemAssetsCollection.RemoveDynamicEntry(item); + } + } + _registeredItems.Clear(); + + // Destroy created GameObjects (including icon holders) + foreach (var go in _createdGameObjects) + { + if (go != null) + { + UnityEngine.Object.Destroy(go); + } + } + _createdGameObjects.Clear(); + + // Clean up tags + TagHelper.Cleanup(); _loadedCards.Clear(); + + Debug.Log("[TradingCardMod] Cleanup complete."); } } /// - /// Represents a trading card's data loaded from a card set file. + /// Component to hold texture and sprite references to prevent garbage collection. /// - public class TradingCard + public class CardResourceHolder : MonoBehaviour { - 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); - } + public Texture2D? Texture; + public Sprite? Sprite; } } diff --git a/src/TagHelper.cs b/src/TagHelper.cs new file mode 100644 index 0000000..8efbb10 --- /dev/null +++ b/src/TagHelper.cs @@ -0,0 +1,124 @@ +using System.Collections.Generic; +using System.Linq; +using Duckov.Utilities; +using SodaCraft.Localizations; +using UnityEngine; + +namespace TradingCardMod +{ + /// + /// Helper class for working with the game's Tag system. + /// Tags are ScriptableObjects used for filtering items in slots. + /// + public static class TagHelper + { + // Track tags we create for cleanup + private static readonly List _createdTags = new List(); + + /// + /// Gets an existing game tag by name. + /// + /// The internal name of the tag (e.g., "Luxury", "Food"). + /// The Tag if found, null otherwise. + public static Tag GetTargetTag(string tagName) + { + return Resources.FindObjectsOfTypeAll() + .FirstOrDefault(t => t.name == tagName); + } + + /// + /// Creates a new custom tag by cloning an existing game tag. + /// If a tag with the same name already exists, returns that tag instead. + /// + /// The internal name for the new tag. + /// The localized display name shown to players. + /// The name of an existing tag to clone (default: "Luxury"). + /// The created or existing tag, or null if template not found. + public static Tag CreateOrCloneTag(string tagName, string displayName, string templateTagName = "Luxury") + { + // Check if tag already exists + Tag existing = Resources.FindObjectsOfTypeAll() + .FirstOrDefault(t => t.name == tagName); + + if (existing != null) + { + Debug.Log($"[TradingCardMod] Tag '{tagName}' already exists, reusing."); + return existing; + } + + // Find template tag to clone + Tag template = Resources.FindObjectsOfTypeAll() + .FirstOrDefault(t => t.name == templateTagName); + + if (template == null) + { + Debug.LogError($"[TradingCardMod] Template tag '{templateTagName}' not found. Cannot create '{tagName}'."); + return null; + } + + // Clone the template + Tag newTag = Object.Instantiate(template); + newTag.name = tagName; + Object.DontDestroyOnLoad(newTag); + + // Track for cleanup + _createdTags.Add(newTag); + + // Set localization for display + LocalizationManager.SetOverrideText($"Tag_{tagName}", displayName); + LocalizationManager.SetOverrideText($"Tag_{tagName}_Desc", ""); + + Debug.Log($"[TradingCardMod] Created custom tag '{tagName}' (display: '{displayName}')."); + return newTag; + } + + /// + /// Gets the "TradingCard" tag, creating it if it doesn't exist. + /// This is the primary tag used to identify and filter trading cards. + /// + /// The TradingCard tag. + public static Tag GetOrCreateTradingCardTag() + { + return CreateOrCloneTag("TradingCard", "Trading Card"); + } + + /// + /// Gets all tags created by this mod. + /// + /// List of tags created by the mod. + public static IReadOnlyList GetCreatedTags() + { + return _createdTags.AsReadOnly(); + } + + /// + /// Cleans up all tags created by the mod. + /// Should be called when the mod is unloaded. + /// + public static void Cleanup() + { + foreach (var tag in _createdTags) + { + if (tag != null) + { + Object.Destroy(tag); + } + } + _createdTags.Clear(); + Debug.Log("[TradingCardMod] TagHelper cleaned up."); + } + + /// + /// Logs all available tags in the game (for debugging). + /// + public static void LogAvailableTags() + { + var tags = Resources.FindObjectsOfTypeAll(); + Debug.Log($"[TradingCardMod] Available tags ({tags.Length}):"); + foreach (var tag in tags.OrderBy(t => t.name)) + { + Debug.Log($" - {tag.name}"); + } + } + } +}