Compare commits
828 commits
holding-bl
...
next
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
253e094c74 | ||
|
|
fef94f03fb | ||
|
|
e9f91f8ecd | ||
|
|
634df8d03d |
||
|
|
a88c8b5470 | ||
|
|
f51254d97a | ||
|
|
05cd560d6b | ||
|
|
b239636356 | ||
|
|
4f421ae45f | ||
|
|
3b94889bed |
||
|
|
636a7fdb54 |
||
|
|
c930365e32 | ||
|
|
852dd737ae | ||
|
|
06dc3cb033 | ||
|
|
c4097975bf | ||
|
|
1525fac2a1 | ||
|
|
f24cb49a87 | ||
|
|
0b1183f541 | ||
|
|
739a6fad24 | ||
|
|
7f7a14ac65 | ||
|
|
265d02d18d | ||
|
|
b2e36840b9 |
||
|
|
7043bf49f3 |
||
|
|
528d8f516b |
||
|
|
70534d8b5a |
||
|
|
9d54c70fb7 | ||
|
|
7e3ba8bece | ||
|
|
513201be87 | ||
|
|
cb82188272 | ||
|
|
d0d5234ba4 | ||
|
|
e81d608554 | ||
|
|
1f240d8c20 | ||
|
|
2a1746eb7a | ||
|
|
9718610131 | ||
|
|
8f62fbd4da | ||
|
|
bc2972fe99 | ||
|
|
a12c61bc6c |
||
|
|
6e0d54ea17 | ||
|
|
72e9e656cc | ||
|
|
4a5f2e799c | ||
|
|
a8fa3d47d1 | ||
|
|
9a84a7acfb | ||
|
|
d6eb1601e9 | ||
|
|
e1293b6cb3 | ||
|
|
cc4f705aea | ||
|
|
54c114a702 | ||
|
|
65575e2665 | ||
|
|
1ddaa79162 |
||
|
|
e2b141cca0 | ||
|
|
15e3325971 | ||
|
|
60fc5ef315 | ||
|
|
8827aab981 | ||
|
|
0a474e6780 | ||
|
|
cdd8c31a0e | ||
|
|
e7c358d3fc | ||
|
|
fb395041b9 | ||
|
|
353ba2ecb3 | ||
|
|
53cbff7699 | ||
|
|
caf4695637 | ||
|
|
167b49da08 |
||
|
|
d7bd26b6b5 | ||
|
|
d41527edc8 | ||
|
|
24ab260e8e | ||
|
|
c4b284b9b7 | ||
|
|
67855ae25a |
||
|
|
b9c8ade9bf | ||
|
|
4d7e3df859 |
||
|
|
a498778703 | ||
|
|
b6d4728c44 | ||
|
|
0dca8bbbe5 | ||
|
|
de9bfba3a8 | ||
|
|
45408476a5 | ||
|
|
c360115f60 | ||
|
|
a8635e9e2f |
||
|
|
5bd33a546a |
||
|
|
e9c7840dae | ||
|
|
52c0c75ccf | ||
|
|
b2f2d85e4f | ||
|
|
7a83a2a657 | ||
|
|
64da602294 | ||
|
|
a09cd7d3ed |
||
|
|
39aca1735e |
||
|
|
e9320c68d2 | ||
|
|
95cc0e6c74 | ||
|
|
826b24d9e2 |
||
|
|
16609aa010 | ||
|
|
09b0e2e493 | ||
|
|
c844b99cf2 | ||
|
|
089f2224e2 | ||
|
|
2f93c08b1e | ||
|
|
fa56d479b1 | ||
|
|
f489c5f477 |
||
|
|
45bc76d825 | ||
|
|
01567ea589 | ||
|
|
e8b0a34c0b | ||
|
|
5cfd301d10 | ||
|
|
cdd23bc6a6 | ||
|
|
4277c3a262 |
||
|
|
9f3d3f93fb |
||
|
|
7162d2f549 | ||
|
|
fcf987efe4 | ||
|
|
d112b01177 | ||
|
|
043e28ed97 | ||
|
|
08fbc67c31 | ||
|
|
3bf34a8781 | ||
|
|
3cc862b05d | ||
|
|
c913d63c46 | ||
|
|
3320f65b9c | ||
|
|
ed7c33ff9f | ||
|
|
9086435aee | ||
|
|
8a50412395 | ||
|
|
71257bdf13 | ||
|
|
7aea07f83a | ||
|
|
3bcf0f533a | ||
|
|
b1298cbe1f |
||
|
|
661892af7c | ||
|
|
c55827db96 | ||
|
|
a2711dbe6c | ||
|
|
f79e54f11d | ||
|
|
6f5239e1d8 | ||
|
|
13e145cc3a | ||
|
|
d4ff7de64e | ||
|
|
1310109c01 | ||
|
|
dc2c5a2d88 | ||
|
|
31b91e5a33 |
||
|
|
f2a11d0a73 |
||
|
|
6eae7136ec | ||
|
|
34eecc166f | ||
|
|
fec887c28d | ||
|
|
369166e094 | ||
|
|
e161426caf | ||
|
|
0e4435ef91 | ||
|
|
3336680a0e | ||
|
|
83d783226f | ||
|
|
af5a0b2835 | ||
|
|
eedd9f1b8f | ||
|
|
0b1bc76327 | ||
|
|
b839bb8b9b | ||
|
|
3a7f267b5b | ||
|
|
2055579b72 | ||
|
|
1148378ce6 | ||
|
|
383e6c4d80 | ||
|
|
e9e144621f | ||
|
|
332bd4e0f3 |
||
|
|
32b19ab7af | ||
|
|
5221104980 | ||
|
|
7c8ccba2c1 | ||
|
|
fdeb78d96b | ||
|
|
5269ad21b5 |
||
|
|
f126f56844 | ||
|
|
1b20845ed5 | ||
|
|
f3ff4bef03 |
||
|
|
679c3775f7 | ||
|
|
8c71f70db2 | ||
|
|
9f3079b5f5 | ||
|
|
794cafb1f6 | ||
|
|
a3dcfed4d0 | ||
|
|
b69813435c | ||
|
|
1e513f87dd | ||
|
|
243db1dc45 | ||
|
|
ac7d28760f | ||
|
|
cfce898918 | ||
|
|
14effc7400 |
||
|
|
a562316cba | ||
|
|
6a583d2a36 | ||
|
|
5575933559 | ||
|
|
e982bf1493 | ||
|
|
1c93fd7f60 | ||
|
|
a2e9404a70 |
||
|
|
38a1d83cf2 | ||
|
|
314ddf7215 | ||
|
|
829e588ac1 | ||
|
|
8b2276a7ae |
||
|
|
7635375471 |
||
|
|
087e167826 |
||
|
|
c500d08ed7 | ||
|
|
50907138f7 | ||
|
|
99d05fc94b | ||
|
|
0c68e63ba6 | ||
|
|
04a85e9bd1 | ||
|
|
3cd778538c |
||
|
|
9726257577 | ||
|
|
5a663aac2f |
||
|
|
7cea1b8755 |
||
|
|
5ea2ab9c1a | ||
|
|
b36d08528f | ||
|
|
a4e70768dd | ||
|
|
ecb53fab88 | ||
|
|
b2ef71fc19 | ||
|
|
f4196d6aba |
||
|
|
4f78534ca4 |
||
|
|
7799ccc370 |
||
|
|
5efe3508df | ||
|
|
4d70128ac6 |
||
|
|
b4df2e1837 | ||
|
|
83366ec5fa | ||
|
|
b2a1bd10e4 | ||
|
|
90c283c5ee |
||
|
|
67dbd56f14 | ||
|
|
517f5d3501 | ||
|
|
7f6fc00f02 | ||
|
|
f5835f54fa | ||
|
|
970ed614ae | ||
|
|
080d75f939 | ||
|
|
67d365b9c3 | ||
|
|
e2a0df748e | ||
|
|
2dc811b2a1 | ||
|
|
a5d16a75ef | ||
|
|
785ab490f2 |
||
|
|
a9b94ec897 | ||
|
|
42f973e057 |
||
|
|
ff29fc1fc5 |
||
|
|
051cc5b35c | ||
|
|
f921275c87 | ||
|
|
2b0f178fe0 | ||
|
|
6302a3815f | ||
|
|
0a61bbba75 |
||
|
|
7ed3413b28 | ||
|
|
75adc29bf0 | ||
|
|
489c16793b | ||
|
|
48cdd9484f | ||
|
|
e2400ee667 |
||
|
|
a58ff0776e |
||
|
|
674b6ab00d | ||
|
|
25f2fdef4e | ||
|
|
f76c7fb782 | ||
|
|
aa817139b7 | ||
|
|
58799d973c | ||
|
|
b3392bea6b |
||
|
|
bf3381c803 |
||
|
|
bb9bb48efd |
||
|
|
28022a2054 | ||
|
|
1845530e22 |
||
|
|
b01cfe475d | ||
|
|
22483d7a76 | ||
|
|
e250061757 | ||
|
|
1fd9a29192 | ||
|
|
29c6a3d739 | ||
|
|
cd7c053a3c | ||
|
|
5bfb9bebd7 | ||
|
|
951790dad6 |
||
|
|
c5f72f2fb3 |
||
|
|
f12de4ea23 | ||
|
|
813c952420 | ||
|
|
ec142c0ce4 | ||
|
|
378b668d46 | ||
|
|
0d9cb0625e |
||
|
|
221f99ffdf | ||
|
|
4f1cb85301 | ||
|
|
5bf66b8e50 | ||
|
|
5caca68e8e | ||
|
|
95163fb288 | ||
|
|
0f2e4f1329 | ||
|
|
aa0024faa2 | ||
|
|
1599917134 | ||
|
|
e706f7d086 | ||
|
|
e20fb8be53 | ||
|
|
cd2ff62d6d | ||
|
|
fa36ed2678 | ||
|
|
305f4d8a31 | ||
|
|
86ef4f268e | ||
|
|
0c7900a655 | ||
|
|
8c37db4051 | ||
|
|
4ded3b5d2b | ||
|
|
db1b72a582 | ||
|
|
89fc31a2c2 | ||
|
|
01b6d87331 | ||
|
|
a654396238 | ||
|
|
948a52a2a5 | ||
|
|
d0ac00843d | ||
|
|
4ca9a801a8 | ||
|
|
510d163067 | ||
|
|
97533cfddb | ||
|
|
b30e7fc152 | ||
|
|
d7fdf18416 | ||
|
|
28faa9417a |
||
|
|
109daa2783 | ||
|
|
14ad1c5934 | ||
|
|
585b19d8dc | ||
|
|
71f63a3be0 | ||
|
|
2b881ea5ba | ||
|
|
a0bfa275af | ||
|
|
193c748feb | ||
|
|
c3112794c0 | ||
|
|
dbfd2b23f6 | ||
|
|
9646fbbc0f | ||
|
|
a7c35df959 | ||
|
|
529b465d32 | ||
|
|
1582e16d3b | ||
|
|
e8b1f190a7 | ||
|
|
73ccb48d02 | ||
|
|
143d4a3bb3 | ||
|
|
f5ed17d2fb | ||
|
|
6a8de1fdfb | ||
|
|
a541e82e04 | ||
|
|
c5e8fcb90c | ||
|
|
7a53d4de63 | ||
|
|
83502eba60 | ||
|
|
70557a6282 | ||
|
|
7e5a12934c | ||
|
|
4b85b16b73 | ||
|
|
27df313f26 | ||
|
|
77449c5c12 | ||
|
|
deb8ec6c0f | ||
|
|
024da5bf6d | ||
|
|
c755f085d9 |
||
|
|
3c6ee2dbb3 | ||
|
|
bf790861d9 | ||
|
|
a977d09031 |
||
|
|
1fbbf36859 |
||
|
|
31d5089e9c | ||
|
|
7824cf64a2 | ||
|
|
f4bd38fa5c | ||
|
|
5adbce39e0 | ||
|
|
0b72ea61c7 | ||
|
|
33a6f4d088 | ||
|
|
758405da03 | ||
|
|
4fc8011413 | ||
|
|
d347957f64 | ||
|
|
0a85de180e | ||
|
|
cc264e895f | ||
|
|
4dce591f8b | ||
|
|
b35b88236d | ||
|
|
f79472a1da | ||
|
|
d1a646ed54 | ||
|
|
914dcb6110 | ||
|
|
23bab8dbd5 | ||
|
|
881d105c57 | ||
|
|
3109be2d8a | ||
|
|
568ea3d18b | ||
|
|
27e51b65df | ||
|
|
0aa4d11bdd |
||
|
|
ce5ef7c7cb | ||
|
|
9b71ae1a24 | ||
|
|
908fa64f2f |
||
|
|
c025a1c75a | ||
|
|
04c37c1eef | ||
|
|
dbfadde044 | ||
|
|
d78a8b1220 | ||
|
|
70fbe1b0e2 | ||
|
|
394a12b147 | ||
|
|
f895304380 | ||
|
|
4f45cd072a | ||
|
|
5af290ac4e | ||
|
|
c5c9fd9bcd | ||
|
|
1b9b6c954c | ||
|
|
3c2ed440b6 | ||
|
|
9c6bc49921 | ||
|
|
1a87951bc8 | ||
|
|
73e65c6656 | ||
|
|
c324ce29ab | ||
|
|
b666f6e3c3 | ||
|
|
115022a21b | ||
|
|
18ee1dc532 | ||
|
|
291ead079a | ||
|
|
983b8a184b | ||
|
|
66fa59a87a | ||
|
|
47864f0023 |
||
|
|
6f15fcc726 | ||
|
|
af9da93978 | ||
|
|
08bb0b6777 | ||
|
|
187e9fa6b4 | ||
|
|
4fd290c636 | ||
|
|
e2f28e4975 | ||
|
|
c3b4eb953f | ||
|
|
850ae6c2da | ||
|
|
66b9f58c6f | ||
|
|
b58950bec2 | ||
|
|
47be0ac865 |
||
|
|
cd9b796f16 | ||
|
|
b32bab8211 | ||
|
|
52755fc18f | ||
|
|
f8800d5a31 | ||
|
|
797459b0fc | ||
|
|
f8ef748e58 | ||
|
|
f161fd31d4 | ||
|
|
3690cb22aa | ||
|
|
33debc1475 | ||
|
|
46787309e2 | ||
|
|
1015556834 | ||
|
|
db1c8a1e1a | ||
|
|
761c92e27c | ||
|
|
118377cbc3 | ||
|
|
8786448d07 | ||
|
|
2d288153e3 | ||
|
|
a53a6e5f03 | ||
|
|
237aeec6ac | ||
|
|
0f3145bb8e |
||
|
|
df10bc6f1b | ||
|
|
89a8584060 | ||
|
|
b6842508ae | ||
|
|
563f5fa007 |
||
|
|
4a4823fd6a | ||
|
|
f87e7850ec | ||
|
|
e1758a84d0 | ||
|
|
fdd770eeb9 | ||
|
|
abe75c7b8d | ||
|
|
6b1a82a6b3 | ||
|
|
b0eb73cd76 | ||
|
|
8714fd484b | ||
|
|
ba0287f278 | ||
|
|
2277020de7 | ||
|
|
c1012a77d0 | ||
|
|
1b96577402 | ||
|
|
5bb09a88bc | ||
|
|
36bf18b02f |
||
|
|
da35cfb8a2 | ||
|
|
3e056946ec | ||
|
|
72028d925d | ||
|
|
897c991a0e |
||
|
|
baa6158872 | ||
|
|
a67b9d7aa2 | ||
|
|
518d6ad866 | ||
|
|
09cd2c3f64 | ||
|
|
a8564232f7 | ||
|
|
91dc4d1007 | ||
|
|
d921977caf | ||
|
|
1267dcceae | ||
|
|
c947b285ea | ||
|
|
09e61c9aa0 | ||
|
|
e1831eea38 | ||
|
|
214828df0c | ||
|
|
ad13ab83f2 | ||
|
|
fbb3d08bfa | ||
|
|
78313ee225 | ||
|
|
10ed5b6dfb | ||
|
|
87e5ae253d | ||
|
|
b8b1320258 |
||
|
|
f9c042b00f | ||
|
|
a6018c6891 | ||
|
|
ec953fd5d1 | ||
|
|
6263c9ae66 | ||
|
|
d5cc6d325e | ||
|
|
734d195be0 | ||
|
|
14d3bba5f5 | ||
|
|
e60d10e121 | ||
|
|
8232737a75 | ||
|
|
1c2e249031 | ||
|
|
a7e6f9772c |
||
|
|
28da4e60f0 | ||
|
|
4cde65e635 | ||
|
|
d35bf41e8c | ||
|
|
e7b012c08d | ||
|
|
a27fa4cd1d | ||
|
|
c6b8efe4e8 | ||
|
|
a846eb4500 | ||
|
|
6fb18d4438 | ||
|
|
b9df1bcf9e | ||
|
|
0db49e7879 |
||
|
|
998f0f0a85 | ||
|
|
465ce35e83 | ||
|
|
1c700aac1e |
||
|
|
4b54be637d | ||
|
|
1d4dc0ddaa | ||
|
|
874cafc75e | ||
|
|
2a8f514095 | ||
|
|
2619e5da89 | ||
|
|
b0da1e41d6 | ||
|
|
10f17063c0 | ||
|
|
ceb4cb0b66 |
||
|
|
fa9c0813c3 | ||
|
|
dec93c2b64 | ||
|
|
d348a44bb8 | ||
|
|
dffadbb06c | ||
|
|
2414111b9c | ||
|
|
edad57a225 | ||
|
|
52ae41a78d |
||
|
|
8ff05924dd |
||
|
|
322e2f9b44 | ||
|
|
89fd5dde71 | ||
|
|
deedcda467 | ||
|
|
ecf55727bc | ||
|
|
59cb442225 | ||
|
|
e8d980b790 | ||
|
|
acd8144d76 | ||
|
|
b7560f716a | ||
|
|
2833b33b4e | ||
|
|
077dc9df26 | ||
|
|
0b2d676d93 | ||
|
|
6d29413a5d | ||
|
|
2f200a876a | ||
|
|
cde239211c | ||
|
|
2f29a9a5cb | ||
|
|
2e3363dce8 | ||
|
|
2cb8bea374 | ||
|
|
bdea1fc50c | ||
|
|
9613a0e644 | ||
|
|
75e44407ed | ||
|
|
0505b64539 | ||
|
|
334e8a502d | ||
|
|
1387cb036b |
||
|
|
81a692272c | ||
|
|
78d923d817 | ||
|
|
f0d5ad616d | ||
|
|
ba6a618443 | ||
|
|
a268c69879 | ||
|
|
2f81bafd75 |
||
|
|
7110b8c66d |
||
|
|
8e4987e685 | ||
|
|
2ea74b22fd | ||
|
|
730cf656de | ||
|
|
fda38bbf59 | ||
|
|
f1c945d22a |
||
|
|
3b9503982c | ||
|
|
795f241cbd |
||
|
|
5fd3584823 | ||
|
|
11bfcb8f1a | ||
|
|
bfd88ce544 | ||
|
|
0bb6301056 | ||
|
|
b7e6793c07 | ||
|
|
a0a01d9a3f | ||
|
|
f8c44ae4f0 | ||
|
|
44d630b1b3 | ||
|
|
0cd11ebe29 | ||
|
|
7196623853 | ||
|
|
612b35426e | ||
|
|
191043380e | ||
|
|
d83fbb0393 |
||
|
|
9a8451fff7 | ||
|
|
5e0ece8288 | ||
|
|
c1bf8bf1d7 | ||
|
|
4223800fd8 | ||
|
|
4a1138f21c | ||
|
|
5f6ce69b51 |
||
|
|
a20dca18f8 |
||
|
|
874a3b3ab0 |
||
|
|
847fed5142 | ||
|
|
65af9a73c2 |
||
|
|
0f29053ca6 | ||
|
|
df4dd69c80 | ||
|
|
2f70575534 | ||
|
|
25c07b14a9 |
||
|
|
cab9eed5e7 |
||
|
|
e2ee1ff133 |
||
|
|
193616b147 | ||
|
|
90e002a3a1 | ||
|
|
8595d545a5 | ||
|
|
cf8d8e51fc | ||
|
|
946fc26d86 | ||
|
|
d05898ab1c | ||
|
|
85fbe0cec0 | ||
|
|
0dd7b4d802 | ||
|
|
f03046573d | ||
|
|
208f5b7e0d | ||
|
|
d27f2b4a61 | ||
|
|
e9e8641f10 | ||
|
|
cea4d7f277 | ||
|
|
dcbeed42ad | ||
|
|
82693ac80c | ||
|
|
7ab03e3245 | ||
|
|
f560e952d3 | ||
|
|
c063ff7244 | ||
|
|
f96673bc17 |
||
|
|
b196ea5955 | ||
|
|
43580511e2 | ||
|
|
17dc564ef1 | ||
|
|
c0dc516a2d | ||
|
|
a0bf1e6a71 | ||
|
|
d295100f34 |
||
|
|
d7374a5206 | ||
|
|
2993bd2e45 | ||
|
|
c5a895a18e | ||
|
|
9be5950760 |
||
|
|
c81da88eb7 | ||
|
|
3e00dcb3e9 | ||
|
|
1b7ccbd77f | ||
|
|
3bfaa2980c | ||
|
|
09f91d1491 | ||
|
|
42275df14d | ||
|
|
e54c262972 | ||
|
|
20830bc257 | ||
|
|
6bf4a4f2a3 | ||
|
|
97e1395464 | ||
|
|
2ccc462679 | ||
|
|
e2a093baf1 |
||
|
|
50baabad90 | ||
|
|
0d778dad80 | ||
|
|
d4345b19f1 | ||
|
|
b5a16d520a | ||
|
|
32acb55526 | ||
|
|
41489130d6 | ||
|
|
72058f14f2 |
||
|
|
4a77ba15b6 | ||
|
|
84d38ba21c | ||
|
|
d4e93d4d39 |
||
|
|
6ad1a4d63b |
||
|
|
b35685fc13 | ||
|
|
5bc55c1bdf | ||
|
|
4cc6767c78 | ||
|
|
297d94d419 | ||
|
|
379484327e |
||
|
|
b5a8bf16ff | ||
|
|
978bd16785 | ||
|
|
51d8975f5e | ||
|
|
28552bd1de | ||
|
|
ad2ba0e24f | ||
|
|
b7da6e201f | ||
|
|
81aaaab76d | ||
|
|
8766eaae21 | ||
|
|
92ce4dd0d8 | ||
|
|
f755385981 | ||
|
|
14b7cb039a | ||
|
|
28f0546f3b | ||
|
|
317b84943a | ||
|
|
2490fbe211 | ||
|
|
df442338f8 |
||
|
|
a628f64d37 | ||
|
|
1f5404be9d | ||
|
|
3de1089a1c |
||
|
|
062115c42b |
||
|
|
41684bc028 |
||
|
|
a442acf49a | ||
|
|
ff36f75e44 |
||
|
|
91ef3ccd3c | ||
|
|
e6ce6dc268 | ||
|
|
5a948828b7 |
||
|
|
6d91ad3d41 | ||
|
|
ccc1d760e0 | ||
|
|
463b9ef80a | ||
|
|
382a685b65 | ||
|
|
5694d14d58 | ||
|
|
491b5d66c5 | ||
|
|
3fc644082a | ||
|
|
7186b183f1 | ||
|
|
84bc3d5911 | ||
|
|
540f90fd0c | ||
|
|
1a45381b3d | ||
|
|
e0be30b86f | ||
|
|
7d6986ccb4 | ||
|
|
798eb34466 | ||
|
|
77e529b6f4 | ||
|
|
f2fde37e6a | ||
|
|
61659d82b4 | ||
|
|
4ce95de03f | ||
|
|
d0f7f57400 | ||
|
|
b052422c99 | ||
|
|
ce8e414528 | ||
|
|
8f91d282d2 | ||
|
|
b946271d87 | ||
|
|
102a3e499b | ||
|
|
2edf425ecf | ||
|
|
5d480d4265 | ||
|
|
a5bd38619c | ||
|
|
cbd90aeb53 | ||
|
|
c070fe836f | ||
|
|
30dd64a3c6 | ||
|
|
87b795bdc4 | ||
|
|
36747e6bef | ||
|
|
9dad509bc2 | ||
|
|
d754593849 | ||
|
|
a064892a9b | ||
|
|
dd7c9c172e | ||
|
|
cb82963b86 |
||
|
|
16fe17edf5 | ||
|
|
db7a9b9dd2 | ||
|
|
bf676cdf52 | ||
|
|
b13c8df469 | ||
|
|
c289283e7f | ||
|
|
10e14bd675 | ||
|
|
10ee4c00ae | ||
|
|
961cf01a0e | ||
|
|
2c0b99ffdb | ||
|
|
51c1346456 | ||
|
|
064d70480d | ||
|
|
7b0ead5595 | ||
|
|
ee257d7916 |
||
|
|
45b8ae6f7c | ||
|
|
652120c71b | ||
|
|
963a769db3 | ||
|
|
5f87385486 | ||
|
|
372583be7d | ||
|
|
725f6ec364 | ||
|
|
ad0502dcb9 | ||
|
|
9b5155d3fe | ||
|
|
b10c6809ff | ||
|
|
4d411fe561 |
||
|
|
c783094068 | ||
|
|
13a55b4414 | ||
|
|
0624e018dd | ||
|
|
a4c86d707b | ||
|
|
689bebde3d | ||
|
|
376c358d43 | ||
|
|
5902918729 | ||
|
|
c5f6c087ac | ||
|
|
75965203fc | ||
|
|
2d77bdb9b2 | ||
|
|
50d1d37ff3 | ||
|
|
37b84ae003 | ||
|
|
74cb815940 | ||
|
|
b02b250ee8 | ||
|
|
68dba89bf5 | ||
|
|
2f21e2b453 | ||
|
|
3c5c8b78e3 | ||
|
|
18a191a03c | ||
|
|
3bacef1251 | ||
|
|
5783b98e74 | ||
|
|
84dce0941c | ||
|
|
cf7c4664f2 | ||
|
|
0b8eaa4ad2 | ||
|
|
dd20994f78 |
||
|
|
af088d92e1 |
||
|
|
b89cf522a0 | ||
|
|
50f35cf176 |
||
|
|
c97dbbde9a | ||
|
|
b881a61610 | ||
|
|
c44ad90acf | ||
|
|
4dac577dfc |
||
|
|
f2552e70e1 |
||
|
|
c441792d3f | ||
|
|
85ece5b4eb | ||
|
|
0506d9de47 | ||
|
|
a0a2c628b4 | ||
|
|
e28608f86e | ||
|
|
c8c4e3267d | ||
|
|
d32f510744 | ||
|
|
32931efef0 | ||
|
|
574cdfc531 | ||
|
|
c303a0e0d5 | ||
|
|
7284d88ae9 | ||
|
|
270da682da | ||
|
|
ab3174a45f | ||
|
|
0f2bc5c1d4 | ||
|
|
dcc0960d7f | ||
|
|
c279b4bbe6 | ||
|
|
b267cb77be | ||
|
|
b3a089323f | ||
|
|
2c441c3434 | ||
|
|
ad9f5be486 | ||
|
|
5405987de3 | ||
|
|
667eff49af | ||
|
|
26d25b77f5 | ||
|
|
7d0c3643d3 | ||
|
|
5791626cc5 | ||
|
|
84b3f8913d | ||
|
|
03c6a3f724 |
||
|
|
f13c4e4581 | ||
|
|
2fbfc18d2e | ||
|
|
7d699f24bb | ||
|
|
547525d615 | ||
|
|
900bcb0b56 | ||
|
|
dbd4058912 | ||
|
|
153101fa6f | ||
|
|
d497299235 | ||
|
|
6b23eb6bad | ||
|
|
5fa019e7b3 |
||
|
|
ebb5056540 | ||
|
|
ece59e1744 | ||
|
|
8955075d75 |
||
|
|
427ec21213 | ||
|
|
42cc0bd818 | ||
|
|
9a7a13c2dd | ||
|
|
e35873e106 | ||
|
|
f900d6933c | ||
|
|
6354ba6bb8 | ||
|
|
95c185fc0b | ||
|
|
6c994a54f0 |
||
|
|
de6e82d94f | ||
|
|
347d155884 | ||
|
|
70867564ed | ||
|
|
a4180500d1 | ||
|
|
b21146b92a | ||
|
|
c53ba87309 | ||
|
|
a1c41e8767 | ||
|
|
e19980800b |
||
|
|
0368e12635 | ||
|
|
bdcde9a4bb |
||
|
|
af0d7d14ec | ||
|
|
bd180ef652 | ||
|
|
0a0b87bee6 |
||
|
|
5b56518122 | ||
|
|
ab5f6ab448 | ||
|
|
2953554c53 | ||
|
|
00150dda1d | ||
|
|
40f81d84cd |
||
|
|
3ea95d509a | ||
|
|
9bac681c29 | ||
|
|
7da41b02c9 |
||
|
|
18a6f2c1f5 | ||
|
|
33437823f3 | ||
|
|
d0b921a48e | ||
|
|
16bb43c7d9 | ||
|
|
5a3fb6f225 | ||
|
|
755eead976 | ||
|
|
25db002bc3 | ||
|
|
74fe84e10d | ||
|
|
76bed4d496 | ||
|
|
4d3c92f611 | ||
|
|
1446ccc0a7 | ||
|
|
f9b87d5087 | ||
|
|
18bf1aa80a | ||
|
|
2c971f331e | ||
|
|
a5dddfaad5 | ||
|
|
c6c25a7bb9 | ||
|
|
3fb872129e | ||
|
|
ad8dc1a21a | ||
|
|
a3ef16a81a | ||
|
|
f518dce04d | ||
|
|
e89196041e | ||
|
|
f9a4960c31 | ||
|
|
d743981fc2 | ||
|
|
fad9fd6e3a | ||
|
|
a063a0d75b | ||
|
|
1c7fdc21a6 | ||
|
|
a30106342e | ||
|
|
9160ff33c2 | ||
|
|
cd9ead74d2 | ||
|
|
3f761430d7 | ||
|
|
8e330c0253 | ||
|
|
d6964b89eb | ||
|
|
ea5a48967b | ||
|
|
9dfff40afd | ||
|
|
65ba687e08 | ||
|
|
a9d2104dbf | ||
|
|
c6ea9f79dc | ||
|
|
266d34c1cf | ||
|
|
5aaa687392 |
||
|
|
306f894d8c | ||
|
|
9e7711e386 | ||
|
|
684261e515 | ||
|
|
c2a34ea9f1 | ||
|
|
698fb1d388 | ||
|
|
559f535207 | ||
|
|
b2ac80602c | ||
|
|
0d3a3affd7 | ||
|
|
00dd606091 | ||
|
|
574dbafc28 | ||
|
|
66d26ad2e6 |
||
|
|
ee966395c6 |
496 changed files with 56572 additions and 38794 deletions
18
.cursor/rules/vars-usage.mdc
Normal file
18
.cursor/rules/vars-usage.mdc
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
---
|
||||
description: Restricts usage of the global Mineflayer `bot` variable to only src/ files; prohibits usage in renderer/. Specifies correct usage of player state and appViewer globals.
|
||||
globs: src/**/*.ts,renderer/**/*.ts
|
||||
alwaysApply: false
|
||||
---
|
||||
Ask AI
|
||||
|
||||
- The global variable `bot` refers to the Mineflayer bot instance.
|
||||
- You may use `bot` directly in any file under the `src/` directory (e.g., `src/mineflayer/playerState.ts`).
|
||||
- Do **not** use `bot` directly in any file under the `renderer/` directory or its subfolders (e.g., `renderer/viewer/three/worldrendererThree.ts`).
|
||||
- In renderer code, all bot/player state and events must be accessed via explicit interfaces, state managers, or passed-in objects, never by referencing `bot` directly.
|
||||
- In renderer code (such as in `WorldRendererThree`), use the `playerState` property (e.g., `worldRenderer.playerState.gameMode`) to access player state. The implementation for `playerState` lives in `src/mineflayer/playerState.ts`.
|
||||
- In `src/` code, you may use the global variable `appViewer` from `src/appViewer.ts` directly. Do **not** import `appViewer` or use `window.appViewer`; use the global `appViewer` variable as-is.
|
||||
- Some other global variables that can be used without window prefixes are listed in src/globals.d.ts
|
||||
|
||||
Rationale: This ensures a clean separation between the Mineflayer logic (server-side/game logic) and the renderer (client-side/view logic), making the renderer portable and testable, and maintains proper usage of global state.
|
||||
|
||||
For more general project contributing guides see CONTRIBUTING.md on like how to setup the project. Use pnpm tsc if needed to validate result with typechecking the whole project.
|
||||
|
|
@ -3,4 +3,7 @@ rsbuild.config.ts
|
|||
*.module.css.d.ts
|
||||
*.generated.ts
|
||||
generated
|
||||
dist
|
||||
public
|
||||
**/*/rsbuildSharedConfig.ts
|
||||
src/mcDataTypes.ts
|
||||
|
|
@ -23,6 +23,7 @@
|
|||
// ],
|
||||
"@stylistic/arrow-spacing": "error",
|
||||
"@stylistic/block-spacing": "error",
|
||||
"@typescript-eslint/no-this-alias": "off",
|
||||
"@stylistic/brace-style": [
|
||||
"error",
|
||||
"1tbs",
|
||||
|
|
@ -102,6 +103,7 @@
|
|||
// "@stylistic/multiline-ternary": "error", // not needed
|
||||
// "@stylistic/newline-per-chained-call": "error", // not sure if needed
|
||||
"@stylistic/new-parens": "error",
|
||||
"@typescript-eslint/class-literal-property-style": "off",
|
||||
"@stylistic/no-confusing-arrow": "error",
|
||||
"@stylistic/wrap-iife": "error",
|
||||
"@stylistic/space-before-blocks": "error",
|
||||
|
|
@ -197,7 +199,8 @@
|
|||
"no-async-promise-executor": "off",
|
||||
"no-bitwise": "off",
|
||||
"unicorn/filename-case": "off",
|
||||
"max-depth": "off"
|
||||
"max-depth": "off",
|
||||
"unicorn/no-typeof-undefined": "off"
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
|
|
|
|||
59
.github/workflows/benchmark.yml
vendored
Normal file
59
.github/workflows/benchmark.yml
vendored
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
name: Benchmark
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
push:
|
||||
branches:
|
||||
- perf-test
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
if: >-
|
||||
(github.event_name == 'push' && github.ref == 'refs/heads/perf-test') ||
|
||||
(
|
||||
github.event_name == 'issue_comment' &&
|
||||
github.event.issue.pull_request != '' &&
|
||||
(startsWith(github.event.comment.body, '/benchmark'))
|
||||
)
|
||||
permissions:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- run: lscpu
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: "pnpm"
|
||||
- name: Move Cypress to dependencies
|
||||
run: |
|
||||
jq '.dependencies.cypress = .optionalDependencies.cypress | del(.optionalDependencies.cypress)' package.json > package.json.tmp
|
||||
mv package.json.tmp package.json
|
||||
- run: pnpm install --no-frozen-lockfile
|
||||
|
||||
- run: pnpm build
|
||||
- run: nohup pnpm prod-start &
|
||||
|
||||
- run: pnpm test:benchmark
|
||||
id: benchmark
|
||||
continue-on-error: true
|
||||
# read benchmark results from stdout
|
||||
- run: |
|
||||
if [ -f benchmark.txt ]; then
|
||||
# Format the benchmark results for GitHub comment
|
||||
BENCHMARK_RESULT=$(cat benchmark.txt | sed 's/^/- /')
|
||||
echo "BENCHMARK_RESULT<<EOF" >> $GITHUB_ENV
|
||||
echo "$BENCHMARK_RESULT" >> $GITHUB_ENV
|
||||
echo "EOF" >> $GITHUB_ENV
|
||||
else
|
||||
echo "BENCHMARK_RESULT=Benchmark failed to run or produce results" >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
- uses: mshick/add-pr-comment@v2
|
||||
with:
|
||||
allow-repeats: true
|
||||
message: |
|
||||
Benchmark result: ${{ env.BENCHMARK_RESULT }}
|
||||
33
.github/workflows/build-single-file.yml
vendored
Normal file
33
.github/workflows/build-single-file.yml
vendored
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
name: build-single-file
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build-and-bundle:
|
||||
runs-on: ubuntu-latest
|
||||
permissions: write-all
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@master
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Build single-file version - minecraft.html
|
||||
run: pnpm build-single-file && mv dist/single/index.html minecraft.html
|
||||
env:
|
||||
LOCAL_CONFIG_FILE: config.mcraft-only.json
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: minecraft.html
|
||||
path: minecraft.html
|
||||
45
.github/workflows/build-zip.yml
vendored
Normal file
45
.github/workflows/build-zip.yml
vendored
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
name: Make Self Host Zip
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build-and-bundle:
|
||||
runs-on: ubuntu-latest
|
||||
permissions: write-all
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@master
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Build project
|
||||
run: pnpm build
|
||||
env:
|
||||
LOCAL_CONFIG_FILE: config.mcraft-only.json
|
||||
|
||||
- name: Bundle server.js
|
||||
run: |
|
||||
pnpm esbuild server.js --bundle --platform=node --outfile=bundled-server.js --define:process.env.NODE_ENV="'production'"
|
||||
|
||||
- name: Create distribution package
|
||||
run: |
|
||||
mkdir -p package
|
||||
cp -r dist package/
|
||||
cp bundled-server.js package/server.js
|
||||
cd package
|
||||
zip -r ../self-host.zip .
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: self-host
|
||||
path: self-host.zip
|
||||
157
.github/workflows/ci.yml
vendored
157
.github/workflows/ci.yml
vendored
|
|
@ -13,30 +13,165 @@ jobs:
|
|||
with:
|
||||
java-version: 17
|
||||
java-package: jre
|
||||
- name: Install pnpm
|
||||
run: npm i -g pnpm@9.0.4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18
|
||||
node-version: 22
|
||||
# cache: "pnpm"
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
- run: pnpm install
|
||||
- run: pnpm build-single-file
|
||||
- name: Store minecraft.html size
|
||||
run: |
|
||||
SIZE_BYTES=$(du -s dist/single/minecraft.html 2>/dev/null | cut -f1)
|
||||
echo "SIZE_BYTES=$SIZE_BYTES" >> $GITHUB_ENV
|
||||
- run: pnpm check-build
|
||||
- name: Create zip package for size comparison
|
||||
run: |
|
||||
mkdir -p package
|
||||
cp -r dist package/
|
||||
cd package
|
||||
zip -r ../self-host.zip .
|
||||
- run: pnpm build-playground
|
||||
- run: pnpm build-storybook
|
||||
# - run: pnpm build-storybook
|
||||
- run: pnpm test-unit
|
||||
- run: pnpm lint
|
||||
- run: pnpm tsx scripts/buildNpmReact.ts
|
||||
|
||||
- name: Parse Bundle Stats
|
||||
run: |
|
||||
GZIP_BYTES=$(du -s self-host.zip 2>/dev/null | cut -f1)
|
||||
SIZE=$(echo "scale=2; $SIZE_BYTES/1024/1024" | bc)
|
||||
GZIP_SIZE=$(echo "scale=2; $GZIP_BYTES/1024/1024" | bc)
|
||||
echo "{\"total\": ${SIZE}, \"gzipped\": ${GZIP_SIZE}}" > /tmp/bundle-stats.json
|
||||
|
||||
# - name: Compare Bundle Stats
|
||||
# id: compare
|
||||
# uses: actions/github-script@v6
|
||||
# env:
|
||||
# GITHUB_TOKEN: ${{ secrets.GIST_TOKEN }}
|
||||
# with:
|
||||
# script: |
|
||||
# const gistId = '${{ secrets.BUNDLE_STATS_GIST_ID }}';
|
||||
|
||||
# async function getGistContent() {
|
||||
# const { data } = await github.rest.gists.get({
|
||||
# gist_id: gistId,
|
||||
# headers: {
|
||||
# authorization: `token ${process.env.GITHUB_TOKEN}`
|
||||
# }
|
||||
# });
|
||||
# return JSON.parse(data.files['bundle-stats.json'].content || '{}');
|
||||
# }
|
||||
|
||||
# const content = await getGistContent();
|
||||
# const baseStats = content['${{ github.event.pull_request.base.ref }}'];
|
||||
# const newStats = require('/tmp/bundle-stats.json');
|
||||
|
||||
# const comparison = `minecraft.html (normal build gzip)\n${baseStats.total}MB (${baseStats.gzipped}MB compressed) -> ${newStats.total}MB (${newStats.gzipped}MB compressed)`;
|
||||
# core.setOutput('stats', comparison);
|
||||
|
||||
# - run: pnpm tsx scripts/buildNpmReact.ts
|
||||
- run: nohup pnpm prod-start &
|
||||
- run: nohup pnpm test-mc-server &
|
||||
- uses: cypress-io/github-action@v5
|
||||
with:
|
||||
install: false
|
||||
- uses: actions/upload-artifact@v3
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: failure()
|
||||
with:
|
||||
name: cypress-images
|
||||
path: cypress/integration/__image_snapshots__/
|
||||
- run: node scripts/outdatedGitPackages.mjs
|
||||
if: github.ref == 'refs/heads/next'
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
path: cypress/screenshots/
|
||||
# - run: node scripts/outdatedGitPackages.mjs
|
||||
# if: ${{ github.event.pull_request.base.ref == 'release' }}
|
||||
# env:
|
||||
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# - name: Store Bundle Stats
|
||||
# if: github.event.pull_request.base.ref == 'next'
|
||||
# uses: actions/github-script@v6
|
||||
# env:
|
||||
# GITHUB_TOKEN: ${{ secrets.GIST_TOKEN }}
|
||||
# with:
|
||||
# script: |
|
||||
# const gistId = '${{ secrets.BUNDLE_STATS_GIST_ID }}';
|
||||
|
||||
# async function getGistContent() {
|
||||
# const { data } = await github.rest.gists.get({
|
||||
# gist_id: gistId,
|
||||
# headers: {
|
||||
# authorization: `token ${process.env.GITHUB_TOKEN}`
|
||||
# }
|
||||
# });
|
||||
# return JSON.parse(data.files['bundle-stats.json'].content || '{}');
|
||||
# }
|
||||
|
||||
# async function updateGistContent(content) {
|
||||
# await github.rest.gists.update({
|
||||
# gist_id: gistId,
|
||||
# headers: {
|
||||
# authorization: `token ${process.env.GITHUB_TOKEN}`
|
||||
# },
|
||||
# files: {
|
||||
# 'bundle-stats.json': {
|
||||
# content: JSON.stringify(content, null, 2)
|
||||
# }
|
||||
# }
|
||||
# });
|
||||
# }
|
||||
|
||||
# const stats = require('/tmp/bundle-stats.json');
|
||||
# const content = await getGistContent();
|
||||
# content['${{ github.event.pull_request.base.ref }}'] = stats;
|
||||
# await updateGistContent(content);
|
||||
|
||||
# - name: Update PR Description
|
||||
# uses: actions/github-script@v6
|
||||
# with:
|
||||
# script: |
|
||||
# const { data: pr } = await github.rest.pulls.get({
|
||||
# owner: context.repo.owner,
|
||||
# repo: context.repo.repo,
|
||||
# pull_number: context.issue.number
|
||||
# });
|
||||
|
||||
# let body = pr.body || '';
|
||||
# const statsMarker = '### Bundle Size';
|
||||
# const comparison = '${{ steps.compare.outputs.stats }}';
|
||||
|
||||
# if (body.includes(statsMarker)) {
|
||||
# body = body.replace(
|
||||
# new RegExp(`${statsMarker}[^\n]*\n[^\n]*`),
|
||||
# `${statsMarker}\n${comparison}`
|
||||
# );
|
||||
# } else {
|
||||
# body += `\n\n${statsMarker}\n${comparison}`;
|
||||
# }
|
||||
|
||||
# await github.rest.pulls.update({
|
||||
# owner: context.repo.owner,
|
||||
# repo: context.repo.repo,
|
||||
# pull_number: context.issue.number,
|
||||
# body
|
||||
# });
|
||||
# dedupe-check:
|
||||
# runs-on: ubuntu-latest
|
||||
# if: github.event.pull_request.head.ref == 'next'
|
||||
# steps:
|
||||
# - name: Checkout repository
|
||||
# uses: actions/checkout@v2
|
||||
|
||||
# - name: Install pnpm
|
||||
# run: npm install -g pnpm@9.0.4
|
||||
|
||||
# - name: Run pnpm dedupe
|
||||
# run: pnpm dedupe
|
||||
|
||||
# - name: Check for changes
|
||||
# run: |
|
||||
# if ! git diff --exit-code --quiet pnpm-lock.yaml; then
|
||||
# echo "pnpm dedupe introduced changes:"
|
||||
# git diff --color=always pnpm-lock.yaml
|
||||
# exit 1
|
||||
# else
|
||||
# echo "No changes detected after pnpm dedupe in pnpm-lock.yaml"
|
||||
# fi
|
||||
|
|
|
|||
29
.github/workflows/fix-lint.yml
vendored
Normal file
29
.github/workflows/fix-lint.yml
vendored
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
name: Fix Lint Command
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
if: >-
|
||||
github.event.issue.pull_request != '' &&
|
||||
(
|
||||
contains(github.event.comment.body, '/fix')
|
||||
)
|
||||
permissions:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
ref: refs/pull/${{ github.event.issue.number }}/head
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
- run: pnpm install
|
||||
- run: pnpm lint --fix
|
||||
- name: Push Changes
|
||||
uses: ad-m/github-push-action@master
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
28
.github/workflows/merge-next.yml
vendored
Normal file
28
.github/workflows/merge-next.yml
vendored
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
name: Update Base Branch Command
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
if: >-
|
||||
github.event.issue.pull_request != '' &&
|
||||
(
|
||||
contains(github.event.comment.body, '/update')
|
||||
)
|
||||
permissions:
|
||||
pull-requests: write
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0 # Fetch all history so we can merge branches
|
||||
ref: refs/pull/${{ github.event.issue.number }}/head
|
||||
- name: Fetch All Branches
|
||||
run: git fetch --all
|
||||
# - name: Checkout PR
|
||||
# run: git checkout ${{ github.event.issue.pull_request.head.ref }}
|
||||
- name: Merge From Next
|
||||
run: git merge origin/next --strategy-option=theirs
|
||||
- name: Push Changes
|
||||
run: git push
|
||||
75
.github/workflows/next-deploy.yml
vendored
75
.github/workflows/next-deploy.yml
vendored
|
|
@ -16,25 +16,76 @@ jobs:
|
|||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
- name: Install Global Dependencies
|
||||
run: npm install --global vercel pnpm@9.0.4
|
||||
run: pnpm add -g vercel
|
||||
- name: Install Dependencies
|
||||
run: pnpm install
|
||||
- name: Pull Vercel Environment Information
|
||||
run: vercel pull --yes --environment=preview --token=${{ secrets.VERCEL_TOKEN }}
|
||||
- name: Build Project Artifacts
|
||||
run: vercel build --token=${{ secrets.VERCEL_TOKEN }}
|
||||
- run: pnpm build-storybook
|
||||
- name: Copy playground files
|
||||
run: pnpm build-playground && cp prismarine-viewer/public/index.html .vercel/output/static/playground.html && cp prismarine-viewer/public/playground.js .vercel/output/static/playground.js
|
||||
- name: Write Release Info
|
||||
run: |
|
||||
echo "{\"latestTag\": \"$(git rev-parse --short $GITHUB_SHA)\", \"isCommit\": true}" > assets/release.json
|
||||
- name: Download Generated Sounds map
|
||||
run: node scripts/downloadSoundsMap.mjs
|
||||
- name: Build Project Artifacts
|
||||
run: vercel build --token=${{ secrets.VERCEL_TOKEN }}
|
||||
env:
|
||||
CONFIG_JSON_SOURCE: BUNDLED
|
||||
LOCAL_CONFIG_FILE: config.mcraft-only.json
|
||||
- name: Copy playground files
|
||||
run: |
|
||||
mkdir -p .vercel/output/static/playground
|
||||
pnpm build-playground
|
||||
cp -r renderer/dist/* .vercel/output/static/playground/
|
||||
- name: Deploy Project Artifacts to Vercel
|
||||
uses: mathiasvr/command-output@v2.0.0
|
||||
with:
|
||||
run: vercel deploy --prebuilt --token=${{ secrets.VERCEL_TOKEN }}
|
||||
id: deploy
|
||||
- name: Set deployment alias
|
||||
run: vercel alias set ${{ steps.deploy.outputs.stdout }} ${{ secrets.TEST_PREVIEW_DOMAIN }} --token=${{ secrets.VERCEL_TOKEN }} --scope=zaro
|
||||
# - uses: mshick/add-pr-comment@v2
|
||||
# with:
|
||||
# message: |
|
||||
# Deployed to Vercel Preview: ${{ steps.deploy.outputs.stdout }}
|
||||
- name: Start servers for testing
|
||||
run: |
|
||||
nohup pnpm prod-start &
|
||||
nohup pnpm test-mc-server &
|
||||
- name: Run Cypress smoke tests
|
||||
uses: cypress-io/github-action@v5
|
||||
with:
|
||||
install: false
|
||||
spec: cypress/e2e/smoke.spec.ts
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: failure()
|
||||
with:
|
||||
name: cypress-smoke-test-screenshots
|
||||
path: cypress/screenshots/
|
||||
- name: Set deployment aliases
|
||||
run: |
|
||||
for alias in $(echo ${{ secrets.TEST_PREVIEW_DOMAIN }} | tr "," "\n"); do
|
||||
vercel alias set ${{ steps.deploy.outputs.stdout }} $alias --token=${{ secrets.VERCEL_TOKEN }} --scope=zaro
|
||||
done
|
||||
|
||||
- name: Create Release Pull Request
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
script: |
|
||||
const { data: pulls } = await github.rest.pulls.list({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
head: `${context.repo.owner}:next`,
|
||||
base: 'release',
|
||||
state: 'open'
|
||||
});
|
||||
|
||||
if (pulls.length === 0) {
|
||||
await github.rest.pulls.create({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
title: 'Release',
|
||||
head: 'next',
|
||||
base: 'release',
|
||||
body: 'PR was created automatically by the release workflow, hope you release it as soon as possible!',
|
||||
});
|
||||
}
|
||||
|
|
|
|||
94
.github/workflows/preview.yml
vendored
94
.github/workflows/preview.yml
vendored
|
|
@ -1,4 +1,4 @@
|
|||
name: Vercel Deploy Preview
|
||||
name: Vercel PR Deploy (Preview)
|
||||
env:
|
||||
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
|
||||
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
|
||||
|
|
@ -6,57 +6,109 @@ env:
|
|||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
pull_request_target:
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
# todo skip already created deploys on that commit
|
||||
if: >-
|
||||
github.event.issue.pull_request != '' &&
|
||||
(
|
||||
contains(github.event.comment.body, '/deploy')
|
||||
(
|
||||
github.event_name == 'issue_comment' &&
|
||||
contains(github.event.comment.body, '/deploy') &&
|
||||
github.event.issue.pull_request != null
|
||||
) ||
|
||||
(
|
||||
github.event_name == 'pull_request_target' &&
|
||||
contains(fromJson(vars.AUTO_DEPLOY_PRS), github.event.pull_request.number)
|
||||
)
|
||||
)
|
||||
permissions:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Checkout
|
||||
- name: Checkout Base To Temp
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
path: temp-base-repo
|
||||
- name: Get deployment alias
|
||||
run: node temp-base-repo/scripts/githubActions.mjs getAlias
|
||||
id: alias
|
||||
env:
|
||||
ALIASES: ${{ env.ALIASES }}
|
||||
PULL_URL: ${{ github.event.issue.pull_request.url || github.event.pull_request.url }}
|
||||
- name: Checkout PR (comment)
|
||||
uses: actions/checkout@v2
|
||||
if: github.event_name == 'issue_comment'
|
||||
with:
|
||||
ref: refs/pull/${{ github.event.issue.number }}/head
|
||||
- run: npm i -g pnpm@9.0.4
|
||||
- name: Checkout PR (pull_request)
|
||||
uses: actions/checkout@v2
|
||||
if: github.event_name == 'pull_request_target'
|
||||
with:
|
||||
ref: refs/pull/${{ github.event.pull_request.number }}/head
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18
|
||||
node-version: 22
|
||||
cache: "pnpm"
|
||||
- name: Update deployAlwaysUpdate packages
|
||||
run: |
|
||||
if [ -f package.json ]; then
|
||||
PACKAGES=$(node -e "const pkg = require('./package.json'); if (pkg.deployAlwaysUpdate) console.log(pkg.deployAlwaysUpdate.join(' '))")
|
||||
if [ ! -z "$PACKAGES" ]; then
|
||||
echo "Updating packages: $PACKAGES"
|
||||
pnpm up -L $PACKAGES
|
||||
else
|
||||
echo "No deployAlwaysUpdate packages found in package.json"
|
||||
fi
|
||||
else
|
||||
echo "package.json not found"
|
||||
fi
|
||||
- name: Install Global Dependencies
|
||||
run: npm install --global vercel
|
||||
run: pnpm add -g vercel
|
||||
- name: Pull Vercel Environment Information
|
||||
run: vercel pull --yes --environment=preview --token=${{ secrets.VERCEL_TOKEN }}
|
||||
- name: Build Project Artifacts
|
||||
run: vercel build --token=${{ secrets.VERCEL_TOKEN }}
|
||||
- run: pnpm build-storybook
|
||||
- name: Copy playground files
|
||||
run: pnpm build-playground && cp prismarine-viewer/public/index.html .vercel/output/static/playground.html && cp prismarine-viewer/public/playground.js .vercel/output/static/playground.js
|
||||
- name: Write Release Info
|
||||
run: |
|
||||
echo "{\"latestTag\": \"$(git rev-parse --short ${{ github.event.pull_request.head.sha }})\", \"isCommit\": true}" > assets/release.json
|
||||
- name: Download Generated Sounds map
|
||||
run: node scripts/downloadSoundsMap.mjs
|
||||
- name: Build Project Artifacts
|
||||
run: vercel build --token=${{ secrets.VERCEL_TOKEN }}
|
||||
env:
|
||||
CONFIG_JSON_SOURCE: BUNDLED
|
||||
LOCAL_CONFIG_FILE: config.mcraft-only.json
|
||||
- name: Copy playground files
|
||||
run: |
|
||||
mkdir -p .vercel/output/static/playground
|
||||
pnpm build-playground
|
||||
cp -r renderer/dist/* .vercel/output/static/playground/
|
||||
- name: Write pr redirect index.html
|
||||
run: |
|
||||
mkdir -p .vercel/output/static/pr
|
||||
echo "<meta http-equiv='refresh' content='0;url=https://github.com/${{ github.repository }}/pull/${{ github.event.issue.number || github.event.pull_request.number }}'>" > .vercel/output/static/pr/index.html
|
||||
- name: Write commit redirect index.html
|
||||
run: |
|
||||
mkdir -p .vercel/output/static/commit
|
||||
echo "<meta http-equiv='refresh' content='0;url=https://github.com/${{ github.repository }}/pull/${{ github.event.issue.number || github.event.pull_request.number }}/commits/${{ github.event.pull_request.head.sha }}'>" > .vercel/output/static/commit/index.html
|
||||
- name: Deploy Project Artifacts to Vercel
|
||||
uses: mathiasvr/command-output@v2.0.0
|
||||
with:
|
||||
run: vercel deploy --prebuilt --token=${{ secrets.VERCEL_TOKEN }}
|
||||
id: deploy
|
||||
- uses: mshick/add-pr-comment@v2
|
||||
# if: github.event_name == 'issue_comment'
|
||||
with:
|
||||
allow-repeats: true
|
||||
message: |
|
||||
Deployed to Vercel Preview: ${{ steps.deploy.outputs.stdout }}
|
||||
[Playground](${{ steps.deploy.outputs.stdout }}/playground.html)
|
||||
[Playground](${{ steps.deploy.outputs.stdout }}/playground/)
|
||||
[Storybook](${{ steps.deploy.outputs.stdout }}/storybook/)
|
||||
# - run: git checkout next scripts/githubActions.mjs
|
||||
- name: Get deployment alias
|
||||
run: node scripts/githubActions.mjs getAlias
|
||||
id: alias
|
||||
env:
|
||||
ALIASES: ${{ env.ALIASES }}
|
||||
PULL_URL: ${{ github.event.issue.pull_request.url }}
|
||||
- name: Set deployment alias
|
||||
if: ${{ steps.alias.outputs.alias != '' && steps.alias.outputs.alias != 'mcraft.fun' && steps.alias.outputs.alias != 's.mcraft.fun' }}
|
||||
run: vercel alias set ${{ steps.deploy.outputs.stdout }} ${{ steps.alias.outputs.alias }} --token=${{ secrets.VERCEL_TOKEN }} --scope=zaro
|
||||
run: |
|
||||
for alias in $(echo ${{ steps.alias.outputs.alias }} | tr "," "\n"); do
|
||||
vercel alias set ${{ steps.deploy.outputs.stdout }} $alias --token=${{ secrets.VERCEL_TOKEN }} --scope=zaro
|
||||
done
|
||||
|
|
|
|||
50
.github/workflows/publish.yml
vendored
50
.github/workflows/publish.yml
vendored
|
|
@ -1,50 +0,0 @@
|
|||
name: Release
|
||||
env:
|
||||
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
|
||||
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
|
||||
MAIN_MENU_LINKS: ${{ vars.MAIN_MENU_LINKS }}
|
||||
on:
|
||||
push:
|
||||
branches: [release]
|
||||
jobs:
|
||||
build-and-deploy:
|
||||
runs-on: ubuntu-latest
|
||||
permissions: write-all
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@master
|
||||
- name: Install pnpm
|
||||
run: npm i -g vercel pnpm@9.0.4
|
||||
# - run: pnpm install
|
||||
# - run: pnpm build
|
||||
- run: vercel pull --yes --environment=production --token=${{ secrets.VERCEL_TOKEN }}
|
||||
- run: node scripts/replaceFavicon.mjs ${{ secrets.FAVICON_MAIN }}
|
||||
# will install + build to .vercel/output/static
|
||||
- run: vercel build --token=${{ secrets.VERCEL_TOKEN }} --prod
|
||||
- run: pnpm build-storybook
|
||||
- name: Copy playground files
|
||||
run: pnpm build-playground && cp prismarine-viewer/public/index.html .vercel/output/static/playground.html && cp prismarine-viewer/public/playground.js .vercel/output/static/playground.js
|
||||
- name: Download Generated Sounds map
|
||||
run: node scripts/downloadSoundsMap.mjs
|
||||
- name: Deploy Project to Vercel
|
||||
uses: mathiasvr/command-output@v2.0.0
|
||||
with:
|
||||
run: vercel deploy --prebuilt --token=${{ secrets.VERCEL_TOKEN }} --prod
|
||||
id: deploy
|
||||
- run: |
|
||||
pnpx zardoy-release node --footer "This release URL: ${{ steps.deploy.outputs.stdout }}"
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
# has possible output: tag
|
||||
id: release
|
||||
# has output
|
||||
- run: cp vercel.json .vercel/output/static/vercel.json
|
||||
- uses: peaceiris/actions-gh-pages@v3
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
publish_dir: .vercel/output/static
|
||||
force_orphan: true
|
||||
- run: pnpm tsx scripts/buildNpmReact.ts ${{ steps.release.outputs.tag }}
|
||||
if: steps.release.outputs.tag
|
||||
env:
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
116
.github/workflows/release.yml
vendored
Normal file
116
.github/workflows/release.yml
vendored
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
name: Release
|
||||
env:
|
||||
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
|
||||
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
|
||||
MAIN_MENU_LINKS: ${{ vars.MAIN_MENU_LINKS }}
|
||||
on:
|
||||
push:
|
||||
branches: [release]
|
||||
jobs:
|
||||
build-and-deploy:
|
||||
runs-on: ubuntu-latest
|
||||
permissions: write-all
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@master
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
- name: Install Global Dependencies
|
||||
run: pnpm add -g vercel
|
||||
# - run: pnpm install
|
||||
# - run: pnpm build
|
||||
- run: vercel pull --yes --environment=production --token=${{ secrets.VERCEL_TOKEN }}
|
||||
- run: node scripts/replaceFavicon.mjs ${{ secrets.FAVICON_MAIN }}
|
||||
# will install + build to .vercel/output/static
|
||||
- name: Get Release Info
|
||||
run: pnpx zardoy-release empty --skip-github --output-file assets/release.json
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Download Generated Sounds map
|
||||
run: node scripts/downloadSoundsMap.mjs
|
||||
- run: vercel build --token=${{ secrets.VERCEL_TOKEN }} --prod
|
||||
env:
|
||||
CONFIG_JSON_SOURCE: BUNDLED
|
||||
LOCAL_CONFIG_FILE: config.mcraft-only.json
|
||||
- name: Copy playground files
|
||||
run: |
|
||||
mkdir -p .vercel/output/static/playground
|
||||
pnpm build-playground
|
||||
cp -r renderer/dist/* .vercel/output/static/playground/
|
||||
|
||||
# publish to github
|
||||
- run: cp vercel.json .vercel/output/static/vercel.json
|
||||
- uses: peaceiris/actions-gh-pages@v3
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
publish_dir: .vercel/output/static
|
||||
force_orphan: true
|
||||
|
||||
# Create CNAME file for custom domain
|
||||
- name: Create CNAME file
|
||||
run: echo "github.mcraft.fun" > .vercel/output/static/CNAME
|
||||
|
||||
- name: Deploy to mwc-mcraft-pages repository
|
||||
uses: peaceiris/actions-gh-pages@v3
|
||||
with:
|
||||
personal_token: ${{ secrets.MCW_MCRAFT_PAGE_DEPLOY_TOKEN }}
|
||||
external_repository: ${{ github.repository_owner }}/mwc-mcraft-pages
|
||||
publish_dir: .vercel/output/static
|
||||
publish_branch: main
|
||||
destination_dir: docs
|
||||
force_orphan: true
|
||||
|
||||
- name: Change index.html title
|
||||
run: |
|
||||
# change <title>Minecraft Web Client</title> to <title>Minecraft Web Client — Free Online Browser Version</title>
|
||||
sed -i 's/<title>Minecraft Web Client<\/title>/<title>Minecraft Web Client — Free Online Browser Version<\/title>/' .vercel/output/static/index.html
|
||||
|
||||
- name: Deploy Project to Vercel
|
||||
uses: mathiasvr/command-output@v2.0.0
|
||||
with:
|
||||
run: vercel deploy --prebuilt --token=${{ secrets.VERCEL_TOKEN }} --prod
|
||||
id: deploy
|
||||
- name: Get releasing alias
|
||||
run: node scripts/githubActions.mjs getReleasingAlias
|
||||
id: alias
|
||||
- name: Set deployment alias
|
||||
run: |
|
||||
for alias in $(echo ${{ steps.alias.outputs.alias }} | tr "," "\n"); do
|
||||
vercel alias set ${{ steps.deploy.outputs.stdout }} $alias --token=${{ secrets.VERCEL_TOKEN }} --scope=zaro
|
||||
done
|
||||
|
||||
- name: Build single-file version - minecraft.html
|
||||
run: pnpm build-single-file && mv dist/single/index.html minecraft.html
|
||||
- name: Build self-host version
|
||||
run: pnpm build
|
||||
- name: Bundle server.js
|
||||
run: |
|
||||
pnpm esbuild server.js --bundle --platform=node --outfile=bundled-server.js --define:process.env.NODE_ENV="'production'"
|
||||
|
||||
- name: Create zip package
|
||||
run: |
|
||||
mkdir -p package
|
||||
cp -r dist package/
|
||||
cp bundled-server.js package/server.js
|
||||
cd package
|
||||
zip -r ../self-host.zip .
|
||||
|
||||
- run: |
|
||||
pnpx zardoy-release node --footer "This release URL: https://$(echo ${{ steps.alias.outputs.alias }} | cut -d',' -f1) (Vercel URL: ${{ steps.deploy.outputs.stdout }})"
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
# has possible output: tag
|
||||
id: release
|
||||
|
||||
# has output
|
||||
- name: Set publishing config
|
||||
run: pnpm config set '//registry.npmjs.org/:_authToken' "${NODE_AUTH_TOKEN}"
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
# - run: pnpm tsx scripts/buildNpmReact.ts ${{ steps.release.outputs.tag }}
|
||||
# if: steps.release.outputs.tag
|
||||
# env:
|
||||
# NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -10,7 +10,7 @@ localSettings.mjs
|
|||
dist*
|
||||
.DS_Store
|
||||
.idea/
|
||||
world
|
||||
/world
|
||||
data*.json
|
||||
out
|
||||
*.iml
|
||||
|
|
@ -18,5 +18,7 @@ out
|
|||
generated
|
||||
storybook-static
|
||||
server-jar
|
||||
config.local.json
|
||||
logs/
|
||||
|
||||
src/react/npmReactComponents.ts
|
||||
|
|
|
|||
|
|
@ -2,12 +2,18 @@
|
|||
|
||||
After forking the repository, run the following commands to get started:
|
||||
|
||||
0. Ensure you have [Node.js](https://nodejs.org) and `pnpm` installed. To install pnpm run `npm i -g pnpm@9.0.4`.
|
||||
0. Ensure you have [Node.js](https://nodejs.org) installed. Enable corepack with `corepack enable` *(1).
|
||||
1. Install dependencies: `pnpm i`
|
||||
2. Start the project in development mode: `pnpm start`
|
||||
2. Start the project in development mode: `pnpm start` or build the project for production: `pnpm build`
|
||||
3. Read the [Tasks Categories](#tasks-categories) and [Workflow](#workflow) sections below
|
||||
4. Let us know if you are working on something and be sure to open a PR if you got any changes. Happy coding!
|
||||
|
||||
*(1): If you are getting `Cannot find matching keyid` update corepack to the latest version with `npm i -g corepack`.
|
||||
|
||||
*(2): If still something doesn't work ensure you have the right nodejs version with `node -v` (tested on 22.x)
|
||||
|
||||
<!-- *(3): For GitHub codespaces (cloud ide): Run `pnpm i @rsbuild/core@1.2.4 @rsbuild/plugin-node-polyfill@1.3.0 @rsbuild/plugin-react@1.1.0 @rsbuild/plugin-typed-css-modules@1.0.2` command to avoid crashes because of limited ram -->
|
||||
|
||||
## Project Structure
|
||||
|
||||
There are 3 main parts of the project:
|
||||
|
|
@ -27,11 +33,11 @@ Paths:
|
|||
- `src` - main app source code
|
||||
- `src/react` - React components - almost all UI is in this folder. Almost every component has its base (reused in app and storybook) and `Provider` - which is a component that provides context to its children. Consider looking at DeathScreen component to see how it's used.
|
||||
|
||||
### Renderer: Playground & Mesher (`prismarine-viewer`)
|
||||
### Renderer: Playground & Mesher (`renderer`)
|
||||
|
||||
- Playground Scripts:
|
||||
- Start: `pnpm run-playground` (playground, mesher + server) or `pnpm watch-playground`
|
||||
- Build: `pnpm build-playground` or `node prismarine-viewer/esbuild.mjs`
|
||||
- Build: `pnpm build-playground` or `node renderer/esbuild.mjs`
|
||||
|
||||
- Mesher Scripts:
|
||||
- Start: `pnpm watch-mesher`
|
||||
|
|
@ -39,10 +45,10 @@ Paths:
|
|||
|
||||
Paths:
|
||||
|
||||
- `prismarine-viewer` - Improved and refactored version of <https://github.com/prismarineJS/prismarine-viewer>. Here is everything related to rendering the game world itself (no ui at all). Two most important parts here are:
|
||||
- `prismarine-viewer/viewer/lib/worldrenderer.ts` - adding new objects to three.js happens here (sections)
|
||||
- `prismarine-viewer/viewer/lib/models.ts` - preparing data for rendering (blocks) - happens in worker: out file - `worker.js`, building - `prismarine-viewer/buildWorker.mjs`
|
||||
- `prismarine-viewer/examples/playground.ts` - Playground (source of <mcraft.fun/playground.html>) Use this for testing any rendering changes. You can also modify the playground code.
|
||||
- `renderer` - Improved and refactored version of <https://github.com/PrismarineJS/prismarine-viewer>. Here is everything related to rendering the game world itself (no ui at all). Two most important parts here are:
|
||||
- `renderer/viewer/lib/worldrenderer.ts` - adding new objects to three.js happens here (sections)
|
||||
- `renderer/viewer/lib/models.ts` - preparing data for rendering (blocks) - happens in worker: out file - `worker.js`, building - `renderer/buildWorker.mjs`
|
||||
- `renderer/playground/playground.ts` - Playground (source of <mcraft.fun/playground.html>) Use this for testing any rendering changes. You can also modify the playground code.
|
||||
|
||||
### Storybook (`.storybook`)
|
||||
|
||||
|
|
@ -70,7 +76,7 @@ Cypress tests are located in `cypress` folder. To run them, run `pnpm test-mc-se
|
|||
## Unit Tests
|
||||
|
||||
There are not many unit tests for now (which we are trying to improve).
|
||||
Location of unit tests: `**/*.test.ts` files in `src` folder and `prismarine-viewer` folder.
|
||||
Location of unit tests: `**/*.test.ts` files in `src` folder and `renderer` folder.
|
||||
Start them with `pnpm test-unit`.
|
||||
|
||||
## Making protocol-related changes
|
||||
|
|
@ -169,6 +175,21 @@ New React components, improve UI (including mobile support).
|
|||
3. Develop, try to fix and test. Finally we should find a way to fix it. It's ideal to have an automatic test but it's not necessary for now
|
||||
3. Repeat step 1 to make sure the task is done and the problem is fixed (or the feature is implemented)
|
||||
|
||||
## Updating Dependencies
|
||||
|
||||
1. Use `pnpm update-git-deps` to check and update git dependencies (like mineflayer fork, prismarine packages etc). The script will:
|
||||
- Show which git dependencies have updates available
|
||||
- Ask if you want to update them
|
||||
- Skip dependencies listed in `pnpm.updateConfig.ignoreDependencies`
|
||||
|
||||
2. Update PrismarineJS dependencies to the latest version: `minecraft-data` (be sure to replace the version twice in the package.json), `mineflayer`, `minecraft-protocol`, `prismarine-block`, `prismarine-chunk`, `prismarine-item`, ...
|
||||
|
||||
3. If `minecraft-protocol` patch fails, do this:
|
||||
1. Remove the patch from `patchedDependencies` in `package.json`
|
||||
2. Run `pnpm patch minecraft-protocol`, open patch directory
|
||||
3. Apply the patch manually in this directory: `patch -p1 < minecraft-protocol@<version>.patch`
|
||||
4. Run the suggested command from `pnpm patch ...` (previous step) to update the patch
|
||||
|
||||
### Would be useful to have
|
||||
|
||||
- cleanup folder & modules structure, cleanup playground code
|
||||
|
|
|
|||
19
Dockerfile
19
Dockerfile
|
|
@ -4,20 +4,28 @@ FROM node:18-alpine AS build
|
|||
RUN apk add git
|
||||
WORKDIR /app
|
||||
COPY . /app
|
||||
# install pnpm
|
||||
RUN npm i -g pnpm@9.0.4
|
||||
# install pnpm with corepack
|
||||
RUN corepack enable
|
||||
# Build arguments
|
||||
ARG DOWNLOAD_SOUNDS=false
|
||||
ARG DISABLE_SERVICE_WORKER=false
|
||||
ARG CONFIG_JSON_SOURCE=REMOTE
|
||||
# TODO need flat --no-root-optional
|
||||
RUN node ./scripts/dockerPrepare.mjs
|
||||
RUN pnpm i
|
||||
# Download sounds if flag is enabled
|
||||
RUN if [ "$DOWNLOAD_SOUNDS" = "true" ] ; then node scripts/downloadSoundsMap.mjs ; fi
|
||||
|
||||
# TODO for development
|
||||
# EXPOSE 9090
|
||||
# VOLUME /app/src
|
||||
# VOLUME /app/prismarine-viewer
|
||||
# VOLUME /app/renderer
|
||||
# ENTRYPOINT ["pnpm", "run", "run-all"]
|
||||
|
||||
# only for prod
|
||||
RUN pnpm run build
|
||||
RUN DISABLE_SERVICE_WORKER=$DISABLE_SERVICE_WORKER \
|
||||
CONFIG_JSON_SOURCE=$CONFIG_JSON_SOURCE \
|
||||
pnpm run build
|
||||
|
||||
# ---- Run Stage ----
|
||||
FROM node:18-alpine
|
||||
|
|
@ -27,8 +35,9 @@ WORKDIR /app
|
|||
COPY --from=build /app/dist /app/dist
|
||||
COPY server.js /app/server.js
|
||||
# Install express
|
||||
RUN npm i -g pnpm@9.0.4
|
||||
RUN npm i -g pnpm@10.8.0
|
||||
RUN npm init -yp
|
||||
RUN pnpm i express github:zardoy/prismarinejs-net-browserify compression cors
|
||||
EXPOSE 8080
|
||||
VOLUME /app/public
|
||||
ENTRYPOINT ["node", "server.js", "--prod"]
|
||||
|
|
|
|||
90
README.MD
90
README.MD
|
|
@ -2,34 +2,66 @@
|
|||
|
||||

|
||||
|
||||
A true Minecraft client running in your browser! A port of the original game to the web, written in JavaScript using the best modern web technologies.
|
||||
Minecraft **clone** rewritten in TypeScript using the best modern web technologies. Minecraft vanilla-compatible client and integrated server packaged into a single web app.
|
||||
|
||||
You can try this out at [mcraft.fun](https://mcraft.fun/), [pcm.gg](https://pcm.gg) (short link) [mcon.vercel.app](https://mcon.vercel.app/) or the GitHub pages deploy. Every commit from the `develop` (default) branch is deployed to [s.mcraft.fun](https://s.mcraft.fun/) - so it's usually newer, but might be less stable.
|
||||
You can try this out at [mcraft.fun](https://mcraft.fun/), [pcm.gg](https://pcm.gg) (short link), [mcon.vercel.app](https://mcon.vercel.app/) or the GitHub pages deploy. Every commit from the default (`develop`) branch is deployed to [s.mcraft.fun](https://s.mcraft.fun/) and [s.pcm.gg](https://s.pcm.gg/) - so it's usually newer, but might be less stable.
|
||||
|
||||
For building the project yourself / contributing, see [Development, Debugging & Contributing](#development-debugging--contributing). If you encounter any bugs or usability issues, please report them!
|
||||
> For Turkey/Russia use [ru.mcraft.fun](https://ru.mcraft.fun/) (since Cloudflare is blocked)
|
||||
|
||||
Don't confuse with [Eaglercraft](https://git.eaglercraft.rip/eaglercraft/eaglercraft-1.8) which is a REAL vanilla Minecraft Java edition port to the web (but with its own limitations). Eaglercraft is a fully playable solution, meanwhile this project is aimed for *device-compatiiblity* and better performance so it feels portable, flexible and lightweight. It's also a very strong example on how to build true HTML games for the web at scale entirely with the JS ecosystem. Have fun!
|
||||
|
||||
For building the project yourself / contributing, see [Development, Debugging & Contributing](#development-debugging--contributing). For reference at what and how web technologies / frameworks are used, see [TECH.md](./TECH.md) (also for comparison with Eaglercraft).
|
||||
|
||||
> **Note**: You can deploy it on your own server in less than a minute using a one-liner script from [Minecraft Everywhere repo](https://github.com/zardoy/minecraft-everywhere)
|
||||
|
||||
### Big Features
|
||||
|
||||
- Official Mineflayer [plugin integration](https://github.com/zardoy/mcraft-fun-mineflayer-plugin)! View / Control your bot remotely.
|
||||
- Open any zip world file or even folder in read-write mode!
|
||||
- Connect to Java servers running in both offline (cracked) and online mode* (it's possible because of proxy servers, see below)
|
||||
- Integrated JS server clone capable of opening Java world saves in any way (folders, zip, web chunks streaming, etc)
|
||||
- Singleplayer mode with simple world generations!
|
||||
- Google Drive support for reading / saving worlds
|
||||
- Works offline
|
||||
- Play with friends over internet! (P2P is powered by Peer.js discovery servers)
|
||||
- First-class touch (mobile) & controller support
|
||||
- FULL Resource pack support: Custom GUI, all textures & custom models! Server resource packs are also supported.
|
||||
- Builtin JEI with recipes & guides for every item (also replaces creative inventory)
|
||||
- First-class keybindings configuration
|
||||
- Advanced Resource pack support: Custom GUI, all textures. Server resource packs are supported with proper CORS configuration.
|
||||
- Builtin JEI with recipes & descriptions for almost every item (JEI is creative inventory replacement)
|
||||
- Custom protocol channel extensions (eg for custom block models in the world)
|
||||
- Play with friends over internet! (P2P is powered by Peer.js discovery servers)
|
||||
- ~~Google Drive support for reading / saving worlds back to the cloud~~
|
||||
- Support for custom rendering 3D engines. Modular architecture.
|
||||
- even even more!
|
||||
|
||||
All components that are in [Storybook](https://mcraft.fun/storybook) are published as npm module and can be used in other projects: [`minecraft-react`](https://npmjs.com/minecraft-react)
|
||||
All components that are in [Storybook](https://minimap.mcraft.fun/storybook/) are published as npm module and can be used in other projects: [`minecraft-react`](https://npmjs.com/minecraft-react)
|
||||
|
||||
### Recommended Settings
|
||||
|
||||
- Controls -> **Touch Controls Type** -> **Joystick**
|
||||
- Controls -> **Auto Full Screen** -> **On** - To avoid ctrl+w issue
|
||||
- Interface -> **Enable Minimap** -> **Always** - To enable useful minimap (why not?)
|
||||
- Controls -> **Raw Input** -> **On** - This will make the controls more precise (UPD: already enabled by default)
|
||||
- Interface -> **Chat Select** -> **On** - To select chat messages (UPD: already enabled by default)
|
||||
|
||||
### Browser Notes
|
||||
|
||||
This project is tested with BrowserStack. Special thanks to [BrowserStack](https://www.browserstack.com/) for providing testing infrastructure!
|
||||
|
||||
Howerver, it's known that these browsers have issues:
|
||||
|
||||
**Opera Mini**: Disable *mouse gestures* in browsre settings to avoid opening new tab on right click hold
|
||||
|
||||
**Vivaldi**: Disable Controls -> *Raw Input* in game settings if experiencing issues
|
||||
|
||||
### Versions Support
|
||||
|
||||
Server versions 1.8 - 1.21.5 are supported.
|
||||
First class versions (most of the features are tested on these versions):
|
||||
|
||||
- 1.19.4
|
||||
- 1.21.4
|
||||
|
||||
Versions below 1.13 are not tested currently and may not work correctly.
|
||||
|
||||
### World Loading
|
||||
|
||||
Zip files and folders are supported. Just drag and drop them into the browser window. You can open folders in readonly and read-write mode. New chunks may be generated incorrectly for now.
|
||||
|
|
@ -38,11 +70,15 @@ Whatever offline mode you used (zip, folder, just single player), you can always
|
|||
|
||||

|
||||
|
||||
### Servers
|
||||
### Servers & Proxy
|
||||
|
||||
You can play almost on any Java server, vanilla servers are fully supported.
|
||||
See the [Mineflayer](https://github.com/PrismarineJS/mineflayer) repo for the list of supported versions (should support majority of versions).
|
||||
There is a builtin proxy, but you can also host your one! Just clone the repo, run `pnpm i` (following CONTRIBUTING.MD) and run `pnpm prod-start`, then you can specify `http://localhost:8080` in the proxy field.
|
||||
There is a builtin proxy, but you can also host your one! Just clone the repo, run `pnpm i` (following CONTRIBUTING.MD) and run `pnpm prod-start`, then you can specify `http://localhost:8080` in the proxy field. Or you can deploy it to the cloud service:
|
||||
|
||||
[](https://app.koyeb.com/deploy?name=minecraft-web-client&type=git&repository=zardoy%2Fminecraft-web-client&branch=next&builder=dockerfile&env%5B%5D=&ports=8080%3Bhttp%3B%2F)
|
||||
|
||||
> **Note**: If you want to make **your own** Minecraft server accessible to web clients (without our proxies), you can use [mwc-proxy](https://github.com/zardoy/mwc-proxy) - a lightweight JS WebSocket proxy that runs on the same server as your Minecraft server, allowing players to connect directly via `wss://play.example.com`. `?client_mcraft` is added to the URL, so the proxy will know that it's this client.
|
||||
|
||||
Proxy servers are used to connect to Minecraft servers which use TCP protocol. When you connect connect to a server with a proxy, websocket connection is created between you (browser client) and the proxy server located in Europe, then the proxy connects to the Minecraft server and sends the data to the client (you) without any packet deserialization to avoid any additional delays. That said all the Minecraft protocol packets are processed by the client, right in your browser.
|
||||
|
||||
|
|
@ -87,24 +123,25 @@ To open the console, press `F12`, or if you are on mobile, you can type `#dev` i
|
|||
|
||||
It should be easy to build/start the project locally. See [CONTRIBUTING.MD](./CONTRIBUTING.md) for more info. Also you can look at Dockerfile for reference.
|
||||
|
||||
There is world renderer playground ([link](https://mcon.vercel.app/playground.html)).
|
||||
There is world renderer playground ([link](https://mcon.vercel.app/playground/)).
|
||||
|
||||
However, there are many things that can be done in online production version (like debugging actual source code). Also you can access some global variables in the console and there are a few useful examples:
|
||||
|
||||
- `localStorage.debug = '*'` - Enables all debug messages! Warning: this will start all packets spam.
|
||||
- If you type `debugToggle`, press enter in console - It will enables all debug messages! Warning: this will start all packets spam.
|
||||
Instead I recommend setting `options.debugLogNotFrequentPackets`. Also you can use `debugTopPackets` (with JSON.stringify) to see what packets were received/sent by name
|
||||
|
||||
- `bot` - Mineflayer bot instance. See Mineflayer documentation for more.
|
||||
- `viewer` - Three.js viewer instance, basically does all the rendering.
|
||||
- `viewer.world.sectionObjects` - Object with all active chunk sections (geometries) in the world. Each chunk section is a Three.js mesh or group.
|
||||
- `world` - Three.js world instance, basically does all the rendering (part of renderer backend).
|
||||
- `world.sectionObjects` - Object with all active chunk sections (geometries) in the world. Each chunk section is a Three.js mesh or group.
|
||||
- `debugSceneChunks` - The same as above, but relative to current bot position (e.g. 0,0 is the current chunk).
|
||||
- `debugChangedOptions` - See what options are changed. Don't change options here.
|
||||
- `localServer`/`server` - Only for singleplayer mode/host. Flying Squid server instance, see it's documentation for more.
|
||||
- `localServer.overworld.storageProvider.regions` - See ALL LOADED region files with all raw data.
|
||||
- `localServer.levelData.LevelName = 'test'; localServer.writeLevelDat()` - Change name of the world
|
||||
|
||||
- `nbt.simplify(someNbt)` - Simplifies nbt data, so it's easier to read.
|
||||
|
||||
The most useful thing in devtools is the watch expression. You can add any expression there and it will be re-evaluated in real time. For example, you can add `viewer.camera.position` to see the camera position and so on.
|
||||
The most useful thing in devtools is the watch expression. You can add any expression there and it will be re-evaluated in real time. For example, you can add `world.getCameraPosition()` to see the camera position and so on.
|
||||
|
||||
<img src="./docs-assets/watch-expr.png" alt="Watch expression" width="480"/>
|
||||
|
||||
|
|
@ -125,6 +162,12 @@ Press `Y` to set query parameters to url of your current game state.
|
|||
|
||||
There are some parameters you can set in the url to archive some specific behaviors:
|
||||
|
||||
General:
|
||||
|
||||
- **`?setting=<setting_name>:<setting_value>`** - Set and lock the setting on load. You can set multiple settings by separating them with `&` e.g. `?setting=autoParkour:true&setting=renderDistance:4`
|
||||
- `?modal=<modal>` - Open specific modal on page load eg `keybindings`. Very useful on UI changes testing during dev. For path use `,` as separator. To get currently opened modal type this in the console: `activeModalStack.at(-1).reactType`
|
||||
- `?replayFileUrl=<url>` - Load and start a packet replay session from a URL with a integrated server. For debugging / previewing recorded sessions. The file must be CORS enabled.
|
||||
|
||||
Server specific:
|
||||
|
||||
- `?ip=<server_address>` - Display connect screen to the server on load with predefined server ip. `:<port>` is optional and can be added to the ip.
|
||||
|
|
@ -133,14 +176,17 @@ Server specific:
|
|||
- `?proxy=<proxy_address>` - Set the proxy server address to use for the server
|
||||
- `?username=<username>` - Set the username for the server
|
||||
- `?lockConnect=true` - Only works then `ip` parameter is set. Disables cancel/save buttons and all inputs in the connect screen already set as parameters. Useful for integrates iframes.
|
||||
- `?reconnect=true` - Reconnect to the server on page reloads. Available in **dev mode only** and very useful on server testing.
|
||||
- `?autoConnect=true` - Only works then `ip` and `version` parameters are set and `allowAutoConnect` is `true` in config.json! Directly connects to the specified server. Useful for integrates iframes.
|
||||
- `?serversList=<list_or_url>` - `<list_or_url>` can be a list of servers in the format `ip:version,ip` or a url to a json file with the same format (array) or a txt file with line-delimited list of server IPs.
|
||||
- `?addPing=<ping>` - Add a latency to both sides of the connection. Useful for testing ping issues. For example `?addPing=100` will add 200ms to your ping.
|
||||
|
||||
Single player specific:
|
||||
|
||||
- `?loadSave=<save_name>` - Load the save on load with the specified folder name (not title)
|
||||
- `?singleplayer=1` - Create empty world on load. Nothing will be saved
|
||||
- `?singleplayer=1` or `?sp=1` - Create empty world on load. Nothing will be saved
|
||||
- `?version=<version>` - Set the version for the singleplayer world (when used with `?singleplayer=1`)
|
||||
- `?noSave=true` - Disable auto save on unload / disconnect / export whenever a world is loaded. Only manual save with `/save` command will work.
|
||||
- `?serverSetting=<key>:<value>` - Set local server [options](https://github.com/zardoy/space-squid/tree/everything/src/modules.ts#L51). For example `?serverSetting=noInitialChunksSend:true` will disable initial chunks loading on the loading screen.
|
||||
- `?map=<map_url>` - Load the map from ZIP. You can use any url, but it must be **CORS enabled**.
|
||||
- `?mapDir=<index_file_url>` - Load the map from a file descriptor. It's recommended and the fastest way to load world but requires additional setup. The file must be in the following format:
|
||||
|
||||
|
|
@ -165,12 +211,12 @@ In this case you must use `?mapDirBaseUrl` to specify the base URL to fetch the
|
|||
|
||||
- `?mapDirBaseUrl` - See above.
|
||||
|
||||
Only during development:
|
||||
|
||||
- `?reconnect=true` - Reconnect to the server on page reloads. Very useful on server testing.
|
||||
|
||||
<!-- - `?mapDirGuess=<base_url>` - Load the map from the provided URL and paths will be guessed with a few additional fetch requests. -->
|
||||
|
||||
General:
|
||||
|
||||
- `?setting=<setting_name>:<setting_value>` - Set and lock the setting on load. You can set multiple settings by separating them with `&` e.g. `?setting=autoParkour:true&setting=renderDistance:4`
|
||||
|
||||
### Notable Things that Power this Project
|
||||
|
||||
- [Mineflayer](https://github.com/PrismarineJS/mineflayer) - Handles all client-side communications with the server (including the builtin one) - forked
|
||||
|
|
@ -188,3 +234,5 @@ General:
|
|||
### Alternatives
|
||||
|
||||
- [https://github.com/ClassiCube/ClassiCube](ClassiCube - Better C# Rewrite) [DEMO](https://www.classicube.net/server/play/?warned=true)
|
||||
- [https://m.eaglercraft.com/](EaglerCraft) - Eaglercraft runnable on mobile (real Minecraft in the browser)
|
||||
- [js-minecraft](https://github.com/LabyStudio/js-minecraft) - An insanely well done clone from the graphical side that inspired many features here
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ Minecraft UI components for React extracted from [mcraft.fun](https://mcraft.fun
|
|||
pnpm i minecraft-react
|
||||
```
|
||||
|
||||

|
||||

|
||||
|
||||
## Usage
|
||||
|
||||
|
|
|
|||
58
TECH.md
Normal file
58
TECH.md
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
### Eaglercraft Comparison
|
||||
|
||||
This project uses proxies so you can connect to almost any vanilla server. Though proxies have some limitations such as increased latency and servers will complain about using VPN (though we have a workaround for that, but ping will be much higher).
|
||||
This client generally has better performance but some features reproduction might be inaccurate eg its less stable and more buggy in some cases.
|
||||
|
||||
| Feature | This project | Eaglercraft | Description |
|
||||
| --------------------------------- | ----------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| General | | | |
|
||||
| Mobile Support (touch) | ✅(+) | ✅ | |
|
||||
| Gamepad Support | ✅ | ❌ | |
|
||||
| A11Y | ✅ | ❌ | We have DOM for almost all UI so your extensions and other browser features will work natively like on any other web page (but maybe it's not needed) |
|
||||
| Game Features | | | |
|
||||
| Servers Support (quality) | ❌(+) | ✅ | Eaglercraft is vanilla Minecraft, while this project tries to emulate original game behavior at protocol level (Mineflayer is used) |
|
||||
| Servers Support (any version, ip) | ✅ | ❌ | We support almost all Minecraft versions, only important if you connect to a server where you need new content like blocks or if you play with friends. And you can connect to almost any server using proxy servers! |
|
||||
| Servers Support (online mode) | ✅ | ❌ | Join to online servers like Hypixel using your Microsoft account without additional proxies |
|
||||
| Singleplayer Survival Features | ❌ | ✅ | Just like Eaglercraft this project can generate and save worlds, but generator is simple and only a few survival features are supported (look here for [supported features list](https://github.com/zardoy/space-squid)) |
|
||||
| Singleplayer Maps | ✅ | ✅ | We support any version, but adventure maps won't work, but simple parkour and build maps might be interesting to explore... |
|
||||
| Singleplayer Maps World Streaming | ✅ | ❌ | Thanks to Browserfs, saves can be loaded to local singleplayer server using multiple ways: from local folder, server directory (not zip), dropbox or other cloud *backend* etc... |
|
||||
| P2P Multiplayer | ✅ | ✅ | A way to connect to other browser running the project. But it's almost useless here since many survival features are not implemented. Maybe only to build / explore maps together... |
|
||||
| Voice Chat | ❌(+) | ✅ | Eaglercraft has custom WebRTC voice chat implementation, though it could also be easily implemented there |
|
||||
| Online Servers | ✅ | ❌ | We have custom implementation (including integration on proxy side) for joining to servers |
|
||||
| Plugin Features | ✅ | ❌ | We have Mineflayer plugins support, like Auto Jump & Auto Parkour was added here that way |
|
||||
| Direct Connection | ✅ | ✅ | We have DOM for almost all UI so your extensions and other browser features will work natively like on any other web page |
|
||||
| Moding | ✅(own js mods) | ❌ | This project will support mods for singleplayer. In theory its possible to implement support for modded servers on protocol level (including all needed mods) |
|
||||
| Video Recording | ❌ | ✅ | Doesn't feel needed |
|
||||
| Metaverse Features | ✅(50%) | ❌ | We have videos / images support inside world, but not iframes (custom protocol channel) |
|
||||
| Sounds | ✅ | ✅ | |
|
||||
| Resource Packs | ✅(+extras) | ✅ | This project has very limited support for them (only textures images are loadable for now) |
|
||||
| Assets Compressing & Splitting | ✅ | ❌ | We have advanced Minecraft data processing and good code chunk splitting so the web app will open much faster and use less memory |
|
||||
| Graphics | | | |
|
||||
| Fancy Graphics | ❌ | ✅ | While Eaglercraft has top-level shaders we don't even support lighting |
|
||||
| Fast & Efficient Graphics | ❌(+) | ❌ | Feels like no one needs to have 64 rendering distance work smoothly |
|
||||
| VR | ✅(-) | ❌ | Feels like not needed feature. UI is missing in this project since DOM can't be rendered in VR so Eaglercraft could be better in that aspect |
|
||||
| AR | ❌ | ❌ | Would be the most useless feature |
|
||||
| Minimap & Waypoints | ✅(-) | ❌ | We have buggy minimap, which can be enabled in settings and full map is opened by pressing `M` key |
|
||||
|
||||
Features available to only this project:
|
||||
|
||||
- CSS & JS Customization
|
||||
- JS Real Time Debugging & Console Scripting (eg Devtools)
|
||||
|
||||
### Tech Stack
|
||||
|
||||
Bundler: Rsbuild!
|
||||
UI: powered by React and css modules. Storybook helps with UI development.
|
||||
|
||||
### Rare WEB Features
|
||||
|
||||
There are a number of web features that are not commonly used but you might be interested in them if you decide to build your own game in the web.
|
||||
|
||||
TODO
|
||||
|
||||
| API | Usage & Description |
|
||||
| ------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `Crypto` API | Used to make chat features work when joining online servers with authentication. |
|
||||
| `requestPointerLock({ unadjustedMovement: true })` API | Required for games. Disables system mouse acceleration (important for Mac users). Aka mouse raw input |
|
||||
| `navigator.keyboard.lock()` | (only in Chromium browsers) When entering fullscreen it allows to use any key combination like ctrl+w in the game |
|
||||
| `navigator.keyboard.getLayoutMap()` | (only in Chromium browsers) To display the right keyboard symbol for the key keybinding on different keyboard layouts (e.g. QWERTY vs AZERTY) |
|
||||
39
assets/config.html
Normal file
39
assets/config.html
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Configure client</title>
|
||||
<script>
|
||||
function removeSettings() {
|
||||
if (confirm('Are you sure you want to RESET ALL SETTINGS?')) {
|
||||
localStorage.setItem('options', '{}');
|
||||
location.reload();
|
||||
}
|
||||
}
|
||||
|
||||
function removeAllData() {
|
||||
localStorage.removeItem('serversList')
|
||||
localStorage.removeItem('serversHistory')
|
||||
localStorage.removeItem('authenticatedAccounts')
|
||||
localStorage.removeItem('modsAutoUpdateLastCheck')
|
||||
localStorage.removeItem('firstModsPageVisit')
|
||||
localStorage.removeItem('proxiesData')
|
||||
localStorage.removeItem('keybindings')
|
||||
localStorage.removeItem('username')
|
||||
localStorage.removeItem('customCommands')
|
||||
localStorage.removeItem('options')
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div style="display: flex;gap: 10px;">
|
||||
<button onclick="removeSettings()">Reset all settings</button>
|
||||
<button onclick="removeAllData()">Remove all user data (but not mods or worlds)</button>
|
||||
<!-- <button>Remove all user data (worlds, resourcepacks)</button> -->
|
||||
<!-- <button>Remove all mods</button> -->
|
||||
<!-- <button>Remove all mod repositories</button> -->
|
||||
</div>
|
||||
<input />
|
||||
</body>
|
||||
</html>
|
||||
2
assets/customTextures/readme.md
Normal file
2
assets/customTextures/readme.md
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
here you can place custom textures for bundled files (blocks/items) e.g. blocks/stone.png
|
||||
get file names from here (blocks/items) https://zardoy.github.io/mc-assets/
|
||||
237
assets/debug-inputs.html
Normal file
237
assets/debug-inputs.html
Normal file
|
|
@ -0,0 +1,237 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Web Input Debugger</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
padding: 20px;
|
||||
background: #f0f0f0;
|
||||
}
|
||||
.key-container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 60px);
|
||||
gap: 5px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
.key {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border: 2px solid #333;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: bold;
|
||||
background: white;
|
||||
position: relative;
|
||||
user-select: none;
|
||||
}
|
||||
.key.pressed {
|
||||
background: #90EE90;
|
||||
}
|
||||
.key .duration {
|
||||
position: absolute;
|
||||
bottom: 2px;
|
||||
font-size: 10px;
|
||||
}
|
||||
.key .count {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
right: 2px;
|
||||
font-size: 10px;
|
||||
}
|
||||
.controls {
|
||||
margin: 20px 0;
|
||||
padding: 10px;
|
||||
background: white;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.wasd-container {
|
||||
position: relative;
|
||||
width: 190px;
|
||||
height: 130px;
|
||||
}
|
||||
#KeyW {
|
||||
position: absolute;
|
||||
left: 65px;
|
||||
top: 0;
|
||||
}
|
||||
#KeyA {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 65px;
|
||||
}
|
||||
#KeyS {
|
||||
position: absolute;
|
||||
left: 65px;
|
||||
top: 65px;
|
||||
}
|
||||
#KeyD {
|
||||
position: absolute;
|
||||
left: 130px;
|
||||
top: 65px;
|
||||
}
|
||||
.space-container {
|
||||
margin-top: 20px;
|
||||
}
|
||||
#Space {
|
||||
width: 190px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="controls">
|
||||
<label>
|
||||
<input type="checkbox" id="repeatMode"> Use keydown repeat mode (auto key-up after 150ms of no repeat)
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="wasd-container">
|
||||
<div id="KeyW" class="key" data-code="KeyW">W</div>
|
||||
<div id="KeyA" class="key" data-code="KeyA">A</div>
|
||||
<div id="KeyS" class="key" data-code="KeyS">S</div>
|
||||
<div id="KeyD" class="key" data-code="KeyD">D</div>
|
||||
</div>
|
||||
|
||||
<div class="key-container">
|
||||
<div id="ControlLeft" class="key" data-code="ControlLeft">Ctrl</div>
|
||||
</div>
|
||||
|
||||
<div class="space-container">
|
||||
<div id="Space" class="key" data-code="Space">Space</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const keys = {};
|
||||
const keyStats = {};
|
||||
const pressStartTimes = {};
|
||||
const keyTimeouts = {};
|
||||
|
||||
function initKeyStats(code) {
|
||||
if (!keyStats[code]) {
|
||||
keyStats[code] = {
|
||||
pressCount: 0,
|
||||
duration: 0,
|
||||
startTime: 0
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function updateKeyVisuals(code) {
|
||||
const element = document.getElementById(code);
|
||||
if (!element) return;
|
||||
|
||||
const stats = keyStats[code];
|
||||
if (keys[code]) {
|
||||
element.classList.add('pressed');
|
||||
const currentDuration = ((Date.now() - stats.startTime) / 1000).toFixed(1);
|
||||
element.innerHTML = `${element.getAttribute('data-code').replace('Key', '').replace('Left', '')}<span class="duration">${currentDuration}s</span><span class="count">${stats.pressCount}</span>`;
|
||||
} else {
|
||||
element.classList.remove('pressed');
|
||||
element.innerHTML = `${element.getAttribute('data-code').replace('Key', '').replace('Left', '')}<span class="count">${stats.pressCount}</span>`;
|
||||
}
|
||||
}
|
||||
|
||||
function releaseKey(code) {
|
||||
keys[code] = false;
|
||||
if (pressStartTimes[code]) {
|
||||
keyStats[code].duration += (Date.now() - pressStartTimes[code]) / 1000;
|
||||
delete pressStartTimes[code];
|
||||
}
|
||||
updateKeyVisuals(code);
|
||||
}
|
||||
|
||||
function handleKeyDown(event) {
|
||||
const code = event.code;
|
||||
const isRepeatMode = document.getElementById('repeatMode').checked;
|
||||
|
||||
initKeyStats(code);
|
||||
|
||||
// Clear any existing timeout for this key
|
||||
if (keyTimeouts[code]) {
|
||||
clearTimeout(keyTimeouts[code]);
|
||||
delete keyTimeouts[code];
|
||||
}
|
||||
|
||||
if (isRepeatMode) {
|
||||
// In repeat mode, always handle the keydown
|
||||
if (!keys[code] || event.repeat) {
|
||||
keys[code] = true;
|
||||
if (!event.repeat) {
|
||||
// Only increment count on initial press, not repeats
|
||||
keyStats[code].pressCount++;
|
||||
keyStats[code].startTime = Date.now();
|
||||
pressStartTimes[code] = Date.now();
|
||||
}
|
||||
}
|
||||
|
||||
// Set timeout to release key if no repeat events come
|
||||
keyTimeouts[code] = setTimeout(() => {
|
||||
releaseKey(code);
|
||||
}, 150);
|
||||
} else {
|
||||
// In normal mode, only handle keydown if key is not already pressed
|
||||
if (!keys[code]) {
|
||||
keys[code] = true;
|
||||
keyStats[code].pressCount++;
|
||||
keyStats[code].startTime = Date.now();
|
||||
pressStartTimes[code] = Date.now();
|
||||
}
|
||||
}
|
||||
|
||||
updateKeyVisuals(code);
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
function handleKeyUp(event) {
|
||||
const code = event.code;
|
||||
const isRepeatMode = document.getElementById('repeatMode').checked;
|
||||
|
||||
if (!isRepeatMode) {
|
||||
releaseKey(code);
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
// Initialize all monitored keys
|
||||
const monitoredKeys = ['KeyW', 'KeyA', 'KeyS', 'KeyD', 'ControlLeft', 'Space'];
|
||||
monitoredKeys.forEach(code => {
|
||||
initKeyStats(code);
|
||||
const element = document.getElementById(code);
|
||||
if (element) {
|
||||
element.innerHTML = `${element.getAttribute('data-code').replace('Key', '').replace('Left', '')}<span class="count">0</span>`;
|
||||
}
|
||||
});
|
||||
|
||||
// Start visual updates
|
||||
setInterval(() => {
|
||||
monitoredKeys.forEach(code => {
|
||||
if (keys[code]) {
|
||||
updateKeyVisuals(code);
|
||||
}
|
||||
});
|
||||
}, 100);
|
||||
|
||||
// Event listeners
|
||||
document.addEventListener('keydown', handleKeyDown);
|
||||
document.addEventListener('keyup', handleKeyUp);
|
||||
|
||||
// Handle mode changes
|
||||
document.getElementById('repeatMode').addEventListener('change', () => {
|
||||
// Release all keys when switching modes
|
||||
monitoredKeys.forEach(code => {
|
||||
if (keys[code]) {
|
||||
releaseKey(code);
|
||||
}
|
||||
if (keyTimeouts[code]) {
|
||||
clearTimeout(keyTimeouts[code]);
|
||||
delete keyTimeouts[code];
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "Prismarine Web Client",
|
||||
"short_name": "Prismarine Web Client",
|
||||
"name": "Minecraft Web Client",
|
||||
"short_name": "Minecraft Web Client",
|
||||
"scope": "./",
|
||||
"start_url": "./",
|
||||
"icons": [
|
||||
|
|
|
|||
4
assets/playground.html
Normal file
4
assets/playground.html
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<!-- just redirect to /playground/ -->
|
||||
<script>
|
||||
window.location.href = `/playground/${window.location.search}`
|
||||
</script>
|
||||
75
config.json
75
config.json
|
|
@ -1,23 +1,80 @@
|
|||
{
|
||||
"version": 1,
|
||||
"defaultHost": "<from-proxy>",
|
||||
"defaultProxy": "proxy.mcraft.fun",
|
||||
"defaultProxy": "https://proxy.mcraft.fun",
|
||||
"mapsProvider": "https://maps.mcraft.fun/",
|
||||
"skinTexturesProxy": "",
|
||||
"peerJsServer": "",
|
||||
"peerJsServerFallback": "https://p2p.mcraft.fun",
|
||||
"promoteServers": [
|
||||
{
|
||||
"ip": "wss://play.mcraft.fun"
|
||||
},
|
||||
{
|
||||
"ip": "wss://play.webmc.fun",
|
||||
"name": "WebMC"
|
||||
},
|
||||
{
|
||||
"ip": "wss://ws.fuchsmc.net"
|
||||
},
|
||||
{
|
||||
"ip": "wss://play2.mcraft.fun"
|
||||
},
|
||||
{
|
||||
"ip": "wss://play-creative.mcraft.fun",
|
||||
"description": "Might be available soon, stay tuned!"
|
||||
},
|
||||
{
|
||||
"ip": "kaboom.pw",
|
||||
"version": "1.18.2",
|
||||
"description": "Chaos and destruction server. Free for everyone."
|
||||
"version": "1.20.3",
|
||||
"description": "Very nice a polite server. Must try for everyone!"
|
||||
}
|
||||
],
|
||||
"rightSideText": "A Minecraft client clone in the browser!",
|
||||
"splashText": "The sunset is coming!",
|
||||
"splashTextFallback": "Welcome!",
|
||||
"pauseLinks": [
|
||||
[
|
||||
{
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"type": "discord"
|
||||
}
|
||||
]
|
||||
],
|
||||
"defaultUsername": "mcrafter{0-9999}",
|
||||
"mobileButtons": [
|
||||
{
|
||||
"action": "general.drop",
|
||||
"actionHold": "general.dropStack",
|
||||
"label": "Q"
|
||||
},
|
||||
{
|
||||
"ip": "go.mineberry.org",
|
||||
"version": "1.18.2",
|
||||
"description": "One of the best servers here. Join now!"
|
||||
"action": "general.selectItem",
|
||||
"actionHold": "",
|
||||
"label": "S"
|
||||
},
|
||||
{
|
||||
"ip": "sus.shhnowisnottheti.me",
|
||||
"version": "1.18.2",
|
||||
"description": "Creative, your own 'boxes' (islands)"
|
||||
"action": "general.debugOverlay",
|
||||
"actionHold": "general.debugOverlayHelpMenu",
|
||||
"label": "F3"
|
||||
},
|
||||
{
|
||||
"action": "general.playersList",
|
||||
"actionHold": "",
|
||||
"icon": "pixelarticons:users",
|
||||
"label": "TAB"
|
||||
},
|
||||
{
|
||||
"action": "general.chat",
|
||||
"actionHold": "",
|
||||
"label": ""
|
||||
},
|
||||
{
|
||||
"action": "ui.pauseMenu",
|
||||
"actionHold": "",
|
||||
"label": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
5
config.mcraft-only.json
Normal file
5
config.mcraft-only.json
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"alwaysReconnectButton": true,
|
||||
"reportBugButtonWithReconnect": true,
|
||||
"allowAutoConnect": true
|
||||
}
|
||||
|
|
@ -1,8 +1,11 @@
|
|||
import { defineConfig } from 'cypress'
|
||||
|
||||
const isPerformanceTest = process.env.PERFORMANCE_TEST === 'true'
|
||||
|
||||
export default defineConfig({
|
||||
video: false,
|
||||
chromeWebSecurity: false,
|
||||
screenshotOnRunFailure: true, // Enable screenshots on test failures
|
||||
e2e: {
|
||||
// We've imported your old cypress plugins here.
|
||||
// You may want to clean this up later by importing these.
|
||||
|
|
@ -31,7 +34,7 @@ export default defineConfig({
|
|||
return require('./cypress/plugins/index.js')(on, config)
|
||||
},
|
||||
baseUrl: 'http://localhost:8080',
|
||||
specPattern: 'cypress/e2e/**/*.spec.ts',
|
||||
specPattern: !isPerformanceTest ? 'cypress/e2e/smoke.spec.ts' : 'cypress/e2e/rendering_performance.spec.ts',
|
||||
excludeSpecPattern: ['**/__snapshots__/*', '**/__image_snapshots__/*'],
|
||||
},
|
||||
})
|
||||
|
|
|
|||
32
cypress/e2e/rendering_performance.spec.ts
Normal file
32
cypress/e2e/rendering_performance.spec.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
/// <reference types="cypress" />
|
||||
import { BenchmarkAdapterInfo, getAllInfoLines } from '../../src/benchmarkAdapter'
|
||||
import { cleanVisit } from './shared'
|
||||
|
||||
it('Benchmark rendering performance', () => {
|
||||
cleanVisit('/?openBenchmark=true&renderDistance=5')
|
||||
// wait for render end event
|
||||
return cy.document().then({ timeout: 180_000 }, doc => {
|
||||
return new Cypress.Promise(resolve => {
|
||||
cy.log('Waiting for world to load')
|
||||
doc.addEventListener('cypress-world-ready', resolve)
|
||||
}).then(() => {
|
||||
cy.log('World loaded')
|
||||
})
|
||||
}).then(() => {
|
||||
cy.window().then(win => {
|
||||
const adapter = win.benchmarkAdapter as BenchmarkAdapterInfo
|
||||
|
||||
const messages = getAllInfoLines(adapter)
|
||||
// wait for 10 seconds
|
||||
cy.wait(10_000)
|
||||
const messages2 = getAllInfoLines(adapter, true)
|
||||
for (const message of messages) {
|
||||
cy.log(message)
|
||||
}
|
||||
for (const message of messages2) {
|
||||
cy.log(message)
|
||||
}
|
||||
cy.writeFile('benchmark.txt', [...messages, ...messages2].join('\n'))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -38,18 +38,18 @@ it('Loads & renders singleplayer', () => {
|
|||
testWorldLoad()
|
||||
})
|
||||
|
||||
it('Joins to local flying-squid server', () => {
|
||||
it.skip('Joins to local flying-squid server', () => {
|
||||
visit('/?ip=localhost&version=1.16.1')
|
||||
window.localStorage.version = ''
|
||||
// todo replace with data-test
|
||||
// cy.get('[data-test-id="servers-screen-button"]').click()
|
||||
// cy.get('[data-test-id="server-ip"]').clear().focus().type('localhost')
|
||||
// cy.get('[data-test-id="version"]').clear().focus().type('1.16.1') // todo needs to fix autoversion
|
||||
cy.get('[data-test-id="connect-qs"]').click()
|
||||
cy.get('[data-test-id="connect-qs"]').click() // todo! cypress sometimes doesn't click
|
||||
testWorldLoad()
|
||||
})
|
||||
|
||||
it('Joins to local latest Java vanilla server', () => {
|
||||
it.skip('Joins to local latest Java vanilla server', () => {
|
||||
const version = supportedVersions.at(-1)!
|
||||
cy.task('startServer', [version, 25_590]).then(() => {
|
||||
visit('/?ip=localhost:25590&username=bot')
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
//@ts-check
|
||||
import mcServer from 'flying-squid'
|
||||
import defaultOptions from 'flying-squid/config/default-settings.json' assert { type: 'json' }
|
||||
import defaultOptions from 'flying-squid/config/default-settings.json' with { type: 'json' }
|
||||
|
||||
/** @type {Options} */
|
||||
const serverOptions = {
|
||||
|
|
|
|||
|
|
@ -32,8 +32,8 @@
|
|||
❌ world_border_warning_reach
|
||||
❌ simulation_distance
|
||||
❌ chunk_biomes
|
||||
❌ damage_event
|
||||
❌ hurt_animation
|
||||
✅ damage_event
|
||||
✅ spawn_entity
|
||||
✅ spawn_entity_experience_orb
|
||||
✅ named_entity_spawn
|
||||
|
|
|
|||
BIN
docs-assets/npm-banner.jpeg
Normal file
BIN
docs-assets/npm-banner.jpeg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 96 KiB |
1
experiments/state.html
Normal file
1
experiments/state.html
Normal file
|
|
@ -0,0 +1 @@
|
|||
<script src="state.ts" type="module"></script>
|
||||
37
experiments/state.ts
Normal file
37
experiments/state.ts
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import { SmoothSwitcher } from '../renderer/viewer/lib/smoothSwitcher'
|
||||
|
||||
const div = document.createElement('div')
|
||||
div.style.width = '100px'
|
||||
div.style.height = '100px'
|
||||
div.style.backgroundColor = 'red'
|
||||
document.body.appendChild(div)
|
||||
|
||||
const pos = {x: 0, y: 0}
|
||||
|
||||
const positionSwitcher = new SmoothSwitcher(() => pos, (key, value) => {
|
||||
pos[key] = value
|
||||
})
|
||||
globalThis.positionSwitcher = positionSwitcher
|
||||
|
||||
document.body.addEventListener('keydown', e => {
|
||||
if (e.code === 'ArrowLeft' || e.code === 'ArrowRight') {
|
||||
const to = {
|
||||
x: e.code === 'ArrowLeft' ? -100 : 100
|
||||
}
|
||||
console.log(pos, to)
|
||||
positionSwitcher.transitionTo(to, e.code === 'ArrowLeft' ? 'Left' : 'Right', () => {
|
||||
console.log('Switched to ', e.code === 'ArrowLeft' ? 'Left' : 'Right')
|
||||
})
|
||||
}
|
||||
if (e.code === 'Space') {
|
||||
pos.x = 200
|
||||
}
|
||||
})
|
||||
|
||||
const render = () => {
|
||||
positionSwitcher.update()
|
||||
div.style.transform = `translate(${pos.x}px, ${pos.y}px)`
|
||||
requestAnimationFrame(render)
|
||||
}
|
||||
|
||||
render()
|
||||
13
experiments/three-item.html
Normal file
13
experiments/three-item.html
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Minecraft Item Viewer</title>
|
||||
<style>
|
||||
body { margin: 0; overflow: hidden; }
|
||||
canvas { display: block; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<script type="module" src="./three-item.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
108
experiments/three-item.ts
Normal file
108
experiments/three-item.ts
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
import * as THREE from 'three'
|
||||
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
|
||||
import itemsAtlas from 'mc-assets/dist/itemsAtlasLegacy.png'
|
||||
import { createItemMeshFromCanvas, createItemMesh } from '../renderer/viewer/three/itemMesh'
|
||||
|
||||
// Create scene, camera and renderer
|
||||
const scene = new THREE.Scene()
|
||||
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000)
|
||||
const renderer = new THREE.WebGLRenderer({ antialias: true })
|
||||
renderer.setSize(window.innerWidth, window.innerHeight)
|
||||
document.body.appendChild(renderer.domElement)
|
||||
|
||||
// Setup camera and controls
|
||||
camera.position.set(0, 0, 3)
|
||||
const controls = new OrbitControls(camera, renderer.domElement)
|
||||
controls.enableDamping = true
|
||||
|
||||
// Background and lights
|
||||
scene.background = new THREE.Color(0x333333)
|
||||
const ambientLight = new THREE.AmbientLight(0xffffff, 0.7)
|
||||
scene.add(ambientLight)
|
||||
|
||||
// Animation loop
|
||||
function animate () {
|
||||
requestAnimationFrame(animate)
|
||||
controls.update()
|
||||
renderer.render(scene, camera)
|
||||
}
|
||||
|
||||
async function setupItemMesh () {
|
||||
try {
|
||||
const loader = new THREE.TextureLoader()
|
||||
const atlasTexture = await loader.loadAsync(itemsAtlas)
|
||||
|
||||
// Pixel-art configuration
|
||||
atlasTexture.magFilter = THREE.NearestFilter
|
||||
atlasTexture.minFilter = THREE.NearestFilter
|
||||
atlasTexture.generateMipmaps = false
|
||||
atlasTexture.wrapS = atlasTexture.wrapT = THREE.ClampToEdgeWrapping
|
||||
|
||||
// Extract the tile at x=2, y=0 (16x16)
|
||||
const tileSize = 16
|
||||
const tileX = 2
|
||||
const tileY = 0
|
||||
|
||||
const canvas = document.createElement('canvas')
|
||||
canvas.width = tileSize
|
||||
canvas.height = tileSize
|
||||
const ctx = canvas.getContext('2d')!
|
||||
|
||||
ctx.imageSmoothingEnabled = false
|
||||
ctx.drawImage(
|
||||
atlasTexture.image,
|
||||
tileX * tileSize,
|
||||
tileY * tileSize,
|
||||
tileSize,
|
||||
tileSize,
|
||||
0,
|
||||
0,
|
||||
tileSize,
|
||||
tileSize
|
||||
)
|
||||
|
||||
// Test both approaches - working manual extraction:
|
||||
const meshOld = createItemMeshFromCanvas(canvas, { depth: 0.1 })
|
||||
meshOld.position.x = -1
|
||||
meshOld.rotation.x = -Math.PI / 12
|
||||
meshOld.rotation.y = Math.PI / 12
|
||||
scene.add(meshOld)
|
||||
|
||||
// And new unified function:
|
||||
const atlasWidth = atlasTexture.image.width
|
||||
const atlasHeight = atlasTexture.image.height
|
||||
const u = (tileX * tileSize) / atlasWidth
|
||||
const v = (tileY * tileSize) / atlasHeight
|
||||
const sizeX = tileSize / atlasWidth
|
||||
const sizeY = tileSize / atlasHeight
|
||||
|
||||
console.log('Debug texture coords:', {u, v, sizeX, sizeY, atlasWidth, atlasHeight})
|
||||
|
||||
const resultNew = createItemMesh(atlasTexture, {
|
||||
u, v, sizeX, sizeY
|
||||
}, {
|
||||
faceCamera: false,
|
||||
use3D: true,
|
||||
depth: 0.1
|
||||
})
|
||||
|
||||
resultNew.mesh.position.x = 1
|
||||
resultNew.mesh.rotation.x = -Math.PI / 12
|
||||
resultNew.mesh.rotation.y = Math.PI / 12
|
||||
scene.add(resultNew.mesh)
|
||||
|
||||
animate()
|
||||
} catch (err) {
|
||||
console.error('Failed to create item mesh:', err)
|
||||
}
|
||||
}
|
||||
|
||||
// Handle window resize
|
||||
window.addEventListener('resize', () => {
|
||||
camera.aspect = window.innerWidth / window.innerHeight
|
||||
camera.updateProjectionMatrix()
|
||||
renderer.setSize(window.innerWidth, window.innerHeight)
|
||||
})
|
||||
|
||||
// Start
|
||||
setupItemMesh()
|
||||
5
experiments/three-labels.html
Normal file
5
experiments/three-labels.html
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<script type="module" src="three-labels.ts"></script>
|
||||
<style>
|
||||
body { margin: 0; }
|
||||
canvas { display: block; }
|
||||
</style>
|
||||
67
experiments/three-labels.ts
Normal file
67
experiments/three-labels.ts
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
import * as THREE from 'three'
|
||||
import { FirstPersonControls } from 'three/addons/controls/FirstPersonControls.js'
|
||||
import { createWaypointSprite, WAYPOINT_CONFIG } from '../renderer/viewer/three/waypointSprite'
|
||||
|
||||
// Create scene, camera and renderer
|
||||
const scene = new THREE.Scene()
|
||||
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000)
|
||||
const renderer = new THREE.WebGLRenderer({ antialias: true })
|
||||
renderer.setSize(window.innerWidth, window.innerHeight)
|
||||
document.body.appendChild(renderer.domElement)
|
||||
|
||||
// Add FirstPersonControls
|
||||
const controls = new FirstPersonControls(camera, renderer.domElement)
|
||||
controls.lookSpeed = 0.1
|
||||
controls.movementSpeed = 10
|
||||
controls.lookVertical = true
|
||||
controls.constrainVertical = true
|
||||
controls.verticalMin = 0.1
|
||||
controls.verticalMax = Math.PI - 0.1
|
||||
|
||||
// Position camera
|
||||
camera.position.y = 1.6 // Typical eye height
|
||||
camera.lookAt(0, 1.6, -1)
|
||||
|
||||
// Create a helper grid and axes
|
||||
const grid = new THREE.GridHelper(20, 20)
|
||||
scene.add(grid)
|
||||
const axes = new THREE.AxesHelper(5)
|
||||
scene.add(axes)
|
||||
|
||||
// Create waypoint sprite via utility
|
||||
const waypoint = createWaypointSprite({
|
||||
position: new THREE.Vector3(0, 0, -5),
|
||||
color: 0xff0000,
|
||||
label: 'Target',
|
||||
})
|
||||
scene.add(waypoint.group)
|
||||
|
||||
// Use built-in offscreen arrow from utils
|
||||
waypoint.enableOffscreenArrow(true)
|
||||
waypoint.setArrowParent(scene)
|
||||
|
||||
// Animation loop
|
||||
function animate() {
|
||||
requestAnimationFrame(animate)
|
||||
|
||||
const delta = Math.min(clock.getDelta(), 0.1)
|
||||
controls.update(delta)
|
||||
|
||||
// Unified camera update (size, distance text, arrow, visibility)
|
||||
const sizeVec = renderer.getSize(new THREE.Vector2())
|
||||
waypoint.updateForCamera(camera.position, camera, sizeVec.width, sizeVec.height)
|
||||
|
||||
renderer.render(scene, camera)
|
||||
}
|
||||
|
||||
// Handle window resize
|
||||
window.addEventListener('resize', () => {
|
||||
camera.aspect = window.innerWidth / window.innerHeight
|
||||
camera.updateProjectionMatrix()
|
||||
renderer.setSize(window.innerWidth, window.innerHeight)
|
||||
})
|
||||
|
||||
// Add clock for controls
|
||||
const clock = new THREE.Clock()
|
||||
|
||||
animate()
|
||||
|
|
@ -1,101 +1,60 @@
|
|||
import * as THREE from 'three'
|
||||
import * as tweenJs from '@tweenjs/tween.js'
|
||||
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
|
||||
import * as THREE from 'three';
|
||||
import Jimp from 'jimp';
|
||||
|
||||
// Create scene, camera and renderer
|
||||
const scene = new THREE.Scene()
|
||||
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000)
|
||||
camera.position.set(0, 0, 5)
|
||||
const renderer = new THREE.WebGLRenderer()
|
||||
renderer.setSize(window.innerWidth, window.innerHeight)
|
||||
document.body.appendChild(renderer.domElement)
|
||||
|
||||
const controls = new OrbitControls(camera, renderer.domElement)
|
||||
// Position camera
|
||||
camera.position.z = 5
|
||||
|
||||
const geometry = new THREE.BoxGeometry(1, 1, 1)
|
||||
const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 })
|
||||
const cube = new THREE.Mesh(geometry, material)
|
||||
cube.position.set(0.5, 0.5, 0.5);
|
||||
const group = new THREE.Group()
|
||||
group.add(cube)
|
||||
group.position.set(-0.5, -0.5, -0.5);
|
||||
const outerGroup = new THREE.Group()
|
||||
outerGroup.add(group)
|
||||
outerGroup.scale.set(0.2, 0.2, 0.2)
|
||||
outerGroup.position.set(1, 1, 0)
|
||||
scene.add(outerGroup)
|
||||
// Create a canvas with some content
|
||||
const canvas = document.createElement('canvas')
|
||||
canvas.width = 256
|
||||
canvas.height = 256
|
||||
const ctx = canvas.getContext('2d')
|
||||
|
||||
// const mesh = new THREE.Mesh(new THREE.BoxGeometry(1, 1, 1), new THREE.MeshBasicMaterial({ color: 0x00_00_ff, transparent: true, opacity: 0.5 }))
|
||||
// mesh.position.set(0.5, 1, 0.5)
|
||||
// const group = new THREE.Group()
|
||||
// group.add(mesh)
|
||||
// group.position.set(-0.5, -1, -0.5)
|
||||
// const outerGroup = new THREE.Group()
|
||||
// outerGroup.add(group)
|
||||
// // outerGroup.position.set(this.camera.position.x, this.camera.position.y, this.camera.position.z)
|
||||
// scene.add(outerGroup)
|
||||
scene.background = new THREE.Color(0x444444)
|
||||
|
||||
new tweenJs.Tween(group.rotation).to({ z: THREE.MathUtils.degToRad(90) }, 1000).yoyo(true).repeat(Infinity).start()
|
||||
// Draw something on the canvas
|
||||
ctx.fillStyle = '#444444'
|
||||
// ctx.fillRect(0, 0, 256, 256)
|
||||
ctx.fillStyle = 'red'
|
||||
ctx.font = '48px Arial'
|
||||
ctx.textAlign = 'center'
|
||||
ctx.textBaseline = 'middle'
|
||||
ctx.fillText('Hello!', 128, 128)
|
||||
|
||||
const tweenGroup = new tweenJs.Group()
|
||||
function animate () {
|
||||
tweenGroup.update()
|
||||
requestAnimationFrame(animate)
|
||||
// cube.rotation.x += 0.01
|
||||
// cube.rotation.y += 0.01
|
||||
renderer.render(scene, camera)
|
||||
// Create bitmap and texture
|
||||
async function createTexturedBox() {
|
||||
const canvas2 = new OffscreenCanvas(256, 256)
|
||||
const ctx2 = canvas2.getContext('2d')!
|
||||
ctx2.drawImage(canvas, 0, 0)
|
||||
const texture = new THREE.Texture(canvas2)
|
||||
texture.magFilter = THREE.NearestFilter
|
||||
texture.minFilter = THREE.NearestFilter
|
||||
texture.needsUpdate = true
|
||||
texture.flipY = false
|
||||
|
||||
// Create box with texture
|
||||
const geometry = new THREE.BoxGeometry(2, 2, 2)
|
||||
const material = new THREE.MeshBasicMaterial({
|
||||
map: texture,
|
||||
side: THREE.DoubleSide,
|
||||
premultipliedAlpha: false,
|
||||
})
|
||||
const cube = new THREE.Mesh(geometry, material)
|
||||
scene.add(cube)
|
||||
}
|
||||
|
||||
// Create the textured box
|
||||
createTexturedBox()
|
||||
|
||||
// Animation loop
|
||||
function animate() {
|
||||
requestAnimationFrame(animate)
|
||||
renderer.render(scene, camera)
|
||||
}
|
||||
animate()
|
||||
|
||||
// let animation
|
||||
|
||||
window.animate = () => {
|
||||
// new Tween.Tween(group.position).to({ y: group.position.y - 1}, 1000 * 0.35/2).yoyo(true).repeat(1).start()
|
||||
new tweenJs.Tween(group.rotation, tweenGroup).to({ z: THREE.MathUtils.degToRad(90) }, 1000 * 0.35 / 2).yoyo(true).repeat(Infinity).start().onRepeat(() => {
|
||||
console.log('done')
|
||||
})
|
||||
}
|
||||
|
||||
window.stop = () => {
|
||||
tweenGroup.removeAll()
|
||||
}
|
||||
|
||||
|
||||
function createGeometryFromImage() {
|
||||
return new Promise<THREE.ShapeGeometry>((resolve, reject) => {
|
||||
const img = new Image();
|
||||
img.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QA/wD/AP+gvaeTAAABEElEQVQ4jWNkIAPw2Zv9J0cfXPOSvx/+L/n74T+HqsJ/JlI1T9u3i6H91B7ybdY+vgZuO1majV+fppFmPnuz/+ihy2dv9t/49Wm8mlECkV1FHh5FfPZm/1XXTGX4cechA4eKPMNVq1CGH7cfMBJ0rlxX+X8OVYX/xq9P/5frKifoZ0Z0AwS8HRkYGBgYvt+8xyDXUUbQZgwJPnuz/+wq8gw/7zxk+PXsFUFno0h6mon+l5fgZFhwnYmBTUqMgYGBgaAhLMiaHQyFGOZvf8Lw49FXRgYGhv8MDAwwg/7jMoQFFury/C8Y5m9/wnADohnZVryJhoWBARJ9Cw69gtmMAgiFAcuvZ68Yfj17hU8NXgAATdKfkzbQhBEAAAAASUVORK5CYII='
|
||||
console.log('img.complete', img.complete)
|
||||
img.onload = () => {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = img.width;
|
||||
canvas.height = img.height;
|
||||
const context = canvas.getContext('2d');
|
||||
context.drawImage(img, 0, 0, img.width, img.height);
|
||||
const imgData = context.getImageData(0, 0, img.width, img.height);
|
||||
|
||||
const shape = new THREE.Shape();
|
||||
for (let y = 0; y < img.height; y++) {
|
||||
for (let x = 0; x < img.width; x++) {
|
||||
const index = (y * img.width + x) * 4;
|
||||
const alpha = imgData.data[index + 3];
|
||||
if (alpha !== 0) {
|
||||
shape.lineTo(x, y);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const geometry = new THREE.ShapeGeometry(shape);
|
||||
resolve(geometry);
|
||||
};
|
||||
img.onerror = reject;
|
||||
});
|
||||
}
|
||||
|
||||
// Usage:
|
||||
const shapeGeomtry = createGeometryFromImage().then(geometry => {
|
||||
const material = new THREE.MeshBasicMaterial({ color: 0xffffff });
|
||||
const mesh = new THREE.Mesh(geometry, material);
|
||||
scene.add(mesh);
|
||||
})
|
||||
|
|
|
|||
85
index.html
85
index.html
|
|
@ -1,8 +1,18 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta name="darkreader-lock">
|
||||
<script>
|
||||
window.startLoad = Date.now()
|
||||
// g663 fix: forbid change of string prototype
|
||||
Object.defineProperty(String.prototype, 'format', {
|
||||
writable: false,
|
||||
configurable: false
|
||||
});
|
||||
Object.defineProperty(String.prototype, 'replaceAll', {
|
||||
writable: false,
|
||||
configurable: false
|
||||
});
|
||||
</script>
|
||||
<!-- // #region initial loader -->
|
||||
<script async>
|
||||
|
|
@ -15,6 +25,9 @@
|
|||
<div>
|
||||
<div style="font-size: calc(var(--font-size) * 1.8);color: lightgray;" class="title">Loading...</div>
|
||||
<div style="font-size: var(--font-size);color: rgb(176, 176, 176);margin-top: 3px;text-align: center" class="subtitle">A true Minecraft client in your browser!</div>
|
||||
<!-- small text pre -->
|
||||
<div style="font-size: calc(var(--font-size) * 0.6);color: rgb(150, 150, 150);margin-top: 3px;text-align: center;white-space: pre-line;" class="advanced-info"></div>
|
||||
<div style="font-size: calc(var(--font-size) * 0.6);color: rgb(255, 100, 100);margin-top: 10px;text-align: center;display: none;" class="ios-warning">Only iOS 15+ is supported due to performance optimizations</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
|
|
@ -24,17 +37,48 @@
|
|||
if (!window.pageLoaded) {
|
||||
document.documentElement.appendChild(loadingDivElem)
|
||||
}
|
||||
|
||||
// iOS version detection
|
||||
const getIOSVersion = () => {
|
||||
const match = navigator.userAgent.match(/OS (\d+)_(\d+)_?(\d+)?/);
|
||||
return match ? parseInt(match[1], 10) : null;
|
||||
}
|
||||
|
||||
// load error handling
|
||||
const onError = (message) => {
|
||||
console.log(message)
|
||||
const onError = (errorOrMessage, log = false) => {
|
||||
let message = errorOrMessage instanceof Error ? (errorOrMessage.stack ?? errorOrMessage.message) : errorOrMessage
|
||||
if (log) console.log(message)
|
||||
if (typeof message !== 'string') message = String(message)
|
||||
if (document.querySelector('.initial-loader') && document.querySelector('.initial-loader').querySelector('.title').textContent !== 'Error') {
|
||||
document.querySelector('.initial-loader').querySelector('.title').textContent = 'Error'
|
||||
document.querySelector('.initial-loader').querySelector('.subtitle').textContent = message
|
||||
const [errorMessage, ...errorStack] = message.split('\n')
|
||||
document.querySelector('.initial-loader').querySelector('.subtitle').textContent = errorMessage
|
||||
document.querySelector('.initial-loader').querySelector('.advanced-info').textContent = errorStack.join('\n')
|
||||
|
||||
// Show iOS warning if applicable
|
||||
const iosVersion = getIOSVersion();
|
||||
if (iosVersion !== null && iosVersion < 15) {
|
||||
document.querySelector('.initial-loader').querySelector('.ios-warning').style.display = 'block';
|
||||
}
|
||||
|
||||
if (window.navigator.maxTouchPoints > 1) window.location.hash = '#dev' // show eruda
|
||||
// unregister all sw
|
||||
if (window.navigator.serviceWorker && document.querySelector('.initial-loader').style.opacity !== 0) {
|
||||
console.log('got worker')
|
||||
window.navigator.serviceWorker.getRegistrations().then(registrations => {
|
||||
registrations.forEach(registration => {
|
||||
console.log('got registration')
|
||||
registration.unregister().then(() => {
|
||||
console.log('worker unregistered')
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
window.lastError = errorOrMessage instanceof Error ? errorOrMessage : new Error(errorOrMessage)
|
||||
}
|
||||
}
|
||||
window.addEventListener('unhandledrejection', (e) => onError(e.reason))
|
||||
window.addEventListener('error', (e) => onError(e.message))
|
||||
window.addEventListener('unhandledrejection', (e) => onError(e.reason, true))
|
||||
window.addEventListener('error', (e) => onError(e.error ?? e.message))
|
||||
}
|
||||
insertLoadingDiv()
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
|
|
@ -51,6 +95,25 @@
|
|||
import('https://cdn.skypack.dev/eruda').then(({ default: eruda }) => {
|
||||
eruda.init()
|
||||
})
|
||||
Promise.all([import('https://cdn.skypack.dev/stacktrace-gps'), import('https://cdn.skypack.dev/error-stack-parser')]).then(async ([{ default: StackTraceGPS }, { default: ErrorStackParser }]) => {
|
||||
if (!window.lastError) return
|
||||
|
||||
let stackFrames = [];
|
||||
if (window.lastError instanceof Error) {
|
||||
stackFrames = ErrorStackParser.parse(window.lastError);
|
||||
}
|
||||
console.log('stackFrames', stackFrames)
|
||||
const gps = new StackTraceGPS()
|
||||
const mappedFrames = await Promise.all(
|
||||
stackFrames.map(frame => gps.pinpoint(frame))
|
||||
);
|
||||
console.log('mappedFrames', mappedFrames)
|
||||
|
||||
const stackTrace = mappedFrames
|
||||
.map(frame => `at ${frame.functionName} (${frame.fileName}:${frame.lineNumber}:${frame.columnNumber})`)
|
||||
.join('\n');
|
||||
console.log('stackTrace', stackTrace)
|
||||
})
|
||||
}
|
||||
}
|
||||
checkLoadEruda()
|
||||
|
|
@ -84,21 +147,17 @@
|
|||
window.loadedPlugins[pluginName] = await import(script)
|
||||
}
|
||||
</script> -->
|
||||
<title>Prismarine Web Client</title>
|
||||
<link rel="favicon" href="favicon.png">
|
||||
<link rel="icon" type="image/png" href="favicon.png" />
|
||||
<link rel="canonical" href="https://mcraft.fun">
|
||||
<meta name="description" content="Minecraft web client running in your browser">
|
||||
<title>Minecraft Web Client</title>
|
||||
<!-- <link rel="canonical" href="https://mcraft.fun"> -->
|
||||
<meta name="description" content="Minecraft Java Edition Client in Browser — Full Multiplayer Support, Server Connect, Offline Play — Join real Minecraft servers">
|
||||
<meta name="keywords" content="Play, Minecraft, Online, Web, Java, Server, Single player, Javascript, PrismarineJS, Voxel, WebGL, Three.js">
|
||||
<meta name="date" content="2024-07-11" scheme="YYYY-MM-DD">
|
||||
<meta name="language" content="English">
|
||||
<meta name="theme-color" content="#349474">
|
||||
<meta name='viewport' content='width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover'>
|
||||
<meta property="og:title" content="Prismarine Web Client" />
|
||||
<meta property="og:title" content="Minecraft Web Client" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:image" content="favicon.png" />
|
||||
<meta name="format-detection" content="telephone=no">
|
||||
<link rel="manifest" href="manifest.json" crossorigin="use-credentials">
|
||||
</head>
|
||||
<body>
|
||||
<div id="react-root"></div>
|
||||
|
|
|
|||
123
package.json
123
package.json
|
|
@ -5,34 +5,45 @@
|
|||
"scripts": {
|
||||
"dev-rsbuild": "rsbuild dev",
|
||||
"dev-proxy": "node server.js",
|
||||
"start": "run-p dev-rsbuild dev-proxy watch-mesher",
|
||||
"start-watch-script": "nodemon -w rsbuild.config.ts --watch",
|
||||
"build": "rsbuild build",
|
||||
"build-analyze": "BUNDLE_ANALYZE=true rsbuild build",
|
||||
"check-build": "tsx scripts/genShims.ts && tsc && pnpm build",
|
||||
"start": "run-p dev-proxy dev-rsbuild watch-mesher",
|
||||
"start2": "run-p dev-rsbuild watch-mesher",
|
||||
"start-metrics": "ENABLE_METRICS=true rsbuild dev",
|
||||
"build": "pnpm build-other-workers && rsbuild build",
|
||||
"build-analyze": "BUNDLE_ANALYZE=true rsbuild build && pnpm build-other-workers",
|
||||
"build-single-file": "SINGLE_FILE_BUILD=true rsbuild build",
|
||||
"prepare-project": "tsx scripts/genShims.ts && tsx scripts/makeOptimizedMcData.mjs && tsx scripts/genLargeDataAliases.ts",
|
||||
"check-build": "pnpm prepare-project && tsc && pnpm build",
|
||||
"test:cypress": "cypress run",
|
||||
"test:benchmark": "PERFORMANCE_TEST=true cypress run",
|
||||
"test:cypress:open": "cypress open",
|
||||
"test-unit": "vitest",
|
||||
"test:e2e": "start-test http-get://localhost:8080 test:cypress",
|
||||
"prod-start": "node server.js --prod",
|
||||
"test-mc-server": "tsx cypress/minecraft-server.mjs",
|
||||
"lint": "eslint \"{src,cypress,prismarine-viewer}/**/*.{ts,js,jsx,tsx}\"",
|
||||
"lint": "eslint \"{src,cypress,renderer}/**/*.{ts,js,jsx,tsx}\"",
|
||||
"lint-fix": "pnpm lint --fix",
|
||||
"storybook": "storybook dev -p 6006",
|
||||
"build-storybook": "storybook build && node scripts/build.js moveStorybookFiles",
|
||||
"start-experiments": "vite --config experiments/vite.config.ts --host",
|
||||
"watch-other-workers": "echo NOT IMPLEMENTED",
|
||||
"build-mesher": "node prismarine-viewer/buildMesherWorker.mjs",
|
||||
"build-other-workers": "echo NOT IMPLEMENTED",
|
||||
"build-mesher": "node renderer/buildMesherWorker.mjs",
|
||||
"watch-mesher": "pnpm build-mesher -w",
|
||||
"run-playground": "run-p watch-mesher watch-other-workers playground-server watch-playground",
|
||||
"run-playground": "run-p watch-mesher watch-other-workers watch-playground",
|
||||
"run-all": "run-p start run-playground",
|
||||
"playground-server": "live-server --port=9090 prismarine-viewer/public",
|
||||
"build-playground": "node prismarine-viewer/esbuild.mjs",
|
||||
"watch-playground": "node prismarine-viewer/esbuild.mjs -w"
|
||||
"build-playground": "rsbuild build --config renderer/rsbuild.config.ts",
|
||||
"watch-playground": "rsbuild dev --config renderer/rsbuild.config.ts",
|
||||
"update-git-deps": "tsx scripts/updateGitDeps.ts",
|
||||
"request-data": "tsx scripts/requestData.ts"
|
||||
},
|
||||
"keywords": [
|
||||
"prismarine",
|
||||
"web",
|
||||
"client"
|
||||
],
|
||||
"release": {
|
||||
"attachReleaseFiles": "{self-host.zip,minecraft.html}"
|
||||
},
|
||||
"publish": {
|
||||
"preset": {
|
||||
"publishOnlyIfChanged": true,
|
||||
|
|
@ -43,9 +54,9 @@
|
|||
"dependencies": {
|
||||
"@dimaka/interface": "0.0.3-alpha.0",
|
||||
"@floating-ui/react": "^0.26.1",
|
||||
"@mui/base": "5.0.0-beta.40",
|
||||
"@nxg-org/mineflayer-auto-jump": "^0.7.7",
|
||||
"@nxg-org/mineflayer-tracker": "^1.2.1",
|
||||
"@monaco-editor/react": "^4.7.0",
|
||||
"@nxg-org/mineflayer-auto-jump": "^0.7.18",
|
||||
"@nxg-org/mineflayer-tracker": "1.3.0",
|
||||
"@react-oauth/google": "^0.12.1",
|
||||
"@stylistic/eslint-plugin": "^2.6.1",
|
||||
"@types/gapi": "^0.0.47",
|
||||
|
|
@ -62,18 +73,21 @@
|
|||
"compression": "^1.7.4",
|
||||
"cors": "^2.8.5",
|
||||
"debug": "^4.3.4",
|
||||
"deepslate": "^0.23.5",
|
||||
"diff-match-patch": "^1.0.5",
|
||||
"eruda": "^3.0.1",
|
||||
"esbuild": "^0.19.3",
|
||||
"esbuild-plugin-polyfill-node": "^0.3.0",
|
||||
"express": "^4.18.2",
|
||||
"filesize": "^10.0.12",
|
||||
"flying-squid": "npm:@zardoy/flying-squid@^0.0.36",
|
||||
"flying-squid": "npm:@zardoy/flying-squid@^0.0.104",
|
||||
"framer-motion": "^12.9.2",
|
||||
"fs-extra": "^11.1.1",
|
||||
"google-drive-browserfs": "github:zardoy/browserfs#google-drive",
|
||||
"jszip": "^3.10.1",
|
||||
"lodash-es": "^4.17.21",
|
||||
"minecraft-data": "3.65.0",
|
||||
"mcraft-fun-mineflayer": "^0.1.23",
|
||||
"minecraft-data": "3.98.0",
|
||||
"minecraft-protocol": "github:PrismarineJS/node-minecraft-protocol#master",
|
||||
"mineflayer-item-map-downloader": "github:zardoy/mineflayer-item-map-downloader",
|
||||
"mojangson": "^2.0.4",
|
||||
|
|
@ -92,7 +106,7 @@
|
|||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-select": "^5.8.0",
|
||||
"react-transition-group": "^4.4.5",
|
||||
"react-zoom-pan-pinch": "3.4.4",
|
||||
"remark": "^15.0.1",
|
||||
"sanitize-filename": "^1.6.3",
|
||||
"skinview3d": "^3.0.1",
|
||||
|
|
@ -109,11 +123,11 @@
|
|||
"workbox-build": "^7.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rsbuild/core": "^1.0.1-beta.9",
|
||||
"@rsbuild/plugin-node-polyfill": "^1.0.3",
|
||||
"@rsbuild/plugin-react": "^1.0.1-beta.9",
|
||||
"@rsbuild/plugin-type-check": "^1.0.1-beta.9",
|
||||
"@rsbuild/plugin-typed-css-modules": "^1.0.1",
|
||||
"@rsbuild/core": "1.3.5",
|
||||
"@rsbuild/plugin-node-polyfill": "1.3.0",
|
||||
"@rsbuild/plugin-react": "1.2.0",
|
||||
"@rsbuild/plugin-type-check": "1.2.1",
|
||||
"@rsbuild/plugin-typed-css-modules": "1.0.2",
|
||||
"@storybook/addon-essentials": "^7.4.6",
|
||||
"@storybook/addon-links": "^7.4.6",
|
||||
"@storybook/blocks": "^7.4.6",
|
||||
|
|
@ -121,7 +135,6 @@
|
|||
"@storybook/react-vite": "^7.4.6",
|
||||
"@types/diff-match-patch": "^1.0.36",
|
||||
"@types/lodash-es": "^4.17.9",
|
||||
"@types/react-transition-group": "^4.4.7",
|
||||
"@types/stats.js": "^0.17.1",
|
||||
"@types/three": "0.154.0",
|
||||
"@types/ua-parser-js": "^0.7.39",
|
||||
|
|
@ -131,7 +144,7 @@
|
|||
"browserify-zlib": "^0.2.0",
|
||||
"buffer": "^6.0.3",
|
||||
"constants-browserify": "^1.0.0",
|
||||
"contro-max": "^0.1.8",
|
||||
"contro-max": "^0.1.9",
|
||||
"crypto-browserify": "^3.12.0",
|
||||
"cypress-esbuild-preprocessor": "^1.0.2",
|
||||
"eslint": "^8.50.0",
|
||||
|
|
@ -141,16 +154,16 @@
|
|||
"http-browserify": "^1.7.0",
|
||||
"http-server": "^14.1.1",
|
||||
"https-browserify": "^1.0.0",
|
||||
"mc-assets": "^0.2.12",
|
||||
"mc-assets": "^0.2.62",
|
||||
"minecraft-inventory-gui": "github:zardoy/minecraft-inventory-gui#next",
|
||||
"mineflayer": "github:zardoy/mineflayer",
|
||||
"mineflayer-pathfinder": "^2.4.4",
|
||||
"mineflayer": "github:zardoy/mineflayer#gen-the-master",
|
||||
"mineflayer-mouse": "^0.1.21",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"os-browserify": "^0.3.0",
|
||||
"path-browserify": "^1.0.1",
|
||||
"path-exists-cli": "^2.0.0",
|
||||
"prismarine-viewer": "link:prismarine-viewer",
|
||||
"process": "github:PrismarineJS/node-process",
|
||||
"renderer": "link:renderer",
|
||||
"rimraf": "^5.0.1",
|
||||
"storybook": "^7.4.6",
|
||||
"stream-browserify": "^3.0.0",
|
||||
|
|
@ -163,31 +176,67 @@
|
|||
"optionalDependencies": {
|
||||
"cypress": "^10.11.0",
|
||||
"cypress-plugin-snapshots": "^1.4.4",
|
||||
"sharp": "^0.33.5",
|
||||
"systeminformation": "^5.21.22"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
"iOS >= 14",
|
||||
"Android >= 13",
|
||||
"Chrome >= 103",
|
||||
"not dead",
|
||||
"not ie <= 11",
|
||||
"not op_mini all",
|
||||
"> 0.5%"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
"mineflayer": "github:zardoy/mineflayer#gen-the-master",
|
||||
"@nxg-org/mineflayer-physics-util": "1.8.10",
|
||||
"buffer": "^6.0.3",
|
||||
"@nxg-org/mineflayer-physics-util": "1.5.8",
|
||||
"vec3": "0.1.10",
|
||||
"three": "0.154.0",
|
||||
"diamond-square": "github:zardoy/diamond-square",
|
||||
"prismarine-block": "github:zardoy/prismarine-block#next-era",
|
||||
"prismarine-world": "github:zardoy/prismarine-world#next-era",
|
||||
"minecraft-data": "3.65.0",
|
||||
"minecraft-data": "3.98.0",
|
||||
"prismarine-provider-anvil": "github:zardoy/prismarine-provider-anvil#everything",
|
||||
"prismarine-physics": "github:zardoy/prismarine-physics",
|
||||
"minecraft-protocol": "github:PrismarineJS/node-minecraft-protocol#master",
|
||||
"react": "^18.2.0",
|
||||
"prismarine-chunk": "github:zardoy/prismarine-chunk"
|
||||
"prismarine-chunk": "github:zardoy/prismarine-chunk#master",
|
||||
"prismarine-item": "latest"
|
||||
},
|
||||
"updateConfig": {
|
||||
"ignoreDependencies": []
|
||||
"ignoreDependencies": [
|
||||
"browserfs",
|
||||
"google-drive-browserfs"
|
||||
]
|
||||
},
|
||||
"patchedDependencies": {
|
||||
"minecraft-protocol@1.47.0": "patches/minecraft-protocol@1.47.0.patch",
|
||||
"three@0.154.0": "patches/three@0.154.0.patch",
|
||||
"pixelarticons@1.8.1": "patches/pixelarticons@1.8.1.patch",
|
||||
"mineflayer-item-map-downloader@1.2.0": "patches/mineflayer-item-map-downloader@1.2.0.patch"
|
||||
}
|
||||
"mineflayer-item-map-downloader@1.2.0": "patches/mineflayer-item-map-downloader@1.2.0.patch",
|
||||
"minecraft-protocol": "patches/minecraft-protocol.patch"
|
||||
},
|
||||
"ignoredBuiltDependencies": [
|
||||
"canvas",
|
||||
"core-js",
|
||||
"gl"
|
||||
],
|
||||
"onlyBuiltDependencies": [
|
||||
"sharp",
|
||||
"cypress",
|
||||
"esbuild",
|
||||
"fsevents"
|
||||
],
|
||||
"ignorePatchFailures": false,
|
||||
"allowUnusedPatches": false
|
||||
},
|
||||
"packageManager": "pnpm@9.0.4"
|
||||
"packageManager": "pnpm@10.8.0+sha512.0e82714d1b5b43c74610193cb20734897c1d00de89d0e18420aebc5977fa13d780a9cb05734624e81ebd81cc876cd464794850641c48b9544326b5622ca29971"
|
||||
}
|
||||
|
|
|
|||
138
patches/minecraft-protocol.patch
Normal file
138
patches/minecraft-protocol.patch
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
diff --git a/src/client/chat.js b/src/client/chat.js
|
||||
index 0021870994fc59a82f0ac8aba0a65a8be43ef2f4..a53fceb843105ea2a1d88722b3fc7c3b43cb102a 100644
|
||||
--- a/src/client/chat.js
|
||||
+++ b/src/client/chat.js
|
||||
@@ -116,7 +116,7 @@ module.exports = function (client, options) {
|
||||
for (const player of packet.data) {
|
||||
if (player.chatSession) {
|
||||
client._players[player.uuid] = {
|
||||
- publicKey: crypto.createPublicKey({ key: player.chatSession.publicKey.keyBytes, format: 'der', type: 'spki' }),
|
||||
+ // publicKey: crypto.createPublicKey({ key: player.chatSession.publicKey.keyBytes, format: 'der', type: 'spki' }),
|
||||
publicKeyDER: player.chatSession.publicKey.keyBytes,
|
||||
sessionUuid: player.chatSession.uuid
|
||||
}
|
||||
@@ -126,7 +126,7 @@ module.exports = function (client, options) {
|
||||
|
||||
if (player.crypto) {
|
||||
client._players[player.uuid] = {
|
||||
- publicKey: crypto.createPublicKey({ key: player.crypto.publicKey, format: 'der', type: 'spki' }),
|
||||
+ // publicKey: crypto.createPublicKey({ key: player.crypto.publicKey, format: 'der', type: 'spki' }),
|
||||
publicKeyDER: player.crypto.publicKey,
|
||||
signature: player.crypto.signature,
|
||||
displayName: player.displayName || player.name
|
||||
@@ -196,7 +196,7 @@ module.exports = function (client, options) {
|
||||
if (mcData.supportFeature('useChatSessions')) {
|
||||
const tsDelta = BigInt(Date.now()) - packet.timestamp
|
||||
const expired = !packet.timestamp || tsDelta > messageExpireTime || tsDelta < 0
|
||||
- const verified = !packet.unsignedChatContent && updateAndValidateSession(packet.senderUuid, packet.plainMessage, packet.signature, packet.index, packet.previousMessages, packet.salt, packet.timestamp) && !expired
|
||||
+ const verified = false && !packet.unsignedChatContent && updateAndValidateSession(packet.senderUuid, packet.plainMessage, packet.signature, packet.index, packet.previousMessages, packet.salt, packet.timestamp) && !expired
|
||||
if (verified) client._signatureCache.push(packet.signature)
|
||||
client.emit('playerChat', {
|
||||
globalIndex: packet.globalIndex,
|
||||
@@ -362,7 +362,7 @@ module.exports = function (client, options) {
|
||||
}
|
||||
}
|
||||
|
||||
- client._signedChat = (message, options = {}) => {
|
||||
+ client._signedChat = async (message, options = {}) => {
|
||||
options.timestamp = options.timestamp || BigInt(Date.now())
|
||||
options.salt = options.salt || 1n
|
||||
|
||||
@@ -407,7 +407,7 @@ module.exports = function (client, options) {
|
||||
message,
|
||||
timestamp: options.timestamp,
|
||||
salt: options.salt,
|
||||
- signature: (client.profileKeys && client._session) ? client.signMessage(message, options.timestamp, options.salt, undefined, acknowledgements) : undefined,
|
||||
+ signature: (client.profileKeys && client._session) ? await client.signMessage(message, options.timestamp, options.salt, undefined, acknowledgements) : undefined,
|
||||
offset: client._lastSeenMessages.pending,
|
||||
checksum: computeChatChecksum(client._lastSeenMessages), // 1.21.5+
|
||||
acknowledged
|
||||
@@ -422,7 +422,7 @@ module.exports = function (client, options) {
|
||||
message,
|
||||
timestamp: options.timestamp,
|
||||
salt: options.salt,
|
||||
- signature: client.profileKeys ? client.signMessage(message, options.timestamp, options.salt, options.preview) : Buffer.alloc(0),
|
||||
+ signature: client.profileKeys ? await client.signMessage(message, options.timestamp, options.salt, options.preview) : Buffer.alloc(0),
|
||||
signedPreview: options.didPreview,
|
||||
previousMessages: client._lastSeenMessages.map((e) => ({
|
||||
messageSender: e.sender,
|
||||
diff --git a/src/client/encrypt.js b/src/client/encrypt.js
|
||||
index 63cc2bd9615100bd2fd63dfe14c094aa6b8cd1c9..36df57d1196af9761d920fa285ac48f85410eaef 100644
|
||||
--- a/src/client/encrypt.js
|
||||
+++ b/src/client/encrypt.js
|
||||
@@ -25,7 +25,11 @@ module.exports = function (client, options) {
|
||||
if (packet.serverId !== '-') {
|
||||
debug('This server appears to be an online server and you are providing no password, the authentication will probably fail')
|
||||
}
|
||||
- sendEncryptionKeyResponse()
|
||||
+ client.end('This server appears to be an online server and you are providing no authentication. Try authenticating first.')
|
||||
+ // sendEncryptionKeyResponse()
|
||||
+ // client.once('set_compression', () => {
|
||||
+ // clearTimeout(loginTimeout)
|
||||
+ // })
|
||||
}
|
||||
|
||||
function onJoinServerResponse (err) {
|
||||
diff --git a/src/client/pluginChannels.js b/src/client/pluginChannels.js
|
||||
index 671eb452f31e6b5fcd57d715f1009d010160c65f..7f69f511c8fb97d431ec5125c851b49be8e2ab76 100644
|
||||
--- a/src/client/pluginChannels.js
|
||||
+++ b/src/client/pluginChannels.js
|
||||
@@ -57,7 +57,7 @@ module.exports = function (client, options) {
|
||||
try {
|
||||
packet.data = proto.parsePacketBuffer(channel, packet.data).data
|
||||
} catch (error) {
|
||||
- client.emit('error', error)
|
||||
+ client.emit('error', error, { customPayload: packet })
|
||||
return
|
||||
}
|
||||
}
|
||||
diff --git a/src/client.js b/src/client.js
|
||||
index e369e77d055ba919e8f9da7b8e8b5dc879c74cf4..54bb9e6644388e9b6bd42b3012951875989cdf0c 100644
|
||||
--- a/src/client.js
|
||||
+++ b/src/client.js
|
||||
@@ -111,7 +111,13 @@ class Client extends EventEmitter {
|
||||
this._hasBundlePacket = false
|
||||
}
|
||||
} else {
|
||||
- emitPacket(parsed)
|
||||
+ try {
|
||||
+ emitPacket(parsed)
|
||||
+ } catch (err) {
|
||||
+ console.log('Client incorrectly handled packet ' + parsed.metadata.name)
|
||||
+ console.error(err)
|
||||
+ // todo investigate why it doesn't close the stream even if unhandled there
|
||||
+ }
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -169,7 +175,10 @@ class Client extends EventEmitter {
|
||||
}
|
||||
|
||||
const onFatalError = (err) => {
|
||||
- this.emit('error', err)
|
||||
+ // todo find out what is trying to write after client disconnect
|
||||
+ if(err.code !== 'ECONNABORTED') {
|
||||
+ this.emit('error', err)
|
||||
+ }
|
||||
endSocket()
|
||||
}
|
||||
|
||||
@@ -198,6 +207,10 @@ class Client extends EventEmitter {
|
||||
serializer -> framer -> socket -> splitter -> deserializer */
|
||||
if (this.serializer) {
|
||||
this.serializer.end()
|
||||
+ setTimeout(() => {
|
||||
+ this.socket?.end()
|
||||
+ this.socket?.emit('end')
|
||||
+ }, 2000) // allow the serializer to finish writing
|
||||
} else {
|
||||
if (this.socket) this.socket.end()
|
||||
}
|
||||
@@ -243,6 +256,7 @@ class Client extends EventEmitter {
|
||||
debug('writing packet ' + this.state + '.' + name)
|
||||
debug(params)
|
||||
}
|
||||
+ this.emit('writePacket', name, params)
|
||||
this.serializer.write({ name, params })
|
||||
}
|
||||
|
||||
|
|
@ -1,188 +0,0 @@
|
|||
diff --git a/src/client/autoVersion.js b/src/client/autoVersion.js
|
||||
index c437ecf3a0e4ab5758a48538c714b7e9651bb5da..d9c9895ae8614550aa09ad60a396ac32ffdf1287 100644
|
||||
--- a/src/client/autoVersion.js
|
||||
+++ b/src/client/autoVersion.js
|
||||
@@ -9,7 +9,7 @@ module.exports = function (client, options) {
|
||||
client.wait_connect = true // don't let src/client/setProtocol proceed on socket 'connect' until 'connect_allowed'
|
||||
debug('pinging', options.host)
|
||||
// TODO: use 0xfe ping instead for better compatibility/performance? https://github.com/deathcap/node-minecraft-ping
|
||||
- ping(options, function (err, response) {
|
||||
+ ping(options, async function (err, response) {
|
||||
if (err) { return client.emit('error', err) }
|
||||
debug('ping response', response)
|
||||
// TODO: could also use ping pre-connect to save description, type, max players, etc.
|
||||
@@ -40,6 +40,7 @@ module.exports = function (client, options) {
|
||||
|
||||
// Reinitialize client object with new version TODO: move out of its constructor?
|
||||
client.version = minecraftVersion
|
||||
+ await options.versionSelectedHook?.(client)
|
||||
client.state = states.HANDSHAKING
|
||||
|
||||
// Let other plugins such as Forge/FML (modinfo) respond to the ping response
|
||||
diff --git a/src/client/encrypt.js b/src/client/encrypt.js
|
||||
index b9d21bab9faccd5dbf1975fc423fc55c73e906c5..99ffd76527b410e3a393181beb260108f4c63536 100644
|
||||
--- a/src/client/encrypt.js
|
||||
+++ b/src/client/encrypt.js
|
||||
@@ -25,7 +25,11 @@ module.exports = function (client, options) {
|
||||
if (packet.serverId !== '-') {
|
||||
debug('This server appears to be an online server and you are providing no password, the authentication will probably fail')
|
||||
}
|
||||
- sendEncryptionKeyResponse()
|
||||
+ client.end('This server appears to be an online server and you are providing no authentication. Try authenticating first.')
|
||||
+ // sendEncryptionKeyResponse()
|
||||
+ // client.once('set_compression', () => {
|
||||
+ // clearTimeout(loginTimeout)
|
||||
+ // })
|
||||
}
|
||||
|
||||
function onJoinServerResponse (err) {
|
||||
diff --git a/src/client.js b/src/client.js
|
||||
index c89375e32babbf3559655b1e95f6441b9a30796f..f24cd5dc8fa9a0a4000b184fb3c79590a3ad8b8a 100644
|
||||
--- a/src/client.js
|
||||
+++ b/src/client.js
|
||||
@@ -88,10 +88,12 @@ class Client extends EventEmitter {
|
||||
parsed.metadata.name = parsed.data.name
|
||||
parsed.data = parsed.data.params
|
||||
parsed.metadata.state = state
|
||||
- debug('read packet ' + state + '.' + parsed.metadata.name)
|
||||
- if (debug.enabled) {
|
||||
- const s = JSON.stringify(parsed.data, null, 2)
|
||||
- debug(s && s.length > 10000 ? parsed.data : s)
|
||||
+ if (!globalThis.excludeCommunicationDebugEvents?.includes(parsed.metadata.name)) {
|
||||
+ debug('read packet ' + state + '.' + parsed.metadata.name)
|
||||
+ if (debug.enabled) {
|
||||
+ const s = JSON.stringify(parsed.data, null, 2)
|
||||
+ debug(s && s.length > 10000 ? parsed.data : s)
|
||||
+ }
|
||||
}
|
||||
if (this._hasBundlePacket && parsed.metadata.name === 'bundle_delimiter') {
|
||||
if (this._mcBundle.length) { // End bundle
|
||||
@@ -109,7 +111,13 @@ class Client extends EventEmitter {
|
||||
this._hasBundlePacket = false
|
||||
}
|
||||
} else {
|
||||
- emitPacket(parsed)
|
||||
+ try {
|
||||
+ emitPacket(parsed)
|
||||
+ } catch (err) {
|
||||
+ console.log('Client incorrectly handled packet ' + parsed.metadata.name)
|
||||
+ console.error(err)
|
||||
+ // todo investigate why it doesn't close the stream even if unhandled there
|
||||
+ }
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -166,7 +174,10 @@ class Client extends EventEmitter {
|
||||
}
|
||||
|
||||
const onFatalError = (err) => {
|
||||
- this.emit('error', err)
|
||||
+ // todo find out what is trying to write after client disconnect
|
||||
+ if(err.code !== 'ECONNABORTED') {
|
||||
+ this.emit('error', err)
|
||||
+ }
|
||||
endSocket()
|
||||
}
|
||||
|
||||
@@ -195,6 +206,8 @@ class Client extends EventEmitter {
|
||||
serializer -> framer -> socket -> splitter -> deserializer */
|
||||
if (this.serializer) {
|
||||
this.serializer.end()
|
||||
+ this.socket?.end()
|
||||
+ this.socket?.emit('end')
|
||||
} else {
|
||||
if (this.socket) this.socket.end()
|
||||
}
|
||||
@@ -236,8 +249,11 @@ class Client extends EventEmitter {
|
||||
|
||||
write (name, params) {
|
||||
if (!this.serializer.writable) { return }
|
||||
- debug('writing packet ' + this.state + '.' + name)
|
||||
- debug(params)
|
||||
+ if (!globalThis.excludeCommunicationDebugEvents?.includes(name)) {
|
||||
+ debug(`[${this.state}] from ${this.isServer ? 'server' : 'client'}: ` + name)
|
||||
+ debug(params)
|
||||
+ }
|
||||
+ this.emit('writePacket', name, params)
|
||||
this.serializer.write({ name, params })
|
||||
}
|
||||
|
||||
diff --git a/src/index.d.ts b/src/index.d.ts
|
||||
index 0a5821c32d735e11205a280aa5a503c13533dc14..94a49f661d922478b940d853169b6087e6ec3df5 100644
|
||||
--- a/src/index.d.ts
|
||||
+++ b/src/index.d.ts
|
||||
@@ -121,6 +121,7 @@ declare module 'minecraft-protocol' {
|
||||
sessionServer?: string
|
||||
keepAlive?: boolean
|
||||
closeTimeout?: number
|
||||
+ closeTimeout?: number
|
||||
noPongTimeout?: number
|
||||
checkTimeoutInterval?: number
|
||||
version?: string
|
||||
@@ -141,6 +142,8 @@ declare module 'minecraft-protocol' {
|
||||
disableChatSigning?: boolean
|
||||
/** Pass custom client implementation if needed. */
|
||||
Client?: Client
|
||||
+ /** Can be used to prepare mc data on autoVersion (client.version has selected version) */
|
||||
+ versionSelectedHook?: (client: Client) => Promise<void> | void
|
||||
}
|
||||
|
||||
export class Server extends EventEmitter {
|
||||
diff --git a/src/client/chat.js b/src/client/chat.js
|
||||
index 5cad9954db13d7121ed0a03792c2304156cdf436..ffd7c7d6299ef54854e0923f8d5296bf2a58956b 100644
|
||||
--- a/src/client/chat.js
|
||||
+++ b/src/client/chat.js
|
||||
@@ -111,7 +111,7 @@ module.exports = function (client, options) {
|
||||
for (const player of packet.data) {
|
||||
if (!player.chatSession) continue
|
||||
client._players[player.UUID] = {
|
||||
- publicKey: crypto.createPublicKey({ key: player.chatSession.publicKey.keyBytes, format: 'der', type: 'spki' }),
|
||||
+ // publicKey: crypto.createPublicKey({ key: player.chatSession.publicKey.keyBytes, format: 'der', type: 'spki' }),
|
||||
publicKeyDER: player.chatSession.publicKey.keyBytes,
|
||||
sessionUuid: player.chatSession.uuid
|
||||
}
|
||||
@@ -127,7 +127,7 @@ module.exports = function (client, options) {
|
||||
for (const player of packet.data) {
|
||||
if (player.crypto) {
|
||||
client._players[player.UUID] = {
|
||||
- publicKey: crypto.createPublicKey({ key: player.crypto.publicKey, format: 'der', type: 'spki' }),
|
||||
+ // publicKey: crypto.createPublicKey({ key: player.crypto.publicKey, format: 'der', type: 'spki' }),
|
||||
publicKeyDER: player.crypto.publicKey,
|
||||
signature: player.crypto.signature,
|
||||
displayName: player.displayName || player.name
|
||||
@@ -198,7 +198,7 @@ module.exports = function (client, options) {
|
||||
if (mcData.supportFeature('useChatSessions')) {
|
||||
const tsDelta = BigInt(Date.now()) - packet.timestamp
|
||||
const expired = !packet.timestamp || tsDelta > messageExpireTime || tsDelta < 0
|
||||
- const verified = !packet.unsignedChatContent && updateAndValidateSession(packet.senderUuid, packet.plainMessage, packet.signature, packet.index, packet.previousMessages, packet.salt, packet.timestamp) && !expired
|
||||
+ const verified = false && !packet.unsignedChatContent && updateAndValidateSession(packet.senderUuid, packet.plainMessage, packet.signature, packet.index, packet.previousMessages, packet.salt, packet.timestamp) && !expired
|
||||
if (verified) client._signatureCache.push(packet.signature)
|
||||
client.emit('playerChat', {
|
||||
plainMessage: packet.plainMessage,
|
||||
@@ -363,7 +363,7 @@ module.exports = function (client, options) {
|
||||
}
|
||||
}
|
||||
|
||||
- client._signedChat = (message, options = {}) => {
|
||||
+ client._signedChat = async (message, options = {}) => {
|
||||
options.timestamp = options.timestamp || BigInt(Date.now())
|
||||
options.salt = options.salt || 1n
|
||||
|
||||
@@ -404,7 +404,7 @@ module.exports = function (client, options) {
|
||||
message,
|
||||
timestamp: options.timestamp,
|
||||
salt: options.salt,
|
||||
- signature: (client.profileKeys && client._session) ? client.signMessage(message, options.timestamp, options.salt, undefined, acknowledgements) : undefined,
|
||||
+ signature: (client.profileKeys && client._session) ? await client.signMessage(message, options.timestamp, options.salt, undefined, acknowledgements) : undefined,
|
||||
offset: client._lastSeenMessages.pending,
|
||||
acknowledged
|
||||
})
|
||||
@@ -418,7 +418,7 @@ module.exports = function (client, options) {
|
||||
message,
|
||||
timestamp: options.timestamp,
|
||||
salt: options.salt,
|
||||
- signature: client.profileKeys ? client.signMessage(message, options.timestamp, options.salt, options.preview) : Buffer.alloc(0),
|
||||
+ signature: client.profileKeys ? await client.signMessage(message, options.timestamp, options.salt, options.preview) : Buffer.alloc(0),
|
||||
signedPreview: options.didPreview,
|
||||
previousMessages: client._lastSeenMessages.map((e) => ({
|
||||
messageSender: e.sender,
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
diff --git a/fonts/pixelart-icons-font.css b/fonts/pixelart-icons-font.css
|
||||
index 3b2ebe839370d96bf93ef5ca94a827f07e49378d..103ab4d6b9f3b5c9f41d1407e3cbf4ac392fbf41 100644
|
||||
index 3b2ebe839370d96bf93ef5ca94a827f07e49378d..4f8d76be2ca6e4ddc43c68d0a6f0f69979165ab4 100644
|
||||
--- a/fonts/pixelart-icons-font.css
|
||||
+++ b/fonts/pixelart-icons-font.css
|
||||
@@ -1,16 +1,13 @@
|
||||
|
|
@ -10,10 +10,11 @@ index 3b2ebe839370d96bf93ef5ca94a827f07e49378d..103ab4d6b9f3b5c9f41d1407e3cbf4ac
|
|||
+ src:
|
||||
url("pixelart-icons-font.woff2?t=1711815892278") format("woff2"),
|
||||
url("pixelart-icons-font.woff?t=1711815892278") format("woff"),
|
||||
url('pixelart-icons-font.ttf?t=1711815892278') format('truetype'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+*/
|
||||
- url('pixelart-icons-font.ttf?t=1711815892278') format('truetype'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+*/
|
||||
- url('pixelart-icons-font.svg?t=1711815892278#pixelart-icons-font') format('svg'); /* iOS 4.1- */
|
||||
+ url('pixelart-icons-font.ttf?t=1711815892278') format('truetype'); /* chrome, firefox, opera, Safari, Android, iOS 4.2+*/
|
||||
}
|
||||
|
||||
|
||||
[class^="pixelart-icons-font-"], [class*=" pixelart-icons-font-"] {
|
||||
font-family: 'pixelart-icons-font' !important;
|
||||
- font-size:24px;
|
||||
|
|
|
|||
|
|
@ -1,16 +0,0 @@
|
|||
diff --git a/examples/jsm/webxr/VRButton.js b/examples/jsm/webxr/VRButton.js
|
||||
index 6856a21b17aa45d7922bbf776fd2d7e63c7a9b4e..0925b706f7629bd52f0bb5af469536af8f5fce2c 100644
|
||||
--- a/examples/jsm/webxr/VRButton.js
|
||||
+++ b/examples/jsm/webxr/VRButton.js
|
||||
@@ -62,7 +62,10 @@ class VRButton {
|
||||
// ('local' is always available for immersive sessions and doesn't need to
|
||||
// be requested separately.)
|
||||
|
||||
- const sessionInit = { optionalFeatures: [ 'local-floor', 'bounded-floor', 'hand-tracking', 'layers' ] };
|
||||
+ const sessionInit = {
|
||||
+ optionalFeatures: ['local-floor', 'bounded-floor', 'layers'],
|
||||
+ domOverlay: { root: document.body },
|
||||
+ };
|
||||
navigator.xr.requestSession( 'immersive-vr', sessionInit ).then( onSessionStarted );
|
||||
|
||||
} else {
|
||||
13553
pnpm-lock.yaml
generated
13553
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
|
|
@ -1,4 +1,4 @@
|
|||
packages:
|
||||
- "."
|
||||
- "prismarine-viewer"
|
||||
- "prismarine-viewer/viewer/sign-renderer/"
|
||||
- "renderer"
|
||||
- "renderer/viewer/sign-renderer/"
|
||||
|
|
|
|||
5
prismarine-viewer/README.MD
Normal file
5
prismarine-viewer/README.MD
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
# Prismarine Viewer
|
||||
|
||||
Renamed to `renderer`.
|
||||
|
||||
For more info see [CONTRIBUTING.md](../CONTRIBUTING.md).
|
||||
|
|
@ -1,96 +0,0 @@
|
|||
//@ts-check
|
||||
import * as fs from 'fs'
|
||||
import fsExtra from 'fs-extra'
|
||||
|
||||
import * as esbuild from 'esbuild'
|
||||
import { polyfillNode } from 'esbuild-plugin-polyfill-node'
|
||||
import path, { dirname, join } from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
import childProcess from 'child_process'
|
||||
import supportedVersions from '../src/supportedVersions.mjs'
|
||||
|
||||
const dev = process.argv.includes('-w')
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(new URL(import.meta.url)))
|
||||
|
||||
const mcDataPath = join(__dirname, '../generated/minecraft-data-optimized.json')
|
||||
if (!fs.existsSync(mcDataPath)) {
|
||||
childProcess.execSync('tsx ../scripts/makeOptimizedMcData.mjs', { stdio: 'inherit', cwd: __dirname })
|
||||
}
|
||||
|
||||
fs.copyFileSync(join(__dirname, 'playground.html'), join(__dirname, 'public/index.html'))
|
||||
|
||||
/** @type {import('esbuild').BuildOptions} */
|
||||
const buildOptions = {
|
||||
bundle: true,
|
||||
entryPoints: [join(__dirname, './examples/playground.ts')],
|
||||
// target: ['es2020'],
|
||||
// logLevel: 'debug',
|
||||
logLevel: 'info',
|
||||
platform: 'browser',
|
||||
sourcemap: dev ? 'inline' : false,
|
||||
minify: !dev,
|
||||
outfile: join(__dirname, 'public/playground.js'),
|
||||
mainFields: [
|
||||
'browser', 'module', 'main'
|
||||
],
|
||||
keepNames: true,
|
||||
banner: {
|
||||
js: `globalThis.global = globalThis;globalThis.includedVersions = ${JSON.stringify(supportedVersions)};`,
|
||||
},
|
||||
alias: {
|
||||
events: 'events',
|
||||
buffer: 'buffer',
|
||||
'fs': 'browserfs/dist/shims/fs.js',
|
||||
http: 'http-browserify',
|
||||
stream: 'stream-browserify',
|
||||
net: 'net-browserify',
|
||||
// 'mc-assets': '/Users/vitaly/Documents/mc-assets',
|
||||
},
|
||||
inject: [],
|
||||
metafile: true,
|
||||
loader: {
|
||||
'.png': 'dataurl',
|
||||
'.obj': 'text',
|
||||
},
|
||||
plugins: [
|
||||
{
|
||||
name: 'minecraft-data',
|
||||
setup(build) {
|
||||
build.onLoad({
|
||||
filter: /minecraft-data[\/\\]data.js$/,
|
||||
}, () => {
|
||||
const defaultVersionsObj = {}
|
||||
return {
|
||||
contents: fs.readFileSync(join(__dirname, '../src/shims/minecraftData.ts'), 'utf8'),
|
||||
loader: 'ts',
|
||||
resolveDir: join(__dirname, '../src/shims'),
|
||||
}
|
||||
})
|
||||
build.onEnd((e) => {
|
||||
if (e.errors.length) return
|
||||
fs.writeFileSync(join(__dirname, './public/metafile.json'), JSON.stringify(e.metafile), 'utf8')
|
||||
})
|
||||
}
|
||||
},
|
||||
polyfillNode({
|
||||
polyfills: {
|
||||
fs: false,
|
||||
crypto: false,
|
||||
events: false,
|
||||
http: false,
|
||||
stream: false,
|
||||
buffer: false,
|
||||
perf_hooks: false,
|
||||
net: false,
|
||||
},
|
||||
})
|
||||
],
|
||||
}
|
||||
if (dev) {
|
||||
(await esbuild.context(buildOptions)).watch()
|
||||
} else {
|
||||
await esbuild.build(buildOptions)
|
||||
}
|
||||
|
||||
// await ctx.rebuild()
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { default as rotation } from './rotation'
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
import { Vec3 } from 'vec3'
|
||||
import { ExampleSetupFunction } from './type'
|
||||
|
||||
const setup: ExampleSetupFunction = (world, mcData, mesherConfig, setupParam) => {
|
||||
mesherConfig.debugModelVariant = [3]
|
||||
void world.setBlockStateId(new Vec3(0, 0, 0), mcData.blocksByName.sand.defaultState!)
|
||||
}
|
||||
|
||||
export default setup
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
import { CustomWorld } from 'flying-squid/dist/lib/modules/world'
|
||||
import { IndexedData } from 'minecraft-data'
|
||||
import { MesherConfig } from '../../viewer/lib/mesher/shared'
|
||||
|
||||
type SetupParams = {}
|
||||
export type ExampleSetupFunction = (world: CustomWorld, mcData: IndexedData, mesherConfig: MesherConfig, setupParam: SetupParams) => void
|
||||
|
|
@ -1,498 +0,0 @@
|
|||
import _ from 'lodash'
|
||||
import { Vec3 } from 'vec3'
|
||||
import BlockLoader from 'prismarine-block'
|
||||
import ChunkLoader from 'prismarine-chunk'
|
||||
import WorldLoader from 'prismarine-world'
|
||||
import * as THREE from 'three'
|
||||
import { GUI } from 'lil-gui'
|
||||
import JSZip from 'jszip'
|
||||
import blockstatesModels from 'mc-assets/dist/blockStatesModels.json'
|
||||
|
||||
//@ts-expect-error
|
||||
import { OrbitControls } from 'three/addons/controls/OrbitControls.js'
|
||||
import { IndexedData } from 'minecraft-data'
|
||||
import { loadScript } from '../viewer/lib/utils'
|
||||
import { TWEEN_DURATION } from '../viewer/lib/entities'
|
||||
import { EntityMesh } from '../viewer/lib/entity/EntityMesh'
|
||||
import { WorldDataEmitter, Viewer } from '../viewer'
|
||||
import '../../src/getCollisionShapes'
|
||||
import { toMajorVersion } from '../../src/utils'
|
||||
|
||||
window.THREE = THREE
|
||||
|
||||
const gui = new GUI()
|
||||
|
||||
// initial values
|
||||
const params = {
|
||||
skipQs: '',
|
||||
version: globalThis.includedVersions.sort((a, b) => {
|
||||
const s = (x) => {
|
||||
const parts = x.split('.')
|
||||
return +parts[0] + (+parts[1])
|
||||
}
|
||||
return s(a) - s(b)
|
||||
}).at(-1),
|
||||
block: '',
|
||||
metadata: 0,
|
||||
supportBlock: false,
|
||||
entity: '',
|
||||
removeEntity () {
|
||||
this.entity = ''
|
||||
},
|
||||
entityRotate: false,
|
||||
camera: '',
|
||||
playSound () { },
|
||||
blockIsomorphicRenderBundle () { },
|
||||
modelVariant: 0
|
||||
}
|
||||
|
||||
const qs = new URLSearchParams(window.location.search)
|
||||
for (const [key, value] of qs.entries()) {
|
||||
const parsed = /^-?\d+$/.test(value) ? Number(value) : value === 'true' ? true : value === 'false' ? false : value
|
||||
params[key] = parsed
|
||||
}
|
||||
const setQs = () => {
|
||||
const newQs = new URLSearchParams()
|
||||
for (const [key, value] of Object.entries(params)) {
|
||||
if (!value || typeof value === 'function' || params.skipQs.includes(key)) continue
|
||||
newQs.set(key, value)
|
||||
}
|
||||
window.history.replaceState({}, '', `${window.location.pathname}?${newQs.toString()}`)
|
||||
}
|
||||
|
||||
let ignoreResize = false
|
||||
|
||||
async function main () {
|
||||
let continuousRender = false
|
||||
|
||||
const { version } = params
|
||||
await window._LOAD_MC_DATA()
|
||||
// temporary solution until web worker is here, cache data for faster reloads
|
||||
// const globalMcData = window['mcData']
|
||||
// if (!globalMcData['version']) {
|
||||
// const major = toMajorVersion(version)
|
||||
// const sessionKey = `mcData-${major}`
|
||||
// if (sessionStorage[sessionKey]) {
|
||||
// Object.assign(globalMcData, JSON.parse(sessionStorage[sessionKey]))
|
||||
// } else {
|
||||
// if (sessionStorage.length > 1) sessionStorage.clear()
|
||||
// try {
|
||||
// sessionStorage[sessionKey] = JSON.stringify(Object.fromEntries(Object.entries(globalMcData).filter(([ver]) => ver.startsWith(major))))
|
||||
// } catch { }
|
||||
// }
|
||||
// }
|
||||
|
||||
const mcData: IndexedData = require('minecraft-data')(version)
|
||||
window['loadedData'] = mcData
|
||||
|
||||
gui.add(params, 'version', globalThis.includedVersions)
|
||||
gui.add(params, 'block', mcData.blocksArray.map(b => b.name).sort((a, b) => a.localeCompare(b)))
|
||||
const metadataGui = gui.add(params, 'metadata')
|
||||
gui.add(params, 'modelVariant')
|
||||
gui.add(params, 'supportBlock')
|
||||
gui.add(params, 'entity', mcData.entitiesArray.map(b => b.name).sort((a, b) => a.localeCompare(b))).listen()
|
||||
gui.add(params, 'removeEntity')
|
||||
gui.add(params, 'entityRotate')
|
||||
gui.add(params, 'skipQs')
|
||||
gui.add(params, 'playSound')
|
||||
gui.add(params, 'blockIsomorphicRenderBundle')
|
||||
gui.open(false)
|
||||
let metadataFolder = gui.addFolder('metadata')
|
||||
// let entityRotationFolder = gui.addFolder('entity metadata')
|
||||
|
||||
const Chunk = ChunkLoader(version)
|
||||
const Block = BlockLoader(version)
|
||||
// const data = await fetch('smallhouse1.schem').then(r => r.arrayBuffer())
|
||||
// const schem = await Schematic.read(Buffer.from(data), version)
|
||||
|
||||
const viewDistance = 0
|
||||
const targetPos = new Vec3(2, 90, 2)
|
||||
|
||||
const World = WorldLoader(version)
|
||||
|
||||
// const diamondSquare = require('diamond-square')({ version, seed: Math.floor(Math.random() * Math.pow(2, 31)) })
|
||||
|
||||
//@ts-expect-error
|
||||
const chunk1 = new Chunk()
|
||||
//@ts-expect-error
|
||||
const chunk2 = new Chunk()
|
||||
chunk1.setBlockStateId(targetPos, 34)
|
||||
chunk2.setBlockStateId(targetPos.offset(1, 0, 0), 34)
|
||||
//@ts-expect-error
|
||||
const world = new World((chunkX, chunkZ) => {
|
||||
// if (chunkX === 0 && chunkZ === 0) return chunk1
|
||||
// if (chunkX === 1 && chunkZ === 0) return chunk2
|
||||
//@ts-expect-error
|
||||
const chunk = new Chunk()
|
||||
return chunk
|
||||
})
|
||||
|
||||
// await schem.paste(world, new Vec3(0, 60, 0))
|
||||
|
||||
const worldView = new WorldDataEmitter(world, viewDistance, targetPos)
|
||||
|
||||
// Create three.js context, add to page
|
||||
const renderer = new THREE.WebGLRenderer({ alpha: true, ...localStorage['renderer'] })
|
||||
renderer.setPixelRatio(window.devicePixelRatio || 1)
|
||||
renderer.setSize(window.innerWidth, window.innerHeight)
|
||||
document.body.appendChild(renderer.domElement)
|
||||
|
||||
// Create viewer
|
||||
const viewer = new Viewer(renderer, { numWorkers: 1, showChunkBorders: false, })
|
||||
viewer.world.blockstatesModels = blockstatesModels
|
||||
viewer.entities.setDebugMode('basic')
|
||||
viewer.setVersion(version)
|
||||
viewer.entities.onSkinUpdate = () => {
|
||||
viewer.render()
|
||||
}
|
||||
viewer.world.mesherConfig.enableLighting = false
|
||||
|
||||
viewer.listen(worldView)
|
||||
// Load chunks
|
||||
await worldView.init(targetPos)
|
||||
window['worldView'] = worldView
|
||||
window['viewer'] = viewer
|
||||
|
||||
params.blockIsomorphicRenderBundle = () => {
|
||||
const canvas = renderer.domElement
|
||||
const onlyCurrent = !confirm('Ok - render all blocks, Cancel - render only current one')
|
||||
const sizeRaw = prompt('Size', '512')
|
||||
if (!sizeRaw) return
|
||||
const size = parseInt(sizeRaw, 10)
|
||||
// const size = 512
|
||||
|
||||
ignoreResize = true
|
||||
canvas.width = size
|
||||
canvas.height = size
|
||||
renderer.setSize(size, size)
|
||||
|
||||
//@ts-expect-error
|
||||
viewer.camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 10)
|
||||
viewer.scene.background = null
|
||||
|
||||
const rad = THREE.MathUtils.degToRad(-120)
|
||||
viewer.directionalLight.position.set(
|
||||
Math.cos(rad),
|
||||
Math.sin(rad),
|
||||
0.2
|
||||
).normalize()
|
||||
viewer.directionalLight.intensity = 1
|
||||
|
||||
const cameraPos = targetPos.offset(2, 2, 2)
|
||||
const pitch = THREE.MathUtils.degToRad(-30)
|
||||
const yaw = THREE.MathUtils.degToRad(45)
|
||||
viewer.camera.rotation.set(pitch, yaw, 0, 'ZYX')
|
||||
// viewer.camera.lookAt(center.x + 0.5, center.y + 0.5, center.z + 0.5)
|
||||
viewer.camera.position.set(cameraPos.x + 1, cameraPos.y + 0.5, cameraPos.z + 1)
|
||||
|
||||
const allBlocks = mcData.blocksArray.map(b => b.name)
|
||||
// const allBlocks = ['stone', 'warped_slab']
|
||||
|
||||
let blockCount = 1
|
||||
let blockName = allBlocks[0]
|
||||
|
||||
const updateBlock = () => {
|
||||
// viewer.setBlockStateId(targetPos, mcData.blocksByName[blockName].minStateId)
|
||||
params.block = blockName
|
||||
// todo cleanup (introduce getDefaultState)
|
||||
onUpdate.block()
|
||||
applyChanges(false, true)
|
||||
}
|
||||
void viewer.waitForChunksToRender().then(async () => {
|
||||
// wait for next macro task
|
||||
await new Promise(resolve => {
|
||||
setTimeout(resolve, 0)
|
||||
})
|
||||
if (onlyCurrent) {
|
||||
viewer.render()
|
||||
onWorldUpdate()
|
||||
} else {
|
||||
// will be called on every render update
|
||||
viewer.world.renderUpdateEmitter.addListener('update', onWorldUpdate)
|
||||
updateBlock()
|
||||
}
|
||||
})
|
||||
|
||||
const zip = new JSZip()
|
||||
zip.file('description.txt', 'Generated with prismarine-viewer')
|
||||
|
||||
const end = async () => {
|
||||
// download zip file
|
||||
|
||||
const a = document.createElement('a')
|
||||
const blob = await zip.generateAsync({ type: 'blob' })
|
||||
const dataUrlZip = URL.createObjectURL(blob)
|
||||
a.href = dataUrlZip
|
||||
a.download = 'blocks_render.zip'
|
||||
a.click()
|
||||
URL.revokeObjectURL(dataUrlZip)
|
||||
console.log('end')
|
||||
|
||||
viewer.world.renderUpdateEmitter.removeListener('update', onWorldUpdate)
|
||||
}
|
||||
|
||||
async function onWorldUpdate () {
|
||||
// await new Promise(resolve => {
|
||||
// setTimeout(resolve, 50)
|
||||
// })
|
||||
const dataUrl = canvas.toDataURL('image/png')
|
||||
|
||||
zip.file(`${blockName}.png`, dataUrl.split(',')[1], { base64: true })
|
||||
|
||||
if (onlyCurrent) {
|
||||
end()
|
||||
} else {
|
||||
nextBlock()
|
||||
}
|
||||
}
|
||||
const nextBlock = async () => {
|
||||
blockName = allBlocks[blockCount++]
|
||||
console.log(allBlocks.length, '/', blockCount, blockName)
|
||||
if (blockCount % 5 === 0) {
|
||||
await new Promise(resolve => {
|
||||
setTimeout(resolve, 100)
|
||||
})
|
||||
}
|
||||
if (blockName) {
|
||||
updateBlock()
|
||||
} else {
|
||||
end()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const controls = new OrbitControls(viewer.camera, renderer.domElement)
|
||||
controls.target.set(targetPos.x + 0.5, targetPos.y + 0.5, targetPos.z + 0.5)
|
||||
|
||||
const cameraPos = targetPos.offset(2, 2, 2)
|
||||
const pitch = THREE.MathUtils.degToRad(-45)
|
||||
const yaw = THREE.MathUtils.degToRad(45)
|
||||
viewer.camera.rotation.set(pitch, yaw, 0, 'ZYX')
|
||||
viewer.camera.lookAt(targetPos.x + 0.5, targetPos.y + 0.5, targetPos.z + 0.5)
|
||||
viewer.camera.position.set(cameraPos.x + 0.5, cameraPos.y + 0.5, cameraPos.z + 0.5)
|
||||
controls.update()
|
||||
|
||||
let blockProps = {}
|
||||
const entityOverrides = {}
|
||||
const getBlock = () => {
|
||||
return mcData.blocksByName[params.block || 'air']
|
||||
}
|
||||
|
||||
const entityUpdateShared = () => {
|
||||
viewer.entities.clear()
|
||||
if (!params.entity) return
|
||||
worldView.emit('entity', {
|
||||
id: 'id', name: params.entity, pos: targetPos.offset(0.5, 1, 0.5), width: 1, height: 1, username: localStorage.testUsername, yaw: Math.PI, pitch: 0
|
||||
})
|
||||
const enableSkeletonDebug = (obj) => {
|
||||
const { children, isSkeletonHelper } = obj
|
||||
if (!Array.isArray(children)) return
|
||||
if (isSkeletonHelper) {
|
||||
obj.visible = true
|
||||
return
|
||||
}
|
||||
for (const child of children) {
|
||||
if (typeof child === 'object') enableSkeletonDebug(child)
|
||||
}
|
||||
}
|
||||
enableSkeletonDebug(viewer.entities.entities['id'])
|
||||
setTimeout(() => {
|
||||
viewer.render()
|
||||
}, TWEEN_DURATION)
|
||||
}
|
||||
|
||||
const onUpdate = {
|
||||
version (initialUpdate) {
|
||||
// if (initialUpdate) return
|
||||
// viewer.world.texturesVersion = params.version
|
||||
// viewer.world.updateTexturesData()
|
||||
// todo warning
|
||||
},
|
||||
block () {
|
||||
blockProps = {}
|
||||
metadataFolder.destroy()
|
||||
const block = mcData.blocksByName[params.block]
|
||||
if (!block) return
|
||||
console.log('block', block.name)
|
||||
const props = new Block(block.id, 0, 0).getProperties()
|
||||
//@ts-expect-error
|
||||
const { states } = mcData.blocksByStateId[getBlock()?.minStateId] ?? {}
|
||||
metadataFolder = gui.addFolder('metadata')
|
||||
if (states) {
|
||||
for (const state of states) {
|
||||
let defaultValue: string | number | boolean
|
||||
if (state.values) { // int, enum
|
||||
defaultValue = state.values[0]
|
||||
} else {
|
||||
switch (state.type) {
|
||||
case 'bool':
|
||||
defaultValue = false
|
||||
break
|
||||
case 'int':
|
||||
defaultValue = 0
|
||||
break
|
||||
case 'direction':
|
||||
defaultValue = 'north'
|
||||
break
|
||||
|
||||
default:
|
||||
continue
|
||||
}
|
||||
}
|
||||
blockProps[state.name] = defaultValue
|
||||
if (state.values) {
|
||||
metadataFolder.add(blockProps, state.name, state.values)
|
||||
} else {
|
||||
metadataFolder.add(blockProps, state.name)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (const [name, value] of Object.entries(props)) {
|
||||
blockProps[name] = value
|
||||
metadataFolder.add(blockProps, name)
|
||||
}
|
||||
}
|
||||
console.log('props', blockProps)
|
||||
metadataFolder.open()
|
||||
},
|
||||
entity () {
|
||||
continuousRender = params.entity === 'player'
|
||||
entityUpdateShared()
|
||||
if (!params.entity) return
|
||||
if (params.entity === 'player') {
|
||||
viewer.entities.updatePlayerSkin('id', viewer.entities.entities.id.username, true, true)
|
||||
viewer.entities.playAnimation('id', 'running')
|
||||
}
|
||||
// let prev = false
|
||||
// setInterval(() => {
|
||||
// viewer.entities.playAnimation('id', prev ? 'running' : 'idle')
|
||||
// prev = !prev
|
||||
// }, 1000)
|
||||
|
||||
EntityMesh.getStaticData(params.entity)
|
||||
// entityRotationFolder.destroy()
|
||||
// entityRotationFolder = gui.addFolder('entity metadata')
|
||||
// entityRotationFolder.add(params, 'entityRotate')
|
||||
// entityRotationFolder.open()
|
||||
},
|
||||
supportBlock () {
|
||||
viewer.setBlockStateId(targetPos.offset(0, -1, 0), params.supportBlock ? 1 : 0)
|
||||
},
|
||||
modelVariant () {
|
||||
viewer.world.mesherConfig.debugModelVariant = params.modelVariant === 0 ? undefined : [params.modelVariant]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const applyChanges = (metadataUpdate = false, skipQs = false) => {
|
||||
const blockId = getBlock()?.id
|
||||
let block: BlockLoader.Block
|
||||
if (metadataUpdate) {
|
||||
block = new Block(blockId, 0, params.metadata)
|
||||
Object.assign(blockProps, block.getProperties())
|
||||
for (const _child of metadataFolder.children) {
|
||||
const child = _child as import('lil-gui').Controller
|
||||
child.updateDisplay()
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
block = Block.fromProperties(blockId ?? -1, blockProps, 0)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
block = Block.fromStateId(0, 0)
|
||||
}
|
||||
}
|
||||
|
||||
//@ts-expect-error
|
||||
viewer.setBlockStateId(targetPos, block.stateId)
|
||||
console.log('up stateId', block.stateId)
|
||||
params.metadata = block.metadata
|
||||
metadataGui.updateDisplay()
|
||||
if (!skipQs) {
|
||||
setQs()
|
||||
}
|
||||
}
|
||||
gui.onChange(({ property, object }) => {
|
||||
if (object === params) {
|
||||
if (property === 'camera') return
|
||||
onUpdate[property]?.()
|
||||
applyChanges(property === 'metadata')
|
||||
} else {
|
||||
applyChanges()
|
||||
}
|
||||
})
|
||||
void viewer.waitForChunksToRender().then(async () => {
|
||||
// TODO!
|
||||
await new Promise(resolve => {
|
||||
setTimeout(resolve, 50)
|
||||
})
|
||||
for (const update of Object.values(onUpdate)) {
|
||||
update(true)
|
||||
}
|
||||
applyChanges()
|
||||
gui.openAnimated()
|
||||
})
|
||||
|
||||
const animate = () => {
|
||||
// if (controls) controls.update()
|
||||
// worldView.updatePosition(controls.target)
|
||||
viewer.render()
|
||||
// window.requestAnimationFrame(animate)
|
||||
}
|
||||
viewer.world.renderUpdateEmitter.addListener('update', () => {
|
||||
animate()
|
||||
})
|
||||
animate()
|
||||
|
||||
// #region camera rotation param
|
||||
if (params.camera) {
|
||||
const [x, y] = params.camera.split(',')
|
||||
viewer.camera.rotation.set(parseFloat(x), parseFloat(y), 0, 'ZYX')
|
||||
controls.update()
|
||||
console.log(viewer.camera.rotation.x, parseFloat(x))
|
||||
}
|
||||
const throttledCamQsUpdate = _.throttle(() => {
|
||||
const { camera } = viewer
|
||||
// params.camera = `${camera.rotation.x.toFixed(2)},${camera.rotation.y.toFixed(2)}`
|
||||
setQs()
|
||||
}, 200)
|
||||
controls.addEventListener('change', () => {
|
||||
throttledCamQsUpdate()
|
||||
animate()
|
||||
})
|
||||
// #endregion
|
||||
|
||||
const continuousUpdate = () => {
|
||||
if (continuousRender) {
|
||||
animate()
|
||||
}
|
||||
requestAnimationFrame(continuousUpdate)
|
||||
}
|
||||
continuousUpdate()
|
||||
|
||||
window.onresize = () => {
|
||||
if (ignoreResize) return
|
||||
// const vec3 = new THREE.Vector3()
|
||||
// vec3.set(-1, -1, -1).unproject(viewer.camera)
|
||||
// console.log(vec3)
|
||||
// box.position.set(vec3.x, vec3.y, vec3.z-1)
|
||||
|
||||
const { camera } = viewer
|
||||
viewer.camera.aspect = window.innerWidth / window.innerHeight
|
||||
viewer.camera.updateProjectionMatrix()
|
||||
renderer.setSize(window.innerWidth, window.innerHeight)
|
||||
|
||||
animate()
|
||||
}
|
||||
window.dispatchEvent(new Event('resize'))
|
||||
|
||||
params.playSound = () => {
|
||||
viewer.playSound(targetPos, 'button_click.mp3')
|
||||
}
|
||||
addEventListener('keydown', (e) => {
|
||||
if (e.code === 'KeyE') {
|
||||
params.playSound()
|
||||
}
|
||||
}, { capture: true })
|
||||
}
|
||||
main()
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
module.exports = {
|
||||
Viewer: require('./lib/viewer').Viewer,
|
||||
WorldDataEmitter: require('./lib/worldDataEmitter').WorldDataEmitter,
|
||||
Entity: require('./lib/entity/EntityMesh'),
|
||||
getBufferFromStream: require('./lib/simpleUtils').getBufferFromStream
|
||||
}
|
||||
|
|
@ -1,499 +0,0 @@
|
|||
//@ts-check
|
||||
import EventEmitter from 'events'
|
||||
import nbt from 'prismarine-nbt'
|
||||
import * as TWEEN from '@tweenjs/tween.js'
|
||||
import * as THREE from 'three'
|
||||
import { PlayerObject, PlayerAnimation } from 'skinview3d'
|
||||
import { loadSkinToCanvas, loadEarsToCanvasFromSkin, inferModelType, loadCapeToCanvas, loadImage } from 'skinview-utils'
|
||||
// todo replace with url
|
||||
import stevePng from 'mc-assets/dist/other-textures/latest/entity/player/wide/steve.png'
|
||||
import { NameTagObject } from 'skinview3d/libs/nametag'
|
||||
import { flat, fromFormattedString } from '@xmcl/text-component'
|
||||
import mojangson from 'mojangson'
|
||||
import * as Entity from './entity/EntityMesh'
|
||||
import { WalkingGeneralSwing } from './entity/animations'
|
||||
import externalTexturesJson from './entity/externalTextures.json'
|
||||
import { disposeObject } from './threeJsUtils'
|
||||
|
||||
export const TWEEN_DURATION = 120
|
||||
|
||||
/**
|
||||
* @param {string} username
|
||||
*/
|
||||
function getUsernameTexture(username, { fontFamily = 'sans-serif' }) {
|
||||
const canvas = document.createElement('canvas')
|
||||
const ctx = canvas.getContext('2d')
|
||||
if (!ctx) throw new Error('Could not get 2d context')
|
||||
|
||||
const fontSize = 50
|
||||
const padding = 5
|
||||
ctx.font = `${fontSize}px ${fontFamily}`
|
||||
|
||||
const textWidth = ctx.measureText(username).width + padding * 2
|
||||
|
||||
canvas.width = textWidth
|
||||
canvas.height = fontSize + padding * 2
|
||||
|
||||
ctx.fillStyle = 'rgba(0, 0, 0, 0.3)'
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height)
|
||||
|
||||
ctx.font = `${fontSize}px ${fontFamily}`
|
||||
ctx.fillStyle = 'white'
|
||||
ctx.fillText(username, padding, fontSize)
|
||||
|
||||
return canvas
|
||||
}
|
||||
|
||||
const addNametag = (entity, options, mesh) => {
|
||||
if (entity.username !== undefined) {
|
||||
if (mesh.children.some(c => c.name === 'nametag')) return // todo update
|
||||
const canvas = getUsernameTexture(entity.username, options)
|
||||
const tex = new THREE.Texture(canvas)
|
||||
tex.needsUpdate = true
|
||||
const spriteMat = new THREE.SpriteMaterial({ map: tex })
|
||||
const sprite = new THREE.Sprite(spriteMat)
|
||||
sprite.renderOrder = 1000
|
||||
sprite.scale.set(canvas.width * 0.005, canvas.height * 0.005, 1)
|
||||
sprite.position.y += entity.height + 0.6
|
||||
sprite.name = 'nametag'
|
||||
|
||||
mesh.add(sprite)
|
||||
}
|
||||
}
|
||||
|
||||
// todo cleanup
|
||||
const nametags = {}
|
||||
|
||||
function getEntityMesh(entity, scene, options, overrides) {
|
||||
if (entity.name) {
|
||||
try {
|
||||
// https://github.com/PrismarineJS/prismarine-viewer/pull/410
|
||||
const entityName = entity.name.toLowerCase()
|
||||
const e = new Entity.EntityMesh('1.16.4', entityName, scene, overrides)
|
||||
|
||||
if (e.mesh) {
|
||||
addNametag(entity, options, e.mesh)
|
||||
return e.mesh
|
||||
}
|
||||
} catch (err) {
|
||||
reportError?.(err)
|
||||
}
|
||||
}
|
||||
|
||||
const geometry = new THREE.BoxGeometry(entity.width, entity.height, entity.width)
|
||||
geometry.translate(0, entity.height / 2, 0)
|
||||
const material = new THREE.MeshBasicMaterial({ color: 0xff_00_ff })
|
||||
const cube = new THREE.Mesh(geometry, material)
|
||||
const nametagCount = (nametags[entity.name] = (nametags[entity.name] || 0) + 1)
|
||||
if (nametagCount < 6) {
|
||||
addNametag({
|
||||
username: entity.name,
|
||||
height: entity.height,
|
||||
}, options, cube)
|
||||
}
|
||||
return cube
|
||||
}
|
||||
|
||||
export class Entities extends EventEmitter {
|
||||
constructor(scene) {
|
||||
super()
|
||||
/** @type {THREE.Scene} */
|
||||
this.scene = scene
|
||||
this.entities = {}
|
||||
this.entitiesOptions = {}
|
||||
this.debugMode = 'none'
|
||||
this.onSkinUpdate = () => { }
|
||||
this.clock = new THREE.Clock()
|
||||
this.rendering = true
|
||||
/** @type {THREE.Texture | null} */
|
||||
this.itemsTexture = null
|
||||
this.getItemUv = undefined
|
||||
}
|
||||
|
||||
clear() {
|
||||
for (const mesh of Object.values(this.entities)) {
|
||||
this.scene.remove(mesh)
|
||||
disposeObject(mesh)
|
||||
}
|
||||
this.entities = {}
|
||||
}
|
||||
|
||||
setDebugMode(mode, /** @type {THREE.Object3D?} */entity = null) {
|
||||
this.debugMode = mode
|
||||
for (const mesh of entity ? [entity] : Object.values(this.entities)) {
|
||||
const boxHelper = mesh.children.find(c => c.name === 'debug')
|
||||
boxHelper.visible = false
|
||||
if (this.debugMode === 'basic') {
|
||||
boxHelper.visible = true
|
||||
}
|
||||
// todo advanced
|
||||
}
|
||||
}
|
||||
|
||||
setRendering(rendering, /** @type {THREE.Object3D?} */entity = null) {
|
||||
this.rendering = rendering
|
||||
for (const ent of entity ? [entity] : Object.values(this.entities)) {
|
||||
if (rendering) {
|
||||
if (!this.scene.children.includes(ent)) this.scene.add(ent)
|
||||
} else {
|
||||
this.scene.remove(ent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const dt = this.clock.getDelta()
|
||||
for (const entityId of Object.keys(this.entities)) {
|
||||
const playerObject = this.getPlayerObject(entityId)
|
||||
if (playerObject?.animation) {
|
||||
playerObject.animation.update(playerObject, dt)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getPlayerObject(entityId) {
|
||||
/** @type {(PlayerObject & { animation?: PlayerAnimation }) | undefined} */
|
||||
const playerObject = this.entities[entityId]?.playerObject
|
||||
return playerObject
|
||||
}
|
||||
|
||||
// fixme workaround
|
||||
defaultSteveTexture
|
||||
|
||||
// true means use default skin url
|
||||
updatePlayerSkin(entityId, username, /** @type {string | true} */skinUrl, /** @type {string | true | undefined} */capeUrl = undefined) {
|
||||
let playerObject = this.getPlayerObject(entityId)
|
||||
if (!playerObject) return
|
||||
// const username = this.entities[entityId].username
|
||||
// or https://mulv.vercel.app/
|
||||
if (skinUrl === true) {
|
||||
skinUrl = `https://mulv.tycrek.dev/api/lookup?username=${username}&type=skin`
|
||||
if (!username) return
|
||||
}
|
||||
loadImage(skinUrl).then(image => {
|
||||
playerObject = this.getPlayerObject(entityId)
|
||||
if (!playerObject) return
|
||||
/** @type {THREE.CanvasTexture} */
|
||||
let skinTexture
|
||||
if (skinUrl === stevePng && this.defaultSteveTexture) {
|
||||
skinTexture = this.defaultSteveTexture
|
||||
} else {
|
||||
const skinCanvas = document.createElement('canvas')
|
||||
loadSkinToCanvas(skinCanvas, image)
|
||||
skinTexture = new THREE.CanvasTexture(skinCanvas)
|
||||
if (skinUrl === stevePng) {
|
||||
this.defaultSteveTexture = skinTexture
|
||||
}
|
||||
}
|
||||
skinTexture.magFilter = THREE.NearestFilter
|
||||
skinTexture.minFilter = THREE.NearestFilter
|
||||
skinTexture.needsUpdate = true
|
||||
//@ts-expect-error
|
||||
playerObject.skin.map = skinTexture
|
||||
playerObject.skin.modelType = inferModelType(skinTexture.image)
|
||||
|
||||
const earsCanvas = document.createElement('canvas')
|
||||
loadEarsToCanvasFromSkin(earsCanvas, image)
|
||||
if (isCanvasBlank(earsCanvas)) {
|
||||
playerObject.ears.map = null
|
||||
playerObject.ears.visible = false
|
||||
} else {
|
||||
const earsTexture = new THREE.CanvasTexture(earsCanvas)
|
||||
earsTexture.magFilter = THREE.NearestFilter
|
||||
earsTexture.minFilter = THREE.NearestFilter
|
||||
earsTexture.needsUpdate = true
|
||||
//@ts-expect-error
|
||||
playerObject.ears.map = earsTexture
|
||||
playerObject.ears.visible = true
|
||||
}
|
||||
this.onSkinUpdate?.()
|
||||
if (capeUrl) {
|
||||
if (capeUrl === true) capeUrl = `https://mulv.tycrek.dev/api/lookup?username=${username}&type=cape`
|
||||
loadImage(capeUrl).then(capeImage => {
|
||||
playerObject = this.getPlayerObject(entityId)
|
||||
if (!playerObject) return
|
||||
const capeCanvas = document.createElement('canvas')
|
||||
loadCapeToCanvas(capeCanvas, capeImage)
|
||||
|
||||
const capeTexture = new THREE.CanvasTexture(capeCanvas)
|
||||
capeTexture.magFilter = THREE.NearestFilter
|
||||
capeTexture.minFilter = THREE.NearestFilter
|
||||
capeTexture.needsUpdate = true
|
||||
//@ts-expect-error
|
||||
playerObject.cape.map = capeTexture
|
||||
playerObject.cape.visible = true
|
||||
//@ts-expect-error
|
||||
playerObject.elytra.map = capeTexture
|
||||
this.onSkinUpdate?.()
|
||||
|
||||
if (!playerObject.backEquipment) {
|
||||
playerObject.backEquipment = 'cape'
|
||||
}
|
||||
}, () => { })
|
||||
}
|
||||
}, () => { })
|
||||
|
||||
|
||||
playerObject.cape.visible = false
|
||||
if (!capeUrl) {
|
||||
playerObject.backEquipment = null
|
||||
playerObject.elytra.map = null
|
||||
if (playerObject.cape.map) {
|
||||
playerObject.cape.map.dispose()
|
||||
}
|
||||
playerObject.cape.map = null
|
||||
}
|
||||
|
||||
function isCanvasBlank(canvas) {
|
||||
return !canvas.getContext('2d')
|
||||
.getImageData(0, 0, canvas.width, canvas.height).data
|
||||
.some(channel => channel !== 0)
|
||||
}
|
||||
}
|
||||
|
||||
playAnimation(entityPlayerId, /** @type {'walking' | 'running' | 'oneSwing' | 'idle'} */animation) {
|
||||
const playerObject = this.getPlayerObject(entityPlayerId)
|
||||
if (!playerObject) return
|
||||
|
||||
if (animation === 'oneSwing') {
|
||||
if (!(playerObject.animation instanceof WalkingGeneralSwing)) throw new Error('Expected WalkingGeneralSwing')
|
||||
playerObject.animation.swingArm()
|
||||
return
|
||||
}
|
||||
|
||||
if (playerObject.animation instanceof WalkingGeneralSwing) {
|
||||
playerObject.animation.switchAnimationCallback = () => {
|
||||
if (!(playerObject.animation instanceof WalkingGeneralSwing)) throw new Error('Expected WalkingGeneralSwing')
|
||||
playerObject.animation.isMoving = animation !== 'idle'
|
||||
playerObject.animation.isRunning = animation === 'running'
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
parseEntityLabel(jsonLike) {
|
||||
if (!jsonLike) return
|
||||
try {
|
||||
const parsed = typeof jsonLike === 'string' ? mojangson.simplify(mojangson.parse(jsonLike)) : nbt.simplify(jsonLike)
|
||||
const text = flat(parsed).map(x => x.text)
|
||||
return text.join('')
|
||||
} catch (err) {
|
||||
return jsonLike
|
||||
}
|
||||
}
|
||||
|
||||
getItemMesh(item) {
|
||||
const textureUv = this.getItemUv?.(item.itemId ?? item.blockId)
|
||||
if (textureUv) {
|
||||
// todo use geometry buffer uv instead!
|
||||
const { u, v, size, su, sv, texture } = textureUv
|
||||
const itemsTexture = texture.clone()
|
||||
itemsTexture.flipY = true
|
||||
itemsTexture.offset.set(u, 1 - v - (sv ?? size))
|
||||
itemsTexture.repeat.set(su ?? size, sv ?? size)
|
||||
itemsTexture.needsUpdate = true
|
||||
itemsTexture.magFilter = THREE.NearestFilter
|
||||
itemsTexture.minFilter = THREE.NearestFilter
|
||||
const itemsTextureFlipped = itemsTexture.clone()
|
||||
itemsTextureFlipped.repeat.x *= -1
|
||||
itemsTextureFlipped.needsUpdate = true
|
||||
itemsTextureFlipped.offset.set(u + (su ?? size), 1 - v - (sv ?? size))
|
||||
const material = new THREE.MeshStandardMaterial({
|
||||
map: itemsTexture,
|
||||
transparent: true,
|
||||
alphaTest: 0.1,
|
||||
})
|
||||
const materialFlipped = new THREE.MeshStandardMaterial({
|
||||
map: itemsTextureFlipped,
|
||||
transparent: true,
|
||||
alphaTest: 0.1,
|
||||
})
|
||||
const mesh = new THREE.Mesh(new THREE.BoxGeometry(1, 1, 0), [
|
||||
// top left and right bottom are black box materials others are transparent
|
||||
new THREE.MeshBasicMaterial({ color: 0x00_00_00 }), new THREE.MeshBasicMaterial({ color: 0x00_00_00 }),
|
||||
new THREE.MeshBasicMaterial({ color: 0x00_00_00 }), new THREE.MeshBasicMaterial({ color: 0x00_00_00 }),
|
||||
material, materialFlipped,
|
||||
])
|
||||
return {
|
||||
mesh,
|
||||
itemsTexture,
|
||||
itemsTextureFlipped,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
update(/** @type {import('prismarine-entity').Entity & {delete?, pos}} */entity, overrides) {
|
||||
let isPlayerModel = entity.name === 'player'
|
||||
if (entity.name === 'zombie' || entity.name === 'zombie_villager' || entity.name === 'husk') {
|
||||
isPlayerModel = true
|
||||
overrides.texture = `textures/1.16.4/entity/${entity.name === 'zombie_villager' ? 'zombie_villager/zombie_villager.png' : `zombie/${entity.name}.png`}`
|
||||
}
|
||||
if (!this.entities[entity.id] && !entity.delete) {
|
||||
const group = new THREE.Group()
|
||||
let mesh
|
||||
if (entity.name === 'item') {
|
||||
/** @type {any} */
|
||||
//@ts-expect-error
|
||||
const item = entity.metadata?.find(m => typeof m === 'object' && m?.itemCount)
|
||||
if (item) {
|
||||
const object = this.getItemMesh(item)
|
||||
if (object) {
|
||||
object.scale.set(0.5, 0.5, 0.5)
|
||||
object.position.set(0, 0.2, 0)
|
||||
// set faces
|
||||
// mesh.position.set(targetPos.x + 0.5 + 2, targetPos.y + 0.5, targetPos.z + 0.5)
|
||||
// viewer.scene.add(mesh)
|
||||
const clock = new THREE.Clock()
|
||||
object.onBeforeRender = () => {
|
||||
const delta = clock.getDelta()
|
||||
object.rotation.y += delta
|
||||
}
|
||||
//@ts-expect-error
|
||||
group.additionalCleanup = () => {
|
||||
// important: avoid texture memory leak and gpu slowdown
|
||||
object.itemsTexture.dispose()
|
||||
object.itemsTextureFlipped.dispose()
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (isPlayerModel) {
|
||||
// CREATE NEW PLAYER ENTITY
|
||||
const wrapper = new THREE.Group()
|
||||
/** @type {PlayerObject & { animation?: PlayerAnimation }} */
|
||||
const playerObject = new PlayerObject()
|
||||
playerObject.position.set(0, 16, 0)
|
||||
|
||||
//@ts-expect-error
|
||||
wrapper.add(playerObject)
|
||||
const scale = 1 / 16
|
||||
wrapper.scale.set(scale, scale, scale)
|
||||
|
||||
if (entity.username) {
|
||||
// todo proper colors
|
||||
const nameTag = new NameTagObject(fromFormattedString(entity.username).text, {
|
||||
font: `48px ${this.entitiesOptions.fontFamily}`,
|
||||
})
|
||||
nameTag.position.y = playerObject.position.y + playerObject.scale.y * 16 + 3
|
||||
nameTag.renderOrder = 1000
|
||||
|
||||
//@ts-expect-error
|
||||
wrapper.add(nameTag)
|
||||
}
|
||||
|
||||
//@ts-expect-error
|
||||
group.playerObject = playerObject
|
||||
wrapper.rotation.set(0, Math.PI, 0)
|
||||
mesh = wrapper
|
||||
playerObject.animation = new WalkingGeneralSwing()
|
||||
//@ts-expect-error
|
||||
playerObject.animation.isMoving = false
|
||||
} else {
|
||||
mesh = getEntityMesh(entity, this.scene, this.entitiesOptions, overrides)
|
||||
}
|
||||
if (!mesh) return
|
||||
mesh.name = 'mesh'
|
||||
// set initial position so there are no weird jumps update after
|
||||
group.position.set(entity.pos.x, entity.pos.y, entity.pos.z)
|
||||
|
||||
// todo use width and height instead
|
||||
const boxHelper = new THREE.BoxHelper(
|
||||
mesh,
|
||||
entity.type === 'hostile' ? 0xff_00_00 :
|
||||
entity.type === 'mob' ? 0x00_ff_00 :
|
||||
entity.type === 'player' ? 0x00_00_ff :
|
||||
0xff_a5_00,
|
||||
)
|
||||
boxHelper.name = 'debug'
|
||||
group.add(mesh)
|
||||
group.add(boxHelper)
|
||||
boxHelper.visible = false
|
||||
this.scene.add(group)
|
||||
|
||||
this.entities[entity.id] = group
|
||||
|
||||
this.emit('add', entity)
|
||||
|
||||
if (isPlayerModel) {
|
||||
this.updatePlayerSkin(entity.id, '', overrides?.texture || stevePng)
|
||||
}
|
||||
this.setDebugMode(this.debugMode, group)
|
||||
this.setRendering(this.rendering, group)
|
||||
}
|
||||
|
||||
//@ts-expect-error
|
||||
// set visibility
|
||||
const isInvisible = entity.metadata?.[0] & 0x20
|
||||
for (const child of this.entities[entity.id]?.children.find(c => c.name === 'mesh')?.children ?? []) {
|
||||
if (child.name !== 'nametag') {
|
||||
child.visible = !isInvisible
|
||||
}
|
||||
}
|
||||
// ---
|
||||
// not player
|
||||
const displayText = entity.metadata?.[3] && this.parseEntityLabel(entity.metadata[2])
|
||||
if (entity.name !== 'player' && displayText) {
|
||||
addNametag({ ...entity, username: displayText }, this.entitiesOptions, this.entities[entity.id].children.find(c => c.name === 'mesh'))
|
||||
}
|
||||
|
||||
// todo handle map, map_chunks events
|
||||
// if (entity.name === 'item_frame' || entity.name === 'glow_item_frame') {
|
||||
// const example = {
|
||||
// "present": true,
|
||||
// "itemId": 847,
|
||||
// "itemCount": 1,
|
||||
// "nbtData": {
|
||||
// "type": "compound",
|
||||
// "name": "",
|
||||
// "value": {
|
||||
// "map": {
|
||||
// "type": "int",
|
||||
// "value": 2146483444
|
||||
// },
|
||||
// "interactiveboard": {
|
||||
// "type": "byte",
|
||||
// "value": 1
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// const item = entity.metadata?.[8]
|
||||
// if (item.nbtData) {
|
||||
// const nbt = nbt.simplify(item.nbtData)
|
||||
// }
|
||||
// }
|
||||
|
||||
// this can be undefined in case where packet entity_destroy was sent twice (so it was already deleted)
|
||||
const e = this.entities[entity.id]
|
||||
|
||||
if (entity.username) {
|
||||
e.username = entity.username
|
||||
}
|
||||
|
||||
if (e?.playerObject && overrides?.rotation?.head) {
|
||||
/** @type {PlayerObject} */
|
||||
// eslint-disable-next-line prefer-destructuring
|
||||
const playerObject = e.playerObject
|
||||
const headRotationDiff = overrides.rotation.head.y ? overrides.rotation.head.y - entity.yaw : 0
|
||||
playerObject.skin.head.rotation.y = -headRotationDiff
|
||||
playerObject.skin.head.rotation.x = overrides.rotation.head.x ? - overrides.rotation.head.x : 0
|
||||
}
|
||||
|
||||
if (entity.delete && e) {
|
||||
if (e.additionalCleanup) e.additionalCleanup()
|
||||
this.emit('remove', entity)
|
||||
this.scene.remove(e)
|
||||
disposeObject(e)
|
||||
// todo dispose textures as well ?
|
||||
delete this.entities[entity.id]
|
||||
}
|
||||
|
||||
if (entity.pos) {
|
||||
new TWEEN.Tween(e.position).to({ x: entity.pos.x, y: entity.pos.y, z: entity.pos.z }, TWEEN_DURATION).start()
|
||||
}
|
||||
if (entity.yaw) {
|
||||
const da = (entity.yaw - e.rotation.y) % (Math.PI * 2)
|
||||
const dy = 2 * da % (Math.PI * 2) - da
|
||||
new TWEEN.Tween(e.rotation).to({ y: e.rotation.y + dy }, TWEEN_DURATION).start()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,381 +0,0 @@
|
|||
//@ts-check
|
||||
import * as THREE from 'three'
|
||||
import { OBJLoader } from 'three-stdlib'
|
||||
import entities from './entities.json'
|
||||
import { externalModels } from './objModels'
|
||||
import externalTexturesJson from './externalTextures.json'
|
||||
// import { loadTexture } from globalThis.isElectron ? '../utils.electron.js' : '../utils';
|
||||
const { loadTexture } = globalThis.isElectron ? require('../utils.electron.js') : require('../utils')
|
||||
|
||||
const elemFaces = {
|
||||
up: {
|
||||
dir: [0, 1, 0],
|
||||
u0: [0, 0, 1],
|
||||
v0: [0, 0, 0],
|
||||
u1: [1, 0, 1],
|
||||
v1: [0, 0, 1],
|
||||
corners: [
|
||||
[0, 1, 1, 0, 0],
|
||||
[1, 1, 1, 1, 0],
|
||||
[0, 1, 0, 0, 1],
|
||||
[1, 1, 0, 1, 1]
|
||||
]
|
||||
},
|
||||
down: {
|
||||
dir: [0, -1, 0],
|
||||
u0: [1, 0, 1],
|
||||
v0: [0, 0, 0],
|
||||
u1: [2, 0, 1],
|
||||
v1: [0, 0, 1],
|
||||
corners: [
|
||||
[1, 0, 1, 0, 0],
|
||||
[0, 0, 1, 1, 0],
|
||||
[1, 0, 0, 0, 1],
|
||||
[0, 0, 0, 1, 1]
|
||||
]
|
||||
},
|
||||
east: {
|
||||
dir: [1, 0, 0],
|
||||
u0: [0, 0, 0],
|
||||
v0: [0, 0, 1],
|
||||
u1: [0, 0, 1],
|
||||
v1: [0, 1, 1],
|
||||
corners: [
|
||||
[1, 1, 1, 0, 0],
|
||||
[1, 0, 1, 0, 1],
|
||||
[1, 1, 0, 1, 0],
|
||||
[1, 0, 0, 1, 1]
|
||||
]
|
||||
},
|
||||
west: {
|
||||
dir: [-1, 0, 0],
|
||||
u0: [1, 0, 1],
|
||||
v0: [0, 0, 1],
|
||||
u1: [1, 0, 2],
|
||||
v1: [0, 1, 1],
|
||||
corners: [
|
||||
[0, 1, 0, 0, 0],
|
||||
[0, 0, 0, 0, 1],
|
||||
[0, 1, 1, 1, 0],
|
||||
[0, 0, 1, 1, 1]
|
||||
]
|
||||
},
|
||||
north: {
|
||||
dir: [0, 0, -1],
|
||||
u0: [0, 0, 1],
|
||||
v0: [0, 0, 1],
|
||||
u1: [1, 0, 1],
|
||||
v1: [0, 1, 1],
|
||||
corners: [
|
||||
[1, 0, 0, 0, 1],
|
||||
[0, 0, 0, 1, 1],
|
||||
[1, 1, 0, 0, 0],
|
||||
[0, 1, 0, 1, 0]
|
||||
]
|
||||
},
|
||||
south: {
|
||||
dir: [0, 0, 1],
|
||||
u0: [1, 0, 2],
|
||||
v0: [0, 0, 1],
|
||||
u1: [2, 0, 2],
|
||||
v1: [0, 1, 1],
|
||||
corners: [
|
||||
[0, 0, 1, 0, 1],
|
||||
[1, 0, 1, 1, 1],
|
||||
[0, 1, 1, 0, 0],
|
||||
[1, 1, 1, 1, 0]
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
function dot(a, b) {
|
||||
return a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
|
||||
}
|
||||
|
||||
function addCube(attr, boneId, bone, cube, texWidth = 64, texHeight = 64) {
|
||||
const cubeRotation = new THREE.Euler(0, 0, 0)
|
||||
if (cube.rotation) {
|
||||
cubeRotation.x = -cube.rotation[0] * Math.PI / 180
|
||||
cubeRotation.y = -cube.rotation[1] * Math.PI / 180
|
||||
cubeRotation.z = -cube.rotation[2] * Math.PI / 180
|
||||
}
|
||||
for (const { dir, corners, u0, v0, u1, v1 } of Object.values(elemFaces)) {
|
||||
const ndx = Math.floor(attr.positions.length / 3)
|
||||
|
||||
for (const pos of corners) {
|
||||
const u = (cube.uv[0] + dot(pos[3] ? u1 : u0, cube.size)) / texWidth
|
||||
const v = (cube.uv[1] + dot(pos[4] ? v1 : v0, cube.size)) / texHeight
|
||||
|
||||
const inflate = cube.inflate ?? 0
|
||||
let vecPos = new THREE.Vector3(
|
||||
cube.origin[0] + pos[0] * cube.size[0] + (pos[0] ? inflate : -inflate),
|
||||
cube.origin[1] + pos[1] * cube.size[1] + (pos[1] ? inflate : -inflate),
|
||||
cube.origin[2] + pos[2] * cube.size[2] + (pos[2] ? inflate : -inflate)
|
||||
)
|
||||
|
||||
vecPos = vecPos.applyEuler(cubeRotation)
|
||||
vecPos = vecPos.sub(bone.position)
|
||||
vecPos = vecPos.applyEuler(bone.rotation)
|
||||
vecPos = vecPos.add(bone.position)
|
||||
|
||||
attr.positions.push(vecPos.x, vecPos.y, vecPos.z)
|
||||
attr.normals.push(...dir)
|
||||
attr.uvs.push(u, v)
|
||||
attr.skinIndices.push(boneId, 0, 0, 0)
|
||||
attr.skinWeights.push(1, 0, 0, 0)
|
||||
}
|
||||
|
||||
attr.indices.push(ndx, ndx + 1, ndx + 2, ndx + 2, ndx + 1, ndx + 3)
|
||||
}
|
||||
}
|
||||
|
||||
function getMesh(texture, jsonModel, overrides = {}) {
|
||||
const bones = {}
|
||||
|
||||
const geoData = {
|
||||
positions: [],
|
||||
normals: [],
|
||||
uvs: [],
|
||||
indices: [],
|
||||
skinIndices: [],
|
||||
skinWeights: []
|
||||
}
|
||||
let i = 0
|
||||
for (const jsonBone of jsonModel.bones) {
|
||||
const bone = new THREE.Bone()
|
||||
if (jsonBone.pivot) {
|
||||
bone.position.x = jsonBone.pivot[0]
|
||||
bone.position.y = jsonBone.pivot[1]
|
||||
bone.position.z = jsonBone.pivot[2]
|
||||
}
|
||||
if (jsonBone.bind_pose_rotation) {
|
||||
bone.rotation.x = -jsonBone.bind_pose_rotation[0] * Math.PI / 180
|
||||
bone.rotation.y = -jsonBone.bind_pose_rotation[1] * Math.PI / 180
|
||||
bone.rotation.z = -jsonBone.bind_pose_rotation[2] * Math.PI / 180
|
||||
} else if (jsonBone.rotation) {
|
||||
bone.rotation.x = -jsonBone.rotation[0] * Math.PI / 180
|
||||
bone.rotation.y = -jsonBone.rotation[1] * Math.PI / 180
|
||||
bone.rotation.z = -jsonBone.rotation[2] * Math.PI / 180
|
||||
}
|
||||
if (overrides.rotation?.[jsonBone.name]) {
|
||||
bone.rotation.x -= (overrides.rotation[jsonBone.name].x ?? 0) * Math.PI / 180
|
||||
bone.rotation.y -= (overrides.rotation[jsonBone.name].y ?? 0) * Math.PI / 180
|
||||
bone.rotation.z -= (overrides.rotation[jsonBone.name].z ?? 0) * Math.PI / 180
|
||||
}
|
||||
bone.name = `bone_${jsonBone.name}`
|
||||
bones[jsonBone.name] = bone
|
||||
|
||||
if (jsonBone.cubes) {
|
||||
for (const cube of jsonBone.cubes) {
|
||||
addCube(geoData, i, bone, cube, jsonModel.texturewidth, jsonModel.textureheight)
|
||||
}
|
||||
}
|
||||
i++
|
||||
}
|
||||
|
||||
const rootBones = []
|
||||
for (const jsonBone of jsonModel.bones) {
|
||||
if (jsonBone.parent && bones[jsonBone.parent]) { bones[jsonBone.parent].add(bones[jsonBone.name]) } else {
|
||||
rootBones.push(bones[jsonBone.name])
|
||||
}
|
||||
}
|
||||
|
||||
const skeleton = new THREE.Skeleton(Object.values(bones))
|
||||
|
||||
const geometry = new THREE.BufferGeometry()
|
||||
geometry.setAttribute('position', new THREE.Float32BufferAttribute(geoData.positions, 3))
|
||||
geometry.setAttribute('normal', new THREE.Float32BufferAttribute(geoData.normals, 3))
|
||||
geometry.setAttribute('uv', new THREE.Float32BufferAttribute(geoData.uvs, 2))
|
||||
geometry.setAttribute('skinIndex', new THREE.Uint16BufferAttribute(geoData.skinIndices, 4))
|
||||
geometry.setAttribute('skinWeight', new THREE.Float32BufferAttribute(geoData.skinWeights, 4))
|
||||
geometry.setIndex(geoData.indices)
|
||||
|
||||
const material = new THREE.MeshLambertMaterial({ transparent: true, alphaTest: 0.1 })
|
||||
const mesh = new THREE.SkinnedMesh(geometry, material)
|
||||
mesh.add(...rootBones)
|
||||
mesh.bind(skeleton)
|
||||
mesh.scale.set(1 / 16, 1 / 16, 1 / 16)
|
||||
|
||||
loadTexture(texture, texture => {
|
||||
if (material.map) {
|
||||
// texture is already loaded
|
||||
return
|
||||
}
|
||||
texture.magFilter = THREE.NearestFilter
|
||||
texture.minFilter = THREE.NearestFilter
|
||||
texture.flipY = false
|
||||
texture.wrapS = THREE.RepeatWrapping
|
||||
texture.wrapT = THREE.RepeatWrapping
|
||||
material.map = texture
|
||||
})
|
||||
|
||||
return mesh
|
||||
}
|
||||
|
||||
export const knownNotHandled = [
|
||||
'area_effect_cloud', 'block_display',
|
||||
'chest_boat', 'end_crystal',
|
||||
'falling_block', 'furnace_minecart',
|
||||
'giant', 'glow_item_frame',
|
||||
'glow_squid', 'illusioner',
|
||||
'interaction', 'item',
|
||||
'item_display', 'item_frame',
|
||||
'lightning_bolt', 'marker',
|
||||
'painting', 'spawner_minecart',
|
||||
'spectral_arrow', 'text_display',
|
||||
'tnt', 'trader_llama', 'zombie_horse'
|
||||
]
|
||||
|
||||
export const temporaryMap = {
|
||||
'furnace_minecart': 'minecart',
|
||||
'spawner_minecart': 'minecart',
|
||||
'chest_minecart': 'minecart',
|
||||
'hopper_minecart': 'minecart',
|
||||
'command_block_minecart': 'minecart',
|
||||
'tnt_minecart': 'minecart',
|
||||
'glow_squid': 'squid',
|
||||
'trader_llama': 'llama',
|
||||
'chest_boat': 'boat',
|
||||
'spectral_arrow': 'arrow',
|
||||
'husk': 'zombie',
|
||||
'zombie_horse': 'horse',
|
||||
'donkey': 'horse',
|
||||
'skeleton_horse': 'horse',
|
||||
'mule': 'horse',
|
||||
'ocelot': 'cat',
|
||||
// 'falling_block': 'block',
|
||||
// 'lightning_bolt': 'lightning',
|
||||
}
|
||||
|
||||
const getEntity = (name) => {
|
||||
return entities[name]
|
||||
}
|
||||
|
||||
// const externalModelsTextures = {
|
||||
// allay: 'allay/allay',
|
||||
// axolotl: 'axolotl/axolotl_blue',
|
||||
// blaze: 'blaze',
|
||||
// camel: 'camel/camel',
|
||||
// cat: 'cat/black',
|
||||
// chicken: 'chicken',
|
||||
// cod: 'fish/cod',
|
||||
// creeper: 'creeper/creeper',
|
||||
// dolphin: 'dolphin',
|
||||
// ender_dragon: 'enderdragon/dragon',
|
||||
// enderman: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAAAgCAYAAACinX6EAAAABGdBTUEAALGPC/xhBQAAAY5JREFUaN7lWNESgzAI8yv8/z/tXjZPHSShYitb73rXedo1AQJ0WchY17WhudQZ7TS18Qb5AXtY/yUBO8tXIaCRqRNwXlcgwDJgmAALfBUP8AjYEdHnAZUIAGdvPy+CnobJIVw9DVIPEABawuEyyvYx1sMIMP8fAbUO7ukBImZmCCEP2AhglnRip8vio7MIxYEsaVkdeYNjYfbN/BBA1twP9AxpB0qlMwj48gBP5Ji1rXc8nfBImk6A5+KqShNwdTwgKy0xYRzdS4yoY651W8EDRwGVJEDVITGtjiEAaEBq3o4SwGqRVAKsdVYIsAzDCACV6VwCFMBCpqLvgudzQ6CnjL5afmeX4pdE0LIQuYCBzZbQfT4rC6COUQGn9B3MQ28pSIxDSDdNrKdQSZJ7lDurMeZm6iEjKVENh8cQgBowBFK5gEHhsO3xFA/oKXp6vg8RoHaD2QRkiaDnAYcZAcB+E6GTRVAhQCVJyVImKOUiBLW3KL4jzU2POHp64RIQ/ADO6D6Ry1gl9tlN1Xm+AK8s2jHadDijAAAAAElFTkSuQmCC',
|
||||
// endermite: 'endermite',
|
||||
// fox: 'fox/fox',
|
||||
// frog: 'frog/cold_frog',
|
||||
// ghast: 'ghast/ghast',
|
||||
// goat: 'goat/goat',
|
||||
// guardian: 'guardian',
|
||||
// horse: 'horse/horse_brown',
|
||||
// llama: 'llama/creamy',
|
||||
// minecart: 'minecart',
|
||||
// parrot: 'parrot/parrot_grey',
|
||||
// piglin: 'piglin/piglin',
|
||||
// pillager: 'illager/pillager',
|
||||
// rabbit: 'rabbit/brown',
|
||||
// sheep: 'sheep/sheep',
|
||||
// shulker: 'shulker/shulker',
|
||||
// sniffer: 'sniffer/sniffer',
|
||||
// spider: 'spider/spider',
|
||||
// tadpole: 'tadpole/tadpole',
|
||||
// turtle: 'turtle/big_sea_turtle',
|
||||
// vex: 'illager/vex',
|
||||
// villager: 'villager/villager',
|
||||
// warden: 'warden/warden',
|
||||
// witch: 'witch',
|
||||
// wolf: 'wolf/wolf',
|
||||
// zombie_villager: 'zombie_villager/zombie_villager'
|
||||
// }
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-extraneous-class
|
||||
export class EntityMesh {
|
||||
constructor(version, type, scene, /** @type {{textures?, rotation?: Record<string, {x,y,z}>}} */overrides = {}) {
|
||||
const originalType = type
|
||||
const mappedValue = temporaryMap[type]
|
||||
if (mappedValue) type = mappedValue
|
||||
|
||||
if (externalModels[type]) {
|
||||
const objLoader = new OBJLoader()
|
||||
let texturePath = externalTexturesJson[type]
|
||||
if (originalType === 'zombie_horse') {
|
||||
texturePath = `textures/${version}/entity/horse/horse_zombie.png`
|
||||
}
|
||||
if (originalType === 'skeleton_horse') {
|
||||
texturePath = `textures/${version}/entity/horse/horse_skeleton.png`
|
||||
}
|
||||
if (originalType === 'donkey') {
|
||||
texturePath = `textures/${version}/entity/horse/donkey.png`
|
||||
}
|
||||
if (originalType === 'mule') {
|
||||
texturePath = `textures/${version}/entity/horse/mule.png`
|
||||
}
|
||||
if (originalType === 'ocelot') {
|
||||
texturePath = `textures/${version}/entity/cat/ocelot.png`
|
||||
}
|
||||
if (!texturePath) throw new Error(`No texture for ${type}`)
|
||||
const texture = new THREE.TextureLoader().load(texturePath)
|
||||
texture.minFilter = THREE.NearestFilter
|
||||
texture.magFilter = THREE.NearestFilter
|
||||
const material = new THREE.MeshBasicMaterial({
|
||||
map: texture,
|
||||
transparent: true,
|
||||
alphaTest: 0.1
|
||||
})
|
||||
const obj = objLoader.parse(externalModels[type])
|
||||
if (type === 'boat') obj.position.y = -1 // todo, should not be hardcoded
|
||||
obj.traverse((child) => {
|
||||
if (child instanceof THREE.Mesh) {
|
||||
child.material = material
|
||||
// todo
|
||||
if (child.name === 'Head layer') child.visible = false
|
||||
if (child.name === 'Head' && overrides.rotation?.head) { // todo
|
||||
child.rotation.x -= (overrides.rotation.head.x ?? 0) * Math.PI / 180
|
||||
child.rotation.y -= (overrides.rotation.head.y ?? 0) * Math.PI / 180
|
||||
child.rotation.z -= (overrides.rotation.head.z ?? 0) * Math.PI / 180
|
||||
}
|
||||
}
|
||||
})
|
||||
this.mesh = obj
|
||||
return
|
||||
}
|
||||
|
||||
const e = getEntity(type)
|
||||
if (!e) {
|
||||
if (knownNotHandled.includes(type)) return
|
||||
throw new Error(`Unknown entity ${type}`)
|
||||
}
|
||||
|
||||
this.mesh = new THREE.Object3D()
|
||||
for (const [name, jsonModel] of Object.entries(e.geometry)) {
|
||||
const texture = overrides.textures?.[name] ?? e.textures[name]
|
||||
if (!texture) continue
|
||||
// console.log(JSON.stringify(jsonModel, null, 2))
|
||||
const mesh = getMesh(texture + '.png', jsonModel, overrides)
|
||||
mesh.name = `geometry_${name}`
|
||||
this.mesh.add(mesh)
|
||||
|
||||
const skeletonHelper = new THREE.SkeletonHelper(mesh)
|
||||
//@ts-expect-error
|
||||
skeletonHelper.material.linewidth = 2
|
||||
skeletonHelper.visible = false
|
||||
this.mesh.add(skeletonHelper)
|
||||
}
|
||||
}
|
||||
|
||||
static getStaticData(name) {
|
||||
name = temporaryMap[name] || name
|
||||
if (externalModels[name]) {
|
||||
return {
|
||||
boneNames: [] // todo
|
||||
}
|
||||
}
|
||||
const e = getEntity(name)
|
||||
if (!e) throw new Error(`Unknown entity ${name}`)
|
||||
return {
|
||||
boneNames: Object.values(e.geometry).flatMap(x => x.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,103 +0,0 @@
|
|||
import { PlayerAnimation } from 'skinview3d'
|
||||
|
||||
export class WalkingGeneralSwing extends PlayerAnimation {
|
||||
|
||||
switchAnimationCallback
|
||||
|
||||
isRunning = false
|
||||
isMoving = true
|
||||
|
||||
_startArmSwing
|
||||
|
||||
swingArm() {
|
||||
this._startArmSwing = this.progress
|
||||
}
|
||||
|
||||
animate(player) {
|
||||
// Multiply by animation's natural speed
|
||||
let t
|
||||
const updateT = () => {
|
||||
if (!this.isMoving) {
|
||||
t = 0
|
||||
return
|
||||
}
|
||||
if (this.isRunning) {
|
||||
t = this.progress * 10 + Math.PI * 0.5
|
||||
} else {
|
||||
t = this.progress * 8
|
||||
}
|
||||
}
|
||||
updateT()
|
||||
let reset = false
|
||||
|
||||
if ((this.isRunning ? Math.cos(t) : Math.sin(t)) < 0.01) {
|
||||
if (this.switchAnimationCallback) {
|
||||
reset = true
|
||||
this.progress = 0
|
||||
updateT()
|
||||
}
|
||||
}
|
||||
|
||||
if (this.isRunning) {
|
||||
// Leg swing with larger amplitude
|
||||
player.skin.leftLeg.rotation.x = Math.cos(t + Math.PI) * 1.3
|
||||
player.skin.rightLeg.rotation.x = Math.cos(t) * 1.3
|
||||
} else {
|
||||
// Leg swing
|
||||
player.skin.leftLeg.rotation.x = Math.sin(t) * 0.5
|
||||
player.skin.rightLeg.rotation.x = Math.sin(t + Math.PI) * 0.5
|
||||
}
|
||||
|
||||
if (this._startArmSwing) {
|
||||
const tHand = (this.progress - this._startArmSwing) * 18 + Math.PI * 0.5
|
||||
player.skin.rightArm.rotation.x = Math.cos(tHand) * 1.5
|
||||
const basicArmRotationZ = Math.PI * 0.1
|
||||
player.skin.rightArm.rotation.z = Math.cos(t + Math.PI) * 0.3 - basicArmRotationZ
|
||||
|
||||
if (tHand > Math.PI + Math.PI * 0.5) {
|
||||
this._startArmSwing = null
|
||||
player.skin.rightArm.rotation.z = 0
|
||||
}
|
||||
}
|
||||
|
||||
if (this.isRunning) {
|
||||
player.skin.leftArm.rotation.x = Math.cos(t) * 1.5
|
||||
if (!this._startArmSwing) {
|
||||
player.skin.rightArm.rotation.x = Math.cos(t + Math.PI) * 1.5
|
||||
}
|
||||
const basicArmRotationZ = Math.PI * 0.1
|
||||
player.skin.leftArm.rotation.z = Math.cos(t) * 0.1 + basicArmRotationZ
|
||||
if (!this._startArmSwing) {
|
||||
player.skin.rightArm.rotation.z = Math.cos(t + Math.PI) * 0.1 - basicArmRotationZ
|
||||
}
|
||||
} else {
|
||||
// Arm swing
|
||||
player.skin.leftArm.rotation.x = Math.sin(t + Math.PI) * 0.5
|
||||
if (!this._startArmSwing) {
|
||||
player.skin.rightArm.rotation.x = Math.sin(t) * 0.5
|
||||
}
|
||||
const basicArmRotationZ = Math.PI * 0.02
|
||||
player.skin.leftArm.rotation.z = Math.cos(t) * 0.03 + basicArmRotationZ
|
||||
if (!this._startArmSwing) {
|
||||
player.skin.rightArm.rotation.z = Math.cos(t + Math.PI) * 0.03 - basicArmRotationZ
|
||||
}
|
||||
}
|
||||
|
||||
if (this.isRunning) {
|
||||
player.rotation.z = Math.cos(t + Math.PI) * 0.01
|
||||
}
|
||||
if (this.isRunning) {
|
||||
const basicCapeRotationX = Math.PI * 0.3
|
||||
player.cape.rotation.x = Math.sin(t * 2) * 0.1 + basicCapeRotationX
|
||||
} else {
|
||||
// Always add an angle for cape around the x axis
|
||||
const basicCapeRotationX = Math.PI * 0.06
|
||||
player.cape.rotation.x = Math.sin(t / 1.5) * 0.06 + basicCapeRotationX
|
||||
}
|
||||
|
||||
if (reset) {
|
||||
this.switchAnimationCallback()
|
||||
this.switchAnimationCallback = null
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,325 +0,0 @@
|
|||
# Made in Blockbench 4.9.4
|
||||
mtllib materials.mtl
|
||||
|
||||
o Body
|
||||
v 0.25 1.5 0.125
|
||||
v 0.25 1.5 -0.125
|
||||
v 0.25 0.75 0.125
|
||||
v 0.25 0.75 -0.125
|
||||
v -0.25 1.5 -0.125
|
||||
v -0.25 1.5 0.125
|
||||
v -0.25 0.75 -0.125
|
||||
v -0.25 0.75 0.125
|
||||
vt 0.3125 0.375
|
||||
vt 0.4375 0.375
|
||||
vt 0.4375 0
|
||||
vt 0.3125 0
|
||||
vt 0.25 0.375
|
||||
vt 0.3125 0.375
|
||||
vt 0.3125 0
|
||||
vt 0.25 0
|
||||
vt 0.5 0.375
|
||||
vt 0.625 0.375
|
||||
vt 0.625 0
|
||||
vt 0.5 0
|
||||
vt 0.4375 0.375
|
||||
vt 0.5 0.375
|
||||
vt 0.5 0
|
||||
vt 0.4375 0
|
||||
vt 0.4375 0.375
|
||||
vt 0.3125 0.375
|
||||
vt 0.3125 0.5
|
||||
vt 0.4375 0.5
|
||||
vt 0.5625 0.5
|
||||
vt 0.4375 0.5
|
||||
vt 0.4375 0.375
|
||||
vt 0.5625 0.375
|
||||
vn 0 0 -1
|
||||
vn 1 0 0
|
||||
vn 0 0 1
|
||||
vn -1 0 0
|
||||
vn 0 1 0
|
||||
vn 0 -1 0
|
||||
usemtl m_9eb5cf2e-0212-52a4-6070-8cb3b67f2e24
|
||||
f 4/4/1 7/3/1 5/2/1 2/1/1
|
||||
f 3/8/2 4/7/2 2/6/2 1/5/2
|
||||
f 8/12/3 3/11/3 1/10/3 6/9/3
|
||||
f 7/16/4 8/15/4 6/14/4 5/13/4
|
||||
f 6/20/5 1/19/5 2/18/5 5/17/5
|
||||
f 7/24/6 4/23/6 3/22/6 8/21/6
|
||||
o Head
|
||||
v 0.25 2 0.25
|
||||
v 0.25 2 -0.25
|
||||
v 0.25 1.5 0.25
|
||||
v 0.25 1.5 -0.25
|
||||
v -0.25 2 -0.25
|
||||
v -0.25 2 0.25
|
||||
v -0.25 1.5 -0.25
|
||||
v -0.25 1.5 0.25
|
||||
vt 0.125 0.75
|
||||
vt 0.25 0.75
|
||||
vt 0.25 0.5
|
||||
vt 0.125 0.5
|
||||
vt 0 0.75
|
||||
vt 0.125 0.75
|
||||
vt 0.125 0.5
|
||||
vt 0 0.5
|
||||
vt 0.375 0.75
|
||||
vt 0.5 0.75
|
||||
vt 0.5 0.5
|
||||
vt 0.375 0.5
|
||||
vt 0.25 0.75
|
||||
vt 0.375 0.75
|
||||
vt 0.375 0.5
|
||||
vt 0.25 0.5
|
||||
vt 0.25 0.75
|
||||
vt 0.125 0.75
|
||||
vt 0.125 1
|
||||
vt 0.25 1
|
||||
vt 0.375 1
|
||||
vt 0.25 1
|
||||
vt 0.25 0.75
|
||||
vt 0.375 0.75
|
||||
vn 0 0 -1
|
||||
vn 1 0 0
|
||||
vn 0 0 1
|
||||
vn -1 0 0
|
||||
vn 0 1 0
|
||||
vn 0 -1 0
|
||||
usemtl m_9eb5cf2e-0212-52a4-6070-8cb3b67f2e24
|
||||
f 12/28/7 15/27/7 13/26/7 10/25/7
|
||||
f 11/32/8 12/31/8 10/30/8 9/29/8
|
||||
f 16/36/9 11/35/9 9/34/9 14/33/9
|
||||
f 15/40/10 16/39/10 14/38/10 13/37/10
|
||||
f 14/44/11 9/43/11 10/42/11 13/41/11
|
||||
f 15/48/12 12/47/12 11/46/12 16/45/12
|
||||
o Hat Layer
|
||||
v 0.28125 2.03125 0.28125
|
||||
v 0.28125 2.03125 -0.28125
|
||||
v 0.28125 1.46875 0.28125
|
||||
v 0.28125 1.46875 -0.28125
|
||||
v -0.28125 2.03125 -0.28125
|
||||
v -0.28125 2.03125 0.28125
|
||||
v -0.28125 1.46875 -0.28125
|
||||
v -0.28125 1.46875 0.28125
|
||||
vt 0.625 0.75
|
||||
vt 0.75 0.75
|
||||
vt 0.75 0.5
|
||||
vt 0.625 0.5
|
||||
vt 0.5 0.75
|
||||
vt 0.625 0.75
|
||||
vt 0.625 0.5
|
||||
vt 0.5 0.5
|
||||
vt 0.875 0.75
|
||||
vt 1 0.75
|
||||
vt 1 0.5
|
||||
vt 0.875 0.5
|
||||
vt 0.75 0.75
|
||||
vt 0.875 0.75
|
||||
vt 0.875 0.5
|
||||
vt 0.75 0.5
|
||||
vt 0.75 0.75
|
||||
vt 0.625 0.75
|
||||
vt 0.625 1
|
||||
vt 0.75 1
|
||||
vt 0.875 1
|
||||
vt 0.75 1
|
||||
vt 0.75 0.75
|
||||
vt 0.875 0.75
|
||||
vn 0 0 -1
|
||||
vn 1 0 0
|
||||
vn 0 0 1
|
||||
vn -1 0 0
|
||||
vn 0 1 0
|
||||
vn 0 -1 0
|
||||
usemtl m_9eb5cf2e-0212-52a4-6070-8cb3b67f2e24
|
||||
f 20/52/13 23/51/13 21/50/13 18/49/13
|
||||
f 19/56/14 20/55/14 18/54/14 17/53/14
|
||||
f 24/60/15 19/59/15 17/58/15 22/57/15
|
||||
f 23/64/16 24/63/16 22/62/16 21/61/16
|
||||
f 22/68/17 17/67/17 18/66/17 21/65/17
|
||||
f 23/72/18 20/71/18 19/70/18 24/69/18
|
||||
o RightArm
|
||||
v 0.5 1.5 0.125
|
||||
v 0.5 1.5 -0.125
|
||||
v 0.5 0.75 0.125
|
||||
v 0.5 0.75 -0.125
|
||||
v 0.25 1.5 -0.125
|
||||
v 0.25 1.5 0.125
|
||||
v 0.25 0.75 -0.125
|
||||
v 0.25 0.75 0.125
|
||||
vt 0.6875 0.375
|
||||
vt 0.75 0.375
|
||||
vt 0.75 0
|
||||
vt 0.6875 0
|
||||
vt 0.625 0.375
|
||||
vt 0.6875 0.375
|
||||
vt 0.6875 0
|
||||
vt 0.625 0
|
||||
vt 0.8125 0.375
|
||||
vt 0.875 0.375
|
||||
vt 0.875 0
|
||||
vt 0.8125 0
|
||||
vt 0.75 0.375
|
||||
vt 0.8125 0.375
|
||||
vt 0.8125 0
|
||||
vt 0.75 0
|
||||
vt 0.75 0.375
|
||||
vt 0.6875 0.375
|
||||
vt 0.6875 0.5
|
||||
vt 0.75 0.5
|
||||
vt 0.8125 0.5
|
||||
vt 0.75 0.5
|
||||
vt 0.75 0.375
|
||||
vt 0.8125 0.375
|
||||
vn 0 0 -1
|
||||
vn 1 0 0
|
||||
vn 0 0 1
|
||||
vn -1 0 0
|
||||
vn 0 1 0
|
||||
vn 0 -1 0
|
||||
usemtl m_9eb5cf2e-0212-52a4-6070-8cb3b67f2e24
|
||||
f 28/76/19 31/75/19 29/74/19 26/73/19
|
||||
f 27/80/20 28/79/20 26/78/20 25/77/20
|
||||
f 32/84/21 27/83/21 25/82/21 30/81/21
|
||||
f 31/88/22 32/87/22 30/86/22 29/85/22
|
||||
f 30/92/23 25/91/23 26/90/23 29/89/23
|
||||
f 31/96/24 28/95/24 27/94/24 32/93/24
|
||||
o LeftArm
|
||||
v -0.25 1.5 0.125
|
||||
v -0.25 1.5 -0.125
|
||||
v -0.25 0.75 0.125
|
||||
v -0.25 0.75 -0.125
|
||||
v -0.5 1.5 -0.125
|
||||
v -0.5 1.5 0.125
|
||||
v -0.5 0.75 -0.125
|
||||
v -0.5 0.75 0.125
|
||||
vt 0.75 0.375
|
||||
vt 0.6875 0.375
|
||||
vt 0.6875 0
|
||||
vt 0.75 0
|
||||
vt 0.8125 0.375
|
||||
vt 0.75 0.375
|
||||
vt 0.75 0
|
||||
vt 0.8125 0
|
||||
vt 0.875 0.375
|
||||
vt 0.8125 0.375
|
||||
vt 0.8125 0
|
||||
vt 0.875 0
|
||||
vt 0.6875 0.375
|
||||
vt 0.625 0.375
|
||||
vt 0.625 0
|
||||
vt 0.6875 0
|
||||
vt 0.6875 0.375
|
||||
vt 0.75 0.375
|
||||
vt 0.75 0.5
|
||||
vt 0.6875 0.5
|
||||
vt 0.75 0.5
|
||||
vt 0.8125 0.5
|
||||
vt 0.8125 0.375
|
||||
vt 0.75 0.375
|
||||
vn 0 0 -1
|
||||
vn 1 0 0
|
||||
vn 0 0 1
|
||||
vn -1 0 0
|
||||
vn 0 1 0
|
||||
vn 0 -1 0
|
||||
usemtl m_9eb5cf2e-0212-52a4-6070-8cb3b67f2e24
|
||||
f 36/100/25 39/99/25 37/98/25 34/97/25
|
||||
f 35/104/26 36/103/26 34/102/26 33/101/26
|
||||
f 40/108/27 35/107/27 33/106/27 38/105/27
|
||||
f 39/112/28 40/111/28 38/110/28 37/109/28
|
||||
f 38/116/29 33/115/29 34/114/29 37/113/29
|
||||
f 39/120/30 36/119/30 35/118/30 40/117/30
|
||||
o RightLeg
|
||||
v 0.24375000000000002 0.75 0.125
|
||||
v 0.24375000000000002 0.75 -0.125
|
||||
v 0.24375000000000002 0 0.125
|
||||
v 0.24375000000000002 0 -0.125
|
||||
v -0.006249999999999978 0.75 -0.125
|
||||
v -0.006249999999999978 0.75 0.125
|
||||
v -0.006249999999999978 0 -0.125
|
||||
v -0.006249999999999978 0 0.125
|
||||
vt 0.0625 0.375
|
||||
vt 0.125 0.375
|
||||
vt 0.125 0
|
||||
vt 0.0625 0
|
||||
vt 0 0.375
|
||||
vt 0.0625 0.375
|
||||
vt 0.0625 0
|
||||
vt 0 0
|
||||
vt 0.1875 0.375
|
||||
vt 0.25 0.375
|
||||
vt 0.25 0
|
||||
vt 0.1875 0
|
||||
vt 0.125 0.375
|
||||
vt 0.1875 0.375
|
||||
vt 0.1875 0
|
||||
vt 0.125 0
|
||||
vt 0.125 0.375
|
||||
vt 0.0625 0.375
|
||||
vt 0.0625 0.5
|
||||
vt 0.125 0.5
|
||||
vt 0.1875 0.5
|
||||
vt 0.125 0.5
|
||||
vt 0.125 0.375
|
||||
vt 0.1875 0.375
|
||||
vn 0 0 -1
|
||||
vn 1 0 0
|
||||
vn 0 0 1
|
||||
vn -1 0 0
|
||||
vn 0 1 0
|
||||
vn 0 -1 0
|
||||
usemtl m_9eb5cf2e-0212-52a4-6070-8cb3b67f2e24
|
||||
f 44/124/31 47/123/31 45/122/31 42/121/31
|
||||
f 43/128/32 44/127/32 42/126/32 41/125/32
|
||||
f 48/132/33 43/131/33 41/130/33 46/129/33
|
||||
f 47/136/34 48/135/34 46/134/34 45/133/34
|
||||
f 46/140/35 41/139/35 42/138/35 45/137/35
|
||||
f 47/144/36 44/143/36 43/142/36 48/141/36
|
||||
o LeftLeg
|
||||
v 0.006249999999999978 0.75 0.125
|
||||
v 0.006249999999999978 0.75 -0.125
|
||||
v 0.006249999999999978 0 0.125
|
||||
v 0.006249999999999978 0 -0.125
|
||||
v -0.24375000000000002 0.75 -0.125
|
||||
v -0.24375000000000002 0.75 0.125
|
||||
v -0.24375000000000002 0 -0.125
|
||||
v -0.24375000000000002 0 0.125
|
||||
vt 0.125 0.375
|
||||
vt 0.0625 0.375
|
||||
vt 0.0625 0
|
||||
vt 0.125 0
|
||||
vt 0.1875 0.375
|
||||
vt 0.125 0.375
|
||||
vt 0.125 0
|
||||
vt 0.1875 0
|
||||
vt 0.25 0.375
|
||||
vt 0.1875 0.375
|
||||
vt 0.1875 0
|
||||
vt 0.25 0
|
||||
vt 0.0625 0.375
|
||||
vt 0 0.375
|
||||
vt 0 0
|
||||
vt 0.0625 0
|
||||
vt 0.0625 0.375
|
||||
vt 0.125 0.375
|
||||
vt 0.125 0.5
|
||||
vt 0.0625 0.5
|
||||
vt 0.125 0.5
|
||||
vt 0.1875 0.5
|
||||
vt 0.1875 0.375
|
||||
vt 0.125 0.375
|
||||
vn 0 0 -1
|
||||
vn 1 0 0
|
||||
vn 0 0 1
|
||||
vn -1 0 0
|
||||
vn 0 1 0
|
||||
vn 0 -1 0
|
||||
usemtl m_9eb5cf2e-0212-52a4-6070-8cb3b67f2e24
|
||||
f 52/148/37 55/147/37 53/146/37 50/145/37
|
||||
f 51/152/38 52/151/38 50/150/38 49/149/38
|
||||
f 56/156/39 51/155/39 49/154/39 54/153/39
|
||||
f 55/160/40 56/159/40 54/158/40 53/157/40
|
||||
f 54/164/41 49/163/41 50/162/41 53/161/41
|
||||
f 55/168/42 52/167/42 51/166/42 56/165/42
|
||||
|
|
@ -1,207 +0,0 @@
|
|||
import * as THREE from 'three'
|
||||
import * as tweenJs from '@tweenjs/tween.js'
|
||||
import worldBlockProvider from 'mc-assets/dist/worldBlockProvider'
|
||||
import { getThreeBlockModelGroup, renderBlockThree, setBlockPosition } from './mesher/standaloneRenderer'
|
||||
|
||||
export type HandItemBlock = {
|
||||
name
|
||||
properties
|
||||
}
|
||||
|
||||
export default class HoldingBlock {
|
||||
holdingBlock: THREE.Object3D | undefined = undefined
|
||||
swingAnimation: tweenJs.Group | undefined = undefined
|
||||
blockSwapAnimation: {
|
||||
tween: tweenJs.Group
|
||||
hidden: boolean
|
||||
} | undefined = undefined
|
||||
cameraGroup = new THREE.Mesh()
|
||||
objectOuterGroup = new THREE.Group()
|
||||
objectInnerGroup = new THREE.Group()
|
||||
camera: THREE.Group | THREE.PerspectiveCamera
|
||||
stopUpdate = false
|
||||
lastHeldItem: HandItemBlock | undefined
|
||||
toBeRenderedItem: HandItemBlock | undefined
|
||||
isSwinging = false
|
||||
nextIterStopCallbacks: Array<() => void> | undefined
|
||||
|
||||
constructor (public scene: THREE.Scene) {
|
||||
this.initCameraGroup()
|
||||
}
|
||||
|
||||
initCameraGroup () {
|
||||
this.cameraGroup = new THREE.Mesh()
|
||||
this.scene.add(this.cameraGroup)
|
||||
}
|
||||
|
||||
startSwing () {
|
||||
this.nextIterStopCallbacks = undefined // forget about cancelling
|
||||
if (this.isSwinging) return
|
||||
this.swingAnimation = new tweenJs.Group()
|
||||
this.isSwinging = true
|
||||
const cube = this.cameraGroup.children[0]
|
||||
if (cube) {
|
||||
// const DURATION = 1000 * 0.35 / 2
|
||||
const DURATION = 1000 * 0.35 / 3
|
||||
// const DURATION = 1000
|
||||
const initialPos = {
|
||||
x: this.objectInnerGroup.position.x,
|
||||
y: this.objectInnerGroup.position.y,
|
||||
z: this.objectInnerGroup.position.z
|
||||
}
|
||||
const initialRot = {
|
||||
x: this.objectInnerGroup.rotation.x,
|
||||
y: this.objectInnerGroup.rotation.y,
|
||||
z: this.objectInnerGroup.rotation.z
|
||||
}
|
||||
const mainAnim = new tweenJs.Tween(this.objectInnerGroup.position, this.swingAnimation).to({ y: this.objectInnerGroup.position.y - this.objectInnerGroup.scale.y / 2 }, DURATION).yoyo(true).repeat(Infinity).start()
|
||||
let i = 0
|
||||
mainAnim.onRepeat(() => {
|
||||
i++
|
||||
if (this.nextIterStopCallbacks && i % 2 === 0) {
|
||||
for (const callback of this.nextIterStopCallbacks) {
|
||||
callback()
|
||||
}
|
||||
this.nextIterStopCallbacks = undefined
|
||||
this.isSwinging = false
|
||||
this.swingAnimation!.removeAll()
|
||||
this.swingAnimation = undefined
|
||||
// todo refactor to be more generic for animations
|
||||
this.objectInnerGroup.position.set(initialPos.x, initialPos.y, initialPos.z)
|
||||
// this.objectInnerGroup.rotation.set(initialRot.x, initialRot.y, initialRot.z)
|
||||
Object.assign(this.objectInnerGroup.rotation, initialRot)
|
||||
}
|
||||
})
|
||||
|
||||
new tweenJs.Tween(this.objectInnerGroup.rotation, this.swingAnimation).to({ z: THREE.MathUtils.degToRad(90) }, DURATION).yoyo(true).repeat(Infinity).start()
|
||||
new tweenJs.Tween(this.objectInnerGroup.rotation, this.swingAnimation).to({ x: -THREE.MathUtils.degToRad(90) }, DURATION).yoyo(true).repeat(Infinity).start()
|
||||
}
|
||||
}
|
||||
|
||||
async stopSwing () {
|
||||
if (!this.isSwinging) return
|
||||
// might never resolve!
|
||||
/* return */void new Promise<void>((resolve) => {
|
||||
this.nextIterStopCallbacks ??= []
|
||||
this.nextIterStopCallbacks.push(() => {
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
update (camera: typeof this.camera) {
|
||||
this.camera = camera
|
||||
this.swingAnimation?.update()
|
||||
this.blockSwapAnimation?.tween.update()
|
||||
this.updateCameraGroup()
|
||||
}
|
||||
|
||||
// worldTest () {
|
||||
// const mesh = new THREE.Mesh(new THREE.BoxGeometry(1, 1, 1), new THREE.MeshPhongMaterial({ color: 0x00_00_ff, transparent: true, opacity: 0.5 }))
|
||||
// mesh.position.set(0.5, 0.5, 0.5)
|
||||
// const group = new THREE.Group()
|
||||
// group.add(mesh)
|
||||
// group.position.set(-0.5, -0.5, -0.5)
|
||||
// const outerGroup = new THREE.Group()
|
||||
// outerGroup.add(group)
|
||||
// outerGroup.position.set(this.camera.position.x, this.camera.position.y, this.camera.position.z)
|
||||
// this.scene.add(outerGroup)
|
||||
|
||||
// new tweenJs.Tween(group.rotation).to({ z: THREE.MathUtils.degToRad(90) }, 1000).yoyo(true).repeat(Infinity).start()
|
||||
// }
|
||||
|
||||
async playBlockSwapAnimation () {
|
||||
// if (this.blockSwapAnimation) return
|
||||
this.blockSwapAnimation ??= {
|
||||
tween: new tweenJs.Group(),
|
||||
hidden: false
|
||||
}
|
||||
const DURATION = 1000 * 0.35 / 2
|
||||
const tween = new tweenJs.Tween(this.objectInnerGroup.position, this.blockSwapAnimation.tween).to({
|
||||
y: this.objectInnerGroup.position.y + (this.objectInnerGroup.scale.y * 1.5 * (this.blockSwapAnimation.hidden ? 1 : -1))
|
||||
}, DURATION).start()
|
||||
return new Promise<void>((resolve) => {
|
||||
tween.onComplete(() => {
|
||||
if (this.blockSwapAnimation!.hidden) {
|
||||
this.blockSwapAnimation = undefined
|
||||
} else {
|
||||
this.blockSwapAnimation!.hidden = !this.blockSwapAnimation!.hidden
|
||||
}
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
isDifferentItem (block: HandItemBlock | undefined) {
|
||||
return this.lastHeldItem && (this.lastHeldItem.name !== block?.name || JSON.stringify(this.lastHeldItem.properties) !== JSON.stringify(block?.properties ?? '{}'))
|
||||
}
|
||||
|
||||
updateCameraGroup () {
|
||||
if (this.stopUpdate) return
|
||||
const { camera } = this
|
||||
this.cameraGroup.position.copy(camera.position)
|
||||
this.cameraGroup.rotation.copy(camera.rotation)
|
||||
|
||||
const viewerSize = viewer.renderer.getSize(new THREE.Vector2())
|
||||
// const x = window.x ?? 0.25 * viewerSize.width / viewerSize.height
|
||||
// const x = 0 * viewerSize.width / viewerSize.height
|
||||
const x = 0.2 * viewerSize.width / viewerSize.height
|
||||
this.objectOuterGroup.position.set(x, -0.3, -0.45)
|
||||
}
|
||||
|
||||
async initHandObject (material: THREE.Material, blockstatesModels: any, blocksAtlases: any, block?: HandItemBlock) {
|
||||
let animatingCurrent = false
|
||||
if (!this.swingAnimation && !this.blockSwapAnimation && this.isDifferentItem(block)) {
|
||||
animatingCurrent = true
|
||||
await this.playBlockSwapAnimation()
|
||||
this.holdingBlock?.removeFromParent()
|
||||
this.holdingBlock = undefined
|
||||
}
|
||||
this.lastHeldItem = block
|
||||
if (!block) {
|
||||
this.holdingBlock?.removeFromParent()
|
||||
this.holdingBlock = undefined
|
||||
this.swingAnimation = undefined
|
||||
this.blockSwapAnimation = undefined
|
||||
return
|
||||
}
|
||||
const blockProvider = worldBlockProvider(blockstatesModels, blocksAtlases, 'latest')
|
||||
const models = blockProvider.getAllResolvedModels0_1(block, true)
|
||||
const blockInner = getThreeBlockModelGroup(material, models, undefined, 'plains', loadedData)
|
||||
// const { mesh: itemMesh } = viewer.entities.getItemMesh({
|
||||
// itemId: 541,
|
||||
// })!
|
||||
// itemMesh.position.set(0.5, 0.5, 0.5)
|
||||
// const blockInner = itemMesh
|
||||
blockInner.name = 'holdingBlock'
|
||||
const blockOuterGroup = new THREE.Group()
|
||||
blockOuterGroup.add(blockInner)
|
||||
this.holdingBlock = blockInner
|
||||
this.objectInnerGroup = new THREE.Group()
|
||||
this.objectInnerGroup.add(blockOuterGroup)
|
||||
this.objectInnerGroup.position.set(-0.5, -0.5, -0.5)
|
||||
// todo cleanup
|
||||
if (animatingCurrent) {
|
||||
this.objectInnerGroup.position.y -= this.objectInnerGroup.scale.y * 1.5
|
||||
}
|
||||
Object.assign(blockOuterGroup.position, { x: 0.5, y: 0.5, z: 0.5 })
|
||||
|
||||
this.objectOuterGroup = new THREE.Group()
|
||||
this.objectOuterGroup.add(this.objectInnerGroup)
|
||||
|
||||
this.cameraGroup.add(this.objectOuterGroup)
|
||||
const rotation = -45 + -90
|
||||
// const rotation = -45 // should be for item
|
||||
this.holdingBlock.rotation.set(0, THREE.MathUtils.degToRad(rotation), 0, 'ZYX')
|
||||
|
||||
// const scale = window.scale ?? 0.2
|
||||
const scale = 0.2
|
||||
this.objectOuterGroup.scale.set(scale, scale, scale)
|
||||
// this.objectOuterGroup.position.set(x, window.y ?? -0.41, window.z ?? -0.45)
|
||||
// this.objectOuterGroup.position.set(x, 0, -0.45)
|
||||
|
||||
if (animatingCurrent) {
|
||||
await this.playBlockSwapAnimation()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,555 +0,0 @@
|
|||
import { Vec3 } from 'vec3'
|
||||
import worldBlockProvider, { WorldBlockProvider } from 'mc-assets/dist/worldBlockProvider'
|
||||
import legacyJson from '../../../../src/preflatMap.json'
|
||||
import { World, BlockModelPartsResolved, WorldBlock as Block } from './world'
|
||||
import { BlockElement, buildRotationMatrix, elemFaces, matmul3, matmulmat3, vecadd3, vecsub3 } from './modelsGeometryCommon'
|
||||
|
||||
let blockProvider: WorldBlockProvider
|
||||
|
||||
const tints: any = {}
|
||||
let needTiles = false
|
||||
|
||||
let tintsData
|
||||
try {
|
||||
tintsData = require('esbuild-data').tints
|
||||
} catch (err) {
|
||||
tintsData = require('minecraft-data/minecraft-data/data/pc/1.16.2/tints.json')
|
||||
}
|
||||
for (const key of Object.keys(tintsData)) {
|
||||
tints[key] = prepareTints(tintsData[key])
|
||||
}
|
||||
|
||||
function prepareTints (tints) {
|
||||
const map = new Map()
|
||||
const defaultValue = tintToGl(tints.default)
|
||||
for (let { keys, color } of tints.data) {
|
||||
color = tintToGl(color)
|
||||
for (const key of keys) {
|
||||
map.set(`${key}`, color)
|
||||
}
|
||||
}
|
||||
return new Proxy(map, {
|
||||
get (target, key) {
|
||||
return target.has(key) ? target.get(key) : defaultValue
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function mod (x: number, n: number) {
|
||||
return ((x % n) + n) % n
|
||||
}
|
||||
|
||||
const calculatedBlocksEntries = Object.entries(legacyJson.clientCalculatedBlocks)
|
||||
export function preflatBlockCalculation (block: Block, world: World, position: Vec3) {
|
||||
const type = calculatedBlocksEntries.find(([name, blocks]) => blocks.includes(block.name))?.[0]
|
||||
if (!type) return
|
||||
switch (type) {
|
||||
case 'directional': {
|
||||
const isSolidConnection = !block.name.includes('redstone') && !block.name.includes('tripwire')
|
||||
const neighbors = [
|
||||
world.getBlock(position.offset(0, 0, 1)),
|
||||
world.getBlock(position.offset(0, 0, -1)),
|
||||
world.getBlock(position.offset(1, 0, 0)),
|
||||
world.getBlock(position.offset(-1, 0, 0))
|
||||
]
|
||||
// set needed props to true: east:'false',north:'false',south:'false',west:'false'
|
||||
const props = {}
|
||||
for (const [i, neighbor] of neighbors.entries()) {
|
||||
const isConnectedToSolid = isSolidConnection ? (neighbor && !neighbor.transparent) : false
|
||||
if (isConnectedToSolid || neighbor?.name === block.name) {
|
||||
props[['south', 'north', 'east', 'west'][i]] = 'true'
|
||||
}
|
||||
}
|
||||
return props
|
||||
}
|
||||
// case 'gate_in_wall': {}
|
||||
case 'block_snowy': {
|
||||
const aboveIsSnow = world.getBlock(position.offset(0, 1, 0))?.name === 'snow'
|
||||
return {
|
||||
snowy: `${aboveIsSnow}`
|
||||
}
|
||||
}
|
||||
case 'door': {
|
||||
// upper half matches lower in
|
||||
const { half } = block.getProperties()
|
||||
if (half === 'upper') {
|
||||
// copy other properties
|
||||
const lower = world.getBlock(position.offset(0, -1, 0))
|
||||
if (lower?.name === block.name) {
|
||||
return {
|
||||
...lower.getProperties(),
|
||||
half: 'upper'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function tintToGl (tint) {
|
||||
const r = (tint >> 16) & 0xff
|
||||
const g = (tint >> 8) & 0xff
|
||||
const b = tint & 0xff
|
||||
return [r / 255, g / 255, b / 255]
|
||||
}
|
||||
|
||||
function getLiquidRenderHeight (world, block, type, pos) {
|
||||
if (!block || block.type !== type) return 1 / 9
|
||||
if (block.metadata === 0) { // source block
|
||||
const blockAbove = world.getBlock(pos.offset(0, 1, 0))
|
||||
if (blockAbove && blockAbove.type === type) return 1
|
||||
return 8 / 9
|
||||
}
|
||||
return ((block.metadata >= 8 ? 8 : 7 - block.metadata) + 1) / 9
|
||||
}
|
||||
|
||||
|
||||
const isCube = (block: Block) => {
|
||||
if (!block || block.transparent) return false
|
||||
if (block.isCube) return true
|
||||
if (!block.models?.length || block.models.length !== 1) return false
|
||||
// all variants
|
||||
return block.models[0].every(v => v.elements!.every(e => {
|
||||
return e.from[0] === 0 && e.from[1] === 0 && e.from[2] === 0 && e.to[0] === 16 && e.to[1] === 16 && e.to[2] === 16
|
||||
}))
|
||||
}
|
||||
|
||||
function renderLiquid (world: World, cursor: Vec3, texture: any | undefined, type: number, biome: string, water: boolean, attr: Record<string, any>) {
|
||||
const heights: number[] = []
|
||||
for (let z = -1; z <= 1; z++) {
|
||||
for (let x = -1; x <= 1; x++) {
|
||||
const pos = cursor.offset(x, 0, z)
|
||||
heights.push(getLiquidRenderHeight(world, world.getBlock(pos), type, pos))
|
||||
}
|
||||
}
|
||||
const cornerHeights = [
|
||||
Math.max(Math.max(heights[0], heights[1]), Math.max(heights[3], heights[4])),
|
||||
Math.max(Math.max(heights[1], heights[2]), Math.max(heights[4], heights[5])),
|
||||
Math.max(Math.max(heights[3], heights[4]), Math.max(heights[6], heights[7])),
|
||||
Math.max(Math.max(heights[4], heights[5]), Math.max(heights[7], heights[8]))
|
||||
]
|
||||
|
||||
// eslint-disable-next-line guard-for-in
|
||||
for (const face in elemFaces) {
|
||||
const { dir, corners } = elemFaces[face]
|
||||
const isUp = dir[1] === 1
|
||||
|
||||
const neighborPos = cursor.offset(...dir as [number, number, number])
|
||||
const neighbor = world.getBlock(neighborPos)
|
||||
if (!neighbor) continue
|
||||
if (neighbor.type === type) continue
|
||||
const isGlass = neighbor.name.includes('glass')
|
||||
if ((isCube(neighbor) && !isUp) || neighbor.getProperties().waterlogged) continue
|
||||
|
||||
let tint = [1, 1, 1]
|
||||
if (water) {
|
||||
let m = 1 // Fake lighting to improve lisibility
|
||||
if (Math.abs(dir[0]) > 0) m = 0.6
|
||||
else if (Math.abs(dir[2]) > 0) m = 0.8
|
||||
tint = tints.water[biome]
|
||||
tint = [tint[0] * m, tint[1] * m, tint[2] * m]
|
||||
}
|
||||
|
||||
if (needTiles) {
|
||||
attr.tiles[`${cursor.x},${cursor.y},${cursor.z}`] ??= {
|
||||
block: 'water',
|
||||
faces: [],
|
||||
}
|
||||
attr.tiles[`${cursor.x},${cursor.y},${cursor.z}`].faces.push({
|
||||
face,
|
||||
neighbor: `${neighborPos.x},${neighborPos.y},${neighborPos.z}`,
|
||||
// texture: eFace.texture.name,
|
||||
})
|
||||
}
|
||||
|
||||
const { u } = texture
|
||||
const { v } = texture
|
||||
const { su } = texture
|
||||
const { sv } = texture
|
||||
|
||||
for (const pos of corners) {
|
||||
const height = cornerHeights[pos[2] * 2 + pos[0]]
|
||||
attr.t_positions.push(
|
||||
(pos[0] ? 0.999 : 0.001) + (cursor.x & 15) - 8,
|
||||
(pos[1] ? height - 0.001 : 0.001) + (cursor.y & 15) - 8,
|
||||
(pos[2] ? 0.999 : 0.001) + (cursor.z & 15) - 8
|
||||
)
|
||||
attr.t_normals.push(...dir)
|
||||
attr.t_uvs.push(pos[3] * su + u, pos[4] * sv * (pos[1] ? 1 : height) + v)
|
||||
attr.t_colors.push(tint[0], tint[1], tint[2])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let needRecompute = false
|
||||
|
||||
function renderElement (world: World, cursor: Vec3, element: BlockElement, doAO: boolean, attr: Record<string, any>, globalMatrix: any, globalShift: any, block: Block, biome: string) {
|
||||
const position = cursor
|
||||
// const key = `${position.x},${position.y},${position.z}`
|
||||
// if (!globalThis.allowedBlocks.includes(key)) return
|
||||
const cullIfIdentical = block.name.includes('glass')
|
||||
|
||||
// eslint-disable-next-line guard-for-in
|
||||
for (const face in element.faces) {
|
||||
const eFace = element.faces[face]
|
||||
const { corners, mask1, mask2 } = elemFaces[face]
|
||||
const dir = matmul3(globalMatrix, elemFaces[face].dir)
|
||||
|
||||
if (eFace.cullface) {
|
||||
const neighbor = world.getBlock(cursor.plus(new Vec3(...dir)))
|
||||
if (neighbor) {
|
||||
if (cullIfIdentical && neighbor.type === block.type) continue
|
||||
if (!neighbor.transparent && isCube(neighbor)) continue
|
||||
} else {
|
||||
needRecompute = true
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
const minx = element.from[0]
|
||||
const miny = element.from[1]
|
||||
const minz = element.from[2]
|
||||
const maxx = element.to[0]
|
||||
const maxy = element.to[1]
|
||||
const maxz = element.to[2]
|
||||
|
||||
const texture = eFace.texture as any
|
||||
const { u, v, su, sv } = texture
|
||||
|
||||
const ndx = Math.floor(attr.positions.length / 3)
|
||||
|
||||
let tint = [1, 1, 1]
|
||||
if (eFace.tintindex !== undefined) {
|
||||
if (eFace.tintindex === 0) {
|
||||
if (block.name === 'redstone_wire') {
|
||||
tint = tints.redstone[`${block.getProperties().power}`]
|
||||
} else if (block.name === 'birch_leaves' ||
|
||||
block.name === 'spruce_leaves' ||
|
||||
block.name === 'lily_pad') {
|
||||
tint = tints.constant[block.name]
|
||||
} else if (block.name.includes('leaves') || block.name === 'vine') {
|
||||
tint = tints.foliage[biome]
|
||||
} else {
|
||||
tint = tints.grass[biome]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// UV rotation
|
||||
let r = eFace.rotation || 0
|
||||
if (face === 'down') {
|
||||
r += 180
|
||||
}
|
||||
const uvcs = Math.cos(r * Math.PI / 180)
|
||||
const uvsn = -Math.sin(r * Math.PI / 180)
|
||||
|
||||
let localMatrix = null as any
|
||||
let localShift = null as any
|
||||
|
||||
if (element.rotation) {
|
||||
// todo do we support rescale?
|
||||
localMatrix = buildRotationMatrix(
|
||||
element.rotation.axis,
|
||||
element.rotation.angle
|
||||
)
|
||||
|
||||
localShift = vecsub3(
|
||||
element.rotation.origin,
|
||||
matmul3(
|
||||
localMatrix,
|
||||
element.rotation.origin
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
const aos: number[] = []
|
||||
const neighborPos = position.plus(new Vec3(...dir))
|
||||
const baseLight = world.getLight(neighborPos, undefined, undefined, block.name) / 15
|
||||
for (const pos of corners) {
|
||||
let vertex = [
|
||||
(pos[0] ? maxx : minx),
|
||||
(pos[1] ? maxy : miny),
|
||||
(pos[2] ? maxz : minz)
|
||||
]
|
||||
|
||||
vertex = vecadd3(matmul3(localMatrix, vertex), localShift)
|
||||
vertex = vecadd3(matmul3(globalMatrix, vertex), globalShift)
|
||||
vertex = vertex.map(v => v / 16)
|
||||
|
||||
attr.positions.push(
|
||||
vertex[0] + (cursor.x & 15) - 8,
|
||||
vertex[1] + (cursor.y & 15) - 8,
|
||||
vertex[2] + (cursor.z & 15) - 8
|
||||
)
|
||||
|
||||
attr.normals.push(...dir)
|
||||
|
||||
const baseu = (pos[3] - 0.5) * uvcs - (pos[4] - 0.5) * uvsn + 0.5
|
||||
const basev = (pos[3] - 0.5) * uvsn + (pos[4] - 0.5) * uvcs + 0.5
|
||||
attr.uvs.push(baseu * su + u, basev * sv + v)
|
||||
|
||||
let light = 1
|
||||
if (doAO) {
|
||||
const dx = pos[0] * 2 - 1
|
||||
const dy = pos[1] * 2 - 1
|
||||
const dz = pos[2] * 2 - 1
|
||||
const cornerDir = matmul3(globalMatrix, [dx, dy, dz])
|
||||
const side1Dir = matmul3(globalMatrix, [dx * mask1[0], dy * mask1[1], dz * mask1[2]])
|
||||
const side2Dir = matmul3(globalMatrix, [dx * mask2[0], dy * mask2[1], dz * mask2[2]])
|
||||
const side1 = world.getBlock(cursor.offset(...side1Dir))
|
||||
const side2 = world.getBlock(cursor.offset(...side2Dir))
|
||||
const corner = world.getBlock(cursor.offset(...cornerDir))
|
||||
|
||||
let cornerLightResult = 15
|
||||
// eslint-disable-next-line no-constant-condition, sonarjs/no-gratuitous-expressions
|
||||
if (/* world.config.smoothLighting */false) { // todo fix
|
||||
const side1Light = world.getLight(cursor.plus(new Vec3(...side1Dir)), true)
|
||||
const side2Light = world.getLight(cursor.plus(new Vec3(...side2Dir)), true)
|
||||
const cornerLight = world.getLight(cursor.plus(new Vec3(...cornerDir)), true)
|
||||
// interpolate
|
||||
cornerLightResult = (side1Light + side2Light + cornerLight) / 3
|
||||
}
|
||||
|
||||
const side1Block = world.shouldMakeAo(side1) ? 1 : 0
|
||||
const side2Block = world.shouldMakeAo(side2) ? 1 : 0
|
||||
const cornerBlock = world.shouldMakeAo(corner) ? 1 : 0
|
||||
|
||||
// TODO: correctly interpolate ao light based on pos (evaluate once for each corner of the block)
|
||||
|
||||
const ao = (side1Block && side2Block) ? 0 : (3 - (side1Block + side2Block + cornerBlock))
|
||||
// todo light should go upper on lower blocks
|
||||
light = (ao + 1) / 4 * (cornerLightResult / 15)
|
||||
aos.push(ao)
|
||||
}
|
||||
|
||||
attr.colors.push(baseLight * tint[0] * light, baseLight * tint[1] * light, baseLight * tint[2] * light)
|
||||
}
|
||||
|
||||
if (needTiles) {
|
||||
attr.tiles[`${cursor.x},${cursor.y},${cursor.z}`] ??= {
|
||||
block: block.name,
|
||||
faces: [],
|
||||
}
|
||||
attr.tiles[`${cursor.x},${cursor.y},${cursor.z}`].faces.push({
|
||||
face,
|
||||
neighbor: `${neighborPos.x},${neighborPos.y},${neighborPos.z}`,
|
||||
light: baseLight
|
||||
// texture: eFace.texture.name,
|
||||
})
|
||||
}
|
||||
|
||||
if (doAO && aos[0] + aos[3] >= aos[1] + aos[2]) {
|
||||
attr.indices.push(
|
||||
// eslint-disable-next-line @stylistic/function-call-argument-newline
|
||||
ndx, ndx + 3, ndx + 2,
|
||||
ndx, ndx + 1, ndx + 3
|
||||
)
|
||||
} else {
|
||||
attr.indices.push(
|
||||
// eslint-disable-next-line @stylistic/function-call-argument-newline
|
||||
ndx, ndx + 1, ndx + 2,
|
||||
ndx + 2, ndx + 1, ndx + 3
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const makeLooseObj = <T extends string> (obj: Record<T, any>) => obj
|
||||
|
||||
const invisibleBlocks = new Set(['air', 'cave_air', 'void_air', 'barrier'])
|
||||
|
||||
const isBlockWaterlogged = (block: Block) => block.getProperties().waterlogged === true || block.getProperties().waterlogged === 'true'
|
||||
|
||||
let unknownBlockModel: BlockModelPartsResolved
|
||||
let erroredBlockModel: BlockModelPartsResolved
|
||||
export function getSectionGeometry (sx, sy, sz, world: World) {
|
||||
let delayedRender = [] as Array<() => void>
|
||||
|
||||
const attr = makeLooseObj({
|
||||
sx: sx + 8,
|
||||
sy: sy + 8,
|
||||
sz: sz + 8,
|
||||
positions: [],
|
||||
normals: [],
|
||||
colors: [],
|
||||
uvs: [],
|
||||
t_positions: [],
|
||||
t_normals: [],
|
||||
t_colors: [],
|
||||
t_uvs: [],
|
||||
indices: [],
|
||||
tiles: {},
|
||||
// todo this can be removed here
|
||||
signs: {},
|
||||
highestBlocks: {},
|
||||
hadErrors: false
|
||||
} as Record<string, any>)
|
||||
|
||||
const cursor = new Vec3(0, 0, 0)
|
||||
for (cursor.y = sy; cursor.y < sy + 16; cursor.y++) {
|
||||
for (cursor.z = sz; cursor.z < sz + 16; cursor.z++) {
|
||||
for (cursor.x = sx; cursor.x < sx + 16; cursor.x++) {
|
||||
const block = world.getBlock(cursor)!
|
||||
if (!invisibleBlocks.has(block.name)) {
|
||||
const highest = attr.highestBlocks[`${cursor.x},${cursor.z}`]
|
||||
if (!highest || highest.y < cursor.y) {
|
||||
attr.highestBlocks[`${cursor.x},${cursor.z}`] = {
|
||||
y: cursor.y,
|
||||
name: block.name
|
||||
}
|
||||
}
|
||||
}
|
||||
if (invisibleBlocks.has(block.name)) continue
|
||||
if (block.name.includes('_sign') || block.name === 'sign') {
|
||||
const key = `${cursor.x},${cursor.y},${cursor.z}`
|
||||
const props: any = block.getProperties()
|
||||
const facingRotationMap = {
|
||||
'north': 2,
|
||||
'south': 0,
|
||||
'west': 1,
|
||||
'east': 3
|
||||
}
|
||||
const isWall = block.name.endsWith('wall_sign') || block.name.endsWith('wall_hanging_sign')
|
||||
const isHanging = block.name.endsWith('hanging_sign')
|
||||
attr.signs[key] = {
|
||||
isWall,
|
||||
isHanging,
|
||||
rotation: isWall ? facingRotationMap[props.facing] : +props.rotation
|
||||
}
|
||||
}
|
||||
const biome = block.biome.name
|
||||
|
||||
let preflatRecomputeVariant = !!(block as any)._originalProperties
|
||||
if (world.preflat) {
|
||||
const patchProperties = preflatBlockCalculation(block, world, cursor)
|
||||
if (patchProperties) {
|
||||
//@ts-expect-error
|
||||
block._originalProperties ??= block._properties
|
||||
//@ts-expect-error
|
||||
block._properties = { ...block._originalProperties, ...patchProperties }
|
||||
preflatRecomputeVariant = true
|
||||
} else {
|
||||
//@ts-expect-error
|
||||
block._properties = block._originalProperties ?? block._properties
|
||||
//@ts-expect-error
|
||||
block._originalProperties = undefined
|
||||
}
|
||||
}
|
||||
|
||||
const isWaterlogged = isBlockWaterlogged(block)
|
||||
if (block.name === 'water' || isWaterlogged) {
|
||||
const pos = cursor.clone()
|
||||
// eslint-disable-next-line @typescript-eslint/no-loop-func
|
||||
delayedRender.push(() => {
|
||||
renderLiquid(world, pos, blockProvider.getTextureInfo('water_still'), block.type, biome, true, attr)
|
||||
})
|
||||
} else if (block.name === 'lava') {
|
||||
renderLiquid(world, cursor, blockProvider.getTextureInfo('lava_still'), block.type, biome, false, attr)
|
||||
}
|
||||
if (block.name !== 'water' && block.name !== 'lava' && !invisibleBlocks.has(block.name)) {
|
||||
// cache
|
||||
let { models } = block
|
||||
if (block.models === undefined || preflatRecomputeVariant) {
|
||||
try {
|
||||
models = blockProvider.getAllResolvedModels0_1({
|
||||
name: block.name,
|
||||
properties: block.getProperties(),
|
||||
})!
|
||||
if (!models.length) models = null
|
||||
} catch (err) {
|
||||
models ??= erroredBlockModel
|
||||
console.error(`Critical assets error. Unable to get block model for ${block.name}[${JSON.stringify(block.getProperties())}]: ` + err.message, err.stack)
|
||||
attr.hadErrors = true
|
||||
}
|
||||
}
|
||||
block.models = models ?? null
|
||||
|
||||
models ??= unknownBlockModel
|
||||
|
||||
const firstForceVar = world.config.debugModelVariant?.[0]
|
||||
let part = 0
|
||||
for (const modelVars of models ?? []) {
|
||||
const pos = cursor.clone()
|
||||
// const variantRuntime = mod(Math.floor(pos.x / 16) + Math.floor(pos.y / 16) + Math.floor(pos.z / 16), modelVars.length)
|
||||
const variantRuntime = 0
|
||||
const useVariant = world.config.debugModelVariant?.[part] ?? firstForceVar ?? variantRuntime
|
||||
part++
|
||||
const model = modelVars[useVariant] ?? modelVars[0]
|
||||
if (!model) continue
|
||||
|
||||
let globalMatrix = null as any
|
||||
let globalShift = null as any
|
||||
for (const axis of ['x', 'y', 'z'] as const) {
|
||||
if (axis in model) {
|
||||
globalMatrix = globalMatrix ?
|
||||
matmulmat3(globalMatrix, buildRotationMatrix(axis, -(model[axis] ?? 0))) :
|
||||
buildRotationMatrix(axis, -(model[axis] ?? 0))
|
||||
}
|
||||
}
|
||||
if (globalMatrix) {
|
||||
globalShift = [8, 8, 8]
|
||||
globalShift = vecsub3(globalShift, matmul3(globalMatrix, globalShift))
|
||||
}
|
||||
|
||||
for (const element of model.elements ?? []) {
|
||||
const ao = model.ao ?? true
|
||||
if (block.transparent) {
|
||||
const pos = cursor.clone()
|
||||
delayedRender.push(() => {
|
||||
renderElement(world, pos, element, ao, attr, globalMatrix, globalShift, block, biome)
|
||||
})
|
||||
} else {
|
||||
renderElement(world, cursor, element, ao, attr, globalMatrix, globalShift, block, biome)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const render of delayedRender) {
|
||||
render()
|
||||
}
|
||||
delayedRender = []
|
||||
|
||||
let ndx = attr.positions.length / 3
|
||||
for (let i = 0; i < attr.t_positions.length / 12; i++) {
|
||||
attr.indices.push(
|
||||
ndx, ndx + 1, ndx + 2, ndx + 2, ndx + 1, ndx + 3,
|
||||
// eslint-disable-next-line @stylistic/function-call-argument-newline
|
||||
// back face
|
||||
ndx, ndx + 2, ndx + 1, ndx + 2, ndx + 3, ndx + 1
|
||||
)
|
||||
ndx += 4
|
||||
}
|
||||
|
||||
attr.positions.push(...attr.t_positions)
|
||||
attr.normals.push(...attr.t_normals)
|
||||
attr.colors.push(...attr.t_colors)
|
||||
attr.uvs.push(...attr.t_uvs)
|
||||
|
||||
delete attr.t_positions
|
||||
delete attr.t_normals
|
||||
delete attr.t_colors
|
||||
delete attr.t_uvs
|
||||
|
||||
attr.positions = new Float32Array(attr.positions) as any
|
||||
attr.normals = new Float32Array(attr.normals) as any
|
||||
attr.colors = new Float32Array(attr.colors) as any
|
||||
attr.uvs = new Float32Array(attr.uvs) as any
|
||||
|
||||
return attr
|
||||
}
|
||||
|
||||
export const setBlockStatesData = (blockstatesModels, blocksAtlas: any, _needTiles = false, useUnknownBlockModel = true) => {
|
||||
blockProvider = worldBlockProvider(blockstatesModels, blocksAtlas, 'latest')
|
||||
globalThis.blockProvider = blockProvider
|
||||
if (useUnknownBlockModel) {
|
||||
unknownBlockModel = blockProvider.getAllResolvedModels0_1({ name: 'unknown', properties: {} })
|
||||
erroredBlockModel = blockProvider.getAllResolvedModels0_1({ name: 'errored', properties: {} })
|
||||
}
|
||||
|
||||
needTiles = _needTiles
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
export const defaultMesherConfig = {
|
||||
version: '',
|
||||
enableLighting: true,
|
||||
skyLight: 15,
|
||||
smoothLighting: true,
|
||||
outputFormat: 'threeJs' as 'threeJs' | 'webgl',
|
||||
textureSize: 1024, // for testing
|
||||
debugModelVariant: undefined as undefined | number[]
|
||||
}
|
||||
|
||||
export type MesherConfig = typeof defaultMesherConfig
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
import { setup } from './mesherTester'
|
||||
|
||||
const addPositions = [
|
||||
// [[0, 0, 0], 'diamond_block'],
|
||||
[[1, 0, 0], 'stone'],
|
||||
[[-1, 0, 0], 'stone'],
|
||||
[[0, 1, 0], 'stone'],
|
||||
[[0, -1, 0], 'stone'],
|
||||
[[0, 0, 1], 'stone'],
|
||||
[[0, 0, -1], 'stone'],
|
||||
] as const
|
||||
|
||||
const { mesherWorld, getGeometry, pos, mcData } = setup('1.18.1', addPositions as any)
|
||||
|
||||
// mesherWorld.setBlockStateId(pos, mcData.blocksByName.soul_sand.defaultState)
|
||||
|
||||
// console.log(getGeometry().centerTileNeighbors)
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
import { fromFormattedString } from '@xmcl/text-component'
|
||||
|
||||
export const formattedStringToSimpleString = (str) => {
|
||||
const result = fromFormattedString(str)
|
||||
str = result.text
|
||||
// todo recursive
|
||||
for (const extra of result.extra) {
|
||||
str += extra.text
|
||||
}
|
||||
return str
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
import * as THREE from 'three'
|
||||
|
||||
export const disposeObject = (obj: THREE.Object3D) => {
|
||||
// not cleaning texture there as it might be used by other objects, but would be good to also do that
|
||||
if (obj instanceof THREE.Mesh) {
|
||||
obj.geometry?.dispose?.()
|
||||
obj.material?.dispose?.()
|
||||
}
|
||||
if (obj.children) {
|
||||
// eslint-disable-next-line unicorn/no-array-for-each
|
||||
obj.children.forEach(disposeObject)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
const path = require('path')
|
||||
const THREE = require('three')
|
||||
|
||||
const textureCache = {}
|
||||
function loadTexture(texture, cb) {
|
||||
if (!textureCache[texture]) {
|
||||
const url = path.resolve(__dirname, '../../public/' + texture)
|
||||
textureCache[texture] = new THREE.TextureLoader().load(url)
|
||||
}
|
||||
cb(textureCache[texture])
|
||||
}
|
||||
|
||||
function loadJSON(json, cb) {
|
||||
cb(require(path.resolve(__dirname, '../../public/' + json)))
|
||||
}
|
||||
|
||||
module.exports = { loadTexture, loadJSON }
|
||||
|
|
@ -1,57 +0,0 @@
|
|||
function safeRequire(path) {
|
||||
try {
|
||||
return require(path)
|
||||
} catch (e) {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
const { loadImage } = safeRequire('node-canvas-webgl/lib')
|
||||
const path = require('path')
|
||||
const THREE = require('three')
|
||||
|
||||
const textureCache = {}
|
||||
// todo not ideal, export different functions for browser and node
|
||||
export function loadTexture(texture, cb) {
|
||||
if (process.platform === 'browser') {
|
||||
return require('./utils.web').loadTexture(texture, cb)
|
||||
}
|
||||
|
||||
if (textureCache[texture]) {
|
||||
cb(textureCache[texture])
|
||||
} else {
|
||||
loadImage(path.resolve(__dirname, '../../public/' + texture)).then(image => {
|
||||
textureCache[texture] = new THREE.CanvasTexture(image)
|
||||
cb(textureCache[texture])
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export function loadJSON(json, cb) {
|
||||
if (process.platform === 'browser') {
|
||||
return require('./utils.web').loadJSON(json, cb)
|
||||
}
|
||||
cb(require(path.resolve(__dirname, '../../public/' + json)))
|
||||
}
|
||||
|
||||
export const loadScript = async function (/** @type {string} */scriptSrc) {
|
||||
if (document.querySelector(`script[src="${scriptSrc}"]`)) {
|
||||
return
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const scriptElement = document.createElement('script')
|
||||
scriptElement.src = scriptSrc
|
||||
scriptElement.async = true
|
||||
|
||||
scriptElement.addEventListener('load', () => {
|
||||
resolve(scriptElement)
|
||||
})
|
||||
|
||||
scriptElement.onerror = (error) => {
|
||||
reject(new Error(error.message))
|
||||
scriptElement.remove()
|
||||
}
|
||||
|
||||
document.head.appendChild(scriptElement)
|
||||
})
|
||||
}
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
/* global XMLHttpRequest */
|
||||
const THREE = require('three')
|
||||
|
||||
const textureCache = {}
|
||||
function loadTexture(texture, cb, onLoad) {
|
||||
const cached = textureCache[texture]
|
||||
if (!cached) {
|
||||
textureCache[texture] = new THREE.TextureLoader().load(texture, onLoad)
|
||||
}
|
||||
cb(textureCache[texture])
|
||||
if (cached) onLoad?.()
|
||||
}
|
||||
|
||||
function loadJSON(url, callback) {
|
||||
const xhr = new XMLHttpRequest()
|
||||
xhr.open('GET', url, true)
|
||||
xhr.responseType = 'json'
|
||||
xhr.onload = function () {
|
||||
const { status } = xhr
|
||||
if (status === 200) {
|
||||
callback(xhr.response)
|
||||
} else {
|
||||
throw new Error(url + ' not found')
|
||||
}
|
||||
}
|
||||
xhr.send()
|
||||
}
|
||||
|
||||
module.exports = { loadTexture, loadJSON }
|
||||
|
|
@ -1,279 +0,0 @@
|
|||
import EventEmitter from 'events'
|
||||
import * as THREE from 'three'
|
||||
import { Vec3 } from 'vec3'
|
||||
import { generateSpiralMatrix } from 'flying-squid/dist/utils'
|
||||
import worldBlockProvider from 'mc-assets/dist/worldBlockProvider'
|
||||
import { Entities } from './entities'
|
||||
import { Primitives } from './primitives'
|
||||
import { WorldRendererThree } from './worldrendererThree'
|
||||
import { WorldRendererCommon, WorldRendererConfig, defaultWorldRendererConfig } from './worldrendererCommon'
|
||||
import { getThreeBlockModelGroup, renderBlockThree, setBlockPosition } from './mesher/standaloneRenderer'
|
||||
|
||||
export class Viewer {
|
||||
scene: THREE.Scene
|
||||
ambientLight: THREE.AmbientLight
|
||||
directionalLight: THREE.DirectionalLight
|
||||
world: WorldRendererCommon
|
||||
entities: Entities
|
||||
// primitives: Primitives
|
||||
domElement: HTMLCanvasElement
|
||||
playerHeight = 1.62
|
||||
isSneaking = false
|
||||
threeJsWorld: WorldRendererThree
|
||||
cameraObjectOverride?: THREE.Object3D // for xr
|
||||
audioListener: THREE.AudioListener
|
||||
renderingUntilNoUpdates = false
|
||||
processEntityOverrides = (e, overrides) => overrides
|
||||
|
||||
get camera () {
|
||||
return this.world.camera
|
||||
}
|
||||
|
||||
set camera (camera) {
|
||||
this.world.camera = camera
|
||||
}
|
||||
|
||||
constructor (public renderer: THREE.WebGLRenderer, worldConfig = defaultWorldRendererConfig) {
|
||||
// https://discourse.threejs.org/t/updates-to-color-management-in-three-js-r152/50791
|
||||
THREE.ColorManagement.enabled = false
|
||||
renderer.outputColorSpace = THREE.LinearSRGBColorSpace
|
||||
|
||||
this.scene = new THREE.Scene()
|
||||
this.scene.matrixAutoUpdate = false // for perf
|
||||
this.threeJsWorld = new WorldRendererThree(this.scene, this.renderer, worldConfig)
|
||||
this.setWorld()
|
||||
this.resetScene()
|
||||
this.entities = new Entities(this.scene)
|
||||
// this.primitives = new Primitives(this.scene, this.camera)
|
||||
|
||||
this.domElement = renderer.domElement
|
||||
}
|
||||
|
||||
setWorld () {
|
||||
this.world = this.threeJsWorld
|
||||
}
|
||||
|
||||
resetScene () {
|
||||
this.scene.background = new THREE.Color('lightblue')
|
||||
|
||||
if (this.ambientLight) this.scene.remove(this.ambientLight)
|
||||
this.ambientLight = new THREE.AmbientLight(0xcc_cc_cc)
|
||||
this.scene.add(this.ambientLight)
|
||||
|
||||
if (this.directionalLight) this.scene.remove(this.directionalLight)
|
||||
this.directionalLight = new THREE.DirectionalLight(0xff_ff_ff, 0.5)
|
||||
this.directionalLight.position.set(1, 1, 0.5).normalize()
|
||||
this.directionalLight.castShadow = true
|
||||
this.scene.add(this.directionalLight)
|
||||
|
||||
const size = this.renderer.getSize(new THREE.Vector2())
|
||||
this.camera = new THREE.PerspectiveCamera(75, size.x / size.y, 0.1, 1000)
|
||||
}
|
||||
|
||||
resetAll () {
|
||||
this.resetScene()
|
||||
this.world.resetWorld()
|
||||
this.entities.clear()
|
||||
// this.primitives.clear()
|
||||
}
|
||||
|
||||
setVersion (userVersion: string, texturesVersion = userVersion) {
|
||||
console.log('[viewer] Using version:', userVersion, 'textures:', texturesVersion)
|
||||
void this.world.setVersion(userVersion, texturesVersion).then(async () => {
|
||||
return new THREE.TextureLoader().loadAsync(this.world.itemsAtlasParser!.latestImage)
|
||||
}).then((texture) => {
|
||||
this.entities.itemsTexture = texture
|
||||
})
|
||||
this.entities.clear()
|
||||
// this.primitives.clear()
|
||||
}
|
||||
|
||||
addColumn (x, z, chunk, isLightUpdate = false) {
|
||||
this.world.addColumn(x, z, chunk, isLightUpdate)
|
||||
}
|
||||
|
||||
removeColumn (x: string, z: string) {
|
||||
this.world.removeColumn(x, z)
|
||||
}
|
||||
|
||||
setBlockStateId (pos: Vec3, stateId: number) {
|
||||
this.world.setBlockStateId(pos, stateId)
|
||||
}
|
||||
|
||||
demoModel () {
|
||||
//@ts-expect-error
|
||||
const pos = cursorBlockRel(0, 1, 0).position
|
||||
const blockProvider = worldBlockProvider(this.world.blockstatesModels, this.world.blocksAtlases, 'latest')
|
||||
const models = blockProvider.getAllResolvedModels0_1({
|
||||
name: 'furnace',
|
||||
properties: {
|
||||
// map: false
|
||||
}
|
||||
}, true)
|
||||
const { material } = this.world
|
||||
const mesh = getThreeBlockModelGroup(material, models, undefined, 'plains', loadedData)
|
||||
// mesh.rotation.y = THREE.MathUtils.degToRad(90)
|
||||
setBlockPosition(mesh, pos)
|
||||
const helper = new THREE.BoxHelper(mesh, 0xff_ff_00)
|
||||
mesh.add(helper)
|
||||
this.scene.add(mesh)
|
||||
}
|
||||
|
||||
demoItem () {
|
||||
//@ts-expect-error
|
||||
const pos = cursorBlockRel(0, 1, 0).position
|
||||
const { mesh } = this.entities.getItemMesh({
|
||||
itemId: 541,
|
||||
})!
|
||||
mesh.position.set(pos.x + 0.5, pos.y + 0.5, pos.z + 0.5)
|
||||
// mesh.scale.set(0.5, 0.5, 0.5)
|
||||
const helper = new THREE.BoxHelper(mesh, 0xff_ff_00)
|
||||
mesh.add(helper)
|
||||
this.scene.add(mesh)
|
||||
}
|
||||
|
||||
updateEntity (e) {
|
||||
this.entities.update(e, this.processEntityOverrides(e, {
|
||||
rotation: {
|
||||
head: {
|
||||
x: e.headPitch ?? e.pitch,
|
||||
y: e.headYaw,
|
||||
z: 0
|
||||
}
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
setFirstPersonCamera (pos: Vec3 | null, yaw: number, pitch: number, roll = 0) {
|
||||
const cam = this.cameraObjectOverride || this.camera
|
||||
let yOffset = this.playerHeight
|
||||
if (this.isSneaking) yOffset -= 0.3
|
||||
|
||||
if (this.world instanceof WorldRendererThree) {
|
||||
this.world.camera = cam as THREE.PerspectiveCamera
|
||||
}
|
||||
this.world.updateCamera(pos?.offset(0, yOffset, 0) ?? null, yaw, pitch)
|
||||
}
|
||||
|
||||
playSound (position: Vec3, path: string, volume = 1, pitch = 1) {
|
||||
if (!this.audioListener) {
|
||||
this.audioListener = new THREE.AudioListener()
|
||||
this.camera.add(this.audioListener)
|
||||
}
|
||||
|
||||
const sound = new THREE.PositionalAudio(this.audioListener)
|
||||
|
||||
const audioLoader = new THREE.AudioLoader()
|
||||
const start = Date.now()
|
||||
void audioLoader.loadAsync(path).then((buffer) => {
|
||||
if (Date.now() - start > 500) return
|
||||
// play
|
||||
sound.setBuffer(buffer)
|
||||
sound.setRefDistance(20)
|
||||
sound.setVolume(volume)
|
||||
sound.setPlaybackRate(pitch) // set the pitch
|
||||
this.scene.add(sound)
|
||||
// set sound position
|
||||
sound.position.set(position.x, position.y, position.z)
|
||||
sound.onEnded = () => {
|
||||
this.scene.remove(sound)
|
||||
sound.disconnect()
|
||||
audioLoader.manager.itemEnd(path)
|
||||
}
|
||||
sound.play()
|
||||
})
|
||||
}
|
||||
|
||||
addChunksBatchWaitTime = 200
|
||||
|
||||
// todo type
|
||||
listen (emitter: EventEmitter) {
|
||||
emitter.on('entity', (e) => {
|
||||
this.updateEntity(e)
|
||||
})
|
||||
|
||||
emitter.on('primitive', (p) => {
|
||||
// this.updatePrimitive(p)
|
||||
})
|
||||
|
||||
let currentLoadChunkBatch = null as {
|
||||
timeout
|
||||
data
|
||||
} | null
|
||||
emitter.on('loadChunk', ({ x, z, chunk, worldConfig, isLightUpdate }) => {
|
||||
this.world.worldConfig = worldConfig
|
||||
if (!currentLoadChunkBatch) {
|
||||
// add a setting to use debounce instead
|
||||
currentLoadChunkBatch = {
|
||||
data: [],
|
||||
timeout: setTimeout(() => {
|
||||
for (const args of currentLoadChunkBatch!.data) {
|
||||
//@ts-expect-error
|
||||
this.addColumn(...args)
|
||||
}
|
||||
currentLoadChunkBatch = null
|
||||
}, this.addChunksBatchWaitTime)
|
||||
}
|
||||
}
|
||||
currentLoadChunkBatch.data.push([x, z, chunk, isLightUpdate])
|
||||
})
|
||||
// todo remove and use other architecture instead so data flow is clear
|
||||
emitter.on('blockEntities', (blockEntities) => {
|
||||
if (this.world instanceof WorldRendererThree) this.world.blockEntities = blockEntities
|
||||
})
|
||||
|
||||
emitter.on('unloadChunk', ({ x, z }) => {
|
||||
this.removeColumn(x, z)
|
||||
})
|
||||
|
||||
emitter.on('blockUpdate', ({ pos, stateId }) => {
|
||||
this.setBlockStateId(new Vec3(pos.x, pos.y, pos.z), stateId)
|
||||
})
|
||||
|
||||
emitter.on('chunkPosUpdate', ({ pos }) => {
|
||||
this.world.updateViewerPosition(pos)
|
||||
})
|
||||
|
||||
emitter.on('renderDistance', (d) => {
|
||||
this.world.viewDistance = d
|
||||
this.world.chunksLength = d === 0 ? 1 : generateSpiralMatrix(d).length
|
||||
this.world.allChunksFinished = Object.keys(this.world.finishedChunks).length === this.world.chunksLength
|
||||
})
|
||||
|
||||
emitter.on('updateLight', ({ pos }) => {
|
||||
if (this.world instanceof WorldRendererThree) this.world.updateLight(pos.x, pos.z)
|
||||
})
|
||||
|
||||
emitter.on('time', (timeOfDay) => {
|
||||
this.world.timeUpdated?.(timeOfDay)
|
||||
|
||||
let skyLight = 15
|
||||
if (timeOfDay < 0 || timeOfDay > 24_000) {
|
||||
throw new Error('Invalid time of day. It should be between 0 and 24000.')
|
||||
} else if (timeOfDay <= 6000 || timeOfDay >= 18_000) {
|
||||
skyLight = 15
|
||||
} else if (timeOfDay > 6000 && timeOfDay < 12_000) {
|
||||
skyLight = 15 - ((timeOfDay - 6000) / 6000) * 15
|
||||
} else if (timeOfDay >= 12_000 && timeOfDay < 18_000) {
|
||||
skyLight = ((timeOfDay - 12_000) / 6000) * 15
|
||||
}
|
||||
|
||||
skyLight = Math.floor(skyLight) // todo: remove this after optimization
|
||||
|
||||
if (this.world.mesherConfig.skyLight === skyLight) return
|
||||
this.world.mesherConfig.skyLight = skyLight;
|
||||
(this.world as WorldRendererThree).rerenderAllChunks?.()
|
||||
})
|
||||
|
||||
emitter.emit('listening')
|
||||
}
|
||||
|
||||
render () {
|
||||
this.world.render()
|
||||
this.entities.render()
|
||||
}
|
||||
|
||||
async waitForChunksToRender () {
|
||||
await this.world.waitForChunksToRender()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,120 +0,0 @@
|
|||
import * as THREE from 'three'
|
||||
import { statsEnd, statsStart } from '../../../src/topRightStats'
|
||||
|
||||
// wrapper for now
|
||||
export class ViewerWrapper {
|
||||
previousWindowWidth: number
|
||||
previousWindowHeight: number
|
||||
globalObject = globalThis as any
|
||||
stopRenderOnBlur = false
|
||||
addedToPage = false
|
||||
renderInterval = 0
|
||||
renderIntervalUnfocused: number | undefined
|
||||
fpsInterval
|
||||
|
||||
constructor (public canvas: HTMLCanvasElement, public renderer?: THREE.WebGLRenderer) {
|
||||
if (this.renderer) this.globalObject.renderer = this.renderer
|
||||
}
|
||||
|
||||
addToPage (startRendering = true) {
|
||||
if (this.addedToPage) throw new Error('Already added to page')
|
||||
let pixelRatio = window.devicePixelRatio || 1 // todo this value is too high on ios, need to check, probably we should use avg, also need to make it configurable
|
||||
if (this.renderer) {
|
||||
if (!this.renderer.capabilities.isWebGL2) pixelRatio = 1 // webgl1 has issues with high pixel ratio (sometimes screen is clipped)
|
||||
this.renderer.setPixelRatio(pixelRatio)
|
||||
this.renderer.setSize(window.innerWidth, window.innerHeight)
|
||||
} else {
|
||||
this.canvas.width = window.innerWidth * pixelRatio
|
||||
this.canvas.height = window.innerHeight * pixelRatio
|
||||
}
|
||||
this.previousWindowWidth = window.innerWidth
|
||||
this.previousWindowHeight = window.innerHeight
|
||||
|
||||
this.canvas.id = 'viewer-canvas'
|
||||
document.body.appendChild(this.canvas)
|
||||
|
||||
this.addedToPage = true
|
||||
|
||||
let max = 0
|
||||
this.fpsInterval = setInterval(() => {
|
||||
if (max > 0) {
|
||||
viewer.world.droppedFpsPercentage = this.renderedFps / max
|
||||
}
|
||||
max = Math.max(this.renderedFps, max)
|
||||
this.renderedFps = 0
|
||||
}, 1000)
|
||||
if (startRendering) {
|
||||
this.globalObject.requestAnimationFrame(this.render.bind(this))
|
||||
}
|
||||
if (typeof window !== 'undefined') {
|
||||
this.trackWindowFocus()
|
||||
}
|
||||
}
|
||||
|
||||
windowFocused = true
|
||||
trackWindowFocus () {
|
||||
window.addEventListener('focus', () => {
|
||||
this.windowFocused = true
|
||||
})
|
||||
window.addEventListener('blur', () => {
|
||||
this.windowFocused = false
|
||||
})
|
||||
}
|
||||
|
||||
dispose () {
|
||||
if (!this.addedToPage) throw new Error('Not added to page')
|
||||
this.canvas.remove()
|
||||
this.renderer?.dispose()
|
||||
// this.addedToPage = false
|
||||
clearInterval(this.fpsInterval)
|
||||
}
|
||||
|
||||
|
||||
renderedFps = 0
|
||||
lastTime = performance.now()
|
||||
delta = 0
|
||||
preRender = () => { }
|
||||
postRender = () => { }
|
||||
render (time: DOMHighResTimeStamp) {
|
||||
if (this.globalObject.stopLoop) return
|
||||
for (const fn of beforeRenderFrame) fn()
|
||||
this.globalObject.requestAnimationFrame(this.render.bind(this))
|
||||
if (this.globalObject.stopRender || this.renderer?.xr.isPresenting || (this.stopRenderOnBlur && !this.windowFocused)) return
|
||||
const renderInterval = (this.windowFocused ? this.renderInterval : this.renderIntervalUnfocused) ?? this.renderInterval
|
||||
if (renderInterval) {
|
||||
this.delta += time - this.lastTime
|
||||
this.lastTime = time
|
||||
if (this.delta > renderInterval) {
|
||||
this.delta %= renderInterval
|
||||
// continue rendering
|
||||
} else {
|
||||
return
|
||||
}
|
||||
}
|
||||
this.preRender()
|
||||
statsStart()
|
||||
// ios bug: viewport dimensions are updated after the resize event
|
||||
if (this.previousWindowWidth !== window.innerWidth || this.previousWindowHeight !== window.innerHeight) {
|
||||
this.resizeHandler()
|
||||
this.previousWindowWidth = window.innerWidth
|
||||
this.previousWindowHeight = window.innerHeight
|
||||
}
|
||||
viewer.render()
|
||||
this.renderedFps++
|
||||
statsEnd()
|
||||
this.postRender()
|
||||
}
|
||||
|
||||
resizeHandler () {
|
||||
const width = window.innerWidth
|
||||
const height = window.innerHeight
|
||||
|
||||
viewer.camera.aspect = width / height
|
||||
viewer.camera.updateProjectionMatrix()
|
||||
|
||||
if (this.renderer) {
|
||||
this.renderer.setSize(width, height)
|
||||
}
|
||||
viewer.world.handleResize()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,60 +0,0 @@
|
|||
export function createWorkerProxy<T extends Record<string, (...args: any[]) => void>> (handlers: T): { __workerProxy: T } {
|
||||
addEventListener('message', (event) => {
|
||||
const { type, args } = event.data
|
||||
if (handlers[type]) {
|
||||
handlers[type](...args)
|
||||
}
|
||||
})
|
||||
return null as any
|
||||
}
|
||||
|
||||
/**
|
||||
* in main thread
|
||||
* ```ts
|
||||
* // either:
|
||||
* import type { importedTypeWorkerProxy } from './worker'
|
||||
* // or:
|
||||
* type importedTypeWorkerProxy = import('./worker').importedTypeWorkerProxy
|
||||
*
|
||||
* const workerChannel = useWorkerProxy<typeof importedTypeWorkerProxy>(worker)
|
||||
* ```
|
||||
*/
|
||||
export const useWorkerProxy = <T extends { __workerProxy: Record<string, (...args: any[]) => void> }> (worker: Worker, autoTransfer = true): T['__workerProxy'] & {
|
||||
transfer: (...args: Transferable[]) => T['__workerProxy']
|
||||
} => {
|
||||
// in main thread
|
||||
return new Proxy({} as any, {
|
||||
get (target, prop) {
|
||||
if (prop === 'transfer') {
|
||||
return (...transferable: Transferable[]) => {
|
||||
return new Proxy({}, {
|
||||
get (target, prop) {
|
||||
return (...args: any[]) => {
|
||||
worker.postMessage({
|
||||
type: prop,
|
||||
args,
|
||||
}, transferable)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
return (...args: any[]) => {
|
||||
const transfer = autoTransfer ? args.filter(arg => arg instanceof ArrayBuffer || arg instanceof MessagePort || arg instanceof ImageBitmap || arg instanceof OffscreenCanvas) : []
|
||||
worker.postMessage({
|
||||
type: prop,
|
||||
args,
|
||||
}, transfer)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// const workerProxy = createWorkerProxy({
|
||||
// startRender (canvas: HTMLCanvasElement) {
|
||||
// },
|
||||
// })
|
||||
|
||||
// const worker = useWorkerProxy(null, workerProxy)
|
||||
|
||||
// worker.
|
||||
|
|
@ -1,256 +0,0 @@
|
|||
/* eslint-disable guard-for-in */
|
||||
|
||||
// todo refactor into its own commons module
|
||||
import { EventEmitter } from 'events'
|
||||
import { generateSpiralMatrix, ViewRect } from 'flying-squid/dist/utils'
|
||||
import { Vec3 } from 'vec3'
|
||||
import { BotEvents } from 'mineflayer'
|
||||
import { getItemFromBlock } from '../../../src/botUtils'
|
||||
import { chunkPos } from './simpleUtils'
|
||||
|
||||
export type ChunkPosKey = string
|
||||
type ChunkPos = { x: number, z: number }
|
||||
|
||||
/**
|
||||
* Usually connects to mineflayer bot and emits world data (chunks, entities)
|
||||
* It's up to the consumer to serialize the data if needed
|
||||
*/
|
||||
export class WorldDataEmitter extends EventEmitter {
|
||||
private loadedChunks: Record<ChunkPosKey, boolean>
|
||||
private readonly lastPos: Vec3
|
||||
private eventListeners: Record<string, any> = {}
|
||||
private readonly emitter: WorldDataEmitter
|
||||
keepChunksDistance = 0
|
||||
_handDisplay = false
|
||||
get handDisplay () {
|
||||
return this._handDisplay
|
||||
}
|
||||
set handDisplay (newVal) {
|
||||
this._handDisplay = newVal
|
||||
this.eventListeners.heldItemChanged?.()
|
||||
}
|
||||
|
||||
constructor (public world: typeof __type_bot['world'], public viewDistance: number, position: Vec3 = new Vec3(0, 0, 0)) {
|
||||
super()
|
||||
this.loadedChunks = {}
|
||||
this.lastPos = new Vec3(0, 0, 0).update(position)
|
||||
// todo
|
||||
this.emitter = this
|
||||
|
||||
this.emitter.on('mouseClick', async (click) => {
|
||||
const ori = new Vec3(click.origin.x, click.origin.y, click.origin.z)
|
||||
const dir = new Vec3(click.direction.x, click.direction.y, click.direction.z)
|
||||
const block = this.world.raycast(ori, dir, 256)
|
||||
if (!block) return
|
||||
this.emit('blockClicked', block, block.face, click.button)
|
||||
})
|
||||
}
|
||||
|
||||
updateViewDistance (viewDistance: number) {
|
||||
this.viewDistance = viewDistance
|
||||
this.emitter.emit('renderDistance', viewDistance)
|
||||
}
|
||||
|
||||
listenToBot (bot: typeof __type_bot) {
|
||||
const emitEntity = (e) => {
|
||||
if (!e || e === bot.entity) return
|
||||
this.emitter.emit('entity', {
|
||||
...e,
|
||||
pos: e.position,
|
||||
username: e.username,
|
||||
// set debugTree (obj) {
|
||||
// e.debugTree = obj
|
||||
// }
|
||||
})
|
||||
}
|
||||
|
||||
this.eventListeners = {
|
||||
// 'move': botPosition,
|
||||
entitySpawn (e: any) {
|
||||
emitEntity(e)
|
||||
},
|
||||
entityUpdate (e: any) {
|
||||
emitEntity(e)
|
||||
},
|
||||
entityMoved (e: any) {
|
||||
emitEntity(e)
|
||||
},
|
||||
entityGone: (e: any) => {
|
||||
this.emitter.emit('entity', { id: e.id, delete: true })
|
||||
},
|
||||
chunkColumnLoad: (pos: Vec3) => {
|
||||
void this.loadChunk(pos)
|
||||
},
|
||||
chunkColumnUnload: (pos: Vec3) => {
|
||||
this.unloadChunk(pos)
|
||||
},
|
||||
blockUpdate: (oldBlock: any, newBlock: any) => {
|
||||
const stateId = newBlock.stateId ?? ((newBlock.type << 4) | newBlock.metadata)
|
||||
this.emitter.emit('blockUpdate', { pos: oldBlock.position, stateId })
|
||||
},
|
||||
time: () => {
|
||||
this.emitter.emit('time', bot.time.timeOfDay)
|
||||
},
|
||||
heldItemChanged: () => {
|
||||
if (!this.handDisplay) {
|
||||
viewer.world.onHandItemSwitch(undefined)
|
||||
return
|
||||
}
|
||||
const newItem = bot.heldItem
|
||||
if (!newItem) {
|
||||
viewer.world.onHandItemSwitch(undefined)
|
||||
return
|
||||
}
|
||||
const block = loadedData.blocksByName[newItem.name]
|
||||
// todo clean types
|
||||
const blockProperties = block ? new window.PrismarineBlock(block.id, 'void', newItem.metadata).getProperties() : {}
|
||||
viewer.world.onHandItemSwitch({ name: newItem.name, properties: blockProperties })
|
||||
},
|
||||
} satisfies Partial<BotEvents>
|
||||
this.eventListeners.heldItemChanged()
|
||||
|
||||
|
||||
bot._client.on('update_light', ({ chunkX, chunkZ }) => {
|
||||
const chunkPos = new Vec3(chunkX * 16, 0, chunkZ * 16)
|
||||
void this.loadChunk(chunkPos, true)
|
||||
})
|
||||
|
||||
this.emitter.on('listening', () => {
|
||||
this.emitter.emit('blockEntities', new Proxy({}, {
|
||||
get (_target, posKey, receiver) {
|
||||
if (typeof posKey !== 'string') return
|
||||
const [x, y, z] = posKey.split(',').map(Number)
|
||||
return bot.world.getBlock(new Vec3(x, y, z))?.entity
|
||||
},
|
||||
}))
|
||||
this.emitter.emit('renderDistance', this.viewDistance)
|
||||
this.emitter.emit('time', bot.time.timeOfDay)
|
||||
})
|
||||
// node.js stream data event pattern
|
||||
if (this.emitter.listenerCount('blockEntities')) {
|
||||
this.emitter.emit('listening')
|
||||
}
|
||||
|
||||
for (const [evt, listener] of Object.entries(this.eventListeners)) {
|
||||
bot.on(evt as any, listener)
|
||||
}
|
||||
|
||||
for (const id in bot.entities) {
|
||||
const e = bot.entities[id]
|
||||
emitEntity(e)
|
||||
}
|
||||
}
|
||||
|
||||
removeListenersFromBot (bot: import('mineflayer').Bot) {
|
||||
for (const [evt, listener] of Object.entries(this.eventListeners)) {
|
||||
bot.removeListener(evt as any, listener)
|
||||
}
|
||||
}
|
||||
|
||||
async init (pos: Vec3) {
|
||||
this.updateViewDistance(this.viewDistance)
|
||||
this.emitter.emit('chunkPosUpdate', { pos })
|
||||
const [botX, botZ] = chunkPos(pos)
|
||||
|
||||
const positions = generateSpiralMatrix(this.viewDistance).map(([x, z]) => new Vec3((botX + x) * 16, 0, (botZ + z) * 16))
|
||||
|
||||
this.lastPos.update(pos)
|
||||
this._loadChunks(positions)
|
||||
}
|
||||
|
||||
_loadChunks (positions: Vec3[], sliceSize = 5, waitTime = 0) {
|
||||
let i = 0
|
||||
const interval = setInterval(() => {
|
||||
if (i >= positions.length) {
|
||||
clearInterval(interval)
|
||||
return
|
||||
}
|
||||
void this.loadChunk(positions[i])
|
||||
i++
|
||||
}, 1)
|
||||
}
|
||||
|
||||
readdDebug () {
|
||||
const clonedLoadedChunks = { ...this.loadedChunks }
|
||||
this.unloadAllChunks()
|
||||
for (const loadedChunk in clonedLoadedChunks) {
|
||||
const [x, z] = loadedChunk.split(',').map(Number)
|
||||
void this.loadChunk(new Vec3(x, 0, z))
|
||||
}
|
||||
}
|
||||
|
||||
// debugGotChunkLatency = [] as number[]
|
||||
// lastTime = 0
|
||||
|
||||
async loadChunk (pos: ChunkPos, isLightUpdate = false) {
|
||||
const [botX, botZ] = chunkPos(this.lastPos)
|
||||
const dx = Math.abs(botX - Math.floor(pos.x / 16))
|
||||
const dz = Math.abs(botZ - Math.floor(pos.z / 16))
|
||||
if (dx <= this.viewDistance && dz <= this.viewDistance) {
|
||||
// eslint-disable-next-line @typescript-eslint/await-thenable -- todo allow to use async world provider but not sure if needed
|
||||
const column = await this.world.getColumnAt(pos['y'] ? pos as Vec3 : new Vec3(pos.x, 0, pos.z))
|
||||
if (column) {
|
||||
// const latency = Math.floor(performance.now() - this.lastTime)
|
||||
// this.debugGotChunkLatency.push(latency)
|
||||
// this.lastTime = performance.now()
|
||||
// todo optimize toJson data, make it clear why it is used
|
||||
const chunk = column.toJson()
|
||||
// TODO: blockEntities
|
||||
const worldConfig = {
|
||||
minY: column['minY'] ?? 0,
|
||||
worldHeight: column['worldHeight'] ?? 256,
|
||||
}
|
||||
//@ts-expect-error
|
||||
this.emitter.emit('loadChunk', { x: pos.x, z: pos.z, chunk, blockEntities: column.blockEntities, worldConfig, isLightUpdate })
|
||||
this.loadedChunks[`${pos.x},${pos.z}`] = true
|
||||
}
|
||||
} else {
|
||||
// console.debug('skipped loading chunk', dx, dz, '>', this.viewDistance)
|
||||
}
|
||||
}
|
||||
|
||||
unloadAllChunks () {
|
||||
for (const coords of Object.keys(this.loadedChunks)) {
|
||||
const [x, z] = coords.split(',').map(Number)
|
||||
this.unloadChunk({ x, z })
|
||||
}
|
||||
}
|
||||
|
||||
unloadChunk (pos: ChunkPos) {
|
||||
this.emitter.emit('unloadChunk', { x: pos.x, z: pos.z })
|
||||
delete this.loadedChunks[`${pos.x},${pos.z}`]
|
||||
}
|
||||
|
||||
async updatePosition (pos: Vec3, force = false) {
|
||||
const [lastX, lastZ] = chunkPos(this.lastPos)
|
||||
const [botX, botZ] = chunkPos(pos)
|
||||
if (lastX !== botX || lastZ !== botZ || force) {
|
||||
this.emitter.emit('chunkPosUpdate', { pos })
|
||||
const newViewToUnload = new ViewRect(botX, botZ, this.viewDistance + this.keepChunksDistance)
|
||||
const chunksToUnload: Vec3[] = []
|
||||
for (const coords of Object.keys(this.loadedChunks)) {
|
||||
const x = parseInt(coords.split(',')[0], 10)
|
||||
const z = parseInt(coords.split(',')[1], 10)
|
||||
const p = new Vec3(x, 0, z)
|
||||
const [chunkX, chunkZ] = chunkPos(p)
|
||||
if (!newViewToUnload.contains(chunkX, chunkZ)) {
|
||||
chunksToUnload.push(p)
|
||||
}
|
||||
}
|
||||
console.log('unloading', chunksToUnload.length, 'total now', Object.keys(this.loadedChunks).length)
|
||||
for (const p of chunksToUnload) {
|
||||
this.unloadChunk(p)
|
||||
}
|
||||
const positions = generateSpiralMatrix(this.viewDistance).map(([x, z]) => {
|
||||
const pos = new Vec3((botX + x) * 16, 0, (botZ + z) * 16)
|
||||
if (!this.loadedChunks[`${pos.x},${pos.z}`]) return pos
|
||||
return undefined!
|
||||
}).filter(a => !!a)
|
||||
this.lastPos.update(pos)
|
||||
this._loadChunks(positions)
|
||||
} else {
|
||||
this.emitter.emit('chunkPosUpdate', { pos }) // todo-low
|
||||
this.lastPos.update(pos)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,413 +0,0 @@
|
|||
/* eslint-disable guard-for-in */
|
||||
import { EventEmitter } from 'events'
|
||||
import { Vec3 } from 'vec3'
|
||||
import * as THREE from 'three'
|
||||
import mcDataRaw from 'minecraft-data/data.js' // note: using alias
|
||||
import blocksAtlases from 'mc-assets/dist/blocksAtlases.json'
|
||||
import blocksAtlasLatest from 'mc-assets/dist/blocksAtlasLatest.png'
|
||||
import blocksAtlasLegacy from 'mc-assets/dist/blocksAtlasLegacy.png'
|
||||
import itemsAtlases from 'mc-assets/dist/itemsAtlases.json'
|
||||
import itemsAtlasLatest from 'mc-assets/dist/itemsAtlasLatest.png'
|
||||
import itemsAtlasLegacy from 'mc-assets/dist/itemsAtlasLegacy.png'
|
||||
import { AtlasParser } from 'mc-assets'
|
||||
import TypedEmitter from 'typed-emitter'
|
||||
import { dynamicMcDataFiles } from '../../buildMesherConfig.mjs'
|
||||
import { toMajorVersion } from '../../../src/utils'
|
||||
import { buildCleanupDecorator } from './cleanupDecorator'
|
||||
import { defaultMesherConfig } from './mesher/shared'
|
||||
import { chunkPos } from './simpleUtils'
|
||||
import { HandItemBlock } from './holdingBlock'
|
||||
|
||||
function mod (x, n) {
|
||||
return ((x % n) + n) % n
|
||||
}
|
||||
|
||||
export const worldCleanup = buildCleanupDecorator('resetWorld')
|
||||
|
||||
export const defaultWorldRendererConfig = {
|
||||
showChunkBorders: false,
|
||||
numWorkers: 4
|
||||
}
|
||||
|
||||
export type WorldRendererConfig = typeof defaultWorldRendererConfig
|
||||
|
||||
type CustomTexturesData = {
|
||||
tileSize: number | undefined
|
||||
textures: Record<string, HTMLImageElement>
|
||||
}
|
||||
|
||||
export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any> {
|
||||
worldConfig = { minY: 0, worldHeight: 256 }
|
||||
// todo need to cleanup
|
||||
material = new THREE.MeshLambertMaterial({ vertexColors: true, transparent: true, alphaTest: 0.1 })
|
||||
|
||||
@worldCleanup()
|
||||
active = false
|
||||
|
||||
version = undefined as string | undefined
|
||||
@worldCleanup()
|
||||
loadedChunks = {} as Record<string, boolean>
|
||||
|
||||
@worldCleanup()
|
||||
finishedChunks = {} as Record<string, boolean>
|
||||
|
||||
@worldCleanup()
|
||||
sectionsOutstanding = new Map<string, number>()
|
||||
|
||||
@worldCleanup()
|
||||
renderUpdateEmitter = new EventEmitter() as unknown as TypedEmitter<{
|
||||
dirty (pos: Vec3, value: boolean): void
|
||||
update (/* pos: Vec3, value: boolean */): void
|
||||
textureDownloaded (): void
|
||||
}>
|
||||
customTexturesDataUrl = undefined as string | undefined
|
||||
@worldCleanup()
|
||||
currentTextureImage = undefined as any
|
||||
workers: any[] = []
|
||||
viewerPosition?: Vec3
|
||||
lastCamUpdate = 0
|
||||
droppedFpsPercentage = 0
|
||||
initialChunksLoad = true
|
||||
enableChunksLoadDelay = false
|
||||
texturesVersion?: string
|
||||
viewDistance = -1
|
||||
chunksLength = 0
|
||||
@worldCleanup()
|
||||
allChunksFinished = false
|
||||
|
||||
handleResize = () => { }
|
||||
mesherConfig = defaultMesherConfig
|
||||
camera: THREE.PerspectiveCamera
|
||||
highestBlocks: Record<string, { y: number, name: string }> = {}
|
||||
blockstatesModels: any
|
||||
customBlockStates: Record<string, any> | undefined
|
||||
customModels: Record<string, any> | undefined
|
||||
itemsAtlasParser: AtlasParser | undefined
|
||||
blocksAtlasParser: AtlasParser | undefined
|
||||
|
||||
blocksAtlases = blocksAtlases
|
||||
itemsAtlases = itemsAtlases
|
||||
customTextures: {
|
||||
items?: CustomTexturesData
|
||||
blocks?: CustomTexturesData
|
||||
} = {}
|
||||
|
||||
abstract outputFormat: 'threeJs' | 'webgl'
|
||||
|
||||
constructor (public config: WorldRendererConfig) {
|
||||
// this.initWorkers(1) // preload script on page load
|
||||
this.snapshotInitialValues()
|
||||
}
|
||||
|
||||
snapshotInitialValues () { }
|
||||
|
||||
initWorkers (numWorkers = this.config.numWorkers) {
|
||||
// init workers
|
||||
for (let i = 0; i < numWorkers; i++) {
|
||||
// Node environment needs an absolute path, but browser needs the url of the file
|
||||
const workerName = 'mesher.js'
|
||||
// eslint-disable-next-line node/no-path-concat
|
||||
const src = typeof window === 'undefined' ? `${__dirname}/${workerName}` : workerName
|
||||
|
||||
const worker: any = new Worker(src)
|
||||
const handleMessage = (data) => {
|
||||
if (!this.active) return
|
||||
this.handleWorkerMessage(data)
|
||||
if (data.type === 'geometry') {
|
||||
for (const key in data.geometry.highestBlocks) {
|
||||
const highest = data.geometry.highestBlocks[key]
|
||||
if (!this.highestBlocks[key] || this.highestBlocks[key].y < highest.y) {
|
||||
this.highestBlocks[key] = highest
|
||||
}
|
||||
}
|
||||
}
|
||||
if (data.type === 'sectionFinished') { // on after load & unload section
|
||||
if (!this.sectionsOutstanding.get(data.key)) throw new Error(`sectionFinished event for non-outstanding section ${data.key}`)
|
||||
this.sectionsOutstanding.set(data.key, this.sectionsOutstanding.get(data.key)! - 1)
|
||||
if (this.sectionsOutstanding.get(data.key) === 0) this.sectionsOutstanding.delete(data.key)
|
||||
|
||||
const chunkCoords = data.key.split(',').map(Number)
|
||||
if (this.loadedChunks[`${chunkCoords[0]},${chunkCoords[2]}`]) { // ensure chunk data was added, not a neighbor chunk update
|
||||
const loadingKeys = [...this.sectionsOutstanding.keys()]
|
||||
if (!loadingKeys.some(key => {
|
||||
const [x, y, z] = key.split(',').map(Number)
|
||||
return x === chunkCoords[0] && z === chunkCoords[2]
|
||||
})) {
|
||||
this.finishedChunks[`${chunkCoords[0]},${chunkCoords[2]}`] = true
|
||||
}
|
||||
}
|
||||
if (this.sectionsOutstanding.size === 0) {
|
||||
const allFinished = Object.keys(this.finishedChunks).length === this.chunksLength
|
||||
if (allFinished) {
|
||||
this.allChunksLoaded?.()
|
||||
this.allChunksFinished = true
|
||||
}
|
||||
}
|
||||
|
||||
this.renderUpdateEmitter.emit('update')
|
||||
}
|
||||
}
|
||||
worker.onmessage = ({ data }) => {
|
||||
if (Array.isArray(data)) {
|
||||
// eslint-disable-next-line unicorn/no-array-for-each
|
||||
data.forEach(handleMessage)
|
||||
return
|
||||
}
|
||||
handleMessage(data)
|
||||
}
|
||||
if (worker.on) worker.on('message', (data) => { worker.onmessage({ data }) })
|
||||
this.workers.push(worker)
|
||||
}
|
||||
}
|
||||
|
||||
onHandItemSwitch (item: HandItemBlock | undefined): void { }
|
||||
changeHandSwingingState (isAnimationPlaying: boolean): void { }
|
||||
|
||||
abstract handleWorkerMessage (data: WorkerReceive): void
|
||||
|
||||
abstract updateCamera (pos: Vec3 | null, yaw: number, pitch: number): void
|
||||
|
||||
abstract render (): void
|
||||
|
||||
/**
|
||||
* Optionally update data that are depedendent on the viewer position
|
||||
*/
|
||||
updatePosDataChunk? (key: string): void
|
||||
|
||||
allChunksLoaded? (): void
|
||||
|
||||
timeUpdated? (newTime: number): void
|
||||
|
||||
updateViewerPosition (pos: Vec3) {
|
||||
this.viewerPosition = pos
|
||||
for (const [key, value] of Object.entries(this.loadedChunks)) {
|
||||
if (!value) continue
|
||||
this.updatePosDataChunk?.(key)
|
||||
}
|
||||
}
|
||||
|
||||
sendWorkers (message: WorkerSend) {
|
||||
for (const worker of this.workers) {
|
||||
worker.postMessage(message)
|
||||
}
|
||||
}
|
||||
|
||||
getDistance (posAbsolute: Vec3) {
|
||||
const [botX, botZ] = chunkPos(this.viewerPosition!)
|
||||
const dx = Math.abs(botX - Math.floor(posAbsolute.x / 16))
|
||||
const dz = Math.abs(botZ - Math.floor(posAbsolute.z / 16))
|
||||
return [dx, dz] as [number, number]
|
||||
}
|
||||
|
||||
abstract updateShowChunksBorder (value: boolean): void
|
||||
|
||||
resetWorld () {
|
||||
// destroy workers
|
||||
for (const worker of this.workers) {
|
||||
worker.terminate()
|
||||
}
|
||||
this.workers = []
|
||||
this.currentTextureImage = undefined
|
||||
this.blocksAtlasParser = undefined
|
||||
this.itemsAtlasParser = undefined
|
||||
}
|
||||
|
||||
// new game load happens here
|
||||
async setVersion (version, texturesVersion = version) {
|
||||
if (!this.blockstatesModels) throw new Error('Blockstates models is not loaded yet')
|
||||
this.version = version
|
||||
this.texturesVersion = texturesVersion
|
||||
this.resetWorld()
|
||||
this.initWorkers()
|
||||
this.active = true
|
||||
this.mesherConfig.outputFormat = this.outputFormat
|
||||
this.mesherConfig.version = this.version!
|
||||
|
||||
this.sendMesherMcData()
|
||||
await this.updateTexturesData()
|
||||
}
|
||||
|
||||
sendMesherMcData () {
|
||||
const allMcData = mcDataRaw.pc[this.version] ?? mcDataRaw.pc[toMajorVersion(this.version)]
|
||||
const mcData = {
|
||||
version: JSON.parse(JSON.stringify(allMcData.version))
|
||||
}
|
||||
for (const key of dynamicMcDataFiles) {
|
||||
mcData[key] = allMcData[key]
|
||||
}
|
||||
|
||||
for (const worker of this.workers) {
|
||||
worker.postMessage({ type: 'mcData', mcData, config: this.mesherConfig })
|
||||
}
|
||||
}
|
||||
|
||||
async updateTexturesData () {
|
||||
const blocksAssetsParser = new AtlasParser(this.blocksAtlases, blocksAtlasLatest, blocksAtlasLegacy)
|
||||
const itemsAssetsParser = new AtlasParser(this.itemsAtlases, itemsAtlasLatest, itemsAtlasLegacy)
|
||||
const { atlas: blocksAtlas, canvas: blocksCanvas } = await blocksAssetsParser.makeNewAtlas(this.texturesVersion ?? this.version ?? 'latest', (textureName) => {
|
||||
const texture = this.customTextures?.blocks?.textures[textureName]
|
||||
if (!texture) return
|
||||
return texture
|
||||
}, this.customTextures?.blocks?.tileSize)
|
||||
const { atlas: itemsAtlas, canvas: itemsCanvas } = await itemsAssetsParser.makeNewAtlas(this.texturesVersion ?? this.version ?? 'latest', (textureName) => {
|
||||
const texture = this.customTextures?.items?.textures[textureName]
|
||||
if (!texture) return
|
||||
return texture
|
||||
}, this.customTextures?.items?.tileSize)
|
||||
this.blocksAtlasParser = new AtlasParser({ latest: blocksAtlas }, blocksCanvas.toDataURL())
|
||||
this.itemsAtlasParser = new AtlasParser({ latest: itemsAtlas }, itemsCanvas.toDataURL())
|
||||
|
||||
const texture = await new THREE.TextureLoader().loadAsync(this.blocksAtlasParser.latestImage)
|
||||
texture.magFilter = THREE.NearestFilter
|
||||
texture.minFilter = THREE.NearestFilter
|
||||
texture.flipY = false
|
||||
this.material.map = texture
|
||||
this.currentTextureImage = this.material.map.image
|
||||
this.mesherConfig.textureSize = this.material.map.image.width
|
||||
|
||||
for (const worker of this.workers) {
|
||||
const { blockstatesModels } = this
|
||||
if (this.customBlockStates) {
|
||||
// TODO! remove from other versions as well
|
||||
blockstatesModels.blockstates.latest = {
|
||||
...blockstatesModels.blockstates.latest,
|
||||
...this.customBlockStates
|
||||
}
|
||||
}
|
||||
if (this.customModels) {
|
||||
blockstatesModels.models.latest = {
|
||||
...blockstatesModels.models.latest,
|
||||
...this.customModels
|
||||
}
|
||||
}
|
||||
worker.postMessage({
|
||||
type: 'mesherData',
|
||||
blocksAtlas: {
|
||||
latest: blocksAtlas
|
||||
},
|
||||
blockstatesModels,
|
||||
config: this.mesherConfig,
|
||||
})
|
||||
}
|
||||
this.renderUpdateEmitter.emit('textureDownloaded')
|
||||
console.log('texture loaded')
|
||||
}
|
||||
|
||||
addColumn (x: number, z: number, chunk: any, isLightUpdate: boolean) {
|
||||
if (!this.active) return
|
||||
if (this.workers.length === 0) throw new Error('workers not initialized yet')
|
||||
this.initialChunksLoad = false
|
||||
this.loadedChunks[`${x},${z}`] = true
|
||||
for (const worker of this.workers) {
|
||||
// todo optimize
|
||||
worker.postMessage({ type: 'chunk', x, z, chunk })
|
||||
}
|
||||
for (let y = this.worldConfig.minY; y < this.worldConfig.worldHeight; y += 16) {
|
||||
const loc = new Vec3(x, y, z)
|
||||
this.setSectionDirty(loc)
|
||||
if (!isLightUpdate || this.mesherConfig.smoothLighting) {
|
||||
this.setSectionDirty(loc.offset(-16, 0, 0))
|
||||
this.setSectionDirty(loc.offset(16, 0, 0))
|
||||
this.setSectionDirty(loc.offset(0, 0, -16))
|
||||
this.setSectionDirty(loc.offset(0, 0, 16))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
removeColumn (x, z) {
|
||||
delete this.loadedChunks[`${x},${z}`]
|
||||
for (const worker of this.workers) {
|
||||
worker.postMessage({ type: 'unloadChunk', x, z })
|
||||
}
|
||||
this.allChunksFinished = Object.keys(this.finishedChunks).length === this.chunksLength
|
||||
delete this.finishedChunks[`${x},${z}`]
|
||||
for (let y = this.worldConfig.minY; y < this.worldConfig.worldHeight; y += 16) {
|
||||
this.setSectionDirty(new Vec3(x, y, z), false)
|
||||
}
|
||||
// remove from highestBlocks
|
||||
const startX = Math.floor(x / 16) * 16
|
||||
const startZ = Math.floor(z / 16) * 16
|
||||
const endX = Math.ceil((x + 1) / 16) * 16
|
||||
const endZ = Math.ceil((z + 1) / 16) * 16
|
||||
for (let x = startX; x < endX; x += 16) {
|
||||
for (let z = startZ; z < endZ; z += 16) {
|
||||
delete this.highestBlocks[`${x},${z}`]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setBlockStateId (pos: Vec3, stateId: number) {
|
||||
for (const worker of this.workers) {
|
||||
worker.postMessage({ type: 'blockUpdate', pos, stateId })
|
||||
}
|
||||
this.setSectionDirty(pos)
|
||||
if ((pos.x & 15) === 0) this.setSectionDirty(pos.offset(-16, 0, 0))
|
||||
if ((pos.x & 15) === 15) this.setSectionDirty(pos.offset(16, 0, 0))
|
||||
if ((pos.y & 15) === 0) this.setSectionDirty(pos.offset(0, -16, 0))
|
||||
if ((pos.y & 15) === 15) this.setSectionDirty(pos.offset(0, 16, 0))
|
||||
if ((pos.z & 15) === 0) this.setSectionDirty(pos.offset(0, 0, -16))
|
||||
if ((pos.z & 15) === 15) this.setSectionDirty(pos.offset(0, 0, 16))
|
||||
}
|
||||
|
||||
queueAwaited = false
|
||||
messagesQueue = {} as { [workerIndex: string]: any[] }
|
||||
|
||||
setSectionDirty (pos: Vec3, value = true) { // value false is used for unloading chunks
|
||||
if (this.viewDistance === -1) throw new Error('viewDistance not set')
|
||||
this.allChunksFinished = false
|
||||
const distance = this.getDistance(pos)
|
||||
if (!this.workers.length || distance[0] > this.viewDistance || distance[1] > this.viewDistance) return
|
||||
const key = `${Math.floor(pos.x / 16) * 16},${Math.floor(pos.y / 16) * 16},${Math.floor(pos.z / 16) * 16}`
|
||||
// if (this.sectionsOutstanding.has(key)) return
|
||||
this.renderUpdateEmitter.emit('dirty', pos, value)
|
||||
// Dispatch sections to workers based on position
|
||||
// This guarantees uniformity accross workers and that a given section
|
||||
// is always dispatched to the same worker
|
||||
const hash = mod(Math.floor(pos.x / 16) + Math.floor(pos.y / 16) + Math.floor(pos.z / 16), this.workers.length)
|
||||
this.sectionsOutstanding.set(key, (this.sectionsOutstanding.get(key) ?? 0) + 1)
|
||||
this.messagesQueue[hash] ??= []
|
||||
this.messagesQueue[hash].push({
|
||||
// this.workers[hash].postMessage({
|
||||
type: 'dirty',
|
||||
x: pos.x,
|
||||
y: pos.y,
|
||||
z: pos.z,
|
||||
value,
|
||||
config: this.mesherConfig,
|
||||
})
|
||||
this.dispatchMessages()
|
||||
}
|
||||
|
||||
dispatchMessages () {
|
||||
if (this.queueAwaited) return
|
||||
this.queueAwaited = true
|
||||
setTimeout(() => {
|
||||
// group messages and send as one
|
||||
for (const workerIndex in this.messagesQueue) {
|
||||
const worker = this.workers[Number(workerIndex)]
|
||||
worker.postMessage(this.messagesQueue[workerIndex])
|
||||
}
|
||||
this.messagesQueue = {}
|
||||
this.queueAwaited = false
|
||||
})
|
||||
}
|
||||
|
||||
// Listen for chunk rendering updates emitted if a worker finished a render and resolve if the number
|
||||
// of sections not rendered are 0
|
||||
async waitForChunksToRender () {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
if ([...this.sectionsOutstanding].length === 0) {
|
||||
resolve()
|
||||
return
|
||||
}
|
||||
|
||||
const updateHandler = () => {
|
||||
if (this.sectionsOutstanding.size === 0) {
|
||||
this.renderUpdateEmitter.removeListener('update', updateHandler)
|
||||
resolve()
|
||||
}
|
||||
}
|
||||
this.renderUpdateEmitter.on('update', updateHandler)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -1,462 +0,0 @@
|
|||
import * as THREE from 'three'
|
||||
import { Vec3 } from 'vec3'
|
||||
import nbt from 'prismarine-nbt'
|
||||
import PrismarineChatLoader from 'prismarine-chat'
|
||||
import * as tweenJs from '@tweenjs/tween.js'
|
||||
import { BloomPass, RenderPass, UnrealBloomPass, EffectComposer, WaterPass, GlitchPass } from 'three-stdlib'
|
||||
import worldBlockProvider from 'mc-assets/dist/worldBlockProvider'
|
||||
import { renderSign } from '../sign-renderer'
|
||||
import { chunkPos, sectionPos } from './simpleUtils'
|
||||
import { WorldRendererCommon, WorldRendererConfig } from './worldrendererCommon'
|
||||
import { disposeObject } from './threeJsUtils'
|
||||
import HoldingBlock, { HandItemBlock } from './holdingBlock'
|
||||
|
||||
export class WorldRendererThree extends WorldRendererCommon {
|
||||
outputFormat = 'threeJs' as const
|
||||
blockEntities = {}
|
||||
sectionObjects: Record<string, THREE.Object3D> = {}
|
||||
chunkTextures = new Map<string, { [pos: string]: THREE.Texture }>()
|
||||
signsCache = new Map<string, any>()
|
||||
starField: StarField
|
||||
cameraSectionPos: Vec3 = new Vec3(0, 0, 0)
|
||||
holdingBlock: HoldingBlock
|
||||
|
||||
get tilesRendered () {
|
||||
return Object.values(this.sectionObjects).reduce((acc, obj) => acc + (obj as any).tilesCount, 0)
|
||||
}
|
||||
|
||||
constructor (public scene: THREE.Scene, public renderer: THREE.WebGLRenderer, public config: WorldRendererConfig) {
|
||||
super(config)
|
||||
this.starField = new StarField(scene)
|
||||
this.holdingBlock = new HoldingBlock(this.scene)
|
||||
this.onHandItemSwitch({
|
||||
name: 'furnace',
|
||||
properties: {}
|
||||
})
|
||||
|
||||
this.renderUpdateEmitter.on('textureDownloaded', () => {
|
||||
if (this.holdingBlock.toBeRenderedItem) {
|
||||
this.onHandItemSwitch(this.holdingBlock.toBeRenderedItem)
|
||||
this.holdingBlock.toBeRenderedItem = undefined
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
onHandItemSwitch (item: HandItemBlock | undefined) {
|
||||
if (!this.currentTextureImage) {
|
||||
this.holdingBlock.toBeRenderedItem = item
|
||||
return
|
||||
}
|
||||
void this.holdingBlock.initHandObject(this.material, this.blockstatesModels, this.blocksAtlases, item)
|
||||
}
|
||||
|
||||
changeHandSwingingState (isAnimationPlaying: boolean) {
|
||||
if (isAnimationPlaying) {
|
||||
this.holdingBlock.startSwing()
|
||||
} else {
|
||||
void this.holdingBlock.stopSwing()
|
||||
}
|
||||
}
|
||||
|
||||
timeUpdated (newTime: number): void {
|
||||
const nightTime = 13_500
|
||||
const morningStart = 23_000
|
||||
const displayStars = newTime > nightTime && newTime < morningStart
|
||||
if (displayStars && !this.starField.points) {
|
||||
this.starField.addToScene()
|
||||
} else if (!displayStars && this.starField.points) {
|
||||
this.starField.remove()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Optionally update data that are depedendent on the viewer position
|
||||
*/
|
||||
updatePosDataChunk (key: string) {
|
||||
const [x, y, z] = key.split(',').map(x => Math.floor(+x / 16))
|
||||
// sum of distances: x + y + z
|
||||
const chunkDistance = Math.abs(x - this.cameraSectionPos.x) + Math.abs(y - this.cameraSectionPos.y) + Math.abs(z - this.cameraSectionPos.z)
|
||||
const section = this.sectionObjects[key].children.find(child => child.name === 'mesh')!
|
||||
section.renderOrder = 500 - chunkDistance
|
||||
}
|
||||
|
||||
updateViewerPosition (pos: Vec3): void {
|
||||
this.viewerPosition = pos
|
||||
const cameraPos = this.camera.position.toArray().map(x => Math.floor(x / 16)) as [number, number, number]
|
||||
this.cameraSectionPos = new Vec3(...cameraPos)
|
||||
// eslint-disable-next-line guard-for-in
|
||||
for (const key in this.sectionObjects) {
|
||||
const value = this.sectionObjects[key]
|
||||
if (!value) continue
|
||||
this.updatePosDataChunk(key)
|
||||
}
|
||||
}
|
||||
|
||||
// debugRecomputedDeletedObjects = 0
|
||||
handleWorkerMessage (data: any): void {
|
||||
if (data.type !== 'geometry') return
|
||||
let object: THREE.Object3D = this.sectionObjects[data.key]
|
||||
if (object) {
|
||||
this.scene.remove(object)
|
||||
disposeObject(object)
|
||||
delete this.sectionObjects[data.key]
|
||||
}
|
||||
|
||||
const chunkCoords = data.key.split(',')
|
||||
if (!this.loadedChunks[chunkCoords[0] + ',' + chunkCoords[2]] || !data.geometry.positions.length || !this.active) return
|
||||
|
||||
// if (object) {
|
||||
// this.debugRecomputedDeletedObjects++
|
||||
// }
|
||||
|
||||
// if (!this.initialChunksLoad && this.enableChunksLoadDelay) {
|
||||
// const newPromise = new Promise(resolve => {
|
||||
// if (this.droppedFpsPercentage > 0.5) {
|
||||
// setTimeout(resolve, 1000 / 50 * this.droppedFpsPercentage)
|
||||
// } else {
|
||||
// setTimeout(resolve)
|
||||
// }
|
||||
// })
|
||||
// this.promisesQueue.push(newPromise)
|
||||
// for (const promise of this.promisesQueue) {
|
||||
// await promise
|
||||
// }
|
||||
// }
|
||||
|
||||
const geometry = new THREE.BufferGeometry()
|
||||
geometry.setAttribute('position', new THREE.BufferAttribute(data.geometry.positions, 3))
|
||||
geometry.setAttribute('normal', new THREE.BufferAttribute(data.geometry.normals, 3))
|
||||
geometry.setAttribute('color', new THREE.BufferAttribute(data.geometry.colors, 3))
|
||||
geometry.setAttribute('uv', new THREE.BufferAttribute(data.geometry.uvs, 2))
|
||||
geometry.setIndex(data.geometry.indices)
|
||||
|
||||
const mesh = new THREE.Mesh(geometry, this.material)
|
||||
mesh.position.set(data.geometry.sx, data.geometry.sy, data.geometry.sz)
|
||||
mesh.name = 'mesh'
|
||||
object = new THREE.Group()
|
||||
object.add(mesh)
|
||||
// mesh with static dimensions: 16x16x16
|
||||
const staticChunkMesh = new THREE.Mesh(new THREE.BoxGeometry(16, 16, 16), new THREE.MeshBasicMaterial({ color: 0x00_00_00, transparent: true, opacity: 0 }))
|
||||
staticChunkMesh.position.set(data.geometry.sx, data.geometry.sy, data.geometry.sz)
|
||||
const boxHelper = new THREE.BoxHelper(staticChunkMesh, 0xff_ff_00)
|
||||
boxHelper.name = 'helper'
|
||||
object.add(boxHelper)
|
||||
object.name = 'chunk'
|
||||
//@ts-expect-error
|
||||
object.tilesCount = data.geometry.positions.length / 3 / 4
|
||||
if (!this.config.showChunkBorders) {
|
||||
boxHelper.visible = false
|
||||
}
|
||||
// should not compute it once
|
||||
if (Object.keys(data.geometry.signs).length) {
|
||||
for (const [posKey, { isWall, isHanging, rotation }] of Object.entries(data.geometry.signs)) {
|
||||
const [x, y, z] = posKey.split(',')
|
||||
const signBlockEntity = this.blockEntities[posKey]
|
||||
if (!signBlockEntity) continue
|
||||
const sign = this.renderSign(new Vec3(+x, +y, +z), rotation, isWall, isHanging, nbt.simplify(signBlockEntity))
|
||||
if (!sign) continue
|
||||
object.add(sign)
|
||||
}
|
||||
}
|
||||
this.sectionObjects[data.key] = object
|
||||
this.updatePosDataChunk(data.key)
|
||||
object.matrixAutoUpdate = false
|
||||
mesh.onAfterRender = (renderer, scene, camera, geometry, material, group) => {
|
||||
// mesh.matrixAutoUpdate = false
|
||||
}
|
||||
|
||||
this.scene.add(object)
|
||||
}
|
||||
|
||||
getSignTexture (position: Vec3, blockEntity, backSide = false) {
|
||||
const chunk = chunkPos(position)
|
||||
let textures = this.chunkTextures.get(`${chunk[0]},${chunk[1]}`)
|
||||
if (!textures) {
|
||||
textures = {}
|
||||
this.chunkTextures.set(`${chunk[0]},${chunk[1]}`, textures)
|
||||
}
|
||||
const texturekey = `${position.x},${position.y},${position.z}`
|
||||
// todo investigate bug and remove this so don't need to clean in section dirty
|
||||
if (textures[texturekey]) return textures[texturekey]
|
||||
|
||||
const PrismarineChat = PrismarineChatLoader(this.version!)
|
||||
const canvas = renderSign(blockEntity, PrismarineChat)
|
||||
if (!canvas) return
|
||||
const tex = new THREE.Texture(canvas)
|
||||
tex.magFilter = THREE.NearestFilter
|
||||
tex.minFilter = THREE.NearestFilter
|
||||
tex.needsUpdate = true
|
||||
textures[texturekey] = tex
|
||||
return tex
|
||||
}
|
||||
|
||||
updateCamera (pos: Vec3 | null, yaw: number, pitch: number): void {
|
||||
if (pos) {
|
||||
new tweenJs.Tween(this.camera.position).to({ x: pos.x, y: pos.y, z: pos.z }, 50).start()
|
||||
}
|
||||
this.camera.rotation.set(pitch, yaw, 0, 'ZYX')
|
||||
}
|
||||
|
||||
render () {
|
||||
tweenJs.update()
|
||||
this.holdingBlock.update(this.camera)
|
||||
// eslint-disable-next-line @typescript-eslint/non-nullable-type-assertion-style
|
||||
const cam = this.camera instanceof THREE.Group ? this.camera.children.find(child => child instanceof THREE.PerspectiveCamera) as THREE.PerspectiveCamera : this.camera
|
||||
this.renderer.render(this.scene, cam)
|
||||
}
|
||||
|
||||
renderSign (position: Vec3, rotation: number, isWall: boolean, isHanging: boolean, blockEntity) {
|
||||
const tex = this.getSignTexture(position, blockEntity)
|
||||
|
||||
if (!tex) return
|
||||
|
||||
// todo implement
|
||||
// const key = JSON.stringify({ position, rotation, isWall })
|
||||
// if (this.signsCache.has(key)) {
|
||||
// console.log('cached', key)
|
||||
// } else {
|
||||
// this.signsCache.set(key, tex)
|
||||
// }
|
||||
|
||||
const mesh = new THREE.Mesh(new THREE.PlaneGeometry(1, 1), new THREE.MeshBasicMaterial({ map: tex, transparent: true }))
|
||||
mesh.renderOrder = 999
|
||||
|
||||
const lineHeight = 7 / 16
|
||||
const scaleFactor = isHanging ? 1.3 : 1
|
||||
mesh.scale.set(1 * scaleFactor, lineHeight * scaleFactor, 1 * scaleFactor)
|
||||
|
||||
const thickness = (isHanging ? 2 : 1.5) / 16
|
||||
const wallSpacing = 0.25 / 16
|
||||
if (isWall && !isHanging) {
|
||||
mesh.position.set(0, 0, -0.5 + thickness + wallSpacing + 0.0001)
|
||||
} else {
|
||||
mesh.position.set(0, 0, thickness / 2 + 0.0001)
|
||||
}
|
||||
|
||||
const group = new THREE.Group()
|
||||
group.rotation.set(
|
||||
0,
|
||||
-THREE.MathUtils.degToRad(rotation * (isWall ? 90 : 45 / 2)),
|
||||
0
|
||||
)
|
||||
group.add(mesh)
|
||||
const height = (isHanging ? 10 : 8) / 16
|
||||
const heightOffset = (isHanging ? 0 : isWall ? 4.333 : 9.333) / 16
|
||||
const textPosition = height / 2 + heightOffset
|
||||
group.position.set(position.x + 0.5, position.y + textPosition, position.z + 0.5)
|
||||
return group
|
||||
}
|
||||
|
||||
updateLight (chunkX: number, chunkZ: number) {
|
||||
// set all sections in the chunk dirty
|
||||
for (let y = this.worldConfig.minY; y < this.worldConfig.worldHeight; y += 16) {
|
||||
this.setSectionDirty(new Vec3(chunkX, y, chunkZ))
|
||||
}
|
||||
}
|
||||
|
||||
async doHmr () {
|
||||
const oldSections = { ...this.sectionObjects }
|
||||
this.sectionObjects = {} // skip clearing
|
||||
worldView!.unloadAllChunks()
|
||||
void this.setVersion(this.version, this.texturesVersion)
|
||||
this.sectionObjects = oldSections
|
||||
// this.rerenderAllChunks()
|
||||
|
||||
// supply new data
|
||||
await worldView!.updatePosition(bot.entity.position, true)
|
||||
}
|
||||
|
||||
rerenderAllChunks () { // todo not clear what to do with loading chunks
|
||||
for (const key of Object.keys(this.sectionObjects)) {
|
||||
const [x, y, z] = key.split(',').map(Number)
|
||||
this.setSectionDirty(new Vec3(x, y, z))
|
||||
}
|
||||
}
|
||||
|
||||
updateShowChunksBorder (value: boolean) {
|
||||
this.config.showChunkBorders = value
|
||||
for (const object of Object.values(this.sectionObjects)) {
|
||||
for (const child of object.children) {
|
||||
if (child.name === 'helper') {
|
||||
child.visible = value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resetWorld () {
|
||||
super.resetWorld()
|
||||
|
||||
for (const mesh of Object.values(this.sectionObjects)) {
|
||||
this.scene.remove(mesh)
|
||||
}
|
||||
}
|
||||
|
||||
getLoadedChunksRelative (pos: Vec3, includeY = false) {
|
||||
const [currentX, currentY, currentZ] = sectionPos(pos)
|
||||
return Object.fromEntries(Object.entries(this.sectionObjects).map(([key, o]) => {
|
||||
const [xRaw, yRaw, zRaw] = key.split(',').map(Number)
|
||||
const [x, y, z] = sectionPos({ x: xRaw, y: yRaw, z: zRaw })
|
||||
const setKey = includeY ? `${x - currentX},${y - currentY},${z - currentZ}` : `${x - currentX},${z - currentZ}`
|
||||
return [setKey, o]
|
||||
}))
|
||||
}
|
||||
|
||||
cleanChunkTextures (x, z) {
|
||||
const textures = this.chunkTextures.get(`${Math.floor(x / 16)},${Math.floor(z / 16)}`) ?? {}
|
||||
for (const key of Object.keys(textures)) {
|
||||
textures[key].dispose()
|
||||
delete textures[key]
|
||||
}
|
||||
}
|
||||
|
||||
readdChunks () {
|
||||
for (const key of Object.keys(this.sectionObjects)) {
|
||||
this.scene.remove(this.sectionObjects[key])
|
||||
}
|
||||
setTimeout(() => {
|
||||
for (const key of Object.keys(this.sectionObjects)) {
|
||||
this.scene.add(this.sectionObjects[key])
|
||||
}
|
||||
}, 500)
|
||||
}
|
||||
|
||||
disableUpdates (children = this.scene.children) {
|
||||
for (const child of children) {
|
||||
child.matrixWorldNeedsUpdate = false
|
||||
this.disableUpdates(child.children ?? [])
|
||||
}
|
||||
}
|
||||
|
||||
removeColumn (x, z) {
|
||||
super.removeColumn(x, z)
|
||||
|
||||
this.cleanChunkTextures(x, z)
|
||||
for (let y = this.worldConfig.minY; y < this.worldConfig.worldHeight; y += 16) {
|
||||
this.setSectionDirty(new Vec3(x, y, z), false)
|
||||
const key = `${x},${y},${z}`
|
||||
const mesh = this.sectionObjects[key]
|
||||
if (mesh) {
|
||||
this.scene.remove(mesh)
|
||||
disposeObject(mesh)
|
||||
}
|
||||
delete this.sectionObjects[key]
|
||||
}
|
||||
}
|
||||
|
||||
setSectionDirty (pos, value = true) {
|
||||
this.cleanChunkTextures(pos.x, pos.z) // todo don't do this!
|
||||
super.setSectionDirty(pos, value)
|
||||
}
|
||||
}
|
||||
|
||||
class StarField {
|
||||
points?: THREE.Points
|
||||
private _enabled = true
|
||||
get enabled () {
|
||||
return this._enabled
|
||||
}
|
||||
|
||||
set enabled (value) {
|
||||
this._enabled = value
|
||||
if (this.points) {
|
||||
this.points.visible = value
|
||||
}
|
||||
}
|
||||
|
||||
constructor (private readonly scene: THREE.Scene) {
|
||||
}
|
||||
|
||||
addToScene () {
|
||||
if (this.points || !this.enabled) return
|
||||
|
||||
const radius = 80
|
||||
const depth = 50
|
||||
const count = 7000
|
||||
const factor = 7
|
||||
const saturation = 10
|
||||
const speed = 0.2
|
||||
|
||||
const geometry = new THREE.BufferGeometry()
|
||||
|
||||
const genStar = r => new THREE.Vector3().setFromSpherical(new THREE.Spherical(r, Math.acos(1 - Math.random() * 2), Math.random() * 2 * Math.PI))
|
||||
|
||||
const positions = [] as number[]
|
||||
const colors = [] as number[]
|
||||
const sizes = Array.from({ length: count }, () => (0.5 + 0.5 * Math.random()) * factor)
|
||||
const color = new THREE.Color()
|
||||
let r = radius + depth
|
||||
const increment = depth / count
|
||||
for (let i = 0; i < count; i++) {
|
||||
r -= increment * Math.random()
|
||||
positions.push(...genStar(r).toArray())
|
||||
color.setHSL(i / count, saturation, 0.9)
|
||||
colors.push(color.r, color.g, color.b)
|
||||
}
|
||||
|
||||
geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3))
|
||||
geometry.setAttribute('color', new THREE.Float32BufferAttribute(colors, 3))
|
||||
geometry.setAttribute('size', new THREE.Float32BufferAttribute(sizes, 1))
|
||||
|
||||
// Create a material
|
||||
const material = new StarfieldMaterial()
|
||||
material.blending = THREE.AdditiveBlending
|
||||
material.depthTest = false
|
||||
material.transparent = true
|
||||
|
||||
// Create points and add them to the scene
|
||||
this.points = new THREE.Points(geometry, material)
|
||||
this.scene.add(this.points)
|
||||
|
||||
const clock = new THREE.Clock()
|
||||
this.points.onBeforeRender = (renderer, scene, camera) => {
|
||||
this.points?.position.copy?.(camera.position)
|
||||
material.uniforms.time.value = clock.getElapsedTime() * speed
|
||||
}
|
||||
}
|
||||
|
||||
remove () {
|
||||
if (this.points) {
|
||||
this.points.geometry.dispose();
|
||||
(this.points.material as THREE.Material).dispose()
|
||||
this.scene.remove(this.points)
|
||||
|
||||
this.points = undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const version = parseInt(THREE.REVISION.replaceAll(/\D+/g, ''), 10)
|
||||
class StarfieldMaterial extends THREE.ShaderMaterial {
|
||||
constructor () {
|
||||
super({
|
||||
uniforms: { time: { value: 0 }, fade: { value: 1 } },
|
||||
vertexShader: /* glsl */ `
|
||||
uniform float time;
|
||||
attribute float size;
|
||||
varying vec3 vColor;
|
||||
attribute vec3 color;
|
||||
void main() {
|
||||
vColor = color;
|
||||
vec4 mvPosition = modelViewMatrix * vec4(position, 0.5);
|
||||
gl_PointSize = size * (30.0 / -mvPosition.z) * (3.0 + sin(time + 100.0));
|
||||
gl_Position = projectionMatrix * mvPosition;
|
||||
}`,
|
||||
fragmentShader: /* glsl */ `
|
||||
uniform sampler2D pointTexture;
|
||||
uniform float fade;
|
||||
varying vec3 vColor;
|
||||
void main() {
|
||||
float opacity = 1.0;
|
||||
if (fade == 1.0) {
|
||||
float d = distance(gl_PointCoord, vec2(0.5, 0.5));
|
||||
opacity = 1.0 / (1.0 + exp(16.0 * (d - 0.25)));
|
||||
}
|
||||
gl_FragColor = vec4(vColor, opacity);
|
||||
|
||||
#include <tonemapping_fragment>
|
||||
#include <${version >= 154 ? 'colorspace_fragment' : 'encodings_fragment'}>
|
||||
}`,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -1,139 +0,0 @@
|
|||
import { fromFormattedString, render, RenderNode, TextComponent } from '@xmcl/text-component'
|
||||
import type { ChatMessage } from 'prismarine-chat'
|
||||
|
||||
type SignBlockEntity = {
|
||||
Color?: string
|
||||
GlowingText?: 0 | 1
|
||||
Text1?: string
|
||||
Text2?: string
|
||||
Text3?: string
|
||||
Text4?: string
|
||||
} | {
|
||||
// todo
|
||||
is_waxed?: 0 | 1
|
||||
front_text: {
|
||||
color: string
|
||||
messages: string[]
|
||||
// todo
|
||||
has_glowing_text?: 0 | 1
|
||||
}
|
||||
// todo
|
||||
// back_text: {}
|
||||
}
|
||||
|
||||
type JsonEncodedType = string | null | Record<string, any>
|
||||
|
||||
const parseSafe = (text: string, task: string) => {
|
||||
try {
|
||||
return JSON.parse(text)
|
||||
} catch (e) {
|
||||
console.warn(`Failed to parse ${task}`, e)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export const renderSign = (blockEntity: SignBlockEntity, PrismarineChat: typeof ChatMessage, ctxHook = (ctx) => { }) => {
|
||||
// todo don't use texture rendering, investigate the font rendering when possible
|
||||
// or increase factor when needed
|
||||
const factor = 40
|
||||
const signboardY = [16, 9]
|
||||
const heightOffset = signboardY[0] - signboardY[1]
|
||||
const heightScalar = heightOffset / 16
|
||||
|
||||
let canvas: HTMLCanvasElement | undefined
|
||||
let _ctx: CanvasRenderingContext2D | null = null
|
||||
const getCtx = () => {
|
||||
if (_ctx) return _ctx
|
||||
canvas = document.createElement('canvas')
|
||||
|
||||
canvas.width = 16 * factor
|
||||
canvas.height = heightOffset * factor
|
||||
|
||||
_ctx = canvas.getContext('2d')!
|
||||
_ctx.imageSmoothingEnabled = false
|
||||
|
||||
ctxHook(_ctx)
|
||||
return _ctx
|
||||
}
|
||||
|
||||
const texts = 'front_text' in blockEntity ? /* > 1.20 */ blockEntity.front_text.messages : [
|
||||
blockEntity.Text1,
|
||||
blockEntity.Text2,
|
||||
blockEntity.Text3,
|
||||
blockEntity.Text4
|
||||
]
|
||||
const defaultColor = ('front_text' in blockEntity ? blockEntity.front_text.color : blockEntity.Color) || 'black'
|
||||
for (const [lineNum, text] of texts.slice(0, 4).entries()) {
|
||||
// todo: in pre flatenning it seems the format was not json
|
||||
if (text === 'null') continue
|
||||
const parsed = text?.startsWith('{') || text?.startsWith('"') ? parseSafe(text ?? '""', 'sign text') : text
|
||||
if (!parsed || (typeof parsed !== 'object' && typeof parsed !== 'string')) continue
|
||||
// todo fix type
|
||||
const message = typeof parsed === 'string' ? fromFormattedString(parsed) : new PrismarineChat(parsed) as never
|
||||
const patchExtra = ({ extra }: TextComponent) => {
|
||||
if (!extra) return
|
||||
for (const child of extra) {
|
||||
if (child.color) {
|
||||
child.color = child.color === 'dark_green' ? child.color.toUpperCase() : child.color.toLowerCase()
|
||||
}
|
||||
patchExtra(child)
|
||||
}
|
||||
}
|
||||
patchExtra(message)
|
||||
const rendered = render(message)
|
||||
|
||||
const toRenderCanvas: Array<{
|
||||
fontStyle: string
|
||||
fillStyle: string
|
||||
underlineStyle: boolean
|
||||
strikeStyle: boolean
|
||||
text: string
|
||||
}> = []
|
||||
let plainText = ''
|
||||
// todo the text should be clipped based on it's render width (needs investigate)
|
||||
const MAX_LENGTH = 50 // avoid abusing the signboard
|
||||
const renderText = (node: RenderNode) => {
|
||||
const { component } = node
|
||||
let { text } = component
|
||||
if (plainText.length + text.length > MAX_LENGTH) {
|
||||
text = text.slice(0, MAX_LENGTH - plainText.length)
|
||||
if (!text) return false
|
||||
}
|
||||
plainText += text
|
||||
toRenderCanvas.push({
|
||||
fontStyle: `${component.bold ? 'bold' : ''} ${component.italic ? 'italic' : ''}`,
|
||||
fillStyle: node.style['color'] || defaultColor,
|
||||
underlineStyle: component.underlined ?? false,
|
||||
strikeStyle: component.strikethrough ?? false,
|
||||
text
|
||||
})
|
||||
for (const child of node.children) {
|
||||
const stop = renderText(child) === false
|
||||
if (stop) return false
|
||||
}
|
||||
}
|
||||
|
||||
renderText(rendered)
|
||||
|
||||
// skip rendering empty lines (and possible signs)
|
||||
if (!plainText.trim()) continue
|
||||
|
||||
const ctx = getCtx()
|
||||
const fontSize = 1.6 * factor
|
||||
ctx.font = `${fontSize}px mojangles`
|
||||
const textWidth = ctx.measureText(plainText).width
|
||||
|
||||
let renderedWidth = 0
|
||||
for (const { fillStyle, fontStyle, strikeStyle, text, underlineStyle } of toRenderCanvas) {
|
||||
// todo strikeStyle, underlineStyle
|
||||
ctx.fillStyle = fillStyle
|
||||
ctx.font = `${fontStyle} ${fontSize}px mojangles`
|
||||
ctx.fillText(text, (canvas!.width - textWidth) / 2 + renderedWidth, fontSize * (lineNum + 1))
|
||||
renderedWidth += ctx.measureText(text).width // todo isn't the font is monospace?
|
||||
}
|
||||
}
|
||||
// ctx.fillStyle = 'red'
|
||||
// ctx.fillRect(0, 0, canvas.width, canvas.height)
|
||||
|
||||
return canvas
|
||||
}
|
||||
|
|
@ -22,23 +22,28 @@ const buildOptions = {
|
|||
},
|
||||
platform: 'browser',
|
||||
entryPoints: [path.join(__dirname, './viewer/lib/mesher/mesher.ts')],
|
||||
minify: true,
|
||||
minify: !watch,
|
||||
logLevel: 'info',
|
||||
drop: !watch ? [
|
||||
'debugger'
|
||||
] : [],
|
||||
sourcemap: 'linked',
|
||||
target: watch ? undefined : ['ios14'],
|
||||
write: false,
|
||||
metafile: true,
|
||||
outdir: path.join(__dirname, './public'),
|
||||
outdir: path.join(__dirname, './dist'),
|
||||
define: {
|
||||
'process.env.BROWSER': '"true"',
|
||||
},
|
||||
loader: {
|
||||
'.png': 'dataurl',
|
||||
'.obj': 'text'
|
||||
},
|
||||
plugins: [
|
||||
...mesherSharedPlugins,
|
||||
{
|
||||
name: 'external-json',
|
||||
setup (build) {
|
||||
setup(build) {
|
||||
build.onResolve({ filter: /\.json$/ }, args => {
|
||||
const fileName = args.path.split('/').pop().replace('.json', '')
|
||||
if (args.resolveDir.includes('minecraft-data')) {
|
||||
|
|
@ -108,9 +113,9 @@ const buildOptions = {
|
|||
})
|
||||
build.onEnd(({ metafile, outputFiles }) => {
|
||||
if (!metafile) return
|
||||
fs.mkdirSync(path.join(__dirname, './public'), { recursive: true })
|
||||
fs.writeFileSync(path.join(__dirname, './public/metafile.json'), JSON.stringify(metafile))
|
||||
for (const outDir of ['../dist/', './public/']) {
|
||||
fs.mkdirSync(path.join(__dirname, './dist'), { recursive: true })
|
||||
fs.writeFileSync(path.join(__dirname, './dist/metafile.json'), JSON.stringify(metafile))
|
||||
for (const outDir of ['../dist/', './dist/']) {
|
||||
for (const outputFile of outputFiles) {
|
||||
if (outDir === '../dist/' && outputFile.path.endsWith('.map')) {
|
||||
// skip writing & browser loading sourcemap there, worker debugging should be done in playground
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"name": "prismarine-viewer",
|
||||
"name": "renderer",
|
||||
"version": "1.25.0",
|
||||
"description": "Web based viewer",
|
||||
"main": "index.js",
|
||||
|
|
@ -26,7 +26,7 @@
|
|||
"prismarine-block": "^1.7.3",
|
||||
"prismarine-chunk": "^1.22.0",
|
||||
"prismarine-schematic": "^1.2.0",
|
||||
"prismarine-viewer": "link:./",
|
||||
"renderer": "link:./",
|
||||
"process": "^0.11.10",
|
||||
"socket.io": "^4.0.0",
|
||||
"socket.io-client": "^4.0.0",
|
||||
|
|
@ -11,11 +11,17 @@
|
|||
|
||||
html, body {
|
||||
height: 100%;
|
||||
touch-action: none;
|
||||
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
* {
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
|
||||
canvas {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
|
@ -28,9 +34,18 @@
|
|||
font-family: mojangles;
|
||||
src: url(../../../assets/mojangles.ttf);
|
||||
}
|
||||
* {
|
||||
user-select: none;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
if (window.location.pathname.endsWith('playground')) {
|
||||
// add trailing slash
|
||||
window.location.href = `${window.location.origin}${window.location.pathname}/${window.location.search}`
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<script type="text/javascript" src="playground.js"></script>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
170
renderer/playground/allEntitiesDebug.ts
Normal file
170
renderer/playground/allEntitiesDebug.ts
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
import { EntityMesh, rendererSpecialHandled, EntityDebugFlags } from '../viewer/three/entity/EntityMesh'
|
||||
|
||||
export const displayEntitiesDebugList = (version: string) => {
|
||||
// Create results container
|
||||
const container = document.createElement('div')
|
||||
container.style.cssText = `
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
background: rgba(0,0,0,0.8);
|
||||
color: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
font-family: monospace;
|
||||
min-width: 400px;
|
||||
z-index: 1000;
|
||||
`
|
||||
document.body.appendChild(container)
|
||||
|
||||
// Add title
|
||||
const title = document.createElement('h2')
|
||||
title.textContent = 'Minecraft Entity Support'
|
||||
title.style.cssText = 'margin-top: 0; text-align: center;'
|
||||
container.appendChild(title)
|
||||
|
||||
// Test entities
|
||||
const results: Array<{
|
||||
entity: string;
|
||||
supported: boolean;
|
||||
type?: 'obj' | 'bedrock' | 'special';
|
||||
mappedFrom?: string;
|
||||
textureMap?: boolean;
|
||||
errors?: string[];
|
||||
}> = []
|
||||
const { mcData } = window
|
||||
const entityNames = Object.keys(mcData.entitiesArray.reduce((acc, entity) => {
|
||||
acc[entity.name] = true
|
||||
return acc
|
||||
}, {}))
|
||||
|
||||
// Add loading indicator
|
||||
const loading = document.createElement('div')
|
||||
loading.textContent = 'Testing entities...'
|
||||
loading.style.textAlign = 'center'
|
||||
container.appendChild(loading)
|
||||
|
||||
for (const entity of entityNames) {
|
||||
const debugFlags: EntityDebugFlags = {}
|
||||
|
||||
if (rendererSpecialHandled.includes(entity)) {
|
||||
results.push({
|
||||
entity,
|
||||
supported: true,
|
||||
type: 'special',
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
const { mesh: entityMesh } = new EntityMesh(version, entity, undefined, {}, debugFlags)
|
||||
// find the most distant pos child
|
||||
window.objects ??= {}
|
||||
window.objects[entity] = entityMesh
|
||||
|
||||
results.push({
|
||||
entity,
|
||||
supported: !!debugFlags.type || rendererSpecialHandled.includes(entity),
|
||||
type: debugFlags.type,
|
||||
mappedFrom: debugFlags.tempMap,
|
||||
textureMap: debugFlags.textureMap,
|
||||
errors: debugFlags.errors
|
||||
})
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
results.push({
|
||||
entity,
|
||||
supported: false,
|
||||
mappedFrom: debugFlags.tempMap
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Remove loading indicator
|
||||
loading.remove()
|
||||
|
||||
const createSection = (title: string, items: any[], filter: (item: any) => boolean) => {
|
||||
const section = document.createElement('div')
|
||||
section.style.marginBottom = '20px'
|
||||
|
||||
const sectionTitle = document.createElement('h3')
|
||||
sectionTitle.textContent = title
|
||||
sectionTitle.style.textAlign = 'center'
|
||||
section.appendChild(sectionTitle)
|
||||
|
||||
const list = document.createElement('ul')
|
||||
list.style.cssText = 'padding-left: 20px; list-style-type: none; margin: 0;'
|
||||
|
||||
const filteredItems = items.filter(filter)
|
||||
for (const item of filteredItems) {
|
||||
const listItem = document.createElement('li')
|
||||
listItem.style.cssText = 'line-height: 1.4; margin: 8px 0;'
|
||||
|
||||
const entityName = document.createElement('strong')
|
||||
entityName.style.cssText = 'user-select: text;-webkit-user-select: text;'
|
||||
entityName.textContent = item.entity
|
||||
listItem.appendChild(entityName)
|
||||
|
||||
let text = ''
|
||||
if (item.mappedFrom) {
|
||||
text += ` -> ${item.mappedFrom}`
|
||||
}
|
||||
if (item.type) {
|
||||
text += ` - ${item.type}`
|
||||
}
|
||||
if (item.textureMap) {
|
||||
text += ' ⚠️'
|
||||
}
|
||||
if (item.errors) {
|
||||
text += ' ❌'
|
||||
}
|
||||
|
||||
listItem.appendChild(document.createTextNode(text))
|
||||
list.appendChild(listItem)
|
||||
}
|
||||
|
||||
section.appendChild(list)
|
||||
return { section, count: filteredItems.length }
|
||||
}
|
||||
|
||||
// Sort results - bedrock first
|
||||
results.sort((a, b) => {
|
||||
if (a.type === 'bedrock' && b.type !== 'bedrock') return -1
|
||||
if (a.type !== 'bedrock' && b.type === 'bedrock') return 1
|
||||
return a.entity.localeCompare(b.entity)
|
||||
})
|
||||
|
||||
// Add sections
|
||||
const sections = [
|
||||
{
|
||||
title: '❌ Unsupported Entities',
|
||||
filter: (r: any) => !r.supported && !r.mappedFrom
|
||||
},
|
||||
{
|
||||
title: '⚠️ Partially Supported Entities',
|
||||
filter: (r: any) => r.mappedFrom
|
||||
},
|
||||
{
|
||||
title: '✅ Supported Entities',
|
||||
filter: (r: any) => r.supported && !r.mappedFrom
|
||||
}
|
||||
]
|
||||
|
||||
for (const { title, filter } of sections) {
|
||||
const { section, count } = createSection(title, results, filter)
|
||||
if (count > 0) {
|
||||
container.appendChild(section)
|
||||
}
|
||||
}
|
||||
|
||||
// log object with errors per entity
|
||||
const errors = results.filter(r => r.errors).map(r => ({
|
||||
entity: r.entity,
|
||||
errors: r.errors
|
||||
}))
|
||||
console.log(errors)
|
||||
}
|
||||
414
renderer/playground/baseScene.ts
Normal file
414
renderer/playground/baseScene.ts
Normal file
|
|
@ -0,0 +1,414 @@
|
|||
//@ts-nocheck
|
||||
import { Vec3 } from 'vec3'
|
||||
import * as THREE from 'three'
|
||||
import '../../src/getCollisionShapes'
|
||||
import { IndexedData } from 'minecraft-data'
|
||||
import BlockLoader from 'prismarine-block'
|
||||
import blockstatesModels from 'mc-assets/dist/blockStatesModels.json'
|
||||
import ChunkLoader from 'prismarine-chunk'
|
||||
import WorldLoader from 'prismarine-world'
|
||||
|
||||
//@ts-expect-error
|
||||
import { OrbitControls } from 'three/addons/controls/OrbitControls.js'
|
||||
// eslint-disable-next-line import/no-named-as-default
|
||||
import GUI from 'lil-gui'
|
||||
import _ from 'lodash'
|
||||
import { toMajorVersion } from '../../src/utils'
|
||||
import { WorldDataEmitter } from '../viewer'
|
||||
import { Viewer } from '../viewer/lib/viewer'
|
||||
import { BlockNames } from '../../src/mcDataTypes'
|
||||
import { initWithRenderer, statsEnd, statsStart } from '../../src/topRightStats'
|
||||
import { defaultWorldRendererConfig } from '../viewer/lib/worldrendererCommon'
|
||||
import { getSyncWorld } from './shared'
|
||||
|
||||
window.THREE = THREE
|
||||
|
||||
export class BasePlaygroundScene {
|
||||
continuousRender = false
|
||||
stopRender = false
|
||||
guiParams = {}
|
||||
viewDistance = 0
|
||||
targetPos = new Vec3(2, 90, 2)
|
||||
params = {} as Record<string, any>
|
||||
paramOptions = {} as Partial<Record<keyof typeof this.params, {
|
||||
hide?: boolean
|
||||
options?: string[]
|
||||
min?: number
|
||||
max?: number
|
||||
reloadOnChange?: boolean
|
||||
}>>
|
||||
version = new URLSearchParams(window.location.search).get('version') || globalThis.includedVersions.at(-1)
|
||||
Chunk: typeof import('prismarine-chunk/types/index').PCChunk
|
||||
Block: typeof import('prismarine-block').Block
|
||||
ignoreResize = false
|
||||
enableCameraControls = true // not finished
|
||||
enableCameraOrbitControl = true
|
||||
gui = new GUI()
|
||||
onParamUpdate = {} as Record<string, () => void>
|
||||
alwaysIgnoreQs = [] as string[]
|
||||
skipUpdateQs = false
|
||||
controls: any
|
||||
windowHidden = false
|
||||
world: ReturnType<typeof getSyncWorld>
|
||||
|
||||
_worldConfig = defaultWorldRendererConfig
|
||||
get worldConfig () {
|
||||
return this._worldConfig
|
||||
}
|
||||
set worldConfig (value) {
|
||||
this._worldConfig = value
|
||||
viewer.world.config = value
|
||||
}
|
||||
|
||||
constructor () {
|
||||
void this.initData().then(() => {
|
||||
this.addKeyboardShortcuts()
|
||||
})
|
||||
}
|
||||
|
||||
onParamsUpdate (paramName: string, object: any) {}
|
||||
updateQs (paramName: string, valueSet: any) {
|
||||
if (this.skipUpdateQs) return
|
||||
const newQs = new URLSearchParams(window.location.search)
|
||||
// if (oldQs.get('scene')) {
|
||||
// newQs.set('scene', oldQs.get('scene')!)
|
||||
// }
|
||||
for (const [key, value] of Object.entries({ [paramName]: valueSet })) {
|
||||
if (typeof value === 'function' || this.params.skipQs?.includes(key) || this.alwaysIgnoreQs.includes(key)) continue
|
||||
if (value) {
|
||||
newQs.set(key, value)
|
||||
} else {
|
||||
newQs.delete(key)
|
||||
}
|
||||
}
|
||||
window.history.replaceState({}, '', `${window.location.pathname}?${newQs.toString()}`)
|
||||
}
|
||||
|
||||
// async initialSetup () {}
|
||||
renderFinish () {
|
||||
this.render()
|
||||
}
|
||||
|
||||
initGui () {
|
||||
const qs = new URLSearchParams(window.location.search)
|
||||
for (const key of Object.keys(this.params)) {
|
||||
const value = qs.get(key)
|
||||
if (!value) continue
|
||||
const parsed = /^-?\d+$/.test(value) ? Number(value) : value === 'true' ? true : value === 'false' ? false : value
|
||||
this.params[key] = parsed
|
||||
}
|
||||
|
||||
for (const param of Object.keys(this.params)) {
|
||||
const option = this.paramOptions[param]
|
||||
if (option?.hide) continue
|
||||
this.gui.add(this.params, param, option?.options ?? option?.min, option?.max)
|
||||
}
|
||||
if (window.innerHeight < 700) {
|
||||
this.gui.open(false)
|
||||
} else {
|
||||
// const observer = new MutationObserver(() => {
|
||||
// this.gui.domElement.classList.remove('transition')
|
||||
// })
|
||||
// observer.observe(this.gui.domElement, {
|
||||
// attributes: true,
|
||||
// attributeFilter: ['class'],
|
||||
// })
|
||||
setTimeout(() => {
|
||||
this.gui.domElement.classList.remove('transition')
|
||||
}, 500)
|
||||
}
|
||||
|
||||
this.gui.onChange(({ property, object }) => {
|
||||
if (object === this.params) {
|
||||
this.onParamUpdate[property]?.()
|
||||
this.onParamsUpdate(property, object)
|
||||
const value = this.params[property]
|
||||
if (this.paramOptions[property]?.reloadOnChange && (typeof value === 'boolean' || this.paramOptions[property].options)) {
|
||||
setTimeout(() => {
|
||||
window.location.reload()
|
||||
})
|
||||
}
|
||||
this.updateQs(property, value)
|
||||
} else {
|
||||
this.onParamsUpdate(property, object)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// mainChunk: import('prismarine-chunk/types/index').PCChunk
|
||||
|
||||
// overridables
|
||||
setupWorld () { }
|
||||
sceneReset () {}
|
||||
|
||||
// eslint-disable-next-line max-params
|
||||
addWorldBlock (xOffset: number, yOffset: number, zOffset: number, blockName: BlockNames, properties?: Record<string, any>) {
|
||||
if (xOffset > 16 || yOffset > 16 || zOffset > 16) throw new Error('Offset too big')
|
||||
const block =
|
||||
properties ?
|
||||
this.Block.fromProperties(loadedData.blocksByName[blockName].id, properties ?? {}, 0) :
|
||||
this.Block.fromStateId(loadedData.blocksByName[blockName].defaultState, 0)
|
||||
this.world.setBlock(this.targetPos.offset(xOffset, yOffset, zOffset), block)
|
||||
}
|
||||
|
||||
resetCamera () {
|
||||
const { targetPos } = this
|
||||
this.controls?.target.set(targetPos.x + 0.5, targetPos.y + 0.5, targetPos.z + 0.5)
|
||||
|
||||
const cameraPos = targetPos.offset(2, 2, 2)
|
||||
const pitch = THREE.MathUtils.degToRad(-45)
|
||||
const yaw = THREE.MathUtils.degToRad(45)
|
||||
viewer.camera.rotation.set(pitch, yaw, 0, 'ZYX')
|
||||
viewer.camera.lookAt(targetPos.x + 0.5, targetPos.y + 0.5, targetPos.z + 0.5)
|
||||
viewer.camera.position.set(cameraPos.x + 0.5, cameraPos.y + 0.5, cameraPos.z + 0.5)
|
||||
this.controls?.update()
|
||||
}
|
||||
|
||||
async initData () {
|
||||
await window._LOAD_MC_DATA()
|
||||
const mcData: IndexedData = require('minecraft-data')(this.version)
|
||||
window.loadedData = window.mcData = mcData
|
||||
|
||||
this.Chunk = (ChunkLoader as any)(this.version)
|
||||
this.Block = (BlockLoader as any)(this.version)
|
||||
|
||||
const world = getSyncWorld(this.version)
|
||||
world.setBlockStateId(this.targetPos, 0)
|
||||
this.world = world
|
||||
|
||||
this.initGui()
|
||||
|
||||
const worldView = new WorldDataEmitter(world, this.viewDistance, this.targetPos)
|
||||
worldView.addWaitTime = 0
|
||||
window.worldView = worldView
|
||||
|
||||
// Create three.js context, add to page
|
||||
const renderer = new THREE.WebGLRenderer({ alpha: true, ...localStorage['renderer'] })
|
||||
renderer.setPixelRatio(window.devicePixelRatio || 1)
|
||||
renderer.setSize(window.innerWidth, window.innerHeight)
|
||||
|
||||
// Create viewer
|
||||
const viewer = new Viewer(renderer, this.worldConfig)
|
||||
window.viewer = viewer
|
||||
window.world = window.viewer.world
|
||||
const isWebgpu = false
|
||||
const promises = [] as Array<Promise<void>>
|
||||
if (isWebgpu) {
|
||||
// promises.push(initWebgpuRenderer(() => { }, true, true)) // todo
|
||||
} else {
|
||||
initWithRenderer(renderer.domElement)
|
||||
renderer.domElement.id = 'viewer-canvas'
|
||||
document.body.appendChild(renderer.domElement)
|
||||
}
|
||||
viewer.addChunksBatchWaitTime = 0
|
||||
viewer.world.blockstatesModels = blockstatesModels
|
||||
viewer.entities.setDebugMode('basic')
|
||||
viewer.setVersion(this.version)
|
||||
viewer.entities.onSkinUpdate = () => {
|
||||
viewer.render()
|
||||
}
|
||||
viewer.world.mesherConfig.enableLighting = false
|
||||
await Promise.all(promises)
|
||||
this.setupWorld()
|
||||
|
||||
viewer.connect(worldView)
|
||||
|
||||
await worldView.init(this.targetPos)
|
||||
|
||||
if (this.enableCameraControls) {
|
||||
const { targetPos } = this
|
||||
const canvas = document.querySelector('#viewer-canvas')
|
||||
const controls = this.enableCameraOrbitControl ? new OrbitControls(viewer.camera, canvas) : undefined
|
||||
this.controls = controls
|
||||
|
||||
this.resetCamera()
|
||||
|
||||
// #region camera rotation param
|
||||
const cameraSet = this.params.camera || localStorage.camera
|
||||
if (cameraSet) {
|
||||
const [x, y, z, rx, ry] = cameraSet.split(',').map(Number)
|
||||
viewer.camera.position.set(x, y, z)
|
||||
viewer.camera.rotation.set(rx, ry, 0, 'ZYX')
|
||||
this.controls?.update()
|
||||
}
|
||||
const throttledCamQsUpdate = _.throttle(() => {
|
||||
const { camera } = viewer
|
||||
// params.camera = `${camera.rotation.x.toFixed(2)},${camera.rotation.y.toFixed(2)}`
|
||||
// this.updateQs()
|
||||
localStorage.camera = [
|
||||
camera.position.x.toFixed(2),
|
||||
camera.position.y.toFixed(2),
|
||||
camera.position.z.toFixed(2),
|
||||
camera.rotation.x.toFixed(2),
|
||||
camera.rotation.y.toFixed(2),
|
||||
].join(',')
|
||||
}, 200)
|
||||
if (this.controls) {
|
||||
this.controls.addEventListener('change', () => {
|
||||
throttledCamQsUpdate()
|
||||
this.render()
|
||||
})
|
||||
} else {
|
||||
setInterval(() => {
|
||||
throttledCamQsUpdate()
|
||||
}, 200)
|
||||
}
|
||||
// #endregion
|
||||
}
|
||||
|
||||
if (!this.enableCameraOrbitControl) {
|
||||
// mouse
|
||||
let mouseMoveCounter = 0
|
||||
const mouseMove = (e: PointerEvent) => {
|
||||
if ((e.target as HTMLElement).closest('.lil-gui')) return
|
||||
if (e.buttons === 1 || e.pointerType === 'touch') {
|
||||
mouseMoveCounter++
|
||||
viewer.camera.rotation.x -= e.movementY / 100
|
||||
//viewer.camera.
|
||||
viewer.camera.rotation.y -= e.movementX / 100
|
||||
if (viewer.camera.rotation.x < -Math.PI / 2) viewer.camera.rotation.x = -Math.PI / 2
|
||||
if (viewer.camera.rotation.x > Math.PI / 2) viewer.camera.rotation.x = Math.PI / 2
|
||||
|
||||
// yaw += e.movementY / 20;
|
||||
// pitch += e.movementX / 20;
|
||||
}
|
||||
if (e.buttons === 2) {
|
||||
viewer.camera.position.set(0, 0, 0)
|
||||
}
|
||||
}
|
||||
setInterval(() => {
|
||||
// updateTextEvent(`Mouse Events: ${mouseMoveCounter}`)
|
||||
mouseMoveCounter = 0
|
||||
}, 1000)
|
||||
window.addEventListener('pointermove', mouseMove)
|
||||
}
|
||||
|
||||
// await this.initialSetup()
|
||||
this.onResize()
|
||||
window.addEventListener('resize', () => this.onResize())
|
||||
void viewer.waitForChunksToRender().then(async () => {
|
||||
this.renderFinish()
|
||||
})
|
||||
|
||||
viewer.world.renderUpdateEmitter.addListener('update', () => {
|
||||
this.render()
|
||||
})
|
||||
|
||||
this.loop()
|
||||
}
|
||||
|
||||
loop () {
|
||||
if (this.continuousRender && !this.windowHidden) {
|
||||
this.render(true)
|
||||
requestAnimationFrame(() => this.loop())
|
||||
}
|
||||
}
|
||||
|
||||
render (fromLoop = false) {
|
||||
if (!fromLoop && this.continuousRender) return
|
||||
if (this.stopRender) return
|
||||
statsStart()
|
||||
viewer.render()
|
||||
statsEnd()
|
||||
}
|
||||
|
||||
addKeyboardShortcuts () {
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (!e.shiftKey && !e.ctrlKey && !e.altKey && !e.metaKey) {
|
||||
if (e.code === 'KeyR') {
|
||||
this.controls?.reset()
|
||||
this.resetCamera()
|
||||
}
|
||||
if (e.code === 'KeyE') { // refresh block (main)
|
||||
worldView!.setBlockStateId(this.targetPos, this.world.getBlockStateId(this.targetPos))
|
||||
}
|
||||
if (e.code === 'KeyF') { // reload all chunks
|
||||
this.sceneReset()
|
||||
worldView!.unloadAllChunks()
|
||||
void worldView!.init(this.targetPos)
|
||||
}
|
||||
}
|
||||
})
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
this.windowHidden = document.visibilityState === 'hidden'
|
||||
})
|
||||
document.addEventListener('blur', () => {
|
||||
this.windowHidden = true
|
||||
})
|
||||
document.addEventListener('focus', () => {
|
||||
this.windowHidden = false
|
||||
})
|
||||
|
||||
const updateKeys = () => {
|
||||
if (pressedKeys.has('ControlLeft') || pressedKeys.has('MetaLeft')) {
|
||||
return
|
||||
}
|
||||
// if (typeof viewer === 'undefined') return
|
||||
// Create a vector that points in the direction the camera is looking
|
||||
const direction = new THREE.Vector3(0, 0, 0)
|
||||
if (pressedKeys.has('KeyW')) {
|
||||
direction.z = -0.5
|
||||
}
|
||||
if (pressedKeys.has('KeyS')) {
|
||||
direction.z += 0.5
|
||||
}
|
||||
if (pressedKeys.has('KeyA')) {
|
||||
direction.x -= 0.5
|
||||
}
|
||||
if (pressedKeys.has('KeyD')) {
|
||||
direction.x += 0.5
|
||||
}
|
||||
|
||||
|
||||
if (pressedKeys.has('ShiftLeft')) {
|
||||
viewer.camera.position.y -= 0.5
|
||||
}
|
||||
if (pressedKeys.has('Space')) {
|
||||
viewer.camera.position.y += 0.5
|
||||
}
|
||||
direction.applyQuaternion(viewer.camera.quaternion)
|
||||
direction.y = 0
|
||||
|
||||
if (pressedKeys.has('ShiftLeft')) {
|
||||
direction.y *= 2
|
||||
direction.x *= 2
|
||||
direction.z *= 2
|
||||
}
|
||||
// Add the vector to the camera's position to move the camera
|
||||
viewer.camera.position.add(direction.normalize())
|
||||
this.controls?.update()
|
||||
this.render()
|
||||
}
|
||||
setInterval(updateKeys, 1000 / 30)
|
||||
|
||||
const pressedKeys = new Set<string>()
|
||||
const keys = (e) => {
|
||||
const { code } = e
|
||||
const pressed = e.type === 'keydown'
|
||||
if (pressed) {
|
||||
pressedKeys.add(code)
|
||||
} else {
|
||||
pressedKeys.delete(code)
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('keydown', keys)
|
||||
window.addEventListener('keyup', keys)
|
||||
window.addEventListener('blur', (e) => {
|
||||
for (const key of pressedKeys) {
|
||||
keys(new KeyboardEvent('keyup', { code: key }))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
onResize () {
|
||||
if (this.ignoreResize) return
|
||||
|
||||
const { camera, renderer } = viewer
|
||||
viewer.camera.aspect = window.innerWidth / window.innerHeight
|
||||
viewer.camera.updateProjectionMatrix()
|
||||
renderer.setSize(window.innerWidth, window.innerHeight)
|
||||
|
||||
this.render()
|
||||
}
|
||||
}
|
||||
12
renderer/playground/playground.ts
Normal file
12
renderer/playground/playground.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
if (!new URL(location.href).searchParams.get('playground')) location.href = '/?playground=true'
|
||||
// import { BasePlaygroundScene } from './baseScene'
|
||||
// import { playgroundGlobalUiState } from './playgroundUi'
|
||||
// import * as scenes from './scenes'
|
||||
|
||||
// const qsScene = new URLSearchParams(window.location.search).get('scene')
|
||||
// const Scene: typeof BasePlaygroundScene = qsScene ? scenes[qsScene] : scenes.main
|
||||
// playgroundGlobalUiState.scenes = ['main', 'railsCobweb', 'floorRandom', 'lightingStarfield', 'transparencyIssue', 'entities', 'frequentUpdates', 'slabsOptimization', 'allEntities']
|
||||
// playgroundGlobalUiState.selected = qsScene ?? 'main'
|
||||
|
||||
// const scene = new Scene()
|
||||
// globalThis.scene = scene
|
||||
175
renderer/playground/playgroundUi.tsx
Normal file
175
renderer/playground/playgroundUi.tsx
Normal file
|
|
@ -0,0 +1,175 @@
|
|||
import { renderToDom } from '@zardoy/react-util'
|
||||
import { useEffect } from 'react'
|
||||
import { proxy, useSnapshot } from 'valtio'
|
||||
import { LeftTouchArea, RightTouchArea, useInterfaceState } from '@dimaka/interface'
|
||||
import { css } from '@emotion/css'
|
||||
import { Vec3 } from 'vec3'
|
||||
import useLongPress from '../../src/react/useLongPress'
|
||||
import { isMobile } from '../viewer/lib/simpleUtils'
|
||||
|
||||
export const playgroundGlobalUiState = proxy({
|
||||
scenes: [] as string[],
|
||||
selected: '',
|
||||
selectorOpened: false,
|
||||
actions: {} as Record<string, () => void>,
|
||||
})
|
||||
|
||||
renderToDom(<Playground />)
|
||||
|
||||
function Playground () {
|
||||
useEffect(() => {
|
||||
const style = document.createElement('style')
|
||||
style.innerHTML = /* css */ `
|
||||
.lil-gui {
|
||||
top: 60px !important;
|
||||
right: 0 !important;
|
||||
}
|
||||
`
|
||||
document.body.appendChild(style)
|
||||
return () => {
|
||||
style.remove()
|
||||
}
|
||||
}, [])
|
||||
|
||||
return <div style={{
|
||||
fontFamily: 'monospace',
|
||||
color: 'white',
|
||||
}}>
|
||||
<Controls />
|
||||
<SceneSelector />
|
||||
<ActionsSelector />
|
||||
</div>
|
||||
}
|
||||
|
||||
function SceneSelector () {
|
||||
const mobile = isMobile()
|
||||
const { scenes, selected } = useSnapshot(playgroundGlobalUiState)
|
||||
const longPressEvents = useLongPress(() => {
|
||||
playgroundGlobalUiState.selectorOpened = true
|
||||
}, () => { })
|
||||
|
||||
return <div
|
||||
style={{
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
left: 0,
|
||||
}} {...longPressEvents}>
|
||||
{scenes.map(scene => <div
|
||||
key={scene}
|
||||
style={{
|
||||
padding: mobile ? '5px' : '2px 5px',
|
||||
cursor: 'pointer',
|
||||
userSelect: 'none',
|
||||
background: scene === selected ? 'rgba(0, 0, 0, 0.5)' : 'rgba(0, 0, 0, 0.6)',
|
||||
fontWeight: scene === selected ? 'bold' : 'normal',
|
||||
}}
|
||||
onClick={() => {
|
||||
const qs = new URLSearchParams(window.location.search)
|
||||
qs.set('scene', scene)
|
||||
location.search = qs.toString()
|
||||
}}
|
||||
>{scene}</div>)}
|
||||
</div>
|
||||
}
|
||||
|
||||
const ActionsSelector = () => {
|
||||
const { actions, selectorOpened } = useSnapshot(playgroundGlobalUiState)
|
||||
|
||||
if (!selectorOpened) return null
|
||||
return <div style={{
|
||||
position: 'fixed',
|
||||
inset: 0,
|
||||
background: 'rgba(0, 0, 0, 0.5)',
|
||||
zIndex: 10,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
gap: 5,
|
||||
fontSize: 24,
|
||||
}}>{Object.entries({
|
||||
...actions,
|
||||
'Close' () {
|
||||
playgroundGlobalUiState.selectorOpened = false
|
||||
}
|
||||
}).map(([name, action]) => <div
|
||||
key={name}
|
||||
style={{
|
||||
padding: '2px 5px',
|
||||
cursor: 'pointer',
|
||||
userSelect: 'none',
|
||||
background: 'rgba(0, 0, 0, 0.5)',
|
||||
}}
|
||||
onClick={() => {
|
||||
action()
|
||||
playgroundGlobalUiState.selectorOpened = false
|
||||
}}
|
||||
>{name}</div>)}</div>
|
||||
}
|
||||
|
||||
const Controls = () => {
|
||||
// todo setting
|
||||
const usingTouch = navigator.maxTouchPoints > 0
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('touchstart', (e) => {
|
||||
e.preventDefault()
|
||||
})
|
||||
|
||||
const pressedKeys = new Set<string>()
|
||||
useInterfaceState.setState({
|
||||
isFlying: false,
|
||||
uiCustomization: {
|
||||
touchButtonSize: 40,
|
||||
},
|
||||
updateCoord ([coord, state]) {
|
||||
const vec3 = new Vec3(0, 0, 0)
|
||||
vec3[coord] = state
|
||||
let key: string | undefined
|
||||
if (vec3.z < 0) key = 'KeyW'
|
||||
if (vec3.z > 0) key = 'KeyS'
|
||||
if (vec3.y > 0) key = 'Space'
|
||||
if (vec3.y < 0) key = 'ShiftLeft'
|
||||
if (vec3.x < 0) key = 'KeyA'
|
||||
if (vec3.x > 0) key = 'KeyD'
|
||||
if (key) {
|
||||
if (!pressedKeys.has(key)) {
|
||||
pressedKeys.add(key)
|
||||
window.dispatchEvent(new KeyboardEvent('keydown', { code: key }))
|
||||
}
|
||||
}
|
||||
for (const k of pressedKeys) {
|
||||
if (k !== key) {
|
||||
window.dispatchEvent(new KeyboardEvent('keyup', { code: k }))
|
||||
pressedKeys.delete(k)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}, [])
|
||||
|
||||
if (!usingTouch) return null
|
||||
return (
|
||||
<div
|
||||
style={{ zIndex: 8 }}
|
||||
className={css`
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
align-items: flex-end;
|
||||
pointer-events: none;
|
||||
touch-action: none;
|
||||
& > div {
|
||||
pointer-events: auto;
|
||||
}
|
||||
`}
|
||||
>
|
||||
<LeftTouchArea />
|
||||
<div />
|
||||
<RightTouchArea />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
13
renderer/playground/scenes/allEntities.ts
Normal file
13
renderer/playground/scenes/allEntities.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import { BasePlaygroundScene } from '../baseScene'
|
||||
import { EntityDebugFlags, EntityMesh, rendererSpecialHandled } from '../../viewer/three/entity/EntityMesh'
|
||||
import { displayEntitiesDebugList } from '../allEntitiesDebug'
|
||||
|
||||
export default class AllEntities extends BasePlaygroundScene {
|
||||
continuousRender = false
|
||||
enableCameraControls = false
|
||||
|
||||
async initData () {
|
||||
await super.initData()
|
||||
displayEntitiesDebugList(this.version)
|
||||
}
|
||||
}
|
||||
37
renderer/playground/scenes/entities.ts
Normal file
37
renderer/playground/scenes/entities.ts
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
//@ts-nocheck
|
||||
import * as THREE from 'three'
|
||||
import { Vec3 } from 'vec3'
|
||||
import { BasePlaygroundScene } from '../baseScene'
|
||||
import { WorldRendererThree } from '../../viewer/three/worldrendererThree'
|
||||
|
||||
export default class extends BasePlaygroundScene {
|
||||
continuousRender = true
|
||||
|
||||
override initGui (): void {
|
||||
this.params = {
|
||||
starfield: false,
|
||||
entity: 'player',
|
||||
count: 4
|
||||
}
|
||||
}
|
||||
|
||||
override renderFinish (): void {
|
||||
if (this.params.starfield) {
|
||||
;(viewer.world as WorldRendererThree).scene.background = new THREE.Color(0x00_00_00)
|
||||
;(viewer.world as WorldRendererThree).starField.enabled = true
|
||||
;(viewer.world as WorldRendererThree).starField.addToScene()
|
||||
}
|
||||
|
||||
for (let i = 0; i < this.params.count; i++) {
|
||||
for (let j = 0; j < this.params.count; j++) {
|
||||
for (let k = 0; k < this.params.count; k++) {
|
||||
viewer.entities.update({
|
||||
id: i * 1000 + j * 100 + k,
|
||||
name: this.params.entity,
|
||||
pos: this.targetPos.offset(i, j, k)
|
||||
} as any, {})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
33
renderer/playground/scenes/floorRandom.ts
Normal file
33
renderer/playground/scenes/floorRandom.ts
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import { BasePlaygroundScene } from '../baseScene'
|
||||
|
||||
export default class RailsCobwebScene extends BasePlaygroundScene {
|
||||
viewDistance = 5
|
||||
continuousRender = true
|
||||
|
||||
override initGui (): void {
|
||||
this.params = {
|
||||
squareSize: 50
|
||||
}
|
||||
|
||||
super.initGui()
|
||||
}
|
||||
|
||||
setupWorld () {
|
||||
const squareSize = this.params.squareSize ?? 30
|
||||
const maxSquareSize = this.viewDistance * 16 * 2
|
||||
if (squareSize > maxSquareSize) throw new Error(`Square size too big, max is ${maxSquareSize}`)
|
||||
// const fullBlocks = loadedData.blocksArray.map(x => x.name)
|
||||
const fullBlocks = loadedData.blocksArray.filter(block => {
|
||||
const b = this.Block.fromStateId(block.defaultState, 0)
|
||||
if (b.shapes?.length !== 1) return false
|
||||
const shape = b.shapes[0]
|
||||
return shape[0] === 0 && shape[1] === 0 && shape[2] === 0 && shape[3] === 1 && shape[4] === 1 && shape[5] === 1
|
||||
})
|
||||
for (let x = -squareSize; x <= squareSize; x++) {
|
||||
for (let z = -squareSize; z <= squareSize; z++) {
|
||||
const i = Math.abs(x + z) * squareSize
|
||||
worldView!.world.setBlock(this.targetPos.offset(x, 0, z), this.Block.fromStateId(fullBlocks[i % fullBlocks.length].defaultState, 0))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
148
renderer/playground/scenes/frequentUpdates.ts
Normal file
148
renderer/playground/scenes/frequentUpdates.ts
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
//@ts-nocheck
|
||||
import { Vec3 } from 'vec3'
|
||||
import { BasePlaygroundScene } from '../baseScene'
|
||||
|
||||
export default class extends BasePlaygroundScene {
|
||||
viewDistance = 5
|
||||
continuousRender = true
|
||||
|
||||
override initGui (): void {
|
||||
this.params = {
|
||||
testActive: false,
|
||||
testUpdatesPerSecond: 10,
|
||||
testInitialUpdate: false,
|
||||
stopGeometryUpdate: false,
|
||||
manualTest: () => {
|
||||
this.updateBlock()
|
||||
},
|
||||
testNeighborUpdates: () => {
|
||||
this.testNeighborUpdates()
|
||||
}
|
||||
}
|
||||
|
||||
super.initGui()
|
||||
}
|
||||
|
||||
lastUpdatedOffset = 0
|
||||
lastUpdatedId = 2
|
||||
updateBlock () {
|
||||
const x = this.lastUpdatedOffset % 16
|
||||
const z = Math.floor(this.lastUpdatedOffset / 16)
|
||||
const y = 90
|
||||
worldView!.setBlockStateId(new Vec3(x, y, z), this.lastUpdatedId++)
|
||||
this.lastUpdatedOffset++
|
||||
if (this.lastUpdatedOffset > 16 * 16) this.lastUpdatedOffset = 0
|
||||
if (this.lastUpdatedId > 500) this.lastUpdatedId = 1
|
||||
}
|
||||
|
||||
testNeighborUpdates () {
|
||||
viewer.world.setBlockStateId(new Vec3(15, 95, 15), 1)
|
||||
viewer.world.setBlockStateId(new Vec3(0, 95, 15), 1)
|
||||
viewer.world.setBlockStateId(new Vec3(15, 95, 0), 1)
|
||||
viewer.world.setBlockStateId(new Vec3(0, 95, 0), 1)
|
||||
|
||||
viewer.world.setBlockStateId(new Vec3(16, 95, 15), 1)
|
||||
viewer.world.setBlockStateId(new Vec3(-1, 95, 15), 1)
|
||||
viewer.world.setBlockStateId(new Vec3(15, 95, -1), 1)
|
||||
viewer.world.setBlockStateId(new Vec3(-1, 95, 0), 1)
|
||||
setTimeout(() => {
|
||||
viewer.world.setBlockStateId(new Vec3(16, 96, 16), 1)
|
||||
viewer.world.setBlockStateId(new Vec3(-1, 96, 16), 1)
|
||||
viewer.world.setBlockStateId(new Vec3(16, 96, -1), 1)
|
||||
viewer.world.setBlockStateId(new Vec3(-1, 96, -1), 1)
|
||||
}, 3000)
|
||||
}
|
||||
|
||||
setupTimer () {
|
||||
// this.stopRender = true
|
||||
|
||||
let lastTime = 0
|
||||
const tick = () => {
|
||||
viewer.world.debugStopGeometryUpdate = this.params.stopGeometryUpdate
|
||||
const updateEach = 1000 / this.params.testUpdatesPerSecond
|
||||
requestAnimationFrame(tick)
|
||||
if (!this.params.testActive) return
|
||||
const updateCount = Math.floor(performance.now() - lastTime) / updateEach
|
||||
for (let i = 0; i < updateCount; i++) {
|
||||
this.updateBlock()
|
||||
}
|
||||
lastTime = performance.now()
|
||||
}
|
||||
|
||||
requestAnimationFrame(tick)
|
||||
|
||||
// const limit = 1000
|
||||
// const limit = 100
|
||||
// const limit = 1
|
||||
// const updatedChunks = new Set<string>()
|
||||
// const updatedBlocks = new Set<string>()
|
||||
// let lastSecond = 0
|
||||
// setInterval(() => {
|
||||
// const second = Math.floor(performance.now() / 1000)
|
||||
// if (lastSecond !== second) {
|
||||
// lastSecond = second
|
||||
// updatedChunks.clear()
|
||||
// updatedBlocks.clear()
|
||||
// }
|
||||
// const isEven = second % 2 === 0
|
||||
// if (updatedBlocks.size > limit) {
|
||||
// return
|
||||
// }
|
||||
// const changeBlock = (x, z) => {
|
||||
// const chunkKey = `${Math.floor(x / 16)},${Math.floor(z / 16)}`
|
||||
// const key = `${x},${z}`
|
||||
// if (updatedBlocks.has(chunkKey)) return
|
||||
|
||||
// updatedChunks.add(chunkKey)
|
||||
// worldView!.world.setBlock(this.targetPos.offset(x, 0, z), this.Block.fromStateId(isEven ? 2 : 3, 0))
|
||||
// updatedBlocks.add(key)
|
||||
// }
|
||||
// const { squareSize } = this.params
|
||||
// const xStart = -squareSize
|
||||
// const zStart = -squareSize
|
||||
// const xEnd = squareSize
|
||||
// const zEnd = squareSize
|
||||
// for (let x = xStart; x <= xEnd; x += 16) {
|
||||
// for (let z = zStart; z <= zEnd; z += 16) {
|
||||
// const key = `${x},${z}`
|
||||
// if (updatedChunks.has(key)) continue
|
||||
// changeBlock(x, z)
|
||||
// return
|
||||
// }
|
||||
// }
|
||||
// for (let x = xStart; x <= xEnd; x += 16) {
|
||||
// for (let z = zStart; z <= zEnd; z += 16) {
|
||||
// const key = `${x},${z}`
|
||||
// if (updatedChunks.has(key)) continue
|
||||
// changeBlock(x, z)
|
||||
// return
|
||||
// }
|
||||
// }
|
||||
// }, 1)
|
||||
}
|
||||
|
||||
setupWorld () {
|
||||
this.worldConfig.showChunkBorders = true
|
||||
|
||||
const maxSquareRadius = this.viewDistance * 16
|
||||
// const fullBlocks = loadedData.blocksArray.map(x => x.name)
|
||||
const squareSize = maxSquareRadius
|
||||
for (let x = -squareSize; x <= squareSize; x++) {
|
||||
for (let z = -squareSize; z <= squareSize; z++) {
|
||||
const i = Math.abs(x + z) * squareSize
|
||||
worldView!.world.setBlock(this.targetPos.offset(x, 0, z), this.Block.fromStateId(1, 0))
|
||||
}
|
||||
}
|
||||
let done = false
|
||||
viewer.world.renderUpdateEmitter.on('update', () => {
|
||||
if (!viewer.world.allChunksFinished || done) return
|
||||
done = true
|
||||
this.setupTimer()
|
||||
})
|
||||
setTimeout(() => {
|
||||
if (this.params.testInitialUpdate) {
|
||||
this.updateBlock()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
11
renderer/playground/scenes/index.ts
Normal file
11
renderer/playground/scenes/index.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
// export { default as rotation } from './rotation'
|
||||
export { default as main } from './main'
|
||||
export { default as railsCobweb } from './railsCobweb'
|
||||
export { default as floorRandom } from './floorRandom'
|
||||
export { default as lightingStarfield } from './lightingStarfield'
|
||||
export { default as transparencyIssue } from './transparencyIssue'
|
||||
export { default as rotationIssue } from './rotationIssue'
|
||||
export { default as entities } from './entities'
|
||||
export { default as frequentUpdates } from './frequentUpdates'
|
||||
export { default as slabsOptimization } from './slabsOptimization'
|
||||
export { default as allEntities } from './allEntities'
|
||||
40
renderer/playground/scenes/lightingStarfield.ts
Normal file
40
renderer/playground/scenes/lightingStarfield.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
//@ts-nocheck
|
||||
import * as THREE from 'three'
|
||||
import { Vec3 } from 'vec3'
|
||||
import { BasePlaygroundScene } from '../baseScene'
|
||||
import { WorldRendererThree } from '../../viewer/three/worldrendererThree'
|
||||
|
||||
export default class extends BasePlaygroundScene {
|
||||
continuousRender = true
|
||||
|
||||
override setupWorld (): void {
|
||||
viewer.world.mesherConfig.enableLighting = true
|
||||
viewer.world.mesherConfig.skyLight = 0
|
||||
this.addWorldBlock(0, 0, 0, 'stone')
|
||||
this.addWorldBlock(0, 0, 1, 'stone')
|
||||
this.addWorldBlock(1, 0, 0, 'stone')
|
||||
this.addWorldBlock(1, 0, 1, 'stone')
|
||||
// chess like
|
||||
worldView?.world.setBlockLight(this.targetPos.offset(0, 1, 0), 15)
|
||||
worldView?.world.setBlockLight(this.targetPos.offset(0, 1, 1), 0)
|
||||
worldView?.world.setBlockLight(this.targetPos.offset(1, 1, 0), 0)
|
||||
worldView?.world.setBlockLight(this.targetPos.offset(1, 1, 1), 15)
|
||||
}
|
||||
|
||||
override renderFinish (): void {
|
||||
viewer.scene.background = new THREE.Color(0x00_00_00)
|
||||
// starfield and test entities
|
||||
;(viewer.world as WorldRendererThree).starField.enabled = true
|
||||
;(viewer.world as WorldRendererThree).starField.addToScene()
|
||||
viewer.entities.update({
|
||||
id: 0,
|
||||
name: 'player',
|
||||
pos: this.targetPos.clone()
|
||||
} as any, {})
|
||||
viewer.entities.update({
|
||||
id: 1,
|
||||
name: 'creeper',
|
||||
pos: this.targetPos.offset(1, 0, 0)
|
||||
} as any, {})
|
||||
}
|
||||
}
|
||||
314
renderer/playground/scenes/main.ts
Normal file
314
renderer/playground/scenes/main.ts
Normal file
|
|
@ -0,0 +1,314 @@
|
|||
//@ts-nocheck
|
||||
// eslint-disable-next-line import/no-named-as-default
|
||||
import GUI, { Controller } from 'lil-gui'
|
||||
import * as THREE from 'three'
|
||||
import JSZip from 'jszip'
|
||||
import { BasePlaygroundScene } from '../baseScene'
|
||||
import { TWEEN_DURATION } from '../../viewer/three/entities'
|
||||
import { EntityMesh } from '../../viewer/three/entity/EntityMesh'
|
||||
|
||||
class MainScene extends BasePlaygroundScene {
|
||||
// eslint-disable-next-line @typescript-eslint/no-useless-constructor
|
||||
constructor (...args) {
|
||||
//@ts-expect-error
|
||||
super(...args)
|
||||
}
|
||||
|
||||
override initGui (): void {
|
||||
// initial values
|
||||
this.params = {
|
||||
version: globalThis.includedVersions.at(-1),
|
||||
skipQs: '',
|
||||
block: '',
|
||||
metadata: 0,
|
||||
supportBlock: false,
|
||||
entity: '',
|
||||
removeEntity () {
|
||||
this.entity = ''
|
||||
},
|
||||
entityRotate: false,
|
||||
camera: '',
|
||||
playSound () { },
|
||||
blockIsomorphicRenderBundle () { },
|
||||
modelVariant: 0
|
||||
}
|
||||
this.metadataGui = this.gui.add(this.params, 'metadata')
|
||||
this.paramOptions = {
|
||||
version: {
|
||||
options: globalThis.includedVersions,
|
||||
hide: false
|
||||
},
|
||||
block: {
|
||||
options: mcData.blocksArray.map(b => b.name).sort((a, b) => a.localeCompare(b))
|
||||
},
|
||||
entity: {
|
||||
options: mcData.entitiesArray.map(b => b.name).sort((a, b) => a.localeCompare(b))
|
||||
},
|
||||
camera: {
|
||||
hide: true,
|
||||
}
|
||||
}
|
||||
super.initGui()
|
||||
}
|
||||
|
||||
blockProps = {}
|
||||
metadataFolder: GUI | undefined
|
||||
metadataGui: Controller
|
||||
|
||||
override onParamUpdate = {
|
||||
version () {
|
||||
// if (initialUpdate) return
|
||||
// viewer.world.texturesVersion = params.version
|
||||
// viewer.world.updateTexturesData()
|
||||
// todo warning
|
||||
},
|
||||
block: () => {
|
||||
this.blockProps = {}
|
||||
this.metadataFolder?.destroy()
|
||||
const block = mcData.blocksByName[this.params.block]
|
||||
if (!block) return
|
||||
console.log('block', block.name)
|
||||
const props = new this.Block(block.id, 0, 0).getProperties()
|
||||
const { states } = mcData.blocksByStateId[this.getBlock()?.minStateId] ?? {}
|
||||
this.metadataFolder = this.gui.addFolder('metadata')
|
||||
if (states) {
|
||||
for (const state of states) {
|
||||
let defaultValue: string | number | boolean
|
||||
if (state.values) { // int, enum
|
||||
defaultValue = state.values[0]
|
||||
} else {
|
||||
switch (state.type) {
|
||||
case 'bool':
|
||||
defaultValue = false
|
||||
break
|
||||
case 'int':
|
||||
defaultValue = 0
|
||||
break
|
||||
case 'direction':
|
||||
defaultValue = 'north'
|
||||
break
|
||||
|
||||
default:
|
||||
continue
|
||||
}
|
||||
}
|
||||
this.blockProps[state.name] = defaultValue
|
||||
if (state.values) {
|
||||
this.metadataFolder.add(this.blockProps, state.name, state.values)
|
||||
} else {
|
||||
this.metadataFolder.add(this.blockProps, state.name)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (const [name, value] of Object.entries(props)) {
|
||||
this.blockProps[name] = value
|
||||
this.metadataFolder.add(this.blockProps, name)
|
||||
}
|
||||
}
|
||||
console.log('props', this.blockProps)
|
||||
this.metadataFolder.open()
|
||||
},
|
||||
entity: () => {
|
||||
this.continuousRender = this.params.entity === 'player'
|
||||
this.entityUpdateShared()
|
||||
if (!this.params.entity) return
|
||||
if (this.params.entity === 'player') {
|
||||
viewer.entities.updatePlayerSkin('id', viewer.entities.entities.id.username, undefined, true, true)
|
||||
viewer.entities.playAnimation('id', 'running')
|
||||
}
|
||||
// let prev = false
|
||||
// setInterval(() => {
|
||||
// viewer.entities.playAnimation('id', prev ? 'running' : 'idle')
|
||||
// prev = !prev
|
||||
// }, 1000)
|
||||
|
||||
EntityMesh.getStaticData(this.params.entity)
|
||||
// entityRotationFolder.destroy()
|
||||
// entityRotationFolder = gui.addFolder('entity metadata')
|
||||
// entityRotationFolder.add(params, 'entityRotate')
|
||||
// entityRotationFolder.open()
|
||||
},
|
||||
supportBlock: () => {
|
||||
viewer.setBlockStateId(this.targetPos.offset(0, -1, 0), this.params.supportBlock ? 1 : 0)
|
||||
},
|
||||
modelVariant: () => {
|
||||
viewer.world.mesherConfig.debugModelVariant = this.params.modelVariant === 0 ? undefined : [this.params.modelVariant]
|
||||
}
|
||||
}
|
||||
|
||||
entityUpdateShared () {
|
||||
viewer.entities.clear()
|
||||
if (!this.params.entity) return
|
||||
worldView!.emit('entity', {
|
||||
id: 'id', name: this.params.entity, pos: this.targetPos.offset(0.5, 1, 0.5), width: 1, height: 1, username: localStorage.testUsername, yaw: Math.PI, pitch: 0
|
||||
})
|
||||
const enableSkeletonDebug = (obj) => {
|
||||
const { children, isSkeletonHelper } = obj
|
||||
if (!Array.isArray(children)) return
|
||||
if (isSkeletonHelper) {
|
||||
obj.visible = true
|
||||
return
|
||||
}
|
||||
for (const child of children) {
|
||||
if (typeof child === 'object') enableSkeletonDebug(child)
|
||||
}
|
||||
}
|
||||
enableSkeletonDebug(viewer.entities.entities['id'])
|
||||
setTimeout(() => {
|
||||
viewer.render()
|
||||
}, TWEEN_DURATION)
|
||||
}
|
||||
|
||||
blockIsomorphicRenderBundle () {
|
||||
const { renderer } = viewer
|
||||
|
||||
const canvas = renderer.domElement
|
||||
const onlyCurrent = !confirm('Ok - render all blocks, Cancel - render only current one')
|
||||
const sizeRaw = prompt('Size', '512')
|
||||
if (!sizeRaw) return
|
||||
const size = parseInt(sizeRaw, 10)
|
||||
// const size = 512
|
||||
|
||||
this.ignoreResize = true
|
||||
canvas.width = size
|
||||
canvas.height = size
|
||||
renderer.setSize(size, size)
|
||||
|
||||
viewer.camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 10)
|
||||
viewer.scene.background = null
|
||||
|
||||
const rad = THREE.MathUtils.degToRad(-120)
|
||||
viewer.directionalLight.position.set(
|
||||
Math.cos(rad),
|
||||
Math.sin(rad),
|
||||
0.2
|
||||
).normalize()
|
||||
viewer.directionalLight.intensity = 1
|
||||
|
||||
const cameraPos = this.targetPos.offset(2, 2, 2)
|
||||
const pitch = THREE.MathUtils.degToRad(-30)
|
||||
const yaw = THREE.MathUtils.degToRad(45)
|
||||
viewer.camera.rotation.set(pitch, yaw, 0, 'ZYX')
|
||||
// viewer.camera.lookAt(center.x + 0.5, center.y + 0.5, center.z + 0.5)
|
||||
viewer.camera.position.set(cameraPos.x + 1, cameraPos.y + 0.5, cameraPos.z + 1)
|
||||
|
||||
const allBlocks = mcData.blocksArray.map(b => b.name)
|
||||
// const allBlocks = ['stone', 'warped_slab']
|
||||
|
||||
let blockCount = 1
|
||||
let blockName = allBlocks[0]
|
||||
|
||||
const updateBlock = () => {
|
||||
// viewer.setBlockStateId(targetPos, mcData.blocksByName[blockName].minStateId)
|
||||
this.params.block = blockName
|
||||
// todo cleanup (introduce getDefaultState)
|
||||
// TODO
|
||||
// onUpdate.block()
|
||||
// applyChanges(false, true)
|
||||
}
|
||||
void viewer.waitForChunksToRender().then(async () => {
|
||||
// wait for next macro task
|
||||
await new Promise(resolve => {
|
||||
setTimeout(resolve, 0)
|
||||
})
|
||||
if (onlyCurrent) {
|
||||
viewer.render()
|
||||
onWorldUpdate()
|
||||
} else {
|
||||
// will be called on every render update
|
||||
viewer.world.renderUpdateEmitter.addListener('update', onWorldUpdate)
|
||||
updateBlock()
|
||||
}
|
||||
})
|
||||
|
||||
const zip = new JSZip()
|
||||
zip.file('description.txt', 'Generated with mcraft.fun/playground')
|
||||
|
||||
const end = async () => {
|
||||
// download zip file
|
||||
|
||||
const a = document.createElement('a')
|
||||
const blob = await zip.generateAsync({ type: 'blob' })
|
||||
const dataUrlZip = URL.createObjectURL(blob)
|
||||
a.href = dataUrlZip
|
||||
a.download = 'blocks_render.zip'
|
||||
a.click()
|
||||
URL.revokeObjectURL(dataUrlZip)
|
||||
console.log('end')
|
||||
|
||||
viewer.world.renderUpdateEmitter.removeListener('update', onWorldUpdate)
|
||||
}
|
||||
|
||||
async function onWorldUpdate () {
|
||||
// await new Promise(resolve => {
|
||||
// setTimeout(resolve, 50)
|
||||
// })
|
||||
const dataUrl = canvas.toDataURL('image/png')
|
||||
|
||||
zip.file(`${blockName}.png`, dataUrl.split(',')[1], { base64: true })
|
||||
|
||||
if (onlyCurrent) {
|
||||
end()
|
||||
} else {
|
||||
nextBlock()
|
||||
}
|
||||
}
|
||||
const nextBlock = async () => {
|
||||
blockName = allBlocks[blockCount++]
|
||||
console.log(allBlocks.length, '/', blockCount, blockName)
|
||||
if (blockCount % 5 === 0) {
|
||||
await new Promise(resolve => {
|
||||
setTimeout(resolve, 100)
|
||||
})
|
||||
}
|
||||
if (blockName) {
|
||||
updateBlock()
|
||||
} else {
|
||||
end()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getBlock () {
|
||||
return mcData.blocksByName[this.params.block || 'air']
|
||||
}
|
||||
|
||||
// applyChanges (metadataUpdate = false, skipQs = false) {
|
||||
override onParamsUpdate (paramName: string, object: any) {
|
||||
const metadataUpdate = paramName === 'metadata'
|
||||
|
||||
const blockId = this.getBlock()?.id
|
||||
let block: import('prismarine-block').Block
|
||||
if (metadataUpdate) {
|
||||
block = new this.Block(blockId, 0, this.params.metadata)
|
||||
Object.assign(this.blockProps, block.getProperties())
|
||||
for (const _child of this.metadataFolder!.children) {
|
||||
const child = _child as import('lil-gui').Controller
|
||||
child.updateDisplay()
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
block = this.Block.fromProperties(blockId ?? -1, this.blockProps, 0)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
block = this.Block.fromStateId(0, 0)
|
||||
}
|
||||
}
|
||||
|
||||
worldView!.setBlockStateId(this.targetPos, block.stateId ?? 0)
|
||||
console.log('up stateId', block.stateId)
|
||||
this.params.metadata = block.metadata
|
||||
this.metadataGui.updateDisplay()
|
||||
}
|
||||
|
||||
override renderFinish () {
|
||||
for (const update of Object.values(this.onParamUpdate)) {
|
||||
// update(true)
|
||||
update()
|
||||
}
|
||||
this.onParamsUpdate('', {})
|
||||
this.gui.openAnimated()
|
||||
}
|
||||
}
|
||||
|
||||
export default MainScene
|
||||
14
renderer/playground/scenes/railsCobweb.ts
Normal file
14
renderer/playground/scenes/railsCobweb.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import { BasePlaygroundScene } from '../baseScene'
|
||||
|
||||
export default class RailsCobwebScene extends BasePlaygroundScene {
|
||||
setupWorld () {
|
||||
this.addWorldBlock(0, 0, 0, 'cobweb')
|
||||
this.addWorldBlock(0, -1, 0, 'cobweb')
|
||||
this.addWorldBlock(1, -1, 0, 'cobweb')
|
||||
this.addWorldBlock(1, 0, 0, 'cobweb')
|
||||
|
||||
this.addWorldBlock(0, 0, 1, 'powered_rail', { shape: 'north_south', waterlogged: false })
|
||||
this.addWorldBlock(0, 0, 2, 'powered_rail', { shape: 'ascending_south', waterlogged: false })
|
||||
this.addWorldBlock(0, 1, 3, 'powered_rail', { shape: 'north_south', waterlogged: false })
|
||||
}
|
||||
}
|
||||
7
renderer/playground/scenes/rotationIssue.ts
Normal file
7
renderer/playground/scenes/rotationIssue.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import { BasePlaygroundScene } from '../baseScene'
|
||||
|
||||
export default class RotationIssueScene extends BasePlaygroundScene {
|
||||
setupWorld () {
|
||||
// todo
|
||||
}
|
||||
}
|
||||
15
renderer/playground/scenes/slabsOptimization.ts
Normal file
15
renderer/playground/scenes/slabsOptimization.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import { BasePlaygroundScene } from '../baseScene'
|
||||
|
||||
export default class extends BasePlaygroundScene {
|
||||
expectedNumberOfFaces = 30
|
||||
|
||||
setupWorld () {
|
||||
this.addWorldBlock(0, 1, 0, 'stone_slab')
|
||||
this.addWorldBlock(0, 0, 0, 'stone')
|
||||
this.addWorldBlock(0, -1, 0, 'stone_slab', { type: 'top', waterlogged: false })
|
||||
this.addWorldBlock(0, -1, -1, 'stone_slab', { type: 'top', waterlogged: false })
|
||||
this.addWorldBlock(0, -1, 1, 'stone_slab', { type: 'top', waterlogged: false })
|
||||
this.addWorldBlock(-1, -1, 0, 'stone_slab', { type: 'top', waterlogged: false })
|
||||
this.addWorldBlock(1, -1, 0, 'stone_slab', { type: 'top', waterlogged: false })
|
||||
}
|
||||
}
|
||||
11
renderer/playground/scenes/transparencyIssue.ts
Normal file
11
renderer/playground/scenes/transparencyIssue.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import { BasePlaygroundScene } from '../baseScene'
|
||||
|
||||
export default class extends BasePlaygroundScene {
|
||||
setupWorld () {
|
||||
this.addWorldBlock(0, 0, 0, 'water')
|
||||
this.addWorldBlock(0, 1, 0, 'lime_stained_glass')
|
||||
this.addWorldBlock(0, 0, -1, 'lime_stained_glass')
|
||||
this.addWorldBlock(0, -1, 0, 'lime_stained_glass')
|
||||
this.addWorldBlock(0, -1, -1, 'stone')
|
||||
}
|
||||
}
|
||||
79
renderer/playground/shared.ts
Normal file
79
renderer/playground/shared.ts
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
import WorldLoader, { world } from 'prismarine-world'
|
||||
import ChunkLoader from 'prismarine-chunk'
|
||||
|
||||
export type BlockFaceType = {
|
||||
side: number
|
||||
textureIndex: number
|
||||
tint?: [number, number, number]
|
||||
isTransparent?: boolean
|
||||
|
||||
// for testing
|
||||
face?: string
|
||||
neighbor?: string
|
||||
light?: number
|
||||
}
|
||||
|
||||
export type BlockType = {
|
||||
faces: BlockFaceType[]
|
||||
|
||||
// for testing
|
||||
block: string
|
||||
}
|
||||
|
||||
export const makeError = (str: string) => {
|
||||
reportError?.(str)
|
||||
}
|
||||
export const makeErrorCritical = (str: string) => {
|
||||
throw new Error(str)
|
||||
}
|
||||
|
||||
export const getSyncWorld = (version: string): world.WorldSync => {
|
||||
const World = (WorldLoader as any)(version)
|
||||
const Chunk = (ChunkLoader as any)(version)
|
||||
|
||||
const world = new World(version).sync
|
||||
|
||||
const methods = getAllMethods(world)
|
||||
for (const method of methods) {
|
||||
if (method.startsWith('set') && method !== 'setColumn') {
|
||||
const oldMethod = world[method].bind(world)
|
||||
world[method] = (...args) => {
|
||||
const arg = args[0]
|
||||
if (arg.x !== undefined && !world.getColumnAt(arg)) {
|
||||
world.setColumn(Math.floor(arg.x / 16), Math.floor(arg.z / 16), new Chunk(undefined as any))
|
||||
}
|
||||
oldMethod(...args)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return world
|
||||
}
|
||||
|
||||
function getAllMethods (obj) {
|
||||
const methods = new Set()
|
||||
let currentObj = obj
|
||||
|
||||
do {
|
||||
for (const name of Object.getOwnPropertyNames(currentObj)) {
|
||||
if (typeof obj[name] === 'function' && name !== 'constructor') {
|
||||
methods.add(name)
|
||||
}
|
||||
}
|
||||
} while ((currentObj = Object.getPrototypeOf(currentObj)))
|
||||
|
||||
return [...methods] as string[]
|
||||
}
|
||||
|
||||
export const delayedIterator = async <T> (arr: T[], delay: number, exec: (item: T, index: number) => Promise<void>, chunkSize = 1) => {
|
||||
// if delay is 0 then don't use setTimeout
|
||||
for (let i = 0; i < arr.length; i += chunkSize) {
|
||||
if (delay) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await new Promise(resolve => {
|
||||
setTimeout(resolve, delay)
|
||||
})
|
||||
}
|
||||
await exec(arr[i], i)
|
||||
}
|
||||
}
|
||||
59
renderer/rsbuild.config.ts
Normal file
59
renderer/rsbuild.config.ts
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import { defineConfig, mergeRsbuildConfig, RsbuildPluginAPI } from '@rsbuild/core';
|
||||
import supportedVersions from '../src/supportedVersions.mjs'
|
||||
import childProcess from 'child_process'
|
||||
import path, { dirname, join } from 'path'
|
||||
import { pluginReact } from '@rsbuild/plugin-react';
|
||||
import { pluginNodePolyfill } from '@rsbuild/plugin-node-polyfill';
|
||||
import fs from 'fs'
|
||||
import fsExtra from 'fs-extra'
|
||||
import { appAndRendererSharedConfig, rspackViewerConfig } from './rsbuildSharedConfig';
|
||||
|
||||
const mcDataPath = join(__dirname, '../generated/minecraft-data-optimized.json')
|
||||
|
||||
// if (!fs.existsSync('./playground/textures')) {
|
||||
// fsExtra.copySync('node_modules/mc-assets/dist/other-textures/latest/entity', './playground/textures/entity')
|
||||
// }
|
||||
|
||||
if (!fs.existsSync(mcDataPath)) {
|
||||
childProcess.execSync('tsx ./scripts/makeOptimizedMcData.mjs', { stdio: 'inherit', cwd: path.join(__dirname, '..') })
|
||||
}
|
||||
|
||||
export default mergeRsbuildConfig(
|
||||
appAndRendererSharedConfig(),
|
||||
defineConfig({
|
||||
html: {
|
||||
template: join(__dirname, './playground.html'),
|
||||
},
|
||||
output: {
|
||||
cleanDistPath: false,
|
||||
distPath: {
|
||||
root: join(__dirname, './dist'),
|
||||
},
|
||||
},
|
||||
server: {
|
||||
port: 9090,
|
||||
},
|
||||
source: {
|
||||
entry: {
|
||||
index: join(__dirname, './playground/playground.ts')
|
||||
},
|
||||
define: {
|
||||
'globalThis.includedVersions': JSON.stringify(supportedVersions),
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
{
|
||||
name: 'test',
|
||||
setup (build: RsbuildPluginAPI) {
|
||||
const prep = async () => {
|
||||
fsExtra.copySync(join(__dirname, '../node_modules/mc-assets/dist/other-textures/latest/entity'), join(__dirname, './dist/textures/entity'))
|
||||
}
|
||||
build.onBeforeBuild(async () => {
|
||||
await prep()
|
||||
})
|
||||
build.onBeforeStartDevServer(() => prep())
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
)
|
||||
124
renderer/rsbuildSharedConfig.ts
Normal file
124
renderer/rsbuildSharedConfig.ts
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
import { defineConfig, ModifyRspackConfigUtils } from '@rsbuild/core';
|
||||
import { pluginNodePolyfill } from '@rsbuild/plugin-node-polyfill';
|
||||
import { pluginReact } from '@rsbuild/plugin-react';
|
||||
import path from 'path'
|
||||
import fs from 'fs'
|
||||
|
||||
export const appAndRendererSharedConfig = () => defineConfig({
|
||||
dev: {
|
||||
progressBar: true,
|
||||
writeToDisk: true,
|
||||
watchFiles: {
|
||||
paths: [
|
||||
path.join(__dirname, './dist/webgpuRendererWorker.js'),
|
||||
path.join(__dirname, './dist/mesher.js'),
|
||||
]
|
||||
},
|
||||
},
|
||||
output: {
|
||||
polyfill: 'usage',
|
||||
// 50kb limit for data uri
|
||||
dataUriLimit: 50 * 1024,
|
||||
assetPrefix: './',
|
||||
},
|
||||
source: {
|
||||
alias: {
|
||||
fs: path.join(__dirname, `../src/shims/fs.js`),
|
||||
http: 'http-browserify',
|
||||
stream: 'stream-browserify',
|
||||
net: 'net-browserify',
|
||||
'minecraft-protocol$': 'minecraft-protocol/src/index.js',
|
||||
'buffer$': 'buffer',
|
||||
// avoid bundling, not used on client side
|
||||
'prismarine-auth': path.join(__dirname, `../src/shims/prismarineAuthReplacement.ts`),
|
||||
perf_hooks: path.join(__dirname, `../src/shims/perf_hooks_replacement.js`),
|
||||
crypto: path.join(__dirname, `../src/shims/crypto.js`),
|
||||
dns: path.join(__dirname, `../src/shims/dns.js`),
|
||||
yggdrasil: path.join(__dirname, `../src/shims/yggdrasilReplacement.ts`),
|
||||
'three$': 'three/src/Three.js',
|
||||
'stats.js$': 'stats.js/src/Stats.js',
|
||||
},
|
||||
define: {
|
||||
'process.platform': '"browser"',
|
||||
},
|
||||
decorators: {
|
||||
version: 'legacy', // default is a lie
|
||||
},
|
||||
},
|
||||
server: {
|
||||
htmlFallback: false,
|
||||
// publicDir: false,
|
||||
headers: {
|
||||
// enable shared array buffer
|
||||
'Cross-Origin-Opener-Policy': 'same-origin',
|
||||
'Cross-Origin-Embedder-Policy': 'require-corp',
|
||||
},
|
||||
open: process.env.OPEN_BROWSER === 'true',
|
||||
},
|
||||
plugins: [
|
||||
pluginReact(),
|
||||
pluginNodePolyfill()
|
||||
],
|
||||
tools: {
|
||||
rspack (config, helpers) {
|
||||
const packageJson = JSON.parse(fs.readFileSync(path.join(__dirname, '../package.json'), 'utf8'))
|
||||
const hasFileProtocol = Object.values(packageJson.pnpm.overrides).some((dep) => (dep as string).startsWith('file:'))
|
||||
if (hasFileProtocol) {
|
||||
// enable node_modules watching
|
||||
config.watchOptions.ignored = /\.git/
|
||||
}
|
||||
rspackViewerConfig(config, helpers)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
export const rspackViewerConfig = (config, { appendPlugins, addRules, rspack }: ModifyRspackConfigUtils) => {
|
||||
appendPlugins(new rspack.NormalModuleReplacementPlugin(/data|prismarine-physics/, (resource) => {
|
||||
let absolute: string
|
||||
const request = resource.request.replaceAll('\\', '/')
|
||||
absolute = path.join(resource.context, request).replaceAll('\\', '/')
|
||||
if (request.includes('minecraft-data/data/pc/1.') || request.includes('prismarine-physics')) {
|
||||
console.log('Error: incompatible resource', request, 'from', resource.contextInfo.issuer)
|
||||
process.exit(1)
|
||||
// throw new Error(`${resource.request} was requested by ${resource.contextInfo.issuer}`)
|
||||
}
|
||||
if (absolute.endsWith('/minecraft-data/data.js')) {
|
||||
resource.request = path.join(__dirname, `../src/shims/minecraftData.ts`)
|
||||
}
|
||||
if (absolute.endsWith('/minecraft-data/data/bedrock/common/legacy.json')) {
|
||||
resource.request = path.join(__dirname, `../src/shims/empty.ts`)
|
||||
}
|
||||
if (absolute.endsWith('/minecraft-data/data/pc/common/legacy.json')) {
|
||||
resource.request = path.join(__dirname, `../src/preflatMap.json`)
|
||||
}
|
||||
}))
|
||||
addRules([
|
||||
{
|
||||
test: /\.obj$/,
|
||||
type: 'asset/source',
|
||||
},
|
||||
{
|
||||
test: /\.wgsl$/,
|
||||
type: 'asset/source',
|
||||
},
|
||||
{
|
||||
test: /\.mp3$/,
|
||||
type: 'asset/source',
|
||||
},
|
||||
{
|
||||
test: /\.txt$/,
|
||||
type: 'asset/source',
|
||||
},
|
||||
{
|
||||
test: /\.log$/,
|
||||
type: 'asset/source',
|
||||
}
|
||||
])
|
||||
config.ignoreWarnings = [
|
||||
/the request of a dependency is an expression/,
|
||||
/Unsupported pseudo class or element: xr-overlay/
|
||||
]
|
||||
if (process.env.SINGLE_FILE_BUILD === 'true') {
|
||||
config.module!.parser!.javascript!.dynamicImportMode = 'eager'
|
||||
}
|
||||
}
|
||||
27
renderer/viewer/baseGraphicsBackend.ts
Normal file
27
renderer/viewer/baseGraphicsBackend.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import { proxy } from 'valtio'
|
||||
import { NonReactiveState, RendererReactiveState } from '../../src/appViewer'
|
||||
|
||||
export const getDefaultRendererState = (): {
|
||||
reactive: RendererReactiveState
|
||||
nonReactive: NonReactiveState
|
||||
} => {
|
||||
return {
|
||||
reactive: proxy({
|
||||
world: {
|
||||
chunksLoaded: new Set(),
|
||||
heightmaps: new Map(),
|
||||
allChunksLoaded: true,
|
||||
mesherWork: false,
|
||||
intersectMedia: null
|
||||
},
|
||||
renderer: '',
|
||||
preventEscapeMenu: false
|
||||
}),
|
||||
nonReactive: {
|
||||
world: {
|
||||
chunksLoaded: new Set(),
|
||||
chunksTotalNumber: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue