From afb393cca22e80926d5a3765eb3e440e864d1a37 Mon Sep 17 00:00:00 2001 From: GigabiteStudios Date: Tue, 30 Dec 2025 00:25:33 -0600 Subject: [PATCH] i asset viewer --- .../__pycache__/ikv_loader.cpython-311.pyc | Bin 0 -> 12626 bytes .../__pycache__/imesh_loader.cpython-311.pyc | Bin 0 -> 6370 bytes .../__pycache__/itex_loader.cpython-311.pyc | Bin 0 -> 2799 bytes .../material_loader.cpython-311.pyc | Bin 0 -> 7760 bytes loaders/ikv_loader.py | 228 ++++ loaders/imesh_loader.py | 156 +++ loaders/itex_loader.py | 68 ++ loaders/material_loader.py | 121 ++ main.py | 1088 +++++++++++++++++ 9 files changed, 1661 insertions(+) create mode 100644 loaders/__pycache__/ikv_loader.cpython-311.pyc create mode 100644 loaders/__pycache__/imesh_loader.cpython-311.pyc create mode 100644 loaders/__pycache__/itex_loader.cpython-311.pyc create mode 100644 loaders/__pycache__/material_loader.cpython-311.pyc create mode 100644 loaders/ikv_loader.py create mode 100644 loaders/imesh_loader.py create mode 100644 loaders/itex_loader.py create mode 100644 loaders/material_loader.py create mode 100644 main.py diff --git a/loaders/__pycache__/ikv_loader.cpython-311.pyc b/loaders/__pycache__/ikv_loader.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..785cb409b418441b781918dc36c750b7d94875b1 GIT binary patch literal 12626 zcmb_CYiv_jn)h5k;#X`Fhd2*toF{Riq&!-fE-(Z_2&4^#q%Q{6HNFl6LIU?XX)sQk zsp!;+tnCd-7t?~)t>`qUGum|3N@z8eIy)Vy(rQL_i)N^G34V4o6XfJ8zP8559#$KzS<3OsY_1tG`_r-Vzv z;lQcRk-+H1;BZJdb$Bq;J$mL;hadz)z9Yj>9SDs8-q47jL;S%DV?OzQ>!mA_$>$p! z9t`<>;dQw=Y|Ry6UqU5-NhVXdKB|wIGp-sbaN9`z4pYs@~MFfj9tlx{w4MLdM7tqQqk543gXMTidnF&LFlu+l;&T+yRF>;pd%+jT?Q1~@a$gVoSG6nN06#lr<>P5;{X>}j=EZ46}euf)Xq*iJ#ig;Kg^A0ty zaZYYsLdAQzody0dXh)~D6EaVVY|Ip4CrlBO!c84i127R&UVgQ7geQUDRJ9-pgl~;RriCmEXB0uTalk5rnl63$s93Fu$p;SM9igTq>>|ceniv4rJaYo0%(uCmA&_I z-832&<;4ZR2WYZyFcGlCV~ET4hV>qExrN6{FOf(3+FmjY3clBba22&g!SSto zhDHMZAz|NET8^Xyv4t*C)Z9o$|0OV=oTNWL zGC1tDNIHQJ@E&L_kwLy1{&_b(=tf3E5*s)tvBTpMIWLjd1ndF~arTlYR+gO+za1*e z;3xQi>;4Nqlgy&Y8a+5=ikT)mGA8q6#}Z2nk1a6@FfvwK)IZ%a)snF}L=x+Yl8n_E zeM($6wL7*OOrLpu##RnbUD1w=tukY^iMr|C(cN(p@3?75koU}Sb0RR;Fz3J9a_9W~ z!G+dc$<|%@CKWExf4xO)i62amTU`n6z210l#<@;BK4TQgl8$jz7T15P>gPwHR2JP6 z-J?)itf-A|o7p_OIceJ{fAbTY+O42vDk`G~GxGRnS~BaaN~)fmOP19KC)t&d>DD_KLmWgT>d)bcr3A z$_+Sq*Y%%>KT#FdWSp)Wmg|=I_8I%EU9{(S_ovz2ooV^&e!{rw;=2^RuaG%{FHcSVj!T1aD7ZN)Xm+w4%^YpFL(crvpzT-3F zy|c+D55ZU2Cg33u*EO{G7WrFj5OuL`=moo4v{#8^@yjWDL)zYuG-ba_6GV}TzwT!l zt4n-psy)`8)YquLR5b)Z*pw^h(LLA)MQXpD`urFfp!beGzaF5G#sXzD7BJCRz|5JT z+`^eblUXD)_{GordGNIZYWD!m3H~0uN;}awmPD&W)vkRAT+!SWVb14Y0z zRc(w1W`<^mz_GV3y4-R7Eo;ivoOU(mJbihA)pJM3n$G?2;ReN#^HLf(h)taO|AncD?!;hQ9ciZpvcD}C<1XpM6f5E8=1>3YMJFmcZ%+`4Bx!Eua( zf}AI8+~PSq67qNr{BCH$<9fuIK0Dt50KWJ7fAjKhPRsYHis#Z5&n0co zt#RFPJ?Xlif}2&=o$eK1r(SnhIl3Fru$b4dkX{_=gT0}v*AIp=ze4#{;r|9O)l4QC zeHZ;#&IH3w)q>EHR}gaxfZ`=_*Tp+;^`zWwX?I(+GuEAPxy79sM`d(W<~BtxptUk< z5lUB@HAk{8^cxz^Qn;35v-J?nC6tu_>dFO41fZV}2f@uR$=93?f|{BT3S9OAJVmOt zG&CW8X=?hclsG~d6=yH5z%6|fk_8Es9OUp|FX8qR<*{$2G3SEO%C*x;U~FG>753*f z_VTTOCF#NU*#?TJ86IfKGPxRVEG2Diui;CHdEKB2#3P1S8l*KHB?v!*OU zLbu>(N;;Yrt31huL#e8+bX8Z<*#%7gjjLtB)sk{;O1m~CZJTnM;^2a{K54Blf#7g` z<7i!Qw5A+wX-8Yq)V7Lz3}xjb0dp(9iurqxnILB1n1ou|l!ly6%ga0&errYC0QF|HLvA!p7@I-o{z1U-~t_#vmz4!~2`oo988gkUO5 z_-6pX@+R9m=4rFoG-Z$3qxOv58PzSZR`W*S-FL1|UybibIqK7ndI(L*t1`~@C5pk( ztj=+|X#u#>(MCD_aH^_1UDcg*c4sQ8X4_`BfZLvxp>ru)ecD!^wADk^Hx*3_6-}v% zmUKl6ptx(J2SwdfH{$k8_r!H6+lI7lL(;Zk$qnOLs%D(7sHK?iasJdAnjVqwA(ixK zeBZZL&Y*lxN~e*4MoGJ)UDP=S- z6!6_xDtk7P&d>82Q=VfxDED6pR~2y+Esp>N;Vl3wF_ZC;naSpO=jilNG4zeQZNc4^ zazCDSKc2E}PTL?f++2b$%At!T`w#I&C+MHI*&p;6&?YLH3Z|u8+MQX^w&pwwq_c({ zUXhM2>!CmbR^?A4m(yzkeE>xh=1TFuP*VfffOgVQQFus6Gyg(UukoEc)bTM(yK7*#W{?u9-M%XMq=^l>y!na^z<_#cFH& zn9w?PEoajvy!zk-AXJgZXaRR{@(DEs^?f9>3BW@+85a^K88QxI+ax$#opy$ibkSdtIC9-8RCy2Ks!d252%^p zE3is%=mg>F;BZJX9~ut?FNFq2hN*Yw)yeja2 zgJoXVyjbUfG}rE1yTxORZb+lW&&-5p!%1hu!$sEywB0p%ka1UwCj2bvp#0&(hu_(& zlQmDL?7PzTT}jg}0TZ3SEAw^|=6Bm10KX!(PN(iG2LV{b(A4DiQj?1gq(+V8)l4q< zzrK=k-#V*E27wpYRd-Ef>haIV^ z1L>**N#}vp+(UPr+6h?EGhD|&ET$UAQbpby=mm|0CWCh(K=VKoVJc6k6U*|d3KxpU zrJ(AU>OOgSc48R<)M!Esz@+lQBk2}AlFkB?4(}9T(c^)&0&Et8cZpFzwZq_AVieG5 z!AAvDR~sPXuYjFatStueKyi4bg%oGG`jL0($gj}vk+Y91T?ihD!ikP9G#*mN+IBf! z!2^&Gq$T-R0qq~*C)5IfETgqj?3>yb+xLyN9%9{;wIOY7Na`D8uU;)idcdbDMt;R( zwY(}v!k_{;HVpV0yn+muj28ywK{H$TzyF?L?jvtN&fp~ev2Y|c%mv3K3z}BmkCsW} z7uQ_!2QK)i&G8NJBNySF2|)J&Ha`Fa{t13U8vvMSxl^p2^2L1du~d1>Tw}_%<#X5k z3!m10R(o$CwXHK{J(ScRlBca`IB2O%+X)*BS49|}2W26A|JF$)dp=U=)~Pc(I`nqhM2HjDdnuLbLAZ z$Cmm*mQXTK@sacG`isGk|G!bpzklQVH*^et25#YC)-osz4htdwa3Cn@xxqk)$|sM3 zE4*byINl0(^mJDu7kPY7NI=t$SrFMfmTG9`+2`lDthnOaSU&77ya)A?KM?Yr^AB@F zLBYod;p{{h91G?h<`rAt!@m9p0Jxfqj+%JG?f#UbG3{ssH_PnG*ear38Eb{;#{1tfF4^v`RM%oy3Y@$T6d>g z!3bc<7bm|af7p@Qdn~>8m|T)Q>AMYQxi`DuH1}j`Lp!LMsPH`KmFVGWZj9BvoG!JOXj{yR>oEh!^9!NXUT+}$b?sd#R^N2K|4s>|5*m5tPEzd zGWc(tp2}kSQDqVERZ46^uaTqGWb*?J=4!I}cN)yqWb>*9Q@@H7pVORfK^{`8IVKEK z#)L|*0RLq#jSLr@7OX*rjYU#k;7)_$2R-m;Ql681%!_g`PCkeL1(Zksm&YumEa?V* z2=6ZgKCKE%y3rlS+d{&<=)l;i**KaGPT=du)6Q+ z)^4}?t67`9S0h;(d%d#N=NrL60voTk?%LpyFo1#fU1k4EU;vF0rO>XhbfutI>xO~* zRXzbPDNlg|RY)ObM`_Z|He`hL%U)VN!8;=iP;;zK4JpE_SyI`cYylxTtcamxq~;mk zDi|6RLTGHqC3b9#4?%zYB>-T*!8nJq_I7eowzvFMs0z2OLC|RHKLWO}1OP?Lx?c3( zu1r}UOIzVZD2ur^1-{?X%e$LFs6>eLrIQ;qx6jr*goXPn-o(+h7m9r@Lk zlyg_wxhv`1m2oyDolS|3#NeH0Q_gK^=eDGCTgKLsw6!GsDO+3G)|RxjW$3X(f~0JX zXg03aKL z$uo;>^5Bx*WI*{|0ajXQ?Fz8kYJfSf3ThdHW%5w8`R6^?dM10mv)ZSgiaj;inX#4w zD0gD8Tt@-C6;y7IUXJyP$HmLD{qYksCt>knpZ#|_=MH>uG*#Y?FUQ%$&gkAKJ*MrR z?iPRGcEs%x^_Qw Yh<;`c7zyb@e=QGNBhud%^2P#XxacI*&H!4Odgv|4+{uwlJRXU7yaGpZG} z(k4Rcx-!bPqLNd!DOFl2AN`i9mMT8@GfMIksZGzl`|!>Lhz4J$x%T7T1BDava7bJ*(qfVn4>&TZ60r{Aq37X6jiv(?0Bx&QK!9Xy? zdkr3fHhoGG#Aoo-+M-EEH|ywT9o?d%TNu-O)}}TaV{YkDjHRV#r>%<)+V*o|(MeMP zU9^L?|D0TO)8qmXcRoW(&J|1m=g%(8Ju?7ObWTqe=24rQn?GOlPS4Mu2XqBDz>8)y za{k#k@GdufgpL^=)c-OI;A4Vo`b!Wzq4j_!i$K+IkU9>-cQ_0$;#e_)AI5W8H1W4T z`}Su>BJ=IH1Q91yVlSn#bV#gE&uS!r2@kit`u%E zx#cU#{PNl=lj90k-pFusYfD#7777fPn$H1wnacxTHlL;ue`b{_Tu;?{V>j=p*3|XX z3Y%YL9iZ9^PXPsIov1oJW%Gw`-hH#uE;;-Du<#)8$>rZ){@fxB{NNwPhsnQDf2SVJ zN=M!l9cM(-nJ+H$WK+7X((fl^99j7;ZP zgg!(1S)}w&C`jb>0vd?1sMJ+E_>n23iVT})eJ~J!r_c*volu;vQu=PPJW)QsIr-t4 z%D&3U`~8x$duw9b(Jz|%Stqb93l$PmWEb>*lN+SH_8({`6>AGS<|O05F^1Tm;|$6 z5v+nupai?%5S+Y`!y83S_6D|CXSr?z0Lx7r3Ru0pP)4^YfX$u;Kltg?dIVNDn-QdV zCl3)Vf>k%rqSVJxx1FFk@=g>z!Nt2Yv*ai=Sx57KXGo2_X$NtfPiC)R>MM(@=YL)Lu>ac$-E>t-tWvd(IPv+A9{tX?>n;b zm#mRdKEaRi2l#{hp;o(T|9#!QM-q4c%llreMHAbgc5c3p@8<>@@c?hzwR8=w;}x_{ zEp5f9k?hh^HMH(m(0a7A6(b+xd-*sWxUXB?B<#v}|E2L%_de9nbniV4O}G1NXu7qo zq3I5nt+dt?gw`VsSIe9468s$9!O2k%qDB=6z9ypNycwQoM14|M3h1&(#$ z+Oe+aN9Xq+#+K4FJOw6|Us)-7YGYhJA1kb;v)RS~Vp>gK%Pbdtds4UHV4KP3ibJ)b z!rBr##x;V!TQ5VfD=V@F($Ti1x2Hkm5*z`a{f(0Iy zt_^yS4J`v~C(ISMsFu1LCYD(#s8n5)DX6AQZY8gpma_RJ)z&E1ctAB~nVf2W2VK`@ zP!*_Nl%Uky%*o`;h1A0Q;*5%`yg735!lly}RmWQHW_tMszJRHAEOPeb8}p}CC&u5I zxv((*_Bj?E_*Go*sfA0Y&dz{lq!M4*R5LDVHGn0WWUL=wgj7rVCfpHe)l!R&>cX6F zy`8Mtg{v=xyEUT%XWvfFyp@8>wggECYAbTMRv}I#wYe*;MT!Yp-?SI?Jea9NlVFbLwWA9%qM;thKPpp?s>Q z9K+D;DAJrjU<^^oj7sKJTVXX-D?Fd3&Qd4~LG4hz%qbYHTVX>ewgF?<0(+xCQ@7PU z{7wsAY~bnJdpv+Qc>pi6XnRzhU|ur`y9GeBPnQZ?1+FoSL*g~M>tfo}yiM=ALsb(dN{saQSbeIM}aeEIg)zF+ZW zzUq#M(S+n4lig#YZ4Bs-{k@X4ckBGNbx^bpV*0S;9+BN6qHW}BJJCJ@5#h-c!tW#T?zSI`7%Ht?i<QkA?u6`4h_=Mjj&8B%kkm0LcT9?=h+^%iq_?d-BK-Hg_VDB*+uz>W?wc3; z=CRx<$$eUOpB8PWcids@OJe;E#o@oVQ0bCFLvm>7@$i%snvxt->q*7xD5XERy?z^u zPD$=***z`Vrk}ck<#)G+AG{~Ij>@j1>$5u!7jS$1>+?@t{(FbYf)p5#0|Sz4P<9RO z5*J9b<2i;UQgH^#>BsFOQg}pij+Dr+gG6-nN#wQd$ZHQx57ScQbvg3-bDxb0mm&() zu25YH2kmMTz z)Z;79eI%5GEyLFN-&wXS)o@JQcUlT3<#1B;CY831%Bc!lnb~~*Nn3ooE&gEW;eN3# zF14MO+fJ9}b{xS9`O)#x@v6U5>>8K+6S99obWZF9Ix07$Kpad6c2@5E{ z#Sti9-*$A1j_$4agY@qbTL~;WCHbdi|Fr0w-tqWKrm7zTBKdn|fA22QO?fatmPae6 z{?H|Lj!C|;(y40v$dh<-JDz-W@t+5!_&GU#PI3;GX3Mjcp@N3=snRIaV^g=DSTWG9~ZsH zcWPNxVuMQ0fYLXl^bRWfhM-&BH@WMvMcumufaiqG<@pK$=r%x*p!`p4k!@Q9`5f7} zx_MRUi7Q`NWUJ$ocQ*P@=s`$Dk67 z?E0v7`z`_CIYCj5uMmJP1K39SON30lbf) z#G(^&bYeGPcUpD{0PD%!b|Mf}yg?<@ji5J(R+DYx?B>~Bmlbini&?0_giKe&MrdcV^Lt zCp-z>$iJ~o!@GAa%N%F(KtSIc3V#K#Ya~fhAzbVDZ^so}w<{j+x^pMgTYhKrs@V6s z6gnw~PKtzI3HOy(Hdn>|87Vv~hi641p!j>rgPVyhtK{D=`}ePVcF-O+ED~NNI3x#0 rMZ%|edt`6_x@#vK0fAi;Nxnty5-rd|lCXU=fo0Y}LYQ6!u~+{CCd%>X literal 0 HcmV?d00001 diff --git a/loaders/__pycache__/itex_loader.cpython-311.pyc b/loaders/__pycache__/itex_loader.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..46943e066c7563844443c4267dfb8bbee41742aa GIT binary patch literal 2799 zcma)8-A~(A6u(YF?8J#f_$CBMJ83MX;-ieIbZ9FBTF^u%0qtl?n}ygRF8P{mx{}%= zMV-W?P74#$QmGGU58bA!QXl&dOq#S88>L!wrASCqHT8|D+Dl)$dmWP0p+&n+PJZX( zo^$Rw_g?2`r_&D5_&YYo|3L!q7iP-Cd;ng|BJd+%0ZYU{p!7=w2%R?tOspwjX3YT$ zYYEhu00*vH%MRGO2Lu2Q(YG`LBulc^J4AqDi8o@4O^etLd#;W;*Xej1Uqdvtng zYPviy44bgw!Kg>SII|ah7Mzt-@eY6tjPYbbdTnMJOHi>6^G(f7{#U3D{k2&>D4azThZ$) zw#a75QH^jQrDeixPHnmsEqclmb3IncFW&8kvhh+vR z%~E590oN-vmz#_cl$QVHt<|uS7NZZDeq(P)qZQgyt<{WVr%}sNX|#(C7EFzW?U04* zO%@Te(q`~e(<_6nrrri!P5uTQWsGdl*^bcJkI*@e(9uSn+$uMxQBPTW8l7T8^qP~* z()aC3yWA$X%PkQzx^WyA(k^`4obgG?q)&{8VzG36Nw5@ReNisNa)N+QP@1WVSw1e7 z=V3FEPjh0r^}vy^nh1v^j`gKNYq4aAP5Vj;mJ282DS;D3q{CBuaUMmK!qFH$_^;Mq zt(yV=>J=zcwqlE_&V1>+*rS(XbXBN364{0?Z2aS5oGB{(%XHZeIH zoSP0zLi?pqY?Yf7gropzyvE?%{OrZ?c}TA&QlaolFd`)5(5`D{#?DWVLx+wpP0r3u ze|7;{BtFhTM~V|7!EkalA#nn6V<5xJk|ftJMKDbfpQioSAlBO0aKvr z^^dziwH2uL0@YfenshBPKtlJv6;RF}?7O5#B|W+2N8a78VYO@cNypQvh24<_b!0&g z-K#Mt^UO(=I=M$WvcyI@lg?hsefj;B?3JCV$HY%FJ2SeSUt><>nG-5?VvlUe9or>4 zRP^Z{cs#b-J*;*Q*Q{f&!IQh9HN2Pkv|w+}xiouEX1qY!zn#lAeRKKl<&E{sx<2ES z#th||A(a~1qg`1#H=cFnX+q{`iica_4U~XP>9?v_EXU-Xw&ro4M zxP9x+Ew!a*m+Vo=9^LbY8Z(+_MpbI`na#1esM$QoVJ@KAdLA|Yaz*ox z*0t?xMQ^w28_>LidGBDcqbs+rb@b;u`iq@D_1Gz`b12_ARPcCn!o8lIOImwxvE7?n zxYv2VbHAB(SoZ-UnTh>2;BaoT*>js8s}|1-v~Pf)(CSPe@8exV8Y)hsk) z(t?Q_5onz&8Q^!YzVUtq!XU=|h`gjm<4JZk#(g9VBNqRA6Td)Y-%Joh0XQ@IQvh^E we=1qGKPth3Fal2P6ibWB>pF literal 0 HcmV?d00001 diff --git a/loaders/__pycache__/material_loader.cpython-311.pyc b/loaders/__pycache__/material_loader.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4e7fc7c2913e423a8d5ef4f03e22d4e40238d60d GIT binary patch literal 7760 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 literal 0 HcmV?d00001 diff --git a/loaders/ikv_loader.py b/loaders/ikv_loader.py new file mode 100644 index 0000000..fef7414 --- /dev/null +++ b/loaders/ikv_loader.py @@ -0,0 +1,228 @@ +class IKVToken: + __slots__ = ("type", "val") + def __init__(self, t, v=None): + self.type = t + self.val = v + +def ikv_lex(src): + n = len(src) + i = 0 + def is_ws(c): + return c in " \t\r\n" + + while i < n: + c = src[i] + if is_ws(c): + i += 1 + continue + if c == "/" and i + 1 < n and src[i + 1] == "/": + i += 2 + while i < n and src[i] != "\n": + i += 1 + continue + if c == "#": + i += 1 + while i < n and src[i] != "\n": + i += 1 + continue + if c in "{}[],": + i += 1 + yield IKVToken(c) + continue + if c == '"': + i += 1 + out = [] + while i < n: + ch = src[i] + if ch == "\\" and i + 1 < n: + nx = src[i + 1] + if nx == "n": + out.append("\n") + i += 2 + continue + if nx == "r": + out.append("\r") + i += 2 + continue + if nx == "t": + out.append("\t") + i += 2 + continue + if nx == "\\": + out.append("\\") + i += 2 + continue + if nx == '"': + out.append('"') + i += 2 + continue + out.append(nx) + i += 2 + continue + if ch == '"': + i += 1 + break + out.append(ch) + i += 1 + yield IKVToken("STRING", "".join(out)) + continue + + j = i + while j < n and (not is_ws(src[j])) and src[j] not in "{}[],": + j += 1 + w = src[i:j] + i = j + yield IKVToken("WORD", w) + + yield IKVToken("EOF") + +class IKVParser: + def __init__(self, src): + self.tokens = ikv_lex(src) + self.cur = next(self.tokens) + + def _eat(self, t): + if self.cur.type != t: + raise ValueError(f"IKV parse: expected {t}, got {self.cur.type}") + self.cur = next(self.tokens) + + def _maybe(self, t): + if self.cur.type == t: + self.cur = next(self.tokens) + return True + return False + + def parse(self): + if self.cur.type == "WORD" and self.cur.val == "ikv1": + self._eat("WORD") + if self.cur.type in ("STRING", "WORD"): + self.cur = next(self.tokens) + if self.cur.type != "{": + raise ValueError("IKV parse: expected { after header") + return self._parse_object() + if self.cur.type == "{": + return self._parse_object() + return self._parse_object_flat() + + def _parse_object(self): + self._eat("{") + obj = {} + while True: + if self._maybe("}"): + break + if self._maybe(","): + continue + if self.cur.type != "STRING": + raise ValueError("IKV parse: object key must be string") + key = self.cur.val + self._eat("STRING") + val = self._parse_value() + obj[key] = val + self._maybe(",") + return obj + + def _parse_array(self): + self._eat("[") + arr = [] + while True: + if self._maybe("]"): + break + if self._maybe(","): + continue + arr.append(self._parse_value()) + self._maybe(",") + return arr + + def _parse_value(self): + if self.cur.type == "{": + return self._parse_object() + if self.cur.type == "[": + return self._parse_array() + if self.cur.type == "STRING": + s = self.cur.val + self._eat("STRING") + return s + if self.cur.type == "WORD": + w = self.cur.val + self._eat("WORD") + if w == "true": + return True + if w == "false": + return False + if w == "null": + return None + try: + if any(ch in w for ch in ".eE"): + return float(w) + return int(w, 10) + except Exception: + return w + raise ValueError("IKV parse: unexpected token") + + def _parse_object_flat(self): + obj = {} + while self.cur.type != "EOF": + if self._maybe(","): + continue + if self.cur.type != "STRING": + raise ValueError("IKV parse: expected string key") + key = self.cur.val + self._eat("STRING") + obj[key] = self._parse_value() + self._maybe(",") + return obj + +def parse_ikv_text(txt): + return IKVParser(txt).parse() + +def pack_value_from_index_gen(index, generation): + return ((int(generation) & 0xFFFF) << 16) | (int(index) & 0xFFFF) + +def extract_handles_recursive(node): + out = [] + + def maybe_add_value_type_meta(d): + if not isinstance(d, dict): + return + if "value" in d and "type" in d and "meta" in d: + v = d["value"] + t = d["type"] + m = d["meta"] + if isinstance(v, int) and isinstance(t, int) and isinstance(m, int): + out.append((int(v) & 0xFFFFFFFF, int(t) & 0xFFFF, int(m) & 0xFFFF)) + + def maybe_add_index_gen(d): + if not isinstance(d, dict): + return + if "index" in d and "generation" in d and "type" in d and "meta" in d: + idx = d["index"] + gen = d["generation"] + t = d["type"] + m = d["meta"] + if isinstance(idx, int) and isinstance(gen, int) and isinstance(t, int) and isinstance(m, int): + v = pack_value_from_index_gen(idx, gen) + out.append((int(v) & 0xFFFFFFFF, int(t) & 0xFFFF, int(m) & 0xFFFF)) + + def visit(x): + if isinstance(x, dict): + if "ihandle" in x and isinstance(x["ihandle"], dict): + maybe_add_value_type_meta(x["ihandle"]) + maybe_add_index_gen(x["ihandle"]) + maybe_add_value_type_meta(x) + maybe_add_index_gen(x) + for vv in x.values(): + visit(vv) + elif isinstance(x, list): + for vv in x: + visit(vv) + + visit(node) + + seen = set() + uniq = [] + for h in out: + k = (h[0] | (h[1] << 32) | (h[2] << 48)) + if k not in seen: + seen.add(k) + uniq.append(h) + return uniq diff --git a/loaders/imesh_loader.py b/loaders/imesh_loader.py new file mode 100644 index 0000000..592c632 --- /dev/null +++ b/loaders/imesh_loader.py @@ -0,0 +1,156 @@ +import struct + +IMSH_MAGIC = b"IMSH" +IMSH_VERSION = 2 + +IMSH_HEADER_STRUCT = struct.Struct("<4sIIII" + "IHH" + "I" + "Q") +IMSH_SUBMESH_STRUCT = struct.Struct(" n: + raise ValueError("IMSH: bad base_off") + if base_off + IMSH_HEADER_SIZE > n: + raise ValueError("IMSH: too small") + + (magic, version, flags, submesh_count, reserved0, + model_value, model_type, model_meta, + _pad0, + submesh_table_offset) = IMSH_HEADER_STRUCT.unpack_from(data, base_off) + + if magic != IMSH_MAGIC: + raise ValueError("IMSH: bad magic") + if version != IMSH_VERSION: + raise ValueError("IMSH: bad version") + + submesh_table_offset = int(submesh_table_offset) + if submesh_table_offset < 0 or submesh_table_offset >= (n - base_off): + raise ValueError("IMSH: submesh table offset out of range") + + smt_off = base_off + submesh_table_offset + need = smt_off + int(submesh_count) * IMSH_SUBMESH_SIZE + if need > n: + raise ValueError("IMSH: submesh table out of range") + + handle = _h_from(model_value, model_type, model_meta) + + refs = [] + submeshes = [] + + for i in range(int(submesh_count)): + o = smt_off + i * IMSH_SUBMESH_SIZE + + (sm_flags, + material_name_len, + material_name_offset, + mat_value, mat_type, mat_meta, + aabb_min_x, aabb_min_y, aabb_min_z, + aabb_max_x, aabb_max_y, aabb_max_z, + lod_count, + sm_reserved0, + lods_offset) = IMSH_SUBMESH_STRUCT.unpack_from(data, o) + + material_name_len = int(material_name_len) + material_name_offset = int(material_name_offset) + lod_count = int(lod_count) + lods_offset = int(lods_offset) + + mat_handle = _h_from(mat_value, mat_type, mat_meta) + if not _h_is_zero(mat_handle): + refs.append(mat_handle) + else: + mat_handle = None + + mat_name = None + if material_name_len > 0: + mo = base_off + material_name_offset + me = mo + material_name_len + if mo < base_off or me > n: + raise ValueError("IMSH: material name out of range") + mat_name = data[mo:me].decode("utf-8", errors="ignore") + + if lod_count <= 0: + raise ValueError("IMSH: lod_count=0") + + lods_abs = base_off + lods_offset + lods_need = lods_abs + lod_count * IMSH_LOD_SIZE + if lods_abs < base_off or lods_need > n: + raise ValueError("IMSH: lod table out of range") + + lods = [] + for li in range(lod_count): + lo = lods_abs + li * IMSH_LOD_SIZE + vcount, icount, voff, ioff = IMSH_LOD_STRUCT.unpack_from(data, lo) + + vcount = int(vcount) + icount = int(icount) + voff = int(voff) + ioff = int(ioff) + + if vcount <= 0 or icount <= 0: + raise ValueError("IMSH: empty lod") + + vb = base_off + voff + ib = base_off + ioff + vbytes = vcount * MODEL_VERTEX_STRIDE + ibytes = icount * 4 + + if vb < base_off or vb + vbytes > n: + raise ValueError("IMSH: vertices out of range") + if ib < base_off or ib + ibytes > n: + raise ValueError("IMSH: indices out of range") + + lods.append({ + "vertex_count": vcount, + "index_count": icount, + "vertices_offset": voff, + "indices_offset": ioff, + "vertices_size": vbytes, + "indices_size": ibytes, + "vertex_stride": MODEL_VERTEX_STRIDE, + }) + + submeshes.append({ + "flags": int(sm_flags), + "material_handle": mat_handle, + "material_name": mat_name, + "aabb_min": (float(aabb_min_x), float(aabb_min_y), float(aabb_min_z)), + "aabb_max": (float(aabb_max_x), float(aabb_max_y), float(aabb_max_z)), + "lod_count": lod_count, + "lods_offset": lods_offset, + "lods": lods, + }) + + info = { + "version": int(version), + "flags": int(flags), + "submesh_count": int(submesh_count), + "submesh_table_offset": int(submesh_table_offset), + "decode_offset": int(base_off), + "header_size": int(IMSH_HEADER_SIZE), + "submesh_record_size": int(IMSH_SUBMESH_SIZE), + "lod_record_size": int(IMSH_LOD_SIZE), + "vertex_stride": int(MODEL_VERTEX_STRIDE), + } + + return { + "handle": handle, + "refs": refs, + "submeshes": submeshes, + "info": info, + "blob": data, + "base_off": int(base_off), + } diff --git a/loaders/itex_loader.py b/loaders/itex_loader.py new file mode 100644 index 0000000..89a2fad --- /dev/null +++ b/loaders/itex_loader.py @@ -0,0 +1,68 @@ +import struct +import time +import zlib + +ITEX_MAGIC = 0x58455449 +ITEX_VERSION = 1 +ITEX_HEADER_SIZE = 56 +ITEX_STRUCT = struct.Struct(" len(data): + raise ValueError("ITEX: truncated payload") + + comp = data[payload_off:end] + t0 = time.perf_counter() + pixels = zlib.decompress(comp) + t1 = time.perf_counter() + + if len(pixels) != uncompressed_size: + raise ValueError("ITEX: decompressed size mismatch") + + h = (int(handle_value) & 0xFFFFFFFF, int(handle_type) & 0xFFFF, int(handle_meta) & 0xFFFF) + info = { + "width": int(width), + "height": int(height), + "channels": int(channels), + "is_float": int(is_float), + "has_alpha": int(has_alpha), + "has_smooth_alpha": int(has_smooth_alpha), + "compressed_size": int(compressed_size), + "decompressed_size": int(uncompressed_size), + "decompress_ms": (t1 - t0) * 1000.0, + "decode_offset": int(base_off), + } + return h, info, pixels diff --git a/loaders/material_loader.py b/loaders/material_loader.py new file mode 100644 index 0000000..368fd69 --- /dev/null +++ b/loaders/material_loader.py @@ -0,0 +1,121 @@ +from .ikv_loader import parse_ikv_text, extract_handles_recursive + + +def try_load_material_from_bytes(data): + try: + txt = data.decode("utf-8", errors="strict") + except Exception: + try: + txt = data.decode("utf-8", errors="ignore") + except Exception: + return None + + s = txt.lstrip() + if not (s.startswith("ikv1") or s.startswith("{") or s.startswith('"')): + return None + + obj = parse_ikv_text(txt) + + root_handle = None + if isinstance(obj, dict) and "ihandle" in obj and isinstance(obj["ihandle"], dict): + d = obj["ihandle"] + if all(k in d for k in ("value", "type", "meta")) and all( + isinstance(d[k], int) for k in ("value", "type", "meta") + ): + v = int(d["value"]) & 0xFFFFFFFF + t = int(d["type"]) & 0xFFFF + m = int(d["meta"]) & 0xFFFF + if t != 0: + root_handle = (v, t, m) + + refs = extract_handles_recursive(obj) + + if root_handle is not None: + rk = root_handle[0] | (root_handle[1] << 32) | (root_handle[2] << 48) + refs = [h for h in refs if ((h[0] | (h[1] << 32) | (h[2] << 48)) != rk)] + + info = {} + if isinstance(obj, dict): + if "shader_id" in obj: + info["shader_id"] = obj.get("shader_id") + if "flags" in obj: + info["flags"] = obj.get("flags") + + return { + "handle": root_handle, + "refs": refs, + "obj": obj, + "info": info, + "text_len": len(txt), + } + + +def pack_value_from_index_gen(index, generation): + return ((int(generation) & 0xFFFF) << 16) | (int(index) & 0xFFFF) + + +def extract_handles_recursive(node): + out = [] + + def is_valid_triplet(v, t, m): + v = int(v) & 0xFFFFFFFF + t = int(t) & 0xFFFF + m = int(m) & 0xFFFF + if t == 0: + return False + return True + + def maybe_add_value_type_meta(d): + if not isinstance(d, dict): + return + if "value" in d and "type" in d and "meta" in d: + v = d["value"] + t = d["type"] + m = d["meta"] + if isinstance(v, int) and isinstance(t, int) and isinstance(m, int): + if is_valid_triplet(v, t, m): + out.append((int(v) & 0xFFFFFFFF, int(t) & 0xFFFF, int(m) & 0xFFFF)) + + def maybe_add_index_gen(d): + if not isinstance(d, dict): + return + if "index" in d and "generation" in d and "type" in d and "meta" in d: + idx = d["index"] + gen = d["generation"] + t = d["type"] + m = d["meta"] + if ( + isinstance(idx, int) + and isinstance(gen, int) + and isinstance(t, int) + and isinstance(m, int) + ): + if (idx == 0 and gen == 0) or int(t) == 0: + return + v = pack_value_from_index_gen(idx, gen) + if is_valid_triplet(v, t, m): + out.append((int(v) & 0xFFFFFFFF, int(t) & 0xFFFF, int(m) & 0xFFFF)) + + def visit(x): + if isinstance(x, dict): + if "ihandle" in x and isinstance(x["ihandle"], dict): + maybe_add_value_type_meta(x["ihandle"]) + maybe_add_index_gen(x["ihandle"]) + maybe_add_value_type_meta(x) + maybe_add_index_gen(x) + for vv in x.values(): + visit(vv) + elif isinstance(x, list): + for vv in x: + visit(vv) + + visit(node) + + seen = set() + uniq = [] + for h in out: + k = h[0] | (h[1] << 32) | (h[2] << 48) + if k not in seen: + seen.add(k) + uniq.append(h) + return uniq diff --git a/main.py b/main.py new file mode 100644 index 0000000..4f1c114 --- /dev/null +++ b/main.py @@ -0,0 +1,1088 @@ +import os +import time +import struct +import tkinter as tk +from tkinter import ttk, filedialog, messagebox + +from loaders.itex_loader import ITEX_MAGIC, load_itex_from_bytes +from loaders.imesh_loader import IMSH_MAGIC, load_imesh_from_bytes +from loaders.material_loader import try_load_material_from_bytes + +try: + from PIL import Image, ImageTk +except Exception: + Image = None + ImageTk = None + +try: + import numpy as np +except Exception: + np = None + +try: + from pyopengltk import OpenGLFrame +except Exception: + OpenGLFrame = None + +try: + from OpenGL import GL +except Exception: + GL = None + + +ASSET_IMAGE = "IMAGE" +ASSET_MODEL = "MODEL" +ASSET_MATERIAL = "MATERIAL" +ASSET_UNKNOWN = "UNKNOWN" + + +def handle_key(h): + if not h: + return None + v, t, m = h + return (int(v) & 0xFFFFFFFF) | ((int(t) & 0xFFFF) << 32) | ((int(m) & 0xFFFF) << 48) + + +def fmt_handle(h): + if not h: + return "-" + v, t, m = h + return f"type={t} meta={m} value=0x{v:08X}" + + +def fmt_bytes(n): + n = float(n) + if n < 1024: + return f"{int(n)} B" + n /= 1024.0 + if n < 1024: + return f"{n:.2f} KiB" + n /= 1024.0 + if n < 1024: + return f"{n:.2f} MiB" + n /= 1024.0 + return f"{n:.2f} GiB" + + +class AssetRecord: + __slots__ = ( + "path", + "kind", + "handle", + "refs", + "ref_by", + "load_ms", + "compressed_size", + "decompressed_size", + "info", + "payload", + ) + + def __init__(self, path): + self.path = path + self.kind = ASSET_UNKNOWN + self.handle = None + self.refs = [] + self.ref_by = [] + self.load_ms = 0.0 + self.compressed_size = 0 + self.decompressed_size = 0 + self.info = {} + self.payload = None + + +def read_all(path): + with open(path, "rb") as f: + return f.read() + + +def find_first_signature(data): + n = len(data) + lim = min(n - 4, 512) + for o in range(0, max(0, lim)): + if struct.unpack_from(" 0 else 0.0 + g = struct.unpack_from(" 1 else r + b = struct.unpack_from(" 2 else r + a = struct.unpack_from(" 3 else 1.0 + rr = max(0, min(255, int(r * 255.0))) + gg = max(0, min(255, int(g * 255.0))) + bb = max(0, min(255, int(b * 255.0))) + aa = max(0, min(255, int(a * 255.0))) + o = i * 4 + raw[o : o + 4] = bytes((rr, gg, bb, aa)) + return Image.frombytes("RGBA", (w, h), bytes(raw)) + + arr = np.frombuffer(pixels, dtype=np.float32) + arr = arr.reshape((h, w, ch)) + if ch == 1: + rgb = np.repeat(arr, 3, axis=2) + a = np.ones((h, w, 1), dtype=np.float32) + arr = np.concatenate([rgb, a], axis=2) + elif ch == 3: + a = np.ones((h, w, 1), dtype=np.float32) + arr = np.concatenate([arr, a], axis=2) + else: + arr = arr[:, :, :4] + arr = np.nan_to_num(arr, nan=0.0, posinf=1.0, neginf=0.0) + arr = np.clip(arr, 0.0, 1.0) + u8 = (arr * 255.0 + 0.5).astype(np.uint8) + return Image.fromarray(u8, mode="RGBA") + + +class ZoomPanImageCanvas(tk.Canvas): + def __init__(self, parent): + super().__init__(parent, highlightthickness=0, background="#101014") + self._pil = None + self._photo = None + self._img_item = None + self._scale = 1.0 + self._pan_last = None + self._fit_on_set = True + self._min_scale = 0.02 + self._max_scale = 64.0 + + self.bind("", self._pan_start) + self.bind("", self._pan_move) + self.bind("", self._wheel) + self.bind("", self._wheel_linux) + self.bind("", self._wheel_linux) + self.bind("", self._on_configure) + + def clear(self): + self.delete("all") + self._pil = None + self._photo = None + self._img_item = None + self._scale = 1.0 + + def set_image(self, pil_image, fit=True): + self._pil = pil_image + self.delete("all") + self._img_item = None + self._photo = None + if fit: + self.fit_to_view() + else: + self._scale = 1.0 + self._redraw(center=True) + + def fit_to_view(self): + if self._pil is None: + return + w = max(1, int(self.winfo_width())) + h = max(1, int(self.winfo_height())) + iw, ih = self._pil.size + if iw <= 0 or ih <= 0: + return + pad = 24 + avail_w = max(1, w - pad) + avail_h = max(1, h - pad) + s = min(avail_w / float(iw), avail_h / float(ih)) + self._scale = max(self._min_scale, min(self._max_scale, s)) + self._redraw(center=True) + + def zoom_100(self): + if self._pil is None: + return + self._scale = 1.0 + self._redraw(center=True) + + def center(self): + if self._pil is None or self._img_item is None: + return + w = max(1, int(self.winfo_width())) + h = max(1, int(self.winfo_height())) + iw, ih = self._pil.size + sw = max(1, int(iw * self._scale)) + sh = max(1, int(ih * self._scale)) + x = (w - sw) // 2 + y = (h - sh) // 2 + self.coords(self._img_item, x, y) + self.configure(scrollregion=self.bbox("all")) + + def _on_configure(self, _e): + if self._pil is None: + self._redraw() + return + if self._fit_on_set: + self.fit_to_view() + else: + self._redraw() + + def _pan_start(self, e): + self._pan_last = (e.x, e.y) + + def _pan_move(self, e): + if not self._pan_last: + return + x0, y0 = self._pan_last + dx = e.x - x0 + dy = e.y - y0 + self.move("all", dx, dy) + self._pan_last = (e.x, e.y) + + def _wheel(self, e): + if e.delta == 0: + return + factor = 1.1 if e.delta > 0 else 1.0 / 1.1 + self._fit_on_set = False + self._zoom_at(e.x, e.y, factor) + + def _wheel_linux(self, e): + self._fit_on_set = False + if e.num == 4: + self._zoom_at(e.x, e.y, 1.1) + elif e.num == 5: + self._zoom_at(e.x, e.y, 1.0 / 1.1) + + def _zoom_at(self, x, y, factor): + if self._pil is None: + return + self._scale *= factor + self._scale = max(self._min_scale, min(self._max_scale, self._scale)) + self._redraw(anchor=(x, y)) + + def _redraw(self, center=False, anchor=None): + if self._pil is None: + self.delete("all") + self.create_text( + 16, 16, anchor="nw", fill="#cfcfe6", text="No image selected" + ) + return + + w = max(1, int(self.winfo_width())) + h = max(1, int(self.winfo_height())) + + iw, ih = self._pil.size + sw = max(1, int(iw * self._scale)) + sh = max(1, int(ih * self._scale)) + + if ImageTk is None: + self.delete("all") + self.create_text( + 16, + 16, + anchor="nw", + fill="#cfcfe6", + text="Pillow required for image preview (pip install pillow)", + ) + return + + img = self._pil.resize( + (sw, sh), resample=Image.NEAREST if max(sw, sh) < 2048 else Image.BILINEAR + ) + self._photo = ImageTk.PhotoImage(img) + + if self._img_item is None: + self.delete("all") + x = (w - sw) // 2 if center else 0 + y = (h - sh) // 2 if center else 0 + self._img_item = self.create_image(x, y, image=self._photo, anchor="nw") + self.configure(scrollregion=self.bbox("all")) + return + + bx0, by0, bx1, by1 = self.bbox(self._img_item) + old_w = max(1, bx1 - bx0) + old_h = max(1, by1 - by0) + + ax, ay = (w // 2, h // 2) if anchor is None else anchor + rel_x = (ax - bx0) / float(old_w) + rel_y = (ay - by0) / float(old_h) + + new_x0 = ax - rel_x * sw + new_y0 = ay - rel_y * sh + + self.itemconfigure(self._img_item, image=self._photo) + self.coords(self._img_item, new_x0, new_y0) + self.configure(scrollregion=self.bbox("all")) + + +if OpenGLFrame is not None and GL is not None and np is not None: + + class ModelViewport(OpenGLFrame): + def __init__(self, parent): + super().__init__(parent) + self._program = None + self._vao = None + self._vbo = None + self._ibo = None + self._idx_count = 0 + self._rot_x = 20.0 + self._rot_y = -30.0 + self._dist = 3.0 + self._pan_x = 0.0 + self._pan_y = 0.0 + self._last = None + self._blob = None + self._base = 0 + self.bind("", self._drag_start) + self.bind("", self._drag_rotate) + self.bind("", self._drag_start) + self.bind("", self._drag_pan) + self.bind("", self._wheel) + self.bind("", self._wheel_linux) + self.bind("", self._wheel_linux) + + def load_imesh(self, blob, base_off): + self._blob = blob + self._base = int(base_off) + self.after(1, self._rebuild_mesh) + + def initgl(self): + vs = """ + #version 330 core + layout(location=0) in vec3 aPos; + layout(location=1) in vec3 aNor; + uniform mat4 uMVP; + uniform mat4 uM; + out vec3 vN; + void main(){ + vN = mat3(uM) * aNor; + gl_Position = uMVP * vec4(aPos,1.0); + } + """ + fs = """ + #version 330 core + in vec3 vN; + out vec4 o; + void main(){ + vec3 n = normalize(vN); + vec3 l = normalize(vec3(0.6,0.8,0.3)); + float ndl = max(dot(n,l),0.0); + vec3 base = vec3(0.78,0.80,0.84); + vec3 col = base*(0.12 + 0.88*ndl); + o = vec4(col,1.0); + } + """ + self._program = self._compile_program(vs, fs) + GL.glEnable(GL.GL_DEPTH_TEST) + GL.glEnable(GL.GL_CULL_FACE) + GL.glCullFace(GL.GL_BACK) + GL.glFrontFace(GL.GL_CCW) + + def redraw(self): + w = max(1, int(self.winfo_width())) + h = max(1, int(self.winfo_height())) + GL.glViewport(0, 0, w, h) + GL.glClearColor(0.06, 0.06, 0.07, 1.0) + GL.glClear(GL.GL_COLOR_BUFFER_BIT | GL.GL_DEPTH_BUFFER_BIT) + if not self._program or not self._vao or self._idx_count == 0: + return + m = self._mat_identity() + v = self._mat_view() + p = self._mat_persp(55.0, w / float(h), 0.01, 500.0) + mvp = self._mat_mul(p, self._mat_mul(v, m)) + GL.glUseProgram(self._program) + loc_mvp = GL.glGetUniformLocation(self._program, "uMVP") + loc_m = GL.glGetUniformLocation(self._program, "uM") + GL.glUniformMatrix4fv(loc_mvp, 1, GL.GL_FALSE, (GL.GLfloat * 16)(*mvp)) + GL.glUniformMatrix4fv(loc_m, 1, GL.GL_FALSE, (GL.GLfloat * 16)(*m)) + GL.glBindVertexArray(self._vao) + GL.glDrawElements( + GL.GL_TRIANGLES, self._idx_count, GL.GL_UNSIGNED_INT, None + ) + GL.glBindVertexArray(0) + GL.glUseProgram(0) + + def _drag_start(self, e): + self._last = (e.x, e.y) + + def _drag_rotate(self, e): + if not self._last: + return + x0, y0 = self._last + dx = e.x - x0 + dy = e.y - y0 + self._rot_y += dx * 0.4 + self._rot_x += dy * 0.4 + self._rot_x = max(-89.9, min(89.9, self._rot_x)) + self._last = (e.x, e.y) + + def _drag_pan(self, e): + if not self._last: + return + x0, y0 = self._last + dx = e.x - x0 + dy = e.y - y0 + self._pan_x += dx * 0.002 + self._pan_y -= dy * 0.002 + self._last = (e.x, e.y) + + def _wheel(self, e): + if e.delta == 0: + return + self._dist *= 0.9 if e.delta > 0 else 1.1 + self._dist = max(0.1, min(200.0, self._dist)) + + def _wheel_linux(self, e): + if e.num == 4: + self._dist *= 0.9 + elif e.num == 5: + self._dist *= 1.1 + self._dist = max(0.1, min(200.0, self._dist)) + + def _compile_program(self, vs_src, fs_src): + v = GL.glCreateShader(GL.GL_VERTEX_SHADER) + GL.glShaderSource(v, vs_src) + GL.glCompileShader(v) + if GL.glGetShaderiv(v, GL.GL_COMPILE_STATUS) != GL.GL_TRUE: + raise RuntimeError( + GL.glGetShaderInfoLog(v).decode("utf-8", errors="ignore") + ) + f = GL.glCreateShader(GL.GL_FRAGMENT_SHADER) + GL.glShaderSource(f, fs_src) + GL.glCompileShader(f) + if GL.glGetShaderiv(f, GL.GL_COMPILE_STATUS) != GL.GL_TRUE: + raise RuntimeError( + GL.glGetShaderInfoLog(f).decode("utf-8", errors="ignore") + ) + p = GL.glCreateProgram() + GL.glAttachShader(p, v) + GL.glAttachShader(p, f) + GL.glLinkProgram(p) + if GL.glGetProgramiv(p, GL.GL_LINK_STATUS) != GL.GL_TRUE: + raise RuntimeError( + GL.glGetProgramInfoLog(p).decode("utf-8", errors="ignore") + ) + GL.glDeleteShader(v) + GL.glDeleteShader(f) + return p + + def _rebuild_mesh(self): + if self._blob is None: + return + verts, idx = self._extract_first_lod0(self._blob, self._base) + if verts is None or idx is None: + return + + if self._vao: + GL.glDeleteVertexArrays(1, [self._vao]) + self._vao = None + if self._vbo: + GL.glDeleteBuffers(1, [self._vbo]) + self._vbo = None + if self._ibo: + GL.glDeleteBuffers(1, [self._ibo]) + self._ibo = None + + self._vao = GL.glGenVertexArrays(1) + self._vbo = GL.glGenBuffers(1) + self._ibo = GL.glGenBuffers(1) + + stride = 12 + 12 + 8 + 16 + + GL.glBindVertexArray(self._vao) + + GL.glBindBuffer(GL.GL_ARRAY_BUFFER, self._vbo) + GL.glBufferData(GL.GL_ARRAY_BUFFER, int(verts.nbytes), verts, GL.GL_STATIC_DRAW) + + GL.glBindBuffer(GL.GL_ELEMENT_ARRAY_BUFFER, self._ibo) + GL.glBufferData(GL.GL_ELEMENT_ARRAY_BUFFER, int(idx.nbytes), idx, GL.GL_STATIC_DRAW) + + GL.glEnableVertexAttribArray(0) + GL.glVertexAttribPointer(0, 3, GL.GL_FLOAT, GL.GL_FALSE, stride, GL.ctypes.c_void_p(0)) + + GL.glEnableVertexAttribArray(1) + GL.glVertexAttribPointer(1, 3, GL.GL_FLOAT, GL.GL_FALSE, stride, GL.ctypes.c_void_p(12)) + + GL.glBindVertexArray(0) + + self._idx_count = int(idx.size) + + self._pan_x = 0.0 + self._pan_y = 0.0 + self._dist = 3.0 + self._rot_x = 20.0 + self._rot_y = -30.0 + + + def _extract_first_lod0(self, blob, base_off): + n = len(blob) + if base_off < 0 or base_off + 40 > n: + return None, None + + submesh_count = struct.unpack_from(" n: + return None, None + + sm0 = smt + lod_count = struct.unpack_from(" n: + return None, None + + vcount, icount, voff, ioff = struct.unpack_from(" n: + return None, None + if ib < base_off or ib + ibytes > n: + return None, None + + verts_u8 = np.frombuffer( + blob, dtype=np.uint8, count=vbytes, offset=vb + ).copy() + idx_u32 = np.frombuffer( + blob, dtype=np.uint32, count=int(icount), offset=ib + ).copy() + + verts_f32 = verts_u8.view(np.float32) + return verts_f32, idx_u32 + + def _mat_identity(self): + return [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1] + + def _mat_mul(self, a, b): + r = [0.0] * 16 + for row in range(4): + for col in range(4): + r[col + row * 4] = ( + a[0 + row * 4] * b[col + 0 * 4] + + a[1 + row * 4] * b[col + 1 * 4] + + a[2 + row * 4] * b[col + 2 * 4] + + a[3 + row * 4] * b[col + 3 * 4] + ) + return r + + def _mat_translate(self, x, y, z): + m = self._mat_identity() + m[12] = x + m[13] = y + m[14] = z + return m + + def _mat_rot_x(self, deg): + import math + + a = math.radians(deg) + c = math.cos(a) + s = math.sin(a) + return [1, 0, 0, 0, 0, c, s, 0, 0, -s, c, 0, 0, 0, 0, 1] + + def _mat_rot_y(self, deg): + import math + + a = math.radians(deg) + c = math.cos(a) + s = math.sin(a) + return [c, 0, -s, 0, 0, 1, 0, 0, s, 0, c, 0, 0, 0, 0, 1] + + def _mat_persp(self, fovy_deg, aspect, zn, zf): + import math + + fovy = math.radians(fovy_deg) + f = 1.0 / math.tan(fovy * 0.5) + m = [0.0] * 16 + m[0] = f / aspect + m[5] = f + m[10] = (zf + zn) / (zn - zf) + m[11] = -1.0 + m[14] = (2.0 * zf * zn) / (zn - zf) + return m + + def _mat_view(self): + t_pan = self._mat_translate(self._pan_x, self._pan_y, -self._dist) + rx = self._mat_rot_x(self._rot_x) + ry = self._mat_rot_y(self._rot_y) + return self._mat_mul(t_pan, self._mat_mul(rx, ry)) + +else: + ModelViewport = None + + +class AssetInspectorApp: + def __init__(self, root): + self.root = root + self.root.title("Asset Inspector") + self.assets = [] + self.by_handle = {} + self.selected_index = None + self._current_pil = None + self._build_ui() + + def _build_ui(self): + self.root.geometry("1560x900") + + top = ttk.Frame(self.root) + top.pack(side="top", fill="x") + + ttk.Button(top, text="Open Assets...", command=self.open_assets).pack( + side="left", padx=6, pady=6 + ) + + self.status_var = tk.StringVar(value="Ready") + ttk.Label(top, textvariable=self.status_var).pack(side="left", padx=10) + + self.progress = ttk.Progressbar( + top, orient="horizontal", length=280, mode="determinate" + ) + self.progress.pack(side="right", padx=8, pady=6) + + body = ttk.Panedwindow(self.root, orient="horizontal") + body.pack(side="top", fill="both", expand=True) + + left = ttk.Frame(body, width=360) + mid = ttk.Frame(body) + right = ttk.Frame(body, width=420) + + body.add(left, weight=0) + body.add(mid, weight=1) + body.add(right, weight=0) + + ttk.Label(left, text="Assets").pack(anchor="w", padx=10, pady=(10, 0)) + self.asset_list = tk.Listbox(left, exportselection=False) + self.asset_list.pack(fill="both", expand=True, padx=10, pady=8) + self.asset_list.bind("<>", self._on_list_select) + + mid_top = ttk.Frame(mid) + mid_top.pack(side="top", fill="both", expand=True) + + self.preview_tabs = ttk.Notebook(mid_top) + self.preview_tabs.pack(fill="both", expand=True) + + self.tab_image = ttk.Frame(self.preview_tabs) + self.tab_model = ttk.Frame(self.preview_tabs) + self.tab_material = ttk.Frame(self.preview_tabs) + + self.preview_tabs.add(self.tab_image, text="Image") + self.preview_tabs.add(self.tab_model, text="Model") + self.preview_tabs.add(self.tab_material, text="Material") + + img_bar = ttk.Frame(self.tab_image) + img_bar.pack(side="top", fill="x") + + self.image_canvas = ZoomPanImageCanvas(self.tab_image) + self.image_canvas.pack(side="top", fill="both", expand=True) + + ttk.Button(img_bar, text="Fit", command=self._img_fit).pack( + side="left", padx=6, pady=6 + ) + ttk.Button(img_bar, text="1:1", command=self._img_100).pack( + side="left", padx=6, pady=6 + ) + ttk.Button(img_bar, text="Center", command=self._img_center).pack( + side="left", padx=6, pady=6 + ) + + self.model_view = None + self.model_fallback = ttk.Label(self.tab_model, text="", anchor="center") + self.model_fallback.pack(fill="both", expand=True) + + if ModelViewport is not None: + try: + self.model_fallback.pack_forget() + self.model_view = ModelViewport(self.tab_model) + self.model_view.pack(fill="both", expand=True) + except Exception as e: + self.model_view = None + self.model_fallback.configure(text=f"Model preview unavailable:\n{e}") + self.model_fallback.pack(fill="both", expand=True) + else: + why = [] + if OpenGLFrame is None: + why.append("pyopengltk") + if GL is None: + why.append("PyOpenGL") + if np is None: + why.append("numpy") + self.model_fallback.configure( + text="Model preview unavailable.\nMissing: " + ", ".join(why) + ) + + self.mat_text = tk.Text(self.tab_material, wrap="word") + self.mat_text.pack(fill="both", expand=True) + + self._build_right_panel(right) + + def _img_fit(self): + self.image_canvas._fit_on_set = True + self.image_canvas.fit_to_view() + + def _img_100(self): + self.image_canvas._fit_on_set = False + self.image_canvas.zoom_100() + + def _img_center(self): + self.image_canvas._fit_on_set = False + self.image_canvas.center() + + def _build_right_panel(self, parent): + ttk.Label(parent, text="Details").pack(anchor="w", padx=10, pady=(10, 0)) + self.details = tk.Text(parent, height=14, wrap="word") + self.details.pack(fill="x", padx=10, pady=8) + + ttk.Separator(parent, orient="horizontal").pack(fill="x", padx=10, pady=8) + + refs_wrap = ttk.Frame(parent) + refs_wrap.pack(fill="both", expand=True, padx=10, pady=(0, 10)) + + ttk.Label(refs_wrap, text="References").pack(anchor="w") + self.refs_list = tk.Listbox(refs_wrap, exportselection=False, height=10) + self.refs_list.pack(fill="both", expand=True, pady=(6, 10)) + self.refs_list.bind( + "", lambda e: self._jump_from_list(self.refs_list, True) + ) + + ttk.Label(refs_wrap, text="Referenced By").pack(anchor="w") + self.refby_list = tk.Listbox(refs_wrap, exportselection=False, height=10) + self.refby_list.pack(fill="both", expand=True, pady=6) + self.refby_list.bind( + "", lambda e: self._jump_from_list(self.refby_list, False) + ) + + def open_assets(self): + paths = filedialog.askopenfilenames( + title="Open Assets", + filetypes=[ + ("Asset files", "*.iasset *.itex *.imesh *.imat *.*"), + ("All files", "*.*"), + ], + ) + if not paths: + return + + self.root.config(cursor="watch") + self.root.update_idletasks() + + self.progress["value"] = 0 + self.progress["maximum"] = len(paths) + self.status_var.set(f"Loading {len(paths)} assets...") + self.assets = [] + self.by_handle = {} + self.selected_index = None + self._current_pil = None + self.asset_list.delete(0, "end") + self.details.delete("1.0", "end") + self.refs_list.delete(0, "end") + self.refby_list.delete(0, "end") + self.mat_text.delete("1.0", "end") + self.image_canvas.clear() + + for i, p in enumerate(paths): + ar = self._load_one(p) + self.assets.append(ar) + if ar.handle: + self.by_handle[handle_key(ar.handle)] = ar + self.progress["value"] = i + 1 + self.status_var.set(f"Loading {i + 1}/{len(paths)}: {os.path.basename(p)}") + self.root.update_idletasks() + + self._rebuild_refs() + self._refresh_list() + + self.status_var.set(f"Loaded {len(paths)} assets") + self.progress["value"] = 0 + self.root.config(cursor="") + + if self.assets: + self._select_asset_index(0) + + def _load_one(self, path): + ar = AssetRecord(path) + t0 = time.perf_counter() + data = read_all(path) + sig, off = find_first_signature(data) + + try: + if sig == "ITEX": + h, info, pixels = load_itex_from_bytes(data, off) + ar.kind = ASSET_IMAGE + ar.handle = h + ar.refs = [] + ar.info = info + ar.payload = {"pixels": pixels} + ar.compressed_size = int(info.get("compressed_size", 0)) + ar.decompressed_size = int(info.get("decompressed_size", 0)) + elif sig == "IMSH": + m = load_imesh_from_bytes(data, off) + ar.kind = ASSET_MODEL + ar.handle = m["handle"] + ar.refs = list(m.get("refs", [])) + ar.info = dict(m.get("info", {})) + ar.payload = m + ar.compressed_size = 0 + ar.decompressed_size = len(data) + else: + mat = try_load_material_from_bytes(data) + if mat is not None: + ar.kind = ASSET_MATERIAL + ar.handle = mat["handle"] + ar.refs = list(mat.get("refs", [])) + ar.info = dict(mat.get("info", {})) + ar.payload = mat + ar.compressed_size = 0 + ar.decompressed_size = len(data) + else: + ar.kind = ASSET_UNKNOWN + ar.info = {"file_size": len(data)} + except Exception as e: + ar.kind = ASSET_UNKNOWN + ar.info = {"error": str(e), "file_size": len(data)} + + t1 = time.perf_counter() + ar.load_ms = (t1 - t0) * 1000.0 + if ar.decompressed_size == 0: + ar.decompressed_size = len(data) + if "file_size" not in ar.info: + ar.info["file_size"] = len(data) + return ar + + def _rebuild_refs(self): + for a in self.assets: + a.ref_by = [] + for a in self.assets: + for h in a.refs: + k = handle_key(h) + if k is None: + continue + b = self.by_handle.get(k) + if b is not None: + b.ref_by.append(a.handle if a.handle else None) + + def _refresh_list(self): + self.asset_list.delete(0, "end") + for a in self.assets: + base = os.path.basename(a.path) + self.asset_list.insert("end", f"[{a.kind}] {base}") + + def _on_list_select(self, _): + sel = self.asset_list.curselection() + if not sel: + return + self._select_asset_index(int(sel[0])) + + def _select_asset_index(self, idx): + if idx < 0 or idx >= len(self.assets): + return + self.selected_index = idx + self.asset_list.selection_clear(0, "end") + self.asset_list.selection_set(idx) + self.asset_list.see(idx) + self._show_selected() + + def _show_selected(self): + a = self.assets[self.selected_index] + self._fill_details(a) + self._fill_refs(a) + + if a.kind == ASSET_IMAGE: + self.preview_tabs.select(self.tab_image) + self._show_image(a) + elif a.kind == ASSET_MODEL: + self.preview_tabs.select(self.tab_model) + self._show_model(a) + elif a.kind == ASSET_MATERIAL: + self.preview_tabs.select(self.tab_material) + self._show_material(a) + else: + self.preview_tabs.select(self.tab_material) + self.mat_text.delete("1.0", "end") + self.mat_text.insert("end", "Unknown asset") + + def _fill_details(self, a): + self.details.delete("1.0", "end") + self.details.insert("end", f"Name: {os.path.basename(a.path)}\n") + self.details.insert("end", f"Type: {a.kind}\n") + self.details.insert("end", f"Handle: {fmt_handle(a.handle)}\n") + self.details.insert("end", f"Load: {a.load_ms:.2f} ms\n") + self.details.insert( + "end", f"File: {fmt_bytes(int(a.info.get('file_size', 0)))}\n" + ) + if a.compressed_size: + self.details.insert("end", f"Compressed: {fmt_bytes(a.compressed_size)}\n") + if a.decompressed_size: + self.details.insert( + "end", f"Decompressed: {fmt_bytes(a.decompressed_size)}\n" + ) + if a.kind == ASSET_IMAGE: + self.details.insert( + "end", f"Size: {a.info.get('width')} x {a.info.get('height')}\n" + ) + self.details.insert("end", f"Channels: {a.info.get('channels')}\n") + self.details.insert("end", f"Float: {a.info.get('is_float')}\n") + self.details.insert( + "end", f"Decompress: {float(a.info.get('decompress_ms', 0.0)):.2f} ms\n" + ) + if a.kind == ASSET_MODEL: + self.details.insert("end", f"Submeshes: {a.info.get('submesh_count')}\n") + self.details.insert("end", f"Flags: {a.info.get('flags')}\n") + if a.kind == ASSET_MATERIAL: + if "shader_id" in a.info: + self.details.insert("end", f"Shader ID: {a.info.get('shader_id')}\n") + if "flags" in a.info: + self.details.insert("end", f"Flags: {a.info.get('flags')}\n") + + def _fill_refs(self, a): + self.refs_list.delete(0, "end") + self.refby_list.delete(0, "end") + for h in a.refs: + target = self.by_handle.get(handle_key(h)) + if target is not None: + self.refs_list.insert( + "end", f"{fmt_handle(h)} -> {os.path.basename(target.path)}" + ) + else: + self.refs_list.insert("end", f"{fmt_handle(h)} -> (unloaded)") + for h in a.ref_by: + if not h: + continue + src = self.by_handle.get(handle_key(h)) + if src is not None: + self.refby_list.insert( + "end", f"{fmt_handle(h)} <- {os.path.basename(src.path)}" + ) + else: + self.refby_list.insert("end", f"{fmt_handle(h)} <- (unloaded)") + + def _jump_from_list(self, lb, _is_refs): + sel = lb.curselection() + if not sel: + return + line = lb.get(int(sel[0])) + if "->" in line: + rhs = line.split("->", 1)[1].strip() + if rhs.startswith("(unloaded)"): + messagebox.showinfo("Jump", "That referenced asset isn't loaded yet.") + return + name = rhs + elif "<-" in line: + rhs = line.split("<-", 1)[1].strip() + if rhs.startswith("(unloaded)"): + messagebox.showinfo("Jump", "That referencing asset isn't loaded yet.") + return + name = rhs + else: + return + for i, a in enumerate(self.assets): + if os.path.basename(a.path) == name: + self._select_asset_index(i) + return + + def _show_image(self, a): + if Image is None: + self.image_canvas.clear() + self.image_canvas.create_text( + 16, + 16, + anchor="nw", + fill="#cfcfe6", + text="Pillow required for image preview (pip install pillow)", + ) + return + + info = a.info + px = a.payload.get("pixels") if a.payload else None + if px is None: + self.image_canvas.clear() + return + + w = int(info.get("width", 0)) + h = int(info.get("height", 0)) + ch = int(info.get("channels", 4)) + is_float = int(info.get("is_float", 0)) + + if w <= 0 or h <= 0: + self.image_canvas.clear() + return + + if is_float: + pil = float_pixels_to_pil(px, w, h, ch) + if pil is None: + self.image_canvas.clear() + self.image_canvas.create_text( + 16, + 16, + anchor="nw", + fill="#cfcfe6", + text="Float preview requires Pillow (and numpy is recommended).", + ) + return + else: + mode = "RGBA" if ch == 4 else ("RGB" if ch == 3 else "L") + pil = Image.frombytes(mode, (w, h), px) + if ch != 4: + pil = pil.convert("RGBA") + + self._current_pil = pil + self.image_canvas._fit_on_set = True + self.image_canvas.set_image(pil, fit=True) + + def _show_model(self, a): + if self.model_view is None: + return + m = a.payload + if not m: + return + self.model_view.load_imesh(m["blob"], m["base_off"]) + + def _show_material(self, a): + self.mat_text.delete("1.0", "end") + mat = a.payload + if not mat: + self.mat_text.insert("end", "No material data") + return + obj = mat.get("obj") + self.mat_text.insert("end", self._pretty(obj, 0)) + + def _pretty(self, x, indent): + sp = " " * indent + if isinstance(x, dict): + parts = [] + for k in sorted(x.keys()): + parts.append(f"{sp}{k}:") + parts.append(self._pretty(x[k], indent + 1)) + return "\n".join(parts) + ("\n" if parts else "") + if isinstance(x, list): + parts = [f"{sp}- {self._pretty(v, indent + 1).lstrip()}" for v in x] + return "\n".join(parts) + ("\n" if parts else "") + return f"{sp}{x}\n" + + +def main(): + root = tk.Tk() + try: + style = ttk.Style() + if "vista" in style.theme_names(): + style.theme_use("vista") + except Exception: + pass + AssetInspectorApp(root) + root.mainloop() + + +if __name__ == "__main__": + main()