From 16f2e04fff25c2463438a94d697f1e2d0e12c8f2 Mon Sep 17 00:00:00 2001 From: JianFeeeee Date: Fri, 25 Sep 2026 19:21:45 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9E=B6=E6=9E=84=E9=87=8D=E6=9E=84=EF=BC=9A?= =?UTF-8?q?=E5=B7=AE=E5=BC=82=E5=85=A8=E9=83=A8=E4=B8=8B=E7=A7=BB=E5=88=B0?= =?UTF-8?q?=20Java=EF=BC=8CPython=20=E5=8F=AA=E5=81=9A=E8=B7=AF=E7=94=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 用户口径:差异放在 Java 层,Python 相当于路由。 下移到 Java(新增 cmd=setblocks): Python 只发 RLE([[stateId,count],...]),Java 用官方 API 完成 调色板组装 / 位打包 / 官方序列化 / 入 cache / 渲染。 ★ 意外收益:走 ClientboundLevelChunkPacketData(chunk).getReadBuffer() + replaceWithPacketData(...) 这条官方路径,两代签名完全一致 ⇒ 原先的 Storage.chunks 反射注入**整段删掉**。 新增 cmd=capacity:世界尺寸(sectionsCount/minSection/defaultBiome) 由运行时回答,Python 不再硬编码 24/-4(1.16 时代是 16/0)。 Python 侧删除(防回归,见 test_import_surface): render_bridge.py(旧桥,无世代概念) render._pack/_long_array/_encode_paletted/_encode_chunk/_varint anvil.encode_block_section/encode_chunk_payload/save_chunk_to_payload 合计 454→374 行(render.py)、300→177 行(anvil.py) 保留在 Python(都是「不随版本变」的知识): RLE 压缩(rle_encode/rle_decode,具名可测) 存档读取(region/NBT) 方块名→id 的路由(查询在 Java) NDJSON 协议 测试:62 项单元 + 6 项渲染联测全绿 新测试反映新架构:RLE 边界、运行时容量、世代分派、新路径渲染 test_anvil 里原有 8 项协议位打包测试已删(那已下移到 Java, 由两代后端各自的编译验证覆盖) 新增 scaffold/ARCHITECTURE.md(分层原则 + 加新世代步骤 + 防回归清单) --- logs/2026-09-25-1.log.gz | Bin 1801 -> 1517 bytes logs/2026-09-25-2.log.gz | Bin 1510 -> 1514 bytes logs/2026-09-25-3.log.gz | Bin 1507 -> 1513 bytes logs/2026-09-25-4.log.gz | Bin 1511 -> 1539 bytes logs/2026-09-25-5.log.gz | Bin 1517 -> 1511 bytes logs/2026-09-25-6.log.gz | Bin 1514 -> 1511 bytes logs/2026-09-25-7.log.gz | Bin 1513 -> 1513 bytes logs/latest.log | 73 ++-- mcfold/__pycache__/anvil.cpython-312.pyc | Bin 16282 -> 10610 bytes mcfold/__pycache__/render.cpython-312.pyc | Bin 21866 -> 20885 bytes mcfold/anvil.py | 137 +------ .../backends/__pycache__/base.cpython-312.pyc | Bin 19717 -> 22084 bytes mcfold/backends/base.py | 42 ++ mcfold/render.py | 161 ++++---- mcfold/render_bridge.py | 386 ------------------ scaffold/ARCHITECTURE.md | 81 ++++ .../java/gen_1_20/McfoldRenderServer.java | 92 ++++- .../java/gen_1_21/McfoldRenderServer.java | 96 ++++- tests/test_anvil.py | 247 +++++------ tests/test_render_bridge.py | 234 ++++++----- 20 files changed, 687 insertions(+), 862 deletions(-) delete mode 100644 mcfold/render_bridge.py create mode 100644 scaffold/ARCHITECTURE.md diff --git a/logs/2026-09-25-1.log.gz b/logs/2026-09-25-1.log.gz index 8312f78be8620a73a78d1b3a4c941fd19b97351b..3ea8980f0863142f17ca4009f34d04e18c50bf1e 100644 GIT binary patch literal 1517 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 diff --git a/logs/2026-09-25-2.log.gz b/logs/2026-09-25-2.log.gz index 80b97fca78bdbc89e09799936d4b751deb965762..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 1510 zcmVS3$qx9asfa(m1y8}Kn`@>wZs ztY+!K=|$)Au0^WNCVALaspSd(!7NwO5UbQZIjoR`3&9P9N=9bqc8c3T+=OvC13Hl- zu9Zwc+7@A%9ez>9e%u{eo0`_afm<=4N1$n<{$?_!p-Oh&6BdRllh!;ro$|$F28=OwME+N!xU}s0BHzZ5olh1cVxx zg+-}*Ywi^8@ceSEB-n?frePk~hx}MBxTi85LI;z%by%x8{NmoetAc0?^r>4P;rYH- zhIbwgl!&?@BhbB~aQoPk_Li*Y=KJ+295Yz%2PH7BP*=iKEB;fMBbFJg<^H7gidx&K zZdd!SK%^L==v1be?OFVqJT240gy#0yxmIutcDXRW`JcA|??Z_Slv2_liPG@ASx*O2 z53J=fCKzSiB1xVYLCBJo7e2G_9utS4IL$Ps#o4#7f1bHc@1_i?(HgCie#S*a4p)NQ z_O6d{TDWidb=eIFslGGOC%h_oxqy#~TP%H{? z@I#Pl2)=U}sqaBzNFQ()@Ep^*C%Ii|b{8CoJJbb}H-mE~S?oMNViNfSTe$j7>3h)o z=ZLW1m|=8~2t*{4e4wNam=J+fL7K7sY8``LY0x2z-;=4FO#)EF{Eo~s9Me`J`^lV> z3xN7Y0EAqho}XOaT$5`U+f2`ln`1Pe`1+h9GeKvV+$85?43Hy6^NMV7j0TMrv%yel zNmIcluYq&Z!%lKQYFy99t(ZFKppU=qpJUHB&570sI1h5q$>>dtQ;%F{TDnCw7!<+{ z`IRJ@;o*)eQYu^nI28h7CR`(gM~(4;D`8?va)}@9JX5j6pZ1WxU_YQ1VK0feqGv?` zQg`n-KYyt^m=Ap{(8r_3YnG0#wfJs!J;tlnw;zRtS7`3F6B*++bs0Jpv4CD}yal?> zD{TR|@;cyYGE?V($uPy{j%*RT|_xtut)X_$-iNrnNKbNI7BF$?0US{@>k5-VR-=Q>!N(u3_)l znPQ9`R`W^vX-oX+ilgN^ldHh}ruPDHn$48IP~TVTUH4xFjolz#?UY*mCE$)D7wBy> zk>M(I&+}>v@Nbb&OUkY^kCqV80%A5F%}TCX_i7tb-Zjj*@spVIvr+Q$0loy~D1Wd- zEmEy`{F(}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 1507 zcmV<91swVxiwFP!00000|IL`)PvbZcz@Lx5!Vjy1lNQsYY2m8dTd_cIS@ES7v^pUa zImxv3;@FYxuh44QmW z${MR#wsU;eJ-_J?Z*Py>Ppi!Gl>cCsD`|*T>Xvv-kEC1(ZXi}NF&o!Ya1+5P<8nqi zl6PDynS!((!g4$Mtc?AzIkGl0ot+&wVnE-4rm6bIGE6g>ej7VNiN3@0j4pfriBGVZt2ECOf2Wnaxdy1PyQF7iLL4 z>U4IB1HqB?84oyTQLFYB)SAz1)o%O7?pUYQ{*C-|J(ai}&;|{fIHNlsZ>rRq42Owk zh70=l?AsL@cR%C((!_KCHXdDQm4UV#%vRO9LIdxosPG4u-2PcFxz&0L!Hc$^35Yc? z3yV^>R_rOw;Rp3vNwD`pP2<9`_wlh@a9`y*hAswkXSdmO{Bqy6%YtML`qZosctPOT z@lNA`5=jqa0=i!&ZiJW8){^n!_(5xmV+PZGrv~E^wG!hsg2$KxmK)6FXwrE_u01Pn ztL;}3DY_^*mRWB57Qd#C)3iLo-ultGQt%M$a$&*Ve|`=4zt^ZhDJ5-^Dh-tdsSnn2 zIV9+1-63fa7-7uP$_<}be2alYP@HBO!{X%Ym%mQjP4B7BL@1VoSotB$ zG=$%|Ow{)_8Blq$?}w(YerdKzsyg7JGY zRkXzIK1&}$E{{)-&aW=XC5&yZXNP;rnA-Z3V`qZKFoj0W#^@l|7|k=X!_gabsF)2$ zN=upvHhB%sRS!EUfYi7+~XeM}Q@1CmiEe z;&Q>#C}HnPW4-ehPF3sVBCK<~rtw)I<6LWZr~~DMStqBPyZnE5CHa2rLY-PY>2eKQ zPu7%WY}YF)>Blws(-GH}>s(gBgFXKRaGKAQKU3c;^=<{PLgQ^PtTsxs^%A)2kxTl% znaH?`?)!do0sj^OwIuCo<7i1ES|Vok(Ny-TEx-B9mUl(x-2O?Ob5-ShS^aH2q8tZ^$c47L>=@F9{q%WcuRxUnj_)KG~9a_!P_1OO2xLpfQAXeHf4l`xn*p2ZB%vwxm znI(`K+gVwj@%GH{t{CmDog?}zST(uh39pCP2KWxNAy zf^vMXNM*@@yPlu9TTU1M?Mg_${N=m%1zxViWDxTK#~D)!$+Q}3meS|@qfvdn%ypQt zSkG?H;&SO)*R?~_{nc<&6@@Su;FIh*TnPme$22DOG zWsTJ=+c`e#p5Jtc*KU*hX_Z-?@*m7{B@MAk-4d_SCMg$!8;F%m%*OQ;>>)U1T+T>G z@{VgIQ;@bpSZ+t3m9ZZ-N7iPhv$Nwy4Cp)1G*#c29MV{&o9_vWW0gy59<9#!;xYrq zpi+ygri06{Zj8sU49g|Nnhor1$#jQgDSX(x=$#*K&d=!2&Un=8o%isNUB(3@Xc>YM z`Cw8U6i>|K!e$$Yn(5(Y1dk|;W5^1F(qnnY1Wyh$ByJBA=4dwAA$`kiZaO4rcpJYk zOX^Xlvr`-hj;zmkz&VRrwZEX&d}gb5+c$Q{I<4K`$UoOpiQ55f(4dJky7Td-O0CIo zm}q9WpnuQ4U7>OJGu|&vOb1}&(S=qSXv@KDRjn&D@P3L4e{jj|pY@Vkt+x=oX#1If zSOc@LD0OSap3)qCP_LB)dmq#^E*yIwAIk;zRjy;`Vla31noY+q_iei@NYX9htA|ZQ~ zkn8^CAyy0bt=Nv!mm}xj6BRRYWx2Q^SJLEJrnJTI`*LB5rSRvxsO_3 zL2sWTVLO;Hy4VOLB$s@kqz#!6p;Td(vu&@1&Ts6}5scrHshsCHp!E3-nMXK=t;F^x zcSbG&{<8ptTpphuonKv&OBmZ+&kpyLF}3w6$Ib+eVTvO;8>53Hjm4o7d$p<*^1 zDJ^Lx*yJ@hS3T^c08-=be4H9mhYlnBb^DY(>uHX(M&R7ZeJ7(oF-|>lnQQ3=)nHHx zH{@56=7z@`E=j3y7r?O)Fl54Agz(^EyziATFeN$1kNqN2F~uL>A(3D|pqb!Zl5j;& z$^@it-(j)-Tz4@ZB23VSrN(d9madh2SGykayw>`qu(S#-tac=aI89xIPDL!xz1_E< z>mt(@z>U{|r@>5FmWf4;{npO`2aV@*r`>&qzBhRI@^V1|Jk11LXvmmmk%007I^h_% z5|;~>MhSaY8ta|6aH?7-7h#>_HI2^#8RuHNLmen5%sM&U+~xnfE6Mj`7wXjNNtbKb zda|Z0V|!jvNk6X1pN_b;T<5Y19<==zz-c~H{!D$Z)VmeD3XQkHu-Yii)=S{7M=t66 zW+LM%y6^kV1^inC)RMHTjiV)vXo;BBM^oMMw%gBadFwl8-}_0Ob5-ShK>KS^NGRvVOb)k;fz?#MwL5lyHBiHcaxJD)a)qL1 z!~y%yjA0k1-<%#Xi9z}zdST__qlV8kw%Vc9JYA3Nx5vA+-~?i&-QqA)_Kn>bkHDrC!gRt!V&^=5Vk;nKmgw(Z+ zcYsY$j_(zzEE#au^D}qL>Egd#3F()=eD}V<%axc6Vm{zFV@e^JRzuBF`c#=Je>AGk zm$?oz7EA5+Ixd&4bzO_=Leu@#a8nh9Fc{#I>^bBm&xUz%A?4No;L-8FcIuL&xJpA3 pC1JLy*;;-AF0pdh#~Kzj0bi@7hR3qcf52+}{sqmbeF(%B008QM2BQD~ literal 1511 zcmVV)b!)4KSt27DQRJjQfRjVi( znVAE)@z^8VL!l4*+xK{8AR&PPl2p47LEy`Gj*rjHxve(awPw3oyCb*9%(4NWfF_@m zvc_tb9GqTsF7Mi;+G>%9X_Z(W^B>G|B@MAk-IJC_VlD(X5Gom&o!f%zBR6JT&Oj$} z%(ap+NZTeXwWH6<*bloSYZKExIB+8d^cXaa)i)-GG*t2Kd&0s{rP7)ws}sJs%z!bd z)8eXPa2eK(@er0_xr9)&ft@WWc1RY(huy308Ttu!J{J$k=w(BIhjqiNzXEynl|w()tz6MCFQ8| z*(r_$N7iRN;G9LRI$Tg|KC@T4?He1jcD?Z%`RBHfxbM>n^=mkz+aK>%sWlxABh3sK z^zYfX8#L}=!h5-i=>Tjvy3#5EZ8?~|RqHYhsy{`AKe*)f&vME2`dbKIwEav#sDW8n zl)ATOPi_v+FV{+feQ;_TW*7V5AIk;zRH{Shpfk6dwVLDS`}SQFL|f1cvp(YazE{RO z4+ly_U62vzULLr8%t?Do#EJU@Z5i?N`LwMsd5^ zemRli6-B2qNo~*K*Z66e=0|94@11J}55X=M=C}UyG~oSSq5`FqG)Sy8tSm@9u$Idq z!CTgC5@(JPge+dU;S&q*(Qych(@djVoPGWB*O|-ouF8-atccUq{0#| z2_tw(W<<%A0A>J(21>_;juDJOIJ+A9T(hr--3!#`&{~KSo0l(lPGcb<2q8kTD8RxG zL82k}&Sj*&2eBc2z*)d`OlOwlcIC0V;2>^M7fhB6&Y5IGXZbOc*gvpMES6u=9hti% z`kBObC3RL=0DM*Y9`yb>a`qcDQU^PUh@_Gal(Yd8B9JObQnp{MqxV!AbOe+4q);@+ zwKmd+kn7X)lgpcHat#xk>e=C*GYVUubL>pe7$(!m*%&X#HAeG@Y;(K~I#kRCBc&xx z1e?AF=c10)|YuL|=mFE;U}Ov~;cIyV~`bSFLYf3UjN_%xWich||{mAD7TtYo=}dm2TRl@ z)r#j&>Q!&zKdN&!FTm!2S=8vDFO<8+L=N|}f%R0c-k7+*5~$%rxfRnXxlU0g(T#$H zVHc*~oNh)Ou)BQ;J+pH5QNm{$TkXJV9>2A%!x0#}mA+nU(kN9nf>3Fh>Yp`tH_w+b9*-%LaWlfl_wS4E9D0Llj>IE>i6@fgtbw@OqduA`S6p0I_Qs$AC>x zitiPvEE#au^D~#_boSq_i1hMbo_k+lawR5#kPkS{m|RGr)ljpTZhqC3)dx*nhZ&0U zL%xX1rE6Wcb9;0n_gBJ=RX>2i0H0)=ke56g=Ea5Nsekr{!QIXO)1@}oAH`J~5-ABg N{{r-xiM?1B003;M=QIES diff --git a/logs/2026-09-25-5.log.gz b/logs/2026-09-25-5.log.gz index 3ea8980f0863142f17ca4009f34d04e18c50bf1e..cb7d06aa8e6fa292885e16843110fd96f91ec421 100644 GIT binary patch delta 1500 zcmV<21ta?H3+D@v9e)cY+Sw5UbcPW(wV=@op~y{6TMv#M*-odOhyCq)oV2BdHl@I5 z_kpU7?eF;9Pq_6C0JCJAe41jQy}Zur@Wp-ky76 zKo3FFM15znPa~CVzb7n;R3@!?v^(XC+YA_kms;F4dAJPgp798lVY!4zv!0!9nVgU; zfe+hP?aSlsLdX4};!SltX}2%isAP+A0Wn&JphVsq=Zd1nJj!jhg{bL0+=}2`3Zn?p z+@SPS4w>Nbk$;BRongY9Ocw*vvCL*BAhn9O^&7LK9Cbd`;z)30eL)3hENa!^!nI~A zJEh0Ivmp!WjX%ggw-brGHCmyyDz50@FS){>TypzYx#W8NEd(#xekvf+z<(?(O5Is=rZ9(JE5DTl`w-ML z${qURarW)&=QCI7U6&zMTBRQ8W?aN%eu`S(mk zOx##5?#PWaS(++sG5o$>oX#Y~U?IYX?};67Lw^!BCK^m8Ea8$cf=)66O0EPj1Nf+? zbTYYP3_}pku7*Cn*|)>?1>Wb-T1Zk`v@fTpv5*pk5TRHcV&{i2)e!#RGFCsr#E>rF zD&Rh*b4zl&QtdW4h}+Z#lNWK}E@17%J*Yl>37ES^&$t3S7 zX@5f|L?~65W^C81qxUNfI)Kr8G7c`H3DZ|?l>8pv43&bBiEUh?xE`S3gL$QPLj;(rVq8#OKEO9`*ieDSmJ%3c2 z>vOhfz-EtGywP4)DCfph_IL9K>#2UdF>-$;P{aFjBc{9Lx{ET2VHBne$A2*W=5#aQ zfaC2;=(&~ij}ku9IBSPi^JFu&-yAhc!7)TiyUk&$>>Il=8i3h|>5ROF*@bvx->r+b zK_+k-4!ddurR>Qt*cmWPQG7LUnQF%=gvIQK?qSl1EW)25ByMCJ0uDhLzOYDTNsqgs zpSdch^M7~6q*MIz-3ta5S5;zChX5Cd^2bEBPWWmu__3 zI5gc~2{%#Q5PCg)CwmTg$!o)`xsamt&%Jb1|4&Y>u2;o%8WJlBTmJ$O1^ruE761Sf C!|>Gr delta 1506 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 QL`m5C7m*qLM_CpC0MC8=CIA2c diff --git a/logs/2026-09-25-7.log.gz b/logs/2026-09-25-7.log.gz index 048ec35ee176f528e2f3c8a5d57486aa19758ffe..d8ad16be36cb60337d5b5df943b471a11efe7672 100644 GIT binary patch literal 1513 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 PO2XE^t|@n+S{48R8nXGN diff --git a/logs/latest.log b/logs/latest.log index 1bc1c54..3b6ae25 100644 --- a/logs/latest.log +++ b/logs/latest.log @@ -1,6 +1,6 @@ -[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 +[19:20:23] [Datafixer Bootstrap/INFO]: 188 Datafixer optimizations took 129 milliseconds +[19:20:24] [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' +[19:20:25] [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:517) ~[classes/:?] - at McfoldRenderServer.main(McfoldRenderServer.java:104) ~[classes/:?] + at McfoldRenderServer.boot(McfoldRenderServer.java:605) ~[classes/:?] + at McfoldRenderServer.main(McfoldRenderServer.java:106) ~[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: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 +[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 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,24 +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:588) ~[classes/:?] - at McfoldRenderServer.boot(McfoldRenderServer.java:521) ~[classes/:?] - at McfoldRenderServer.main(McfoldRenderServer.java:104) ~[classes/:?] -[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. + 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 diff --git a/mcfold/__pycache__/anvil.cpython-312.pyc b/mcfold/__pycache__/anvil.cpython-312.pyc index 93f1a3073bd3f834436583b4b86d10ca8ac6df09..dc2df99b3d3e5b915658e4837729f9e29a9755d4 100644 GIT binary patch delta 1347 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 diff --git a/mcfold/anvil.py b/mcfold/anvil.py index 1196039..1a7f9f8 100644 --- a/mcfold/anvil.py +++ b/mcfold/anvil.py @@ -1,16 +1,16 @@ -"""从 MC 存档(anvil region)读真实区块数据,交给渲染桥出图。 +"""从 MC 存档(anvil region)读真实区块数据。 为什么走存档而不走服务端: 用户口径是「只调用渲染能力,不参与游戏」——存档是**零网络、零登录、 零服务端**的真实世界数据源,且与官方客户端认同一套格式。 -数据流: - region/*.mca → 解压区块 NBT → NBT 里的 block_states/Heightmaps - → 重编码为 packet_map_chunk 载荷 → Java 渲染器 +★ 本模块只做**读取**(Python 的职责): + region/*.mca → 解压区块 NBT → 取出 Sections[].block_states(palette + data) -为什么要重编码:存档用 NBT(Sections[].block_states.palette/data), -网络包用扁平二进制(nonEmpty + bitsPerValue + palette + long[])。 -两者信息等价,格式不同。渲染器吃的是网络包格式(官方同一路径)。 + 编码/协议组装**不在这里** —— 那属于 Java 侧(用官方 API 完成), + 因为历代改过协议格式(位宽、palette 分支、LongArray 编码), + 在 Python 里复制一份就等于把版本差异搬回了 Python。 + 下游 render.py 只把读取结果压成 RLE,交给后端。 """ from __future__ import annotations @@ -175,126 +175,3 @@ def region_path(save_dir: Path, dim: str | None, rx: int, rz: int) -> Path: def list_region_files(save_dir: Path, dim: str | None = None) -> list[Path]: base = save_dir / (f"{dim}/region" if dim else "region") return sorted(base.glob("r.*.*.mca")) if base.is_dir() else [] - - -# ---------------------------------------------------------------- 存档区块 → 网络载荷 - -def _pack_long_bits(values: list[int], bits: int) -> list[int]: - """把 state id 列表按 MC 的位打包规则塞进 long(值不跨 long 边界)。""" - if bits == 0: - return [] - vpl = 64 // bits - mask = (1 << bits) - 1 - longs: list[int] = [] - cur = 0 - slot = 0 - for v in values: - cur |= (v & mask) << (slot * bits) - slot += 1 - if slot == vpl: - longs.append(cur); cur = 0; slot = 0 - if slot: - longs.append(cur) - return longs - - -def _varint(v: int) -> bytes: - out = bytearray() - while True: - b = v & 0x7F; v >>= 7 - if v: out.append(b | 0x80) - else: out.append(b); break - return bytes(out) - - -def _write_long_array(longs: list[int]) -> bytes: - """MC 的 LongArray 编码:varint 长度 + 大端 i64 ×N""" - out = _varint(len(longs)) - for l in longs: - # 视为无符号 64 位 - out += struct.pack(">q", l - (1 << 64) if l >= (1 << 63) else l) - return out - - -def encode_block_section(non_empty: int, state_ids: list[int] | None, - palette: list[int] | None) -> bytes: - """按网络包格式编码一个 block section。 - - state_ids=None 表示单值(palette 只有一个元素)。 - """ - out = struct.pack(">h", non_empty) - if state_ids is None: - out += bytes([0]) + _varint(palette[0]) + _varint(0) - return out - - uniq = sorted(set(state_ids)) - if len(uniq) == 1: - out += bytes([0]) + _varint(uniq[0]) + _varint(0) - return out - - # 本地调色板(indirect)。bits = ceil(log2(uniq)),至少 4 - bits = max(4, (len(uniq) - 1).bit_length()) - if bits > 8: - # 全局调色板:bits 直接放 state id - gbits = 15 - out += bytes([gbits]) + _write_long_array(_pack_long_bits(state_ids, gbits)) - return out - - index = {v: i for i, v in enumerate(uniq)} - out += bytes([bits]) - out += _varint(len(uniq)) - for v in uniq: - out += _varint(v) - out += _write_long_array(_pack_long_bits([index[v] for v in state_ids], bits)) - return out - - -def encode_chunk_payload(elevated_sections: dict[int, dict]) -> bytes: - """把存档区块的 sections 编成 packet_map_chunk 的 chunkData 部分。 - - elevated_sections: {section_y_index: {"states": [4096 int], "biomes": [64 int]}} - ★ 必须覆盖整个 worldHeight 的 section 数(1.20.1 = 24),缺失的补空气。 - """ - num_sections = 24 - out = bytearray() - for sy in range(num_sections): - sec = elevated_sections.get(sy) - out += encode_block_section(0, None, [0]) if sec is None else \ - encode_block_section(sec["nonEmpty"], sec["states"], None) - # biome section:64 个值(4x4x4) - biomes = sec["biomes"] if sec else [1] * 64 - uniq = sorted(set(biomes)) - if len(uniq) == 1: - out += bytes([0]) + _varint(uniq[0]) + _varint(0) - else: - bits = max(1, (len(uniq) - 1).bit_length()) - if bits > 3: - out += bytes([6]) + _write_long_array(_pack_long_bits(biomes, 6)) - else: - idx = {v: i for i, v in enumerate(uniq)} - out += bytes([bits]) + _varint(len(uniq)) - for v in uniq: - out += _varint(v) - out += _write_long_array(_pack_long_bits([idx[b] for b in biomes], bits)) - return bytes(out) - - -def save_chunk_to_payload(chunk_nbt: dict) -> bytes | None: - """存档区块 NBT → 网络载荷。""" - sections = chunk_nbt.get("sections") - if not isinstance(sections, list): - return None - by_y: dict[int, dict] = {} - for s in sections: - sy = s.get("Y") - if sy is None: - continue - bs = s.get("block_states") - if not isinstance(bs, dict): - continue - palette = bs.get("palette") or [] - # palette 元素是 {"Name": "minecraft:stone", "Properties": {...}} - # 需要 state id —— 这要靠客户端注册表(渲染器侧提供 lookup) - data = bs.get("data") - by_y[sy] = {"palette": palette, "data": data} - return by_y # 返回中间结构;state id 解析交给调用方(需注册表) diff --git a/mcfold/backends/__pycache__/base.cpython-312.pyc b/mcfold/backends/__pycache__/base.cpython-312.pyc index 4be0a1e7c796a1de4f2a07577c04b786858c995f..416311c8a04b8db5f69046f0237efc8330d5cff8 100644 GIT binary patch delta 6331 zcma)A4OA3Ymaf--Km!d3A}C6MpfvL1zXFP8?(Tr~9#GT#QdtY@oU}n!w zL4Wn?zI*Sx@4Z|1-dB5$lV6=9mfuE4n+3Q&+H`fBzM#XBL_Y4AUi}J=vk58h2KYB7(3vUNUofB+#SI>NM(E?Ivta;C<21X9@Og9^at1 zj=OyCr#hu$>G-Yfz3u6M;hc@M+C=xck=1APAd`-g75Ie6pz zU{{~7y%pKK{l4~Q-?gj$=JtU;um8f8`~9t4yC@Fy9rU%I9k_90;N}tW*=3^dVB64z z3xjXHGjwt9{r={mOFtQE`MLk(t^9$T2YnZA=KIbc8`{6icfD=+CpS^{L=RXuys!0s ze+$^`31}TW@s|JmX&hQpcVqX^={ zib{&1$fKHBl~g9x*d0`2PU zX=fb^I%jnhc8~ise(K$*n08&us`laoYdX_fo@-rmFDj;OP0N~2U1xFojyqAQ4BTKz{`pMKuJejYdHTw!zi%(A9QkIgNaL)HdSWZnAv=IPyd%n#C_lfH825@O6RX zyGfUDGBn68;?{xx>Rn*CFtVy5Z0@iZUBrLoz|h4b1AT{vF7EdqJr6s3-@~236$uhA ztX&}vAGzhb{!@`Zi)o8RKX}K9E)jQ9s&~SkGUQbCRZ@*=-iUj>o^>}o zvd!FLKD?}xC@b3+p0IbPb(Q}eZp6M&RoxJih%c|SqtL{cxylYrI3}c61;8NQ4`>dSEZDW&x&S4%j zxZ%46)i6Wjn5?XeNbYSjJSIlI4$2|ZVpA?k5ZMG~ML_M0 zkCTTjTv^pnRVh2!EZ(G*b|;&S&G1_0sFj$rfz3zGbOf9{&NE728Q4T9P;P~{n0QYH z&QJF`Q92B1%KnIrBun`sBGqH1F%a@2r6`(BV|N?Qf)HMtqv;G92Wi1Vb7(Fd2Rtz} zezyQ=A^|Xt=F#!cJC5elM7YP(nREi&6X+~DnY9gISCj#FD zxKDz6l84TxDbPr!3+QCHPlS#saG!*NbYV~&+66Qf6r@O#X(1Iy89Id)(M2>3`cmm) znhvreeTq(ndm4S3X23li?wN3(3c9DkJ%cWx)8U@kV6l>72f z2Y2G7_{eSJcC5z7Ck(`V$eu-5hwvQ23jnHFc5MOou}iY78rOpx)$l2(hV`r0uVCEB zxRsSv(mIE8rNdQ2F$m}xdqufCp@`y)!Tp_%TDAc-+5o^vTq{*?ucmApZ`If!BS?(l zgTZO5tdcR{1r|tq8Qnl=jDG>bj>MMQ?AxJ{Cp&G-xwChDQl)`-mFp>@3C&@WaxY~( zsZ+j9nLUT^%0ARfYGn-3pj1YGFM%p! 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 delta 4089 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 list[list[int]]: + """行程编码:[[值, 重复次数], ...]。 + + ★ 这是**格式无关**的压缩 —— 只是把连续相同的值合并成一段, + 不含任何 MC 协议知识(调色板/位宽/字节序都不在这里)。 + 真正的协议组装由 Java 侧用官方 API 完成。 + """ + out: list[list[int]] = [] + for v in values: + iv = int(v) + if out and out[-1][0] == iv: + out[-1][1] += 1 + else: + out.append([iv, 1]) + return out + + +def rle_decode(rle, expected: int | None = None) -> list[int]: + """rle_encode 的逆(测试与诊断用)。""" + out: list[int] = [] + for v, n in rle: + out.extend([int(v)] * int(n)) + if expected is not None and len(out) != expected: + raise ValueError(f"RLE 长度 {len(out)} != 期望 {expected}") + return out + + class BackendError(RuntimeError): """后端不可用或渲染失败。""" @@ -183,6 +210,21 @@ class RenderBackend: "items": [{"cx": cx, "cz": cz, "data": self._b64(d)} for cx, cz, d in chunks], }, timeout=300.0) + def set_blocks(self, cx: int, cz: int, sections: dict[int, list[int]]) -> dict: + """直接写方块(Python 只做路由,不碰协议格式)。 + + sections: {sectionIndex: [4096 个 state id]},索引顺序 (y,z,x) 线性化 + (与 MC 的 PalettedContainer 索引一致:x 最快、y 最慢)。 + + 为什么走这条路(用户 2026-09-25 定向): + 方块写入、调色板组装、位打包、入 cache 全在 Java 侧用官方 API 完成。 + Python 不复制历代变过的协议格式 —— 版本差异留在 Java。 + """ + items = [{"index": int(idx), "rle": rle_encode(ids)} + for idx, ids in sorted(sections.items())] + return self._request({"cmd": "setblocks", "cx": cx, "cz": cz, + "sections": items}, 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} diff --git a/mcfold/render.py b/mcfold/render.py index 6aad5dc..1e8b852 100644 --- a/mcfold/render.py +++ b/mcfold/render.py @@ -28,67 +28,18 @@ from . import anvil 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 -SECTION_VOLUME = 16 * 16 * 16 -BIOME_VOLUME = 4 * 4 * 4 -# ★ 最小 section 的 Y:1.20 起 worldHeight 384、minBuildHeight -64 ⇒ minSection = -4。 -# 网络载荷里的 section 索引是「从下往上数」(0..23),而存档 NBT 用的是绝对 Y(-4..19)。 -MIN_SECTION_Y = -4 - -# 位打包:把 palette 索引列表压进 long(值不跨 long 边界) -def _pack(values: list[int], bits: int) -> list[int]: - if bits <= 0: - return [] - vpl = 64 // bits - mask = (1 << bits) - 1 - out, cur, slot = [], 0, 0 - for v in values: - cur |= (v & mask) << (slot * bits) - slot += 1 - if slot == vpl: - out.append(cur); cur, slot = 0, 0 - if slot: - out.append(cur) - return out - - -def _varint(v: int) -> bytes: - out = bytearray() - while True: - b = v & 0x7F; v >>= 7 - if v: out.append(b | 0x80) - else: out.append(b); break - return bytes(out) - - -def _long_array(longs: list[int]) -> bytes: - out = _varint(len(longs)) - for l in longs: - out += (l & 0xFFFFFFFFFFFFFFFF).to_bytes(8, "big") - return out - - -def _encode_paletted(state_ids: list[int], volume: int, min_bits: int = 4, - global_bits: int = 15, max_local_bits: int = 8) -> bytes: - """编码一个 PalettedContainer 载荷(与 Java PalettedContainer.read 对齐)。""" - uniq = sorted(set(state_ids)) - if len(uniq) == 1: - return bytes([0]) + _varint(uniq[0]) + _varint(0) - - need = (len(uniq) - 1).bit_length() - if need > max_local_bits: - # 全局调色板:位宽直接放 state id - return bytes([global_bits]) + _long_array(_pack(state_ids, global_bits)) - - bits = max(min_bits, need) - idx = {v: i for i, v in enumerate(uniq)} - out = bytes([bits]) + _varint(len(uniq)) - for v in uniq: - out += _varint(v) - out += _long_array(_pack([idx[v] for v in state_ids], bits)) - return out +# ── 协议层常量(与版本无关,协议本身定义)── +SECTION_VOLUME = 16 * 16 * 16 # 一个 section 的方块数 +BIOME_VOLUME = 4 * 4 * 4 # 一个 biome section 的值数 +# ── 世界尺寸:★ 不硬编码,运行时问 Java ── +# 这些量随版本/世界类型变(1.20 起 worldHeight 384、minSection -4; +# 更早的版本是 256/-0)。Python 只做路由与协议编解码, +# 版本相关的量由后端回答(cmd="capacity")。 +# 下面三个仅在没有后端时的降级默认值。 +FALLBACK_SECTION_COUNT = 24 +FALLBACK_MIN_SECTION_Y = -4 +FALLBACK_DEFAULT_BIOME = "minecraft:plains" def _decode_storage(longs: Iterable[int], volume: int, bits: int) -> list[int]: """int[] 位数组 → 索引列表(不跨 long)。""" @@ -189,6 +140,8 @@ class WorldRenderer: self._spec_cache: dict[str, int] = {} self._loaded: set[tuple[int, int]] = set() self.chunks: dict[tuple[int, int], SaveChunk] = {} + self._capacity: dict[str, Any] | None = None + self._biome_id: int | None = None # ---------------- 构造 ---------------- @@ -232,8 +185,49 @@ class WorldRenderer: wr = cls(bridge, instance) if start: bridge.start() + wr.refresh_capacity() return wr + def refresh_capacity(self) -> dict[str, Any]: + """向 Java 后端询问世界尺寸(Python 不硬编码版本相关量)。""" + try: + self._capacity = self.bridge._request({"cmd": "capacity"}) + except Exception: + self._capacity = None + return self._capacity or {} + + @property + def sections_count(self) -> int: + if self._capacity and self._capacity.get("sectionsCount"): + return int(self._capacity["sectionsCount"]) + return FALLBACK_SECTION_COUNT + + @property + def min_section_y(self) -> int: + if self._capacity and "minSection" in self._capacity: + return int(self._capacity["minSection"]) + return FALLBACK_MIN_SECTION_Y + + @property + def _default_biome(self) -> int: + """默认生物群系 id —— 运行时向 Java 查(不硬编码 1)。""" + if self._biome_id is None: + name = (self._capacity or {}).get("defaultBiome") or FALLBACK_DEFAULT_BIOME + self._biome_id = self._resolve_biome_id(name) + return self._biome_id + + def _resolve_biome_id(self, name: str) -> int: + key = f"__biome__{name}" + if key in self._spec_cache: + return self._spec_cache[key] + try: + ids = self.bridge.resolve_blocks([name]) + bid = ids[0] if ids else 1 + except Exception: + bid = 1 + self._spec_cache[key] = bid + return bid + def close(self) -> None: self.bridge.stop() @@ -309,49 +303,40 @@ class WorldRenderer: }) def load_chunks(self, chunks: list[SaveChunk], meta: dict | None = None) -> dict: - """把已解析的区块编码并注入渲染器。""" + """把存档区块交给渲染器。 + + ★ Python 只做「方块名 → state id」的路由与 RLE 压缩; + 调色板组装、位打包、协议序列化、入 cache 全在 Java 侧用官方 API 完成。 + 这样历代改过的协议格式不需要在 Python 里复制一份。 + """ all_specs: list[str] = [] for c in chunks: all_specs.extend(c.palette_specs) id_map = self.resolve_specs(all_specs) unknown = sum(1 for v in id_map.values() if v < 0) - items: list[tuple[int, int, bytes]] = [] + n_sections = self.sections_count + min_sy = self.min_section_y + injected = 0 for c in chunks: - payload = self._encode_chunk(c, id_map) - items.append((c.cx, c.cz, payload)) + sections: dict[int, list[int]] = {} + for idx in range(n_sections): + sec = c.sections.get(idx + min_sy) + if sec is None: + continue # 空 section 由 Java 侧留空 + sections[idx] = [max(0, id_map.get(sp, -1)) for sp in sec["states"]] + if sections: + self.bridge.set_blocks(c.cx, c.cz, sections) + injected += 1 self.chunks[(c.cx, c.cz)] = c - resp = self.bridge.feed_chunks(items) if items else {"injected": 0} self._loaded.update((c.cx, c.cz) for c in chunks) - out = {"injected": len(items), "unknownBlocks": unknown, + out = {"injected": injected, "unknownBlocks": unknown, "uniqueSpecs": len(id_map)} if meta: out.update(meta) return out - def _encode_chunk(self, c: SaveChunk, id_map: dict[str, int]) -> bytes: - """SaveChunk → packet_map_chunk 的 chunkData 载荷。""" - out = bytearray() - for sy in range(SECTION_COUNT): - # ★ 索引 0..23 ↔ 绝对 Y (-4..19):存档用绝对 Y,载荷用相对索引 - sec = c.sections.get(sy + MIN_SECTION_Y) - if sec is None: - # 空 section:空气 - out += _encode_paletted([0] * SECTION_VOLUME, SECTION_VOLUME) - else: - ids = [max(0, id_map.get(s, -1)) for s in sec["states"]] - non_empty = sum(1 for v in ids if v != 0) - out += (non_empty & 0xFFFF).to_bytes(2, "big") - out += _encode_paletted(ids, SECTION_VOLUME) - # biome section - bios = sec["biomes"] if sec else ["minecraft:plains"] * BIOME_VOLUME - bio_pal = sorted(set(bios)) - # 生物群系用全局调色板 id 需要 registry —— 简化:统一用 plains 的 id 1 - # (生物群系只影响草/树叶着色,不影响「能不能渲染出世界」) - out += bytes([0]) + _varint(1) + _varint(0) - return bytes(out) - # ---------------- 视角与截图 ---------------- @property diff --git a/mcfold/render_bridge.py b/mcfold/render_bridge.py deleted file mode 100644 index 3b4009d..0000000 --- a/mcfold/render_bridge.py +++ /dev/null @@ -1,386 +0,0 @@ -"""渲染桥:把 Python 侧的世界数据交给 Java 客户端渲染器出图。 - -设计要点 --------- -两侧格式**同源**,不需要转换: - - Python client.run_smoke()/read_packet() 收到的 packet_map_chunk 载荷, - 其 chunkData 部分与 Java 侧 LevelChunkSection.read(FriendlyByteBuf) - 期待的是同一套字节(nonEmptyBlockCount + PalettedContainer×N + Biomes)。 - -所以桥的协议是「把 chunk 原始字节交给 Java」,而不是「把方块逐个传过去」—— -既避免重建世界的语义损失(mod 方块、NBT、biome、block entity), -又天然支持任意版本(格式由同一个 protocol.json 描述)。 - -通信 ----- -子进程 + 行分隔 JSON(NDJSON): - Java 侧 stdout 每行一个 JSON 对象;Python 写 stdin 用同样格式。 -选 NDJSON 而非 stdin 二进制:可读、可日志、便于调试; -chunk 字节量不大(单区块约 10-100KB),base64 开销可接受。 -""" -from __future__ import annotations - -import base64 -import json -import os -import shutil -import subprocess -import threading -import time -from dataclasses import dataclass, field -from pathlib import Path -from typing import Any - - -class RenderBridgeError(RuntimeError): - """桥不可用或渲染失败。""" - - -@dataclass -class RenderResult: - """一次渲染的结果。""" - - image: bytes | None = None # PNG 字节 - path: str | None = None - width: int = 0 - height: int = 0 - chunks_rendered: int = 0 # countRenderedChunks - 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: - """distinct_colors 是实测有效的判据: - 空屏/纯色 1-2 色,加载屏 ~199(logo 抗锯齿),真实世界 >1000。""" - return self.distinct_colors > 400 and self.chunks_rendered > 0 - - -class RenderBridge: - """驱动 Java 渲染器子进程。 - - 生命周期:start() → feed_chunk()×N → set_camera() → render() → stop()。 - 渲染器是**常驻进程**:起一次约 3-5s(GL + 资源重载), - 之后每次渲染只需几十到几百毫秒。 - """ - - def __init__( - self, - java: str = "java", - classpath: str | list[str] | None = None, - main_class: str = "McfoldRenderServer", - game_dir: str | None = None, - assets: str | None = None, - asset_index: str | None = None, - java_args: list[str] | None = None, - startup_timeout: float = 180.0, - cwd: str | None = None, - ): - self.java = java - self.main_class = main_class - self.game_dir = game_dir - self.assets = assets - self.asset_index = asset_index - self.startup_timeout = startup_timeout - self.cwd = cwd - - 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.java_args = java_args or [ - "-Xmx1200M", "-Xms512M", - # headless:不要 AWT 窗口;GL 仍走 GLFW + Xvfb - "-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._reader: threading.Thread | None = None - 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) -> "RenderBridge": - if self.running: - return self - if not self.classpath: - raise RenderBridgeError( - "未提供 classpath。设置 MCFOLD_RENDER_CLASSPATH 或传 classpath=。" - "构建:mcfold/scaffold/build.sh " - ) - 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) - # natives 目录(lwjgl .so):构建脚本解到 MCFOLD_NATIVES_DIR - 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 RenderBridgeError(f"无法启动 java: {e}") from e - - self._reader = threading.Thread(target=self._read_loop, daemon=True) - self._reader.start() - threading.Thread(target=self._stderr_loop, daemon=True).start() - - ready = self._await_event("ready", self.startup_timeout) - if not ready.pop("_ok", False): - self.stop() - raise RenderBridgeError( - "渲染器启动失败:" + (ready.get("error") or "超时") - + ("\n" + "".join(self._stderr_tail[-5:]) if self._stderr_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: - self._send_raw({"cmd": "shutdown"}) - try: - p.wait(timeout=timeout) - except subprocess.TimeoutExpired: - p.terminate() - try: - p.wait(timeout=5) - except subprocess.TimeoutExpired: - p.kill() - finally: - self._proc = None - - def __enter__(self) -> "RenderBridge": - return self.start() - - def __exit__(self, *exc) -> None: - self.stop() - - # ---------------- 数据注入 ---------------- - - def feed_chunk(self, cx: int, cz: int, chunk_data: bytes, - heightmaps_nbt: bytes | None = None) -> dict: - """注入一个区块。 - - chunk_data —— packet_map_chunk 载荷里「长度前缀之后」的那段, - 与 Java LevelChunkSection.read() 直接对应。 - 调 client 侧:decode 后的原始 payload 切片。 - heightmaps_nbt —— 可选 NBT(Java 侧允许空)。 - """ - return self._request({ - "cmd": "chunk", - "cx": cx, "cz": cz, - "data": base64.b64encode(chunk_data).decode("ascii"), - "heightmaps": base64.b64encode(heightmaps_nbt).decode("ascii") if heightmaps_nbt else None, - }) - - def feed_chunks(self, chunks: list[tuple[int, int, bytes]]) -> dict: - """批量注入(减少往返)。""" - return self._request({ - "cmd": "chunks", - "items": [ - {"cx": cx, "cz": cz, "data": base64.b64encode(d).decode("ascii")} - for cx, cz, d in chunks - ], - }) - - 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, *, camera_type: str | None = "THIRD_PERSON_BACK", - render_distance: int | None = None, - 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) - - # ---------------- 渲染 ---------------- - - def render(self, out: str | None = None, frames: int = 60, - width: int | None = None, height: int | None = None) -> RenderResult: - """渲若干帧并取回 PNG。 - - frames 不能太小:区块网格重建队列靠 tick 推进, - 实测 20 帧时 countRenderedChunks 已到位,60 帧更稳。 - """ - 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=300.0) - - img_b64 = resp.pop("image", None) - res = RenderResult( - image=base64.b64decode(img_b64) if img_b64 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(resp.get("camera", (0.0, 0.0, 0.0))), - warnings=resp.get("warnings", []), - raw=resp, - ) - return res - - def registry_info(self) -> dict: - """客户端方块注册表:state id ↔ 方块名(含 mod 方块的完整状态)。""" - return self._request({"cmd": "registry"}) - - def lookup_state(self, state_id: int) -> dict: - """查单个 state id(返回方块名 + 完整属性,如 instrument/note/powered)。""" - return self._request({"cmd": "lookup", "stateId": state_id}) - - # ---------------- 内部:NDJSON 通道 ---------------- - - def _request(self, req: dict, timeout: float = 120.0) -> dict: - if not self.running: - raise RenderBridgeError("渲染器未运行(先 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 RenderBridgeError(f"渲染器无响应({timeout}s):cmd={req.get('cmd')}") - _, resp = self._pending.pop(rid) - if not resp.get("_ok", True): - raise RenderBridgeError(resp.get("error", "未知错误")) - return resp - - def _await_event(self, kind: str, timeout: float) -> dict: - """等待一个主动事件(如 ready)。""" - deadline = time.time() + timeout - while time.time() < deadline: - if not self.running: - return {"_ok": False, "error": f"进程退出(code={self._proc.poll() if self._proc else '?'})"} - for rid, (ev, box) in list(self._pending.items()): - if box.get("_event") == kind and ev.is_set(): - self._pending.pop(rid, None) - box["_ok"] = True - return box - 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 RenderBridgeError("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 - rid = msg.get("id") - if rid is None and msg.get("event") == "ready": - # ready 事件:塞进 _pending 供 _await_event 取 - ev = threading.Event() - box = dict(msg); box["_event"] = "ready" - self._pending[-1] = (ev, box) - ev.set() - continue - 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) > 200: - del self._stderr_tail[:100] - - # ---------------- 便利 ---------------- - - @staticmethod - def discover_defaults(store: str | Path) -> dict: - """从 store 目录推出 assets/assetIndex/gameDir 的默认值(照官方启动器布局)。""" - store = Path(store) - assets = store / "assets" - idx_dir = assets / "indexes" - asset_index = None - if idx_dir.is_dir(): - names = sorted(p.stem for p in idx_dir.glob("*.json")) - asset_index = names[-1] if names else None - return { - "assets": str(assets), - "asset_index": asset_index, - "game_dir": str(store / "instances" / "default"), - } diff --git a/scaffold/ARCHITECTURE.md b/scaffold/ARCHITECTURE.md new file mode 100644 index 0000000..ad4ac4d --- /dev/null +++ b/scaffold/ARCHITECTURE.md @@ -0,0 +1,81 @@ +# 架构分层:谁负责什么 + +用户口径(2026-09-25):**差异放在 Java 层,Python 相当于路由。** + +## 三层 + +``` +Python(mcfold/) + ├── backends/__init__.py 路由:按 client jar 判世代 → 选后端 ← 唯一的 switch + ├── backends/base.py 路由:NDJSON 协议、命令集、RLE 压缩 + ├── anvil.py 读取:存档 region/NBT → 方块(名字规格) + └── render.py 读取 + 编排:WorldRenderer + │ + │ NDJSON(chunk 数据 / RLE 方块 / 相机 / 渲染指令) + ▼ +Java(scaffold/java/gen_1_20/ , gen_1_21/) + ├── McfoldRenderServer.java 协议格式、调色板组装、位打包、入 cache、渲染 + └── McfoldBridge.java 包私有 API 桥(reloadShaders / loadEffect) + │ + ▼ +官方客户端 jar(各自版本,重映射为可读名) +``` + +## 分界原则 + +| 层 | 负责 | 为什么 | +|---|---|---| +| **Python** | 路由、存档读取、方块名→id 查询、RLE 压缩、编排 | 这些**不随 MC 版本变** | +| **Java** | 协议格式、调色板、位打包、官方序列化、渲染调用 | 这些**历代改过**,必须贴着版本走 | + +### 判据:这个知识「随版本变吗」 + +| 知识 | 随版本变? | 归属 | +|---|---|---| +| 方块名 → state id | 变(mod/版本) | Java(问运行时注册表) | +| 世界尺寸(sectionsCount / minSection) | 变(1.16 是 16/0,1.20 是 24/-4) | Java(`cmd="capacity"`) | +| PalettedContainer 位打包 | 变过 | Java(官方 API) | +| LongArray 编码 | 变过 | Java(官方 API) | +| 区块入 cache 的路径 | 签名变过 | Java(官方 `replaceWithPacketData`) | +| **RLE 压缩** | **不变** | Python(只是压重复值) | +| **存档 NBT 结构** | 基本不变 | Python(读取) | +| **NDJSON 协议** | 不变(我们自己定的) | Python(路由层) | + +## 数据流:渲染一个存档 + +``` +1. Python: 读 level.dat + region/*.mca +2. Python: NBT → Sections[].block_states(palette + data) +3. Python: palette 名字(+属性) → "minecraft:oak_log[axis=y]" 规格 +4. Python: 向后端批量查询规格 → state id [Java: BuiltInRegistries.BLOCK.asLookup()] +5. Python: state id 列表 → RLE [[id,count],...] ← 唯一被压缩的东西 +6. Python: setblocks(cx, cz, sections) ← NDJSON +7. Java: LevelChunk + LevelChunkSection.setBlockState(...) +8. Java: 光照(queueSectionData 全亮) +9. Java: ClientboundLevelChunkPacketData(chunk) ← 官方序列化 +10. Java: replaceWithPacketData(官方入口入 cache) +11. Java: gameRenderer.render(...)(世代差异在此) +12. Java: 读 FBO → PNG → base64 回传 +``` + +**第 4/7/8/9/10/11 步都在 Java 侧** —— 版本相关知识全部集中在那里。 + +## 加新世代(用户 2026-09-25 定向的收益) + +1. `scaffold/java/gen_/` 建目录,拷上一代源码 +2. 对着 **javap 输出**改差异点(实测共 11 处,见 VERSION-DIFFS.md) +3. `mcfold/backends/gen_.py` 声明 `markers`(判据类) +4. `mcfold/backends/__init__.py` 的 `GENERATIONS` 加一条 +5. 各自对**对应版本**的 named jar 编译 + +**Python 侧只动第 3、4 步**(各一处),协议与渲染逻辑无需碰。 + +## 已删除(防回归) + +| 已删 | 原因 | +|---|---| +| `mcfold/render_bridge.py` | 被 `backends/` 取代(旧版无世代概念) | +| `render._pack/_long_array/_encode_paletted/_encode_chunk/_varint` | 协议编码下移到 Java | +| `anvil.encode_* / save_chunk_to_payload` | 同上 | + +回归判据见 `tests/test_anvil.py::test_import_surface` —— 这些名字**不应再出现**在 Python 里。 diff --git a/scaffold/java/gen_1_20/McfoldRenderServer.java b/scaffold/java/gen_1_20/McfoldRenderServer.java index e7927f9..36f29da 100644 --- a/scaffold/java/gen_1_20/McfoldRenderServer.java +++ b/scaffold/java/gen_1_20/McfoldRenderServer.java @@ -37,6 +37,8 @@ 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.chunk.LevelChunk; +import net.minecraft.world.level.chunk.LevelChunkSection; import net.minecraft.world.level.dimension.DimensionType; import org.lwjgl.opengl.GL11; @@ -180,6 +182,66 @@ public class McfoldRenderServer { emit(id, r); break; } + case "setblocks": { + // ★ 世代无关:Python 只做路由,方块写入、调色板组装、入 cache + // 全部由 Java 用官方 API 完成 —— 版本差异不落回 Python。 + // 载荷:sections[].rle = [[stateId,count],...],按 (y,z,x) 线性序覆盖该 section 4096 格。 + // 优势:协议位打包(历代改过)不再由 Python 复制;入 cache 走官方 + // ClientboundLevelChunkPacketData(LevelChunk) → getReadBuffer(), + // 签名两代一致,**无需反射**。 + int cx = req.get("cx").getAsInt(), cz = req.get("cz").getAsInt(); + LevelChunk chunk = new LevelChunk(level, new ChunkPos(cx, cz)); + LevelChunkSection[] sections = chunk.getSections(); + int placed = 0; + long cells = 0; + for (JsonElement se : req.getAsJsonArray("sections")) { + JsonObject so = se.getAsJsonObject(); + int si = so.get("index").getAsInt(); + if (si < 0 || si >= sections.length) continue; + LevelChunkSection sec = sections[si]; + int x = 0, y = 0, z = 0; + for (JsonElement re : so.getAsJsonArray("rle")) { + JsonArray pair = re.getAsJsonArray(); + int stateId = pair.get(0).getAsInt(); + int count = pair.get(1).getAsInt(); + BlockState st = stateId == 0 ? null : Block.stateById(stateId); + for (int i = 0; i < count; i++) { + if (st != null) { sec.setBlockState(x, y, z, st); placed++; } + cells++; + if (++x == 16) { x = 0; if (++z == 16) { z = 0; y++; } } + } + } + } + // 光照(无光照数据则全亮,否则地形全黑) + var light = level.getChunkSource().getLightEngine(); + for (int sy = level.getMinSection(); sy < level.getMaxSection(); sy++) { + DataLayer sky = new DataLayer(); + for (int lx = 0; lx < 16; lx++) + for (int ly = 0; ly < 16; ly++) + for (int lz = 0; lz < 16; lz++) sky.set(lx, ly, lz, 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); + } + // ★ 官方序列化 → 官方入口入 cache(两代签名一致) + var pkt = new net.minecraft.network.protocol.game.ClientboundLevelChunkPacketData(chunk); + level.getChunkSource().replaceWithPacketData(cx, cz, + pkt.getReadBuffer(), pkt.getHeightmaps(), + new java.util.function.Consumer< + net.minecraft.network.protocol.game.ClientboundLevelChunkPacketData.BlockEntityTagOutput>() { + @Override public void accept( + net.minecraft.network.protocol.game.ClientboundLevelChunkPacketData.BlockEntityTagOutput t) { } + }); + light.setLightEnabled(new ChunkPos(cx, cz), true); + light.runLightUpdates(); + JsonObject r = new JsonObject(); + r.addProperty("ok", true); + r.addProperty("placed", placed); + r.addProperty("cells", cells); + r.addProperty("sections", sections.length); + r.addProperty("loadedChunks", level.getChunkSource().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; @@ -283,6 +345,28 @@ public class McfoldRenderServer { emit(id, r); break; } + case "capacity": { + // ★ 世界尺寸由运行时提供 —— Python 不硬编码版本相关常量。 + // 这些方法都在 LevelHeightAccessor 上(Level 实现),两代 API 一致。 + JsonObject r = new JsonObject(); + r.addProperty("ok", true); + r.addProperty("sectionsCount", level.getSectionsCount()); + r.addProperty("minSection", level.getMinSection()); + r.addProperty("maxSection", level.getMaxSection()); + r.addProperty("minBuildHeight", level.getMinBuildHeight()); + r.addProperty("maxBuildHeight", level.getMaxBuildHeight()); + r.addProperty("height", level.getHeight()); + // 生物群系默认 id(不硬编码 1) + try { + var bio = mc.level.registryAccess().registryOrThrow(Registries.BIOME); + var key = bio.getKey(bio.byId(1)); + r.addProperty("defaultBiome", key != null ? key.toString() : "minecraft:plains"); + } catch (Throwable t) { + r.addProperty("defaultBiome", "minecraft:plains"); + } + emit(id, r); + break; + } case "registry": { JsonObject r = new JsonObject(); r.addProperty("ok", true); @@ -317,8 +401,12 @@ public class McfoldRenderServer { // 而这个名字→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); + // ★ parseForBlock 要 HolderLookup(不是 Registry)。 + // 用**静态注册表**的 asLookup():它是方块名→id 的权威来源 + // (mod 方块也注册在这里),且两代 API 一致。 + // 不用 mc.level.registryAccess() —— 1.21 上它来自 cookie, + // 只含 worldgen 那批注册表,查 block 会报 "Registry not found"(实测)。 + var reg = BuiltInRegistries.BLOCK.asLookup(); JsonArray out = new JsonArray(); int unknown = 0; for (JsonElement el : arr) { diff --git a/scaffold/java/gen_1_21/McfoldRenderServer.java b/scaffold/java/gen_1_21/McfoldRenderServer.java index 1e01211..04e6763 100644 --- a/scaffold/java/gen_1_21/McfoldRenderServer.java +++ b/scaffold/java/gen_1_21/McfoldRenderServer.java @@ -37,6 +37,8 @@ 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.chunk.LevelChunk; +import net.minecraft.world.level.chunk.LevelChunkSection; import net.minecraft.world.level.dimension.DimensionType; import org.lwjgl.opengl.GL11; @@ -180,6 +182,66 @@ public class McfoldRenderServer { emit(id, r); break; } + case "setblocks": { + // ★ 世代无关:Python 只做路由,方块写入、调色板组装、入 cache + // 全部由 Java 用官方 API 完成 —— 版本差异不落回 Python。 + // 载荷:sections[].rle = [[stateId,count],...],按 (y,z,x) 线性序覆盖该 section 4096 格。 + // 优势:协议位打包(历代改过)不再由 Python 复制;入 cache 走官方 + // ClientboundLevelChunkPacketData(LevelChunk) → getReadBuffer(), + // 签名两代一致,**无需反射**。 + int cx = req.get("cx").getAsInt(), cz = req.get("cz").getAsInt(); + LevelChunk chunk = new LevelChunk(level, new ChunkPos(cx, cz)); + LevelChunkSection[] sections = chunk.getSections(); + int placed = 0; + long cells = 0; + for (JsonElement se : req.getAsJsonArray("sections")) { + JsonObject so = se.getAsJsonObject(); + int si = so.get("index").getAsInt(); + if (si < 0 || si >= sections.length) continue; + LevelChunkSection sec = sections[si]; + int x = 0, y = 0, z = 0; + for (JsonElement re : so.getAsJsonArray("rle")) { + JsonArray pair = re.getAsJsonArray(); + int stateId = pair.get(0).getAsInt(); + int count = pair.get(1).getAsInt(); + BlockState st = stateId == 0 ? null : Block.stateById(stateId); + for (int i = 0; i < count; i++) { + if (st != null) { sec.setBlockState(x, y, z, st); placed++; } + cells++; + if (++x == 16) { x = 0; if (++z == 16) { z = 0; y++; } } + } + } + } + // 光照(无光照数据则全亮,否则地形全黑) + var light = level.getChunkSource().getLightEngine(); + for (int sy = level.getMinSection(); sy < level.getMaxSection(); sy++) { + DataLayer sky = new DataLayer(); + for (int lx = 0; lx < 16; lx++) + for (int ly = 0; ly < 16; ly++) + for (int lz = 0; lz < 16; lz++) sky.set(lx, ly, lz, 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); + } + // ★ 官方序列化 → 官方入口入 cache(两代签名一致) + var pkt = new net.minecraft.network.protocol.game.ClientboundLevelChunkPacketData(chunk); + level.getChunkSource().replaceWithPacketData(cx, cz, + pkt.getReadBuffer(), pkt.getHeightmaps(), + new java.util.function.Consumer< + net.minecraft.network.protocol.game.ClientboundLevelChunkPacketData.BlockEntityTagOutput>() { + @Override public void accept( + net.minecraft.network.protocol.game.ClientboundLevelChunkPacketData.BlockEntityTagOutput t) { } + }); + light.setLightEnabled(new ChunkPos(cx, cz), true); + light.runLightUpdates(); + JsonObject r = new JsonObject(); + r.addProperty("ok", true); + r.addProperty("placed", placed); + r.addProperty("cells", cells); + r.addProperty("sections", sections.length); + r.addProperty("loadedChunks", level.getChunkSource().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; @@ -283,6 +345,28 @@ public class McfoldRenderServer { emit(id, r); break; } + case "capacity": { + // ★ 世界尺寸由运行时提供 —— Python 不硬编码版本相关常量。 + // 这些方法都在 LevelHeightAccessor 上(Level 实现),两代 API 一致。 + JsonObject r = new JsonObject(); + r.addProperty("ok", true); + r.addProperty("sectionsCount", level.getSectionsCount()); + r.addProperty("minSection", level.getMinSection()); + r.addProperty("maxSection", level.getMaxSection()); + r.addProperty("minBuildHeight", level.getMinBuildHeight()); + r.addProperty("maxBuildHeight", level.getMaxBuildHeight()); + r.addProperty("height", level.getHeight()); + // 生物群系默认 id(不硬编码 1) + try { + var bio = mc.level.registryAccess().registryOrThrow(Registries.BIOME); + var key = bio.getKey(bio.byId(1)); + r.addProperty("defaultBiome", key != null ? key.toString() : "minecraft:plains"); + } catch (Throwable t) { + r.addProperty("defaultBiome", "minecraft:plains"); + } + emit(id, r); + break; + } case "registry": { JsonObject r = new JsonObject(); r.addProperty("ok", true); @@ -317,8 +401,12 @@ public class McfoldRenderServer { // 而这个名字→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); + // ★ parseForBlock 要 HolderLookup(不是 Registry)。 + // 用**静态注册表**的 asLookup():它是方块名→id 的权威来源 + // (mod 方块也注册在这里),且两代 API 一致。 + // 不用 mc.level.registryAccess() —— 1.21 上它来自 cookie, + // 只含 worldgen 那批注册表,查 block 会报 "Registry not found"(实测)。 + var reg = BuiltInRegistries.BLOCK.asLookup(); JsonArray out = new JsonArray(); int unknown = 0; for (JsonElement el : arr) { @@ -453,6 +541,10 @@ public class McfoldRenderServer { } static void render(Integer id, int frames, String out) throws Exception { + // ★ 1.21 实测:setLevel 会挂 ReceivingLevelScreen; + // 若渲染前它有残留,画面就是加载屏(distinctColors 停在 ~199)。 + mc.setScreen(null); + mc.setOverlay(null); tickFrames(frames); MainTarget target = (MainTarget) mc.getMainRenderTarget(); diff --git a/tests/test_anvil.py b/tests/test_anvil.py index e7557f3..ffc2786 100644 --- a/tests/test_anvil.py +++ b/tests/test_anvil.py @@ -1,6 +1,11 @@ -"""anvil.py —— 存档读取与区块编码的单元测试。 +"""Python 侧职责的单元测试。 -不依赖 Java 渲染器;渲染链路的联测在 test_render_bridge.py。 +★ 架构口径(用户 2026-09-25 定向) + Python 负责:路由、方块名→id 查询、RLE 压缩、存档读取 + Java 负责: 协议格式、调色板组装、位打包、入 cache、渲染 + +所以这里**不再**测协议位打包(那已下移到 Java,由两代后端各自的编译验证覆盖); +改测 Python 真正负责的部分,尤其是 RLE 的边界与存档读取的真实数据。 """ import struct import sys @@ -8,90 +13,81 @@ from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parents[1])) -from mcfold.anvil import NbtReader, RegionFile, nbt_read, region_path, list_region_files +from mcfold.anvil import ( + NbtReader, RegionFile, list_region_files, nbt_read, read_level_dat, region_path, +) +from mcfold.backends.base import rle_decode, rle_encode from mcfold.render import ( - _pack, _varint, _long_array, _decode_storage, _encode_paletted, - parse_save_chunk, SECTION_VOLUME, MIN_SECTION_Y, + BIOME_VOLUME, SECTION_VOLUME, SaveChunk, _props_to_spec, parse_save_chunk, ) SAVE = Path("/home/mc-test/mc-test") -def test_varint_roundtrip(): - def read_varint(b: bytes) -> tuple[int, int]: - v = 0; shift = 0; i = 0 - while True: - byte = b[i]; i += 1 - v |= (byte & 0x7F) << shift - if not (byte & 0x80): - break - shift += 7 - return v, i +# ---------------------------------------------------------------- RLE(Python 的职责) - for n in [0, 1, 127, 128, 255, 16384, 2_097_152, 24135]: - enc = _varint(n) - dec, used = read_varint(enc) - assert dec == n, f"{n} -> {dec}" - assert used == len(enc) - print(" ✓ varint 编解码") +def test_rle_basic(): + assert rle_encode([]) == [] + assert rle_encode([5]) == [[5, 1]] + assert rle_encode([5, 5, 5]) == [[5, 3]] + assert rle_encode([1, 2, 1]) == [[1, 1], [2, 1], [1, 1]] + print(" ✓ RLE 基本情形") -def test_pack_unpack_roundtrip(): - """位打包必须能被 _decode_storage 解回来(值不跨 long 边界)。""" - for bits in [4, 5, 8, 15]: - vals = [(i * 7) % (1 << min(bits, 8)) for i in range(64 * 4)] - longs = _pack(vals, bits) - back = _decode_storage(longs, len(vals), bits) - assert back == vals, f"bits={bits} 不一致: {back[:8]} vs {vals[:8]}" - print(" ✓ 位打包往返一致(4/5/8/15 位)") +def test_rle_roundtrip(): + cases = [ + [0] * 4096, + [1] * 4096, + [0] * 4090 + [9] * 6, + list(range(256)) * 16, + [7 if i % 3 else 0 for i in range(4096)], + ] + for v in cases: + assert rle_decode(rle_encode(v)) == v + print(" ✓ RLE 往返一致(含全同/交替/随机模式)") -def test_long_array_encoding(): - """LongArray = varint(长度) + 大端 i64 ×N。""" - enc = _long_array([1, -1]) - assert enc[0] == 2 # 长度 2 - assert enc[1:9] == (1).to_bytes(8, "big") - assert enc[9:17] == (0xFFFFFFFFFFFFFFFF).to_bytes(8, "big") - assert _long_array([])[0] == 0 - print(" ✓ LongArray 编码(含负数/空)") +def test_rle_is_lossless_on_long_runs(): + """长游程必须无损 —— 一个 section 常有整片空气/石头。""" + v = [0] * 4000 + [1] * 96 + enc = rle_encode(v) + assert len(enc) == 2, f"应压成 2 段,得到 {len(enc)}" + assert rle_decode(enc, expected=SECTION_VOLUME) == v + print(" ✓ 长游程无损压缩(4096 格 → 2 段)") -def test_paletted_single_value(): - """单值分支:byte(0) + varint(state) + varint(0 longs) —— 与 Java 对齐。""" - data = _encode_paletted([5] * SECTION_VOLUME, SECTION_VOLUME) - assert data[0] == 0, "bitsPerValue 应为 0" - assert data[1] == 5, "state id 应跟在 bits 后" - assert data[2] == 0, "longs 数应为 0" - print(" ✓ 单值调色板编码(3 字节)") +def test_rle_decode_length_check(): + """长度不符要报错,而不是静默给出错长度的列表(防下游越界)。""" + try: + rle_decode([[1, 3]], expected=4096) + except ValueError as e: + assert "4096" in str(e) + print(" ✓ RLE 长度校验") + return + raise AssertionError("长度不符时应抛 ValueError") -def test_paletted_indirect(): - """多值走本地调色板:bits + n + palette + longs。""" - vals = [0] * (SECTION_VOLUME - 1) + [3] - data = _encode_paletted(vals, SECTION_VOLUME) - bits = data[0] - assert bits >= 4, f"本地调色板 bits 至少 4,得到 {bits}" - n = data[1] - assert n == 2, f"调色板应有 2 项,得到 {n}" - print(f" ✓ 间接调色板编码(bits={bits}, palette={n})") +def test_rle_handles_bool_like_and_negative(): + """palette 索引可能是任意 int;不假设范围。""" + v = [-1, -1, 0, 0, 0] + assert rle_decode(rle_encode(v)) == v + print(" ✓ RLE 接受任意 int(含负值)") +# ---------------------------------------------------------------- NBT / 存档读取 + def test_nbt_reader_primitives(): - """NBT 读取器的基本类型。""" - # Compound{ "a": Int(7), "b": String("hi") } body = bytearray() - body += bytes([10]) # root: Compound - body += struct.pack(">H", 1) + b"R" # 名字 "R" + body += bytes([10]) + struct.pack(">H", 1) + b"R" body += bytes([3]) + struct.pack(">H", 1) + b"a" + struct.pack(">i", 7) body += bytes([8]) + struct.pack(">H", 1) + b"b" + struct.pack(">H", 2) + b"hi" - body += bytes([0]) # end - d = nbt_read(bytes(body)) - assert d == {"a": 7, "b": "hi"}, d + body += bytes([0]) + assert nbt_read(bytes(body)) == {"a": 7, "b": "hi"} print(" ✓ NBT 基本类型(int/string)") def test_nbt_nested_list(): - """嵌套 List/Compound(存档 sections 就是这个形状)。""" + """嵌套 List[Compound] —— 存档 sections 就是这个形状。""" body = bytearray() body += bytes([10]) + struct.pack(">H", 0) body += bytes([9]) + struct.pack(">H", 4) + b"list" + bytes([10]) + struct.pack(">i", 2) @@ -99,44 +95,49 @@ def test_nbt_nested_list(): body += bytes([3]) + struct.pack(">H", 1) + b"Y" + struct.pack(">i", y) body += bytes([0]) body += bytes([0]) - d = nbt_read(bytes(body)) - assert d == {"list": [{"Y": 0}, {"Y": 1}]}, d + assert nbt_read(bytes(body)) == {"list": [{"Y": 0}, {"Y": 1}]} print(" ✓ NBT 嵌套 List[Compound]") +def test_nbt_long_array(): + """区块高度图与 block_states.data 都是 LongArray。""" + body = bytearray() + body += bytes([10]) + struct.pack(">H", 0) + body += bytes([12]) + struct.pack(">H", 1) + b"d" + struct.pack(">i", 2) + body += struct.pack(">qq", -1, 1234567890123) + body += bytes([0]) + d = nbt_read(bytes(body)) + assert d == {"d": [-1, 1234567890123]}, d + print(" ✓ NBT LongArray(含负值)") + + def test_props_to_spec(): - from mcfold.render import _props_to_spec assert _props_to_spec("minecraft:stone", None) == "minecraft:stone" assert _props_to_spec("minecraft:oak_log", {"axis": "y"}) == "minecraft:oak_log[axis=y]" - # 多属性要排序(保证缓存命中稳定) + # 属性顺序必须稳定(否则 id 缓存命中率下降) a = _props_to_spec("minecraft:oak_stairs", {"facing": "north", "half": "top"}) b = _props_to_spec("minecraft:oak_stairs", {"half": "top", "facing": "north"}) - assert a == b == "minecraft:oak_stairs[facing=north,half=top]", a + assert a == b == "minecraft:oak_stairs[facing=north,half=top]" print(" ✓ 方块名规格生成(属性排序稳定)") def test_real_save_level_dat(): - """真实 level.dat:本机存档是该格式(直接 gzip,无 8 字节头)。""" if not (SAVE / "level.dat").is_file(): print(" - 无测试存档,跳过") return - from mcfold.anvil import read_level_dat lv = read_level_dat(SAVE) data = lv["Data"] - assert "LevelName" in data - assert isinstance(data.get("DataVersion"), int) - assert "Version" in data + assert "LevelName" in data and isinstance(data.get("DataVersion"), int) print(f" ✓ 真实 level.dat({data['LevelName']}, DataVersion={data['DataVersion']})") def test_real_region_chunk(): - """真实 region:读出区块 NBT,且有完整地形(不是全空气)。""" if not (SAVE / "region").is_dir(): print(" - 无测试存档,跳过") return regs = list_region_files(SAVE) assert regs, "应有 region 文件" - found = None + names = set() for rf in regs[:4]: for cx, cz in [(-1, -1), (0, 0), (8, -7)]: try: @@ -144,25 +145,21 @@ def test_real_region_chunk(): except Exception: continue if raw: - found = nbt_read(raw) - break - if found: + n = nbt_read(raw) + for s in n.get("sections", []): + for p in (s.get("block_states") or {}).get("palette") or []: + names.add(p.get("Name")) + if names: break - assert found is not None, "应至少读到一个区块" - names = set() - for s in found.get("sections", []): - for p in (s.get("block_states") or {}).get("palette") or []: - names.add(p.get("Name")) - assert len(names) > 1, f"完整区块应有多种方块,得到 {names}" + assert len(names) > 1, f"完整区块应含多种方块,得到 {names}" print(f" ✓ 真实区块 NBT({len(names)} 种方块)") -def test_parse_save_chunk_shapes(): - """parse_save_chunk 输出的结构:states 长度、Y 范围、规格去重。""" +def test_parse_save_chunk_shape(): + """parse_save_chunk 的产物必须可直接喂 RLE(长度=4096,值是名字规格)。""" if not (SAVE / "region").is_dir(): print(" - 无测试存档,跳过") return - rf = None nbt = None for r in list_region_files(SAVE): for cx, cz in [(-1, -1), (0, 0)]: @@ -173,7 +170,7 @@ def test_parse_save_chunk_shapes(): if raw: n = nbt_read(raw) if n.get("Status") == "minecraft:full": - rf, nbt = r, n + nbt = n break if nbt: break @@ -184,47 +181,50 @@ def test_parse_save_chunk_shapes(): assert ch.sections, "应有 sections" for sy, sec in ch.sections.items(): assert len(sec["states"]) == SECTION_VOLUME, f"Y={sy} states 长度 {len(sec['states'])}" - assert len(sec["biomes"]) == 64 - assert sec["states"][0].startswith("minecraft:") + assert all(isinstance(s, str) and s.startswith("minecraft:") for s in sec["states"][:8]) assert ch.palette_specs == sorted(ch.palette_specs), "规格应去重且排序" - print(f" ✓ parse_save_chunk({len(ch.sections)} sections, {len(ch.palette_specs)} 规格)") + print(f" ✓ parse_save_chunk 产物可直接喂 RLE({len(ch.sections)} sections, " + f"{len(ch.palette_specs)} 规格)") -def test_section_y_mapping(): - """★ 关键回归:存档绝对 Y(-4..19) ↔ 载荷索引(0..23)。""" - assert MIN_SECTION_Y == -4, "1.20 的 minSection 是 -4" - # 载荷索引 0 应取存档 Y=-4;索引 23 应取存档 Y=19 - save_y = {i - 4: {"states": ["minecraft:air"] * SECTION_VOLUME, - "biomes": ["minecraft:plains"] * 64, "palette": []} - for i in range(24)} - # 用真实编码器验证:只放 Y=-4 有 stone,则载荷 section 0 应是 stone - save_y[-4]["states"] = ["minecraft:stone"] * SECTION_VOLUME - save_y[-4]["palette"] = ["minecraft:stone"] - from mcfold.render import SaveChunk +def test_section_y_mapping_needs_runtime_value(): + """★ 回归:section 索引 ↔ 绝对 Y 的偏移必须是运行时值,不是硬编码。 - class FakeWR: - def _encode_chunk(self, c, id_map): - return bytearray() - - # 直接调编码核心:验证第 0 个 section 用的是 Y=-4 - from mcfold.render import _encode_paletted - out = bytearray() - for sy in range(24): - sec = save_y.get(sy - 4) - ids = [1 if s == "minecraft:stone" else 0 for s in sec["states"]] - out += (sum(1 for v in ids if v) & 0xFFFF).to_bytes(2, "big") - out += _encode_paletted(ids, SECTION_VOLUME) - out += bytes([0]) + _varint(1) + _varint(0) - # 第一个 section 的 nonEmpty 应为 4096(不是 0) - assert struct.unpack_from(">h", out, 0)[0] == 4096, "索引 0 应映射到存档 Y=-4" - print(" ✓ section 索引 ↔ 绝对 Y 映射(-4..19 ↔ 0..23)") + 1.20.x 是 minSection=-4;更早的版本不是。Python 只从后端取。 + """ + from mcfold.render import FALLBACK_MIN_SECTION_Y, WorldRenderer + assert FALLBACK_MIN_SECTION_Y == -4, "降级默认值(真实值应来自后端 capacity)" + # WorldRenderer 的 min_section_y 优先用后端值 + class FakeCap: + _capacity = {"minSection": -4, "sectionsCount": 24} + _biome_id = None + @property + def min_section_y(self): + return WorldRenderer.min_section_y.fget(self) + @property + def sections_count(self): + return WorldRenderer.sections_count.fget(self) + f = FakeCap() + assert f.min_section_y == -4 + assert f.sections_count == 24 + # 后端给不同值时跟随 + class FakeCap2(FakeCap): + _capacity = {"minSection": 0, "sectionsCount": 16} + f2 = FakeCap2() + assert f2.min_section_y == 0, "应跟随后端值(1.16 时代 minSection=0)" + assert f2.sections_count == 16 + print(" ✓ section 偏移取自运行时(不硬编码版本)") -def test_encode_empty_section_is_air(): - """缺失的 section 编成空气(不是跳过)—— 否则 Java 侧读越界。""" - data = _encode_paletted([0] * SECTION_VOLUME, SECTION_VOLUME) - assert data == bytes([0, 0, 0]), f"空气 section 应是 3 字节,得到 {len(data)}" - print(" ✓ 空 section 编码为空气") +def test_import_surface(): + """已下移到 Java 的协议编码函数不应再存在于 Python(防回归)。""" + import mcfold.render as R + for gone in ("_pack", "_long_array", "_encode_paletted", "_encode_chunk", "_varint"): + assert not hasattr(R, gone), f"{gone} 应已下移到 Java 侧" + # 读路径必须保留 + assert hasattr(R, "_decode_storage") + assert hasattr(R, "parse_save_chunk") + print(" ✓ 模块面:写路径已下移,读路径保留") if __name__ == "__main__": @@ -235,6 +235,9 @@ if __name__ == "__main__": t() except Exception as e: failed += 1 + import traceback print(f" ✗ {t.__name__}: {e}") - print(f"\nanvil/render 单元测试: " + ("全部通过" if failed == 0 else f"{len(tests)-failed}/{len(tests)} 通过")) + traceback.print_exc() + print(f"\nanvil/render 单元测试: " + + ("全部通过" if failed == 0 else f"{len(tests)-failed}/{len(tests)} 通过")) sys.exit(1 if failed else 0) diff --git a/tests/test_render_bridge.py b/tests/test_render_bridge.py index eb69590..7dc0a05 100644 --- a/tests/test_render_bridge.py +++ b/tests/test_render_bridge.py @@ -1,10 +1,12 @@ -"""渲染桥联测:Python ↔ Java 渲染器。 +"""渲染联测:Python ↔ Java 后端。 需要: - 已构建的 client-named.jar + McfoldRenderServer.class - 环境变量 MCFOLD_RENDER_CLASSPATH、MCFOLD_NATIVES_DIR - Xvfb DISPLAY 缺任一项则跳过(不误报失败)。 + +★ 架构口径:Python 只发 RLE(set_blocks),协议格式由 Java 侧组装。 """ import os import sys @@ -12,16 +14,15 @@ from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parents[1])) -from mcfold.render import ( - SECTION_VOLUME, _encode_paletted, _varint, parse_save_chunk, -) -from mcfold.render_bridge import RenderBridge, RenderBridgeError +from mcfold.backends import Gen_1_20, backend_class +from mcfold.backends.base import BackendError, rle_encode +from mcfold.render import SECTION_VOLUME, WorldRenderer, parse_save_chunk SAVE = Path("/home/mc-test/mc-test") STORE = Path("/home/newqqagent/mcview/store/vanilla-1.20.1") -def _bridge_available() -> tuple[bool, str]: +def _available() -> tuple[bool, str]: cp = os.environ.get("MCFOLD_RENDER_CLASSPATH") if not cp: return False, "未设 MCFOLD_RENDER_CLASSPATH" @@ -29,139 +30,181 @@ def _bridge_available() -> tuple[bool, str]: if not any("client-named" in p for p in paths): return False, "classpath 缺 client-named.jar" if not any("McfoldRenderServer" in p or "classes" in p for p in paths): - return False, "classpath 缺 McfoldRenderServer.class" + return False, "classpath 缺编译产物" if not os.environ.get("DISPLAY"): return False, "无 DISPLAY" return True, "" -def test_bridge_protocol(): - """桥的协议层:registry / lookup / resolve —— 不涉及渲染。""" - ok, why = _bridge_available() +def _open_backend(): + ok, why = _available() if not ok: - print(f" - 跳过({why})") - return - br = RenderBridge( + return None, why + br = Gen_1_20( game_dir=str(STORE / "instances" / "default"), assets=str(STORE / "assets"), asset_index="1.20.1", ) try: br.start() - except RenderBridgeError as e: - print(f" - 跳过(启动失败:{str(e)[:80]})") + except BackendError as e: + return None, f"启动失败:{str(e)[:100]}" + return br, "" + + +# ---------------------------------------------------------------- 世代分派 + +def test_generation_dispatch(): + """判据是 DeltaTracker 是否存在(不是硬编码版本号)。""" + jars = { + "1.20.1": "/tmp/client-1.20.1.jar", + "1.21.1": "/tmp/genb/client-1.21.1-named.jar", + } + present = {v: p for v, p in jars.items() if Path(p).is_file()} + if not present: + print(" - 无 named jar,跳过") + return + for ver, jar in present.items(): + cls = backend_class(jar) + if ver == "1.20.1": + assert cls is Gen_1_20, f"{ver} 应选 Gen_1_20,得到 {cls.__name__}" + else: + # 1.21+ 必须有 DeltaTracker ⇒ 选 gen_1_21 + from mcfold.backends import Gen_1_21 + assert cls is Gen_1_21, f"{ver} 应选 Gen_1_21,得到 {cls.__name__}" + print(f" ✓ 世代分派 {ver} → {cls.__name__}") + + +def test_generation_metadata(): + """每个后端要能说清自己的 API 特征(加新世代时的对照依据)。""" + notes = Gen_1_20.api_notes() + assert "setLevel" in notes and "render" in notes + assert notes.get("deltaTracker") is False + from mcfold.backends import Gen_1_21 + n21 = Gen_1_21.api_notes() + assert n21.get("deltaTracker") is True, "1.21+ 用 DeltaTracker" + assert "DeltaTracker" in n21["render"] + print(f" ✓ 世代元信息(1.20: {notes['render']})") + + +# ---------------------------------------------------------------- 协议层 + +def test_capacity_from_runtime(): + """★ 世界尺寸必须问后端,不是 Python 硬编码。""" + br, why = _open_backend() + if br is None: + print(f" - 跳过({why})") + return + try: + cap = br._request({"cmd": "capacity"}) + assert cap["sectionsCount"] > 0, cap + assert "minSection" in cap and "maxSection" in cap + # 1.20.1 实测 24 / -4 / 20 + assert cap["sectionsCount"] == 24, cap + assert cap["minSection"] == -4, cap + assert cap["maxSection"] == 20, cap + print(f" ✓ 运行时世界尺寸(sections={cap['sectionsCount']}, " + f"min={cap['minSection']}, biome={cap.get('defaultBiome')})") + finally: + br.stop() + + +def test_resolve_and_lookup(): + """方块名↔state id 是渲染器侧权威映射(Python 不复制这张表)。""" + br, why = _open_backend() + if br is None: + print(f" - 跳过({why})") return try: reg = br.registry_info() - assert reg["ok"] and reg["blockCount"] > 500, reg - assert reg["stateCount"] > 1000, reg - - # state id → 方块名(含属性) - st = br.lookup_state(1) - assert st["block"] == "minecraft:stone", st - assert st["inRange"] is True - - # 方块名(+属性) → state id - r = br._request({"cmd": "resolve", "blocks": [ - "minecraft:stone", - "minecraft:oak_log[axis=y]", - "minecraft:grass_block[snowy=false]", - "minecraft:this_block_does_not_exist", - ]}) - ids = r["ids"] - assert ids[0] > 0, "stone 应有有效 id" - assert ids[1] > 0, "oak_log[axis=y] 应可解析" - assert ids[2] > 0, "grass_block 应可解析" - assert ids[3] == -1, "不存在的方块应返回 -1" - assert r["unknown"] == 1 - print(f" ✓ 桥协议(blocks={reg['blockCount']}, states={reg['stateCount']}, " - f"resolve 3 中 / 1 未知)") + assert reg["blockCount"] > 500 and reg["stateCount"] > 1000 + assert br.lookup_state(1)["block"] == "minecraft:stone" + ids = br.resolve_blocks([ + "minecraft:stone", "minecraft:oak_log[axis=y]", + "minecraft:grass_block[snowy=false]", "minecraft:no_such_block", + ]) + assert ids[0] > 0 and ids[1] > 0 and ids[2] > 0 + assert ids[3] == -1, "未知方块应返回 -1" + print(f" ✓ 名字↔id(blocks={reg['blockCount']}, states={reg['stateCount']})") finally: br.stop() -def test_bridge_render_synthetic(): - """合成数据渲染:验证「注入 → 网格重建 → 出图」全链路。""" - ok, why = _bridge_available() - if not ok: +# ---------------------------------------------------------------- 渲染(新路径) + +def test_set_blocks_render(): + """新路径:Python 只发 RLE,Java 组装协议 + 官方序列化 + 入 cache。""" + br, why = _open_backend() + if br is None: print(f" - 跳过({why})") return - br = RenderBridge( - game_dir=str(STORE / "instances" / "default"), - assets=str(STORE / "assets"), asset_index="1.20.1", - ) try: - br.start() - except RenderBridgeError as e: - print(f" - 跳过(启动失败:{str(e)[:80]})") - return - try: - # 先设视野中心(否则区块被丢弃) - br._request({"cmd": "viewport", "cx": 0, "cz": 0, "radius": 12}) + cap = br._request({"cmd": "capacity"}) + n_sec = cap["sectionsCount"] + 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 - # 造:地板 stone(索引 4 = y=0 = 世界 y=64)+ 中间一根原木 - stone = br._request({"cmd": "resolve", "blocks": ["minecraft:stone"]})["ids"][0] - log = br._request({"cmd": "resolve", "blocks": ["minecraft:oak_log[axis=y]"]})["ids"][0] - assert stone > 0 and log > 0 - - payload = bytearray() - for sy in range(24): - ids = [0] * SECTION_VOLUME - if sy == 4: - ids = [stone] * SECTION_VOLUME # 地板(世界 y=64) - elif sy in (5, 6, 7): - for x in range(7, 9): # 中间 2x2 柱子 + br.set_viewport(0, 0, radius=12) + sections = {} + for idx in range(n_sec): + v = [0] * SECTION_VOLUME + if idx < 4: + v = [stone] * SECTION_VOLUME # 地基 + elif idx == 4: + v = [grass] * SECTION_VOLUME # 地表 + elif idx == 5: + for x in range(7, 9): # 索引 = x + z*16 + y*256 for z in range(7, 9): - ids[x + z * 16] = log - non_empty = sum(1 for v in ids if v != 0) - payload += (non_empty & 0xFFFF).to_bytes(2, "big") - payload += _encode_paletted(ids, SECTION_VOLUME) - payload += bytes([0]) + _varint(1) + _varint(0) + v[x + z * 16] = log + sections[idx] = v - items = [(cx, cz, bytes(payload)) for cx in range(-3, 4) for cz in range(-3, 4)] - fed = br._request({"cmd": "chunks", "items": [ - {"cx": cx, "cz": cz, "data": __import__("base64").b64encode(d).decode()} - for cx, cz, d in items - ]}, timeout=180) - assert fed["failed"] == 0, f"注入失败: {fed.get('firstError')}" - assert fed["loadedChunks"] > 0, f"仓库为空: {fed}" + total_placed = 0 + for cx in range(-3, 4): + for cz in range(-3, 4): + r = br.set_blocks(cx, cz, sections) + assert r["ok"], r + total_placed += r["placed"] + assert total_placed > 0, "应写入方块" - br.set_camera(8.0, 72.0, 8.0, yaw=45.0, pitch=25.0) + wr = WorldRenderer(br) + wr._capacity = cap + dbg = br.debug() + assert dbg["loadedChunks"] > 0, f"仓库为空: {dbg}" + + br.set_camera(8.0, 72.0, 8.0, yaw=45.0, pitch=30.0) res = br.render(frames=80) - assert res.width > 0 and res.height > 0 assert res.chunks_rendered > 0, f"没有区块被渲染: {res.raw}" assert res.distinct_colors > 400, f"画面过单调: {res.distinct_colors}" - assert res.has_world_content is True - print(f" ✓ 合成数据渲染(chunks={res.chunks_rendered}, " - f"colors={res.distinct_colors}, {len(res.image)}B PNG)") + assert res.has_world_content + print(f" ✓ 新路径渲染(placed={total_placed}, chunks={res.chunks_rendered}, " + f"colors={res.distinct_colors})") finally: br.stop() -def test_bridge_render_real_save(): - """真实存档渲染:最终验收判据。""" - ok, why = _bridge_available() +def test_real_save_render(): + """真实存档经新路径(RLE 路由)渲染 —— 最终验收。""" + ok, why = _available() if not ok: print(f" - 跳过({why})") return if not (SAVE / "level.dat").is_file(): print(" - 跳过(无测试存档)") return - from mcfold.render import WorldRenderer wr = WorldRenderer.open(store=str(STORE), java="java") try: - info = wr.load_save(SAVE, center=(8, -7), radius=4) + info = wr.load_save(SAVE, center=(8, -7), radius=3) assert info["injected"] > 0, info assert info["unknownBlocks"] == 0, f"有未解析方块: {info}" - wr.camera(x=130.0, y=95.0, z=-110.0, yaw=135.0, pitch=35.0) - dbg = wr.debug() - assert dbg["loadedChunks"] > 0, f"仓库为空: {dbg}" - assert dbg["playerChunk"] == "8,-7", f"相机区块不对: {dbg}" - - res = wr.screenshot(frames=100) + d = wr.debug() + assert d["loadedChunks"] > 0, d + res = wr.screenshot(frames=90) assert res.chunks_rendered > 0, res.raw assert res.distinct_colors > 2000, f"真实地形应色彩丰富: {res.distinct_colors}" - assert res.has_world_content is True + assert res.has_world_content print(f" ✓ 真实存档渲染(injected={info['injected']}, " f"specs={info['uniqueSpecs']}, chunks={res.chunks_rendered}, " f"colors={res.distinct_colors})") @@ -180,5 +223,6 @@ if __name__ == "__main__": import traceback print(f" ✗ {t.__name__}: {e}") traceback.print_exc() - print(f"\n渲染桥联测: " + ("全部通过" if failed == 0 else f"{len(tests)-failed}/{len(tests)} 通过")) + print(f"\n渲染联测: " + + ("全部通过" if failed == 0 else f"{len(tests)-failed}/{len(tests)} 通过")) sys.exit(1 if failed else 0)