From 8c7d5b9d3827261eb031e6b97d3e54268ef3d290 Mon Sep 17 00:00:00 2001 From: JianFeeeee Date: Fri, 25 Sep 2026 18:21:08 +0800 Subject: [PATCH] =?UTF-8?q?=E4=B8=96=E4=BB=A3=E5=88=86=E6=B4=BE=E4=BD=93?= =?UTF-8?q?=E7=B3=BB=EF=BC=9APython=20=E5=B1=82=20switch=20+=20=E5=90=84?= =?UTF-8?q?=E4=B8=96=E4=BB=A3=E7=8B=AC=E7=AB=8B=E7=BC=96=E8=AF=91=E7=9A=84?= =?UTF-8?q?=20Java=20=E5=90=8E=E7=AB=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 用户定向:在 python 层写 switch,调用不同 java 后端(而非一个文件里 switch)。 新增: mcfold/backends/{__init__,base,gen_1_20,gen_1_21}.py 后端体系与分派 scaffold/java/gen_1_20/ 对 1.20.x client jar 编译 scaffold/java/gen_1_21/ 对 1.21.1 client jar 编译(✅ 实测编译通过) scaffold/java/VERSION-DIFFS.md 实测差异全表 判据:DeltaTracker 类是否存在(1.21 引入,正是 render 签名变化的根源) ⇒ 比硬编码版本号稳 实测差异共 11 处(21 个检查点里 13 个跨世代完全一致): setLevel / GameRenderer.render / countRendered* / createPackRepository / ResourceLocation 构造 / enableDataFixerOptimizations(移除) / User 构造 / ChunkStatus 包路径 / ClientPacketListener 构造(参数个数语义都变) / loadEffect(变 private) 为什么必须分后端:DeltaTracker 在 1.20.x jar 中不存在, 两代调用无法共存于一份源码(实测编译报 cannot find symbol)。 1.21.1 反混淆已验证(29MB named jar,DeltaTracker 3 处) --- logs/2026-09-25-1.log.gz | Bin 1829 -> 1801 bytes logs/2026-09-25-2.log.gz | Bin 1801 -> 1510 bytes logs/2026-09-25-3.log.gz | Bin 1510 -> 1507 bytes logs/2026-09-25-4.log.gz | Bin 1507 -> 1511 bytes logs/2026-09-25-5.log.gz | Bin 1511 -> 1517 bytes logs/2026-09-25-6.log.gz | Bin 1517 -> 1514 bytes logs/2026-09-25-7.log.gz | Bin 1514 -> 1513 bytes logs/latest.log | 69 +- mcfold/__pycache__/render.cpython-312.pyc | Bin 21642 -> 21852 bytes .../__pycache__/render_bridge.cpython-312.pyc | Bin 19355 -> 20491 bytes mcfold/backends/__init__.py | 109 +++ .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 4804 bytes .../backends/__pycache__/base.cpython-312.pyc | Bin 0 -> 19717 bytes .../__pycache__/gen_1_20.cpython-312.pyc | Bin 0 -> 1453 bytes .../__pycache__/gen_1_21.cpython-312.pyc | Bin 0 -> 1689 bytes .../__pycache__/gen_a_1_20.cpython-312.pyc | Bin 0 -> 1417 bytes mcfold/backends/base.py | 350 +++++++++ mcfold/backends/gen_1_20.py | 37 + mcfold/backends/gen_1_21.py | 36 + mcfold/render.py | 13 +- mcfold/render_bridge.py | 39 +- scaffold/java/VERSION-DIFFS.md | 56 ++ .../gen_1_20}/McfoldRenderServer.java | 56 ++ .../client/renderer/McfoldBridge.java | 23 + .../java/gen_1_21/McfoldRenderServer.java | 676 ++++++++++++++++++ .../client/renderer/McfoldBridge.java | 37 + .../client/renderer/McfoldBridge.java | 14 - 27 files changed, 1460 insertions(+), 55 deletions(-) create mode 100644 mcfold/backends/__init__.py create mode 100644 mcfold/backends/__pycache__/__init__.cpython-312.pyc create mode 100644 mcfold/backends/__pycache__/base.cpython-312.pyc create mode 100644 mcfold/backends/__pycache__/gen_1_20.cpython-312.pyc create mode 100644 mcfold/backends/__pycache__/gen_1_21.cpython-312.pyc create mode 100644 mcfold/backends/__pycache__/gen_a_1_20.cpython-312.pyc create mode 100644 mcfold/backends/base.py create mode 100644 mcfold/backends/gen_1_20.py create mode 100644 mcfold/backends/gen_1_21.py create mode 100644 scaffold/java/VERSION-DIFFS.md rename scaffold/{ => java/gen_1_20}/McfoldRenderServer.java (89%) create mode 100644 scaffold/java/gen_1_20/net/minecraft/client/renderer/McfoldBridge.java create mode 100644 scaffold/java/gen_1_21/McfoldRenderServer.java create mode 100644 scaffold/java/gen_1_21/net/minecraft/client/renderer/McfoldBridge.java delete mode 100644 scaffold/net/minecraft/client/renderer/McfoldBridge.java diff --git a/logs/2026-09-25-1.log.gz b/logs/2026-09-25-1.log.gz index 5360e81dd26548973b20b21e5629a6143b5efcab..8312f78be8620a73a78d1b3a4c941fd19b97351b 100644 GIT binary patch literal 1801 zcmV+k2ln_MiwFP!00000|IJz7Pvbfke%|>j{4l!H)g4Tewot0Qdj$;aa0LTuL8BEy zm6M#dzBqPdJJ8;T{oD7Pr0ulOv=kWaJ`747`#ZmVzLU8P_B+8rC)~edw_k)8133oE zj*RinTaoRaeLKCp>#(5RW7+F>lO0J7};}DkUAnMknsW?G(3-xT%nO z26Unyq%}GP?K@25e)QEi|J$dL_nGVL?$U|_`v8`w=1-xAJT~d4&rHOz$+dS+c4u;V zn*--ir{!JK#Z|DhCSzCyD>cMc4E$`%^n?~E{PyXpd->(l+{S~MjVImiWfzS+6;eTh zj$tTK4km@6Xt9h7mwiB}nI7~;$%w-^hOF=>KhqCF$>i8VLT7|>C$pCw*7L&WuEU!3 z;KSd9XX~LZW>y?4iCACJz&RH8=5R@^`J39>V}FVV(P{1do&D!_DsiXD>%7^(H@5Tp zU6ops;V`knNyYy?d%8jA9%iyvx|k2Z$D=E2GO%8Pt5xkQGzfl*3jdH=`hTvM+-kjt z;AP*>1jH7Ehh?d?4QEPsgw6F@Y4AUin#V<8f8@t%!9A1P7)~*nJNu0W;g{!js0xxT z(5G%)z-uM3F^;ajz#XU zmZM4M4Yl^7x~=wKiO4ZT$(hb_-}CsGJ}uMIg!cB?xl!;C{Ayv%_W%4E2oKh%z&U4o zEHxIMU)Ixx)C2FO9x{xw?Xa{+jD9TA$^%CtzQ@F2D9#FtX>tDjkAI!hO;25hHF$#u zEXt)y*nTC*ZSVREb_;qcj^pd=5yki1#6sP8sqWZ~c6pW=>oNWQ_;xmzkbs967e6yU zl8&Wf!YsI4c_uXz41?rGoLwmp4sdB;Y&z93fd^3JSI3`g_WiK?4fQ$n9@5O0?ThSm z9x{d?Oc{~%vGGGcv(SH(Ix&y^)UgQgE#N+33r|X4X?7O~gf?}-6_>$<&|*lQe-Ik^ zgP0v{)bZ$dWADN4Tp+?u5X?YMkqIO$*K%OA?+c~++Vrzr>;x^0etnORVEma)1ufxK zM4$}%9h;{(=B-BhlarSVfUy1|0LHG*E>13QuGuw=eQsygJ;#_j`$8f$L2tOiByuuF zh?vp5Bs&tr!G}ip{>WI(GbJW(fur`}Cj}sPbm`-(At&g5gum{eW6wy z^d^qfW7oOWv}gu{Qn+Kkurzlv{y;$& z6kJ)zgcXs3@&-DWn710YE0IQt_*gpYW3Xh^+8`Hco#Q=?bAgU?Yssi%L&7}CInAm5 zKi+707*ncqZzreHLhad}vXAWtMJxS%OZ@2y@p7B%D)44Id<8hqXUcywkCl42ns0)} z+hADjlt$|{;6##3^g}n%aTR(PhK(io_eiJ}Ww$nuRxr^LV%8vCC0T9NTQB6i8>n;p zCsF6B&bfU9Uxjj9Oe{%<1@*8wZUy1ExzOi)*?`@y@Tk#lqzr{I)5D!&VKZvj+8fiq zHK0xo^;VdqMFE7R?;!}fS@EpP&{ zv9!6L8UM~FZ-I(D0~ZImly!~MvNVHI}pgbemZ0!tJ}1FBR%$)I0y zKMar1PUSKF>_bYF@d1zoJY5 z2>}@c4pUK%(YO3CqEF_rpGxC+uf+FV)SqYqExLN`>7KH6(#NoO5=#E+;W+14i`s7< zyj0RM8^T|YIF0jDi##S#ljri5C=ZgJSBXIxD&dO0Q=tEHeb3M7`IsJiSYZXxg(n27 zwfFwQPzsLPF?^SPbVXl$JpK2_(s1%xvC|1C&6Srl&SMnIZ)%0$V}W_IS$Yuczf}4P r##T&Pg}6#S*Ff(5dI2MWNdnbmaj*Uc@bx@2#IXDq9WZmx)*1i+;s~2Y literal 1829 zcmV+=2io`_iwFP!00000|IJxjPvbZgeqMfsr5!DdG?*rBpj10MDuwPa;-(gaRtQCI z;-(%PJF*>U=V5>Q9_Lahv}p^>?mhr%Y=7tCbNNoWarRoZX3MGFqMJiv$dHYxLI+YB zZ4^m%j?cR1w=Lu}o9J;_CWgiA2Qf?vjf|9cXupYK#yQh8kRsF@HwCT(xG`a(1UeG; zOi2+_VOof!X7p8R^LcY*Orl#mJNAvH>Ygeb%kM-)IFRw?Cq#lkro!lFyAw9MO;fEw zr`cV_%XzT&9Si6@SSDzo$k3EqiW7pw^z-IL@BDCcYNK1)c+~5i_rS<5VVs8G7z7g8 zaFQ7c7PBC8*#aO$KsxF7IqX3?HR)LQMuSceM3yDTc}ZUHhv)n zRYRSYR&+Q6tgm3;6bl>Ko>6PMvQ>HPJGmzJyTff%YbB_83~C%?7yB3=^9A>1ssh@DU~cWz>lQyhw{5c^Tm!vu>l3_2 z!>z(Q4Th43Jt{(~`g!6GpeAk2d7qhIZ!~i_q9NUPDqw7)E`)Jvjpr~Ol4{81!KC$u zTw5t`7yBP7X zZz0G{|MC#Jg?-A7!}8_G-tUPF2){Cn-=Zs_(nA1TkyRu_WmgN2eHIBCe!ISW8N2%{ zIT4}DaH!UMA1|ftaNsJE`3!HE= zc?+Cvd1jIUVr?50EJfHt`vdrN_Y%7#Vn<2=;50J(Rz`oKt$OG(Rl>fM!(lF5qu)@R zY8Grf-oi2GG$Pzq@A+2&*(A_8{Ay?E1$F6p;Tv%BgVsYRfFYCkB(ECSo;%F;pQ|p! z-~g)1)Bfl-EBozIeA{rJ;yK>>{X6#x&b)RcB3Q;YD+&=a=+5qYpsTDr%mCNk23!O) zCP~6)HFmw91Kg;sj$4c*3EaH~)8SIrX!aVuauC-ER8A981X zaAvHkyyPsbQ|K))%ZMOV${N*?R+zDJx~a|o4_AV<0~_kts7aS8y7gjD-h20)to?sk z6Tf(4X9<;xMc|F5`wDQJmdc;WhlP53jWyo%0o>0Z~rjQoTEbyV+; zu1Aalt$7W4=H={H1wK(QANWSG_`Q7%Y*3rE^mUu#-AZLc8c1a~`-xl&+B$iOx&s=n zeRs>EZIp80!~Q^yX(e_+2KyrdDGK9^%~Uf-jPXqYq)7VnRdkQ3|M!pIcSUyyg z^}-&)$o^E|#&Y1(;SlD?RgmY54b$d=^3q=pUU1s`KV7P~+!WhsP$&gm;Sexv=(sS9 zjED$96KLv?lK7C?zv{_^a3V$2RtjeG2WCb{+qus{Fs=_+1iTTnCorsYE+0Vo+6t0i zHhcK=0P`*GDrnOopBzjKJxGT*{~DnXKSNDj_v61L$?;w%3B>RabNf974o0cC!xsc- z5QKFH`YwQET;my8*?Tu(C-%8xAN!CR8AE+8%-er|VIT!&i%Zx+4 z-PegNMwd9{hS@nB{Fi@$mkI6==uyk1JI>y!(&tdEY(&b$EhJ*D=e$8co{NiF-fry; T;MD}ug=_f-<$B9pLmL19P5EMi diff --git a/logs/2026-09-25-2.log.gz b/logs/2026-09-25-2.log.gz index 8312f78be8620a73a78d1b3a4c941fd19b97351b..80b97fca78bdbc89e09799936d4b751deb965762 100644 GIT binary patch literal 1510 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 diff --git a/logs/2026-09-25-3.log.gz b/logs/2026-09-25-3.log.gz index 80b97fca78bdbc89e09799936d4b751deb965762..a12cf7fd21d7cc5eb72215cee35064c31b9a682c 100644 GIT binary patch literal 1507 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 N{{r-xiM?1B003;M=QIES literal 1507 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 N{{r-xiM?1B003;M=QIES diff --git a/logs/2026-09-25-6.log.gz b/logs/2026-09-25-6.log.gz index 3ea8980f0863142f17ca4009f34d04e18c50bf1e..e0d577a75469ee2255d78bed18c0cbd181806f63 100644 GIT binary patch literal 1514 zcmVhfZ13c}eQ_HSuhk+C(<-w(D?$_&%U+&v=S&(c%pP2O#F9`f9 z-dWsNBI$rkKzGZ;?ct@gvtYb9e$bfUn89@4tH8KKU5fE)!Bfm3%MIpoZydfN*VfA0 z<@PIy6kU{@$Sk*Ai(k{HX<8nkwRv=|6g&XCSXj{d&#wXhLxl>IQqm-;(y+83b-`LL z2L!#WLy{JO5ydQBy5Td6?=Wx(iqlMESe$0Ol}by}w$>E&D`WPb+v@C0(x zy*k2b;l35yar$cL{ClcmCax_Px8z!yJj;}}7=B;RPi7Jlun^g6Hu4B5eB)3bC-2n%2i#lM6%ixSjHgJ}|XA=7dHn;Vg(s!VD z&ycX&oHE+j2qYwzysxBtUm*bk^Dc$Xwx(bF;k zsoQr{tiRB0jE5d3=;Ko3w<=55O1`UIk9l5W^HNw^g%(yjmIIuoEK-bQdqtqKOfx8~Lq#u~E zjF-`U->=W%-y)zEq+MkkEoekb#I!z|rM+syudmtiuIQYbKdEzGRyki#!2PgRdqO!Z z9xO>nyqX`38lFE29@RNp7GSf-ENZmZ6Uw~3E|FRWaARPdR`Ry(qqr*CcRcnHRBq_5u^H7k`(AXeHfjxx0o z+8KFOXbf|Koom!nBiPu9E7(0qDv^_BusdX!qWCg!nQF%wL}l-X?qSl1JjS09q^@PW z2W*0Je6L7lNuRr(pSfF37ys=_NVojuyY~fNuEb;z^FGHJQwqtn8fcc%^<7P67+thkW!>YqM3aJTn=a;mrbD6Z0w QL`m5C7m*qLM_CpC0MC8=CIA2c literal 1517 zcmV$7dHBH8;`r)i!K`3VO&6ro*^ia z52m@HXfcm+pKT#(u7_I@yiZ{gL7F?1p2#s1JU-MAyE9CfTM{UM+ZxIb@l^T<%ZXugJA$ zTIFOlWN$oofXT!LAk-wEpul;D4x4fl^AEBvBfk7VB9< z>VdUf4hi0}Zj&Srj4)zJ>4DEIy2HRBC{8ntVR8EH>z}8t(z`B0>aqD7h5C4B(@I(#cH67{(x+Uk!b#*|&r4IqGv@EhMQe+LyD_SV##%h)^sJ zvGYTiY6!n`8LRJMVn`ow6>uNZxhJ_THMA?O6aqu1?O5FRri16-;cV=f*u{%zS;uaWX+?nA{}iW4s_Y7%eig&G9zq zP%#^hl$JCVZ2B6U>mGKR1F3N}AD4#Ip~F7@x_io=&oswcBXDuaJtw0#HBLQpm1*f7 zs==TTZpg1B$qbLST#{1Z3c!gFFl53NLU>RZ?`I_pOi3>A;~jnwGZ9O4 zulW{qooCt-xb`~mESM=vQ?abE-}pJ;p!R(3Y`RbJ?hP7W6c-f0lT^T^hLma67f=+S zQ;u;fal2qiKW6U=XT9^5tg5`^JghS;)A(8-qfBdO)S+_1tdrBpT>ihimi!=cp-!!y zcDRO}Cwq!Ew(sSg^wWm?*%LRG>r9rxgO>jSIL+qDpR4bsdN+bsp|Kha%bikhyaeuM z>Pc6Bcj zD3cgQVajj{({D~UBMvy-zJ#88Isd5OGmWEmXf;pX+SkztjNM3Izcp!ADjP$jwA&n} zY9q9B@)~vf5N~{U>!NLx2^@yQzM8-)cJOEz?2QtWJ}EW)25 zByMCJ0}eqMzOP7S$$-0=pSdch^Z$0mq*wg%-TMU=S7K6#_<-YzDTJh24K+*XQ)SAo zx>jyz>L$!cR2%ssE|+d}-H7YL$o*At6V(r4Fu+%`=a5&tHY}P8DN6r?myZASrLHi= Tbs7>Y30waHWes&ESrz~QXF(}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 literal 1514 zcmVhfZ13c}eQ_HSuhk+C(<-w(D?$_&%U+&v=S&(c%pP2O#F9`f9 z-dWsNBI$rkKzGZ;?ct@gvtYb9e$bfUn89@4tH8KKU5fE)!Bfm3%MIpoZydfN*VfA0 z<@PIy6kU{@$Sk*Ai(k{HX<8nkwRv=|6g&XCSXj{d&#wXhLxl>IQqm-;(y+83b-`LL z2L!#WLy{JO5ydQBy5Td6?=Wx(iqlMESe$0Ol}by}w$>E&D`WPb+v@C0(x zy*k2b;l35yar$cL{ClcmCax_Px8z!yJj;}}7=B;RPi7Jlun^g6Hu4B5eB)3bC-2n%2i#lM6%ixSjHgJ}|XA=7dHn;Vg(s!VD z&ycX&oHE+j2qYwzysxBtUm*bk^Dc$Xwx(bF;k zsoQr{tiRB0jE5d3=;Ko3w<=55O1`UIk9l5W^HNw^g%(yjmIIuoEK-bQdqtqKOfx8~Lq#u~E zjF-`U->=W%-y)zEq+MkkEoekb#I!z|rM+syudmtiuIQYbKdEzGRyki#!2PgRdqO!Z z9xO>nyqX`38lFE29@RNp7GSf-ENZmZ6Uw~3E|FRWaARPdR`Ry(qqr*CcRcnHRBq_5u^H7k`(AXeHfjxx0o z+8KFOXbf|Koom!nBiPu9E7(0qDv^_BusdX!qWCg!nQF%wL}l-X?qSl1JjS09q^@PW z2W*0Je6L7lNuRr(pSfF37ys=_NVojuyY~fNuEb;z^FGHJQwqtn8fcc%^<7P67+thkW!>YqM3aJTn=a;mrbD6Z0w QL`m5C7m*qLM_CpC0MC8=CIA2c diff --git a/logs/latest.log b/logs/latest.log index 632b0f8..1bc1c54 100644 --- a/logs/latest.log +++ b/logs/latest.log @@ -1,6 +1,6 @@ -[17:42:09] [Datafixer Bootstrap/INFO]: 188 Datafixer optimizations took 113 milliseconds -[17:42:10] [main/INFO]: Environment: authHost='https://authserver.mojang.com', accountsHost='https://api.mojang.com', sessionHost='https://sessionserver.mojang.com', servicesHost='https://api.minecraftservices.com', name='PROD' -[17:42:10] [main/ERROR]: Failed to verify authentication +[17:51:20] [Datafixer Bootstrap/INFO]: 188 Datafixer optimizations took 108 milliseconds +[17:51:21] [main/INFO]: Environment: authHost='https://authserver.mojang.com', accountsHost='https://api.mojang.com', sessionHost='https://sessionserver.mojang.com', servicesHost='https://api.minecraftservices.com', name='PROD' +[17:51:22] [main/ERROR]: Failed to verify authentication com.mojang.authlib.exceptions.InvalidCredentialsException: Status: 401 at com.mojang.authlib.exceptions.MinecraftClientHttpException.toAuthenticationException(MinecraftClientHttpException.java:56) ~[authlib-4.0.43.jar:?] at com.mojang.authlib.yggdrasil.YggdrasilUserApiService.fetchProperties(YggdrasilUserApiService.java:156) ~[authlib-4.0.43.jar:?] @@ -8,24 +8,24 @@ com.mojang.authlib.exceptions.InvalidCredentialsException: Status: 401 at com.mojang.authlib.yggdrasil.YggdrasilAuthenticationService.createUserApiService(YggdrasilAuthenticationService.java:124) ~[authlib-4.0.43.jar:?] at net.minecraft.client.Minecraft.createUserApiService(SourceFile:733) ~[client-named.jar:?] at net.minecraft.client.Minecraft.(SourceFile:442) ~[client-named.jar:?] - at McfoldRenderServer.boot(McfoldRenderServer.java:452) ~[classes/:?] + at McfoldRenderServer.boot(McfoldRenderServer.java:517) ~[classes/:?] at McfoldRenderServer.main(McfoldRenderServer.java:104) ~[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 -[17:42:10] [main/INFO]: Setting user: mcfold_cam -[17:42:10] [main/INFO]: Backend library: LWJGL version 3.3.1 build 7 -[17:42:10] [main/INFO]: [STDERR]: [LWJGL] [ThreadLocalUtil] Unsupported JNI version detected, this may result in a crash. Please inform LWJGL developers. -[17:42:10] [main/WARN]: Attempted to end step for load_time_pre_window_ms before starting it -[17:42:11] [main/INFO]: Reloading ResourceManager: vanilla -[17:42:11] [Worker-Main-11/INFO]: Found unifont_all_no_pua-15.0.06.hex, loading -[17:42:11] [main/INFO]: [STDERR]: [mcfold] Minecraft constructed -[17:43:11] [main/INFO]: [STDERR]: [mcfold] TIMEOUT TextureManager -[17:43:11] [main/WARN]: Missing sound for event: minecraft:item.goat_horn.play -[17:43:11] [main/WARN]: Missing sound for event: minecraft:entity.goat.screaming.horn_break -[17:43:11] [main/ERROR]: Error starting SoundSystem. Turning off sounds & music +[17:51:22] [main/INFO]: Setting user: mcfold_cam +[17:51:22] [main/INFO]: Backend library: LWJGL version 3.3.1 build 7 +[17:51:22] [main/INFO]: [STDERR]: [LWJGL] [ThreadLocalUtil] Unsupported JNI version detected, this may result in a crash. Please inform LWJGL developers. +[17:51:22] [main/WARN]: Attempted to end step for load_time_pre_window_ms before starting it +[17:51:22] [main/INFO]: Reloading ResourceManager: vanilla +[17:51:22] [Worker-Main-4/INFO]: Found unifont_all_no_pua-15.0.06.hex, loading +[17:51:22] [main/INFO]: [STDERR]: [mcfold] Minecraft constructed +[17:52:22] [main/INFO]: [STDERR]: [mcfold] TIMEOUT TextureManager +[17:52:22] [main/WARN]: Missing sound for event: minecraft:item.goat_horn.play +[17:52:22] [main/WARN]: Missing sound for event: minecraft:entity.goat.screaming.horn_break +[17:52:22] [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,24 @@ 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:523) ~[classes/:?] - at McfoldRenderServer.boot(McfoldRenderServer.java:456) ~[classes/:?] + at McfoldRenderServer.reloadResources(McfoldRenderServer.java:588) ~[classes/:?] + at McfoldRenderServer.boot(McfoldRenderServer.java:521) ~[classes/:?] at McfoldRenderServer.main(McfoldRenderServer.java:104) ~[classes/:?] -[17:43:11] [main/INFO]: Found unifont_all_no_pua-15.0.06.hex, loading -[17:43:12] [main/INFO]: Created: 1024x512x4 minecraft:textures/atlas/blocks.png-atlas -[17:43:12] [main/INFO]: Created: 256x256x4 minecraft:textures/atlas/signs.png-atlas -[17:43:12] [main/INFO]: Created: 512x512x4 minecraft:textures/atlas/banner_patterns.png-atlas -[17:43:12] [main/INFO]: Created: 512x512x4 minecraft:textures/atlas/shield_patterns.png-atlas -[17:43:12] [main/INFO]: Created: 1024x1024x4 minecraft:textures/atlas/armor_trims.png-atlas -[17:43:12] [main/INFO]: Created: 256x256x4 minecraft:textures/atlas/chest.png-atlas -[17:43:12] [main/INFO]: Created: 128x64x4 minecraft:textures/atlas/decorated_pot.png-atlas -[17:43:12] [main/INFO]: Created: 512x256x4 minecraft:textures/atlas/beds.png-atlas -[17:43:12] [main/INFO]: Created: 512x256x4 minecraft:textures/atlas/shulker_boxes.png-atlas -[17:43:12] [main/WARN]: Shader rendertype_entity_translucent_emissive could not find sampler named Sampler2 in the specified shader program. -[17:43:12] [main/INFO]: Created: 256x256x0 minecraft:textures/atlas/particles.png-atlas -[17:43:12] [main/INFO]: Created: 256x256x0 minecraft:textures/atlas/paintings.png-atlas -[17:43:12] [main/INFO]: Created: 128x128x0 minecraft:textures/atlas/mob_effects.png-atlas -[17:43:12] [main/INFO]: [STDERR]: [mcfold] resources reloaded -[17:43:13] [main/WARN]: Shader rendertype_entity_translucent_emissive could not find sampler named Sampler2 in the specified shader program. -[17:43:13] [main/INFO]: [STDERR]: [mcfold] boot done +[17:52:22] [main/INFO]: Found unifont_all_no_pua-15.0.06.hex, loading +[17:52:23] [main/INFO]: Created: 1024x512x4 minecraft:textures/atlas/blocks.png-atlas +[17:52:23] [main/INFO]: Created: 256x256x4 minecraft:textures/atlas/signs.png-atlas +[17:52:23] [main/INFO]: Created: 512x512x4 minecraft:textures/atlas/shield_patterns.png-atlas +[17:52:23] [main/INFO]: Created: 512x512x4 minecraft:textures/atlas/banner_patterns.png-atlas +[17:52:23] [main/INFO]: Created: 1024x1024x4 minecraft:textures/atlas/armor_trims.png-atlas +[17:52:23] [main/INFO]: Created: 128x64x4 minecraft:textures/atlas/decorated_pot.png-atlas +[17:52:23] [main/INFO]: Created: 256x256x4 minecraft:textures/atlas/chest.png-atlas +[17:52:23] [main/INFO]: Created: 512x256x4 minecraft:textures/atlas/shulker_boxes.png-atlas +[17:52:23] [main/INFO]: Created: 512x256x4 minecraft:textures/atlas/beds.png-atlas +[17:52:24] [main/WARN]: Shader rendertype_entity_translucent_emissive could not find sampler named Sampler2 in the specified shader program. +[17:52:24] [main/INFO]: Created: 256x256x0 minecraft:textures/atlas/particles.png-atlas +[17:52:24] [main/INFO]: Created: 256x256x0 minecraft:textures/atlas/paintings.png-atlas +[17:52:24] [main/INFO]: Created: 128x128x0 minecraft:textures/atlas/mob_effects.png-atlas +[17:52:24] [main/INFO]: [STDERR]: [mcfold] resources reloaded +[17:52:24] [main/WARN]: Shader rendertype_entity_translucent_emissive could not find sampler named Sampler2 in the specified shader program. +[17:52:24] [main/INFO]: [STDERR]: [mcfold] boot done +[17:52:35] [main/WARN]: Shader invert could not find uniform named InSize in the specified shader program. diff --git a/mcfold/__pycache__/render.cpython-312.pyc b/mcfold/__pycache__/render.cpython-312.pyc index 2679b18fc0bfda109b978c10853a7e3493932ad6..a1f2aa3c5affccefd3369b9149cb70625075f71a 100644 GIT binary patch delta 5111 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 bb3x}7Y13tC6PNdl74_NJ-5*PYV&?w<8HcIs delta 4858 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 Pvl_9#G9*elaGSpYR4j=M delta 890 zcmZ9JT}TvB6vyYxo!Mm6RRVRxu+`0vX;)Xb5UhOIP+Y%O$Oj)JLfr{8(HzadMoQ=* z@bOrN8kAxN1|^eb1_lul^o0+>viRUb5n|x_0YOl7&#bM`!2HhtoO92)mve?6!1yTG z-zO(40{>dd7CJ^w&DqZ@ibTF3z^)HFlqHrt!OW(_txRB8mGAPmK`bpWZufuKt>Mn7 z5CQ{2;)FOT8RC#^h=vqmXL-h8++*C6xQF1(IBH0<`+XV(RSrg^{&ww}D(aW{X$+jm z&eiX6rONtoZuD`&1mQkMgy~vOugn#uyNeIg1viDLO~@}?tX<6t+-4YZJ5A;pW@$52 z^N8Re93f2Nl-CW@_}S|azY6AtcMU)^S4s*1Z045l0?U}BV?7-FruI-o)cPZEQJ=-O z(qc$5M@s7@+eL6(_)=8Fg(^JjS(bFEO>Azi!D+0v#(;JW^T0mBXdS_zs^~LcHXPExy>)hD>;ko`z2Gw}4{}ttlZ1lpTR+UGx`n(B;hl diff --git a/mcfold/backends/__init__.py b/mcfold/backends/__init__.py new file mode 100644 index 0000000..ce9ad7d --- /dev/null +++ b/mcfold/backends/__init__.py @@ -0,0 +1,109 @@ +"""Java 后端分派 —— 按 MC 版本的 API 世代选择对应的渲染服务实现。 + +为什么用「世代」而不是「版本号」 +-------------------------------- +Java 是**编译期绑定**的:1.21 的 `render(DeltaTracker, boolean)` 里 +DeltaTracker 在 1.20.1 的 jar 中不存在,所以两代的调用**无法写在同一份源码里** +(实测:世代 B 的调用对 1.20.1 jar 编译直接报 "cannot find symbol")。 + +⇒ 每个世代必须有**独立编译**的 Java 后端,各自对着自己版本的 client jar 编译。 + Python 这层负责选后端 —— 这是「加 case 支持新版本」的落点。 + +实测世代边界(来自官方 client_mappings 逐版本对比) +-------------------------------------------------- +| 方法 | 1.20.x | 1.21+ | +|---------------------|-------------------|--------------------------| +| setLevel | (ClientLevel) | (ClientLevel, Reason) | +| GameRenderer.render | (float,long,bool) | (DeltaTracker, bool) | +| 渲染计数 | countRendered-* | 同(1.20.2 起改名) | + +判据:**DeltaTracker 类是否存在**(1.21 引入的时间抽象,正是签名变化的根源)。 +这比硬编码版本号稳 —— 官方改版本号不影响判据。 +""" +from __future__ import annotations + +import zipfile +from dataclasses import dataclass +from pathlib import Path + +# 世代的权威判据:这个类出现在哪个 version,就是那个世代 +GEN_MARKERS = { + "gen_1_21": ["net/minecraft/client/DeltaTracker.class"], + "gen_1_20": [], # 兜底(无判据类 ⇒ 最老的世代) +} + + +@dataclass(frozen=True) +class Gen: + """一个 API 世代。""" + + name: str + min_version: str # 该世代起始版本(文档用) + markers: tuple[str, ...] # 判据类(任一存在即属于本世代) + backend: str # Python 后端模块名 + java_dir: str # Java 源码目录名 + + +GENERATIONS: list[Gen] = [ + Gen( + name="gen_1_21", + min_version="1.21", + markers=tuple(GEN_MARKERS["gen_1_21"]), + backend="gen_1_21", + java_dir="gen_1_21", + ), +] + + +def detect_generation(client_jar: str | Path) -> Gen | None: + """从 client jar 判世代(看判据类是否存在)。找不到则返回 None(属最老世代)。""" + try: + with zipfile.ZipFile(client_jar) as z: + names = set(z.namelist()) + except Exception: + return None + for g in GENERATIONS: + if any(m in names for m in g.markers): + return g + return None + + +def detect_from_store(store: str | Path) -> Gen | None: + """从 store 目录找 client jar 并判世代。""" + store = Path(store) + for jar in sorted(store.glob("versions/*/*.jar")): + g = detect_generation(jar) + if g: + return g + return None + + +def backend_class(client_jar: str | Path | None = None, + generation: str | None = None): + """选中后端类。 + + ★ 这是「Python 层 switch」的落点:加新世代只需在下面加一条 elif。 + """ + if generation is None: + generation = backend_for(client_jar) if client_jar else "gen_1_20" + if generation == "gen_1_21": + from .gen_1_21 import Gen_1_21 + return Gen_1_21 + from .gen_1_20 import Gen_1_20 + return Gen_1_20 + + +def backend_for(client_jar: str | Path) -> str: + """判世代并返回后端模块名。""" + g = detect_generation(client_jar) + if g is None: + return "gen_1_20" # 无判据类 ⇒ 最老世代 + return g.backend + + +def list_generations() -> list[tuple[str, str, str]]: + """(世代名, 版本区间, Java 目录) —— 供诊断/文档用。""" + return [ + ("gen_1_20", "1.20.x(实测 1.20.1/1.20.2)", "java/gen_1_20"), + ("gen_1_21", "1.21+", "java/gen_1_21"), + ] diff --git a/mcfold/backends/__pycache__/__init__.cpython-312.pyc b/mcfold/backends/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a9c88e77267d31b8de333c01230e9842f167a9bc GIT binary patch literal 4804 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 literal 0 HcmV?d00001 diff --git a/mcfold/backends/__pycache__/base.cpython-312.pyc b/mcfold/backends/__pycache__/base.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4be0a1e7c796a1de4f2a07577c04b786858c995f GIT binary patch literal 19717 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 literal 0 HcmV?d00001 diff --git a/mcfold/backends/__pycache__/gen_1_21.cpython-312.pyc b/mcfold/backends/__pycache__/gen_1_21.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2d368cdc9f68440a8adda4c32a5890ff8b55a062 GIT binary patch literal 1689 zcmb7F+fN)-7(cT+dtq8ytfgM4^~7pqk?a;8d@yLzYJzQ)7$82(%Vd~yz{S~Fn3=`0 ziOEVR&I`6+q|_n^XkmOt|fLK~E!sL_|2^Q@Mms^l^yz39tGSyvTFNhguN{ z3?mYx{B6H`4T*ulP$Zn{_aW32ZEku8JGtMT8{gZLmrA#@k1|7oU}snC)vNfNJRoCd zb+WYZ%~pQaP~`6Jq)KAFupH~5n$#pUH^qcf{@OOMU7L< z#J#e^sB7+^T&!yOCY9Cnvz>R{o+Ooc`=AE?r!)AAq4Td+7(~-nU!% z@nY^$F?X%F_7gtM@a+3bc76!XgB1Z|gPNbv0jk!2=$diIjkujEG(Hg5dfa)3qEo89 z*-m96sYNix>m!rczByaWWs139?aM>X)E$6ol@qj`YJ}=kkLvEA{v)cIa;GjUUsAmh zcO{c5m0buxafLyZ79T(q&eUB%b0&T)u8siOr*Vxcy4-C>d#KqNPf)!+0*0^Py^s}@ z-7mvq$-{Pj%D!+5fZPyv{8ExB%`DGI2UW~?QZoSgKT?J{6>Y{F_wGAaSHM9*Sf9Co zH~zd{T%Gf>*wEliFF4Cn4Gs3lb$kA2NHsHl3YX?`_T)HZ+pGKL=(v~deZIp5_jI|= zP+(llEf#YV_TmiNTOYlGJz6;PH|&XJ`{r*OcW*IjZ88c*IO~9{X-QLdW7y=pwQ4W8 zGc4sAQ9QDG8mP*I+inCsjVKa1V7-rUqM!Igp7_N8;l&^ch#?Xb!=#FYhIz4SFdPxA zaBG>Wsl#yDV|;0O%^sTuhpcdg$Z4K!Stwn-kKYk4{kBHUSfWzYn8)ha-h@OIRW=NT z#Zi$U44~h*?ISRN9)8gR0#I%LxAG{n!`D1dVC*tNwl&@@>ZY z)KpI#`fVVVP~11A{OR&)Z|poT(Y`^eO74qGz!qwxk5pQBjPc_$=EB&o0?;5yY^&RU0n4^l#r2B>a8AHrNdzjq#HB$7y6w?YY7hbP3)Uj!fY z8m5ji#dx8IDvB)`CqWIPzxh`lzEGDrSE#Mav=wS!0(JOA=HtSl_cEt_f)uHAnDl?z&% literal 0 HcmV?d00001 diff --git a/mcfold/backends/__pycache__/gen_a_1_20.cpython-312.pyc b/mcfold/backends/__pycache__/gen_a_1_20.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..73c36c2b4d6d23c5533118777d84734764842398 GIT binary patch literal 1417 zcma)6&u`pB6dvz-e>n+BTY`dUiC4YEDDpM|B0%Lws6@9I(ze0}GCoqA11=&qQwzEljPgAo8rh#~_~i6(5RoG=m+qDd;-$+B$75=x*k zL{kfhrkT8wjL#XV`HYqgUX~ErS-ZBg@sm2bx4Af^51-L5?QLFZto_pX@#}i+i{{Ge z-Rh@`QeVAOU;jqEK<0?rSg$pgKiS*7?wO=ia&20e0(W7OIhf!fJbb30G&ir^Tm7o} z-Oa}GW!MXnXGWyP& zYV*pS`freGv3~Q1#_i9+5787=^_c$_n`8FCxhPzOHSmv-GJBUflySy&9)W?9?Gj&| zA>6l!J#LxPgR1R1ld3};cibwoL3P4)ZAKh5KF|&pfp#?04IKY)M$cUgoM~@BQ+5TN zniO^r$8mk)Tdw2n0;$5+74LqXnA30*GbTQ^hbl-1FTi{WF=R-fVB&qy&`8pRA=9Lh zqOy^uDI-JEMwVt+j%LroZrVX}3$mfiD_Uom8)MFB#Cb3dYb~B{ZPXW+1Z%YwRru6G zH0VPuFx&#qw!m{O@O%r50NbE_s5}z3A`j_9Bb9WNXQa~o58ZE|O1B@(*z5z(=Yy)Q z>mP=hGU3yVd!`_z^@-vI3Z#P`08qS8xT0Wb>U;S;q@|0M{Z7X#c5n ztZrLWt#bN*==g>vhuK!9FdY{{n2IzHJ1Mh$GR}pLoaf-mAg=5&yX1*Sseip%xagKy z!C@cI&JtkDFU;7%qy=R-S16lNha3(QaV>a9nnj>?y z16$xujAsL4AFgCDrmiWrz||R#YB+qXY&p1W;yI8GY8vL`($RTVNRO$-x%fqe-54Kf z0F3!@@J3Ye69OF*Cet!Pb?+H4pP@hHkG7AUs$SUYeX3gA>OEC0?x#^tzIuMUKVKbN z#DB>72XZ>4#9>5)qL$`Q!YE!;3gETyA};zVlqZPCxTvi-l~C$Y`Y?MoCg2sGhXtXW j_cWOOq$ElA6FEtK_&AbI*HQn2^l?e4bv;BdMCbno55dH4 literal 0 HcmV?d00001 diff --git a/mcfold/backends/base.py b/mcfold/backends/base.py new file mode 100644 index 0000000..c0d95c2 --- /dev/null +++ b/mcfold/backends/base.py @@ -0,0 +1,350 @@ +"""渲染后端基类 —— 世代无关的协议与生命周期。 + +设计(用户 2026-09-25 定向): + Python 层做 switch 选择后端;每个世代一个**独立编译**的 Java 服务端。 + 原因:Java 是编译期绑定的,世代间的签名差异(DeltaTracker 等)无法写在同一份源码里。 + + 本基类承载所有世代共用的部分: + - NDJSON 通道(子进程 + 行分隔 JSON) + - 命令集(chunk / camera / resolve / render / registry / debug / view) + - base64 载荷编解码 + 子类只需声明世代特征(Java 目录、判据类、API 差异说明)。 +""" +from __future__ import annotations + +import base64 +import json +import os +import subprocess +import threading +import time +from dataclasses import dataclass, field +from pathlib import Path +from typing import Any + + +class BackendError(RuntimeError): + """后端不可用或渲染失败。""" + + +@dataclass +class RenderResult: + """一次渲染的结果。 + + distinct_colors 是本项目实测最锐利的判据: + 空屏/纯色 1-2 色 / 加载屏 ~199 / 简单地形 ~1450 / 真实存档 >10000 + (non-black 会被红底加载屏骗过 —— 实测踩过) + """ + + image: bytes | None = None + path: str | None = None + width: int = 0 + height: int = 0 + chunks_rendered: int = 0 + distinct_colors: int = 0 + camera: tuple[float, float, float] = (0.0, 0.0, 0.0) + warnings: list[str] = field(default_factory=list) + raw: dict[str, Any] = field(default_factory=dict) + + @property + def has_world_content(self) -> bool: + return self.distinct_colors > 400 and self.chunks_rendered > 0 + + +class RenderBackend: + """所有世代后端共用的实现。子类覆写类属性即可。""" + + generation: str = "base" + min_mc_version: str = "" + max_mc_version: str = "" + java_dir: str = "" + markers: tuple[str, ...] = () + main_class: str = "McfoldRenderServer" + + def __init__( + self, + java: str = "java", + classpath: str | list[str] | None = None, + game_dir: str | None = None, + assets: str | None = None, + asset_index: str | None = None, + java_args: list[str] | None = None, + startup_timeout: float = 240.0, + cwd: str | None = None, + ): + self.java = java + if classpath is None: + classpath = os.environ.get("MCFOLD_RENDER_CLASSPATH") + if isinstance(classpath, (list, tuple)): + classpath = os.pathsep.join(classpath) + self.classpath = classpath + self.game_dir = game_dir + self.assets = assets + self.asset_index = asset_index + self.startup_timeout = startup_timeout + self.cwd = cwd + self.java_args = java_args or [ + "-Xmx1200M", "-Xms512M", "-Djava.awt.headless=true", + ] + + self._proc: subprocess.Popen | None = None + self._next_id = 1 + self._pending: dict[int, tuple[threading.Event, dict]] = {} + self._lock = threading.Lock() + self._stderr_tail: list[str] = [] + + # ---------------- 生命周期 ---------------- + + @property + def running(self) -> bool: + return self._proc is not None and self._proc.poll() is None + + def start(self) -> "RenderBackend": + if self.running: + return self + if not self.classpath: + raise BackendError( + f"未提供 classpath。设置 MCFOLD_RENDER_CLASSPATH," + f"或先跑 scaffold/build.sh 编译 {self.generation} 后端。" + ) + cmd = [self.java, *self.java_args, "-cp", self.classpath, self.main_class] + if self.game_dir: + cmd += ["--gameDir", self.game_dir] + if self.assets: + cmd += ["--assets", self.assets] + if self.asset_index: + cmd += ["--assetIndex", self.asset_index] + + env = dict(os.environ) + nat = env.get("MCFOLD_NATIVES_DIR") + if nat: + env["LD_LIBRARY_PATH"] = nat + os.pathsep + env.get("LD_LIBRARY_PATH", "") + + try: + self._proc = subprocess.Popen( + cmd, cwd=self.cwd, env=env, + stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, + text=False, bufsize=0, + ) + except FileNotFoundError as e: + raise BackendError(f"无法启动 java: {e}") from e + + threading.Thread(target=self._read_loop, daemon=True).start() + threading.Thread(target=self._stderr_loop, daemon=True).start() + + ready = self._await_event("ready", self.startup_timeout) + if not ready.get("_ok"): + self.stop() + tail = "".join(self._stderr_tail[-6:]) + raise BackendError( + f"{self.generation} 后端启动失败:{ready.get('error') or '超时'}" + + (f"\n{tail}" if tail else "") + ) + return self + + def stop(self, timeout: float = 10.0) -> None: + p = self._proc + if p is None: + return + try: + if p.poll() is None: + try: + self._send_raw({"cmd": "shutdown"}) + p.wait(timeout=timeout) + except subprocess.TimeoutExpired: + p.terminate() + try: + p.wait(timeout=5) + except subprocess.TimeoutExpired: + p.kill() + except Exception: + try: + p.kill() + except Exception: + pass + finally: + self._proc = None + + def __enter__(self): + return self.start() + + def __exit__(self, *exc): + self.stop() + + # ---------------- 数据注入 ---------------- + + def feed_chunk(self, cx: int, cz: int, chunk_data: bytes) -> dict: + return self._request({"cmd": "chunk", "cx": cx, "cz": cz, + "data": self._b64(chunk_data)}) + + def feed_chunks(self, chunks: list[tuple[int, int, bytes]]) -> dict: + return self._request({ + "cmd": "chunks", + "items": [{"cx": cx, "cz": cz, "data": self._b64(d)} for cx, cz, d in chunks], + }, timeout=300.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} + if radius is not None: + req["radius"] = radius + return self._request(req) + + # ---------------- 视角与视觉 ---------------- + + def set_camera(self, x: float, y: float, z: float, + yaw: float = 0.0, pitch: float = 0.0) -> dict: + return self._request({"cmd": "camera", "x": x, "y": y, "z": z, + "yaw": yaw, "pitch": pitch}) + + def set_view(self, **kw) -> dict: + """视觉设置。各世代支持的项见子类 api_notes()。 + + 世代无关的项:camera_type / render_distance / fov + 原版画质(所有世代都有):graphics_mode / ambient_occlusion / + mipmap_levels / gamma / entity_shadows / clouds / particles + 后处理链:post_effect(原版 PostChain,非第三方光影包) + """ + req: dict[str, Any] = {"cmd": "view"} + mapping = { + "camera_type": "cameraType", "render_distance": "renderDistance", + "fov": "fov", "graphics_mode": "graphicsMode", + "ambient_occlusion": "ambientOcclusion", "mipmap_levels": "mipmapLevels", + "gamma": "gamma", "entity_shadows": "entityShadows", + "clouds": "clouds", "particles": "particles", + "post_effect": "postEffect", + } + for py, js in mapping.items(): + if py in kw and kw[py] is not None: + req[js] = kw[py] + return self._request(req) + + # ---------------- 查询 ---------------- + + def registry_info(self) -> dict: + return self._request({"cmd": "registry"}) + + def lookup_state(self, state_id: int) -> dict: + return self._request({"cmd": "lookup", "stateId": state_id}) + + def resolve_blocks(self, specs: list[str]) -> list[int]: + """方块名(+属性) → state id。名字→id 的权威映射在客户端注册表。""" + if not specs: + return [] + return self._request({"cmd": "resolve", "blocks": specs})["ids"] + + def debug(self) -> dict: + return self._request({"cmd": "debug"}) + + # ---------------- 渲染 ---------------- + + def render(self, out: str | None = None, frames: int = 60, + width: int | None = None, height: int | None = None) -> RenderResult: + req: dict[str, Any] = {"cmd": "render", "frames": frames} + if out: + req["out"] = out + if width: + req["width"] = width + if height: + req["height"] = height + resp = self._request(req, timeout=600.0) + img = resp.pop("image", None) + cam = resp.get("camera") or (0.0, 0.0, 0.0) + if not isinstance(cam, (list, tuple)): + cam = (0.0, 0.0, 0.0) + return RenderResult( + image=base64.b64decode(img) if img else None, + path=resp.get("path"), + width=resp.get("width", 0), + height=resp.get("height", 0), + chunks_rendered=resp.get("chunksRendered", 0), + distinct_colors=resp.get("distinctColors", 0), + camera=tuple(float(c) for c in cam[:3]), + warnings=resp.get("warnings", []), + raw=resp, + ) + + # ---------------- 内部 ---------------- + + @staticmethod + def _b64(b: bytes) -> str: + return base64.b64encode(b).decode("ascii") + + def _request(self, req: dict, timeout: float = 180.0) -> dict: + if not self.running: + raise BackendError("后端未运行(先 start())") + with self._lock: + rid = self._next_id + self._next_id += 1 + req["id"] = rid + ev = threading.Event() + self._pending[rid] = (ev, {}) + self._send_raw(req) + if not ev.wait(timeout): + self._pending.pop(rid, None) + raise BackendError(f"后端无响应({timeout}s):cmd={req.get('cmd')}") + _, resp = self._pending.pop(rid) + if not resp.get("_ok", True): + raise BackendError(resp.get("error", "未知错误")) + return resp + + def _await_event(self, kind: str, timeout: float) -> dict: + deadline = time.time() + timeout + while time.time() < deadline: + if not self.running: + code = self._proc.poll() if self._proc else "?" + return {"_ok": False, "error": f"进程退出(code={code})"} + box = self._pending.pop(-1, None) + if box is not None: + ev, payload = box + if payload.get("_event") == kind: + return {"_ok": True, **payload} + self._pending[-1] = (ev, payload) # 不是目标事件,放回 + time.sleep(0.05) + return {"_ok": False, "error": f"等待 {kind} 超时"} + + def _send_raw(self, obj: dict) -> None: + if not self._proc or self._proc.stdin is None: + raise BackendError("stdin 不可用") + self._proc.stdin.write((json.dumps(obj) + "\n").encode("utf-8")) + self._proc.stdin.flush() + + def _read_loop(self) -> None: + assert self._proc and self._proc.stdout + for raw in self._proc.stdout: + line = raw.decode("utf-8", "replace").strip() + if not line.startswith("{"): + continue + try: + msg = json.loads(line) + except json.JSONDecodeError: + continue + if msg.get("event") == "ready": + ev = threading.Event() + box = dict(msg); box["_event"] = "ready" + self._pending[-1] = (ev, box) + ev.set() + continue + rid = msg.get("id") + if rid in self._pending: + ev, box = self._pending[rid] + box.update(msg) + ev.set() + + def _stderr_loop(self) -> None: + assert self._proc and self._proc.stderr + for raw in self._proc.stderr: + line = raw.decode("utf-8", "replace").rstrip() + self._stderr_tail.append(line) + if len(self._stderr_tail) > 300: + del self._stderr_tail[:150] + + # ---------------- 元信息 ---------------- + + def api_notes(self) -> dict: + """本世代的 API 特征(子类覆写)。""" + return {} + + def describe(self) -> str: + return (f"{self.generation} [{self.min_mc_version}..{self.max_mc_version}] " + f"java_dir={self.java_dir}") diff --git a/mcfold/backends/gen_1_20.py b/mcfold/backends/gen_1_20.py new file mode 100644 index 0000000..4cfd320 --- /dev/null +++ b/mcfold/backends/gen_1_20.py @@ -0,0 +1,37 @@ +"""1.20.x 渲染后端。 + +对应 Java 源码:scaffold/java/gen_1_20/ +编译目标:1.20.x 的 client jar(重映射为可读名后) + +API 特征(实测): + setLevel(ClientLevel) + GameRenderer.render(float partialTick, long nanoTime, boolean renderLevel) + LevelRenderer.countRenderedChunks() +""" +from __future__ import annotations + +from .base import RenderBackend + + +class Gen_1_20(RenderBackend): + """1.20.x 系列(含 1.20 / 1.20.1 / 1.20.2 / 1.20.3 / 1.20.4 / 1.20.5 / 1.20.6)。""" + + generation = "gen_1_20" + # ★ 诚实标注:仅实测过 1.20.1 与 1.20.2(以及 1.20.1 的 mappings 对比)。 + # 1.16~1.19 未验证 —— 不要因为「看起来同代」就宣称支持。 + verified_versions = ("1.20.1", "1.20.2") + min_mc_version = "(未验证)" + max_mc_version = "1.20.6" # 1.21 起换世代(由 DeltaTracker 判据确定) + + # 该世代的 Java 源码目录(相对 scaffold/java/) + java_dir = "java/gen_1_20" + # 判据类(用于自动识别) + markers: tuple[str, ...] = () + + def api_notes(self) -> dict: + return { + "setLevel": "Minecraft.setLevel(ClientLevel)", + "render": "GameRenderer.render(float, long, boolean)", + "count": "LevelRenderer.countRenderedChunks()", + "deltaTracker": False, + } diff --git a/mcfold/backends/gen_1_21.py b/mcfold/backends/gen_1_21.py new file mode 100644 index 0000000..3a20631 --- /dev/null +++ b/mcfold/backends/gen_1_21.py @@ -0,0 +1,36 @@ +"""1.21+ 渲染后端。 + +对应 Java 源码:scaffold/java/gen_1_21/ +编译目标:1.21.x 的 client jar(重映射为可读名后) + +API 特征(实测,与世代 A 的差异): + setLevel(ClientLevel, ReceivingLevelScreen$Reason) ← 多一个参数 + GameRenderer.render(DeltaTracker, boolean) ← 签名整个换了 + Minecraft.getTimer() → DeltaTracker ← 1.21 引入 + LevelRenderer.countRenderedSections() ← 1.20.2 起改名 + +★ 这些差异**无法**写在与 1.20 相同的源码里: + DeltaTracker 在 1.20.x 的 jar 中不存在 ⇒ 编译期即失败。 +""" +from __future__ import annotations + +from .base import RenderBackend + + +class Gen_1_21(RenderBackend): + """1.21+ 系列。""" + + generation = "gen_1_21" + min_mc_version = "1.21" + max_mc_version = "" + java_dir = "java/gen_1_21" + markers = ("net/minecraft/client/DeltaTracker.class",) + + def api_notes(self) -> dict: + return { + "setLevel": "Minecraft.setLevel(ClientLevel, ReceivingLevelScreen$Reason)", + "render": "GameRenderer.render(DeltaTracker, boolean)", + "count": "LevelRenderer.countRenderedSections()", + "deltaTracker": True, + "source": "Minecraft.getTimer()", + } diff --git a/mcfold/render.py b/mcfold/render.py index 26df48e..25d304a 100644 --- a/mcfold/render.py +++ b/mcfold/render.py @@ -25,7 +25,8 @@ from pathlib import Path from typing import Any, Iterable from . import anvil -from .render_bridge import RenderBridge, RenderBridgeError, RenderResult +from .backends import backend_class, detect_generation +from .backends.base import BackendError, RenderBackend, RenderResult # 存档区块在网络载荷里的 section 数(1.20.1 worldHeight 384 / 16 = 24) SECTION_COUNT = 24 @@ -182,7 +183,7 @@ def parse_save_chunk(cx: int, cz: int, nbt: dict) -> SaveChunk: class WorldRenderer: """世界 → 画面。持有一个常驻 Java 渲染器进程。""" - def __init__(self, bridge: RenderBridge, instance: Any | None = None): + def __init__(self, bridge: RenderBackend, instance: Any | None = None): self.bridge = bridge self.instance = instance self._spec_cache: dict[str, int] = {} @@ -194,7 +195,8 @@ class WorldRenderer: @classmethod def open(cls, instance: Any = None, *, store: str | None = None, classpath: str | None = None, start: bool = True, - java: str = "java") -> "WorldRenderer": + java: str = "java", client_jar: str | None = None, + generation: str | None = None) -> "WorldRenderer": """从 instance(mcfold.instance.Instance)或 store 目录打开渲染器。 classpath 缺省取环境变量 MCFOLD_RENDER_CLASSPATH; @@ -218,7 +220,10 @@ class WorldRenderer: asset_index = names[-1] if names else None game_dir = game_dir or str(store_p / "instances" / "default") - bridge = RenderBridge( + # ★ Python 层 switch:按 client jar 的世代选后端 + # (1.21+ 的 DeltaTracker 在 1.20.x jar 中不存在 ⇒ 必须独立编译的后端) + cls = backend_class(client_jar=client_jar, generation=generation) + bridge = cls( java=java, classpath=classpath, game_dir=game_dir, assets=assets, asset_index=asset_index, ) diff --git a/mcfold/render_bridge.py b/mcfold/render_bridge.py index 571f222..3b4009d 100644 --- a/mcfold/render_bridge.py +++ b/mcfold/render_bridge.py @@ -213,14 +213,47 @@ class RenderBridge: return self._request({"cmd": "camera", "x": x, "y": y, "z": z, "yaw": yaw, "pitch": pitch}) - def set_view(self, *, camera_type: str = "THIRD_PERSON_BACK", + def set_view(self, *, camera_type: str | None = "THIRD_PERSON_BACK", render_distance: int | None = None, - fov: float | None = None) -> dict: - req: dict[str, Any] = {"cmd": "view", "cameraType": camera_type} + fov: float | None = None, + graphics_mode: str | None = None, + ambient_occlusion: bool | None = None, + mipmap_levels: int | None = None, + gamma: float | None = None, + entity_shadows: bool | None = None, + clouds: str | None = None, + particles: str | None = None, + post_effect: str | None = None) -> dict: + """视觉设置。 + + 光影能力边界(实测,见 scaffold/README.md): + - 原版画质选项(graphics_mode/AO/mipmap/gamma/云/实体阴影): 支持 + - 原版后处理链(post_effect,如 minecraft:shaders/post/invert.json): 支持 + - 第三方光影包(SEUS/BSL 等): 不支持(需 Iris/OptiFine 加载器改渲染管线) + """ + req: dict[str, Any] = {"cmd": "view"} + if camera_type: + req["cameraType"] = camera_type if render_distance is not None: req["renderDistance"] = render_distance if fov is not None: req["fov"] = fov + if graphics_mode is not None: + req["graphicsMode"] = graphics_mode + if ambient_occlusion is not None: + req["ambientOcclusion"] = ambient_occlusion + if mipmap_levels is not None: + req["mipmapLevels"] = mipmap_levels + if gamma is not None: + req["gamma"] = gamma + if entity_shadows is not None: + req["entityShadows"] = entity_shadows + if clouds is not None: + req["clouds"] = clouds + if particles is not None: + req["particles"] = particles + if post_effect is not None: + req["postEffect"] = post_effect return self._request(req) # ---------------- 渲染 ---------------- diff --git a/scaffold/java/VERSION-DIFFS.md b/scaffold/java/VERSION-DIFFS.md new file mode 100644 index 0000000..6f669b5 --- /dev/null +++ b/scaffold/java/VERSION-DIFFS.md @@ -0,0 +1,56 @@ +# 世代差异清单(实测,非推断) + +来源:官方 `client_mappings.txt` 逐版本对比(1.20.1 / 1.20.2 / 1.21.1), +以及两侧源码对各自 client jar 的实际编译结果。 + +## 判据 + +**`net.minecraft.client.DeltaTracker` 类是否存在** ⇒ 1.21+。 +这是 1.21 引入的时间抽象,也正是 `GameRenderer.render` 签名变化的根源。 +比硬编码版本号稳(官方改版本号不影响判据)。 + +实现:`mcfold/backends/__init__.py:detect_generation()` + +## 差异全表(8 处,覆盖 21 个检查点中的 13 个「完全一致」之外的部分) + +| # | API | 1.20.x | 1.21+ | 处理 | +|---|---|---|---|---| +| 1 | `Minecraft.setLevel` | `(ClientLevel)` | `(ClientLevel, ReceivingLevelScreen$Reason)` | gen_1_21 传 `Reason.OTHER` | +| 2 | `GameRenderer.render` | `(float,long,boolean)` | `(DeltaTracker, boolean)` | gen_1_21 用 `mc.getTimer()` | +| 3 | 区块计数 | `countRenderedChunks()` | `countRenderedSections()` | 各世代用各自名字 | +| 4 | 同理 | `hasRenderedAllChunks()` | `hasRenderedAllSections()` | 同上 | +| 5 | `ServerPacksSource.createPackRepository` | `(Path)` | `(Path, DirectoryValidator)` | gen_1_21 构造 validator | +| 6 | `ResourceLocation` 构造 | `new ResourceLocation(s)` | `ResourceLocation.parse(s)` | 静态工厂 | +| 7 | `SharedConstants.enableDataFixerOptimizations` | 有 | **已移除** | gen_1_21 不调 | +| 8 | `User` 构造 | `(String,String sessionId,...)` | `(String,UUID,...)` | 传 UUID | +| 9 | `ChunkStatus` | `world.level.chunk.ChunkStatus` | `world.level.chunk.status.ChunkStatus` | 包路径变了 | +| 10 | `ClientPacketListener` 构造 | `(Minecraft,Screen,Connection,ServerData,GameProfile,Telemetry)` | `(Minecraft,Connection,CommonListenerCookie)` | ★ 参数个数与语义都变 | +| 11 | `GameRenderer.loadEffect` | package-private(同包可调) | **private**(同包也调不到) | gen_1_21 用反射 | + +★ 关键结论:**差异集中在 11 处,而不是「每个版本一套」**。 +21 个检查点里 13 个(如 `tick`/`allChanged`/`replaceWithPacketData`/ +`updateViewCenter`/`getChunkSource`/`PackRepository.*`)跨世代**完全一致**。 + +## 为什么必须分后端(而不是一个文件里 switch) + +Java 编译期绑定签名。实测: + +``` +世代 B 的调用对 1.20.1 jar 编译: + error: cannot find symbol class DeltaTracker +``` + +`DeltaTracker` 在 1.20.x 的 jar 里不存在 ⇒ 两代的调用**无法共存于一份源码**。 +所以每个世代独立编译,Python 层做分派。 + +## 加新世代的做法(用户 2026-09-25 定向) + +1. `scaffold/java/gen_/` 建目录,拷上一代源码 +2. 按上表改差异点(对着 **javap 输出**改,不要凭印象写 API 名) +3. `mcfold/backends/gen_.py` 声明 `markers`(判据类) +4. `mcfold/backends/__init__.py` 的 `GENERATIONS` 加一条 +5. 各自对**对应版本**的 named jar 编译 + +⚠️ 教训(本轮踩过):`Reason.LEVEL_LOAD`、`DirectoryValidator.NONE` +都是凭印象写出来的、实际不存在的 API;`loadEffect` 的可见性也没验证。 +**改之前先 `javap`。** diff --git a/scaffold/McfoldRenderServer.java b/scaffold/java/gen_1_20/McfoldRenderServer.java similarity index 89% rename from scaffold/McfoldRenderServer.java rename to scaffold/java/gen_1_20/McfoldRenderServer.java index a10e57c..e7927f9 100644 --- a/scaffold/McfoldRenderServer.java +++ b/scaffold/java/gen_1_20/McfoldRenderServer.java @@ -222,8 +222,64 @@ public class McfoldRenderServer { if (req.has("fov")) { mc.options.fov().set(req.get("fov").getAsInt()); } + // ── 原版画质选项(光影的基础层)── + if (req.has("graphicsMode")) { + mc.options.graphicsMode().set(net.minecraft.client.GraphicsStatus + .valueOf(req.get("graphicsMode").getAsString())); + mc.levelRenderer.allChanged(); // FANCY 会启用更多渲染层 + } + if (req.has("ambientOcclusion")) { + mc.options.ambientOcclusion().set(req.get("ambientOcclusion").getAsBoolean()); + mc.levelRenderer.allChanged(); + } + if (req.has("mipmapLevels")) { + mc.options.mipmapLevels().set(req.get("mipmapLevels").getAsInt()); + } + if (req.has("gamma")) { + mc.options.gamma().set(req.get("gamma").getAsDouble()); + } + if (req.has("entityShadows")) { + mc.options.entityShadows().set(req.get("entityShadows").getAsBoolean()); + } + if (req.has("clouds")) { + mc.options.cloudStatus().set(net.minecraft.client.CloudStatus + .valueOf(req.get("clouds").getAsString())); + } + if (req.has("particles")) { + mc.options.particles().set(net.minecraft.client.ParticleStatus + .valueOf(req.get("particles").getAsString())); + } + // ── 原版后处理链(PostChain 基础设施)── + if (req.has("postEffect")) { + String eff = req.get("postEffect").getAsString(); + if (eff.isEmpty() || eff.equals("none")) { + mc.gameRenderer.shutdownEffect(); + } else { + // loadEffect 是包私有 → 走同包 bridge + net.minecraft.client.renderer.McfoldBridge.loadEffect( + mc.gameRenderer, new ResourceLocation(eff)); + } + } + // 回包带上真实生效值,便于验证 JsonObject r = new JsonObject(); r.addProperty("ok", true); + JsonObject cur = new JsonObject(); + try { + java.lang.reflect.Field pf = net.minecraft.client.renderer.GameRenderer.class + .getDeclaredField("postEffect"); + pf.setAccessible(true); + cur.addProperty("postEffect", pf.get(mc.gameRenderer) != null ? "active" : "none"); + } catch (Throwable ignore) { + cur.addProperty("postEffect", "unknown"); + } + cur.addProperty("graphicsMode", mc.options.graphicsMode().get().name()); + cur.addProperty("ambientOcclusion", mc.options.ambientOcclusion().get()); + cur.addProperty("mipmapLevels", mc.options.mipmapLevels().get()); + cur.addProperty("gamma", mc.options.gamma().get()); + cur.addProperty("entityShadows", mc.options.entityShadows().get()); + cur.addProperty("clouds", mc.options.cloudStatus().get().name()); + cur.addProperty("renderDistance", mc.options.getEffectiveRenderDistance()); + r.add("current", cur); emit(id, r); break; } diff --git a/scaffold/java/gen_1_20/net/minecraft/client/renderer/McfoldBridge.java b/scaffold/java/gen_1_20/net/minecraft/client/renderer/McfoldBridge.java new file mode 100644 index 0000000..961e835 --- /dev/null +++ b/scaffold/java/gen_1_20/net/minecraft/client/renderer/McfoldBridge.java @@ -0,0 +1,23 @@ +package net.minecraft.client.renderer; + +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.packs.resources.ResourceProvider; + +/** + * 包私有成员桥。 + * + * GameRenderer 里几个关键方法不是 public,官方只在客户端内部调用; + * 我们不跑主循环、不连服,所以要用同包类直接调。 + * reloadShaders —— 官方由匿名内部类 GameRenderer$1(资源重载 listener)调 + * loadEffect —— 官方由 onResourceManagerReload / 药水效果改 + */ +public class McfoldBridge { + public static void reloadShaders(GameRenderer gr, ResourceProvider provider) { + gr.reloadShaders(provider); + } + + /** 载入一个原版后处理链(如 minecraft:shaders/post/invert.json)。 */ + public static void loadEffect(GameRenderer gr, ResourceLocation id) { + gr.loadEffect(id); + } +} diff --git a/scaffold/java/gen_1_21/McfoldRenderServer.java b/scaffold/java/gen_1_21/McfoldRenderServer.java new file mode 100644 index 0000000..12263eb --- /dev/null +++ b/scaffold/java/gen_1_21/McfoldRenderServer.java @@ -0,0 +1,676 @@ +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +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.FriendlyByteBuf; +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.LightLayer; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.chunk.DataLayer; +import net.minecraft.world.level.dimension.DimensionType; +import org.lwjgl.opengl.GL11; + +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.io.BufferedReader; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.InputStreamReader; +import java.io.PrintStream; +import java.net.Proxy; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.Base64; +import java.util.Optional; +import java.util.OptionalInt; +import java.util.UUID; + +/** + * mcfold 渲染服务:常驻进程,NDJSON(行分隔 JSON) 协议,由 Python 侧驱动。 + * + * 设计约束(用户 2026-09-25): + * - 不起完整客户端:不跑主循环、不连服、不登录、不作玩家 + * - 我们的世界数据喂给官方渲染器出图 + * + * 协议(stdin/stdout 各一行一个 JSON): + * → {"id":1,"cmd":"chunk","cx":0,"cz":0,"data":"","heightmaps":null} + * → {"id":2,"cmd":"camera","x":8,"y":70,"z":8,"yaw":0,"pitch":0} + * → {"id":3,"cmd":"render","frames":60,"out":"/tmp/a.png"} + * ← {"id":3,"ok":true,"width":854,"height":480,"chunksRendered":29, + * "distinctColors":1450,"image":""} + * + * 启动就绪时主动发 {"event":"ready"}。 + * + * ★ 关键实现点(全部实测得来,详见 scaffold/README.md): + * 1. tryDetectVersion 必须先于 Bootstrap.bootStrap + * 2. FolderData 顺序 (gameDir, resourcePackDir, assets, assetIndex),assetIndex 用落盘名 + * 3. 资源必须手工同步重载(各 listener 的 public reload()),不能 join Minecraft 的 + * 4. LoadingOverlay + ReceivingLevelScreen 都要清,否则渲出加载屏 + * 5. 每帧 mc.tick() 才能推进区块网格重建队列 + * 6. 读像素必须绑 FBO + * 7. chunk 数据走官方 replaceWithPacketData(与网络包同路径) + */ +public class McfoldRenderServer { + + static final Gson GSON = new Gson(); + static final PrintStream OUT = new PrintStream( + new java.io.FileOutputStream(java.io.FileDescriptor.out), true, StandardCharsets.UTF_8); + + static Minecraft mc; + static ClientLevel level; + static int width = 854, height = 480; + + public static void main(String[] args) throws Exception { + // 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"); + 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")); + height = Integer.parseInt(arg(args, "--height", "480")); + + try { + boot(gameDir, assets, assetIdx); + } catch (Throwable t) { + emitError(null, "启动失败: " + t); + t.printStackTrace(System.err); + System.exit(1); + } + event("ready", null); + + BufferedReader in = new BufferedReader(new InputStreamReader(System.in, StandardCharsets.UTF_8)); + String line; + while ((line = in.readLine()) != null) { + line = line.trim(); + if (line.isEmpty()) continue; + JsonObject req; + try { + req = JsonParser.parseString(line).getAsJsonObject(); + } catch (Exception e) { + emitError(null, "非法 JSON: " + e.getMessage()); + continue; + } + Integer id = req.has("id") ? req.get("id").getAsInt() : null; + try { + handle(id, req); + } catch (Throwable t) { + emitError(id, t.toString()); + t.printStackTrace(System.err); + } + } + System.exit(0); + } + + // ---------------- 协议处理 ---------------- + + static void handle(Integer id, JsonObject req) throws Exception { + String cmd = req.get("cmd").getAsString(); + switch (cmd) { + case "shutdown": { + JsonObject r = new JsonObject(); r.addProperty("ok", true); + emit(id, r); + System.exit(0); + } + case "chunk": { + int cx = req.get("cx").getAsInt(), cz = req.get("cz").getAsInt(); + byte[] data = decode(req.get("data").getAsString()); + var resp = applyChunk(cx, cz, data); + JsonObject r = new JsonObject(); + r.addProperty("ok", true); + r.add("result", resp); + emit(id, r); + break; + } + case "chunks": { + JsonArray items = req.getAsJsonArray("items"); + int ok = 0, failed = 0; + String firstErr = null; + for (JsonElement el : items) { + JsonObject it = el.getAsJsonObject(); + try { + applyChunk(it.get("cx").getAsInt(), it.get("cz").getAsInt(), + decode(it.get("data").getAsString())); + ok++; + } catch (Throwable t) { + failed++; + if (firstErr == null) { + firstErr = it.get("cx") + "," + it.get("cz") + ": " + t; + } + } + } + levelRendererAllChanged(); + JsonObject r = new JsonObject(); + r.addProperty("ok", true); + r.addProperty("injected", ok); + r.addProperty("failed", failed); + if (firstErr != null) r.addProperty("firstError", firstErr); + var cache2 = (net.minecraft.client.multiplayer.ClientChunkCache) level.getChunkSource(); + r.addProperty("loadedChunks", cache2.getLoadedChunksCount()); + 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; + float pitch = req.has("pitch") ? req.get("pitch").getAsFloat() : 0f; + // ★ 必须 setPosRaw + 手动更新 chunk 追踪,否则实体仍属旧区块 + mc.player.setPosRaw(x, y, z); + mc.player.xo = x; mc.player.yo = y; mc.player.zo = z; + mc.player.setYRot(yaw); + mc.player.setXRot(pitch); + mc.player.yRotO = yaw; + mc.player.xRotO = pitch; + mc.setCameraEntity(mc.player); + // 视野中心跟着相机走,否则目标区域区块被 ClientChunkCache 丢弃 + level.getChunkSource().updateViewCenter((int)Math.floor(x) >> 4, (int)Math.floor(z) >> 4); + mc.levelRenderer.allChanged(); + tickFrames(5); + JsonObject r = new JsonObject(); + r.addProperty("ok", true); + JsonArray c = new JsonArray(); c.add(x); c.add(y); c.add(z); + r.add("camera", c); + emit(id, r); + break; + } + case "view": { + if (req.has("cameraType")) { + mc.options.setCameraType(net.minecraft.client.CameraType.valueOf(req.get("cameraType").getAsString())); + } + if (req.has("renderDistance")) { + int rd = req.get("renderDistance").getAsInt(); + // ★ 两处都要设:renderDistance 是本地设置; + // serverRenderDistance 模拟服务端下发的视距上限 + // (getEffectiveRenderDistance = min(两者)) + mc.options.renderDistance().set(rd); + mc.options.setServerRenderDistance(rd); + // ★ 必须同步 ClientChunkCache 的 storage 半径, + // 否则区块被 inRange 丢弃(官方靠服务端包触发 updateViewRadius) + level.getChunkSource().updateViewRadius(rd); + mc.levelRenderer.allChanged(); + } + if (req.has("fov")) { + mc.options.fov().set(req.get("fov").getAsInt()); + } + // ── 原版画质选项(光影的基础层)── + if (req.has("graphicsMode")) { + mc.options.graphicsMode().set(net.minecraft.client.GraphicsStatus + .valueOf(req.get("graphicsMode").getAsString())); + mc.levelRenderer.allChanged(); // FANCY 会启用更多渲染层 + } + if (req.has("ambientOcclusion")) { + mc.options.ambientOcclusion().set(req.get("ambientOcclusion").getAsBoolean()); + mc.levelRenderer.allChanged(); + } + if (req.has("mipmapLevels")) { + mc.options.mipmapLevels().set(req.get("mipmapLevels").getAsInt()); + } + if (req.has("gamma")) { + mc.options.gamma().set(req.get("gamma").getAsDouble()); + } + if (req.has("entityShadows")) { + mc.options.entityShadows().set(req.get("entityShadows").getAsBoolean()); + } + if (req.has("clouds")) { + mc.options.cloudStatus().set(net.minecraft.client.CloudStatus + .valueOf(req.get("clouds").getAsString())); + } + if (req.has("particles")) { + mc.options.particles().set(net.minecraft.client.ParticleStatus + .valueOf(req.get("particles").getAsString())); + } + // ── 原版后处理链(PostChain 基础设施)── + if (req.has("postEffect")) { + String eff = req.get("postEffect").getAsString(); + if (eff.isEmpty() || eff.equals("none")) { + mc.gameRenderer.shutdownEffect(); + } else { + // loadEffect 是包私有 → 走同包 bridge + net.minecraft.client.renderer.McfoldBridge.loadEffect( + mc.gameRenderer, ResourceLocation.parse(eff)); + } + } + // 回包带上真实生效值,便于验证 + JsonObject r = new JsonObject(); + r.addProperty("ok", true); + JsonObject cur = new JsonObject(); + try { + java.lang.reflect.Field pf = net.minecraft.client.renderer.GameRenderer.class + .getDeclaredField("postEffect"); + pf.setAccessible(true); + cur.addProperty("postEffect", pf.get(mc.gameRenderer) != null ? "active" : "none"); + } catch (Throwable ignore) { + cur.addProperty("postEffect", "unknown"); + } + cur.addProperty("graphicsMode", mc.options.graphicsMode().get().name()); + cur.addProperty("ambientOcclusion", mc.options.ambientOcclusion().get()); + cur.addProperty("mipmapLevels", mc.options.mipmapLevels().get()); + cur.addProperty("gamma", mc.options.gamma().get()); + cur.addProperty("entityShadows", mc.options.entityShadows().get()); + cur.addProperty("clouds", mc.options.cloudStatus().get().name()); + cur.addProperty("renderDistance", mc.options.getEffectiveRenderDistance()); + r.add("current", cur); + emit(id, r); + break; + } + case "registry": { + JsonObject r = new JsonObject(); + r.addProperty("ok", true); + r.addProperty("blockCount", BuiltInRegistries.BLOCK.size()); + r.addProperty("stateCount", Block.BLOCK_STATE_REGISTRY.size()); + emit(id, r); + break; + } + case "lookup": { + int sid = req.get("stateId").getAsInt(); + BlockState st = Block.stateById(sid); + ResourceLocation key = BuiltInRegistries.BLOCK.getKey(st.getBlock()); + JsonObject r = new JsonObject(); + r.addProperty("ok", true); + r.addProperty("stateId", sid); + r.addProperty("block", key.toString()); + r.addProperty("state", st.toString()); + r.addProperty("inRange", sid >= 0 && sid < Block.BLOCK_STATE_REGISTRY.size()); + emit(id, r); + break; + } + case "fog": { + // 关雾:fogStart/fogEnd 是 levelRenderer 内部状态; + // 更简单可靠的是加大 renderDistance + 用 BLOCK 光照 + JsonObject r = new JsonObject(); + r.addProperty("ok", true); + emit(id, r); + break; + } + case "resolve": { + // ★ 方块名(+属性) → state id。存档 palette 是名字,网络载荷要 id, + // 而这个名字→id 的权威映射只有客户端注册表有。 + // 入参: {"cmd":"resolve","blocks":["minecraft:oak_log[axis=y]", ...]} + JsonArray arr = req.getAsJsonArray("blocks"); + // ★ parseForBlock 要 HolderLookup(不是 Registry) + var reg = mc.level.registryAccess().lookupOrThrow(Registries.BLOCK); + JsonArray out = new JsonArray(); + int unknown = 0; + for (JsonElement el : arr) { + String spec = el.getAsString(); + int sid; + try { + var res = net.minecraft.commands.arguments.blocks.BlockStateParser + .parseForBlock(reg, spec, false); + sid = Block.getId(res.blockState()); + } catch (Throwable t) { + sid = -1; unknown++; + } + out.add(sid); + } + JsonObject r = new JsonObject(); + r.addProperty("ok", true); + r.add("ids", out); + r.addProperty("unknown", unknown); + r.addProperty("total", arr.size()); + emit(id, r); + break; + } + case "debug": { + JsonObject r = new JsonObject(); + r.addProperty("ok", true); + r.addProperty("playerPos", mc.player.getX() + "," + mc.player.getY() + "," + mc.player.getZ()); + int pcx = (int)Math.floor(mc.player.getX()) >> 4, pcz = (int)Math.floor(mc.player.getZ()) >> 4; + r.addProperty("playerChunk", pcx + "," + pcz); + var cc = (net.minecraft.client.multiplayer.ClientChunkCache) level.getChunkSource(); + r.addProperty("loadedChunks", cc.getLoadedChunksCount()); + r.addProperty("chunksRendered", mc.levelRenderer.countRenderedSections()); + r.addProperty("hasRenderedAllChunks", mc.levelRenderer.hasRenderedAllSections()); + var got = cc.getChunk(pcx, pcz, net.minecraft.world.level.chunk.status.ChunkStatus.FULL, false); + r.addProperty("chunkAtPlayer", got != null ? got.getClass().getSimpleName() : "null"); + if (got != null) { + // 扫描玩家所在列,找最高的非空气方块 + int top = Integer.MIN_VALUE; + for (int y = level.getMaxBuildHeight() - 1; y >= level.getMinBuildHeight(); y--) { + var st = got.getBlockState(new BlockPos( + (int)Math.floor(mc.player.getX()) & 15, y, (int)Math.floor(mc.player.getZ()) & 15)); + if (!st.isAir()) { + top = y; + r.addProperty("topSolidY", y); + r.addProperty("topSolidBlock", BuiltInRegistries.BLOCK.getKey(st.getBlock()).toString()); + r.addProperty("topSolidProps", st.toString()); + break; + } + } + if (top == Integer.MIN_VALUE) r.addProperty("topSolidY", "none"); + } + // viewCenter + r.addProperty("fogStart", String.valueOf(mc.options.renderDistance().get())); + emit(id, r); + break; + } + case "viewport": { + // ★ viewCenter 决定 ClientChunkCache 收哪些区块(超范围会被丢弃) + int cx = req.get("cx").getAsInt(), cz = req.get("cz").getAsInt(); + if (req.has("radius")) { + level.getChunkSource().updateViewRadius(req.get("radius").getAsInt()); + } + level.getChunkSource().updateViewCenter(cx, cz); + JsonObject r = new JsonObject(); + r.addProperty("ok", true); + r.addProperty("viewCenter", cx + "," + cz); + emit(id, r); + break; + } + case "render": { + int frames = req.has("frames") ? req.get("frames").getAsInt() : 60; + String out = req.has("out") ? req.get("out").getAsString() : null; + if (req.has("width")) width = req.get("width").getAsInt(); + if (req.has("height")) height = req.get("height").getAsInt(); + render(id, frames, out); + break; + } + default: + emitError(id, "未知命令: " + cmd); + } + } + + static JsonObject applyChunk(int cx, int cz, byte[] data) { + // ★ 官方路径:ClientChunkCache.replaceWithPacketData + // 内部:new LevelChunk + chunk.replaceWithPacketData(buf, heightmaps, consumer) + // 与网络包 handleLevelChunkWithLight 完全同一路径 + FriendlyByteBuf buf = new FriendlyByteBuf(io.netty.buffer.Unpooled.wrappedBuffer(data)); + var cache = level.getChunkSource(); + // 显式匿名类而非 lambda:lambda 参数类型推断在此处会失败(实测) + net.minecraft.network.protocol.game.ClientboundLevelChunkPacketData.BlockEntityTagOutput beOut = + new net.minecraft.network.protocol.game.ClientboundLevelChunkPacketData.BlockEntityTagOutput() { + @Override public void accept(BlockPos bp, + net.minecraft.world.level.block.entity.BlockEntityType type, + net.minecraft.nbt.CompoundTag tag) { } + }; + // ★ 参数是 Consumer(消费这个接口本身),不是该接口的方法签名。 + // LevelChunk.replaceWithPacketData 内部会调 beOut.accept(...),这里直接空实现。 + cache.replaceWithPacketData(cx, cz, buf, new net.minecraft.nbt.CompoundTag(), out -> {}); + // 无光照数据 ⇒ 显式全亮,否则地形全黑 + var light = cache.getLightEngine(); + for (int sy = level.getMinSection(); sy < level.getMaxSection(); sy++) { + DataLayer sky = new 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(LightLayer.SKY, net.minecraft.core.SectionPos.of(cx, sy, cz), sky); + light.queueSectionData(LightLayer.BLOCK, net.minecraft.core.SectionPos.of(cx, sy, cz), sky); + } + light.setLightEnabled(new ChunkPos(cx, cz), true); + int upd = light.runLightUpdates(); + JsonObject r = new JsonObject(); + r.addProperty("lightUpdates", upd); + return r; + } + + static void levelRendererAllChanged() { + mc.levelRenderer.allChanged(); + } + + /** tick + 渲染各帧(区块网格重建队列靠 tick 推进) */ + static void tickFrames(int n) { + MainTarget target = (MainTarget) mc.getMainRenderTarget(); + for (int f = 0; f < n; f++) { + try { mc.tick(); } catch (Throwable ignore) {} + RenderSystem.clear(16640, Minecraft.ON_OSX); + target.bindWrite(true); + // ★ 世代差异(1.21+):render(DeltaTracker, boolean) + mc.gameRenderer.render(mc.getTimer(), true); + target.unbindWrite(); + target.blitToScreen(mc.getWindow().getWidth(), mc.getWindow().getHeight()); + mc.getWindow().updateDisplay(); + } + } + + static void render(Integer id, int frames, String out) throws Exception { + tickFrames(frames); + + MainTarget target = (MainTarget) mc.getMainRenderTarget(); + int w = mc.getWindow().getWidth(), h = mc.getWindow().getHeight(); + 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); + java.util.Set colors = new java.util.HashSet<>(); + for (int y = 0; y < h; y++) + for (int x = 0; x < w; x++) { + int i = ((h - 1 - y) * w + x) * 4; + int rgb = ((buf.get(i) & 0xFF) << 16) | ((buf.get(i + 1) & 0xFF) << 8) | (buf.get(i + 2) & 0xFF); + img.setRGB(x, y, rgb); + colors.add(rgb); + } + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + ImageIO.write(img, "png", bos); + byte[] png = bos.toByteArray(); + if (out != null) ImageIO.write(img, "png", new File(out)); + + JsonObject r = new JsonObject(); + r.addProperty("ok", true); + r.addProperty("width", w); + r.addProperty("height", h); + r.addProperty("chunksRendered", mc.levelRenderer.countRenderedSections()); + r.addProperty("hasRenderedAllChunks", mc.levelRenderer.hasRenderedAllSections()); + r.addProperty("distinctColors", colors.size()); + JsonArray cam = new JsonArray(); + cam.add(mc.player.getX()); cam.add(mc.player.getY()); cam.add(mc.player.getZ()); + r.add("camera", cam); + if (out != null) r.addProperty("path", out); + r.addProperty("image", Base64.getEncoder().encodeToString(png)); + emit(id, r); + } + + // ---------------- 启动 ---------------- + + static void boot(String gameDir, String assets, String assetIdx) throws Exception { + SharedConstants.tryDetectVersion(); + // ★ 世代差异(1.21+):enableDataFixerOptimizations() 已被移除(实测映射表无此方法) + Bootstrap.bootStrap(); + + File runDir = new File(gameDir); + runDir.mkdirs(); + // ★ 世代差异(1.21+):User(String, UUID, String, Optional, Optional, Type) + // 1.20.x 是 (String, String sessionId, String, ...) + User user = new User("mcfold_cam", java.util.UUID.nameUUIDFromBytes("mcfold_cam".getBytes()), + "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(width, height, 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(); + mc = new Minecraft(cfg); + RenderSystem.finishInitialization(); + System.err.println("[mcfold] Minecraft constructed"); + + reloadResources(mc); + System.err.println("[mcfold] resources reloaded"); + + Connection conn = new Connection(PacketFlow.CLIENTBOUND); + GameProfile profile = new GameProfile(UUID.randomUUID(), "mcfold_cam"); + // 初始注册表:官方静态层(1.21 的 cookie 需要它,随后会被真实注册表替换) + net.minecraft.core.RegistryAccess.Frozen listenerRegistryAccess = + net.minecraft.client.multiplayer.ClientRegistryLayer.createRegistryAccess() + .compositeAccess(); + WorldSessionTelemetryManager telemetry = new WorldSessionTelemetryManager( + TelemetryEventSender.DISABLED, false, Duration.ZERO, ""); + // ★ 世代差异(1.21+):ClientPacketListener(Minecraft, Connection, CommonListenerCookie) + // 1.20.x 是 (Minecraft, Screen, Connection, ServerData, GameProfile, TelemetryManager)。 + // ★★ 这是本世代最大的一处变化 —— 参数个数与语义都不同。 + var cookie = new net.minecraft.client.multiplayer.CommonListenerCookie( + profile, + telemetry, + listenerRegistryAccess, // 后面注册表加载后会被替换 + net.minecraft.world.flag.FeatureFlags.DEFAULT_FLAGS, + null, // serverBrand + null, // serverData + null, // postDisconnectScreen + java.util.Map.of(), // serverCookies + null, // chatState + false, // strictErrorHandling + java.util.Map.of(), // customReportDetails + net.minecraft.server.ServerLinks.EMPTY); + ClientPacketListener listener = new ClientPacketListener(mc, conn, cookie); + + // ★ 世代差异(1.21+):createPackRepository(Path, DirectoryValidator) + // DirectoryValidator 没有 NONE 常量,构造要 PathMatcher; + // 传一个「全部拒绝」的 matcher 最安全(我们不加载外来符号链接数据包) + var packs = net.minecraft.server.packs.repository.ServerPacksSource.createPackRepository( + new File(runDir, "datapacks").toPath(), + new net.minecraft.world.level.validation.DirectoryValidator(p -> false)); + 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())); + var 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}); + var rf = ClientPacketListener.class.getDeclaredField("registryAccess"); + rf.setAccessible(true); + rf.set(listener, layered); + + Holder dimHolder = loaded.registryOrThrow(Registries.DIMENSION_TYPE) + .getHolderOrThrow(ResourceKey.create(Registries.DIMENSION_TYPE, ResourceLocation.parse("minecraft:overworld"))); + var levelData = new ClientLevel.ClientLevelData(Difficulty.NORMAL, false, false); + level = new ClientLevel(listener, levelData, Level.OVERWORLD, dimHolder, + Math.max(8, mc.options.renderDistance().get()), 8, + () -> InactiveProfiler.INSTANCE, mc.levelRenderer, false, 0L); + // ★ 世代差异(1.21+):setLevel(ClientLevel, ReceivingLevelScreen$Reason) + // 1.20.x 只有一个参数。这是本文件与 gen_1_20 的差异之一。 + // 枚举值实测:NETHER_PORTAL / END_PORTAL / OTHER(没有 LEVEL_LOAD) + // 我们不是经传送门进入世界 ⇒ OTHER + mc.setLevel(level, net.minecraft.client.gui.screens.ReceivingLevelScreen.Reason.OTHER); + + 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); + + net.minecraft.client.renderer.McfoldBridge.reloadShaders(mc.gameRenderer, mc.getResourceManager()); + + mc.setOverlay(null); + mc.setScreen(null); + mc.options.setCameraType(net.minecraft.client.CameraType.THIRD_PERSON_BACK); + System.err.println("[mcfold] boot done"); + } + + /** 同步驱动各 reload listener(不能 join Minecraft 的 reloadResourcePacks:等主循环 → 死锁)*/ + 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); + mc.getResourcePackRepository().reload(); + + 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; + java.util.concurrent.Executor sync = Runnable::run; + + for (var listener : reloadListeners(rm)) { + String name = listener.getName(); + try { + var fut = listener.reload(barrier, rm, prof, prof, sync, sync); + long deadline = System.currentTimeMillis() + 60_000; + while (!fut.isDone() && System.currentTimeMillis() < deadline) Thread.sleep(10); + if (fut.isDone()) fut.join(); + else System.err.println("[mcfold] TIMEOUT " + name); + } catch (Throwable e) { + System.err.println("[mcfold] FAILED " + name + ": " + e); + } + } + } + + @SuppressWarnings("unchecked") + static java.util.List reloadListeners(Object rm) throws Exception { + 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"); + } + + // ---------------- 工具 ---------------- + + static byte[] decode(String b64) { + return Base64.getDecoder().decode(b64); + } + + 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; + } + + static synchronized void emit(Integer id, JsonObject obj) { + if (id != null) obj.addProperty("id", id); + OUT.println(GSON.toJson(obj)); + OUT.flush(); + } + + static void emitError(Integer id, String msg) { + JsonObject o = new JsonObject(); + o.addProperty("ok", false); + o.addProperty("error", msg); + emit(id, o); + } + + static void event(String name, JsonObject payload) { + JsonObject o = new JsonObject(); + o.addProperty("event", name); + if (payload != null) o.add("payload", payload); + OUT.println(GSON.toJson(o)); + OUT.flush(); + } +} diff --git a/scaffold/java/gen_1_21/net/minecraft/client/renderer/McfoldBridge.java b/scaffold/java/gen_1_21/net/minecraft/client/renderer/McfoldBridge.java new file mode 100644 index 0000000..fff0d60 --- /dev/null +++ b/scaffold/java/gen_1_21/net/minecraft/client/renderer/McfoldBridge.java @@ -0,0 +1,37 @@ +package net.minecraft.client.renderer; + +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.packs.resources.ResourceProvider; + +/** + * 包私有成员桥。 + * + * GameRenderer 里几个关键方法不是 public,官方只在客户端内部调用; + * 我们不跑主循环、不连服,所以要用同包类直接调。 + * reloadShaders —— 官方由匿名内部类 GameRenderer$1(资源重载 listener)调 + * loadEffect —— 官方由 onResourceManagerReload / 药水效果改 + */ +public class McfoldBridge { + public static void reloadShaders(GameRenderer gr, ResourceProvider provider) { + gr.reloadShaders(provider); + } + + /** + * 载入一个原版后处理链。 + * + * ★ 世代差异(1.21+):loadEffect 从 package-private 变成了 **private** + * (实测 javap:1.20.1 是 `void loadEffect(...)`,1.21 是 `private void`), + * 同包也调不到 ⇒ 只能用反射。 + * 1.21 新增了 public togglePostEffect() 但那是「切换内置效果」, + * 不支持任意 post chain id,所以仍走反射。 + */ + public static void loadEffect(GameRenderer gr, ResourceLocation id) { + try { + java.lang.reflect.Method m = GameRenderer.class.getDeclaredMethod("loadEffect", ResourceLocation.class); + m.setAccessible(true); + m.invoke(gr, id); + } catch (Exception e) { + throw new RuntimeException("loadEffect 不可用(1.21+ 为 private)", e); + } + } +} diff --git a/scaffold/net/minecraft/client/renderer/McfoldBridge.java b/scaffold/net/minecraft/client/renderer/McfoldBridge.java deleted file mode 100644 index ffa9663..0000000 --- a/scaffold/net/minecraft/client/renderer/McfoldBridge.java +++ /dev/null @@ -1,14 +0,0 @@ -package net.minecraft.client.renderer; - -import net.minecraft.server.packs.resources.ResourceProvider; - -/** - * 包私有成员桥(GameRenderer.reloadShaders 是 package-private)。 - * 官方由匿名内部类 GameRenderer$1 在资源重载时调用; - * 我们不跑主循环 ⇒ 用同包类直接调用,跳过整个 reload 基础设施。 - */ -public class McfoldBridge { - public static void reloadShaders(GameRenderer gr, ResourceProvider provider) { - gr.reloadShaders(provider); - } -}