From 39598aa3b99bf0189e77ede99764789392d84b38 Mon Sep 17 00:00:00 2001 From: Christian Semmler Date: Sat, 7 Feb 2026 14:34:57 -0800 Subject: [PATCH] Vehicle textures (#21) * Add vehicle texture editing Parse and serialize Act1State textures in save files. Add a texture picker modal with default presets (from .tex files) and custom uploads persisted to IndexedDB per texture name. Quantize uploaded images against the WDB palette and render texture changes in the 3D preview. Support resetting textures to the WDB default. * Fix vehicle texture not updating when switching save slots Include slot number in the part key so that switching save slots triggers a full part reload with the new slot's textures. * Preload default textures for instant texture picker opening Fetch and parse .tex files in the background when a textured part loads, and pass the results to TexturePickerModal as a prop. The modal no longer fetches on mount, eliminating the loading delay. * Cleanup: parallel fetching, error recovery, dead code removal - Fetch .tex files in parallel with Promise.all instead of sequentially - Clear cached IndexedDB promise on rejection so subsequent calls retry - Remove unused textureOrder array from Act1State parser - Unify selectDefault/applyCustom into single applyTexture function - Remove redundant squareTexture call on already-squared wdbTexture * Add vehicle texture editor to February 2026 changelog * Fix mouseenter error on non-Element targets --- public/CHJETL1.tex | Bin 0 -> 4235 bytes public/CHJETL2.tex | Bin 0 -> 4235 bytes public/CHJETL3.tex | Bin 0 -> 4235 bytes public/CHJETL4.tex | Bin 0 -> 4235 bytes public/CHJETR1.tex | Bin 0 -> 4235 bytes public/CHJETR2.tex | Bin 0 -> 4235 bytes public/CHJETR3.tex | Bin 0 -> 4235 bytes public/CHJETR4.tex | Bin 0 -> 4235 bytes public/CHWIND1.tex | Bin 0 -> 4235 bytes public/CHWIND2.tex | Bin 0 -> 4235 bytes public/CHWIND3.tex | Bin 0 -> 4235 bytes public/CHWIND4.tex | Bin 0 -> 4235 bytes public/Dbfrfn1.tex | Bin 0 -> 16524 bytes public/Dbfrfn2.tex | Bin 0 -> 16524 bytes public/Dbfrfn3.tex | Bin 0 -> 16524 bytes public/Dbfrfn4.tex | Bin 0 -> 16524 bytes public/JSWNSH1.tex | Bin 0 -> 16511 bytes public/JSWNSH2.tex | Bin 0 -> 16484 bytes public/JSWNSH3.tex | Bin 0 -> 16484 bytes public/JSWNSH4.tex | Bin 0 -> 16511 bytes public/jsfrnt1.tex | Bin 0 -> 8325 bytes public/jsfrnt2.tex | Bin 0 -> 8331 bytes public/jsfrnt3.tex | Bin 0 -> 8325 bytes public/jsfrnt4.tex | Bin 0 -> 8331 bytes public/rcback1.tex | Bin 0 -> 16524 bytes public/rcback2.tex | Bin 0 -> 16524 bytes public/rcback3.tex | Bin 0 -> 16524 bytes public/rcback4.tex | Bin 0 -> 16512 bytes public/rcfrnt1.tex | Bin 0 -> 16524 bytes public/rcfrnt2.tex | Bin 0 -> 16524 bytes public/rcfrnt3.tex | Bin 0 -> 16524 bytes public/rcfrnt4.tex | Bin 0 -> 16512 bytes public/rctail1.tex | Bin 0 -> 4227 bytes public/rctail2.tex | Bin 0 -> 4236 bytes public/rctail3.tex | Bin 0 -> 4230 bytes public/rctail4.tex | Bin 0 -> 4236 bytes src/App.svelte | 2 +- src/core/formats/SaveGameParser.js | 87 ++-- src/core/formats/SaveGameSerializer.js | 126 +++++- src/core/formats/TexParser.js | 44 ++ src/core/formats/WdbParser.js | 4 +- src/core/formats/index.js | 3 + src/core/rendering/VehiclePartRenderer.js | 31 ++ src/core/savegame/constants.js | 30 ++ src/core/savegame/imageQuantizer.js | 106 +++++ src/core/savegame/index.js | 14 + src/core/savegame/textureStorage.js | 94 ++++ src/lib/ReadMePage.svelte | 3 +- src/lib/SaveEditorPage.svelte | 2 +- src/lib/save-editor/TexturePickerModal.svelte | 419 ++++++++++++++++++ src/lib/save-editor/VehicleEditor.svelte | 232 +++++++++- workbox-config.cjs | 2 +- 52 files changed, 1141 insertions(+), 58 deletions(-) create mode 100644 public/CHJETL1.tex create mode 100644 public/CHJETL2.tex create mode 100644 public/CHJETL3.tex create mode 100644 public/CHJETL4.tex create mode 100644 public/CHJETR1.tex create mode 100644 public/CHJETR2.tex create mode 100644 public/CHJETR3.tex create mode 100644 public/CHJETR4.tex create mode 100644 public/CHWIND1.tex create mode 100644 public/CHWIND2.tex create mode 100644 public/CHWIND3.tex create mode 100644 public/CHWIND4.tex create mode 100644 public/Dbfrfn1.tex create mode 100644 public/Dbfrfn2.tex create mode 100644 public/Dbfrfn3.tex create mode 100644 public/Dbfrfn4.tex create mode 100644 public/JSWNSH1.tex create mode 100644 public/JSWNSH2.tex create mode 100644 public/JSWNSH3.tex create mode 100644 public/JSWNSH4.tex create mode 100644 public/jsfrnt1.tex create mode 100644 public/jsfrnt2.tex create mode 100644 public/jsfrnt3.tex create mode 100644 public/jsfrnt4.tex create mode 100644 public/rcback1.tex create mode 100644 public/rcback2.tex create mode 100644 public/rcback3.tex create mode 100644 public/rcback4.tex create mode 100644 public/rcfrnt1.tex create mode 100644 public/rcfrnt2.tex create mode 100644 public/rcfrnt3.tex create mode 100644 public/rcfrnt4.tex create mode 100644 public/rctail1.tex create mode 100644 public/rctail2.tex create mode 100644 public/rctail3.tex create mode 100644 public/rctail4.tex create mode 100644 src/core/formats/TexParser.js create mode 100644 src/core/savegame/imageQuantizer.js create mode 100644 src/core/savegame/textureStorage.js create mode 100644 src/lib/save-editor/TexturePickerModal.svelte diff --git a/public/CHJETL1.tex b/public/CHJETL1.tex new file mode 100644 index 0000000000000000000000000000000000000000..8795c99603b4116d01673a93aae65216969c2afe GIT binary patch literal 4235 zcmeHJJ&P1U5N&6mTaoMa=I+42;Dj?06~sV{77Y9eijg3ztC$E17s_I6Vs0eHruzp> z{4oax1`DE9ud2IiW@q-sVA0?jhUu=Zey=`z=A3ix9z9RzFK2IF-F*JynLDm%J^wj( z*XaF--aMZCl6Ot>BS;+fFP*%j)ge9m^sH8^-%Gb#E(_yJdLUwJ?m9z&09M1QG?n$jZ{@Ks&vLDiK~=|3%0`X2Ad)dES4e0r4_x^ z+Fr_*FqlJtHK?=&n_#1t3~+xqAQUr6!qyd6oJ$dEarG@M3WHdCn2LcQ)9B<-6z5ru z9kxgNsCs39sT3?WicwJ-0Sm`W%y*S*S5MU5=d(~lK~$2gUc!!$Pm^{8N|X?C+9}#% z0r`!IoPz?1kOp>|8b@rd8inc*Y+k0+YF)vKTst74yc=L}r7%S)i6+8ntG1cc>;f+k z4mhwXv4e56Ofjc(8VcCisj=jWK|gh>ZbvD4Nu_}m#|2ohL;^dtAu@*;k5J?Y=FOoA z9Fa<+W;2fBfMGM`U)xKd086<5tAwy@_s=YU0KXCye#@qz5W6~vQj*{w?|*Wg-rf=c zTQh3d)IvVB#$jBsK6I>XFTnsCsOQ%Bff!9)Ck;Na7Gnsd{o5oPR>+wTK@66aG!=EW z6qH=6T5K8$6OjxKSGbe@W=K1wLrN$BdKGG~oAM*a5tv;6_~b<#HoY^-jE*Ck3D_g$ z?Auvk1>%*xK$j%lelW(Rn_(1fR${z5uN2p@L%btNMqNu=ON{fEuJWPW{xCz*zgC)s zp*@oVH!|@>xRqXl4Ux|=#w4rJ%^{bxN^)ila_Ok&3|21q<=r6r_`zhKYS>0zsIj4hhC-0 zHfp2O&kU*eGCxh?U;sPXRg={j82r#mj6ct F+#jaJ=0^Yk literal 0 HcmV?d00001 diff --git a/public/CHJETL2.tex b/public/CHJETL2.tex new file mode 100644 index 0000000000000000000000000000000000000000..64183a41b7625ae842520e915d686018bc5500e4 GIT binary patch literal 4235 zcmeHJJ*yNk6pacSXJnFz^B!1OJmFc13SuEv3l{za#Y&LXRcr)>SIS~-V{awaw)+Qc z{4s)s#e(R$Uo)B68F^2z5ck58nIw~Q?ztz)o^sCJ#_!Sc+2ZB%Yfqj&c1Nf2zWly) zm(kuwOY!iBv}f>q$UTn7>#wizc7Wd=ew)qa=i05;>vx|&etUcN)B78%)oQU=ym{b0 zTsd?{S6|%Odv){Pm-7en`TXvF<8JL=LR@g};`P}sb_)D63iuF$|4VFrh;595U_wC= zM@Nm7|Lv&F+G>4hQmiaSFnl%~!`AoK5$d|O=aaS}6-J<{23h)itSZt(6+&H>e}GSQ zrJO=Wpoig8$BdqeG?tuCCE~>Ro>-T44F)-ZDofI`FQPImtOQ<6LwfKsv`||EPmg{^ z*!?d2SAaapdcn+Y3yuCBmtjii+#s@j4^FBACc(;UAH zvwi^tKMJC$Az4S8DvqMsv`D1BprTi30ZsEy=2NNeaqy)kL;PM}!HpMLUIuo#rg_AKEY9;k zHvX*14A?ioaQ`uu!sA)CzHuDnFT0utx!#lc_xC9jYaQ67W?e;0b^xMM;T(x1s_?ZZ z`NN5LoATe5{(}mTJqVe5v2M`XDysX{%H-_IQP>=nC$gh8c?+6ehqOAh(Rnoom&Yw|y zszLiL^lSK}x8$XT#+*8!y3y`016Qj(%LzrURe>0hk~{Q_cy#bY@uDdQ{X>T!2JSE!l{D* literal 0 HcmV?d00001 diff --git a/public/CHJETL3.tex b/public/CHJETL3.tex new file mode 100644 index 0000000000000000000000000000000000000000..e1e7196823d613c40d1e5e808714eb88edbc3c97 GIT binary patch literal 4235 zcmeHKJxc>Y5M8yARC1Sqg++u|i3(yNRsjotf?_4eX%rhlA*BRs8+$9Uw!J@KxDR`$*~QuF@yU_sPlMlk>xm^;_h7~A z;9agWV0S1YC_ndd1zsEOCfvi}@FN#_p5Hz_yxz<{-mMP?gDlIg_r?8kNAy?Dx0{z6 zyU$6x+wJbOQnA@uglk@ig|)`-zY^;jLv6L{Fkq4tH4?kQZTiuGV@3A)=R?GF!A_(C8MCQ@xfS={vx~|GU(4&bvVB3r+0zbo(j^r-=x|Y78YDmk3uG!O+?p0mKy;M=l&~oCBe5BDEEPs?H2w+_Y^gE%l>|C(WxIgf zHm7Zu^?V&?a8ybhWe*4J`~2SPhYTbC0eCbML<%{>Z7rmPGEEU&)F_BsH= NYU9uc82@-$d;$#R&Jq9s literal 0 HcmV?d00001 diff --git a/public/CHJETL4.tex b/public/CHJETL4.tex new file mode 100644 index 0000000000000000000000000000000000000000..5a887db55701711a72163c0f4d4c1acfcb05a300 GIT binary patch literal 4235 zcmc(hy-EW?5P-*c#DE~)UIG>t5n?4Oh=o`MEPMpTN|4hiHiAM*3D!3DR$^^?FJR-t z2o@Fr(b+%uE;qT|P0n-SZgyvP_M4ggxtjriU3^Y@7xDGw#@YEP^lRAeefD4#87O1`hxOf(m9o#Tl|^9x*-?BGJ(xKi@E4# zCe=}-4N^+2qGZW@v+MqWA4OphgenCU^Xs9=WDp2}`>|TSUPi^|O|vPeb=Nv=IF%Gf zEl3r~pK4BJto1ssWPPs6_|;vX z=GV+sq}Pd(Dp_L%Ws}pX4KUA7pFV+Z;g|D~QhbS)Z+RV!4a>PB7b~S;F!%f2XRLwW zAoPeDnAh1H$y-nwfn)017cu@nP%)n;z!c~3t?O^*EUfVzUVZHfyGBeWbY9{%G?^p8;$(>-V6|Nk{F fe0lL>e8O7{XiTW2y1e*>1DQg2Z9}(#bno9n!N;&uX>$y>!dv^5eHJKR%rQ`swy!v6#(f?;pF* z*N@!sjaT>g-`;uny*Zptrw+>G4DVs*wEgi)9U`wI7-Fo?B>sTc?{jZO|lah}!K zVSBWXs#gY>O2J~I7!{=vuyD-8d{?=4^+fG`J_|JzL?y}UCF}_KG-*emLwY>xiu#^k1N(jq#|IG3S@GDW_w`>{;v8#h9B?Uq=W*XSE2U0DL--?fywocPhP}f(>tTg=s2R8fIU*q zzMU0TAYR!EbV<_f2V-2i8Aj1&CC01sN^u=K#5KhFYo%Ej z+A}F|BNJbQTj?d(5cwQqOtKo?9CAskBxlAT_f7$0o2B#vLx~@SCP=w3l*UK)_ugN6LccSb|KdO9pIq3==tTK!`F}*f F{Q(Kc=1l+q literal 0 HcmV?d00001 diff --git a/public/CHJETR2.tex b/public/CHJETR2.tex new file mode 100644 index 0000000000000000000000000000000000000000..04fa132e495d4ebec5302d7e5508009966f6c75e GIT binary patch literal 4235 zcmeHJJ*yNk6pacSXJnFz^B!1OJmFc13SuEv3l{za#Y&LX)lN}(r7YGq_EutTyMMsO zA0t>;EQp@_HItd0k@o}(aW5>HNisR-o_mt)Dd*g6{2nczEndF3_T=efcXS%>%kN8f z8SQ{`wkk2l(ybx7loduHAaQe)sv~x3_0My}z+qtrm;Ln+NX0 zl|y%Q_4%E>S2yo{Ie##p&+qOx?$-V##0BRrUZ3q^r@%j>fDa+~zr@yu*v2RbCKMEL zbktb+-;Ua>t=5Mo#mZs?!)L=WY<+JXp{{FtK4}|LVFapbkfqPZsv=EPA=G902l!N1 z$|+<7dKf-+%;>2|W6AkcB2J9&iFH}mV2~52vLr40A}YheO5nvbqz5lU3$-=y^yp`l z9p-bEWRJd$J)u(!33N#Yu||=wj=Cgr1;~@EM@$AUOOZT2@ya!Mf?OeOkZ#b@V9|_# z?@R~}*qk8hvmiQy1;|_x+cY>8tgUkJ%3Kx6v(a>oKt7GM$}CWs$U;i#=nN7xb4Bz` zZ9<|17;N86k$y+1779v`X@{BV%Na+JNK@9gnXtj<>I&@ENLnePsx4^rGJ&r?&GE}H z>lZ-qqad0Zl6ADH;wY+3i$v-RDtd($&@}&KK9%Yo2VZJ3#P3yw7Td<>PZ*)ahi=M9 z*6ZP8P*xSgjq0xoC7Ic7!~s5Y5sxvP;nOA)arkLqQ_NDYGXcw3I4M-?Kkm)X;ynLj z^Jf3Ci8^=NZva5NJ>phu&f1g6J)`4AW)>Xu02OugH&XGu>3SWDY zKb(lSDgSNhKd1oNgFuSdW={pnPX$6HZo2&o9Eyo&3qBvf!8T(sx2ruz@cRnTMzZ?| z$P(E>N9YU>R_fIP?1*i40O26X%i5iPWB7dn_1Hg%{Q5WejsP(t+A08FCEZK!{29fk z8noX+zlKkGOI})N%&7ya8}0rwaJAaAoKWOi1voD~?LU(km6DkHkB7T2MO=Y&)L7{C lYi0?Fm(VF-fCvFML)}n$C}>M?f5V%@7TUEt1^zY5M8yARC1Sqg++u|i3(yNRsjotf?_4eX|z)mQcAG4v9}Uy+xr7H{usf+ zA|N_5`*AzFIX8E1P{D({ozM5)Z1zGjC4|_5`>=PKU7W8TpB#z)H2A%@o>+o)4_3Sm z-sL(2c84N@@^dd&;I-jy!aW=gKXQ@h`R&ug>&@)r-TGiK$g=EuU)(QuM1SRMyLq{> z`<%49-R@2+6`QR^xaNgeSZnWFCzU_*veq>#D340>TGdxrT6k2v8%8MXtaKow_^3 zQl2M-D&z#E8{+VnZ9B4P2HTd?U`l7p=L=;LJnyW?FeM3m_o}3A>*`qi8c(gn3N})v zN`P&faujNux`vMd5rLxtJ({=!w#|ql@H0H=NdAJkVX5Hd6Xpjpq?Qk5GXeknY&&W= zqju1I{y)9{>f}%K{CV#%y?^iszkg6uW5;<5j_5?-TaOMs2tWhVC`Q|+8Zu6+NsVs+ zf-?q=4j09xK>}2;K&Fzyt!dE&L{|ww35zm65}QHCQepH)|yQ+@o-0%E~~%^6HyxuLCfw MHV%D&@sFp)CvVx#7ytkO literal 0 HcmV?d00001 diff --git a/public/CHJETR4.tex b/public/CHJETR4.tex new file mode 100644 index 0000000000000000000000000000000000000000..17cbca8d1a3319f370e712f9e74c02d8cb9103ed GIT binary patch literal 4235 zcmc(hy-EW?5P-*c#DE~)UIG>t5n?4Oh=o`MEPMpTN|4iNrzoV9U~OY>CDyk00yaL3 zU||suo&9s~a+BNLHf5g;Brk*IAh)hIb=rRHyzSF9mv3lh7%v{#5nO* zRmV&a$c)N`k3?w$bxu`vw@hSUlQtlfZc4hWFDO4Iom1Jl#h)3h8*(8m6WH9dn2T;^ zQXNIwAf?nQN|wwwyY3(OQ4|J2s8UcdzaENA27wT`AFJi-WmJ6LG@F82cdgTgQ%P~u zf>e?GspeG1+Rq}1p<+ql#Uhjwe-T&rNj__652Wjr^K}Yz=Bm7ZBvU3p*5|5>U)}X- ze$8A(dYve#k~LOPHaVTz0Q3Cx=@aM{emNg0#g}OLme@74vxlOmPn1y8dR)!W!QJW&m?Md}vWl{&}{U`U&H6 zX1p3!`hHY25g zKFo=_UAIcNITR4hm~7xMBw@v9PD5Gc!N70Q$OADYhK-4_K@gKs(V z=!HVtz>+Y)kX5LJ=mk=-V8(h>1!QJ{4v}VhFj`LJS202#II@A50fr^aPzf%?6-DU< zoy=@OFm!gea^oY?D{;^vM+KNF(E*`Q5Jr^cG-WDYoF;hiEN9k0rlQ7jBTc3BGZ4AN zUs5I)L8dtFkftP+oit@&{PyGmo5S`x14$wC29k;=2O}Qhr#Vk{OC?La3Sf?GtBCET zNrg$gvqmr*B%K`YJAJg4WcL5Je-|V;1Fe~U(+jM1cHS(6uG?0rwjQXeLeXh%mB{Lm ze3D+<-`kf2IDf*>Q_OVF*%(_oka#%@{F?*ya)`$DB~5$72SRSO_Y#Rv{C`kbtJ_A zciL{fiH!tNt5sh3sr|z}6}^oZ!JiwoY*^RdF9S45{5gml5(5IP+ZQAF{eIm!Ga@lQ zYr6PXhiDj*To&Pz0Tm)Bi)VWM@y*gTDp_7%89R16e%Z|U8I)EHWa&~7WJJazAfwu7 t6ljR7*xRaH9OCQix6I$&$v)W`@0Yil{@!GM-YZ*_Gcw#Q?JA%O`~#<(VO{_L literal 0 HcmV?d00001 diff --git a/public/CHWIND2.tex b/public/CHWIND2.tex new file mode 100644 index 0000000000000000000000000000000000000000..622ec2fa0b5671750e17bdd3503beec2c5e648de GIT binary patch literal 4235 zcmeHJJ&P1U5N#Evot5nm=MD@EPBY?)>HYx| ze~e&Yups)Xs%yGt_vVxn19AJ{4UQkc;Dp_$MfmSEA)2o*}`YJT>e_-7RGAq4-I*!tMFqd|O# zEwy+o1%K@9D0d&a9H-ij!0=`|hS4}7rSZwpU&%zstrD0{SF-eWZrbLNDx@?y2|m@8 zXa_@JfZ=mxMsICCDi@#AQEC+?<^bnh$L55RLAj?mL zfUhQm2XsvtWI@Ub7Qnb7_GwVK1bEAbM*;(NC0!+8mto8bFjbKSztAZwNKoU7=sTEC z8}J8w48AUz6blI}8ASzDU-nxh8Jk8WmtN`O=fxE*4i3`Fb11F9kV$A98;mRx#z-Ty z!DwiP1&}2x_>y(hrDP=^s5XKxqDfd1S%j$gxhVd}_(3!hlhF4%K68;_%@n%zjMnnU zWJ%WQUobp2 ztXV^ny!fA@4+zkfK}e%3L7AHgEJPYTD_Tqv+k#KAQ60jlZPb69o+SY8G&XGZ`d4@W z*3ClrC1-#Fzjm0!vG^oGrI1o*u0G`fjf?!T&ene!xLUoCRAsqVi42AQ1wLy|g+sHK tqoUIjcVEVz^##mYnrIfXM35dBa5&ryspA}}OOa?CBb&BS;Qv;@{REA7sOkU! literal 0 HcmV?d00001 diff --git a/public/CHWIND3.tex b/public/CHWIND3.tex new file mode 100644 index 0000000000000000000000000000000000000000..e732ddb6288328d0faddb25fc58b60cafd345878 GIT binary patch literal 4235 zcmeHKJxc>Y5M8yARC1Sqg++u|i3(yNRsjotf?_3TG-(72(Nco7jlGpv+uk3r@y7@j z76H+j*^k@V&AE5y1{FNG+xdL&&1NqoQ$mO>xDWdmgVUqclfkjLoCd%D))z~#?!k)J z!Mj{%!0u2)P=4;^3cNPlO}Iy+(MKk-EW3Src)gi@yjvd*hrM3!dSBcxcf{q&*>>}M zWA`~}ce~x4Rw_1Ii*U^gv9Q+o{a0dLCDc}{4g)4hUL&z{pIMq%2kk~3tGoiJ+mmV{ zXHDHgztyb>`yPA4VYOTjErI~fPh=jA4)|H#t?R0+76QTtTe*gCeF#t^lpDwsmzZevPMAVg(y1 zQzgK*O*slRPF=&tfQZ1+fF4cU0o!Io5%?LNbR>Vy+^|&e@(J?;8B)uKvYCK?ezqMo zoN+s7KL4NIe|7RFdj7n3nBG5lgx^1?sj=g{1xIuu@U2IO9t5C)X%wSvQwiIg(;HZ>1iXINw_xZio4;jY(1Mq(SQBE(8m5DT#iSojEvm7vk25iG=%60B|Pt;E{)UcknO z5iBeMqO*VQU2bx_o1Evu-R#cp>^C#}b2kS7d-$C8Zu*yJn-~3a7*w#``|QCQ$|ID# zoP0?+kM*|DfaqU(zr|Jyp9Vgo(davdIF27)pFi&FFOS>9;jr87-W|i!dK(5CSNo0Y zox?ZQ>U25>%?Ngzt5}u+R<_t!!Rh|AjNvj(PdH=V1{q{Z;Wr)9Jsn8Fh=vm%?Zi0o zR#nGL5Xh9ug^xsO1658{b+=5UVAD1rlx|A8tS>4*rkzvSxy7FvtQ&G6EECw=vzUu+ zW>OtR+909SDoPg2H@og1_)!!FL8wwtGQS#%Oa_4vxF5^q>t$4Y-ZYznT6e9}hBHZV z)Ph8j{F&xd#@f$3iJ@Xi;Kd>o6Mr68_enmhYY(LBrSo+Pbmq#uePIFOW+edx5+OwjL>qpc=(Sm(LX_zPxqXO|NmFK f@WsWC@df+)T4tUb->u>X~=zjPCUZ$J^ literal 0 HcmV?d00001 diff --git a/public/Dbfrfn1.tex b/public/Dbfrfn1.tex new file mode 100644 index 0000000000000000000000000000000000000000..dcda861dd192ce07dc4c263f5ca7051168a0c81b GIT binary patch literal 16524 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 XI%a{rwErpc5ta8R-Yf8btH6H&qud2_ literal 0 HcmV?d00001 diff --git a/public/Dbfrfn2.tex b/public/Dbfrfn2.tex new file mode 100644 index 0000000000000000000000000000000000000000..1c594eaa53419880fadc42a09baac7fe630ed63a GIT binary patch literal 16524 zcmeHNJ+C846?J>;_xxsYPa7}cN(e0?KoFqKNG98m1PMZfc81-A010UY37QxY5F&&K zASay-_yhb9M1-^f!ANL1=YCY(>aPCs^K61^o?YMfoO|xAa{Ik$Hk-}&@%reeFMsj! z=imL=CoeajZqfhvKOb+tiTcN=S$^_gt)HO%zKm>SzWeu+-=NFayLi2M^X7|x{_%^~ zo7b;j|LYHb{{7!S`}}u@zy0ZZzxwNYuU@@+@#4j=fAI7#-+q7d>34ql!H@p&)vs@U z_}k~tpMUtZ^E==A2DabIO@W&NHwA7AtWjXU-*;Nj+OOI9*tH4R@V)vnSd3wRDIDg& zJsgh}xW3=R?XFJLZy_C4pp)2@8LULVGJ_#Hk6f<|!*MMQdX{^*MYmjf6#_ly3%E+B zWKhrKHKPT51+Ov`DLht!!d3;VJ`{JYakjrUI$uW`8x0IDmmt88;f#-G2A2|e6nwm^ zFC&oQt4^M)qs3s7;1U8e@cZ+5p1~ss%)!G^Fqq?TF@XeM2-@q+p=0@l1j4=^=IX99 z=%4M$zHeHMR@vaTCIQPy-;%7&r&?M%Y`C=&W1{Ek2U9WB$77gS&~*I8r!Z~E#v}gz zQUVh$$EgZ@@30+@M?7LJFK#iSjJADoXCVAk{n>dGX(O^!QV&bZ~GCMtfglGeCE zrcue}h4{G2Ca?1XGIXjpAdwXv4JA-d03*j$M_4lc!aKeNJa3AV`|dn@1uwy;^FZg% zm6=XnAX{u?(11~l;p3^t;K<9e<5Exrz8w-)>|Jc{OR)3_D@!>8`_Uf~M@ zb#N?&b*w+1V$Nf%!eAaB`&6ZjHnM8WPsKsCL z@et)oiw1fOWR85Ix)d6pB{IShtKj}V>%Ztl%lK+bx_qMmN

@KjJ2M zhQG!AH^kQrar8<51p-K>mbCaLEaW7}(cm#KuXBaHB!Ki2Z3+$CqS&4AU-6m2v@2P? z5h#S){I(F(@LL=JTMZ9#4r&Z&RQ@f&CjpS*yC}?9;P|uB3t<#r1mV7bk8MQekC-Kc zp@BT-3k_OLau2NFjg8@U#2;+C|U6yblcMj0ytsf#~>tIEATVA=q_E_sp7XS zFaN|@?uQ}?)S6_FY8=daGyCuI+l-zCU*qt4 zqJ0?Rq%FtF{T{a;K7R57w2B0<8QYTAWsq!;mM!~KYr6@vyt*snZ|FXbe`zfwp#x6i z+W}zah%0*7NXMO}+y>&Il=CS5=kzyzAqh+W3!b54(J}W00eH!Mg;G=QJhxz>xbM`Q zAw~2gl(`xAJ)$1{lw9N>B_Qa4ze9cb@C(jJ5+;1T1O`^(dKVdYY{H=jkUxnHCb}j1 z536na(8lnGIO!>nxEb3LH1^z`0zdsGgR>x@Dd0=B?(1(C7Hsn>00mlf&JnR}4`i0` z7f>lE#VGLO@ng=oP&AyH8+36Qt(lg5psK?P8mg*4+ULTyWI zB!xX<*V+g_;y-R!2)H8wZqO_w(vCiR)KK%7`2TcZEes%Rwmb}0nt740&nV-qhqm2C z|7%LPy$Wa;vQmaX)n%lU4fd9aOyWQQ+4hg## z!WRU9zsHL|G+8OQ|AF7`w!6Cm{c`x6gZ@`dK1U)8D(FoEYzolcp;#0)&8BHw`-w-B zVer6xrM3V`?_1NO0=*B zcaoFNf^{ZSE`Ox+N~n;41l)V^ItjsEi5|a^R()GI*t3dZ69zEU_`zIm2 z_j^xVg6BPWVz~cDJtq*~l5d2}pZ}b^ge3mX!f&-&9e#gzxD?Cf^2gf`Z@zx{_4?H3 z_t&3)I)89*u(PxC;`YYJD_dg!>a!bnzb!6^JFm94w{M+mPhY+W)j6;NtN<&(3a|pK z04u->umY?AE5Hh{0;~WluwDh4c{6N4Gmqy+stz|wa{w{Kz^r&~A$|y=;nIS>fpOEQ zhw&8UG#C`K|5s*`+;aZ+X(@01fBuPBi^GKT?NMZ@x{M8|K+T6{&r)4rwSndh=Hts?PL;B0Pv7+2OmTV^V_Kq!v-K; zh^cQtS$?~TS~>$@*}u)i>=S0#L5fHRzyS2M(zowqSfHTfQvtvNbK|8I`rn^aZCWZX zrCR725U?Qd{Y_Vn6Lq1H2a#2Ef-gew`{PKHdD&e9@A1&Q$U#bAh z0fbQAXoI;0LXhEixveLZpA6HwIqs^E&~ADWx)5}sHtn6$(%VienV>->->rQUvL9Hd zWn@|#Zmp0JZHc@0(ClMKq1VTN8AhxbFPe&jeb{TxVFg$LR)7^?1y})AfE8c`SOHdm N6<`He0ahSKfxn%uIHdpp literal 0 HcmV?d00001 diff --git a/public/Dbfrfn4.tex b/public/Dbfrfn4.tex new file mode 100644 index 0000000000000000000000000000000000000000..d93381c135c867591c9dc986e09c36ae4e74e21f GIT binary patch literal 16524 zcmeHMJF62x6kffJL?oJMDTrLL5G};cCY_~?prwe3*TzB+F4{RS^V&1B^E$H=v6z8mXEKlPJKs5HX0v;&>$;opdH7)UboJ!gqsOc6@Hn&& zejjv~;Cc_PSnmIk*FC83i(fj?fBNU%OK9TfEPS@x?a}vlN1JZ5+5C9>;my}Czh0mC z{Qmm$Pv_U`^>Vpt1=KarLYwf7f+0BLZ%FrC(B(4jv#PEZVb8^+d90ycxH`+;QO`a0I0 z!HzW6ZaQCtK@8CCLjna1V3H{mzr!%#B*Ii|iS`ny79xwJ7hl+-GnjdRVQ4Qv7xUCD za7n{rAfnKfIojqB(CDX3Q6ltAaR|!=;DbyB6uQAy6`)`(uqy)6?JhdDos*p_K$Zg1 zcpthTKqX)ckU>BgA3z@xU`7C(2XiZn`fyUU1iWC-2SI%SAmhWv%N`nqwF#j4DSQ!y zS~zwHMm>-PFVmp+7jF}YgK%D?&Ca3+Z~kZ8cn$2A zM*uqkeEUD&ev^Rg1n}bX36LF_7XUtdFrqvFc{_m5{qRJg1mD~Pa0Kw0Uw}^nfF(cx zzeoUej&tz+@)tTl=WmIt2`HiYOK0K{Bw6Og0R9 zm2gxVz@kZDJpc*7G*`{i_QY(aDGJK+s_4ji0@kQ}2heW60)t`6le-G1)2g)fu=xHpefK4XbMzO;4ijGm45&L literal 0 HcmV?d00001 diff --git a/public/JSWNSH1.tex b/public/JSWNSH1.tex new file mode 100644 index 0000000000000000000000000000000000000000..4061a659ce4a4e1e12bd5c4ca94efc8d3bf46e36 GIT binary patch literal 16511 zcmdU0y^1bH5Sz>ASA#wNak`VRX7CO(W{ zV6Y%qRbAEpU0w6de7!iZO!rK6ol~c(dwS-(cb=x{Q~3M#<@evcy!-gYH(yV$7Uqfyt z{82S{k*?W5&Ew;mST)}gH*?6QV{OE_*CYXqKU#iY0E~Zba29V#0*1>k6HrQUu>Nk% z5pX;YRUj)q$77rXy!gf8V|x&RQhOmXBnMUPg~9bDXlOr*z#JbQG`?tAJ5I7;{1YGj z`|=ky0Vf3ntJ)q0T#9>!bcjMkd%RHs_Q*eP`W%Ib;(cw0t$PUUAUyZpX2}tSnwy45qKo|?K1%0t!%Tp5p8kV$=`jWh{jnb zOEe^H!g3rACqIZ9AF2K6J&swR^RqQ|m-FJJ=fJLSo{N{`%Nu%)!1bdi-i;hso-GIB zj)qoqFhYCbaM8v_2A^`yaxhMNVIOv

JK=4yqfvm0hXSQFmIU*em#EYbM9nZs*e{&8$E4GvC$++WWf+izhu9Lhi*ez|eU1mh+GGFofCr_YW{LaqB%!`sHKxf-0;p3`LYff(m;;cL(F;I42VD4>;;30g9 z##KBgv@@XzKdDhjL&Y;+0&`7J^yVi!6VUj&>8B<<<%030SCU?Y4%eL;UHl#JPOSt| z>kxq+{tkGzSZWE#*dPHNf9cdm>d;M=^qSi1)QdW{yh-x+B1(tNYQ3hm-5%rbNC3Fh z_3vWF22Or;2E^uPv~j2G54WO00^;Kg6vYQ?F3|aTUoI4!<}H=LiOFpeD7OeA!mhd7 z_TRnJkl^~-3$rSLA=(Rrd|Q=&4-&hpoiho>X)h5a3Mg9yu|NAPMeoi-laHZf`VYrX z#(g-|(` zB40T5?|eCbRGT)&m>YLJFJE7li|fay2lLjt{^E0Cu3UZR>b~564fTm@Uv5Xt_U~sO z@2=Nhr|#NpHtY3zwOY;R^Vw`Rolb8r&fU1meGH zkd2X_C#F8KR=ONJ}Q8;j>*+l6TDmG6a0 zc@zjYo5~3%M2}}Lk1Bp0Q1QpS%1*-y`497C`y%-d^TO^bkIH|Tr~HS#qx{2u!L-Vw z%0JBWxAM=B@GO3%JQ$#X#T{JR2`IKRSP#_`+cj9f_xV5D(CoecsRYO3hYeLg>sm}6 zmaF)fK!P!J7*O#KjG`99CI5Ly*nK*G^n9|K8A|z=ff~9Dm-5f!QvP8Ao=;XYLn;3< zP(zpDI=cJ|H$^V#u=abdUP|Cjy z)X-(Plz$!JbnV-e-)^I literal 0 HcmV?d00001 diff --git a/public/JSWNSH3.tex b/public/JSWNSH3.tex new file mode 100644 index 0000000000000000000000000000000000000000..784375e0d9544919102770b8724f409cc488fd7b GIT binary patch literal 16484 zcmcIryRzIw5S13)B_$?v0Vy9q8{GWb)LG5p4 zWRv-?{`s4F{Qpb(|MBC;4Vjtzkc=V)sNqNP2&$7`+mFKj^lW-gW9*- zzCQqUdC7h|?nYaGl8Vo(VZYUsP5(1WxJAGl+B9(ED*4n$cAfHF625Oz4N1Vu@u?M{ z@AqD*?K{*Xi8(%DB_Q-Hzqvf_Co$Mh=1T&p;3KwYm|M!yBnuR-m*ccVD6>HR^0J_q zaPi=cfhvVECI_|XIP?@IejDF5P8XFDA8f+gOGcuUp6V5YeYiiMFlrNCO;K5-?t=40 z;36skrgSB?rgNp9*I=*k->=Xw`bGSEPl<-i`yQ%oR1UVG^;INJ(5AM^ybB_ z{Bs-9@=)=q%6R}3`1AyLH2W-J>YM_f@jsk>=AQ;Y`KKn&g`Wh}wS@f=SRCrKp9%kb zd*cAn(cy%lr^3I)Kdq5xs-K;kWY;{K7vOW@pG9|#pPhK-7)RIkmAU`VaP!)8{z;0K zZ+TTE-99&R$KILkl>a3G%6IS47a(P-zji&#Zz@@bV_E7zT-DJ*8k8KG5megFKQ0lr3pBK=1c0>af{DVCgg`*tWFnmiXR-@f>_=jW6*Q9; zA$stq#b}~!k5ih8LP!oP%&?rNhVRgcPw*cVk>bvl8Diubu3jVQ; zE-=eS{2%)k1$4%50Z3Sj@lTqQ|G}TDCmE#xtnvf$#5K!mUA-Ot;%kTB9`vO}z@L^R zBiz`>K`KjBYjQ@4g5b3{5<+;ER%Ck2Us|2bv{yXBDduH(Ee`b4%9RWzz{roQk;B`C zAM9kg^y>h?gA%ZJC~I8ew}`qwvbsc9#rafZe-eGmn*z^tvR;pw< zQ^5DE4fKiWFaQ>`xk}+rrXj$gZGVm_Z@I?Cvjp1Arh$Qff$elvqk0sbKoh^al1#jV z381Ibg~j=8fX022ZStdYN7Kd?e)??+n-jGPEo+Nv2oFng+lsL2vOqsh{I8bUEvbYz z{ffauQm<(H&drSr>ln#nN53n06z9=(%%9d`XX*#-TTl_WY{~!lbqFLpp;#R6{M!@O ziAUKPeLSt4>Mkf;<+`Cmy(bjqKO`qLqG|T=wDM#F6a~u4`se%#Tp{{J`2znz*BXeR zdce5KZ?|6P2%~G?c@)9R)W>mITuOpZ6{7%ML6=lja+HuNw5FHP%P1~1#SGy5u82pg zXt9bi@AfuA1G?WArd=J@9!-|yB>tj_nS6YaKLi&(7Q`Gk?+el)GMqm<1YCXo&Nl!S zD+d$Gp>7K@J!r8=AL{WT0V8zf@=Dw&O0nHE`2R5a(5R`=H=boAumNR~sH%8@mITBayktzFi@byS# zhmB4XvmP;lNrAE~>Qd4?Xrd_~n3(Me$_5LnDHgPdWY{HqlIFDDz?-Ust`XN;8@nIy z8y$_B3ud(BXb406p8qA15;nh&7gw~@69>h9=Yhgydc_%ZKTdxu)RGsm%sBK+ryY5ge z*WX>k7^8{83sRkspDy5xA6x~{7*)>1EClF&Scd`=fc&_|LV@$N+7ed{2d=f^6t8N} z3+s{Q%`4rc_1J*r)7)!HKdjW`gukJ2U@P+ggx1aCGl3SfC*3-6UHcGh+H zn8ye*oN?_k0s^fGE_$l!MjaXlryYaedb0!gV^AhX?vslAhMz&fXU&dD12bxl`LQ(3 zbN`$wsGomY*LC;W1AfyEYL3kgmVSRJF4N^!ZPc%pj{uCfk-5cx996WYtQ&wo)X_)VOZ#;%r8s0<86ZRx7sW-^_iLXc=olQht?zE z(3j`^1TFj>FQcp=H+OSAW*Mq84BHa==Ie{RF~3EInCJq`2faccwAM0Da#+eNK5P~h zfl&C%1`IB`+76kK`e!P&?%yS_c7t^1X?+=+@W-{m>8gf5lQ4q*`9SgG0EX`b;|?pN zOLQb^Kn5ye?hbalOw(B>Ls4%U55qy~_XV^qu>ZlzzDbt443Ue$Ai?2GmmKr-{lG0!KIND@2#Rl)80TvOLZ_MWZ@c_eZ literal 0 HcmV?d00001 diff --git a/public/JSWNSH4.tex b/public/JSWNSH4.tex new file mode 100644 index 0000000000000000000000000000000000000000..f08f0ead94fe77dead7c4ccc80233b5ac00aa6ab GIT binary patch literal 16511 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 literal 0 HcmV?d00001 diff --git a/public/jsfrnt3.tex b/public/jsfrnt3.tex new file mode 100644 index 0000000000000000000000000000000000000000..ea167676cb4b1faf8c1d8237f48d965466fe8504 GIT binary patch literal 8325 zcmd5>JBwsR6u#ZlYI&}7!tK=1_{%ETgwnTQyOv5CR_ z0Tc0eh|z+9!GNsiJLlAS)P3~r2Wr7gz0dd6SLamKotZUF^9KEV@X6zkKm7F7_uhZp zJe^bh$zM;JmneUSa-?_vmi&y$kGQ4b{s+%Kr`i?$JfxqyySwMVfBXElxxKyp`j;R7 z_~!ese*W#xFIPW(_u9?P&Gq&54=+Dzp1$(Yn=gF!`rE%ge6e}!i>s@v_CHTVecyCa z&ijJX_~P?1=04_CqPSg{m}PZZ*v_yyGJxD1j#p z*=yN0SWI+(&*uexiJ*+B%+_I_wjQHc~H*(VO1Xdr2+oecw#`tKM zN}&gTTz$bmXM8)N^aLPnfURi~g%U zt;6v9)mH-Mg&F?@wSqsWJ{b5_a00|^q6A=E{*t4=I2Q<%FtqT?MvkbJ01UgYAvcSZ zGzL8Uj(|*4H2^G>#GPeV{3TQbFtsa>Q-yE}z?z)?v3lNBGkj1=J|zO|jl z^_w`VmF@aM<6%c#<`&X^6jJ|WK7kEiUy>q9!p9L1PTziGW35Bq>cBGh%ktM9^7tA9 z8R`r7Og$vyqZX10!^G;Tlj~{`hw-eb4iWVE`U5r>ivt{muSV^mns`H+yv)GCiSZSm zCQhytdx&wlNz&AaU;Cfq)2K9SdZoZaN)4}cj-kKETn5BBd;nnLbjUceJTXjt*p36R z0gi81Er!{?;QTu<`t*ilCCzxU1$;k$fvd^j`GFZZPb<@7tHnzd2>|I^-Azz$K+-MZ z#>C1eOe$UQQ?JzvRv)YOl87dF0}P7ab71V8_yLk6i_`ncj19+WMSk<+@~xgGWiUeH zWQ^ointu2hIPkF&{Z;@UdQ5RcvIdUs0WvK9hHBNRmH5X>1ScD^epa83FwWTt6Fx#D zs2!nE=1of>8v$dv0#EqtR~CeEmiXu(0da^tp|92j-)=W8|A_(NEwM+N+Vkg9zMjH! z3#%?k;CH?JI^di8`jx|*OU4p@y_@V4^FANYaOhjGwPkQc#+NwxF*^WcPxv-f3oHQn zfSrS7_4@5I5_|!Cv*9(b1zDnKg$Uqn2Lagcb!HZT`jZ6y{ay1d^#V@Fm()hcf*O9e z@NSM}|5h*4J-5F^;Xe;ccnMAc(JWsVK1aZZk(ESC;&R}LrexDic&U7dKrL{f|G%gv z{WlMTW6e=)Mar2qf` literal 0 HcmV?d00001 diff --git a/public/jsfrnt4.tex b/public/jsfrnt4.tex new file mode 100644 index 0000000000000000000000000000000000000000..d80c799b1166a65c5fb567519e8be8487627c680 GIT binary patch literal 8331 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 literal 0 HcmV?d00001 diff --git a/public/rcback2.tex b/public/rcback2.tex new file mode 100644 index 0000000000000000000000000000000000000000..07055997c1ac38df068dbf26812b31e9e2a60cfc GIT binary patch literal 16524 zcmeHNzpErg5FVWkWOrfRox4zwH_J3=AH~SykOV(_K|vJ-sut{$LyS&CE_ue^p<7Roy-No@<)sE%}5@i0$5A?%2ZlUHYb>`!D|9eh!!Txdoq_o114(zI}Gx zTwh;*{p(Lpzxn>FUw-@J%NKtB?#<7BeCg`y>hkjPhu7{lk6!=e?OTrzFE{UgadC0+ z&h2jV>YeV~&J6qu7-&!2xXYMgCv5E*KV{&|{|VLCG-v&#^6UNs`r9y*&CC7D)YB31 z7mt|u-F|NTwT;K`&W^wDy2EZ{+jjH&4-Rfe!teg4_+#B)!hew8=9~G2ayMYpUKO}3t33NR9kr12S^X$}ckoqrFASc?V)Zms76#p$N4nc=hE_`ZX1TQ1RlULBEdAtFMkH zzC{DHc$)ZH$g1VSRo{Nj%S;CN4n%tiZiQbp7={t#x)g^yz8zEx z;ie$Kq+jXZK=tkTZ0Cx60H>p70YpI5;R3!CfQwf$;krq#+qgFXD__qg1=x=hV1{24 zz+iB&!NJD^y9KqR00>|ykXd>URK~;lv<(hIC(s({O6)$#Enxs!8t1<9yXS!H&J=tc zVV%lW_9Dl42xI^THaW1rW_(4~M=Nd#5B<Yi~tBg{GP6meaN85*dpxELB)?0Rm{GL z_~4!lKvV)p&_C_Pk{}=z;28R64ggrx$7dA%lkg!unE(@f3~#wj%K#<-fb0DqNB#7nB) z0%Vx6kMvXDYWD*Ha!lI$DF&eTjC~>t?Q84Z_ccIfpxgiO5Rijn{}bpx!hK%@@JUXp z+x^C6z|}SZkLtrPR^~oDMU6;?GtO$*{QLdy+NT!Zr0-BFRD6zd-xHwZG;Ax-v$P54 z-R6VbM`;|>Xr}`YYGY#>3{B}lETQ{HiO&s9*25`R0M0R^-v5LF21~2XXdYI|N;=2s zR~RD9k|V|E>@*^vqxRoYl1yVCO~4#L`Dy{~v6K^npU6H@fMZ||)EPkVSRtxbJp{=9 zkOVNpYYZUx$-#G)kOZi>YgwRB52=C4IWhGH8mbK#;47BA1D-frp$4ePj$&YLcu0cY zfU*c&X{OlE<2}+r2DstmER#rpoTnvnJ0Co(xha)t91g0uRaHqf(k%=iH%*ICW1M5qwXSJ~ z&puK4@ zlKXL*?L(?;0uI^UBoKhqbvqs;L$YQS5TNeA7PJN2O~I20Mnc*6ZBVyIrGj!F+(a-A zA_5#^0+?MONB0RiGZ0K<=P(@V>15fik*Fr4MyBuJ?};DoK2&E4S`JF8IvIdghF7A*z3g*dp7>k%;$lJonJ+W8^Gm(4(AsEPGf)rsjsIW9!T~q zyG-ddGwm@z6u($G0-e!EJcv302lPZ>96zuII2OSOKL1=G1JzF=ek22~tUu?IKmdCD zFA{%Yd`BG~z?TFRNC2J{IE2so_!EF9{}s;Vw@*Os0IWG-A6kUN3BVly9`~aKWw^#H z-^WS-7yU)yE&yx=a2mi{fd>fC?uKtC=<$Abj(`D_QqLOfRlg`A9w;8*aKYMXeJ*L1 zy>9rfZUYXwK&S~4AYdm16o*{E7kic)A|aq_+OhyK_)rc|3lvqpGl@p1Y395}EMSh6 z?TLkKuL1@vpmiuR=d2BICgE6iC%gm%==*}`jE=Trb$jf2NI-y{`9OhY=|BRgek?lp z2no=4d8tQEhf_F*u};~9T;D{<2|MeI!Vhpbx5|lJt_3+vfa(6G+Rkxuuo0Hf>4qBA z$GG|&vd&TSeGg;#1@*}3FjU9F54+!@4*W9c~oM2iUE~^pH2I;?C-`!_U&3L_$>ZR!1-IU9dJsw>Cp9 zGxz&DnT019P@$dxf4rH_ZQZ##TpxehP#qo}BJpd!m*RAUzk)l=G*hf~1Ha%w<&EwR zXxYUoM3$r{<9HF~$Av2F2la*?)oZ-@ROb2sQ_{%7 zugFK8+fc*gJ-hW*`MbmI?G%5!TQYok4%!k=>?WHlEB;iPLf zof^5yeDGN-OHE!$9)Jg$UGQWZl0$>^$a=|sqb%&uI%zw_*M9jtuFV?E2*0y7J3Ui| zX92OdQ1gn%oBCa4cIfd@sfh(V9+1OVUR10S_SnO>Xn-^-O2P-~;9b)*cTN0DUQPVV z{9Yyy{zZSKpSN71t>N=FOl{@J4(5O7{8PO&)fsu>@C0p7e`T1zB(fxYQe^M5In`y} zLJnykPrB(c)RkHNg{KAmgZSnE=jfoMMX-Se9EsY=tMH?0XNqbxJcIxWF}*!je{*yR zeECmar+JBKLDKE|UnaFp1a6(1D}WZXmwJ5cwRh+OTl{OT+wG3V z2Z6@BYAs>2Qh;5am<8Q;vFq__b-{E0Y*nrPz< zI*8WZ#4J0t3Bc}vtSEU-(p_2-IO_-EPnD&`9AnhSAPnSxo0XTT>%}bsWMKJbzy8Bl bdCFS?s0iC-V_;)oV_;)oV_;+8R1EwF>mYFh literal 0 HcmV?d00001 diff --git a/public/rcback4.tex b/public/rcback4.tex new file mode 100644 index 0000000000000000000000000000000000000000..76dc7891a0bb720206adb52f5a5ff0ab27114c3a GIT binary patch literal 16512 zcmeHNv5wR*5Dj<5CPlO-M2kdmR0s-MN`8V8I4vBag6c$t_5+X*9YTl#sA(wn2WSxA zfvAuuC{99mZ^p?cYmc3c?RY^r((Yy*d)}KjoF8&a!PgId?mx#@dL7_(dU|^H{mt2`SglsCzI^=g`rXUV z-+sL)KfS&A?8D{da=BP6-d{T`POd+>ed+7PD|J4y0$G8qKvv+NRAA3`r17S-!B71il9RY_1y_59F6JJ7vU4RpViOmYYHUIeRJvqKIeXl+9yu^to|;S zy{!H&)lawSpg3z)cRo)CPg_0%p7uOh{RD+Kt8XSB<^GO0-2X@J|E;$>S^ZsB9?U*E z%892SAk6(fP5s_6AqQ*j_u%PtqFj{g{(RO+GHbP%gaJJ3pv>jJX=y0L&jeY0iH8bD zBIw~C`p9o$u$r6|Z(0deQzcWml?pxsNnh1#9`zA-7OX0V|D*}c9 zkjseGwj_Y1+avX@!zl-Z0aCQAHu@I?b7*q7BMhKLpl@@`xnRz1O%lL8!bL|&zoBT3 zW;_yq$QdDHhlbc0co?HOsAb@gWKMj|7LsWonWx1c_GQ=jh?%C@FlSo?d~ErQ$@sDY z;`~dP|3dv6EGB0D4-S!?N2>?EP=EaK>EAsgCzBYmi0dCFv(uW%IR5(USpNn2!FV8L zt?{n~HQfTheSHUrCI?|VHeC@C(0NY_zIwuh$X^Qy==xbhP(Sdwte~fz0Kx0(Q*ZC^ z*OUN;proi2QtE5KhsiGnK>fg~JH_{)CcHac-PuQ1g&BZ~L<($3pzeT8cOKcRu-gz&xXrsjGu(Yk2COA$8Au$X{Qe|Q0p)A^#6<>2 zAFvCHKBWMRSOFx>yEcr}rwQlOxHqY)br_Db@Z?iODaH;M+a-bzSDz^O5x}Sj>Jz?L z*lljN?L?Xe$2dF)#DrJt(5Oie7hVzAJ_)%z#}K}=!8LwM2_}G7Nw`%ZV)SUK*D1i$ zaF_u_KGwwRfzv7Na0ZI8az0Mk(a`BD6R>Kaz$bEEvEei%5sXx97sE(3PjG&d0h3@B zvlbdnQ=>Bnl?N&o8}F0F-B6=>Gji7n;uR$$ZI%vjSOxtUz)F FigQ0gN&f%< literal 0 HcmV?d00001 diff --git a/public/rcfrnt1.tex b/public/rcfrnt1.tex new file mode 100644 index 0000000000000000000000000000000000000000..1c44503231be98ca5f8eff6b58b42cef8b6f678a GIT binary patch literal 16524 zcmeHOzl$YB5FVXnk@p_E@5dPmA}0o-fvL%Xf&YMkp&)yniQr*y7|F&Wh?$5OsIiH` z{sSiB-yueVfx&{DUv*Ere)Y{)_0?C^J^f~$>ALPs{5^jE;p2}! zdHsVAA9hck#qp!RA9XKd`!2TB-uuUId+gtLXF7L%`)B_d4(ab6{;sdDpZ@;M)2r_4 z>guase*EL>@4o!`x4*u4?x%0x`1FStE-x=HE-t=*^?vu{wU6Jr_v`+}?w!vcJb3W- z^M~~-FKv(Q$iR_-?G|}-hfOkmhv0xZ;&TR${I@dD_bX~WjZUqNv)`J3{c3lZ+HQT) zXuyr|Q}DrzT@wvA!0*=%alhYe&Z%uSXM1;Mx2h*$KK_cZ_8!{ddn23)^&HH_?|D+6$Y^XC} z20q2U;60H611dr=10Tw-fcFLzv!ERxRi_I6{2Ur6R-$Hnu)k+o%O1jjk_Mac!G0Ex zUw{Nkbx7i$nlXd>R^l@YiUgdt;z9WPtzTm z6!79;2cQvu&2P_(yfk2MmVicl;hq>{M1F!GBLg*jBVN4t;O4BiD9>OhkKb$f`2r~9 zr<4~APl*Ig0V?Ug-PYr4_vSWgeMUl5{ColA_r^SPyw$kFV{|y=pNX{cV?2TJ^sB_r zJEVpGKa7uwXHx!DZ8QGO`0M^>1<2#C-Mgkw(2AOVMgs7AYr}g>{;BfcZnJs;emcNa z)E`x&0|Bl0omPJXnSQI(-~0?nPd_68#P|d6lH1Gi$C`rb@-y&x0dRBt=_yAUe<9I) z`N`p136KiF8F1L0&?w~AL-_D-4SyN=8wt>f@cIYMasI{Oks={&k%q{0ESyEJ)Nl&a3;?P9J{&jTlgpe(#vGPj}K&hh%LzD=Q-<8 z->MCO`18^4vhuS8*ii(MK`8b&-3NS99Ae+GpOv2_K%^O@R%HO-4e@ndUVfGUstqW? z8}hU9NdoB{Go^hvLMJ2mruBulT^C6mL zK#lu}03<%#PY4>(u_FnvLy@dt zE5fgHKh6M&Z_rNx;PZr}doBJH0D+%6G zpC`x@AO(xV{LDp%OhBBpqyXHyry>Es^TCHq^#W~oKNMjZ0QZU!pMoh% z;FybrP_;*&IB}*c0dNLPzkX9U^pf6FKTm&#;lgRB00cnLI;_60d}{apg+#TNSL!_X z2LhBCIG7FZUchhHwAC57?{!k}Gke+nZ=HbD0u9}f z@ID6+0a02LFnO>D`;G#V03x8Iw?rM!5WK4n#RwNW1PQbl0C@l0GpFuzi$(+Z=r(%G zcWC+7d4JR3dsCbSslrzp0O>6WKa7NAKusD>?;tPZJ#pX|px1x(LL2i>+}~`};qf00 zsQ3lO4NFci*3WDjLgAdhC&j?vAYPVSj{LzgsssMFcHII$B8O?o5{btsaUvKjIE-IE zxY%zz`1nfToarV*Qx1I$y6Ec84$fB zM_>QY^80$7d;)Z<=wwOY!0D0ybL;4ii;M}Uw;=kM`*?qK8pP%(=P# literal 0 HcmV?d00001 diff --git a/public/rcfrnt2.tex b/public/rcfrnt2.tex new file mode 100644 index 0000000000000000000000000000000000000000..799409e95f9ae2b79a90987e919d60b4aedb9a44 GIT binary patch literal 16524 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 LtGAT`|6YM#nUj5F literal 0 HcmV?d00001 diff --git a/public/rctail3.tex b/public/rctail3.tex new file mode 100644 index 0000000000000000000000000000000000000000..d07c5e812cc11fe83f4b751f1d060da859e36e7b GIT binary patch literal 4230 zcmcgvyNVP+6z!g#qPwPMx}C)j*kFxB12HpM@D~IP6m{qb^-5Oc(dL9W$yz09Z53HpZi`s#c&({TlhadK0f*S`eczU7K@jk-hX@b_Ql69 zKb}_~-ds64I@;UYdv|dsJG}Jp`uQhUZ+_m|&Tc&0-QC5)wOWogLQ78!*Rr$5qKHiN zvLt@VF53U#K&MM`q6CLCs0G9|ubL(hD%i6|*EzyR!A$a|K_P6Ifok1lt_eUp_1<*mLnN#q6!GF!dG;i3zh|gXZ}N|QG_Y-fmOf^ zC<0tqg%w|Y9k&4N5A_fRLN)IF$ zdGHC?fEbdRcH$;7kfJZ6uD}t{ct7!g&4`(E1H__)VXqU_GRt776V})2pVm^ba)1 z3fQJufgt)cAtDL`=JIyVhy{T#JXpL6u$^l`k>DO0W1#rfJa{;T3Q!TSF9fLQRDb~2 zg|wiNbZkir)bD^FiUu4#)Yv%9`)yQ4G(U#-@*~0H9ZQdLtRZ~dnh1o<2tQ-lQm4i2 z`gCb2iBwrynB^n;g+Y3f!;H=I4Q(66sivumD3CVJDz<1seLXm7Q)#&?;zHz9GMjGK zQWlD__bdT4+xjg&R>60KJj8iZisjcc*itq+dBnv?lqt2vHgA1cK%87LmMMiBLqt20 zRyE?{jv3OBA4=n~%q&-zMcb>o_T#K$cD$T9IT9!5as5*Pl6ksu}j zfGK1!VLo^H5gc!D-s#kf`nnwA-Wow1^ zI0_3KM?4o#X>f6r_Zbt-0+CF1_`QTDJ*t5M_I!a|FscHtJ=x^x7OZUa`xhe}cn!|o z8dhDpd$AM-dZ81j9Sz#z=5*R)k&@1*-U=AQ%Ap9q~=5C$Btwh@G~6S*Elgkjt_GeNn*IR|I7IlP?hQ%Y*0x44KMs h&CVn#sM$wg&dQ0v#9qf_XMA9f-#3v(+gbPP_cw|$c4+_r literal 0 HcmV?d00001 diff --git a/public/rctail4.tex b/public/rctail4.tex new file mode 100644 index 0000000000000000000000000000000000000000..ba43831859635c84672a7ac94c318446d8079bde GIT binary patch literal 4236 zcmeHJJ&P1U5N+)ZoLQE&Z_fOX69dse%uE&x`~!l9imcv&iK1|!Y%GG9iHLz1n;7gL zFcE);Ty$VyupnpEtLpBWcII{h4=%XoZnr;PzxS%DduPr#=Wf#fvxm^`~nc=wUp zJ4@r8-#hLSecz*RZukGlx2OI#W*pYfANZFv+@k+Y`d_V9hdMF1}hfw5|!jyT=QFm0n=y>cOP z&_+?KBL{!W)QEakUuMltS23-b$SErIiZS9*MM$ugX^>)KC^QYb1I}*LWuq?+3$3nU z8Li5DuVm0-KN?X?g~}67fuS*uaxJ?w?VOf2fTeDvgc7UtNf*Q#5#y-1q9rmCf=6no zU<4c-z0`~ki)i38nTG*dOgSeU>yste)si$YFZy~)Y=lSmnTUB1gk&MR=S5=F95Iom z0_Z5u)BG1p!?>YM`7{IsY>uu9vkkpaViV#26Dr*DwJJOo*~?4OUtV?W@2?E zj^Nt(6cs{gvm8e|K*YLh1R;}EO?=D|Gf73%SDP9zq0(fv*E>h%wtFeA4J)hZ50N_T ztTa+7`j(?pHchgEG>W@StB&;)@E)BJUx`FI2!($Se7ZmdqGQO~qgo#|U^n@PxtMco zMvQXOUCGAi;fa^sP;B3~%!{)@c9>XQX)0hNC7B+3fgX4aOLL{Uo^f%I5O9n^@~Jsj zk6L)xjz>c39*R(U1dC}?T3!(Li4zshx zNK3sf^CgI((KDB4YNSG-IV(`EJiLPug z!_(hmjE-N=N_(1c!+`B=c{CY!w#lStp+FO^3pmLFYkK49akPy!s(JZ1)Th(o`b9qf O(=f+(+B-oRaDM?ku?SoM literal 0 HcmV?d00001 diff --git a/src/App.svelte b/src/App.svelte index 7b9b68e..50914c3 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -69,7 +69,7 @@ // Desktop: position on hover if (!isTouchDevice) { document.addEventListener('mouseenter', (e) => { - const trigger = e.target.closest('.tooltip-trigger'); + const trigger = e.target.closest?.('.tooltip-trigger'); if (trigger) positionTooltip(trigger); }, true); } diff --git a/src/core/formats/SaveGameParser.js b/src/core/formats/SaveGameParser.js index 7811720..f3e71fd 100644 --- a/src/core/formats/SaveGameParser.js +++ b/src/core/formats/SaveGameParser.js @@ -280,7 +280,7 @@ export class SaveGameParser { } if (name === 'Act1State') { - return this.skipAct1State(); + return this.parseAct1State(); } // Unknown state - this shouldn't happen with valid save files @@ -305,71 +305,86 @@ export class SaveGameParser { } /** - * Skip Act1State (variable length with conditional textures) - * @returns {number} - Number of bytes skipped + * Parse Act1State (variable length with conditional textures) + * @returns {number} - Number of bytes consumed */ - skipAct1State() { + parseAct1State() { const startOffset = this.reader.tell(); // Read 7 named planes - const planeNameLengths = []; + const planes = []; for (let i = 0; i < 7; i++) { const nameLength = this.reader.readS16(); - planeNameLengths.push(nameLength); + let name = ''; if (nameLength > 0) { - this.reader.skip(nameLength); // name + name = this.reader.readString(nameLength); } this.reader.skip(36); // position(12) + direction(12) + up(12) + planes.push({ name, nameLength }); } // Conditional textures based on which planes have names - const helicopterHasName = planeNameLengths[3] > 0; - const jetskiHasName = planeNameLengths[4] > 0; - const dunebuggyHasName = planeNameLengths[5] > 0; - const racecarHasName = planeNameLengths[6] > 0; + const textures = new Map(); - if (helicopterHasName) { - for (let i = 0; i < 3; i++) { - this.skipAct1Texture(); - } + if (planes[3].nameLength > 0) { + for (let i = 0; i < 3; i++) this.readAct1Texture(textures); } - - if (jetskiHasName) { - for (let i = 0; i < 2; i++) { - this.skipAct1Texture(); - } + if (planes[4].nameLength > 0) { + for (let i = 0; i < 2; i++) this.readAct1Texture(textures); } - - if (dunebuggyHasName) { - this.skipAct1Texture(); + if (planes[5].nameLength > 0) { + this.readAct1Texture(textures); } - - if (racecarHasName) { - for (let i = 0; i < 3; i++) { - this.skipAct1Texture(); - } + if (planes[6].nameLength > 0) { + for (let i = 0; i < 3; i++) this.readAct1Texture(textures); } // Final fields - this.reader.skip(2); // cpt_click_dialogue_next_index (S16) - this.reader.skip(1); // played_exit_explanation (U8) + const cptClickDialogueNextIndex = this.reader.readS16(); + const playedExitExplanation = this.reader.readU8(); + + this.parsed.act1State = { + planes, + textures, + startOffset, + endOffset: this.reader.tell(), + cptClickDialogueNextIndex, + playedExitExplanation + }; return this.reader.tell() - startOffset; } /** - * Skip a single Act1 texture + * Read a single Act1 texture into the textures map + * @param {Map} textures - Map to store texture data + * @returns {string} - Texture name */ - skipAct1Texture() { + readAct1Texture(textures) { const nameLength = this.reader.readS16(); + let name = ''; if (nameLength > 0) { - this.reader.skip(nameLength); // name + name = this.reader.readString(nameLength); } + const width = this.reader.readU32(); const height = this.reader.readU32(); - const paletteCount = this.reader.readU32(); - this.reader.skip(paletteCount * 3); // palette (RGB triplets) - this.reader.skip(width * height); // bitmap data + const paletteSize = this.reader.readU32(); + + const palette = []; + for (let i = 0; i < paletteSize; i++) { + palette.push({ + r: this.reader.readU8(), + g: this.reader.readU8(), + b: this.reader.readU8() + }); + } + + const pixels = new Uint8Array(this.reader.slice(width * height)); + + const nameLower = name.toLowerCase(); + textures.set(nameLower, { name, width, height, paletteSize, palette, pixels }); + return nameLower; } /** diff --git a/src/core/formats/SaveGameSerializer.js b/src/core/formats/SaveGameSerializer.js index 71d8b5c..c39110e 100644 --- a/src/core/formats/SaveGameSerializer.js +++ b/src/core/formats/SaveGameSerializer.js @@ -3,7 +3,8 @@ * Uses a "patch in place" approach - copies the original buffer and modifies specific bytes */ import { SaveGameParser } from './SaveGameParser.js'; -import { GameStateTypes, GameStateSizes, Actor } from '../savegame/constants.js'; +import { BinaryWriter } from './BinaryWriter.js'; +import { GameStateTypes, GameStateSizes, Actor, Act1TextureOrder } from '../savegame/constants.js'; /** * Offsets for header fields @@ -337,6 +338,129 @@ export class SaveGameSerializer { } } + /** + * Update an Act1State texture in the save file + * @param {string} textureName - Texture name (e.g. 'chwind.gif') + * @param {{ palette: Array<{r,g,b}>, pixels: Uint8Array, width: number, height: number }} newTextureData + * @param {ArrayBuffer} [buffer] - Optional buffer to use + * @returns {ArrayBuffer|null} - Modified buffer or null on error + */ + updateAct1Texture(textureName, newTextureData, buffer = null) { + const workingBuffer = buffer || this.createCopy(); + + // Re-parse to get fresh Act1State from the working buffer + const freshParser = new SaveGameParser(workingBuffer); + const freshParsed = freshParser.parse(); + const act1State = freshParsed.act1State; + + if (!act1State) { + console.error('Act1State not found in save file'); + return null; + } + + const act1Location = freshParsed.stateLocations.find(loc => loc.name === 'Act1State'); + if (!act1Location) { + console.error('Act1State location not found'); + return null; + } + + const targetKey = textureName.toLowerCase(); + if (!act1State.textures.has(targetKey)) { + console.error(`Texture not found in Act1State: ${textureName}`); + return null; + } + + // Replace texture data, preserving original name + const oldTex = act1State.textures.get(targetKey); + act1State.textures.set(targetKey, { + name: oldTex.name, + width: newTextureData.width, + height: newTextureData.height, + paletteSize: newTextureData.palette.length, + palette: newTextureData.palette, + pixels: newTextureData.pixels + }); + + return this._rebuildAct1State(workingBuffer, act1Location, act1State); + } + + /** + * Rebuild the full buffer with updated Act1State + * @private + */ + _rebuildAct1State(sourceBuffer, act1Location, act1State) { + const writer = new BinaryWriter(sourceBuffer.byteLength + 4096); + const srcArray = new Uint8Array(sourceBuffer); + + // Write 7 planes + let readOffset = act1Location.dataOffset; + for (const plane of act1State.planes) { + writer.writeS16(plane.nameLength); + readOffset += 2; + if (plane.nameLength > 0) { + writer.writeString(plane.name); + readOffset += plane.nameLength; + } + // Copy 36 bytes of position/direction/up from source + writer.writeBytes(srcArray.slice(readOffset, readOffset + 36)); + readOffset += 36; + } + + // Write conditional textures in correct order + const vehicleOrder = ['helicopter', 'jetski', 'dunebuggy', 'racecar']; + const planeIndices = [3, 4, 5, 6]; + + for (let v = 0; v < vehicleOrder.length; v++) { + const vehicleName = vehicleOrder[v]; + const planeIdx = planeIndices[v]; + if (act1State.planes[planeIdx].nameLength <= 0) continue; + + const textureNames = Act1TextureOrder[vehicleName]; + for (const texName of textureNames) { + const texKey = texName.toLowerCase(); + const tex = act1State.textures.get(texKey); + if (!tex) continue; + + writer.writeS16(tex.name.length); + writer.writeString(tex.name); + writer.writeU32(tex.width); + writer.writeU32(tex.height); + writer.writeU32(tex.paletteSize); + for (const color of tex.palette) { + writer.writeU8(color.r); + writer.writeU8(color.g); + writer.writeU8(color.b); + } + writer.writeBytes(tex.pixels); + } + } + + // Write final fields + writer.writeS16(act1State.cptClickDialogueNextIndex); + writer.writeU8(act1State.playedExitExplanation); + + const newAct1Data = writer.toUint8Array(); + const oldAct1Size = act1Location.dataSize; + const newAct1Size = newAct1Data.length; + const sizeDiff = newAct1Size - oldAct1Size; + + // Build final buffer + const newBuffer = new ArrayBuffer(sourceBuffer.byteLength + sizeDiff); + const newArray = new Uint8Array(newBuffer); + + // Copy everything before Act1State data + newArray.set(srcArray.slice(0, act1Location.dataOffset)); + + // Write new Act1State data + newArray.set(newAct1Data, act1Location.dataOffset); + + // Copy everything after old Act1State data + const afterOld = act1Location.dataOffset + oldAct1Size; + newArray.set(srcArray.slice(afterOld), act1Location.dataOffset + newAct1Size); + + return newBuffer; + } + /** * Get the byte offset for a mission score * @param {string} missionType diff --git a/src/core/formats/TexParser.js b/src/core/formats/TexParser.js new file mode 100644 index 0000000..f74b9d8 --- /dev/null +++ b/src/core/formats/TexParser.js @@ -0,0 +1,44 @@ +/** + * Parser for .tex texture files + * Format: U32 num_textures, then per texture: + * U32 name_buffer_length + name (null-terminated within buffer) + * + U32 width + U32 height + U32 palette_size + * + RGB[palette_size] + pixels[width*height] + */ +import { BinaryReader } from './BinaryReader.js'; + +/** + * Parse a .tex file buffer + * @param {ArrayBuffer} buffer - Raw .tex file contents + * @returns {{ textures: Array<{ name: string, width: number, height: number, paletteSize: number, palette: Array<{r,g,b}>, pixels: Uint8Array }> }} + */ +export function parseTex(buffer) { + const reader = new BinaryReader(buffer); + const numTextures = reader.readU32(); + const textures = []; + + for (let i = 0; i < numTextures; i++) { + const nameBufferLength = reader.readU32(); + const nameRaw = reader.readString(nameBufferLength); + const name = nameRaw.split('\0')[0].toLowerCase(); + + const width = reader.readU32(); + const height = reader.readU32(); + const paletteSize = reader.readU32(); + + const palette = []; + for (let j = 0; j < paletteSize; j++) { + palette.push({ + r: reader.readU8(), + g: reader.readU8(), + b: reader.readU8() + }); + } + + const pixels = new Uint8Array(reader.slice(width * height)); + + textures.push({ name, width, height, paletteSize, palette, pixels }); + } + + return { textures }; +} diff --git a/src/core/formats/WdbParser.js b/src/core/formats/WdbParser.js index 66eaf50..3e08deb 100644 --- a/src/core/formats/WdbParser.js +++ b/src/core/formats/WdbParser.js @@ -310,8 +310,10 @@ export class WdbParser { } // Packed vertex/normal counts + // Game source (legolod.cpp): numVerts = lower16 & MAXSHORT (bits 0-14), bit 15 is a flag + // numNormals = (upper16 >> 1) & MAXSHORT (bits 17-31) const vertexNormalCounts = this.reader.readU32(); - const vertexCount = vertexNormalCounts & 0xFFFF; + const vertexCount = vertexNormalCounts & 0x7FFF; const normalCount = (vertexNormalCounts >> 17) & 0x7FFF; const numTextureVertices = this.reader.readS32(); diff --git a/src/core/formats/index.js b/src/core/formats/index.js index 96ea9e3..a6d3ca4 100644 --- a/src/core/formats/index.js +++ b/src/core/formats/index.js @@ -16,3 +16,6 @@ export { SaveGameSerializer, createSerializer } from './SaveGameSerializer.js'; // Players format export { PlayersParser, parsePlayers } from './PlayersParser.js'; export { PlayersSerializer, createPlayersSerializer } from './PlayersSerializer.js'; + +// Texture format +export { parseTex } from './TexParser.js'; diff --git a/src/core/rendering/VehiclePartRenderer.js b/src/core/rendering/VehiclePartRenderer.js index 477f81f..7a872bf 100644 --- a/src/core/rendering/VehiclePartRenderer.js +++ b/src/core/rendering/VehiclePartRenderer.js @@ -73,6 +73,8 @@ export class VehiclePartRenderer { const texture = new THREE.CanvasTexture(canvas); texture.minFilter = THREE.NearestFilter; texture.magFilter = THREE.NearestFilter; + texture.wrapS = THREE.RepeatWrapping; + texture.wrapT = THREE.RepeatWrapping; return texture; } @@ -167,6 +169,9 @@ export class VehiclePartRenderer { } const threeMesh = new THREE.Mesh(geometry, material); + if (meshTextureName) { + threeMesh.userData.textureName = meshTextureName; + } this.modelGroup.add(threeMesh); // Track colorable meshes @@ -252,6 +257,32 @@ export class VehiclePartRenderer { return geometry; } + /** + * Update texture on meshes matching a given texture name + * @param {string} textureName - Texture name to match (case-insensitive) + * @param {{ width: number, height: number, palette: Array<{r,g,b}>, pixels: Uint8Array }} textureData + */ + updateTexture(textureName, textureData) { + if (!this.modelGroup) return; + + const newTexture = this.createTexture(textureData); + const targetName = textureName.toLowerCase(); + + this.modelGroup.traverse((child) => { + if (!(child instanceof THREE.Mesh)) return; + if (child.userData.textureName !== targetName) return; + + const oldMap = child.material.map; + child.material.map = newTexture; + // Set color to white so texture isn't tinted by the fallback color + child.material.color.setRGB(1, 1, 1); + child.material.needsUpdate = true; + if (oldMap && oldMap !== newTexture) oldMap.dispose(); + }); + + this.renderer.render(this.scene, this.camera); + } + /** * Update color of colorable meshes without reloading geometry */ diff --git a/src/core/savegame/constants.js b/src/core/savegame/constants.js index 42cf79e..eb6c8cf 100644 --- a/src/core/savegame/constants.js +++ b/src/core/savegame/constants.js @@ -227,6 +227,36 @@ export const VehicleNames = Object.freeze({ racecar: 'Race Car' }); +// Act1State plane indices for each vehicle type (indices into the 7-plane array) +export const Act1PlaneIndices = Object.freeze({ + helicopter: 3, + jetski: 4, + dunebuggy: 5, + racecar: 6 +}); + +// Parts that have UV-mapped texture regions in Act1State +// textureName: name stored in Act1State, texFiles: base names of 4 default .tex files +export const TexturedParts = Object.freeze({ + chwindy1: { textureName: 'chwind.gif', texFiles: ['CHWIND1', 'CHWIND2', 'CHWIND3', 'CHWIND4'], vehicle: 'helicopter' }, + chljety1: { textureName: 'chjetl.gif', texFiles: ['CHJETL1', 'CHJETL2', 'CHJETL3', 'CHJETL4'], vehicle: 'helicopter' }, + chrjety1: { textureName: 'chjetr.gif', texFiles: ['CHJETR1', 'CHJETR2', 'CHJETR3', 'CHJETR4'], vehicle: 'helicopter' }, + jsfrnty5: { textureName: 'jsfrnt.gif', texFiles: ['jsfrnt1', 'jsfrnt2', 'jsfrnt3', 'jsfrnt4'], vehicle: 'jetski' }, + jswnshy5: { textureName: 'jswnsh.gif', texFiles: ['JSWNSH1', 'JSWNSH2', 'JSWNSH3', 'JSWNSH4'], vehicle: 'jetski' }, + dbfrfny4: { textureName: 'dbfrfn.gif', texFiles: ['Dbfrfn1', 'Dbfrfn2', 'Dbfrfn3', 'Dbfrfn4'], vehicle: 'dunebuggy' }, + rcfrnty6: { textureName: 'rcfrnt.gif', texFiles: ['rcfrnt1', 'rcfrnt2', 'rcfrnt3', 'rcfrnt4'], vehicle: 'racecar' }, + rcbacky6: { textureName: 'rcback.gif', texFiles: ['rcback1', 'rcback2', 'rcback3', 'rcback4'], vehicle: 'racecar' }, + rctailya: { textureName: 'rctail.gif', texFiles: ['rctail1', 'rctail2', 'rctail3', 'rctail4'], vehicle: 'racecar' } +}); + +// Texture write order per vehicle in Act1State (from isle.cpp Act1State::Serialize) +export const Act1TextureOrder = Object.freeze({ + helicopter: ['chwind.gif', 'chjetl.gif', 'chjetr.gif'], + jetski: ['jsfrnt.gif', 'jswnsh.gif'], + dunebuggy: ['dbfrfn.gif'], + racecar: ['rcfrnt.gif', 'rcback.gif', 'rctail.gif'] +}); + // Vehicle part color definitions - 43 parts total (from legogamestate.cpp) export const VehiclePartColors = Object.freeze({ dunebuggy: [ diff --git a/src/core/savegame/imageQuantizer.js b/src/core/savegame/imageQuantizer.js new file mode 100644 index 0000000..2dd4873 --- /dev/null +++ b/src/core/savegame/imageQuantizer.js @@ -0,0 +1,106 @@ +/** + * Image quantization utility for converting uploaded images to palette-indexed format + */ + +/** + * Quantize an image to a palette-indexed format suitable for the game. + * The WDB palette is padded to 256 entries with black — matching how + * LegoTextureInfo::Create() builds the DirectDraw surface palette: + * indices 0..paletteSize-1 get the WDB colors, the rest are {0,0,0}. + * + * @param {HTMLImageElement} img - Source image + * @param {number} targetWidth - Target width in pixels + * @param {number} targetHeight - Target height in pixels + * @param {Array<{r:number,g:number,b:number}>} basePalette - WDB palette to quantize against + * @returns {{ palette: Array<{r:number,g:number,b:number}>, pixels: Uint8Array }} + */ +export function quantizeImage(img, targetWidth, targetHeight, basePalette) { + // Pad to 256 entries with black, mirroring the game's surface palette + const palette = basePalette.slice(); + while (palette.length < 256) { + palette.push({ r: 0, g: 0, b: 0 }); + } + + // Resize to target dimensions + const canvas = document.createElement('canvas'); + canvas.width = targetWidth; + canvas.height = targetHeight; + const ctx = canvas.getContext('2d'); + ctx.drawImage(img, 0, 0, targetWidth, targetHeight); + + const imageData = ctx.getImageData(0, 0, targetWidth, targetHeight); + const rgba = imageData.data; + const pixelCount = targetWidth * targetHeight; + + // Build lookup cache for fast nearest-color matching + const paletteLookup = new Map(); + + // Map each pixel to nearest palette entry + const pixels = new Uint8Array(pixelCount); + for (let i = 0; i < pixelCount; i++) { + const r = rgba[i * 4]; + const g = rgba[i * 4 + 1]; + const b = rgba[i * 4 + 2]; + const key = (r << 16) | (g << 8) | b; + + if (paletteLookup.has(key)) { + pixels[i] = paletteLookup.get(key); + } else { + // Find nearest color in palette + let bestIdx = 0; + let bestDist = Infinity; + for (let j = 0; j < palette.length; j++) { + const dr = r - palette[j].r; + const dg = g - palette[j].g; + const db = b - palette[j].b; + const dist = dr * dr + dg * dg + db * db; + if (dist < bestDist) { + bestDist = dist; + bestIdx = j; + } + } + pixels[i] = bestIdx; + paletteLookup.set(key, bestIdx); + } + } + + return { palette, pixels }; +} + +/** + * Square a palette-indexed texture by duplicating rows or columns. + * Mirrors the game's LegoImage::Read(p_square=TRUE) behavior. + * @param {{ width: number, height: number, palette: Array<{r,g,b}>, pixels: Uint8Array, paletteSize?: number }} tex + * @returns {{ width: number, height: number, palette: Array<{r,g,b}>, pixels: Uint8Array, paletteSize: number }} + */ +export function squareTexture(tex) { + const { width, height, palette, pixels } = tex; + if (width === height) return tex; + + const size = Math.max(width, height); + const squared = new Uint8Array(size * size); + + if (width > height) { + const factor = width / height; + for (let x = 0; x < width; x++) { + for (let y = 0; y < height; y++) { + const src = pixels[y * width + x]; + for (let k = 0; k < factor; k++) { + squared[(y * factor + k) * size + x] = src; + } + } + } + } else { + const factor = height / width; + for (let y = 0; y < height; y++) { + for (let x = 0; x < width; x++) { + const src = pixels[y * width + x]; + for (let k = 0; k < factor; k++) { + squared[y * size + (x * factor + k)] = src; + } + } + } + } + + return { width: size, height: size, palette, pixels: squared, paletteSize: palette.length }; +} diff --git a/src/core/savegame/index.js b/src/core/savegame/index.js index a17593e..14e2e7c 100644 --- a/src/core/savegame/index.js +++ b/src/core/savegame/index.js @@ -90,6 +90,7 @@ export async function listSaveSlots() { header: null, missions: null, variables: null, + act1State: null, playerName: null, buffer: null }; @@ -102,6 +103,7 @@ export async function listSaveSlots() { slot.header = parsed.header; slot.missions = parsed.missions; slot.variables = parsed.variables; + slot.act1State = parsed.act1State || null; slot.buffer = buffer; // Try to get player name @@ -165,6 +167,7 @@ export async function loadSaveSlot(slotNumber) { header: parsed.header, missions: parsed.missions, variables: parsed.variables, + act1State: parsed.act1State || null, playerName, buffer }; @@ -235,6 +238,17 @@ export async function updateSaveSlot(slotNumber, updates) { } } + // Apply texture update + if (updates.texture) { + const { textureName, textureData } = updates.texture; + const texSerializer = createSerializer(newBuffer); + const result = texSerializer.updateAct1Texture(textureName, textureData); + if (result) { + newBuffer = result; + modified = true; + } + } + // Only save if something was actually modified if (!modified) { return slot; diff --git a/src/core/savegame/textureStorage.js b/src/core/savegame/textureStorage.js new file mode 100644 index 0000000..c54af23 --- /dev/null +++ b/src/core/savegame/textureStorage.js @@ -0,0 +1,94 @@ +const DB_NAME = 'isle-pizza-textures'; +const DB_VERSION = 1; +const STORE_NAME = 'custom-textures'; + +let dbPromise = null; + +function openDB() { + if (dbPromise) return dbPromise; + + dbPromise = new Promise((resolve, reject) => { + const request = indexedDB.open(DB_NAME, DB_VERSION); + + request.onupgradeneeded = () => { + const db = request.result; + if (db.objectStoreNames.contains(STORE_NAME)) { + db.deleteObjectStore(STORE_NAME); + } + const store = db.createObjectStore(STORE_NAME, { keyPath: 'id' }); + store.createIndex('textureName', 'textureName', { unique: false }); + }; + + request.onsuccess = () => resolve(request.result); + request.onerror = () => reject(request.error); + }).catch((err) => { + dbPromise = null; + throw err; + }); + + return dbPromise; +} + +/** + * Save a processed (quantized + squared) custom texture to IndexedDB. + * @param {{ width: number, height: number, paletteSize: number, palette: Array<{r:number,g:number,b:number}>, pixels: Uint8Array }} textureData + * @param {string} textureName - The game texture this was quantized for (e.g. 'rcfrnt.gif') + * @returns {Promise} The generated id + */ +export async function saveCustomTexture(textureData, textureName) { + const db = await openDB(); + const id = crypto.randomUUID(); + const record = { + id, + timestamp: Date.now(), + textureName, + width: textureData.width, + height: textureData.height, + paletteSize: textureData.paletteSize, + palette: textureData.palette, + pixels: textureData.pixels + }; + + return new Promise((resolve, reject) => { + const tx = db.transaction(STORE_NAME, 'readwrite'); + tx.objectStore(STORE_NAME).put(record); + tx.oncomplete = () => resolve(id); + tx.onerror = () => reject(tx.error); + }); +} + +/** + * List custom textures for a specific game texture, sorted by timestamp descending. + * @param {string} textureName - Filter by game texture name (e.g. 'rcfrnt.gif') + * @returns {Promise} + */ +export async function listCustomTextures(textureName) { + const db = await openDB(); + + return new Promise((resolve, reject) => { + const tx = db.transaction(STORE_NAME, 'readonly'); + const request = tx.objectStore(STORE_NAME).index('textureName').getAll(textureName); + request.onsuccess = () => { + const results = request.result; + results.sort((a, b) => b.timestamp - a.timestamp); + resolve(results); + }; + request.onerror = () => reject(request.error); + }); +} + +/** + * Delete a custom texture by id. + * @param {string} id + * @returns {Promise} + */ +export async function deleteCustomTexture(id) { + const db = await openDB(); + + return new Promise((resolve, reject) => { + const tx = db.transaction(STORE_NAME, 'readwrite'); + tx.objectStore(STORE_NAME).delete(id); + tx.oncomplete = () => resolve(); + tx.onerror = () => reject(tx.error); + }); +} diff --git a/src/lib/ReadMePage.svelte b/src/lib/ReadMePage.svelte index 5056991..974d4c9 100644 --- a/src/lib/ReadMePage.svelte +++ b/src/lib/ReadMePage.svelte @@ -36,7 +36,8 @@ { id: 'cl0', title: 'February 2026', items: [ { type: 'New', text: 'Save Editor lets you view and modify save files — change your player name, character, and high scores directly from the browser' }, { type: 'New', text: 'Sky Color Editor allows customizing the island sky gradient colors in your save file' }, - { type: 'New', text: 'Vehicle Part Editor enables modifying vehicle parts and colors with a 3D preview' } + { type: 'New', text: 'Vehicle Part Editor enables modifying vehicle parts and colors with a 3D preview' }, + { type: 'New', text: 'Vehicle Texture Editor lets you customize vehicle textures with default presets or your own uploaded images' } ]}, { id: 'cl1', title: 'January 2026', items: [ { type: 'New', text: 'Debug menu for developers and power users. Tap the LEGO Island logo 5 times to unlock OGEL mode and access debug features like teleporting to locations, switching acts, and playing animations' }, diff --git a/src/lib/SaveEditorPage.svelte b/src/lib/SaveEditorPage.svelte index 44ec3f6..54794eb 100644 --- a/src/lib/SaveEditorPage.svelte +++ b/src/lib/SaveEditorPage.svelte @@ -120,7 +120,7 @@ if (updated) { slots = slots.map(s => s.slotNumber === selectedSlot - ? { ...s, variables: updated.variables } + ? { ...s, variables: updated.variables, act1State: updated.act1State } : s ); } diff --git a/src/lib/save-editor/TexturePickerModal.svelte b/src/lib/save-editor/TexturePickerModal.svelte new file mode 100644 index 0000000..38ad4a3 --- /dev/null +++ b/src/lib/save-editor/TexturePickerModal.svelte @@ -0,0 +1,419 @@ + + + + + +

+ + diff --git a/src/lib/save-editor/VehicleEditor.svelte b/src/lib/save-editor/VehicleEditor.svelte index 8861161..058695f 100644 --- a/src/lib/save-editor/VehicleEditor.svelte +++ b/src/lib/save-editor/VehicleEditor.svelte @@ -7,11 +7,16 @@ VehicleWorlds, VehicleModels, VehicleNames, - VehiclePartColors + VehiclePartColors, + TexturedParts, + Act1PlaneIndices } from '../../core/savegame/constants.js'; + import { squareTexture } from '../../core/savegame/imageQuantizer.js'; + import { parseTex } from '../../core/formats/TexParser.js'; import NavButton from '../NavButton.svelte'; import ResetButton from '../ResetButton.svelte'; import EditorTooltip from '../EditorTooltip.svelte'; + import TexturePickerModal from './TexturePickerModal.svelte'; export let slot; export let onUpdate = () => {}; @@ -37,6 +42,12 @@ // Track current loaded part to avoid redundant reloads let loadedPartKey = null; + // Texture modal state + let showTextureModal = false; + let texturePalette = null; + let wdbTexture = null; + let preloadedDefaults = null; + // Current part info from flat list $: currentEntry = allParts[globalIndex]; $: vehicle = currentEntry?.vehicle || 'dunebuggy'; @@ -48,7 +59,20 @@ : 'lego red'; // Check if current color differs from default - $: isDefault = currentPart && currentColorValue === currentPart.defaultColor; + $: isDefaultColor = currentPart && currentColorValue === currentPart.defaultColor; + + // Texture info for current part (if it's a textured part) + $: textureInfo = currentPart ? TexturedParts[currentPart.part] || null : null; + + // Check if vehicle has a plane in Act1State (vehicle is placed in world) + $: vehicleHasPlane = (() => { + if (!textureInfo || !slot?.act1State) return false; + const planeIdx = Act1PlaneIndices[textureInfo.vehicle]; + return planeIdx !== undefined && slot.act1State.planes[planeIdx]?.nameLength > 0; + })(); + + // Can edit texture: part has texture info AND vehicle plane exists in Act1State + $: canEditTexture = textureInfo && vehicleHasPlane; onMount(async () => { try { @@ -81,14 +105,30 @@ renderer?.dispose(); }); - // Reload part when index changes + // Reload part when index or slot changes $: if (renderer && !loading && currentPart) { - const partKey = `${vehicle}-${globalIndex}`; + const partKey = `${slot?.slotNumber}-${vehicle}-${globalIndex}`; if (partKey !== loadedPartKey) { loadCurrentPart(); } } + // Check if current texture matches the WDB default. + // Declared after the loadCurrentPart block: Svelte 5 runs legacy $: effects + // in source order, and wdbTexture must be set before this evaluates. + function isTextureDefault(info, wdbTex, act1Textures) { + if (!info || !wdbTex || !act1Textures) return true; + const act1Tex = act1Textures.get(info.textureName.toLowerCase()); + if (!act1Tex) return true; + if (act1Tex.pixels.length !== wdbTex.pixels.length) return false; + for (let i = 0; i < act1Tex.pixels.length; i++) { + if (act1Tex.pixels[i] !== wdbTex.pixels[i]) return false; + } + return true; + } + + $: isDefaultTexture = isTextureDefault(textureInfo, wdbTexture, slot?.act1State?.textures); + // Update color when variable changes (without reloading geometry) $: if (renderer && !loading && currentColorValue && loadedPartKey) { renderer.updateColor(currentColorValue); @@ -98,7 +138,7 @@ if (!wdbData || !wdbParser || !currentPart || !renderer) return; partError = null; - const partKey = `${vehicle}-${globalIndex}`; + const partKey = `${slot?.slotNumber}-${vehicle}-${globalIndex}`; try { const worldName = VehicleWorlds[vehicle]; @@ -133,15 +173,72 @@ // Build parts map for shared LOD resolution const partsMap = buildPartsMap(wdbParser, world.parts); + // Build texture list, merging Act1State texture if available + let textures = modelData.textures || []; + if (textureInfo && slot?.act1State?.textures) { + const texKey = textureInfo.textureName.toLowerCase(); + const act1Tex = slot.act1State.textures.get(texKey); + if (act1Tex) { + const existingIdx = textures.findIndex(t => t.name?.toLowerCase() === texKey); + if (existingIdx >= 0) { + textures = [...textures]; + textures[existingIdx] = { ...act1Tex, name: texKey }; + } else { + textures = [...textures, { ...act1Tex, name: texKey }]; + } + } + } + + // Extract palette from the WDB texture (the ground truth) for the + // texture picker modal. The game's LoadBits() only overwrites pixel + // data on the DirectDraw surface — the palette always stays from the + // original WDB load. So custom pixel indices must reference THIS palette. + if (textureInfo) { + const texKey = textureInfo.textureName.toLowerCase(); + const wdbTex = (modelData.textures || []).find(t => t.name === texKey); + if (wdbTex) { + texturePalette = wdbTex.palette; + wdbTexture = squareTexture(wdbTex); + } else { + wdbTexture = null; + } + } else { + wdbTexture = null; + } + // Load part with current color, textures, and parts map for shared LOD lookup - renderer.loadPartWithColor(partRoi, currentColorValue, modelData.textures || [], partsMap); + renderer.loadPartWithColor(partRoi, currentColorValue, textures, partsMap) loadedPartKey = partKey; + + // Preload default .tex files in background for the texture picker + if (textureInfo) { + preloadDefaultTextures(textureInfo); + } else { + preloadedDefaults = null; + } } catch (e) { console.error('Failed to load part:', e); partError = e.message; } } + async function preloadDefaultTextures(info) { + const results = await Promise.all(info.texFiles.map(async (texFile) => { + const response = await fetch(`/${texFile}.tex`); + if (!response.ok) return null; + const buffer = await response.arrayBuffer(); + const parsed = parseTex(buffer); + if (parsed.textures.length > 0) { + return { name: texFile, ...parsed.textures[0] }; + } + return null; + })); + // Only apply if textureInfo hasn't changed since we started + if (textureInfo === info) { + preloadedDefaults = results.filter(Boolean); + } + } + function prevPart() { globalIndex = globalIndex > 0 ? globalIndex - 1 : allParts.length - 1; loadedPartKey = null; @@ -171,17 +268,61 @@ function resetColor() { if (!currentPart) return; - onUpdate({ + const update = { variable: { name: currentPart.variable, value: currentPart.defaultColor } + }; + + // Reset texture to WDB default (equivalent to WriteDefaultTexture in the game). + // wdbTexture is already squared when cached in loadCurrentPart(). + if (canEditTexture && wdbTexture && renderer) { + const texKey = textureInfo.textureName.toLowerCase(); + + renderer.updateTexture(texKey, wdbTexture); + + update.texture = { + textureName: textureInfo.textureName, + textureData: wdbTexture + }; + } + + onUpdate(update); + } + + function openTexturePicker() { + if (!canEditTexture) return; + showTextureModal = true; + } + + function handleTextureSelect(textureData) { + if (!textureInfo || !renderer) return; + + const texKey = textureInfo.textureName.toLowerCase(); + + // Square the texture for game compatibility — the game's DirectDraw + // surfaces are always square, and LoadBits() expects matching dimensions. + // No-op if already square. + const saveData = squareTexture(textureData); + + // Update preview immediately + renderer.updateTexture(texKey, saveData); + + // Save to file + onUpdate({ + texture: { + textureName: textureInfo.textureName, + textureData: saveData + } }); + + showTextureModal = false; } - +
-
- -
- {VehicleNames[vehicle]} - {currentPart?.label || 'Unknown'} +
+
+ +
+ {VehicleNames[vehicle]} + {currentPart?.label || 'Unknown'} +
+
- + {#if textureInfo} + + {/if}
- {#if !isDefault && !loading && !error && !partError} + {#if (!isDefaultColor || !isDefaultTexture) && !loading && !error && !partError} {/if}
+{#if showTextureModal && textureInfo} + showTextureModal = false} + /> +{/if} + diff --git a/workbox-config.cjs b/workbox-config.cjs index 92e8d4d..71e1e2d 100644 --- a/workbox-config.cjs +++ b/workbox-config.cjs @@ -1,7 +1,7 @@ module.exports = { globDirectory: 'dist/', globPatterns: [ - '**/*.{js,css,html,webp,wasm,pdf,mp3,gif,png,svg,json}' + '**/*.{js,css,html,webp,wasm,pdf,mp3,gif,png,svg,json,tex}' ], swSrc: 'src-sw/sw.js', swDest: 'dist/sw.js',