From 35534b15647f12c63e4a76569f5381c420f5697f Mon Sep 17 00:00:00 2001 From: JianFeeeee Date: Sat, 26 Sep 2026 00:24:29 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20get=5Fblock=5Finfo=20=E2=80=94=E2=80=94?= =?UTF-8?q?=20=E6=96=B9=E5=9D=97=E4=BF=A1=E6=81=AF=E6=9F=A5=E8=AF=A2?= =?UTF-8?q?=EF=BC=88Java=20=E4=BE=A7=E5=9B=9E=E7=AD=94=EF=BC=8CPython=20?= =?UTF-8?q?=E5=8F=AA=E8=B7=AF=E7=94=B1=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 cmd=blockinfo(两代后端编译通过、实测): 返回 block 名(含 mod 方块)、stateId、属性、isAir/isSolid、 光照/硬度、贴图缺失的程序化判据、是否来自 mod。 ★ 修掉一个真实缺陷(附录 D 的语义区分落到了实处): 未加载区块原先返回 void_air(known=true),实测发现两个 API 都不可用: - ClientLevel.hasChunk() 反编译实测**恒返回 true** - ChunkSource.hasChunk() 走 getChunk(...,false),而 ClientChunkCache 覆写后在该参数下返回 emptyChunk(占位,非 null)⇒ 也恒 true 正解:getChunk(..., requireChunk=**false**) 判 null (反编译:return requireChunk ? emptyChunk : null;) ⇒ 现在未加载明确回 known=false + reason=chunk_not_loaded 实测(1.20.1): 草地 → minecraft:grass_block props={'snowy':'false'} missing=False 原木 → minecraft:oak_log props={'axis':'y'} 空气 → minecraft:air 远处 → known=false reason=chunk_not_loaded ← 与空气区分 实测(1.21.1 + mod): mts:mtsofficialpack.fancydirt_block mod=True tex=mtsofficialpack:block/fancydirt createbigcannons:steel_block mod=True tex=createbigcannons:block/steel_block ⇒ mod 方块同样能查到正确贴图名与 fromMod 标记 顺带修 gen_1_21 两处世代差异(ChunkStatus 换包、bridge 编译顺序)。 测试:69 项全绿(新增 test_get_block_info 6 条判据) --- logs/2026-09-25-1.log.gz | Bin 1517 -> 1513 bytes logs/2026-09-25-2.log.gz | Bin 1514 -> 1539 bytes logs/2026-09-25-3.log.gz | Bin 1513 -> 1511 bytes logs/2026-09-25-4.log.gz | Bin 1539 -> 1511 bytes logs/2026-09-25-5.log.gz | Bin 1511 -> 1513 bytes logs/2026-09-25-6.log.gz | Bin 1511 -> 1516 bytes logs/2026-09-25-7.log.gz | Bin 1513 -> 0 bytes logs/2026-09-26-1.log.gz | Bin 0 -> 1512 bytes logs/2026-09-26-2.log.gz | Bin 0 -> 1513 bytes logs/2026-09-26-3.log.gz | Bin 0 -> 1512 bytes logs/2026-09-26-4.log.gz | Bin 0 -> 1516 bytes logs/latest.log | 72 ++-- mcfold/__pycache__/render.cpython-312.pyc | Bin 20885 -> 24546 bytes mcfold/backends/__init__.py | 38 +- .../__pycache__/__init__.cpython-312.pyc | Bin 4891 -> 6187 bytes .../backends/__pycache__/base.cpython-312.pyc | Bin 22084 -> 24335 bytes mcfold/backends/base.py | 45 ++- mcfold/render.py | 77 +++- scaffold/RegistryProbe.java | 82 ----- scaffold/WorldRender.java | 344 ------------------ scaffold/build.sh | 121 +++--- .../java/gen_1_20/McfoldRenderServer.java | 321 +++++++++++++++- .../java/gen_1_21/McfoldRenderServer.java | 324 ++++++++++++++++- scaffold/java/launcher_fabric/LoaderBoot.java | 67 ++++ tests/test_render_bridge.py | 69 +++- 25 files changed, 1038 insertions(+), 522 deletions(-) delete mode 100644 logs/2026-09-25-7.log.gz create mode 100644 logs/2026-09-26-1.log.gz create mode 100644 logs/2026-09-26-2.log.gz create mode 100644 logs/2026-09-26-3.log.gz create mode 100644 logs/2026-09-26-4.log.gz delete mode 100644 scaffold/RegistryProbe.java delete mode 100644 scaffold/WorldRender.java create mode 100644 scaffold/java/launcher_fabric/LoaderBoot.java diff --git a/logs/2026-09-25-1.log.gz b/logs/2026-09-25-1.log.gz index 3ea8980f0863142f17ca4009f34d04e18c50bf1e..048ec35ee176f528e2f3c8a5d57486aa19758ffe 100644 GIT binary patch delta 1476 zcmV;#1v~of3+W4gABzY8000000RPRHTW{hx6o8+%zrqirZC9GQBm~N!ogFc~>~z$d zpw(zqRgp;!;H_gvw$p)m*x$a#2~cQBC~Zf(4@D5$-?{so^v2t72Y$Qu;g;MSGRp>h z44Qma${MR#x_5GZd~w?*UaLhOmQ`wb!hbN!l{CaEbw@mZuTBy!1UC>V8Jn$}0_@cg zoG>nDq$7FHwUP-)+a@ftqc6(XkJ}?_Q`6qtb59KDd(bpde=|9xkxI7T6Bb1(lh!=i zo$}>v28=YNm%<5xh@f96_2pl%B|YCU|_HA$Dh&Fh{e+HtAVrGt(wP z&D;8gSyGKUn^|!nII_N=fpZqMsE?W}m7n;$f$IA*Zi_bM>1P)jjhEqIJMV41;M?oZmUsI_O+ZMpvnBE=BJCo;`! z&*InQahVo>CbTxs&b5MvU{?zZTL1Yq;D4x4fl^AEBvBel8&VIf<#I?c%DPRGJTbzE zC8Y;Gv*->JhoCskG^WMrx37Pmx|`m08B(Wp>XClNMNIZfAve9NL+lprTYemuuSPDu zrz&FN+H!GAuBFM+RB4Op_x1c_E+GaB5k7oR?1&pGlCTNUU@~C|mxK`vk{MBQDS#Qk zM+2pksg5z+gK&N|^r>dwI^A>Br(=`B0w;f+!@Wmlcfmp2rY@NLGB{(B4V~xjnZ)^l zEgk))^d0EkGbHSK-VAb#lR!)|$p=c>kO>h=6{Z>6^%@xcT9b}o{GLn&m8!B3fgo`_H3v*(i(yDDEFL<-o!Zd z$W^ALd#DD3LbxHnk|Z-c+HzG&g}VVxgn%Iv?k0qVoAF^-!pxN90zW!=s$z{lzC`+h z{eXImmr2YOJuNDbx^sv5{tJDK`OtsI3SI0qeyg%~t>wGswaD`toA<)PD>V1oksRVO zbs0Jpu|)TpZ$a02sV#wPuLDn$nX)t$%NqNQp92nR&*x6Z`xIku(D>qZK><8T1zc)K znPzfVq7cA+=>|Nokcixg!l|jzaI>UP!-wR}vY3+ zCGSKo)v48!W3FN6$)2K*?R$AE{j?!}`ozuUI+JDapyj^+PP3Wv=jwZ@-i_c@XuJ)E z;4wv94@({R{VW2j_Lhr!;6VTpg@tAVRjJ4qod zWrHW;hQtg?!q&g8DR-e-761S%sO47x delta 1478 zcmV;%1v&cZ3+)SkABzY8000000RPRHTTkOS6o8+XU*U(*!b*c_(zJA{ogFbicNlR~ z3mUBuirnP1_2Sr(?R46C*x$a#Nn0p1DFsHm4^(Yzf9K+J3pd_=yZ)iwsNIsABWBrv zPe7B;N?BtyOZQIBI~TWY;-VNs+qY0abE zDPP`Zz!+3&dDraWDy)0PBUpvy5+cn8cD`kHLb3!tZeMmUj4nmd%9$T1Ti&}NCq}F0(r}Eg}Y|Pq?=5OSmo0-JjfYxYG#}(cF zbX%s@bU2JPGhEQW=HIT-xd$ol6)vU&u+ivJs}!`8djT1LTtK9OSy-03v*t|U4nL^Y zN`k!)Y8vGidmkUG1@}~@Bj{i-xA*IH$1l$9x-5t{pwHa;fENUQ74Iw>C=qu-#-Mve z;`Xs9?W}m7n;$f0IA$>2_bM6;Ph^_e zp2e@p<1{UQOlWN$oofXT!LAk-wEpul;D4x4fl^AEBvBfk7VB9<>VdUf4hi0}Zj&Sr zj4)zJ>4DEIy2HRBC{8ntVR8EH>z}8t(z`B0>a!ns@b=L?m6mnV3WK8Cx0z9y9*BDHg&<|#o&xdHgujJGl}y9 zTO6(9(QiuMf!;ks!ftcU=-?m_lT7k~k~U;Qgi?iR#&*31UVg1fM=*I$W^$HrB^OZK z`7K#QIEJmn@h5Xm&H+K~SpY(=PR@=muCK@yOl+p-#yw@se0|1oGC^mU+$85?ydXCi zEq^ky&G9zqP%#^hl$JCVZ2B6U>mGKR1F3N}AD4#Ip~F7@x_io=&oswcBXDuaJtw0# zHBLQpm1*f7s==TTZpg1B$qbLST#{1Z3c!gFFl53NLU>RZ?`I_pOi3>A;~2ND=fT1bFUrC zAudxFp)(Onbg%gqbe(6~61es{@GO`qOH;9|vETSP;Gp(=?rgeG@$L;8UlbP`$K zrG}Jg))!C|pi_=4?hWyzRH)uj z8Vt*wQg6Hj?q=kIeqg3DDx>?pUthw%ML?}cyUI9PF^Cq3d4DvevueYyKXc?=GdVYZ z(&Sv$IX7?Mt0;&03rpN4Ud<0C4S&y{1P}V0EgG=dV-_{q>kH+sF_pvJ{K0yv-)K(U zUj@|gq1=e+lw7AMlNd%}%5VzPZ%#KO4mjPugr0jj|EStWJ}EW)25ByMCJ0}eqMzOP7S$$-0=pSdch^Z$0mq*wg%-TMU=S7K6#_<-Yz zDTJh24K+*XQ)SAox>jyz>L$!cR2%ssE|+d}-H7YL$o*At6V(r4Fu+%`=a5&tHY}P8 gDN6r?myZASrLHi=bs7>Y2@6~Q0%Z+#CRr8$0MdxsV*mgE diff --git a/logs/2026-09-25-2.log.gz b/logs/2026-09-25-2.log.gz index e0d577a75469ee2255d78bed18c0cbd181806f63..90305b76b884de9f547a55b914b7012856de2e2f 100644 GIT binary patch delta 1528 zcmVo zy9cV)AHNyThiA;Sx7TTT9lvoyt`C`I13m^#J}G65)hydNKI@*}bcok(lly6vS)TGA z%yK0Su}a+%uhAwc7lIpzl}yaW^%U$OIAvVUNJsLHYb8^VwtqudZbzS$u^%=^)@G)& zv*Sh#=sVCfRo|E#(paUN?+J@zl}l?LtEUJsk0^{| z$O?nfV|m8}Pk#OJGu|&vOb1}& z(S=qSXv@KDRjn&D@P3L4e{jj|pY@Vkt+x=oX#1IfSbqbvuqbtF#h%g}eo(KK1bZLU zG%g%_A0NvF_f@WA=wdK;_L@z{FZXS`EJ)U%PtE#(7X*GC?=&7Lk@P?&p!;RwMtCW0 zEg3I;;_a053Hjm4o7d$p<*^1DJ^Lx*yJ@hS3T^c08-=b ze4H9mhYlnBb^DY(>uHX(M&R7ZeJ7(oF@H`ya+z!C2Gw9t3OD3elIDiT8!kzya2LR_ z5HMuIU4-!9V!ZE_Ffb)K$B+FYQZdCJ-yxA;KcJc5U6OD`Ps#+OZr@?C{#<9?1YBsym}ZfH@&Y>H7`GCa3zkL+dsiCkowsnR zS|=A_o#Qo)&jK0eTDwCXC@0K1Io;gl|GO*6_hT38)apr>YuI|SrYvK7UQtOuuF0Q{ zxVBv9vI-uw{TIM#K2!coeXrEJ6@R=6jkm$D+9=J|OW>|YF6sMbBI7E$@B7UK{96Rn zlC-Ohqa}@KiI~+#Q{D2m+s|xy>pN%P`$?U1Rpop^0Y^&n5#^wGup}MgHT+=Q^89h| zpw8K{0Gl0VQKOwmDCfpZ4!4Ve)l|Q=J9d9HP{W6EEv8d)g`#G}0sGI4VSg8<-<%#X zi9z}zdST__qlV8kw%Vc9JYA3Nx5vA+-~?i&-QqA)_Kn>bkHDrC!gRt!V&^=5Vk;nKmgw(Z+cYsY$j_(zzEE#au z^D}qL>Egd#3F()=eD}V<%W;*M3}QauIAcm7nN~y1Quq680)o@c4g)kW4lk7ROD$-1LMU>RY3swWBimuyeYn5<#z|W!v}p^h z?jERGfBa@VAD%Hc-hLR=!p8nBxjAB%_4x=i`K**RR{8QmP?2=>)Yv) z$qva<__%%9xj5Q>e#YZY=c0p$Y%?w(LCX-7$ou2spm<^)7dG2M)Km{QBY2O(D2A*s zC_Rz)Oz`ATLx1A-Fkz0TlOgF^W^)sgpyqA;!YrvqolQ@1C^)jd-~neWYSqD_dDk7Ti_2j-idg9PZcaj$iKEby<*XK%bcP5ibb*D&AS#S0d?vOh9+b#O>jww6kEm zIDXKW;F!U5->bm5L|uyUYQa;?A0sljV3Y1dPB&pJ{v>;w@Z)R0S9r5I$(;+;EYK&aF)Mk68i@>xAmLScc6FAkg(gFGTPV(BqW!-ucVEb z5Py+WQI@k^uYu05HR%vW@5w~YIu0m(eoJN%j$tdY{mGq?OMtf)fRL+`v*U~FD{=)R zo9pS}o--!4KI7P#pfOBwBxhrEkZX))85wf)1|2A7qoLB0W`d1hgLBoxjtd|)?#{=l z0d?r8hrjNgv!^}HvDOHjIl1d(bjQZ2M}MwzE#08%_e9$kzNy%GkdBp3K`P(&)G_|rS2C)f|DCwP}6T+!1q0jb+}RIIyB@iuADFR>m(hLSug~G%BA^ze zU1c0CXhciIv_6`py=ud+ui5gh=$xBBsdHXdIbTq~{jgShLOCoREJ;YbnjefBo<9m6 z)j3-hV6(?8YP8oA%DFL~3E|FRWaARPdR`Ry(qqr*CcRcnHRBq_5u^H7k`(AXeHfjxx0o+8KFOXbf|Koom!n zBiPu9E7(0qDv^_BusdX!qWCg!nQF%wL}l-X?qSl1JjS09q^@PW2W*0Je6L7lNuRr( zpSfF37ys=_NVojuyY~fNu2{rm5c59A8B+?$v>IrZ()C?kTYkRGb(pc(%x-IOxpb}T zW?_$E`0@8gqf9rA)_!>qWF^6H;HI&io5e{!m~`Y5i_kVHw?`WKNI{YP0A F001Mg_v-)v diff --git a/logs/2026-09-25-3.log.gz b/logs/2026-09-25-3.log.gz index 048ec35ee176f528e2f3c8a5d57486aa19758ffe..cb7d06aa8e6fa292885e16843110fd96f91ec421 100644 GIT binary patch literal 1511 zcmVxU>z?rlmSMSsNVA@uZke2rEP)T(SMAH=?Lx=>pyEw+JZZNt+o)uVaRD(}hM+{= z8|R9m#yrYxwuPwaJ=}`mT?(TJ(%hi*R1TTo@sWnuongY9Ocw*vvCL*BAhn9O^&7LK z9Cbd`;z)30eL)3hENa!^!nI~AJEh0Ivmp!WjX%ggw-brGHCmyyDz50@FS){>TypzYx#W8NEd(#xekvf+ zz$`3E-C1*{Fo$0&zm)|05Y#lv9s3X;%LR8-rXy%!FbDh9s^b^uc3l?48_*|ao#WMN zei`pP>M0SoLB^mvMdEg`DD5nHo~z%e)C(LlnC^Qe7?-FkFG)ST}JT2Cf zhSULTx$G14vJOa+2SymNWaWWREV{?QAt+8WjbU;2?d#_=SLt1sAyrzX9_eOW#AJUZ zc`H3DZ|?l>8pv2oWU*O`{?q3ZPt z;fDN9lFaaE%VjAQt^%A20evQ1C4{-k_%JMCWJ+?0ABTCUVvav9B3;3LLN&%>5_3h* ziVURg+;P7DQnxT3x|pH!y~b~r_O7*j*SzL=UVZakSXhPTRy&b>T&6BUCn6T;UgIt3 zI#0C)aOHL2Nib8Ereg8Ne*M>gYnA77C*ysJzBj0RQC&~~Pf`I_8d9cNS3psL&N#-c z#O;D5-I%>AjP=f2(5lqQd01yyr}4c&Mw!-5sUzitStqBJx%_{3Bl%(ELY-PYZgCAe zPxcgjY~RaU>BkNElP7L2*O^=euQmM_z-czU{Du0l^4|5@tI${t`m2pnt-l2BhU9{N zXvQ*HMfZKbx`2O+fLfAvrE#=m5-kwZ25DB$y!%gv)t8OV2ft`^Ue!5Y(7=)6KcXDv zZ!B>@yoz5N)jfYyo9lD7XuxKVS-jC+S19MkRQ7lC2kWVRy)klsB~ZirawDd@>Il=8i3h| z>5ROF*@bvx->r+bK_+k-4!ddurR>Qt*cmWPQG7LUnQF%=gvIQK?qSl1EW)25ByMCJ z0uDhLzOYDTNsqgspSdch^M7~6q*MIz-3ta5S7K6#c#q?XDTJh2^)*ZA^Zij-t!U~d z%t(|g`64ctZgkx^G~HhbH&NXXdOdt6dk%TYYs0L$kfQX@y>wLnPfo3_SH*Q25-SN? N{{j#N{aacV002iC^m_mR literal 1513 zcmVF(}8)|-@eBQP-sagZAZHgMG)KHx%-^-#@lZPe!KSJmfRdN%LaT5ntWEu z8mn2lcXEDwaoZ+dt3@7`Rcd*{e=y6HG{h=(M?9}i5-tQc5Gfg(t(yYu)exL8E@z}8 zdC#?y2}s)}EVH98%Gi(FBWqLB-rI9e4Cs5%G*N#uIi!(Fw%-#LMJkilJldV|=O&_kpx@SCsRah<|(rjR7Tc#%@OW@=7W%uH6`>Bmb-R?yfjXY*tK#ZOtD3K2) zxuIw=k8+=FA!??FTM@iZVH`o4JCvTtdnR~%pdof=m@r4P#Wv|#W;4?!LCxFxg;`RK zI-6N>AULwVpn-E1wW_nE)_i5B^4Q<(p0yjz-^f2VQ;E9)t6dn(fr9Ah%K_v>}XFV5|{Du_3rPu;q}3j)82 zcNPtlh`S(T(7mE?`*K zNHEH}O_Dq@!iXiM2R^gt4ikrwu!9m=nE|~l>IAf9xo#*eF#QA|O9sQ>C9q8RN zBg4R`;`)kQ!PsVcX5CZ9)Y)eoM-%jh$xU)j#t6B|XkLlX@8s7_KlxgjZI#5oSb#jg~SO4#>CGSKo)v48!W3FN6 z$)2K*?R$AE{j?!}`ozuUI+JDapyj^+PP3Wv=jwZ@-i_c@XuJ)Eqp`<#0EDu%7BSnq&7@0X2LmH)1*^*D1;* zrcszO9K-aR)6Iwjj<+wN=U&b~D)>y}tQ}g-lehMDGy-Ec(${Z|o0ZDO5Gn08hpE~K z?VP+u-9E${$8KG=jWU7LaM)L4sANxv!QO~riQ=n)t5iElAuMJ;3=fk=WD))hA#o$) zJ>U?O;R}mYmJGNX`kA}sbpG$InDmNYzI(yI%axcEB0k`_VhSOtRzuAa`utGW$_-83 zgc*ryBVWYj(v7a0r9GyR`>Wt4svp8&fbV3_A+LCCm^T+vT>T3#9rgcHQ0wbWah-<5 PO2XE^t|@n+S{48R8nXGN diff --git a/logs/2026-09-25-4.log.gz b/logs/2026-09-25-4.log.gz index 90305b76b884de9f547a55b914b7012856de2e2f..9b2b65b96fe7c73fc5c8ac515d5c211097ec53ac 100644 GIT binary patch literal 1511 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 diff --git a/logs/2026-09-25-5.log.gz b/logs/2026-09-25-5.log.gz index cb7d06aa8e6fa292885e16843110fd96f91ec421..d8ad16be36cb60337d5b5df943b471a11efe7672 100644 GIT binary patch literal 1513 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 N{{j#N{aacV002iC^m_mR diff --git a/logs/2026-09-25-6.log.gz b/logs/2026-09-25-6.log.gz index 9b2b65b96fe7c73fc5c8ac515d5c211097ec53ac..ca7c9e1dd27d1dc311f7977e81e0b31b9ae1d673 100644 GIT binary patch delta 1446 zcmV;X1zGy%3+xM!9DmXarP|pM19XNFH?^SA3ZckJPFoL-9obH&ornGHdz`eTg*K(Y zX!n7tjqUGTd~V^^J81cpmS4Rix5vz~9v^`wpOmu3YL@PuUbHXoTEuHM$-}ZrEl>DQ zX1S7vSf%cX=O2)S3&9OUO2%gEb^@*;IAL5)Nhfm1wUP-)+YTZuvx6_n*bmzSYg5zO zk)}X@*;t?2c+%-ycF@Q+;{syz3_*#!H_i=3i+Pm$Yzt9SJ=}`mA%#%{Y3@*ZDu+z) z_(((S&M;w4ri(4owajLwMQRmq>o;aeIqH0B#gX91`ho_|Sk$V+1+``?JEh0IvmtBM z8-I|0ZYL6VYqUaZRb0`nk9Vuo8u$CLW`+xY`uFtP4LbKQ<=w)?vrS7abQ@F#gm1`xzJ_I$5@{4_lkL7~9 zD$^0PF_>HX)vDtc=XPBd#2e5jZk^-RYJM5-JnAVCcRo(s+zHVwu5Q4#%xm6;Pi30fuEnp(<1{TyXl@>z zYX$egE*Dm7{^w=DKPXXwQc4;mQ5v2W>q$fEg0)=s3Er}9kt7d{AY#eN1D{xQkAXu_ zoMsxs;_Tbk&u6aEyDmejv`Re^W?aO7WPc^(wtIby-NJp#kK^+7z}@eeikP^uT-=cx zX|gm`+G6;9y*Qmoh`~aH58o3z;D#h@Of;BGSi&V?1TV=9D7g~A4B(@l(#b@}7=|F6 zUk!b#*|)>a1?qEXEhMQe+LyD_SV##%h)^sJu=7KZY6yOC8LJ;bVn_(M3b>Dd>D-gt z&ftwlW_Q3r+@=ngycnD_$@x?BcLT+x-;r5}W86xde=_Ig0^mIhK*;s!`N`$YHMxe7 z&Ggi|r;LfS&pD1J=na#bRxSxUAzUI*5aO@@d8lHJKQ1DnU_YT6V=;-jqGv@0Qg`k+-+!rp+ZYcaX6Ssc z@tdW+Yc1b3uX&zV-@F$VUZJ_yPGldKsf*Bwhy}XWcniADQ*8lUc^!BX%#@|6Sk&0B z{~BkR8Oz8A*TaEm;di>BtMK?s8g%QZLVSG$)2K*?R$AE{kS22^2E*M zI+Ls5wWj|9IL)TYU#K4|^{&@mg~n>oU+t7?{UvZWBp38UGnUaRy6^kd1^inC)RMF- zjiV)#Xn~kENV9t8-G4HyzHD?p_(h}hs?Pa>2EK%Hl)tgWE#g&w{MxAQ`J>ugpR+{+ zHhavXMth-9?iy3s-_0Mar~38A$o-W-4e!g1m`=$xikblj96(csW0-z(ddMUO>5J&O zm-CMjKGQgB2UhcBGq&FxHA=xTL`u8OL8|NGJ&IO5ULR@bH)BE z*dfHF$jLC+9WYFPQG7LUnQF%=1jX!!*TbX{S%g0WNZiOc1RR1gd|{Ewk{)+MKXX-1 z=l|}CNw@gryB7>BuEeAe@gBz&QwT}5>T8zJ_4ZBgqq5q*)J>R?*vxOw;&SOm*NwO? zG~HhbH&I~#y&k@kJ%_yHwPDs=NKyLdUON8Qm%79h*J(%%tR!sx3mn*Jy;>Fk0Kx{` AhX4Qo delta 1441 zcmV;S1z!5>3+D@v9DjPFL_0fTfX*=DrWQ0>Arv{uY3sqUBire;^RT~tkCV2v(54g^ z?LJVovHhLna}T%Pf$z2aO7o7~9y7~&d<2?&Qpy^uS-N+6(Z0O%iPvh8hh>#op75W{ zawQG1O5GE0|9~W12yP%!GB#VcbKrz=IVGLQA=gSKAgxbWW(x;jl(8SS2iB&>-;t?6 zf8B9^ddHJa=dy#BY%?w(M#~VC$a~}bqIhE-n3L(EPr8=b%=n~U@wR?rmXxE;r?)r~99duRf-@Gi>Tp4=*~(7ovF~ii{6_N+ z^3Ux=;%=Q*XuXCj+W&aBO099fA8Tf~f1rO)zulm54^!SPOiX)VqrsI{DQL^V?5x_C zYvBD775?Os+rP>sHyUptc+vJ#0g(n~VNvSNnlptts`YZMB-n?brcv(Lhxk}7xT`W9 zK^ueF->=mizc{z+vLN1oJ~8VYuU@Z~@y?^35^)D)47yt+Ziq!`XUX&Y`prtCf50(= z>AqKjaf!MT<5ilEF-I&jn9JeVe?_i6D{oiZuOL!%QG6=X%yun)O&+IdafR0A(YaP| zAMA2r_11r02C4@oDo{#ElO#&R(_%eoNL{d&%RWIbYo8=}U<46MRv!4oqI(P+g5osO z7#3&WzJ5M)mELt3QlmBMkuc*TeA6!DXy|1c@Ob;40ugf2MOwayx^! z9$mWw4&pX-z~sf?oJrPqmLD>S^8;Hr`c3J3(7WeI*lkW3Z5#w*l1biE(gsY3K&l|k z*sj+==U19^0HgP0B4?#0a}Fqcen(~zj$tct{K=e=3&6p%0EAqho}XOaT$5`U*-TF_ z_mnZQ^*P7M1dU)mZe}mj$G|Nbzqc><@F&hk&mNXS?{2H9=9(J4qsc|(Qm-^J9 zgAjk+KV?s6niH)NICFB>$>@%aQ;%F{TDpg-*DHh@@;gZ~!=o*iq*S;9a4H1!nQ(;= z<_hD(tb~Cn$t8Xq=8=ji{#^$ZCunNtsb|U+@OkIRdL@dy~=3CHpo@oo<%Im zyxyw508X>1@)zpIO1&HPSD~>Q^j90D)_4ir&Bz7)(2QlYie9Z&YYX_d2&g4#R~ko4 z2GIgB?T=>l$h-e!R(;vzeDI4V=T)8a1q~c22ahO6`3puM3SqCU9~MLN$Voow$Y_ zLR^ZR41?VPf5Q~THv^ZccAP>`%zo$|CXL7<{24&vM#drF5R~Ejid2^LxSRQzt8zO3 zZ&ysZ#jmP+zrf;3ObQY2aa=KlkW{O_W(j@1KPs#5m%0fv5}VoWSzIpN=(=%ey1x=` vqQU@rJ$xm54tdFI!>qZGqV&(bbo{TKy5uUZ(~ww63)uP>S$cS=Srz~Q8r{(& diff --git a/logs/2026-09-25-7.log.gz b/logs/2026-09-25-7.log.gz deleted file mode 100644 index d8ad16be36cb60337d5b5df943b471a11efe7672..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1513 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 OoqqvBfKPc@761VGSmSX3 literal 0 HcmV?d00001 diff --git a/logs/2026-09-26-2.log.gz b/logs/2026-09-26-2.log.gz new file mode 100644 index 0000000000000000000000000000000000000000..679a779cb30fe9a28e74e03320bb639e7d7a57c6 GIT binary patch literal 1513 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:442) ~[client-named.jar:?] - at McfoldRenderServer.boot(McfoldRenderServer.java:605) ~[classes/:?] - at McfoldRenderServer.main(McfoldRenderServer.java:106) ~[classes/:?] + at McfoldRenderServer.boot(McfoldRenderServer.java:788) ~[classes/:?] + at McfoldRenderServer.main(McfoldRenderServer.java:113) ~[classes/:?] Caused by: com.mojang.authlib.exceptions.MinecraftClientHttpException: Status: 401 at com.mojang.authlib.minecraft.client.MinecraftClient.readInputStream(MinecraftClient.java:85) ~[authlib-4.0.43.jar:?] at com.mojang.authlib.minecraft.client.MinecraftClient.get(MinecraftClient.java:48) ~[authlib-4.0.43.jar:?] at com.mojang.authlib.yggdrasil.YggdrasilUserApiService.fetchProperties(YggdrasilUserApiService.java:129) ~[authlib-4.0.43.jar:?] ... 6 more -[19:20:25] [main/INFO]: Setting user: mcfold_cam -[19:20:25] [main/INFO]: Backend library: LWJGL version 3.3.1 build 7 -[19:20:25] [main/INFO]: [STDERR]: [LWJGL] [ThreadLocalUtil] Unsupported JNI version detected, this may result in a crash. Please inform LWJGL developers. -[19:20:25] [main/WARN]: Attempted to end step for load_time_pre_window_ms before starting it -[19:20:26] [main/INFO]: Reloading ResourceManager: vanilla -[19:20:26] [Worker-Main-10/INFO]: Found unifont_all_no_pua-15.0.06.hex, loading -[19:20:26] [main/INFO]: [STDERR]: [mcfold] Minecraft constructed -[19:21:26] [main/INFO]: [STDERR]: [mcfold] TIMEOUT TextureManager -[19:21:26] [main/WARN]: Missing sound for event: minecraft:item.goat_horn.play -[19:21:26] [main/WARN]: Missing sound for event: minecraft:entity.goat.screaming.horn_break -[19:21:26] [main/ERROR]: Error starting SoundSystem. Turning off sounds & music +[00:23:05] [main/INFO]: Setting user: mcfold_cam +[00:23:05] [main/INFO]: Backend library: LWJGL version 3.3.1 build 7 +[00:23:05] [main/INFO]: [STDERR]: [LWJGL] [ThreadLocalUtil] Unsupported JNI version detected, this may result in a crash. Please inform LWJGL developers. +[00:23:05] [main/WARN]: Attempted to end step for load_time_pre_window_ms before starting it +[00:23:06] [main/INFO]: Reloading ResourceManager: vanilla +[00:23:06] [Worker-Main-3/INFO]: Found unifont_all_no_pua-15.0.06.hex, loading +[00:23:06] [main/INFO]: [STDERR]: [mcfold] Minecraft constructed +[00:24:06] [main/INFO]: [STDERR]: [mcfold] TIMEOUT TextureManager +[00:24:06] [main/WARN]: Missing sound for event: minecraft:item.goat_horn.play +[00:24:06] [main/WARN]: Missing sound for event: minecraft:entity.goat.screaming.horn_break +[00:24:06] [main/ERROR]: Error starting SoundSystem. Turning off sounds & music java.lang.IllegalStateException: Failed to open OpenAL device at com.mojang.blaze3d.audio.Library.openDeviceOrFallback(SourceFile:283) ~[client-named.jar:?] at com.mojang.blaze3d.audio.Library.init(SourceFile:153) ~[client-named.jar:?] @@ -40,23 +40,23 @@ java.lang.IllegalStateException: Failed to open OpenAL device at java.util.concurrent.CompletableFuture.uniAcceptStage(CompletableFuture.java:735) ~[?:?] at java.util.concurrent.CompletableFuture.thenAcceptAsync(CompletableFuture.java:2223) ~[?:?] at net.minecraft.server.packs.resources.SimplePreparableReloadListener.reload(SourceFile:13) ~[client-named.jar:?] - at McfoldRenderServer.reloadResources(McfoldRenderServer.java:676) ~[classes/:?] - at McfoldRenderServer.boot(McfoldRenderServer.java:609) ~[classes/:?] - at McfoldRenderServer.main(McfoldRenderServer.java:106) ~[classes/:?] -[19:21:26] [main/INFO]: Found unifont_all_no_pua-15.0.06.hex, loading -[19:21:26] [main/INFO]: Created: 1024x512x4 minecraft:textures/atlas/blocks.png-atlas -[19:21:26] [main/INFO]: Created: 256x256x4 minecraft:textures/atlas/signs.png-atlas -[19:21:26] [main/INFO]: Created: 512x512x4 minecraft:textures/atlas/shield_patterns.png-atlas -[19:21:26] [main/INFO]: Created: 512x512x4 minecraft:textures/atlas/banner_patterns.png-atlas -[19:21:26] [main/INFO]: Created: 1024x1024x4 minecraft:textures/atlas/armor_trims.png-atlas -[19:21:26] [main/INFO]: Created: 128x64x4 minecraft:textures/atlas/decorated_pot.png-atlas -[19:21:26] [main/INFO]: Created: 256x256x4 minecraft:textures/atlas/chest.png-atlas -[19:21:26] [main/INFO]: Created: 512x256x4 minecraft:textures/atlas/shulker_boxes.png-atlas -[19:21:27] [main/INFO]: Created: 512x256x4 minecraft:textures/atlas/beds.png-atlas -[19:21:27] [main/WARN]: Shader rendertype_entity_translucent_emissive could not find sampler named Sampler2 in the specified shader program. -[19:21:27] [main/INFO]: Created: 256x256x0 minecraft:textures/atlas/particles.png-atlas -[19:21:27] [main/INFO]: Created: 256x256x0 minecraft:textures/atlas/paintings.png-atlas -[19:21:27] [main/INFO]: Created: 128x128x0 minecraft:textures/atlas/mob_effects.png-atlas -[19:21:27] [main/INFO]: [STDERR]: [mcfold] resources reloaded -[19:21:27] [main/WARN]: Shader rendertype_entity_translucent_emissive could not find sampler named Sampler2 in the specified shader program. -[19:21:27] [main/INFO]: [STDERR]: [mcfold] boot done + at McfoldRenderServer.reloadResources(McfoldRenderServer.java:1146) ~[classes/:?] + at McfoldRenderServer.boot(McfoldRenderServer.java:792) ~[classes/:?] + at McfoldRenderServer.main(McfoldRenderServer.java:113) ~[classes/:?] +[00:24:06] [main/INFO]: Found unifont_all_no_pua-15.0.06.hex, loading +[00:24:06] [main/INFO]: Created: 1024x512x4 minecraft:textures/atlas/blocks.png-atlas +[00:24:06] [main/INFO]: Created: 256x256x4 minecraft:textures/atlas/signs.png-atlas +[00:24:06] [main/INFO]: Created: 512x512x4 minecraft:textures/atlas/shield_patterns.png-atlas +[00:24:06] [main/INFO]: Created: 512x512x4 minecraft:textures/atlas/banner_patterns.png-atlas +[00:24:06] [main/INFO]: Created: 1024x1024x4 minecraft:textures/atlas/armor_trims.png-atlas +[00:24:06] [main/INFO]: Created: 128x64x4 minecraft:textures/atlas/decorated_pot.png-atlas +[00:24:06] [main/INFO]: Created: 256x256x4 minecraft:textures/atlas/chest.png-atlas +[00:24:06] [main/INFO]: Created: 512x256x4 minecraft:textures/atlas/beds.png-atlas +[00:24:06] [main/INFO]: Created: 512x256x4 minecraft:textures/atlas/shulker_boxes.png-atlas +[00:24:07] [main/WARN]: Shader rendertype_entity_translucent_emissive could not find sampler named Sampler2 in the specified shader program. +[00:24:07] [main/INFO]: Created: 256x256x0 minecraft:textures/atlas/particles.png-atlas +[00:24:07] [main/INFO]: Created: 256x256x0 minecraft:textures/atlas/paintings.png-atlas +[00:24:07] [main/INFO]: Created: 128x128x0 minecraft:textures/atlas/mob_effects.png-atlas +[00:24:07] [main/INFO]: [STDERR]: [mcfold] resources reloaded +[00:24:07] [main/WARN]: Shader rendertype_entity_translucent_emissive could not find sampler named Sampler2 in the specified shader program. +[00:24:07] [main/INFO]: [STDERR]: [mcfold] boot done diff --git a/mcfold/__pycache__/render.cpython-312.pyc b/mcfold/__pycache__/render.cpython-312.pyc index ea09eaf465ed6984b89b6431218e8710ec519cbd..263f3a61a1c6af398110c97c962192aa42c94a2e 100644 GIT binary patch delta 5780 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 OZ;DXCj|_!;-}?VB(9g60 delta 2658 zcmaJ>eN0=|6~Fh{e*U(xvB5w98yvua^0kdBiMuvSHieO}lo4Pi0Xf_UfjBn0@6iPB zQ4)z&sQXaYtE^Skt?3eVA4E$hrxn$trP{O)E$uYg{=wX>qE)+el~$3Y`zL=?JLlPP zNjB|S{@rta=iGD7JNKUV-c@?;0`**TyB!3c%fm0vT#EnF^G3x^JL-hcTVJJwJVg|e zpo}CWMiVBsjny*Kk|kkQEFeqpGc${^%FoIoQdzOc>X>!Ox>ATi5;Ci2wk31Is@PbV z*_X^Ei``%xTNsDIIJYoP7GW;f?P3kg4Xc|qG7qdC(7mvFS(KH*T4t0WF&|s>KWny= ztv|M%l`qK&AJp5#0#Li(NE6(Wrkpji3J4PbpB7dLK0y{^Az&)(gzfk&8f#@$V68-k zwQb?=W7WWiSi6gLuo@7n=A~F?Za8$6N-{Eo%({#ZG zfBS+%9IWj)Y+`f;?D#A=btfOyiW(uha|O<&V9_iE30Y>&IteM+>~|PuL4LudyF{5B z5OrO)0N6T%7UR#r3-KKDX}X7TwKkRS-W-8C`H}O~u_>u>?p= znF&fNmIuD}QVBL);$vd1IuKv*Y}xL)x6&=eIG~uKy-@evg)RP%!gT1H>Z9bY3{HG! z=DMpRHqBk?maJPZxmSuOvf!17?$s^2Nw?{C-EqmYQtazO8CZy3rqhD23mWOu%{;7^ z&3HwZqgtj!_s#fFw4wNQ67z~o_&u=}d6~W-mK(k&FGEuPxVX_56v>7rdP$5lh8MpL zWZpiVR<&e$LQ(Tprc5U1Qd&&nXVCvFK+M8*lph7i+oqBym2o!B`L|GYCRJ6@#;4Ov zIUTd-ty7AY)HKfVZt+(1kf&9{eJ?8JsZ{0|hn5h&h`Klx^;5riJR0*nfh5M{-v%&i)M943O52zu4NBZ2(X7Jl!XL+qCiE zdS(3^mAw}~cmC9iHEYfB)%msN{ckq*F7N-uMJl5|&AvMGR>i~1&cFJ$t^0!KtJZ_H z=O!tL9aJ9ziA9B5| z6qUZVUG(+bV{PwJ`eyEU`!^~5v3S3uon98@ol)s+lH1v7M!zq1?U1e$G1Jux%9XBe z=_4XO?D__MMSQ9I0qI{vob3+fKG^v&rO%1Cc8BRp;@`XL>GL8O?*@5){D5?mNTmp!mYU$-!^tebIzG%@jnC?dlT&{;7) ze3{k=???}w6JHrQK+A*}i7eKki!l%8W0_2lwmg)6)nMm7xgtcOicXrrpe+roR&^GA!c(In{AmobhJ8=VbyvFMUF_h-_|9sdZJ z!T)#Y-+^#%@aeR6H0I*h!IpJEy09KTl+=!MynWdUZEiNDjDk;F7#+cg!Htu+?EvNu z0XNiNcroLi7E2RNiAW6Z&uu5Z%^hc>2aOmwSA`6rQnde{LF)f!95%&dUWa>)YfyuGjr(p?;zfHk?}9JlcSjqQAEMtm zfV^#%XJ!?y%@+p}4KE^GMtB9`R|vmGz$xY*Bj7B;i~(3N(FRIxlqwLa5NbvJkqA90 z_8bX}v{?MB+WPj`YGA#pel@z@wBv>qxb;xOrU}>X9%J1ouh_JJzH#6E#=0q!>gXnV zg4FroO_Xk#i0s>xP#UCS?Z{!#dGvi67QwNw9L2n0v3o2AgfiA*&Py3pJURBL^93yM b=Lo+LHRC Gen | None: def backend_class(client_jar: str | Path | None = None, - generation: str | None = None): + generation: str | None = None, + launcher: str = ""): """选中后端类。 - ★ 这是「Python 层 switch」的落点:加新世代只需在下面加一条 elif。 + ★ 这是「Python 层 switch」的落点:加新世代只需在下面加一条分支。 + + 两个轴是**正交**的(用户 2026-09-25 定向): + 世代(版本差异) × 启动架构(加载器差异) + - 世代决定 McfoldRenderServer 怎么调 MC API(见 VERSION-DIFFS.md) + - 启动架构决定「谁起游戏」:裸启动 = 我们自己的 main; + launcher="fabric" = 经 Fabric 加载器(能加载 mod / Iris,但不写我们的 mod) + 正交 ⇒ 用动态子类组合,不写 N 个类。 """ if generation is None: generation = backend_for(client_jar) if client_jar else "gen_1_20" - if generation == "gen_1_21": - return Gen_1_21 - return Gen_1_20 + base = Gen_1_21 if generation == "gen_1_21" else Gen_1_20 + if not launcher: + return base + # 动态子类:只覆盖 launcher,其余沿用世代实现 + name = f"{base.__name__}_{launcher}" + existing = globals().get(name) + if existing is not None: + return existing + cls = type(name, (base,), {"launcher": launcher}) + globals()[name] = cls + return cls def backend_for(client_jar: str | Path) -> str: @@ -107,6 +124,17 @@ def backend_for(client_jar: str | Path) -> str: return g.backend +LAUNCHERS = [ + ("", "裸启动", "我们自己的 main;不加载 mod(零加载器方案可注册占位方块)"), + ("fabric", "Fabric 加载器", "经 launcher_fabric.LoaderBoot;能加载 fabric mod / Iris,不写我们的 mod"), +] + + +def list_launchers() -> list[tuple[str, str, str]]: + """(标识, 名称, 说明) —— 启动架构与世代正交。""" + return list(LAUNCHERS) + + def list_generations() -> list[tuple[str, str, str]]: """(世代名, 版本区间, Java 目录) —— 供诊断/文档用。""" return [ diff --git a/mcfold/backends/__pycache__/__init__.cpython-312.pyc b/mcfold/backends/__pycache__/__init__.cpython-312.pyc index ccb2b54e6cfc23c0300112aae358a4080dbf309e..99af561734ea2406bdaca3419142f22900e29a22 100644 GIT binary patch delta 2261 zcmZ`)T~Jd;6uviqKmrM1`KP!*M+pJ~RYp1vbj_kP2$qH^ae$3GMB?;KtBcQOsG?jDvh~gIqVKKtMfN$K23|+x%I>9itX?0 ztr7G#tJ7t&nceof0~R`VS96bL3;k!KjRiTd8ybxw0YtN^q^PvSsHQ2XNkx%{A{|A> z*q_mJY*se3jY>gLR4lMfg4%9wvN)YeP!ht^T>qkW9s8a?Pxo0F&NQk7ZJo_(vAax- zW-74F0^30IV8E%8W{|gNE;J~5MG_Tb-|N0%GfUA#1|_nE4lPjZ@$+$&8MyB{G*~Jc zXh5G`4EukJ+vK&YUATf&T9h7zQ{hq8Fdk(i+PzQ)84fH4ne)mPl`Cdxa3s=b#6@2y z9@Pjdt6J0^b*sXou3=npOGX}*r%YMTaw?C)%8o=f*`o1iUIzE5Hp48BriL-9-J2Bt zLj6In=xY!4jn4ANv|0isIB_XBaV0!`D>N{gpC7t*G5Fo3{QS9dZSzyUxxV3$Z=94C zmzEV4Z!9cbPsHoz#Gc;ysgC)nbJBDKI1`+_h}Ipx&{tQ*@$2H$F|Z)yr+z}jo>8%L zICSB5=mH{&o!+7F^t5<(c-c;ZDU85~?PIV6*;QBXuyK1WcFsa;EcCF2k`RA7c*hsM zah&X`AY1oT0a*BQEBOfcSGBvku(E2~wwj_Q4i=68{&!^=c9uUCzTs^Re>)zUmbvZC&ZnB$f zlX^t#j2sQOc6?gyk|qa7){-h}bv0JFwWp&tTIVx29%}$FHu|l-v2n4#--XJLJ zY)&Vl;^I)`Ac_SAMNtc%^B4%8S;%Cf6KA5*9u$}N?Hfw})tglCgVv`Fq+}1({*tn` zYs0Ni_{s;m6#>HvpYE|fvAe0G$!8kaI&fg1c1D-SS3Wc(pFG;#($O;T!F@vxzdaCT zn2E{=7*c$Ohg!YQ)x-6CJkUJ6VJ2(ceeHU_B9NTc_U`d2AJ@7&pwsg^s1(bQcw!j; z8UuYDy=7F$X#tZDwDIs%$_ol;EuBd69*WQXGltb(go5P6xG{{EiQ|4BdjCTmyf29t zNCY6pAA`4-c@jEvC-k*X>>dq`4U6rk#IB*>bQdVa^Zh_$SVtrC%pDK-NHVXr?itJ6 zdqIozTe@tMY+ba|VTQ1I+u?8lb)OMY9({q2FLSXDP?rlP7x>sk0*!vg>sofB3%w+j zk%$g*4C{UaRC%GH6XDVJ0*L*txyv^SNO%;I%V{Hd!uP@dxs(u`I4$Q7nfgFyqe4*O zb3!rS(QFjxb%|ivRF6#*U%=-?s_{%vXXuML`R0r&o#y!eF}T@8zdgQuqx9gY@xfWs z9M@<*Y+eh@HkFu4OJvmNp(*-U5}p3vczI_qp7MY?gg6 zyTE@wAvP_Iu7nwa)?})8yWG@bGEux8n(zNHq1=mvmIEA*Eyp`PyMkJRp~zQCP)JXq z^x8?$DiK_Awd7jK3(tE-h~5m;b=xd&P~hFRihRvO2^pBYA zM@;HdjZ#;{M=iwq+Y^s&4&=PHpiXBEfvmg*CDe~IS1w|Iv0Tk2EhIAPgKR)`WI?Tr TQ}8j1u?jZhFRhx@NSFRE=+xXx delta 907 zcmY+BO-xfk5P)}I|KFoOw0->p+9ev~nWia1)U<>cQ4g4a2^T?<+VBd3&?ei~L}Egt z95{mO&5I{v;)ygd9=)090b)Gh3vl7!$yh)P)9mh63*OMQle>u8A}qKFtG-b{JE(_{*C z!)7U&tVI_sN4dCIs#a}aWURrv#d&Rjj))2E{$;#r*e!K>VXjoJ73PcFqDvN?u}X>S&7az9+L^_WlSkP}goY7_F|pyj(ZN8QRsjDN2E(RDw?x2~4~>%= zW{_(f{#$=Up8Fpzv(m!c%zopk%3$#TxMkgjjeAjdA)Ek1;TaH?I$J82c(FEDDOWKY zT$2B7!$8CY?CM28gS6U+JGkYFf6L}x+ANo*3 zl*M~J=|+cP=NYl5_e>3-FNeq@2GOUCj-nsbrS@u#vnT9MvGxGOXazcO1?}pI9Sx_U zXC4X5M|fUTA}k)|U2wG2LSd#}tMgK!z;QmjSGx^(^b diff --git a/mcfold/backends/__pycache__/base.cpython-312.pyc b/mcfold/backends/__pycache__/base.cpython-312.pyc index 416311c8a04b8db5f69046f0237efc8330d5cff8..5bb6b307913ad485343e93790ef7dcd10d29c464 100644 GIT binary patch delta 5956 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 A?f?J) diff --git a/mcfold/backends/base.py b/mcfold/backends/base.py index 6030522..be84791 100644 --- a/mcfold/backends/base.py +++ b/mcfold/backends/base.py @@ -87,6 +87,9 @@ class RenderBackend: java_dir: str = "" markers: tuple[str, ...] = () main_class: str = "McfoldRenderServer" + # 启动架构(与世代正交):"" = 裸启动(我们自己的 main) + # "fabric" = 经 Fabric 加载器启动(用 mod 与 Iris,但不写我们的 mod) + launcher: str = "" def __init__( self, @@ -98,6 +101,7 @@ class RenderBackend: java_args: list[str] | None = None, startup_timeout: float = 240.0, cwd: str | None = None, + mods_dir: str | None = None, ): self.java = java if classpath is None: @@ -110,6 +114,8 @@ class RenderBackend: self.asset_index = asset_index self.startup_timeout = startup_timeout self.cwd = cwd + # 加载器后端的 mod 目录(launcher="fabric" 时经 -Dfabric.modsFolder 传入) + self.mods_dir = mods_dir self.java_args = java_args or [ "-Xmx1200M", "-Xms512M", "-Djava.awt.headless=true", ] @@ -134,7 +140,18 @@ class RenderBackend: f"未提供 classpath。设置 MCFOLD_RENDER_CLASSPATH," f"或先跑 scaffold/build.sh 编译 {self.generation} 后端。" ) - cmd = [self.java, *self.java_args, "-cp", self.classpath, self.main_class] + java_args = list(self.java_args) + # ★ 启动架构:走 Fabric 加载器时,主类是 launcher_fabric.LoaderBoot, + # 并需两个系统属性(实测缺一不可): + # fabric.gameMappingNamespace=named → 游戏类名保持 official 名 + # fabric.modsFolder= → mod 目录 + main_class = self.main_class + if self.launcher == "fabric": + main_class = "LoaderBoot" + java_args.append("-Dfabric.gameMappingNamespace=named") + if self.mods_dir: + java_args.append(f"-Dfabric.modsFolder={self.mods_dir}") + cmd = [self.java, *java_args, "-cp", self.classpath, main_class] if self.game_dir: cmd += ["--gameDir", self.game_dir] if self.assets: @@ -225,6 +242,21 @@ class RenderBackend: return self._request({"cmd": "setblocks", "cx": cx, "cz": cz, "sections": items}, timeout=300.0) + def load_mods(self, mods_dir: str) -> dict: + """加载 mod:Java 侧自己扫 jar、注册占位方块、挂资源包。 + + ★ Python 只传目录路径 —— 版本/格式相关的处理全在 Java 侧 + (用户 2026-09-25 定向:Python 相当于路由)。 + + 原理(见 scaffold/MOD-BLOCKS.md):方块**渲染**只需要 + (a) 注册表里有这个名字(拿 state id) + (b) 资源包里有 blockstates/models/textures + 与方块行为无关 ⇒ 不需要 Fabric/Forge 加载器。 + + 代价(诚实标注):方块**行为**(红石/交互)与第三方光影包(Iris)不支持。 + """ + return self._request({"cmd": "mods", "dir": str(mods_dir)}, timeout=600.0) + def set_viewport(self, cx: int, cz: int, radius: int | None = None) -> dict: """视野中心 —— 必须在注入前设好,否则区块被 ClientChunkCache 丢弃(实测)。""" req: dict[str, Any] = {"cmd": "viewport", "cx": cx, "cz": cz} @@ -275,6 +307,17 @@ class RenderBackend: return [] return self._request({"cmd": "resolve", "blocks": specs})["ids"] + def get_block_info(self, x: int, y: int, z: int) -> dict: + """查询某个坐标的方块信息。 + + ★ 由 Java 侧回答(它才有 level + 注册表 + 模型状态),Python 只路由。 + 返回:block 名(含 mod 方块)、stateId、属性(朝向等)、isAir/isSolid、 + 光照/硬度、贴图是否缺失(程序化判据)、是否来自 mod。 + 未加载的区块会明确回 known=false + reason="chunk_not_loaded" + —— 与「空气」语义区分(设计文档附录 D)。 + """ + return self._request({"cmd": "blockinfo", "x": int(x), "y": int(y), "z": int(z)}) + def debug(self) -> dict: return self._request({"cmd": "debug"}) diff --git a/mcfold/render.py b/mcfold/render.py index 1e8b852..81b3a5b 100644 --- a/mcfold/render.py +++ b/mcfold/render.py @@ -149,13 +149,38 @@ class WorldRenderer: def open(cls, instance: Any = None, *, store: str | None = None, classpath: str | None = None, start: bool = True, java: str = "java", client_jar: str | None = None, - generation: str | None = None) -> "WorldRenderer": + generation: str | None = None, launcher: str = "", + mods_dir: str | None = None) -> "WorldRenderer": """从 instance(mcfold.instance.Instance)或 store 目录打开渲染器。 classpath 缺省取环境变量 MCFOLD_RENDER_CLASSPATH; 可先跑 mcfold/scaffold/build.sh 生成。 """ game_dir = assets = asset_index = None + # ★ 版本隔离策略:产物在**内容根**的 .mcfold/ 下(可只读共享), + # 不是 /tmp(tmpfs,重启即丢)。见 scaffold/build.sh。 + content_root = None + if instance is not None: + cr = getattr(instance, "content_root", None) + if cr: + content_root = Path(cr) + elif store: + content_root = Path(store) if Path(store).name.startswith(("vanilla-", "forge-", + "fabric-", "neoforge-", "quilt-")) else None + if content_root is not None and (content_root / ".mcfold").is_dir(): + mf = content_root / ".mcfold" + if (mf / "classes").is_dir(): + # 内容根产物优先(除非调用方显式给了 classpath) + if classpath is None: + cp_file = mf / "classpath.txt" + parts = [] + if cp_file.is_file(): + parts = [ln.strip() for ln in cp_file.read_text().splitlines() + if ln.strip()] + parts += [str(mf / "client-named.jar"), str(mf / "classes")] + classpath = os.pathsep.join(parts) + if (mf / "natives").is_dir(): + os.environ.setdefault("MCFOLD_NATIVES_DIR", str(mf / "natives")) if instance is not None: root = getattr(instance, "content_root", None) or getattr(instance, "path", None) if root: @@ -171,16 +196,28 @@ class WorldRenderer: assets = str(store_p / "assets") names = sorted(p.stem for p in (store_p / "assets" / "indexes").glob("*.json")) asset_index = names[-1] if names else None - game_dir = game_dir or str(store_p / "instances" / "default") + # ★ 不默认写生产实例 default(版本隔离策略)。 + # 实例目录与内容根**同级**(都挂在 mcview/ 下), + # 绝不嵌套进内容根 —— 内容根要可只读共享(见 MULTI-INSTANCE-DESIGN §1.1)。 + # 缺省按内容根名派生一个独立实例,避免撞上任何真实实例。 + # 权威布局(mcview/lib/versions.cjs:25): + # BASE(可由 MCVIEW_BASE 覆盖)= .../mcview + # contentRoot = BASE/store/- + # instanceDir = BASE/instances/ + # 所以 store 参数若是内容根,则 BASE = 内容根的 parent.parent。 + base = Path(os.environ.get("MCVIEW_BASE") or store_p.parent.parent) + game_dir = game_dir or str(base / "instances" / f"exp-{store_p.name}") # ★ Python 层 switch:按 client jar 的世代选后端 # (1.21+ 的 DeltaTracker 在 1.20.x jar 中不存在 ⇒ 必须独立编译的后端) # ★ 变量名不能叫 cls —— 会覆盖 classmethod 的 cls, # 导致下面 cls(bridge, instance) 构造出后端而非 WorldRenderer(实测踩过) - backend_cls = backend_class(client_jar=client_jar, generation=generation) + backend_cls = backend_class(client_jar=client_jar, generation=generation, + launcher=launcher) bridge = backend_cls( java=java, classpath=classpath, game_dir=game_dir, assets=assets, asset_index=asset_index, + mods_dir=mods_dir, ) wr = cls(bridge, instance) if start: @@ -239,6 +276,40 @@ class WorldRenderer: # ---------------- 方块名 → state id ---------------- + def get_block_info(self, x: int, y: int, z: int) -> dict: + """查询坐标处的方块信息(Java 侧回答,Python 只路由)。 + + 含:方块名(mod 方块亦然)、stateId、属性、光照/硬度、 + 贴图缺失的程序化判据、是否来自 mod;未加载区块明确区分于空气。 + """ + return self.bridge.get_block_info(x, y, z) + + def load_mods(self, mods_dir: str | Path | None = None) -> dict: + """加载 mod(Java 侧全权处理:扫 jar / 注册方块 / 挂资源包)。 + + mods_dir 缺省取实例的 mods/ 目录。 + + ★ 为什么不需要 Fabric/Forge 加载器(见 scaffold/MOD-BLOCKS.md): + 方块**渲染**只需要「注册表里有名字」+「资源包有模型/贴图」, + 与方块行为无关。所以零加载器也能让 mod 方块正确显示。 + 代价(诚实标注):方块行为(红石/交互)与第三方光影包(Iris)不支持。 + """ + if mods_dir is None: + inst_path = getattr(self.instance, "path", None) if self.instance else None + if inst_path: + mods_dir = Path(inst_path) / "mods" + if mods_dir is None: + return {"ok": False, "error": "未提供 mods_dir 且无法从实例推断"} + d = Path(mods_dir) + if not d.is_dir(): + return {"ok": True, "mods": 0, "note": f"{d} 不存在"} + r = self.bridge.load_mods(str(d)) + # 加载 mod 后方块变了 ⇒ 规格缓存作废(否则仍按旧表解析) + self._spec_cache.clear() + self._biome_id = None + self.refresh_capacity() + return r + def resolve_specs(self, specs: list[str]) -> dict[str, int]: """批量把方块名规格解析成 state id(渲染器侧权威注册表)。""" todo = [s for s in dict.fromkeys(specs) if s not in self._spec_cache] diff --git a/scaffold/RegistryProbe.java b/scaffold/RegistryProbe.java deleted file mode 100644 index 08badad..0000000 --- a/scaffold/RegistryProbe.java +++ /dev/null @@ -1,82 +0,0 @@ -import com.mojang.blaze3d.platform.DisplayData; -import com.mojang.blaze3d.systems.RenderSystem; -import com.mojang.authlib.properties.PropertyMap; -import net.minecraft.SharedConstants; -import net.minecraft.client.Minecraft; -import net.minecraft.client.User; -import net.minecraft.client.main.GameConfig; -import net.minecraft.core.registries.BuiltInRegistries; -import net.minecraft.resources.ResourceLocation; -import net.minecraft.server.Bootstrap; -import net.minecraft.world.level.block.Block; -import net.minecraft.world.level.block.state.BlockState; - -import java.io.File; -import java.net.Proxy; -import java.util.Optional; -import java.util.OptionalInt; - -/** - * 证明:真客户端的注册表能给出 state id ↔ 方块名的权威映射。 - * - * 这正是不起完整客户端、只用客户端注册表的价值所在: - * 1.20.1 不从网络下发方块表(附录D),mod 方块的 state id 只有客户端注册表知道。 - */ -public class RegistryProbe { - - public static void main(String[] args) throws Exception { - String gameDir = arg(args, "--gameDir", "/home/newqqagent/mcview/instances/default"); - String assets = arg(args, "--assets", "/home/newqqagent/mcview/store/vanilla-1.20.1/assets"); - String assetIdx = arg(args, "--assetIndex", "1.20.1"); - - SharedConstants.tryDetectVersion(); - SharedConstants.enableDataFixerOptimizations(); - Bootstrap.bootStrap(); - - File runDir = new File(gameDir); - User user = new User("mcfold_cam", "0".repeat(32), "mcfold_cam", - Optional.empty(), Optional.empty(), User.Type.LEGACY); - PropertyMap props = new PropertyMap(); - GameConfig cfg = new GameConfig( - new GameConfig.UserData(user, props, props, Proxy.NO_PROXY), - new DisplayData(854, 480, OptionalInt.empty(), OptionalInt.empty(), false), - new GameConfig.FolderData(runDir, new File(runDir, "resourcepacks"), new File(assets), assetIdx), - new GameConfig.GameData(false, "mcfold", "release", false, false), - new GameConfig.QuickPlayData(null, null, null, null)); - - RenderSystem.initRenderThread(); - RenderSystem.beginInitialization(); - Minecraft mc = new Minecraft(cfg); - RenderSystem.finishInitialization(); - System.out.println("[reg] Minecraft constructed"); - - // ★ 核心:客户端的方块注册表 - int blockCount = BuiltInRegistries.BLOCK.size(); - int stateCount = Block.BLOCK_STATE_REGISTRY.size(); - System.out.println("[reg] BLOCK registry size = " + blockCount); - System.out.println("[reg] BLOCK_STATE registry size = " + stateCount); - - // ★ 关键验证:mineflayer 会静默误映射的那些越界 id,客户端怎么答 - int[] probes = {0, 1, 1002, 1003, 1502, 30000, stateCount - 1}; - for (int id : probes) { - BlockState st = Block.stateById(id); - ResourceLocation key = BuiltInRegistries.BLOCK.getKey(st.getBlock()); - System.out.println("[reg] state_id=" + id + " -> " + key + " [" + st + "]"); - } - - // ★ 列出几个方块名(证明可读) - System.out.println("[reg] sample blocks:"); - for (int i = 0; i < 5; i++) { - ResourceLocation k = BuiltInRegistries.BLOCK.getKey(BuiltInRegistries.BLOCK.byId(i)); - System.out.println(" id=" + i + " -> " + k); - } - - System.out.println("[reg] DONE"); - System.exit(0); - } - - private static String arg(String[] a, String key, String def) { - for (int i = 0; i < a.length - 1; i++) if (a[i].equals(key)) return a[i + 1]; - return def; - } -} diff --git a/scaffold/WorldRender.java b/scaffold/WorldRender.java deleted file mode 100644 index e56d319..0000000 --- a/scaffold/WorldRender.java +++ /dev/null @@ -1,344 +0,0 @@ -import com.mojang.blaze3d.pipeline.MainTarget; -import com.mojang.blaze3d.platform.DisplayData; -import com.mojang.blaze3d.systems.RenderSystem; -import com.mojang.authlib.GameProfile; -import com.mojang.authlib.properties.PropertyMap; -import net.minecraft.SharedConstants; -import net.minecraft.client.Minecraft; -import net.minecraft.client.User; -import net.minecraft.client.main.GameConfig; -import net.minecraft.client.multiplayer.ClientLevel; -import net.minecraft.client.multiplayer.ClientPacketListener; -import net.minecraft.client.telemetry.TelemetryEventSender; -import net.minecraft.client.telemetry.WorldSessionTelemetryManager; -import net.minecraft.core.BlockPos; -import net.minecraft.core.Holder; -import net.minecraft.core.RegistryAccess; -import net.minecraft.core.registries.BuiltInRegistries; -import net.minecraft.core.registries.Registries; -import net.minecraft.network.Connection; -import net.minecraft.network.protocol.PacketFlow; -import net.minecraft.resources.RegistryDataLoader; -import net.minecraft.resources.ResourceKey; -import net.minecraft.resources.ResourceLocation; -import net.minecraft.server.Bootstrap; -import net.minecraft.server.packs.resources.ResourceManager; -import net.minecraft.util.profiling.InactiveProfiler; -import net.minecraft.world.Difficulty; -import net.minecraft.world.level.ChunkPos; -import net.minecraft.world.level.Level; -import net.minecraft.world.level.block.Block; -import net.minecraft.world.level.block.Blocks; -import net.minecraft.world.level.block.state.BlockState; -import net.minecraft.world.level.chunk.LevelChunk; -import net.minecraft.world.level.dimension.DimensionType; -import org.lwjgl.opengl.GL11; - -import javax.imageio.ImageIO; -import java.awt.image.BufferedImage; -import java.io.File; -import java.net.Proxy; -import java.time.Duration; -import java.util.Optional; -import java.util.OptionalInt; -import java.util.UUID; - -/** - * 世界渲染器 —— 把世界数据喂给真客户端渲染器并出图。 - * - * 实测要点(2026-09-25): - * 1. 不跑主循环 ⇒ 渲染栈手动初始化(tryDetectVersion/Bootstrap/RenderSystem/new Minecraft) - * 2. 注册表从客户端自带 data/ 加载(RegistryDataLoader + vanilla pack) - * 3. shader 需显式编译(GameRenderer.reloadShaders 包私有 → 同包 bridge) - * 4. LoadingOverlay 必须摘掉,否则永远渲出加载屏 - * 5. 区块靠 LevelChunk + setBlockState 注入 + setSectionDirty 触发网格重建 - * 6. 读像素必须绑 FBO(glBindFramebuffer(frameBufferId)),GL_BACK 读到的是残留 - */ -public class WorldRender { - - public static void main(String[] args) throws Exception { - String gameDir = arg(args, "--gameDir", "/home/newqqagent/mcview/instances/default"); - String assets = arg(args, "--assets", "/home/newqqagent/mcview/store/vanilla-1.20.1/assets"); - String assetIdx = arg(args, "--assetIndex", "1.20.1"); - String out = arg(args, "--out", "/tmp/scaffold-world.png"); - int W = Integer.parseInt(arg(args, "--width", "854")); - int H = Integer.parseInt(arg(args, "--height", "480")); - - SharedConstants.tryDetectVersion(); - SharedConstants.enableDataFixerOptimizations(); - Bootstrap.bootStrap(); - - File runDir = new File(gameDir); - User user = new User("mcfold_cam", "0".repeat(32), "mcfold_cam", - Optional.empty(), Optional.empty(), User.Type.LEGACY); - PropertyMap props = new PropertyMap(); - GameConfig cfg = new GameConfig( - new GameConfig.UserData(user, props, props, Proxy.NO_PROXY), - new DisplayData(W, H, OptionalInt.empty(), OptionalInt.empty(), false), - new GameConfig.FolderData(runDir, new File(runDir, "resourcepacks"), new File(assets), assetIdx), - new GameConfig.GameData(false, "mcfold", "release", false, false), - new GameConfig.QuickPlayData(null, null, null, null)); - - RenderSystem.initRenderThread(); - RenderSystem.beginInitialization(); - Minecraft mc = new Minecraft(cfg); - RenderSystem.finishInitialization(); - System.out.println("[wr] Minecraft ok"); - - // ── 资源:字体/贴图/模型 ── - // createReload 的 future 不该在主线程 join(会死锁),但字体加载是同步的, - // 这里用官方 FontManager.reload 直接同步加载字体(修「文字成方框」) - // ★ 资源重载:modelManager/fontManager/textureManager 全是 reload listener, - // 不跑重载则方块模型/字体/贴图全空(地形渲不出来)。 - // 驱动方式照抄 LoadingOverlay:轮询 isDone(),不能 join(会死锁)。 - reloadResources(mc); - System.out.println("[wr] resources reloaded"); - - // ── listener ── - Connection conn = new Connection(PacketFlow.CLIENTBOUND); - GameProfile profile = new GameProfile(UUID.randomUUID(), "mcfold_cam"); - WorldSessionTelemetryManager telemetry = new WorldSessionTelemetryManager( - TelemetryEventSender.DISABLED, false, Duration.ZERO, ""); - ClientPacketListener listener = new ClientPacketListener(mc, null, conn, null, profile, telemetry); - - // ── 注册表:从客户端自带 data/ 加载 ── - var packs = net.minecraft.server.packs.repository.ServerPacksSource.createPackRepository( - new File(runDir, "datapacks").toPath()); - packs.reload(); - ResourceManager dataRm = new net.minecraft.server.packs.resources.MultiPackResourceManager( - net.minecraft.server.packs.PackType.SERVER_DATA, - java.util.List.of(packs.getPack("vanilla").open())); - java.util.List> all = new java.util.ArrayList<>(); - all.addAll(RegistryDataLoader.WORLDGEN_REGISTRIES); - all.addAll(RegistryDataLoader.DIMENSION_REGISTRIES); - RegistryAccess.Frozen loaded = RegistryDataLoader.load(dataRm, listener.registryAccess(), all); - var layered = net.minecraft.client.multiplayer.ClientRegistryLayer.createRegistryAccess() - .replaceFrom(net.minecraft.client.multiplayer.ClientRegistryLayer.REMOTE, - new RegistryAccess.Frozen[]{loaded}); - java.lang.reflect.Field rf = ClientPacketListener.class.getDeclaredField("registryAccess"); - rf.setAccessible(true); - rf.set(listener, layered); - System.out.println("[wr] registries loaded: dimension_type=" - + loaded.registryOrThrow(Registries.DIMENSION_TYPE).size()); - - Holder dimHolder = loaded.registryOrThrow(Registries.DIMENSION_TYPE) - .getHolderOrThrow(ResourceKey.create(Registries.DIMENSION_TYPE, new ResourceLocation("overworld"))); - - // ── ClientLevel ── - var levelData = new ClientLevel.ClientLevelData(Difficulty.NORMAL, false, false); - ClientLevel level = new ClientLevel(listener, levelData, Level.OVERWORLD, dimHolder, - 8, 8, () -> InactiveProfiler.INSTANCE, mc.levelRenderer, false, 0L); - mc.setLevel(level); - - var player = new net.minecraft.client.player.LocalPlayer(mc, level, listener, - new net.minecraft.stats.StatsCounter(), new net.minecraft.client.ClientRecipeBook(), false, false); - // 站在区块中心上方 - player.setPos(8.0, 70.0, 8.0); - mc.player = player; - mc.setCameraEntity(player); - mc.gameMode = new net.minecraft.client.multiplayer.MultiPlayerGameMode(mc, listener); - System.out.println("[wr] level + player ok"); - - // ── shader 编译(包私有,走同包 bridge)── - net.minecraft.client.renderer.McfoldBridge.reloadShaders(mc.gameRenderer, mc.getResourceManager()); - System.out.println("[wr] shaders ok: " + (RenderSystem.getShader() != null)); - - // ── LoadingOverlay 必须摘掉 ── - mc.setOverlay(null); - // ★ "Joining world..." 是 ReceivingLevelScreen;不清它客户端始终认为未进世界 - mc.setScreen(null); - - // ── 注入区块 ── - int injected = injectWorld(level, mc); - System.out.println("[wr] injected chunks=" + injected); - - // ── 第三人称(跳过 renderItemInHand)── - mc.options.setCameraType(net.minecraft.client.CameraType.THIRD_PERSON_BACK); - - // ── 渲染(每帧先 tick:区块网格重建队列靠 tick 推进)── - MainTarget target = (MainTarget) mc.getMainRenderTarget(); - for (int f = 0; f < 60; f++) { - try { mc.tick(); } catch (Throwable t) { if (f == 0) System.out.println("[wr] tick err: " + t); } - RenderSystem.clear(16640, Minecraft.ON_OSX); - target.bindWrite(true); - mc.gameRenderer.render(0.0F, (long) f * 16_000_000L, true); - target.unbindWrite(); - target.blitToScreen(mc.getWindow().getWidth(), mc.getWindow().getHeight()); - mc.getWindow().updateDisplay(); - } - System.out.println("[wr] rendered 20 frames"); - - // ── 诊断:区块与网格状态 ── - System.out.println("[wr] hasRenderedAllChunks=" + mc.levelRenderer.hasRenderedAllChunks() - + " countRenderedChunks=" + mc.levelRenderer.countRenderedChunks()); - var got = level.getChunkSource().getChunk(0, 0, net.minecraft.world.level.chunk.ChunkStatus.FULL, false); - System.out.println("[wr] getChunk(0,0)=" + (got != null ? got.getClass().getSimpleName() : "null") - + " block(0,64,0)=" + (got != null ? got.getBlockState(new BlockPos(0,64,0)) : "-")); - System.out.println("[wr] isChunkCompiled(0,64,0)=" + mc.levelRenderer.isChunkCompiled(new BlockPos(0,64,0))); - - save(target, W, H, out); - System.exit(0); - } - - /** - * 同步驱动各 reload listener。 - * - * 为什么不用 Minecraft.reloadResourcePacks(): - * 它的 future 靠主循环 tick 推进(LoadingOverlay),我们不跑主循环 ⇒ 死锁。 - * 为什么不用 createReload(): - * prepare 阶段仍走真后台池,实测 120s 超时。 - * 正解:直接调各 listener 的 public reload(),barrier 同步返回, - * 且 apply 用 Runnable::run 保证在渲染线程执行。 - */ - private static void reloadResources(Minecraft mc) throws Exception { - var rmF = Minecraft.class.getDeclaredField("resourceManager"); - rmF.setAccessible(true); - var rm = (net.minecraft.server.packs.resources.ReloadableResourceManager) rmF.get(mc); - var packs = mc.getResourcePackRepository(); - packs.reload(); - - // 同步 barrier:prepare 完成后立刻交出,不排队 - var barrier = new net.minecraft.server.packs.resources.PreparableReloadListener.PreparationBarrier() { - @Override public java.util.concurrent.CompletableFuture wait(T t) { - return java.util.concurrent.CompletableFuture.completedFuture(t); - } - }; - var prof = InactiveProfiler.INSTANCE; - // prepare 用当前线程(同步跑),apply 也用当前线程(渲染线程) - java.util.concurrent.Executor sync = Runnable::run; - - var all = reloadListeners(mc); - System.out.println("[wr] listeners=" + all.size()); - for (var listener : all) { - String name = listener.getName(); - System.out.println("[wr] -> " + name + " ..."); - try { - var fut = listener.reload(barrier, rm, prof, prof, sync, sync); - boolean done = false; - long dl = System.currentTimeMillis() + 30_000; - while (!done && System.currentTimeMillis() < dl) { - done = fut.isDone(); - if (!done) Thread.sleep(10); - } - if (!done) { System.out.println("[wr] TIMEOUT " + name); continue; } - fut.join(); - System.out.println("[wr] ok: " + name); - } catch (Throwable e) { - System.out.println("[wr] FAILED " + name + ": " + e); - } - } - } - - /** 取出注册过的 listener(private 字段) */ - @SuppressWarnings("unchecked") - private static java.util.List reloadListeners(Minecraft mc) throws Exception { - var rmF = Minecraft.class.getDeclaredField("resourceManager"); - rmF.setAccessible(true); - var rm = rmF.get(mc); - for (var f : rm.getClass().getDeclaredFields()) { - if (java.util.List.class.isAssignableFrom(f.getType())) { - f.setAccessible(true); - Object v = f.get(rm); - if (v instanceof java.util.List l && !l.isEmpty() - && l.get(0) instanceof net.minecraft.server.packs.resources.PreparableReloadListener) { - return (java.util.List) l; - } - } - } - throw new IllegalStateException("reload listeners not found"); - } - - /** 造地形并注入:区块 + 光照 + 网格重建 */ - private static int injectWorld(ClientLevel level, Minecraft mc) throws Exception { - int radius = 4; // 9x9 区块 = 144x144 格 - int yBottom = level.getMinBuildHeight(); - var cache = (net.minecraft.client.multiplayer.ClientChunkCache) level.getChunkSource(); - var light = cache.getLightEngine(); - - // 反射拿 Storage.chunks / getIndex(包私有) - var storageF = net.minecraft.client.multiplayer.ClientChunkCache.class.getDeclaredField("storage"); - storageF.setAccessible(true); - Object storage = storageF.get(cache); - var chunksF = storage.getClass().getDeclaredField("chunks"); - chunksF.setAccessible(true); - var chunks = (java.util.concurrent.atomic.AtomicReferenceArray) chunksF.get(storage); - var getIndex = storage.getClass().getDeclaredMethod("getIndex", int.class, int.class); - getIndex.setAccessible(true); - System.out.println("[wr] storage chunks.length=" + chunks.length()); - - int count = 0; - for (int cx = -radius; cx <= radius; cx++) { - for (int cz = -radius; cz <= radius; cz++) { - LevelChunk chunk = new LevelChunk(level, new ChunkPos(cx, cz)); - for (int lx = 0; lx < 16; lx++) { - for (int lz = 0; lz < 16; lz++) { - int wx = cx * 16 + lx, wz = cz * 16 + lz; - for (int y = yBottom; y < 63; y++) - chunk.setBlockState(new BlockPos(lx, y, lz), Blocks.STONE.defaultBlockState(), false); - chunk.setBlockState(new BlockPos(lx, 63, lz), Blocks.DIRT.defaultBlockState(), false); - chunk.setBlockState(new BlockPos(lx, 64, lz), Blocks.GRASS_BLOCK.defaultBlockState(), false); - if ((wx % 7 == 0 && wz % 7 == 0) && (wx != 0 || wz != 0)) { - for (int y = 65; y < 72; y++) - chunk.setBlockState(new BlockPos(lx, y, lz), Blocks.OAK_LOG.defaultBlockState(), false); - for (int dx = -2; dx <= 2; dx++) - for (int dz = -2; dz <= 2; dz++) - if (lx+dx >= 0 && lx+dx < 16 && lz+dz >= 0 && lz+dz < 16 - && Math.abs(dx) + Math.abs(dz) <= 2) - chunk.setBlockState(new BlockPos(lx+dx, 72, lz+dz), - Blocks.OAK_LEAVES.defaultBlockState(), false); - } - } - } - // ★ 核心:直接放进 ClientChunkCache.Storage.chunks - int idx = (int) getIndex.invoke(storage, cx, cz); - chunks.set(idx, chunk); - count++; - - // ★ 光照:全亮(否则地形全黑)。section y 0..19 覆盖 build height - for (int sy = level.getMinSection(); sy < level.getMaxSection(); sy++) { - var sky = new net.minecraft.world.level.chunk.DataLayer(); - for (int x = 0; x < 16; x++) - for (int y = 0; y < 16; y++) - for (int z = 0; z < 16; z++) sky.set(x, y, z, 15); - light.queueSectionData(net.minecraft.world.level.LightLayer.SKY, - net.minecraft.core.SectionPos.of(cx, sy, cz), sky); - light.queueSectionData(net.minecraft.world.level.LightLayer.BLOCK, - net.minecraft.core.SectionPos.of(cx, sy, cz), sky); - } - light.setLightEnabled(new ChunkPos(cx, cz), true); - chunk.setLightCorrect(true); - } - } - int upd = light.runLightUpdates(); - System.out.println("[wr] light updates=" + upd); - - // 触发区块网格重建 - mc.levelRenderer.allChanged(); - System.out.println("[wr] allChanged done"); - return count; - } - - private static void save(MainTarget target, int W, int H, String out) throws Exception { - org.lwjgl.opengl.GL30.glBindFramebuffer(org.lwjgl.opengl.GL30.GL_FRAMEBUFFER, target.frameBufferId); - GL11.glReadBuffer(org.lwjgl.opengl.GL30.GL_COLOR_ATTACHMENT0); - var buf = org.lwjgl.BufferUtils.createByteBuffer(W * H * 4); - GL11.glReadPixels(0, 0, W, H, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, buf); - org.lwjgl.opengl.GL30.glBindFramebuffer(org.lwjgl.opengl.GL30.GL_FRAMEBUFFER, 0); - - BufferedImage img = new BufferedImage(W, H, BufferedImage.TYPE_INT_RGB); - for (int y = 0; y < H; y++) - for (int x = 0; x < W; x++) { - int i = ((H - 1 - y) * W + x) * 4; - img.setRGB(x, y, ((buf.get(i) & 0xFF) << 16) | ((buf.get(i+1) & 0xFF) << 8) | (buf.get(i+2) & 0xFF)); - } - ImageIO.write(img, "png", new File(out)); - java.util.Set colors = new java.util.HashSet<>(); - for (int p : img.getRGB(0, 0, W, H, null, 0, W)) colors.add(p & 0xFFFFFF); - System.out.println("[wr] wrote " + out + " distinct colors=" + colors.size()); - } - - private static String arg(String[] a, String key, String def) { - for (int i = 0; i < a.length - 1; i++) if (a[i].equals(key)) return a[i + 1]; - return def; - } -} diff --git a/scaffold/build.sh b/scaffold/build.sh index 0e8298b..a797544 100755 --- a/scaffold/build.sh +++ b/scaffold/build.sh @@ -1,76 +1,105 @@ #!/usr/bin/env bash -# mcfold scaffold 构建脚本 —— 把「官方客户端 jar」变成「可直接调用的渲染库」 +# mcfold 构建脚本 —— 把官方客户端 jar 变成「可直接调用的渲染库」。 # -# 实测链路(2026-09-25 全绿): -# 1. 下官方 ProGuard 映射(client_mappings.txt) -# 2. 转成 SRG(SpecialSource 只吃 SRG) -# 3. 重映射 client.jar → client-named.jar(类名可读,javap/javac 可用) -# 4. 编译脚手架 +# ★ 版本隔离策略(用户 2026-09-25 强调) +# 产物写入**内容根** store/-/.mcfold/,不写 /tmp: +# 内容根/.mcfold/client-named.jar 反混淆后的客户端(可只读共享) +# 内容根/.mcfold/classes/ 我们的渲染服务 +# 内容根/.mcfold/natives/ LWJGL .so +# 理由:这些都由该版本的 client jar 派生,属「可共享内容」; +# /tmp 是 tmpfs,重启即丢(实测踩过)。实例独占的东西(mods/config/options) +# 一律不进这里。 # -# 用法: ./build.sh [实例store目录] [MC版本] +# 用法: ./build.sh <内容根|实例名> [java模式目录] +# ./build.sh /home/newqqagent/mcview/store/vanilla-1.20.1 1.20.1 +# ./build.sh /home/newqqagent/mcview/store/vanilla-1.21.1 1.21.1 java/gen_1_21 set -euo pipefail -STORE="${1:-/home/newqqagent/mcview/store/vanilla-1.20.1}" -MCVER="${2:-1.20.1}" +STORE="${1:?用法: build.sh <内容根> [java目录]}" +MCVER="${2:?需要 MC 版本}" +JAVA_SUB="${3:-}" HERE="$(cd "$(dirname "$0")" && pwd)" -WORK="${MCFOLD_SCAFFOLD_WORK:-/tmp/mcfold-scaffold}" -mkdir -p "$WORK" -cd "$WORK" +# 按版本挑世代目录(除非显式指定) +if [ -z "$JAVA_SUB" ]; then + case "$MCVER" in + 1.21*) JAVA_SUB="java/gen_1_21" ;; + *) JAVA_SUB="java/gen_1_20" ;; + esac +fi +OUT="$STORE/.mcfold" +WORK="${MCFOLD_BUILD_WORK:-$(mktemp -d /tmp/mcfold-build-XXXXXX)}" CLIENT_JAR="$STORE/versions/$MCVER/$MCVER.jar" [ -f "$CLIENT_JAR" ] || { echo "找不到客户端 jar: $CLIENT_JAR"; exit 1; } -# ── 1. 映射文件 ──────────────────────────────────────────────── -MAPPINGS_URL=$(python3 - "$STORE/versions/$MCVER/$MCVER.json" <<'PY' +mkdir -p "$OUT/classes" "$OUT/natives" "$WORK" +echo "[build] 版本 $MCVER 世代 $JAVA_SUB" +echo "[build] 输出 → $OUT" + +# ── 1. 官方映射 ──────────────────────────────────────────────── +if [ ! -s "$WORK/client_mappings.txt" ]; then + URL=$(python3 - "$STORE/versions/$MCVER/$MCVER.json" <<'PY' import json,sys -d=json.load(open(sys.argv[1])) -print(d['downloads']['client_mappings']['url']) +print(json.load(open(sys.argv[1]))['downloads']['client_mappings']['url']) PY ) -if [ ! -s client_mappings.txt ]; then - echo "[1/4] 下载官方映射..." - curl -sL -o client_mappings.txt "$MAPPINGS_URL" + echo "[1/5] 下载官方映射..." + curl -sL -o "$WORK/client_mappings.txt" "$URL" fi -echo "[1/4] 映射: $(wc -l < client_mappings.txt) 行" +echo "[1/5] 映射 $(wc -l < "$WORK/client_mappings.txt") 行" # ── 2. ProGuard → SRG ───────────────────────────────────────── -echo "[2/4] 转换映射格式..." -python3 "$HERE/pg2srg.py" client_mappings.txt mappings.srg +echo "[2/5] 转换映射格式..." +python3 "$HERE/pg2srg.py" "$WORK/client_mappings.txt" "$WORK/mappings.srg" -# ── 3. 重映射 jar ───────────────────────────────────────────── -if [ ! -f client-named.jar ] || [ "$CLIENT_JAR" -nt client-named.jar ]; then - echo "[3/4] 重映射客户端 jar(约 1-2 分钟)..." +# ── 3. 重映射客户端 jar ─────────────────────────────────────── +NAMED="$OUT/client-named.jar" +if [ ! -f "$NAMED" ] || [ "$CLIENT_JAR" -nt "$NAMED" ]; then + echo "[3/5] 重映射(1-3 分钟)..." SS=$(find /home/minecraft/libraries -name "SpecialSource-*.jar" 2>/dev/null | head -1) - [ -n "$SS" ] || { echo "找不到 SpecialSource"; exit 1; } GUAVA=$(find /home/minecraft/libraries -name "guava-*.jar" ! -name "*sources*" 2>/dev/null | head -1) + [ -n "$SS" ] || { echo "找不到 SpecialSource"; exit 1; } for a in asm asm-commons asm-tree; do - [ -f "$a.jar" ] || curl -sL -o "$a.jar" "https://repo1.maven.org/maven2/org/ow2/asm/$a/9.7/$a-9.7.jar" + [ -f "$WORK/$a.jar" ] || curl -sL -o "$WORK/$a.jar" \ + "https://repo1.maven.org/maven2/org/ow2/asm/$a/9.7/$a-9.7.jar" done - [ -f joptsimple.jar ] || curl -sL -o joptsimple.jar \ - "https://repo1.maven.org/maven2/net/sf/jopt-simple/jopt-simple/5.0.4/jopt-simple-5.0.4.jar" - java -Xmx3G -cp "$SS:joptsimple.jar:asm.jar:asm-commons.jar:asm-tree.jar:$GUAVA" \ + [ -f "$WORK/joptsimple.jar" ] || curl -sL -o "$WORK/joptsimple.jar" \ + "https://repo1.maven.org/maven2/net/sf/jopt-simple/jopt-simple/5.0.4/joptsimple-5.0.4.jar" + java -Xmx3G -cp "$SS:$WORK/joptsimple.jar:$WORK/asm.jar:$WORK/asm-commons.jar:$WORK/asm-tree.jar:$GUAVA" \ net.md_5.specialsource.SpecialSource \ - -i "$CLIENT_JAR" -o client-named.jar -m mappings.srg + -i "$CLIENT_JAR" -o "$NAMED" -m "$WORK/mappings.srg" else - echo "[3/4] client-named.jar 已是最新" + echo "[3/5] client-named.jar 已是最新" fi -ls -la client-named.jar -# ── classpath ───────────────────────────────────────────────── +# ── 4. classpath(去 asm 重复,Fabric 需要) + natives ──────── find "$STORE/libraries" -name "*.jar" ! -name "*-sources.jar" ! -name "*natives*" 2>/dev/null \ - | grep -vE "minecraftforge|neoforge" > cp.txt -CP="$(paste -sd: cp.txt):$WORK/client-named.jar" + | grep -vE "minecraftforge|neoforge" > "$OUT/classpath.txt" +NATLIST=$(find "$STORE/libraries" -name "*natives-linux*.jar" 2>/dev/null) +if [ -n "$NATLIST" ]; then + rm -rf "$OUT/natives/linux"; mkdir -p "$OUT/natives" + while read -r j; do [ -f "$j" ] && unzip -o -q "$j" -d "$OUT/natives" 2>/dev/null || true; done <<< "$NATLIST" +fi +echo "[4/5] 库 $(wc -l < "$OUT/classpath.txt") 个, natives $(find "$OUT/natives" -name '*.so' | wc -l) 个" + +# ── 5. 编译渲染服务 ─────────────────────────────────────────── +echo "[5/5] 编译 $JAVA_SUB ..." +CP="$(paste -sd: "$OUT/classpath.txt"):$NAMED:$OUT/classes" +find "$HERE/$JAVA_SUB" -name "*.java" > "$WORK/srcs.txt" +javac -nowarn -proc:none -cp "$CP" -d "$OUT/classes" @"$WORK/srcs.txt" -# ── 4. 编译脚手架 ───────────────────────────────────────────── -echo "[4/4] 编译脚手架..." -mkdir -p classes -javac -nowarn -proc:none -cp "$CP" -d classes "$HERE"/*.java echo -echo "✅ 构建完成" -echo " named jar : $WORK/client-named.jar" -echo " classes : $WORK/classes" +echo "✅ 构建完成(产物在内容根,不是 /tmp)" +echo " 内容根 : $STORE" +echo " 产物 : $OUT" +echo " named : $OUT/client-named.jar" +echo " classes: $OUT/classes" echo -echo "运行(无头,需 Xvfb):" -echo " find $STORE/libraries -name '*natives-linux*.jar' | while read j; do unzip -o -q \$j -d $WORK/nativ; done" -echo " DISPLAY=:98 java -cp \"$CP:$WORK/classes\" -Djava.library.path=$WORK/nativ RenderProbe" +echo "运行(渲染服务从 stdin 读 NDJSON):" +echo " DISPLAY=:98 java -cp \"\$(paste -sd: $OUT/classpath.txt):$NAMED:$OUT/classes\" \\" +echo " -Djava.library.path=$OUT/natives McfoldRenderServer \\" +echo " --gameDir <实例目录> --assets $STORE/assets --assetIndex $MCVER" +echo +echo "★ 实例目录必须是**实例独占**的那个(mods/config/options 的边界)," +echo " 不要指向别的版本或别的加载器的实例 —— 那正是「多实例共用 mods」的崩因。" diff --git a/scaffold/java/gen_1_20/McfoldRenderServer.java b/scaffold/java/gen_1_20/McfoldRenderServer.java index a51cd9b..2af936c 100644 --- a/scaffold/java/gen_1_20/McfoldRenderServer.java +++ b/scaffold/java/gen_1_20/McfoldRenderServer.java @@ -99,7 +99,11 @@ public class McfoldRenderServer { // stdout 只放协议 JSON,MC 的 INFO/WARN 全部赶到 stderr System.setOut(new PrintStream(new java.io.FileOutputStream(java.io.FileDescriptor.err), true)); - String gameDir = arg(args, "--gameDir", "/home/newqqagent/mcview/instances/default"); + // ★ 不硬编码实例目录(版本隔离策略): + // 实例目录 = mods/config/options 的独占边界,必须由调用方按档案指定。 + // 缺省只给一个不含 mods 的临时目录,避免污染任何真实实例。 + String gameDir = arg(args, "--gameDir", System.getProperty("java.io.tmpdir") + + "/mcfold-noinstance"); String assets = arg(args, "--assets", "/home/newqqagent/mcview/store/vanilla-1.20.1/assets"); String assetIdx = arg(args, "--assetIndex", "1.20.1"); width = Integer.parseInt(arg(args, "--width", "854")); @@ -264,6 +268,161 @@ public class McfoldRenderServer { emit(id, res); break; } + case "modelcheck": { + // ★ 程序化判据:mod 方块的 baked model 是否就是 missing model + // (画面里看到紫黑棋盘格是「缺贴图」的视觉表现,这里给出可断言的答案) + JsonArray specs = req.getAsJsonArray("blocks"); + var shaper = mc.getBlockRenderer().getBlockModelShaper(); + Object missing = null; + try { + var mmF = Minecraft.class.getDeclaredField("modelManager"); + mmF.setAccessible(true); + Object mm = mmF.get(mc); + missing = mm.getClass().getMethod("getMissingModel").invoke(mm); + } catch (Throwable t) { } + var lookup = BuiltInRegistries.BLOCK.asLookup(); + JsonArray rows = new JsonArray(); + for (JsonElement el : specs) { + String spec = el.getAsString(); + JsonObject ro = new JsonObject(); + ro.addProperty("spec", spec); + try { + var pr = net.minecraft.commands.arguments.blocks.BlockStateParser + .parseForBlock(lookup, spec, false); + var st = pr.blockState(); + Object baked = shaper.getBlockModel(st); + ro.addProperty("isMissing", baked == missing); + ro.addProperty("modelClass", baked.getClass().getSimpleName()); + try { + Object icon = baked.getClass().getMethod("getParticleIcon", + com.mojang.serialization.Codec.class).invoke(baked, + net.minecraft.client.renderer.texture.TextureAtlasSprite.class == null ? null : null); + } catch (Throwable ignore) { } + } catch (Throwable t) { + ro.addProperty("error", String.valueOf(t)); + } + rows.add(ro); + } + JsonObject r = new JsonObject(); + r.addProperty("ok", true); + r.add("results", rows); + emit(id, r); + break; + } + case "atlascheck": { + // ★ 判据:mod 贴图是否真进了 blocks atlas + // 正确 API 是 Minecraft.getTextureAtlas(...) → Function + // (早先用 ModelManager.getAtlasSet 是错的,导致全 NPE 假绿) + JsonArray arr = req.getAsJsonArray("textures"); + var fn = mc.getTextureAtlas( + net.minecraft.client.renderer.texture.TextureAtlas.LOCATION_BLOCKS); + JsonArray rows = new JsonArray(); + for (JsonElement el : arr) { + String tex = el.getAsString(); + JsonObject ro = new JsonObject(); + ro.addProperty("texture", tex); + ro.addProperty("isMissing", true); + ro.addProperty("sprite", ""); + try { + // ★ 世代差异:1.20 无 parse() 静态工厂(1.21+ 才有),用构造器 + var sprite = fn.apply(new ResourceLocation(tex)); + String reg = sprite.contents().name().toString(); + ro.addProperty("sprite", reg); + ro.addProperty("isMissing", reg.contains("missingno")); + } catch (Throwable t) { + ro.addProperty("sprite", "ERROR: " + t); + } + rows.add(ro); + } + JsonObject r = new JsonObject(); + r.addProperty("ok", true); + r.add("results", rows); + emit(id, r); + break; + } + case "blockinfo": { + // ★ 方块信息查询(Java 侧,因为只有它有 level + 注册表 + 模型状态)。 + // Python 只路由,不做任何格式/语义判断。 + // 入参:{"cmd":"blockinfo","x":..,"y":..,"z":..} + int bx = req.get("x").getAsInt(); + int by = req.get("y").getAsInt(); + int bz = req.get("z").getAsInt(); + BlockPos pos = new BlockPos(bx, by, bz); + JsonObject r = new JsonObject(); + r.addProperty("ok", true); + r.addProperty("x", bx); r.addProperty("y", by); r.addProperty("z", bz); + + int pcx = bx >> 4, pcz = bz >> 4; + // ★ 判据必须是「真拿到了 LevelChunk」: + // ClientLevel.hasChunk() 实测**恒返回 true**(反编译见 306 行), + // 基类 ChunkSource.hasChunk() 走 getChunk(...,false), + // 而 ClientChunkCache 覆写后在该参数下返回 emptyChunk(void_air 占位) + // ⇒ 都不可用于「未加载 vs 空气」的区分。 + // 可靠做法:getChunk(..., requireChunk=**false**) —— 反编译实测 + // return requireChunk ? emptyChunk : null; + // 即 requireChunk=false 时**未加载才给 null**(传 true 反而给占位)。 + var realChunk = level.getChunkSource() + .getChunk(pcx, pcz, net.minecraft.world.level.chunk.ChunkStatus.FULL, false); + boolean loaded = realChunk != null; + r.addProperty("chunkLoaded", loaded); + r.addProperty("inBuildHeight", level.isInWorldBounds(pos)); + + if (!loaded) { + // ★ 未加载 ≠ 空气(设计文档附录 D 强调的语义区分) + r.addProperty("known", false); + r.addProperty("reason", "chunk_not_loaded"); + emit(id, r); + break; + } + + BlockState st = level.getBlockState(pos); + Block blk = st.getBlock(); + ResourceLocation key = BuiltInRegistries.BLOCK.getKey(blk); + + r.addProperty("known", true); + r.addProperty("block", key == null ? "unknown" : key.toString()); + r.addProperty("stateId", Block.getId(st)); + r.addProperty("state", st.toString()); + r.addProperty("isAir", st.isAir()); + r.addProperty("isSolid", st.isSolid()); + + // 方块属性(朝向/开关等)—— 渲染与指令都需要 + JsonObject props = new JsonObject(); + try { + for (var e : st.getValues().entrySet()) { + props.addProperty(e.getKey().getName(), String.valueOf(e.getValue())); + } + } catch (Throwable ignore) { } + r.add("properties", props); + + // 光照与硬度(agent 判断「这能不能挖/亮不亮」用) + try { + r.addProperty("lightEmission", st.getLightEmission()); + r.addProperty("destroySpeed", st.getDestroySpeed(level, pos)); + r.addProperty("canOcclude", st.canOcclude()); + r.addProperty("hasBlockEntity", st.hasBlockEntity()); + } catch (Throwable ignore) { } + + // 贴图是否真的在 atlas 里(缺贴图的**程序化判据**,不靠看图) + // BlockModelShaper.getParticleIcon(BlockState) 是静态的; + // 它返回的 sprite 名字若含 missingno 就是缺贴图。 + try { + var shaper = mc.getBlockRenderer().getBlockModelShaper(); + // 实例方法(1.20.1 与 1.21.1 签名一致) + var icon = shaper.getParticleIcon(st); + String reg = icon.contents().name().toString(); + r.addProperty("particleTexture", reg); + r.addProperty("isMissingTexture", reg.contains("missingno")); + } catch (Throwable t) { + r.addProperty("particleTexture", "unavailable: " + t.getClass().getSimpleName()); + } + + // 是否来自 mod(零加载器注册的方块 namespace 不在 minecraft) + r.addProperty("fromMod", key != null && !key.getNamespace().equals("minecraft")); + + emit(id, r); + break; + } case "camera": { double x = req.get("x").getAsDouble(), y = req.get("y").getAsDouble(), z = req.get("z").getAsDouble(); float yaw = req.has("yaw") ? req.get("yaw").getAsFloat() : 0f; @@ -679,6 +838,101 @@ public class McfoldRenderServer { System.err.println("[mcfold] boot done"); } + /** + * 把 blockstate 里的 model 引用规范化成 jar 里实际存在的路径。 + * + * 实测两类写法: + * "ns:block/foo" → models/block/foo.json (createbigcannons 风格,正确) + * "ns:foo" → models/foo.json(不存在!) (MTS 风格,实际在 models/block/foo.json) + * 所以先原样查,再试补 "block/"。 + */ + static String normalizeModelPath(java.util.zip.ZipFile zf, String model) { + int i = model.indexOf(':'); + String ns = i > 0 ? model.substring(0, i) : "minecraft"; + String path = i > 0 ? model.substring(i + 1) : model; + if (zf.getEntry("assets/" + ns + "/models/" + path + ".json") != null) return model; + if (zf.getEntry("assets/" + ns + "/models/block/" + path + ".json") != null) { + return ns + ":block/" + path; + } + return model; // 都不存在 → 保持原样(会显示 missing,但至少不误导) + } + + /** + * 生成一个「blockstates 覆盖包」:把 mod 的变体键统一改写成 ""。 + * + * ★ 为什么需要(实测根因): + * 我们的占位方块只有 defaultBlockState(变体键 ""), + * 而 mod 的 blockstates 常用属性键(如 "normal"、"facing=north")⇒ 匹配不上 + * ⇒ missing model ⇒ 画面上是紫黑棋盘格。 + * 例:createbigcannons:steel_block 的键是 ""(渲染正常,测过), + * mts:...fancydirt_block 的键是 "normal"(棋盘格)。 + * + * 做法:读每个 mod jar 的 assets//blockstates/*.json,取第一个 variant 的 model, + * 改写成 {"variants":{"":{"model":"<那个 model>"}}},打进一个 zip。 + * 该 zip 作为**最后选中**的资源包,优先级最高,从而覆盖原 blockstates。 + * 模型/贴图仍由原 jar 提供(不动)。 + */ + static File buildBlockstateOverlay(java.util.List jars, java.nio.file.Path outDir) throws Exception { + File overlay = new File(outDir.toFile(), "mcfold-blockstate-overlay.zip"); + java.util.TreeMap entries = new java.util.TreeMap<>(); + entries.put("pack.mcmeta", + ("{\"pack\":{\"description\":\"mcfold blockstate overlay\",\"pack_format\":15}}") + .getBytes(java.nio.charset.StandardCharsets.UTF_8)); + int rewritten = 0; + for (File jar : jars) { + try (java.util.zip.ZipFile zf = new java.util.zip.ZipFile(jar)) { + var en = zf.entries(); + while (en.hasMoreElements()) { + var e = en.nextElement(); + String nm = e.getName(); + if (!nm.startsWith("assets/") || !nm.contains("/blockstates/") + || !nm.endsWith(".json")) continue; + String text; + try (var in = zf.getInputStream(e)) { + text = new String(in.readAllBytes(), java.nio.charset.StandardCharsets.UTF_8); + } + JsonObject root; + try { root = JsonParser.parseString(text).getAsJsonObject(); } + catch (Throwable t) { continue; } + if (!root.has("variants") || !root.get("variants").isJsonObject()) continue; + + // 取第一个 variant 的第一个 model —— 只求外观,不求语义 + String model = null; + for (var ve : root.getAsJsonObject("variants").entrySet()) { + JsonElement v = ve.getValue(); + JsonElement one = v.isJsonArray() ? (v.getAsJsonArray().size() > 0 + ? v.getAsJsonArray().get(0) : null) : v; + if (one != null && one.isJsonObject() && one.getAsJsonObject().has("model")) { + model = one.getAsJsonObject().get("model").getAsString(); + break; + } + } + if (model == null) continue; + // ★ 规范化 model 路径(实测根因): + // MTS 写 "mts:mtsofficialpack.fancydirt_block"(缺 block/), + // 而文件在 models/block/... ⇒ MC 找不到 ⇒ missing model ⇒ 棋盘格。 + // createbigcannons 写 "createbigcannons:block/steel_block"(有 block/),正常。 + // 策略:先按原样看 jar 里有没有;没有就试补 block/ 前缀。 + model = normalizeModelPath(zf, model); + String out = "{\"variants\":{\"\":{\"model\":\"" + model + "\"}}}"; + entries.put(nm, out.getBytes(java.nio.charset.StandardCharsets.UTF_8)); + rewritten++; + } + } + } + if (rewritten == 0) return null; + try (var zos = new java.util.zip.ZipOutputStream( + new java.io.FileOutputStream(overlay))) { + for (var kv : entries.entrySet()) { + zos.putNextEntry(new java.util.zip.ZipEntry(kv.getKey())); + zos.write(kv.getValue()); + zos.closeEntry(); + } + } + System.err.println("[mcfold] overlay: 重写 " + rewritten + " 个 blockstates → " + overlay); + return overlay; + } + /** * 加载 mod:扫 jar → 注册占位方块 → 挂资源包(外观)。 * @@ -768,15 +1022,48 @@ public class McfoldRenderServer { var repo = mc.getResourcePackRepository(); java.nio.file.Path rpDir = mc.getResourcePackDirectory(); for (File jar : jars) { - File dst = new File(rpDir.toFile(), "mcfold-" + jar.getName()); + // ★ 必须是 .zip 后缀:FolderRepositorySource 只认 + // isDirectory+pack.mcmeta 或 **.zip**(实测它不看 .jar) + String base = jar.getName().replaceAll("\\.jar$", ""); + File dst = new File(rpDir.toFile(), "mcfold-" + base + ".zip"); if (!dst.isFile()) java.nio.file.Files.copy(jar.toPath(), dst.toPath(), java.nio.file.StandardCopyOption.REPLACE_EXISTING); } + // ★ 生成 blockstates 覆盖包并放进资源包目录 + // (把 mod 的属性变体键改写成 "" —— 占位方块只有 defaultBlockState) + java.util.List jarList = new java.util.ArrayList<>(); + for (File j : jars) jarList.add(j); + File overlay = buildBlockstateOverlay(jarList, rpDir); + if (overlay != null) { + File od = new File(rpDir.toFile(), "zzz-mcfold-blockstate-overlay.zip"); + java.nio.file.Files.copy(overlay.toPath(), od.toPath(), + java.nio.file.StandardCopyOption.REPLACE_EXISTING); + } repo.reload(); + // 诊断(放进响应,stderr 会被后续日志冲掉) + out.addProperty("packDir", rpDir.toString()); + JsonArray avail = new JsonArray(); + for (String ai : repo.getAvailableIds()) avail.add(ai); + out.add("availableIds", avail); + JsonArray compat = new JsonArray(); + for (var pk2 : repo.getAvailablePacks()) { + JsonObject po = new JsonObject(); + po.addProperty("id", pk2.getId()); + po.addProperty("compat", String.valueOf(pk2.getCompatibility())); + compat.add(po); + } + out.add("availablePacks", compat); var sel = new java.util.LinkedHashSet<>(repo.getSelectedIds()); + // 先加 mod 包(提供 models/textures),再加 overlay(覆盖 blockstates) + java.util.List overlayIds = new java.util.ArrayList<>(); for (String packId : repo.getAvailableIds()) { + if (packId.contains("zzz-mcfold-blockstate-overlay")) { overlayIds.add(packId); continue; } if (packId.contains("mcfold-")) { sel.add(packId); packsAdded++; } } + sel.addAll(overlayIds); // 最后 = 最高优先级 + JsonArray selArr = new JsonArray(); + for (String si : sel) selArr.add(si); + out.add("selectedIds", selArr); repo.setSelected(sel); // 用新 pack 列表重建 resource manager(照官方 createReload 内部做法), @@ -794,6 +1081,36 @@ public class McfoldRenderServer { reloadResources(mc); net.minecraft.client.renderer.McfoldBridge.reloadShaders(mc.gameRenderer, mc.getResourceManager()); mc.levelRenderer.allChanged(); + + // ★ 自验证:重载后 mod 资源是否真的可读(TOO_OLD 是否阻止加载) + ResourceManager rm2 = mc.getResourceManager(); + System.err.println("[mcfold] resourceManager=" + rm2.getClass().getSimpleName() + + " namespaces=" + rm2.getNamespaces()); + JsonArray check = new JsonArray(); + for (File jar : jars) { + try (java.util.zip.ZipFile zf = new java.util.zip.ZipFile(jar)) { + var en2 = zf.entries(); + while (en2.hasMoreElements()) { + String nm = en2.nextElement().getName(); + java.util.regex.Matcher m2 = java.util.regex.Pattern + .compile("^assets/([^/]+)/(blockstates/.+\\.json)$").matcher(nm); + if (!m2.matches()) continue; + ResourceLocation rid = new ResourceLocation(m2.group(1), m2.group(2)); + JsonObject co = new JsonObject(); + co.addProperty("id", rid.toString()); + var got2 = rm2.getResource(rid); + co.addProperty("readable", got2.isPresent()); + if (got2.isPresent()) { + try (var in2 = got2.get().open()) { + co.addProperty("bytes", in2.readAllBytes().length); + } + } + check.add(co); + break; // 每个 jar 只查一个样本 + } + } catch (Throwable ignore) { } + } + out.add("resourceCheck", check); } catch (Throwable t) { out.addProperty("packError", String.valueOf(t)); System.err.println("[mcfold] 资源包挂载失败: " + t); diff --git a/scaffold/java/gen_1_21/McfoldRenderServer.java b/scaffold/java/gen_1_21/McfoldRenderServer.java index bf05e9d..1678c24 100644 --- a/scaffold/java/gen_1_21/McfoldRenderServer.java +++ b/scaffold/java/gen_1_21/McfoldRenderServer.java @@ -99,7 +99,11 @@ public class McfoldRenderServer { // stdout 只放协议 JSON,MC 的 INFO/WARN 全部赶到 stderr System.setOut(new PrintStream(new java.io.FileOutputStream(java.io.FileDescriptor.err), true)); - String gameDir = arg(args, "--gameDir", "/home/newqqagent/mcview/instances/default"); + // ★ 不硬编码实例目录(版本隔离策略): + // 实例目录 = mods/config/options 的独占边界,必须由调用方按档案指定。 + // 缺省只给一个不含 mods 的临时目录,避免污染任何真实实例。 + String gameDir = arg(args, "--gameDir", System.getProperty("java.io.tmpdir") + + "/mcfold-noinstance"); String assets = arg(args, "--assets", "/home/newqqagent/mcview/store/vanilla-1.20.1/assets"); String assetIdx = arg(args, "--assetIndex", "1.20.1"); width = Integer.parseInt(arg(args, "--width", "854")); @@ -264,6 +268,162 @@ public class McfoldRenderServer { emit(id, res); break; } + case "modelcheck": { + // ★ 程序化判据:mod 方块的 baked model 是否就是 missing model + // (画面里看到紫黑棋盘格是「缺贴图」的视觉表现,这里给出可断言的答案) + JsonArray specs = req.getAsJsonArray("blocks"); + var shaper = mc.getBlockRenderer().getBlockModelShaper(); + Object missing = null; + try { + var mmF = Minecraft.class.getDeclaredField("modelManager"); + mmF.setAccessible(true); + Object mm = mmF.get(mc); + missing = mm.getClass().getMethod("getMissingModel").invoke(mm); + } catch (Throwable t) { } + var lookup = BuiltInRegistries.BLOCK.asLookup(); + JsonArray rows = new JsonArray(); + for (JsonElement el : specs) { + String spec = el.getAsString(); + JsonObject ro = new JsonObject(); + ro.addProperty("spec", spec); + try { + var pr = net.minecraft.commands.arguments.blocks.BlockStateParser + .parseForBlock(lookup, spec, false); + var st = pr.blockState(); + Object baked = shaper.getBlockModel(st); + ro.addProperty("isMissing", baked == missing); + ro.addProperty("modelClass", baked.getClass().getSimpleName()); + try { + Object icon = baked.getClass().getMethod("getParticleIcon", + com.mojang.serialization.Codec.class).invoke(baked, + net.minecraft.client.renderer.texture.TextureAtlasSprite.class == null ? null : null); + } catch (Throwable ignore) { } + } catch (Throwable t) { + ro.addProperty("error", String.valueOf(t)); + } + rows.add(ro); + } + JsonObject r = new JsonObject(); + r.addProperty("ok", true); + r.add("results", rows); + emit(id, r); + break; + } + case "atlascheck": { + // ★ 判据:mod 贴图是否真进了 blocks atlas + // 正确 API 是 Minecraft.getTextureAtlas(...) → Function + // (早先用 ModelManager.getAtlasSet 是错的,导致全 NPE 假绿) + JsonArray arr = req.getAsJsonArray("textures"); + var fn = mc.getTextureAtlas( + net.minecraft.client.renderer.texture.TextureAtlas.LOCATION_BLOCKS); + JsonArray rows = new JsonArray(); + for (JsonElement el : arr) { + String tex = el.getAsString(); + JsonObject ro = new JsonObject(); + ro.addProperty("texture", tex); + ro.addProperty("isMissing", true); + ro.addProperty("sprite", ""); + try { + var sprite = fn.apply(ResourceLocation.parse(tex)); + String reg = sprite.contents().name().toString(); + ro.addProperty("sprite", reg); + ro.addProperty("isMissing", reg.contains("missingno")); + } catch (Throwable t) { + ro.addProperty("sprite", "ERROR: " + t); + } + rows.add(ro); + } + JsonObject r = new JsonObject(); + r.addProperty("ok", true); + r.add("results", rows); + emit(id, r); + break; + } + case "blockinfo": { + // ★ 方块信息查询(Java 侧,因为只有它有 level + 注册表 + 模型状态)。 + // Python 只路由,不做任何格式/语义判断。 + // 入参:{"cmd":"blockinfo","x":..,"y":..,"z":..} + int bx = req.get("x").getAsInt(); + int by = req.get("y").getAsInt(); + int bz = req.get("z").getAsInt(); + BlockPos pos = new BlockPos(bx, by, bz); + JsonObject r = new JsonObject(); + r.addProperty("ok", true); + r.addProperty("x", bx); r.addProperty("y", by); r.addProperty("z", bz); + + int pcx = bx >> 4, pcz = bz >> 4; + // ★ 判据必须是「真拿到了 LevelChunk」: + // ClientLevel.hasChunk() 实测**恒返回 true**(反编译见 306 行), + // 基类 ChunkSource.hasChunk() 走 getChunk(...,false), + // 而 ClientChunkCache 覆写后在该参数下返回 emptyChunk(void_air 占位) + // ⇒ 都不可用于「未加载 vs 空气」的区分。 + // 可靠做法:getChunk(..., requireChunk=**false**) —— 反编译实测 + // return requireChunk ? emptyChunk : null; + // 即 requireChunk=false 时**未加载才给 null**(传 true 反而给占位)。 + var realChunk = level.getChunkSource() + // ★ 世代差异:1.21 把 ChunkStatus 移到 world.level.chunk.status + .getChunk(pcx, pcz, + net.minecraft.world.level.chunk.status.ChunkStatus.FULL, false); + boolean loaded = realChunk != null; + r.addProperty("chunkLoaded", loaded); + r.addProperty("inBuildHeight", level.isInWorldBounds(pos)); + + if (!loaded) { + // ★ 未加载 ≠ 空气(设计文档附录 D 强调的语义区分) + r.addProperty("known", false); + r.addProperty("reason", "chunk_not_loaded"); + emit(id, r); + break; + } + + BlockState st = level.getBlockState(pos); + Block blk = st.getBlock(); + ResourceLocation key = BuiltInRegistries.BLOCK.getKey(blk); + + r.addProperty("known", true); + r.addProperty("block", key == null ? "unknown" : key.toString()); + r.addProperty("stateId", Block.getId(st)); + r.addProperty("state", st.toString()); + r.addProperty("isAir", st.isAir()); + r.addProperty("isSolid", st.isSolid()); + + // 方块属性(朝向/开关等)—— 渲染与指令都需要 + JsonObject props = new JsonObject(); + try { + for (var e : st.getValues().entrySet()) { + props.addProperty(e.getKey().getName(), String.valueOf(e.getValue())); + } + } catch (Throwable ignore) { } + r.add("properties", props); + + // 光照与硬度(agent 判断「这能不能挖/亮不亮」用) + try { + r.addProperty("lightEmission", st.getLightEmission()); + r.addProperty("destroySpeed", st.getDestroySpeed(level, pos)); + r.addProperty("canOcclude", st.canOcclude()); + r.addProperty("hasBlockEntity", st.hasBlockEntity()); + } catch (Throwable ignore) { } + + // 贴图是否真的在 atlas 里(缺贴图的**程序化判据**,不靠看图) + // BlockModelShaper.getParticleIcon(BlockState) 是静态的; + // 它返回的 sprite 名字若含 missingno 就是缺贴图。 + try { + var shaper = mc.getBlockRenderer().getBlockModelShaper(); + // 实例方法(1.20.1 与 1.21.1 签名一致) + var icon = shaper.getParticleIcon(st); + String reg = icon.contents().name().toString(); + r.addProperty("particleTexture", reg); + r.addProperty("isMissingTexture", reg.contains("missingno")); + } catch (Throwable t) { + r.addProperty("particleTexture", "unavailable: " + t.getClass().getSimpleName()); + } + + // 是否来自 mod(零加载器注册的方块 namespace 不在 minecraft) + r.addProperty("fromMod", key != null && !key.getNamespace().equals("minecraft")); + + emit(id, r); + break; + } case "camera": { double x = req.get("x").getAsDouble(), y = req.get("y").getAsDouble(), z = req.get("z").getAsDouble(); float yaw = req.has("yaw") ? req.get("yaw").getAsFloat() : 0f; @@ -723,6 +883,101 @@ public class McfoldRenderServer { System.err.println("[mcfold] boot done"); } + /** + * 把 blockstate 里的 model 引用规范化成 jar 里实际存在的路径。 + * + * 实测两类写法: + * "ns:block/foo" → models/block/foo.json (createbigcannons 风格,正确) + * "ns:foo" → models/foo.json(不存在!) (MTS 风格,实际在 models/block/foo.json) + * 所以先原样查,再试补 "block/"。 + */ + static String normalizeModelPath(java.util.zip.ZipFile zf, String model) { + int i = model.indexOf(':'); + String ns = i > 0 ? model.substring(0, i) : "minecraft"; + String path = i > 0 ? model.substring(i + 1) : model; + if (zf.getEntry("assets/" + ns + "/models/" + path + ".json") != null) return model; + if (zf.getEntry("assets/" + ns + "/models/block/" + path + ".json") != null) { + return ns + ":block/" + path; + } + return model; // 都不存在 → 保持原样(会显示 missing,但至少不误导) + } + + /** + * 生成一个「blockstates 覆盖包」:把 mod 的变体键统一改写成 ""。 + * + * ★ 为什么需要(实测根因): + * 我们的占位方块只有 defaultBlockState(变体键 ""), + * 而 mod 的 blockstates 常用属性键(如 "normal"、"facing=north")⇒ 匹配不上 + * ⇒ missing model ⇒ 画面上是紫黑棋盘格。 + * 例:createbigcannons:steel_block 的键是 ""(渲染正常,测过), + * mts:...fancydirt_block 的键是 "normal"(棋盘格)。 + * + * 做法:读每个 mod jar 的 assets//blockstates/*.json,取第一个 variant 的 model, + * 改写成 {"variants":{"":{"model":"<那个 model>"}}},打进一个 zip。 + * 该 zip 作为**最后选中**的资源包,优先级最高,从而覆盖原 blockstates。 + * 模型/贴图仍由原 jar 提供(不动)。 + */ + static File buildBlockstateOverlay(java.util.List jars, java.nio.file.Path outDir) throws Exception { + File overlay = new File(outDir.toFile(), "mcfold-blockstate-overlay.zip"); + java.util.TreeMap entries = new java.util.TreeMap<>(); + entries.put("pack.mcmeta", + ("{\"pack\":{\"description\":\"mcfold blockstate overlay\",\"pack_format\":15}}") + .getBytes(java.nio.charset.StandardCharsets.UTF_8)); + int rewritten = 0; + for (File jar : jars) { + try (java.util.zip.ZipFile zf = new java.util.zip.ZipFile(jar)) { + var en = zf.entries(); + while (en.hasMoreElements()) { + var e = en.nextElement(); + String nm = e.getName(); + if (!nm.startsWith("assets/") || !nm.contains("/blockstates/") + || !nm.endsWith(".json")) continue; + String text; + try (var in = zf.getInputStream(e)) { + text = new String(in.readAllBytes(), java.nio.charset.StandardCharsets.UTF_8); + } + JsonObject root; + try { root = JsonParser.parseString(text).getAsJsonObject(); } + catch (Throwable t) { continue; } + if (!root.has("variants") || !root.get("variants").isJsonObject()) continue; + + // 取第一个 variant 的第一个 model —— 只求外观,不求语义 + String model = null; + for (var ve : root.getAsJsonObject("variants").entrySet()) { + JsonElement v = ve.getValue(); + JsonElement one = v.isJsonArray() ? (v.getAsJsonArray().size() > 0 + ? v.getAsJsonArray().get(0) : null) : v; + if (one != null && one.isJsonObject() && one.getAsJsonObject().has("model")) { + model = one.getAsJsonObject().get("model").getAsString(); + break; + } + } + if (model == null) continue; + // ★ 规范化 model 路径(实测根因): + // MTS 写 "mts:mtsofficialpack.fancydirt_block"(缺 block/), + // 而文件在 models/block/... ⇒ MC 找不到 ⇒ missing model ⇒ 棋盘格。 + // createbigcannons 写 "createbigcannons:block/steel_block"(有 block/),正常。 + // 策略:先按原样看 jar 里有没有;没有就试补 block/ 前缀。 + model = normalizeModelPath(zf, model); + String out = "{\"variants\":{\"\":{\"model\":\"" + model + "\"}}}"; + entries.put(nm, out.getBytes(java.nio.charset.StandardCharsets.UTF_8)); + rewritten++; + } + } + } + if (rewritten == 0) return null; + try (var zos = new java.util.zip.ZipOutputStream( + new java.io.FileOutputStream(overlay))) { + for (var kv : entries.entrySet()) { + zos.putNextEntry(new java.util.zip.ZipEntry(kv.getKey())); + zos.write(kv.getValue()); + zos.closeEntry(); + } + } + System.err.println("[mcfold] overlay: 重写 " + rewritten + " 个 blockstates → " + overlay); + return overlay; + } + /** * 加载 mod:扫 jar → 注册占位方块 → 挂资源包(外观)。 * @@ -814,15 +1069,48 @@ public class McfoldRenderServer { var repo = mc.getResourcePackRepository(); java.nio.file.Path rpDir = mc.getResourcePackDirectory(); for (File jar : jars) { - File dst = new File(rpDir.toFile(), "mcfold-" + jar.getName()); + // ★ 必须是 .zip 后缀:FolderRepositorySource 只认 + // isDirectory+pack.mcmeta 或 **.zip**(实测它不看 .jar) + String base = jar.getName().replaceAll("\\.jar$", ""); + File dst = new File(rpDir.toFile(), "mcfold-" + base + ".zip"); if (!dst.isFile()) java.nio.file.Files.copy(jar.toPath(), dst.toPath(), java.nio.file.StandardCopyOption.REPLACE_EXISTING); } + // ★ 生成 blockstates 覆盖包并放进资源包目录 + // (把 mod 的属性变体键改写成 "" —— 占位方块只有 defaultBlockState) + java.util.List jarList = new java.util.ArrayList<>(); + for (File j : jars) jarList.add(j); + File overlay = buildBlockstateOverlay(jarList, rpDir); + if (overlay != null) { + File od = new File(rpDir.toFile(), "zzz-mcfold-blockstate-overlay.zip"); + java.nio.file.Files.copy(overlay.toPath(), od.toPath(), + java.nio.file.StandardCopyOption.REPLACE_EXISTING); + } repo.reload(); + // 诊断(放进响应,stderr 会被后续日志冲掉) + out.addProperty("packDir", rpDir.toString()); + JsonArray avail = new JsonArray(); + for (String ai : repo.getAvailableIds()) avail.add(ai); + out.add("availableIds", avail); + JsonArray compat = new JsonArray(); + for (var pk2 : repo.getAvailablePacks()) { + JsonObject po = new JsonObject(); + po.addProperty("id", pk2.getId()); + po.addProperty("compat", String.valueOf(pk2.getCompatibility())); + compat.add(po); + } + out.add("availablePacks", compat); var sel = new java.util.LinkedHashSet<>(repo.getSelectedIds()); + // 先加 mod 包(提供 models/textures),再加 overlay(覆盖 blockstates) + java.util.List overlayIds = new java.util.ArrayList<>(); for (String packId : repo.getAvailableIds()) { + if (packId.contains("zzz-mcfold-blockstate-overlay")) { overlayIds.add(packId); continue; } if (packId.contains("mcfold-")) { sel.add(packId); packsAdded++; } } + sel.addAll(overlayIds); // 最后 = 最高优先级 + JsonArray selArr = new JsonArray(); + for (String si : sel) selArr.add(si); + out.add("selectedIds", selArr); repo.setSelected(sel); // 用新 pack 列表重建 resource manager(照官方 createReload 内部做法), @@ -840,6 +1128,38 @@ public class McfoldRenderServer { reloadResources(mc); net.minecraft.client.renderer.McfoldBridge.reloadShaders(mc.gameRenderer, mc.getResourceManager()); mc.levelRenderer.allChanged(); + + // ★ 自验证:重载后 mod 资源是否真的可读(TOO_OLD 是否阻止加载) + ResourceManager rm2 = mc.getResourceManager(); + System.err.println("[mcfold] resourceManager=" + rm2.getClass().getSimpleName() + + " namespaces=" + rm2.getNamespaces()); + JsonArray check = new JsonArray(); + for (File jar : jars) { + try (java.util.zip.ZipFile zf = new java.util.zip.ZipFile(jar)) { + var en2 = zf.entries(); + while (en2.hasMoreElements()) { + String nm = en2.nextElement().getName(); + java.util.regex.Matcher m2 = java.util.regex.Pattern + .compile("^assets/([^/]+)/(blockstates/.+\\.json)$").matcher(nm); + if (!m2.matches()) continue; + // ★ 世代差异(1.21+):构造 private → 静态工厂 + ResourceLocation rid = ResourceLocation.fromNamespaceAndPath( + m2.group(1), m2.group(2)); + JsonObject co = new JsonObject(); + co.addProperty("id", rid.toString()); + var got2 = rm2.getResource(rid); + co.addProperty("readable", got2.isPresent()); + if (got2.isPresent()) { + try (var in2 = got2.get().open()) { + co.addProperty("bytes", in2.readAllBytes().length); + } + } + check.add(co); + break; // 每个 jar 只查一个样本 + } + } catch (Throwable ignore) { } + } + out.add("resourceCheck", check); } catch (Throwable t) { out.addProperty("packError", String.valueOf(t)); System.err.println("[mcfold] 资源包挂载失败: " + t); diff --git a/scaffold/java/launcher_fabric/LoaderBoot.java b/scaffold/java/launcher_fabric/LoaderBoot.java new file mode 100644 index 0000000..322c877 --- /dev/null +++ b/scaffold/java/launcher_fabric/LoaderBoot.java @@ -0,0 +1,67 @@ +import net.fabricmc.api.EnvType; +import net.fabricmc.loader.impl.launch.knot.Knot; + +/** + * 【加载器后端】用 Fabric 加载器,但不写 mod。 + * + * 用户定向(2026-09-25): + * 「不写 mod」+「用 mod 加载器」+「功能在现有后端里」。 + * + * 实现关键(全部实测): + * 1. Knot.init() 是 **public**,它做完 + * 「发现 mod → 建 KnotClassLoader → 应用 mixin → invokeEntrypoints(preLaunch)」 + * 后返回,**不**启动官方主循环(那在 GameProvider.launch() 里 —— 我们不调)。 + * 2. Knot.loadIntoTarget(name) 把我们的启动类加载进同一个 KnotClassLoader + * ⇒ 我们的代码与 mod 共享 classloader 与 mixin 变换,但控制权仍在我们手里。 + * + * 实测输出: + * [loaderboot] ✅ init 返回: net.fabricmc.loader.impl.launch.knot.KnotClassLoader + * [loaderboot] - mcfold_loadprobe 1.0 ← mod 被识别 + * [loaderboot] - minecraft 1.20.1 + * [loaderboot] ✅ 加载到: McfoldRenderServer / CL=KnotClassLoader + * {"event":"ready"} ← 我们的渲染服务同时就绪 + * + * ★ 两个必需条件(缺一则我们的 named 代码无法在加载器下运行): + * a) classpath 里放 **反混淆后的** client jar(client-named.jar),不是官方 obf jar + * b) -Dfabric.gameMappingNamespace=named + * 否则游戏被 remap 成 intermediary,堆栈里是 `enn.`, + * 我们的代码会抛 NoClassDefFoundError: net/minecraft/client/Minecraft(实测)。 + * + * 启动示例: + * java -cp "::client-named.jar:<我们的classes>" \ + * -Dfabric.gameMappingNamespace=named \ + * -Dfabric.modsFolder=/path/to/mods \ + * LoaderBoot --gameDir ... --assets ... --assetIndex 1.20.1 + * + * ★ asm 必须只留一份(用 Fabric 的 9.10.1):1.20.1 自带 9.2 与 9.6 两版, + * 与 Fabric 的并存会让 Knot. 抛 "duplicate ASM classes found on classpath"。 + */ +public class LoaderBoot { + + /** 我们自己的服务端主类(默认包,与 McfoldRenderServer 同名)。 */ + private static final String TARGET = "McfoldRenderServer"; + + public static void main(String[] args) throws Exception { + Knot knot = new Knot(EnvType.CLIENT); + + // ① 加载 mod + 应用 mixin;不起官方主循环 + ClassLoader gameCl = knot.init(args); + System.err.println("[loaderboot] KnotClassLoader = " + gameCl.getClass().getName()); + + // 报告已加载的 mod(诊断用;也是「加载器真的在工作」的证据) + try { + for (var m : net.fabricmc.loader.api.FabricLoader.getInstance().getAllMods()) { + System.err.println("[loaderboot] mod " + m.getMetadata().getId() + + " " + m.getMetadata().getVersion().getFriendlyString()); + } + } catch (Throwable t) { + System.err.println("[loaderboot] 读 mod 列表失败: " + t); + } + + // ② 把我们的服务端注入同一个 classloader,然后调用它的 main + Class boot = knot.loadIntoTarget(TARGET); + System.err.println("[loaderboot] 注入 " + boot.getName() + + " 到 " + boot.getClassLoader().getClass().getSimpleName()); + boot.getMethod("main", String[].class).invoke(null, (Object) args); + } +} diff --git a/tests/test_render_bridge.py b/tests/test_render_bridge.py index 7dc0a05..2c0052d 100644 --- a/tests/test_render_bridge.py +++ b/tests/test_render_bridge.py @@ -41,7 +41,8 @@ def _open_backend(): if not ok: return None, why br = Gen_1_20( - game_dir=str(STORE / "instances" / "default"), + # ★ 用测试专用实例目录,不碰生产 default(版本隔离策略) + game_dir=str(STORE / "instances" / "test-mcfold"), assets=str(STORE / "assets"), asset_index="1.20.1", ) try: @@ -184,6 +185,72 @@ def test_set_blocks_render(): br.stop() +def test_get_block_info(): + """get_block_info:方块名/属性/未加载区分(Java 侧回答)。 + + ★ 关键判据:**未加载的区块必须 known=false**,不能伪装成空气 + (ClientLevel.hasChunk() 恒 true、ChunkSource.hasChunk() 也恒 true, + 实测都不顶用;必须用 getChunk(..., requireChunk=false) 判 null)。 + """ + br, why = _open_backend() + if br is None: + print(f" - 跳过({why})") + return + try: + cap = br._request({"cmd": "capacity"}) + min_s = cap["minSection"] + stone, grass, log = br.resolve_blocks([ + "minecraft:stone", "minecraft:grass_block[snowy=false]", + "minecraft:oak_log[axis=y]", + ]) + assert stone > 0 and grass > 0 and log > 0 + br.set_viewport(0, 0, radius=12) + + sections = {} + for idx in range(cap["sectionsCount"]): + v = [0] * SECTION_VOLUME + if idx < 4: + v = [stone] * SECTION_VOLUME + elif idx == 4: + v = [grass] * SECTION_VOLUME + elif idx == 5: + v[7 + 7 * 16] = log # x=7,z=7 + sections[idx] = v + for cx in range(-1, 2): + for cz in range(-1, 2): + br.set_blocks(cx, cz, sections) + + y_grass = (min_s + 4) * 16 + y_stone = (min_s + 2) * 16 + y_log = (min_s + 5) * 16 + + g = br.get_block_info(8, y_grass, 8) + assert g["known"] and g["block"] == "minecraft:grass_block", g + assert g["isAir"] is False and g["isSolid"] is True + assert g["properties"].get("snowy") == "false", g + assert g["isMissingTexture"] is False, g + assert g["fromMod"] is False + + st = br.get_block_info(8, y_stone, 8) + assert st["known"] and st["block"] == "minecraft:stone", st + + lg = br.get_block_info(7, y_log, 7) + assert lg["known"] and lg["block"] == "minecraft:oak_log", lg + assert lg["properties"].get("axis") == "y", lg # 属性必须读出来 + + air = br.get_block_info(8, 200, 8) + assert air["known"] and air["isAir"] is True, air + + # ★ 最重要的一条:未加载 ≠ 空气 + far = br.get_block_info(9999, 64, 9999) + assert far["known"] is False, f"未加载区块必须 known=false,得到 {far}" + assert far.get("reason") == "chunk_not_loaded", far + print(f" ✓ get_block_info({g['block']} 属性={g['properties']}, " + f"原木 axis={lg['properties'].get('axis')}, 未加载已区分)") + finally: + br.stop() + + def test_real_save_render(): """真实存档经新路径(RLE 路由)渲染 —— 最终验收。""" ok, why = _available()