Compare commits

..

828 commits

Author SHA1 Message Date
Vitaly Turovsky
253e094c74 add zardoy/mwc-proxy repo ref 2025-10-11 02:25:14 +03:00
Vitaly Turovsky
fef94f03fb feat: add support for alt+arrows navigation to navigate between commands only 2025-10-11 02:25:06 +03:00
Vitaly Turovsky
e9f91f8ecd feat: enable music by default, add slider for controlling its volume 2025-10-11 02:24:51 +03:00
Colbster937
634df8d03d
Add WebMC & WS changes (#431)
Co-authored-by: Colbster937 <96893162+colbychittenden@users.noreply.github.com>
2025-10-11 01:52:06 +03:00
Vitaly Turovsky
a88c8b5470 possible fix for rare edgecase where skins from server were not applied. Cause: renderer due to rare circumnstances could be loaded AFTER gameLoaded which is fired only when starting rendering 3d world. classic no existing data handling issue
why not mineflayerBotCreated? because getThreeJsRendererMethods not available at that time so would make things only much complex
2025-09-30 09:38:37 +03:00
Vitaly Turovsky
f51254d97a fix: dont stop local replay server with keep alive connection error 2025-09-30 07:20:30 +03:00
Vitaly Turovsky
05cd560d6b add shadow and directional light for player in inventory (model viewer) 2025-09-29 02:01:04 +03:00
Vitaly Turovsky
b239636356 feat: add debugServerPacketNames and debugClientPacketNames for quick access of names with intellisense of packets for current protocol. Should be used with window.inspectPacket in console 2025-09-28 22:04:17 +03:00
Vitaly Turovsky
4f421ae45f respect loadPlayerSkins option for inventory skin 2025-09-28 21:59:00 +03:00
Vitaly
3b94889bed
feat: make arrows colorful and metadata (#430)
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2025-09-20 02:57:59 +03:00
Vitaly
636a7fdb54
feat: improve fog a little (#427) 2025-09-19 05:42:22 +03:00
Vitaly Turovsky
c930365e32 fix sometimes inventory player should not be rendered 2025-09-18 07:49:44 +03:00
Vitaly Turovsky
852dd737ae fix: fix some UI like error screen was not visible fully (buttons were clipped behind the screen) on larger scale on large screens 2025-09-11 22:24:04 +03:00
Vitaly Turovsky
06dc3cb033 feat: Add saveLoginPassword option to control password saving behavior in browser for offline auth on servers 2025-09-08 05:38:16 +03:00
Vitaly Turovsky
c4097975bf add a way to disable sky box for old behavior (not tested) 2025-09-08 05:29:34 +03:00
Vitaly Turovsky
1525fac2a1 fix: some visual camera world view issues (visible lines between blocks) 2025-09-08 05:22:24 +03:00
Vitaly Turovsky
f24cb49a87 up lockfile 2025-09-08 04:55:43 +03:00
Vitaly Turovsky
0b1183f541 up minecraft-data 2025-09-08 04:36:09 +03:00
Vitaly Turovsky
739a6fad24 fix lockfile 2025-09-08 04:34:17 +03:00
Vitaly Turovsky
7f7a14ac65 feat: Add overlay model viewer. Already integrated into inventory to display player! 2025-09-08 04:19:38 +03:00
Vitaly
265d02d18d up protocol for 1.21.8 2025-09-07 18:23:13 +00:00
Vitaly
b2e36840b9
feat: brand new default skybox with fog, better daycycle and colors (#425)
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-09-05 05:02:54 +03:00
Vitaly
7043bf49f3
fix: adding support for newer skin profile data structure in player heads (#424) 2025-09-04 21:55:34 +03:00
Vitaly
528d8f516b
Update worldrendererThree.ts 2025-09-04 21:55:02 +03:00
Kesuaheli
70534d8b5a
fix: adding support for newer skin profile data structure in player heads 2025-09-04 12:51:56 +02:00
Vitaly Turovsky
9d54c70fb7 use node 22 2025-09-02 19:05:18 +03:00
Vitaly Turovsky
7e3ba8bece up integrated server for the latest fixes and better stability 2025-09-02 19:02:30 +03:00
Vitaly
513201be87 up browserify 2025-09-01 08:56:08 +03:00
Vitaly Turovsky
cb82188272 add addPing query param for testing 2025-08-31 19:31:26 +03:00
Vitaly Turovsky
d0d5234ba4 fix: stop right click emulation once window got opened eg chest 2025-08-31 18:31:49 +03:00
Vitaly Turovsky
e81d608554 fix cname 2025-08-27 19:52:09 +03:00
Vitaly Turovsky
1f240d8c20 up mouse allowing to disable positive break block 2025-08-27 19:50:53 +03:00
Vitaly Turovsky
2a1746eb7a [skip ci] fix repository name 2025-08-27 13:33:39 +03:00
Vitaly Turovsky
9718610131 ci: add deployment step for mcw-mcraft-page repository in GitHub Actions 2025-08-27 12:08:20 +03:00
Vitaly Turovsky
8f62fbd4da fix: window title sometimes was not showing up on old versions 2025-08-26 13:50:36 +03:00
Vitaly Turovsky
bc2972fe99 fix registering custom channels too late (a few ms diff) 2025-08-24 19:53:44 +03:00
Vitaly
a12c61bc6c
add simple monaco (#418) 2025-08-21 13:21:02 +03:00
Vitaly Turovsky
6e0d54ea17 up mc-protocol patch 2025-08-20 20:45:02 +03:00
Vitaly Turovsky
72e9e656cc new! helpful errors on custom channels payloads! 2025-08-20 20:42:40 +03:00
Vitaly Turovsky
4a5f2e799c disable experimentalClientSelfReload by default until it's reworked with more fine-tuned checks against server connection 2025-08-20 20:02:57 +03:00
Vitaly
a8fa3d47d1 up protocol & mineflayer for 1.21.6 2025-08-19 12:49:33 +03:00
Vitaly Turovsky
9a84a7acfb do less annoying logging 2025-08-18 11:37:20 +03:00
Vitaly Turovsky
d6eb1601e9 disable remote sounds by default 2025-08-16 09:16:29 +03:00
Vitaly Turovsky
e1293b6cb3 - Introduced a patchAssets script to apply custom textures to the blocks and items atlases.
- Enhanced the ThreeJsSound class to support sound playback timeout and volume adjustments.
- Added a custom sound system to handle named sound effects with metadata.
2025-08-16 09:15:37 +03:00
Vitaly Turovsky
cc4f705aea feat: new experimental chunk loading logic by forcing into spiral queue
feat: add highly experimental logic to try to self-restore any issues with chunks loading by automating f3+a action. write debug info into chat for now. can be disabled
feat: rework chunks debug screen showing actually useful information now
2025-08-15 07:46:52 +03:00
Vitaly Turovsky
54c114a702 feat(big): items are now rendered in 3d not in 2d and it makes insanely huge difference on the game visuals 2025-08-15 05:26:11 +03:00
Vitaly Turovsky
65575e2665 rm faulty mineflayer ver, fix kickin in singleplayer 2025-08-15 01:33:37 +03:00
Vitaly
1ddaa79162
rm not tested pathfinder (#415) 2025-08-15 01:12:24 +03:00
Vitaly Turovsky
e2b141cca0 fix a few packet specific errors 2025-08-14 04:51:37 +03:00
Vitaly
15e3325971 add param for testing for immediate reconnect after kick or error (warning: will cause infinite reload loop) 2025-08-14 01:25:24 +03:00
Vitaly Turovsky
60fc5ef315 feat: add skybox renderer: test it by dragging an image window into window, fix waypoint block pos 2025-08-13 19:19:46 +03:00
Vitaly
8827aab981 dont add test waypoints on dev 2025-08-12 06:27:42 +03:00
Vitaly
0a474e6780 feat: add custom experimental waypints impl 2025-08-12 06:27:06 +03:00
Vitaly Turovsky
cdd8c31a0e fix: fix player colored username rendering, fix sometimes skin was overriden 2025-08-11 21:21:44 +03:00
Vitaly Turovsky
e7c358d3fc feat: add minecraft-web-client:block-interactions-customization 2025-08-11 03:12:05 +03:00
Vitaly Turovsky
fb395041b9 fix: fix on 1.18.2 many blocks like mushrom blocks, fence gates, deepslate, basalt, copper stuff like ore, infested stone, cakes and tinted glass was resulting in instant breaking on the client
dev: add debugTestPing
2025-08-11 01:39:08 +03:00
Vitaly Turovsky
353ba2ecb3 fix: some blocks textures were not update in hotbar after texturepack change 2025-08-08 21:52:55 +03:00
Vitaly Turovsky
53cbff7699 dont conflict fire with chat 2025-08-08 18:37:10 +03:00
Vitaly Turovsky
caf4695637 feat: silly player on fire renderer effect 2025-08-08 18:33:20 +03:00
Vitaly
167b49da08
fix: fix cannot write after stream was destroyed message (#413) 2025-08-08 02:07:52 +03:00
Vitaly Turovsky
d7bd26b6b5 up protocol patch 2025-08-06 01:47:09 +03:00
Vitaly Turovsky
d41527edc8 manually fix lockfile because of silly pnpm dep resolution 2025-08-03 03:33:37 +03:00
Vitaly Turovsky
24ab260e8e fix: up protocol to support 1.21.5 2025-08-03 03:20:38 +03:00
Vitaly
c4b284b9b7 fix: fix supported versions display in server menu 2025-08-02 21:34:33 +03:00
Kesu
67855ae25a
fix: fix some window titles (#401) 2025-07-27 15:24:26 +02:00
Vitaly Turovsky
b9c8ade9bf fix: fix chat was crashing sometimes 2025-07-20 10:06:57 +03:00
Max Lee
4d7e3df859
feat: Item projectiles support (#395) 2025-07-18 14:18:05 +03:00
Vitaly Turovsky
a498778703 always wait for config load so autoConnect works on remote config 2025-07-18 09:56:50 +03:00
Vitaly Turovsky
b6d4728c44 display disconnect always last 2025-07-18 09:44:50 +03:00
Vitaly Turovsky
0dca8bbbe5 fix(important): F3 actions didn't work on mobile at all like chunks reload 2025-07-18 08:32:32 +03:00
Vitaly Turovsky
de9bfba3a8 allow auto connect on mcraft for last integrations 2025-07-18 08:02:13 +03:00
Vitaly Turovsky
45408476a5 fix(appStorage): Fix that settings were not possible to save on vercel domains, use robust self-checking mechanism to ensure user data never lost when cookies storage enabled! 2025-07-18 07:53:47 +03:00
Vitaly Turovsky
c360115f60 fix: fix rare ios safari bug where hotbar would not be visible due to unclear fixed&bottom:0 css using 2025-07-18 07:46:25 +03:00
Max Lee
a8635e9e2f
fix: Effects and Game Indicators overlay toggles didn't work (#397) 2025-07-18 05:55:29 +03:00
Vitaly
5bd33a546a
More build configs & optimise reconnect and immediate game enter (#398)
feat(custom-builds): Add a way to bundle only specific minecraft version data, this does not affect assets though
env:
MIN_MC_VERSION
MAX_MC_VERSION
new SKIP_MC_DATA_RECIPES - if recipes are not used in game
fix: refactor QS params handling to ensure panorama & main menu never loaded when immedieate game enter action is expected (eg ?autoConnect=1)
2025-07-18 04:39:05 +03:00
Vitaly Turovsky
e9c7840dae feat(mobile): fix annoying issues with box and foods usage on screen hold 2025-07-16 16:18:15 +03:00
Vitaly
52c0c75ccf docs: update readme 2025-07-16 12:09:34 +03:00
Vitaly Turovsky
b2f2d85e4f feat(setting): add a way to specify default perspective view 2025-07-14 00:18:42 +03:00
Vitaly Turovsky
7a83a2a657 fix(important): fix all known issues wiht panorama crashing whole game in single file build (minecraft.html) 2025-07-14 00:13:51 +03:00
Vitaly Turovsky
64da602294 add creative server 2025-07-12 05:38:24 +03:00
Max Lee
a09cd7d3ed
fix: Nametag & sign text fixes (#391) 2025-07-11 17:56:37 +03:00
Max Lee
39aca1735e
fix: custom item model data on 1.21.4+ (#392) 2025-07-11 17:13:24 +03:00
Vitaly Turovsky
e9320c68d2 fix metrics port 2025-07-09 17:13:36 +03:00
Vitaly Turovsky
95cc0e6c74 reduce ram usage by 15% 2025-07-09 16:33:50 +03:00
Vitaly
826b24d9e2
Metrics server (#390) 2025-07-09 16:10:50 +03:00
Vitaly Turovsky
16609aa010 fix falsey settings apply 2025-07-08 16:35:05 +03:00
Vitaly Turovsky
09b0e2e493 add a way to disable some parts of bars ui via config and select them via devtool elem select 2025-07-08 15:42:06 +03:00
Vitaly Turovsky
c844b99cf2 fix(regression): fix chat completions were not visible on pc 2025-07-08 15:15:54 +03:00
Vitaly Turovsky
089f2224e2 fix(mobile): drop stack on hotbar hold
feat(config): add powerful way to disable some actions in the client entirely (eg opening inventory or dropping items)
2025-07-08 15:12:23 +03:00
Vitaly Turovsky
2f93c08b1e fix lint 2025-07-04 18:05:26 +03:00
Vitaly Turovsky
fa56d479b1 feat: add report bug button 2025-07-04 18:04:24 +03:00
Max Lee
f489c5f477
fix: skin from textures property would not show (#385) 2025-07-04 17:33:36 +03:00
Vitaly Turovsky
45bc76d825 hotfix(chat): fix all annoying issues on mobile. fix chat was not visible on mobile at all! 2025-07-04 17:29:55 +03:00
Vitaly Turovsky
01567ea589 add "mcraft.fun/debug-inputs.html" 2025-07-04 01:44:26 +03:00
Vitaly Turovsky
e8b0a34c0b fix: fix chat visual text alignment issues on chat opening 2025-07-03 19:52:49 +03:00
Vitaly Turovsky
5cfd301d10 fix: fix controls debug was not visible 2025-07-03 19:35:19 +03:00
Vitaly Turovsky
cdd23bc6a6 fix(critical): fix support for all versions above 1.20.2 in mineflayer dependency 2025-07-03 19:29:27 +03:00
Max Lee
4277c3a262
feat: Support nameTagVisibility option of teams (#373) 2025-07-03 18:15:24 +03:00
Vitaly
9f3d3f93fb
docs: update README to clarify BrowserStack testing (#384) 2025-07-03 18:01:38 +03:00
Vitaly Turovsky
7162d2f549 fix music crash 2025-07-02 19:22:47 +03:00
Vitaly Turovsky
fcf987efe4 always display reconnect button on mcraft.fun 2025-07-02 19:21:18 +03:00
Vitaly Turovsky
d112b01177 fix gameLoaded 2025-07-02 18:55:33 +03:00
Vitaly Turovsky
043e28ed97 add more visible integration trigger 2025-07-02 18:16:44 +03:00
Vitaly Turovsky
08fbc67c31 add update git deps script, fix inventory crashes 2025-07-02 17:22:44 +03:00
Vitaly Turovsky
3bf34a8781 add browserstack partner 2025-07-02 16:55:32 +03:00
Vitaly Turovsky
3cc862b05d up mineflayer 2025-07-02 16:53:30 +03:00
Vitaly Turovsky
c913d63c46 fix(regression): fix ios 16 world rendering support! 2025-07-02 16:25:36 +03:00
Vitaly Turovsky
3320f65b9c now app crash should be fixed 2025-07-02 06:24:02 +03:00
Vitaly Turovsky
ed7c33ff9f up mouse 2025-07-02 06:22:17 +03:00
Vitaly Turovsky
9086435aee rm building storybook 2025-07-02 06:21:15 +03:00
Vitaly Turovsky
8a50412395 fix app crash 2025-07-02 06:19:31 +03:00
Vitaly Turovsky
71257bdf13 add fuchsmc.net server partner back! 2025-07-02 06:16:06 +03:00
Vitaly Turovsky
7aea07f83a up patch again 2025-07-02 06:13:40 +03:00
Vitaly Turovsky
3bcf0f533a up protocol patch 2025-07-02 06:06:21 +03:00
Max Lee
b1298cbe1f
feat: Config option to proxy skin textures (#382) 2025-07-02 05:50:15 +03:00
Vitaly Turovsky
661892af7c up mineflayer 2025-07-02 05:47:49 +03:00
Vitaly Turovsky
c55827db96 up mc-data 2025-07-02 05:46:53 +03:00
Vitaly Turovsky
a2711dbe6c up mc-assets 2025-07-02 05:45:18 +03:00
Vitaly Turovsky
f79e54f11d limit columns in player tab 2025-07-02 05:45:14 +03:00
Vitaly Turovsky
6f5239e1d8 fix: fix inventory crash on picking item with gui-generated texture, fix shulker box 2025-07-01 02:29:37 +03:00
Vitaly Turovsky
13e145cc3a docs: add one liner script! 2025-07-01 00:05:43 +03:00
Vitaly Turovsky
d4ff7de64e stop building storybook... 2025-06-30 19:53:09 +03:00
Vitaly Turovsky
1310109c01 fix: username was not saved after properly after resolving the storage conflict 2025-06-30 19:48:43 +03:00
Vitaly Turovsky
dc2c5a2d88 fix: make it run on ios 15! 2025-06-30 19:11:59 +03:00
Vitaly
31b91e5a33
add logging to sw unregistration on error 2025-06-30 02:06:45 +03:00
Vitaly
f2a11d0a73
Steingify if needed 2025-06-29 20:44:58 +03:00
Vitaly Turovsky
6eae7136ec fix storage conflict modal 2025-06-29 15:34:25 +03:00
Vitaly Turovsky
34eecc166f feat: rework singleplayer generators types. now any generator can be used internally. add a few 2025-06-29 02:38:19 +03:00
Vitaly Turovsky
fec887c28d deeply stringify gui items to avoid futher modifications 2025-06-29 00:56:37 +03:00
Vitaly Turovsky
369166e094 fix tsc, up readme 2025-06-28 00:45:54 +03:00
Vitaly Turovsky
e161426caf always dipslay close buttons from settings 2025-06-27 22:11:49 +03:00
Vitaly Turovsky
0e4435ef91 feat: add support for /ping command, fix chat fading! 2025-06-27 22:06:10 +03:00
Vitaly Turovsky
3336680a0e fix z index of modal 2025-06-27 18:08:33 +03:00
Vitaly Turovsky
83d783226f fix migration marking 2025-06-27 18:08:03 +03:00
Vitaly Turovsky
af5a0b2835 fix: fix camera desync updates in 3rd view and starfield 2025-06-27 16:50:44 +03:00
Vitaly Turovsky
eedd9f1b8f feat: Now settings and servers list synced via top-domain cookies! Eg different subdomains like s.mcraft.fun and mcraft.fun will now share the same settings! Can be disabled.
feat: Now its possible to import data!
2025-06-27 16:28:15 +03:00
Vitaly Turovsky
0b1bc76327 fix ws 2025-06-26 06:22:38 +03:00
Vitaly Turovsky
b839bb8b9b rm readme patching 2025-06-26 06:01:13 +03:00
Vitaly Turovsky
3a7f267b5b dont ignore patch failures 2025-06-26 04:34:08 +03:00
Vitaly Turovsky
2055579b72 feat: finally add block RESCALE support! Cobwebs and ascending rails are now rendered correctly 2025-06-25 15:22:07 +03:00
Vitaly Turovsky
1148378ce6 fix vr again 2025-06-25 15:11:18 +03:00
Vitaly
383e6c4d80 fix lint 2025-06-24 10:03:23 +00:00
Vitaly Turovsky
e9e144621f improve auth features in edge cases 2025-06-24 02:13:06 +03:00
Vitaly
332bd4e0f3
Display auth button 2025-06-22 15:18:51 +03:00
Vitaly Turovsky
32b19ab7af fix: fix elytra skin 2025-06-22 01:20:34 +03:00
Vitaly Turovsky
5221104980 feat: F5: 3rd person view camera! 2025-06-22 01:14:15 +03:00
Vitaly Turovsky
7c8ccba2c1 add testIosCrash for debugging these scenarios 2025-06-21 03:34:04 +03:00
Vitaly Turovsky
fdeb78d96b add /ping and /connect GET endpoints for server info/ping data 2025-06-20 13:56:25 +03:00
Max Lee
5269ad21b5
feat: Add spectator mode entity spectating (#369) 2025-06-18 17:07:00 +03:00
Vitaly Turovsky
f126f56844 fix: fix visual gaps between blocks of water! 2025-06-18 16:57:03 +03:00
Vitaly Turovsky
1b20845ed5 fix initial component mount sometimes displays not found modal
the reason why it happens is known
2025-06-18 08:37:22 +03:00
Vitaly
f3ff4bef03
big renderer codebase cleanup: clean player state (#371) 2025-06-18 08:19:04 +03:00
Vitaly Turovsky
679c3775f7 feat: convert formatted text color to display p3 display so its more vibrant on macbooks with xdr display and other p3 monitors 2025-06-15 16:52:24 +03:00
Vitaly Turovsky
8c71f70db2 fix: fix shifting didn't work on some servers after check 2025-06-13 15:12:58 +03:00
Vitaly Turovsky
9f3079b5f5 feat: rework effects display with new UI! fix a few related bugs 2025-06-13 14:14:08 +03:00
Vitaly Turovsky
794cafb1f6 feat: add useful entities debug entry to F3 2025-06-13 13:22:15 +03:00
Vitaly Turovsky
a3dcfed4d0 feat: add time and battery status that is displayed in fullscreen by default 2025-06-13 13:11:06 +03:00
Vitaly Turovsky
b69813435c up test 2025-06-13 08:06:09 +03:00
Vitaly Turovsky
1e513f87dd feat: add End portal & gateway rendering 2025-06-13 05:23:58 +03:00
Vitaly Turovsky
243db1dc45 use range generation instead 2025-06-13 04:56:40 +03:00
Vitaly Turovsky
ac7d28760f feat: Implement always up-to-date recommended servers display! Fix other annoying issues in servers list 2025-06-13 04:51:48 +03:00
Vitaly Turovsky
cfce898918 make random username configurable 2025-06-11 03:19:14 +03:00
Vitaly
14effc7400
Should fix lint 2025-06-10 07:26:50 +03:00
Vitaly Turovsky
a562316cba rm dead servers 2025-06-10 06:33:18 +03:00
Vitaly Turovsky
6a583d2a36 feat: Custom chat ping functionality!!! To ping on any server type @ and on other web client such ping will be highlighted. Can be disabled.
todo: enable a way to display ping-only messages
todo: reply (public/private) command
todo: fix copmletion offset
2025-06-10 06:33:01 +03:00
Vitaly Turovsky
5575933559 change title for mcraft.fun version 2025-06-10 05:26:32 +03:00
Vitaly Turovsky
e982bf1493 fix(important): make chat word breaking match Minecraftt behavior 2025-06-10 05:24:28 +03:00
Vitaly Turovsky
1c93fd7f60 fix wording 2025-06-10 05:22:52 +03:00
Vitaly
a2e9404a70
feat: Simple but effective renderer perf debug features (#347) 2025-06-05 20:22:58 +03:00
Vitaly Turovsky
38a1d83cf2 fix(regression): B on gamepad was opening pause menu instead of start (default actions map conflict). Now start if clicked again closes all modals 2025-06-05 04:07:22 +03:00
Vitaly Turovsky
314ddf7215 mobile: add back select item button, drop stack on hold action (add command!) 2025-06-05 03:37:17 +03:00
Vitaly Turovsky
829e588ac1 fix F3 handling when other keys are pressed (restore), adjust mic button 2025-06-05 03:32:09 +03:00
Max Lee
8b2276a7ae
Add server-side logging and timeout option (#366) 2025-06-04 17:07:00 +03:00
Maksim Grigorev
7635375471
Resolve issue with non-functional F3 key (#365) 2025-06-03 13:14:34 +03:00
Maksim Grigorev
087e167826
feat: configurable mobile top buttons! (#361) 2025-05-30 16:43:06 +03:00
Vitaly Turovsky
c500d08ed7 hotfix: restore hand 2025-05-27 11:30:21 +03:00
Vitaly Turovsky
50907138f7 fix edge case infinite loop in mesher 2025-05-26 01:09:43 +03:00
Vitaly Turovsky
99d05fc94b improve stability of minimap
(though full refactor is still needed)
2025-05-25 16:16:14 +03:00
Vitaly Turovsky
0c68e63ba6 fix: restore VR support. Fix rotation / position camera bugs 2025-05-25 12:55:27 +03:00
Vitaly Turovsky
04a85e9bd1 minimap: don't do more 20 updates per seconds 2025-05-24 18:26:47 +03:00
Vitaly
3cd778538c
feat: Sync armor rotation for players (#363) 2025-05-22 14:50:58 +03:00
Vitaly Turovsky
9726257577 fix: do not display capture lock message when possilbe (avoid flickering - do strategy switch)
feat: make tab (players list) keybindign configurable and add a way to assign to a gamepad button
2025-05-22 14:46:44 +03:00
Max Lee
5a663aac2f
fix: item textures in inventory break after loading resourcepack (#362) 2025-05-21 18:53:20 +03:00
Vitaly
7cea1b8755
fix tsc 2025-05-21 18:45:54 +03:00
Vitaly Turovsky
5ea2ab9c1a fix: update tab header/footer in real time, use player display name in tab 2025-05-21 13:55:52 +03:00
Vitaly Turovsky
b36d08528f fix: fix hanging forever server ping and weboscket connections 2025-05-21 06:36:39 +03:00
Vitaly Turovsky
a4e70768dd fix test 2025-05-21 05:53:59 +03:00
Vitaly Turovsky
ecb53fab88 up tracker 2025-05-21 05:46:36 +03:00
Vitaly Turovsky
b2ef71fc19 up mc data, physics, autojump 2025-05-21 05:37:33 +03:00
Vitaly
f4196d6aba
feat(config): Add support for remote dynamic splash text (#358) 2025-05-20 19:50:26 +03:00
Vitaly
4f78534ca4
Update config.json 2025-05-20 19:49:36 +03:00
Maksim Grigorev
7799ccc370
Update src/react/MainMenu.tsx
Co-authored-by: Vitaly <vital2580@icloud.com>
2025-05-20 19:08:34 +03:00
Maxim Grigorev
5efe3508df fix: optimized splash text loading 2025-05-20 22:03:31 +07:00
Vitaly
4d70128ac6
Update config.json 2025-05-20 13:53:26 +03:00
Maxim Grigorev
b4df2e1837 feat: improved splash text loading for better UI 2025-05-20 16:56:38 +07:00
Vitaly Turovsky
83366ec5fa improve Click to capture mouse by not displaying it in less cases 2025-05-20 02:02:43 +03:00
Maxim Grigorev
b2a1bd10e4 fix: improoved the code 2025-05-19 19:26:12 +07:00
Maksim Grigorev
90c283c5ee
Update src/utils/splashText.ts
Co-authored-by: Vitaly <vital2580@icloud.com>
2025-05-19 15:17:36 +03:00
Vitaly Turovsky
67dbd56f14 dont display new hint when gamepad is used 2025-05-19 03:07:01 +03:00
Vitaly Turovsky
517f5d3501 feat(ui): display persistent capture mouse indicator instead of notification 2025-05-19 03:02:23 +03:00
Vitaly Turovsky
7f6fc00f02 fix: fix lever interaction shape 2025-05-19 02:21:23 +03:00
Vitaly Turovsky
f5835f54fa fix: was not possible to open blast furnace GUI 2025-05-19 02:05:14 +03:00
Vitaly Turovsky
970ed614ae fix crash on error message 2025-05-19 02:04:53 +03:00
Vitaly Turovsky
080d75f939 less annoying false block swap animations 2025-05-19 02:01:11 +03:00
Vitaly Turovsky
67d365b9c3 fix: fix crafting on 1.16.5 and below 2025-05-19 01:57:48 +03:00
Vitaly Turovsky
e2a0df748e rorce button height 2025-05-19 00:32:44 +03:00
Vitaly Turovsky
2dc811b2a1 adjust settings visually 2025-05-19 00:23:20 +03:00
Vitaly Turovsky
a5d16a75ef fix(regression): hotbar on mobile was broken 2025-05-19 00:16:18 +03:00
gguio
785ab490f2
fix: restore minimal and full map (#348)
Co-authored-by: gguio <nikvish150@gmail.com>
2025-05-18 09:58:52 +03:00
Vitaly Turovsky
a9b94ec897 fix: fix sneaking didnt work with water 2025-05-17 06:58:33 +03:00
Vitaly
42f973e057
Update config.json 2025-05-17 05:33:31 +03:00
Maksim Grigorev
ff29fc1fc5
feat: Credits modal (#354) 2025-05-16 19:50:38 +03:00
Maxim Grigorev
051cc5b35c fix: optimized splashText working process 2025-05-16 22:47:59 +07:00
Maxim Grigorev
f921275c87 feat: improoved code safety 2025-05-16 20:44:32 +07:00
Maxim Grigorev
2b0f178fe0 feat: improoved the code 2025-05-16 20:35:04 +07:00
Maxim Grigorev
6302a3815f feat: Add remote splash text loading via URLs 2025-05-16 20:19:23 +07:00
M G
0a61bbba75
Fixed button shown even when cursor is not over the entity (e.g., a video) (#356)
fix: button shown even when cursor is not over the entity (e.g., a video)
2025-05-14 18:15:21 +03:00
Vitaly Turovsky
7ed3413b28 fix save is undefined in rare cases 2025-05-14 03:12:55 +03:00
Vitaly Turovsky
75adc29bf0 up mc assets 2025-05-14 03:07:02 +03:00
Vitaly Turovsky
489c16793b fix original name logging on error 2025-05-14 01:24:53 +03:00
Vitaly Turovsky
48cdd9484f annoying contextmenu on windows on right click sometimes was appearing 2025-05-12 18:56:20 +03:00
M G
e2400ee667
Entity Interaction Button works now (#352) 2025-05-12 18:15:45 +03:00
M G
a58ff0776e
server name field "flex-start" now (#351) 2025-05-12 18:09:10 +03:00
Vitaly Turovsky
674b6ab00d feat: add chat scroll debug and use alternative ways of forcing scroll in DOM! 2025-05-08 21:23:04 +03:00
Vitaly Turovsky
25f2fdef4e fix: dont display hand & cursor block outline in spectator. 2025-05-08 19:56:13 +03:00
Vitaly Turovsky
f76c7fb782 fix: water now supports lighting when its enabled 2025-05-08 19:51:42 +03:00
Vitaly Turovsky
aa817139b7 fix: fix auto jump on mobile! 2025-05-08 04:11:17 +03:00
Vitaly Turovsky
58799d973c fix transfer on 1.20 and above (should have added patch a long time ago) 2025-05-08 00:07:33 +03:00
M G
b3392bea6b
feat: Entity interaction button for mobile (#346) 2025-05-07 16:31:36 +03:00
Vitaly
bf3381c803
Do not automatically enable renderer debug in dev 2025-05-07 15:03:48 +03:00
M G
bb9bb48efd
feat: Combine IP and port input fields (#345) 2025-05-06 16:39:42 +03:00
Vitaly Turovsky
28022a2054 feat: rework server data saving and auto login by using more aggressive strategy 2025-05-06 16:36:31 +03:00
Vitaly
1845530e22
Make link clickable in release body 2025-05-04 14:21:05 +03:00
Vitaly Turovsky
b01cfe475d change color and fix rsbuild hoisting var bug 2025-05-04 11:35:43 +03:00
Vitaly Turovsky
22483d7a76 regression: some UIs like settings became not scrollable 2025-05-04 11:24:15 +03:00
Vitaly Turovsky
e250061757 smooth appear panorama on load textures 2025-05-03 23:11:15 +03:00
Vitaly Turovsky
1fd9a29192 up mouse & chunk 2025-05-03 23:01:47 +03:00
Vitaly Turovsky
29c6a3d739 up browserify and mineflayer to fix a few bugs 2025-05-03 12:50:55 +03:00
Vitaly Turovsky
cd7c053a3c fix(critical-regression): player animation was glitching just a lot 2025-05-03 12:50:55 +03:00
Vitaly Turovsky
5bfb9bebd7 f3 hint 2025-05-03 12:50:55 +03:00
M G
951790dad6
feat: Debug Response Time Indicator (#342) 2025-05-03 11:00:31 +03:00
M G
c5f72f2fb3
fix: corrected scroll of main component at client mods; feat: improoved ux (#341) 2025-05-02 19:10:43 +03:00
Vitaly Turovsky
f12de4ea23 fix releasing alias 2025-05-01 15:23:58 +03:00
Vitaly Turovsky
813c952420 fix: fix lava rendering 2025-05-01 12:53:44 +03:00
Vitaly Turovsky
ec142c0ce4 fix heads & server lighting 2025-04-30 18:33:30 +03:00
Vitaly Turovsky
378b668d46 fix: sometimes walking animation was stuck 2025-04-30 18:29:39 +03:00
M G
0d9cb0625e
chore: migrated from react-transition-group to framer-motion (#339)
Co-authored-by: nikandrovaelena93@gmail.com <ashcat2507@gmail.com>
2025-04-30 14:26:59 +03:00
Vitaly Turovsky
221f99ffdf fix: fix players list disappear on dimension switch 2025-04-30 12:17:47 +03:00
Vitaly Turovsky
4f1cb85301 fix: fix p2p where peerjs server works 2025-04-30 08:11:38 +03:00
Vitaly Turovsky
5bf66b8e50 ci: add fixed short link for released version eg v90.mcraft.fun 2025-04-29 04:22:59 +03:00
Vitaly Turovsky
5caca68e8e disable pr update desc 2025-04-28 07:09:54 +03:00
Vitaly Turovsky
95163fb288 update page description and tech 2025-04-27 11:19:29 +03:00
Vitaly Turovsky
0f2e4f1329 add allEntitiesDebug 2025-04-26 10:50:29 +03:00
Vitaly Turovsky
aa0024faa2 experimental mods actions 2025-04-26 10:39:12 +03:00
Vitaly Turovsky
1599917134 feat: falling block & summoned tnt entities rendering support 2025-04-26 10:38:59 +03:00
Vitaly Turovsky
e706f7d086 fix ocelot rendering 2025-04-26 10:38:32 +03:00
Vitaly Turovsky
e20fb8be53 add ?debugEntities for list of supported entities 2025-04-26 10:38:19 +03:00
Vitaly Turovsky
cd2ff62d6d fix: remove skeleton helper which was causing them flying for ALL bedrock entities
fixes #270
2025-04-26 09:51:43 +03:00
Vitaly Turovsky
fa36ed2678 fix tsc 2025-04-26 06:19:05 +03:00
Vitaly Turovsky
305f4d8a31 fix surrogate pair check in js 2025-04-26 05:51:00 +03:00
Vitaly Turovsky
86ef4f268e fix sound id mapping for versions before 1.19.3 2025-04-26 05:32:17 +03:00
Vitaly Turovsky
0c7900a655 fix skins loading on real prod 2025-04-26 05:19:06 +03:00
Vitaly Turovsky
8c37db4051 fix(critical-regression): chunks never loaded when dimension was changed but position chunk was the same (eg spawn 0,0) 2025-04-26 05:14:33 +03:00
Vitaly Turovsky
4ded3b5d2b feat: implement safe features for chat: limit length, avoid sending formatting symbol to avoid kicks (with very nice ux!) 2025-04-26 05:01:20 +03:00
Vitaly Turovsky
db1b72a582 fix: fix bow usage visually 2025-04-25 09:27:41 +03:00
Vitaly Turovsky
89fc31a2c2 feat: holding item/block display for players! 2025-04-25 09:05:49 +03:00
Vitaly Turovsky
01b6d87331 feat: player crouching and better hit animation 2025-04-25 08:29:18 +03:00
Vitaly Turovsky
a654396238 try to not to use unreliable bot.player 2025-04-25 07:40:31 +03:00
Vitaly Turovsky
948a52a2a5 feat: restore skins display with new API (thanks to Nodecraft!). Unfortunately capes are not supported from API anymore. Restore skin display from server properties. 2025-04-25 07:26:23 +03:00
Vitaly Turovsky
d0ac00843d up packets list 2025-04-25 04:45:59 +03:00
Vitaly Turovsky
4ca9a801a8 fix sound.js caching, increase priority of scripts (make difference only in very rare cases) 2025-04-25 04:04:59 +03:00
Vitaly Turovsky
510d163067 allow client to be loaded from other domains 2025-04-24 23:19:12 +03:00
Vitaly Turovsky
97533cfddb cache sounds, report downloading assets 2025-04-24 21:37:49 +03:00
Vitaly Turovsky
b30e7fc152 dont display no sound id mapping in default sp 2025-04-24 05:36:35 +03:00
Vitaly Turovsky
d7fdf18416 feat: jokes over. use a reliable source (thanks to ViaVersion!) for sound id mappings to avoid screamers on mobs in latest versions 2025-04-24 03:40:40 +03:00
Vitaly
28faa9417a
feat: Client side js mods. Modding! (#255) 2025-04-23 09:17:33 +03:00
Vitaly Turovsky
109daa2783 fix imports 2025-04-23 05:59:49 +03:00
Vitaly Turovsky
14ad1c5934 sync fork: add a bunch of not needed side core features like translation
AND fix critical performance regression (& ram)
2025-04-23 05:55:59 +03:00
Vitaly Turovsky
585b19d8dc up mcraft-fun-mineflayer support 2025-04-22 19:19:59 +03:00
Vitaly Turovsky
71f63a3be0 sync fork: add loading timer for debug, better connecting messages, better recording panel 2025-04-22 19:01:04 +03:00
Vitaly Turovsky
2b881ea5ba fix: disable physics for viewer 2025-04-21 16:06:01 +03:00
Vitaly Turovsky
a0bfa275af lets be safer and use 32array 2025-04-19 00:43:31 +03:00
Vitaly Turovsky
193c748feb fix: add chunks a little faster on low tier devices: use indicies 2025-04-19 00:43:20 +03:00
Vitaly Turovsky
c3112794c0 try to fix scroll chat bug 2025-04-19 00:07:41 +03:00
Vitaly Turovsky
dbfd2b23f6 up physics 2025-04-18 23:43:26 +03:00
Vitaly Turovsky
9646fbbc0f fix(regression): hotbar switch on mobile was broken 2025-04-18 23:42:51 +03:00
Vitaly Turovsky
a7c35df959 fix: fix movement sound 2025-04-18 17:46:10 +03:00
Vitaly Turovsky
529b465d32 sync for changes between forks
fix: sometimes autlogin save prompt is not displayed
fix: add connect server only ui
add some other components for future usage
fix: make some fields custommization in main menu from config.json
fix: adjust logic on player disconnect in some rare cases
2025-04-17 20:51:06 +03:00
Vitaly Turovsky
1582e16d3b fix critical regression that led to not loading chunks twice that was a valid behavior before 2025-04-17 19:58:30 +03:00
Vitaly Turovsky
e8b1f190a7 add more debug into to f3+h 2025-04-17 19:58:03 +03:00
Vitaly Turovsky
73ccb48d02 feat: Add F3+H chunks debug screen! not really useful for now since chunks not visible bug was not fixed yet 2025-04-16 18:24:19 +03:00
Vitaly Turovsky
143d4a3bb3 fix: fix double chests. fix inventory crashing when it doesnt know the texture to render 2025-04-16 14:34:51 +03:00
Vitaly Turovsky
f5ed17d2fb fix(critical-regression): FIX broken inventory! There was a huge regression with a month-old inventory update which was breaking it in some ways 2025-04-16 03:23:41 +03:00
Vitaly Turovsky
6a8de1fdfb fix: sometimes auto login suggestion was not visible due to overlapping notification issue 2025-04-16 03:22:25 +03:00
Vitaly Turovsky
a541e82e04 fix: add freezeSettings param 2025-04-15 02:29:09 +03:00
Vitaly Turovsky
c5e8fcb90c feat: add controls debug interface! Debug happening actions in your game & keyboard buttons!!!! 2025-04-14 17:21:22 +03:00
Vitaly Turovsky
7a53d4de63 fix(feedback-controls): prevent default action for side mouse buttons to avoid page leave annoying modal on accidental clicks 2025-04-14 17:06:13 +03:00
Vitaly Turovsky
83502eba60 fix(important): Formatted text display: fix reading extra when its text which might happen in kick messages or server info 2025-04-12 03:10:29 +03:00
Vitaly Turovsky
70557a6282 fix dockerfile 2025-04-11 21:44:40 +03:00
Vitaly Turovsky
7e5a12934c fix: fix looking camera desync in local saves 2025-04-11 21:44:08 +03:00
Vitaly Turovsky
4b85b16b73 feat(experimental-part1): rework chunk loading strategy by forcing spiral order loading into renderer processor and ignoring server order 2025-04-11 21:42:55 +03:00
Vitaly Turovsky
27df313f26 microoptimisation on big number of chunks load 2025-04-11 18:08:21 +03:00
Vitaly Turovsky
77449c5c12 forgot to fix what codereabbit told to fix 2025-04-10 19:33:39 +03:00
Vitaly Turovsky
deb8ec6c0f build sharp to avoid crashes 2025-04-10 19:25:43 +03:00
Vitaly Turovsky
024da5bf6d fix: now media dont receive global lighting by default 2025-04-10 18:54:02 +03:00
Vitaly
c755f085d9
fix: update rsbuild and pnpm to latest version to resolve long-standi… (#328) 2025-04-10 18:53:33 +03:00
Vitaly Turovsky
3c6ee2dbb3 up lockfile 2025-04-10 03:15:01 +03:00
Vitaly Turovsky
bf790861d9 fix: finally fix plants rendering in underwater and near water! 2025-04-10 01:20:29 +03:00
Vitaly
a977d09031
Up physics to fix jump 2025-04-09 16:53:42 +03:00
Vitaly
1fbbf36859
feat: NEW PHYSICS ENGINE - GRIM PASSES >1.19.4 (#327) 2025-04-09 05:51:34 +03:00
Vitaly Turovsky
31d5089e9c should fix rare case when some tiles are not rendered 2025-04-09 05:50:39 +03:00
Vitaly Turovsky
7824cf64a2 fix lint 2025-04-09 05:46:40 +03:00
Vitaly Turovsky
f4bd38fa5c use more reliable bot entity instead of player.entity 2025-04-09 05:30:51 +03:00
Vitaly Turovsky
5adbce39e0 pick changes from webgpu:
fix cursor lines
fix background color change
2025-04-09 05:07:41 +03:00
Vitaly Turovsky
0b72ea61c7 fix: add time to debug overlay 2025-04-09 04:34:58 +03:00
Vitaly Turovsky
33a6f4d088 fix: a little better perf when media is not in use 2025-04-08 17:23:32 +03:00
Vitaly Turovsky
758405da03 [skip ci] add doc 2025-04-07 19:16:16 +03:00
Vitaly Turovsky
4fc8011413 feat(customChannelsPreview): add sections moving animations 2025-04-07 19:00:21 +03:00
Vitaly Turovsky
d347957f64 Update pnpm install command in benchmark workflow to use --no-frozen-lockfile 2025-04-07 17:07:32 +03:00
Vitaly Turovsky
0a85de180e should fix modal not found error on server connect UI 2025-04-07 17:00:39 +03:00
Vitaly Turovsky
cc264e895f Move Cypress from optionalDependencies to dependencies in benchmark workflow 2025-04-07 17:00:16 +03:00
Vitaly Turovsky
4dce591f8b fix if for benchmark 2025-04-07 16:45:48 +03:00
Vitaly Turovsky
b35b88236d use whitelist to active touch cancel hack to avoid issues with side integrations 2025-04-07 16:20:06 +03:00
Vitaly Turovsky
f79472a1da add client info, fix small width data display 2025-04-07 16:09:01 +03:00
Vitaly Turovsky
d1a646ed54 never wait for load waitForChunksRender 2025-04-07 14:21:25 +03:00
Vitaly Turovsky
914dcb6110 add more fixtures support 2025-04-07 14:16:44 +03:00
Vitaly Turovsky
23bab8dbd5 basically fix all the debug panes, record that info 2025-04-07 13:55:18 +03:00
Vitaly Turovsky
881d105c57 always render debug 2025-04-07 13:42:13 +03:00
Vitaly Turovsky
3109be2d8a universal load backend + fallback 2025-04-07 12:49:33 +03:00
Vitaly Turovsky
568ea3d18b adjust default render distance to match testing on ci 2025-04-07 12:49:24 +03:00
Vitaly Turovsky
27e51b65df refactor: rename benchmarkName to fixture and update related logic in benchmark files 2025-04-07 02:26:11 +03:00
Vitaly
0aa4d11bdd
feat: Performance benchmark!! (#153) 2025-04-07 02:21:37 +03:00
Vitaly Turovsky
ce5ef7c7cb remove not neede functionality 2025-04-06 11:09:17 +03:00
Vitaly Turovsky
9b71ae1a24 feat: rework mobile button control sizes & joystick. Make size of every button and joystick configurable via configurator UI from settings! 2025-04-06 00:23:17 +03:00
Vitaly
908fa64f2f
pick most of changes from webgpu for better stability (#322) 2025-04-06 00:22:27 +03:00
Vitaly Turovsky
c025a1c75a fix a lot annoying sentry errors 2025-04-05 18:55:59 +03:00
Vitaly Turovsky
04c37c1eef fix: fix durability is nan 2025-04-05 18:37:21 +03:00
Vitaly Turovsky
dbfadde044 fix(renderer): rendering of ALL blocks of north side was not correct. Fix texture flipping 2025-04-05 11:15:39 +03:00
Vitaly Turovsky
d78a8b1220 add mesher logging functionality for advanced debugging on other client end 2025-03-31 22:55:17 +03:00
Vitaly Turovsky
70fbe1b0e2 dont clip edition 2025-03-31 22:06:30 +03:00
Vitaly Turovsky
394a12b147 fix(important): fix physics crash in powder snow and pink petals 2025-03-31 14:08:56 +03:00
Vitaly Turovsky
f895304380 add debug method and fix f3 custom block name display 2025-03-31 13:32:40 +03:00
Vitaly Turovsky
4f45cd072a fix(perf): dont load gui textures on panorama start in singlefile build
fix: update textures in inventory & hotbar after textures load, including jei
fix: one row of jei was out of the screen
2025-03-31 13:16:57 +03:00
Vitaly Turovsky
5af290ac4e also remove media on world remove, and stop 2025-03-31 11:49:05 +03:00
Vitaly Turovsky
c5c9fd9bcd fix: fix name display on new versions in edge cases 2025-03-31 11:43:21 +03:00
Vitaly Turovsky
1b9b6c954c fix: remove media and custom blocks on world switch, minor fixes 2025-03-31 11:43:07 +03:00
Vitaly Turovsky
3c2ed440b6 up proxy software to avoid new crashes 2025-03-31 08:22:29 +03:00
Vitaly Turovsky
9c6bc49921 fix msg for proxy 2025-03-30 18:01:21 +03:00
Vitaly Turovsky
1a87951bc8 up proxy 2025-03-30 16:09:50 +03:00
Vitaly Turovsky
73e65c6656 try different intersect raycast media approach 2025-03-30 16:09:45 +03:00
Vitaly Turovsky
c324ce29ab fix: always limit error texture dimension to avoid crashes on high width/height 2025-03-30 14:03:50 +03:00
Vitaly Turovsky
b666f6e3c3 ci: disable functionality that should work but doesnt work because github doesnt make any sense 2025-03-29 11:27:10 +03:00
Vitaly Turovsky
115022a21b ci: use github_token var 2025-03-29 11:23:48 +03:00
Vitaly Turovsky
18ee1dc532 ci: fix syntax 2025-03-29 11:15:54 +03:00
Vitaly Turovsky
291ead079a use token: workaround 2025-03-29 11:13:33 +03:00
Vitaly Turovsky
983b8a184b refactor: remove bundle-stats workflow and integrate its functionality directly into CI 2025-03-29 11:01:35 +03:00
Vitaly Turovsky
66fa59a87a ci: add size tracking for minecraft.html and adjust build steps 2025-03-29 10:53:50 +03:00
Vitaly
47864f0023
ci: report size change + always check single file build (#320)
* init

* should be final

* mbmb

* Update .github/workflows/bundle-stats.yml

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-03-29 10:41:25 +03:00
Vitaly Turovsky
6f15fcc726 fix: display proxy disconnect message to the client when proxy server is being shutdown 2025-03-29 09:57:57 +03:00
Vitaly Turovsky
af9da93978 enabling parsing kick messages 2025-03-29 09:19:54 +03:00
Vitaly Turovsky
08bb0b6777 fix: fix almost all known inventory update bugs 2025-03-29 09:17:48 +03:00
Vitaly Turovsky
187e9fa6b4 important: fix visual update issues in inventory after server packet 2025-03-29 09:16:36 +03:00
Vitaly Turovsky
4fd290c636 fix(regression): fix rendering almost all items in the game, like shield, banners, beds, filled maps, ... 2025-03-29 06:44:48 +03:00
Vitaly Turovsky
e2f28e4975 fix: fix hotbar & inv texture updates on resources change
fix: delay autologin /login sending
2025-03-29 05:34:32 +03:00
Vitaly Turovsky
c3b4eb953f feat: add much better video support 2025-03-29 04:48:39 +03:00
Vitaly Turovsky
850ae6c2da fix: put just connected server to start of list 2025-03-29 02:01:05 +03:00
Vitaly Turovsky
66b9f58c6f fix error not displayed when crash during world display happens, dont crash world display on entity error 2025-03-29 01:53:54 +03:00
Vitaly Turovsky
b58950bec2 update list of servers 2025-03-28 12:46:05 +03:00
Max Lee
47be0ac865
feat: Item custom model data support (#318)
* feat: Item custom model data support

* rename prop, jsdoc for clarity

* explicit resource manager because it can be run in different threads, up mc-assets

* fix tsc

---------

Co-authored-by: Vitaly Turovsky <vital2580@icloud.com>
2025-03-26 08:22:38 +03:00
Vitaly Turovsky
cd9b796f16 feat: now save actually changed options instead of all options in new localstorage key changedSettings for clarity 2025-03-26 07:12:53 +03:00
Vitaly Turovsky
b32bab8211 fix inscreen button 2025-03-26 06:55:09 +03:00
Vitaly Turovsky
52755fc18f feat(renderer): add toggle for batch chunks display in options GUI 2025-03-26 06:47:21 +03:00
Vitaly Turovsky
f8800d5a31 ci: add step to update deployAlwaysUpdate packages in preview workflow 2025-03-26 06:46:51 +03:00
Vitaly Turovsky
797459b0fc fix: pass custom brand for ws:// servers 2025-03-26 05:08:10 +03:00
Vitaly Turovsky
f8ef748e58 fix: disable chunk batch display by default because of incorrect order and its too slow 2025-03-26 05:01:17 +03:00
Vitaly Turovsky
f161fd31d4 feat(renderer): dont display underground sections. Display chunks only when they are fully loaded 2025-03-26 04:40:11 +03:00
Vitaly Turovsky
3690cb22aa fix classic panorama regression 2025-03-26 04:39:33 +03:00
Vitaly Turovsky
33debc1475 feat(experimental): make loading chunks as smooth as possible by delaying work to get more rendered fps at that time. It helps to eliminate massive fps drops on big chunks of data, however might introduce new possible race conditions.
Also disabled full maps completely because it was hard to optimize atm.
Now chunks load ~30-50% slower but smoother. Note that using waitChunksToLoad settings remains unchanged - time is the same
2025-03-26 03:48:22 +03:00
Vitaly Turovsky
46787309e2 fix: fix camera shake effect! rewrite impl. Fix offhand holding block now can become empty 2025-03-25 22:58:24 +03:00
Vitaly Turovsky
1015556834 use graphics backend id 2025-03-25 21:33:12 +03:00
Vitaly Turovsky
db1c8a1e1a fix: allow custom media to be transparent 2025-03-25 21:25:14 +03:00
Vitaly Turovsky
761c92e27c fix: always set sign text even in rich formatted mode so you dont lose your text if nbt edit command doesnt work (which is the case for the latest versions) 2025-03-25 21:09:48 +03:00
Vitaly Turovsky
118377cbc3 ci: fix relative import path 2025-03-25 04:15:05 +03:00
Vitaly Turovsky
8786448d07 fix tsc 2025-03-25 04:07:34 +03:00
Vitaly Turovsky
2d288153e3 move three.js related files to its own renderer dir 2025-03-25 04:01:43 +03:00
Vitaly Turovsky
a53a6e5f03 fix chests regression 2025-03-25 03:55:06 +03:00
Vitaly Turovsky
237aeec6ac feat: add preventBackgroundTimeoutKick which is disabled by default but can be enabled from advanced settings. Allows to avoid kicking you out of the server when tab is not focused for long time by playing 1hz sound at very low volume to keep tab active 2025-03-25 03:46:08 +03:00
Vitaly
0f3145bb8e
feat: app <-> renderer REWORK. Add new layers for stability (#315) 2025-03-25 02:08:32 +03:00
Vitaly Turovsky
df10bc6f1b feat: add client tps info to f3 2025-03-25 00:50:28 +03:00
Vitaly Turovsky
89a8584060 fix test 2025-03-25 00:48:15 +03:00
Vitaly Turovsky
b6842508ae fix(blockPlacing): fix packets order on latest version. Fix placing end crystals. Fix using hoe and axe. Fix offhand placing. Validate sending placing packets so its 90% accurate
fixes #316
2025-03-24 23:58:16 +03:00
Vitaly
563f5fa007
feat: Add videos/images from source with protocol extension (#301) 2025-03-24 20:17:54 +03:00
Vitaly Turovsky
4a4823fd6a highlight main actions 2025-03-21 04:18:24 +03:00
Vitaly Turovsky
f87e7850ec fix: fix false hurt_animation packet handlings 2025-03-20 22:32:37 +03:00
Vitaly Turovsky
e1758a84d0 fix bossbar crash when server tries to upate/remove non existent one 2025-03-20 05:14:23 +03:00
Vitaly Turovsky
fdd770eeb9 fix: per component error boundary was not working properly crashing whole HUD gui when only component was going out of order 2025-03-20 05:07:55 +03:00
Vitaly Turovsky
abe75c7b8d 10x inventory performance improvements in freq updated inventories eg roulettes 2025-03-20 04:53:44 +03:00
Vitaly Turovsky
6b1a82a6b3 fix possible crash on non existing server data update 2025-03-20 04:37:42 +03:00
Vitaly Turovsky
b0eb73cd76 proper functionality for packets recording at any time: only via console for now 2025-03-20 04:33:26 +03:00
Vitaly Turovsky
8714fd484b fix: fix resource pack = never 2025-03-20 04:23:12 +03:00
Vitaly Turovsky
ba0287f278 fix: preserve list of initial servers when adding server, add always to top
feat: focus server ip input on adding or editing
2025-03-20 04:19:11 +03:00
Vitaly Turovsky
2277020de7 fix swing animations and improve replay server functions 2025-03-18 21:46:44 +03:00
Vitaly Turovsky
c1012a77d0 should also export servers list when requested, fix versions slider 2025-03-17 20:14:39 +03:00
Vitaly Turovsky
1b96577402 update group name 2025-03-17 20:09:57 +03:00
Vitaly Turovsky
5bb09a88bc fix: dont confuse with incorrect version display, allow to use config values as params in real time 2025-03-17 20:08:32 +03:00
Vitaly
36bf18b02f
feat: refactor all app storage managment (#310) 2025-03-17 16:05:04 +03:00
Vitaly Turovsky
da35cfb8a2 up mouse 2025-03-15 16:46:51 +03:00
Vitaly Turovsky
3e056946ec add world download button 2025-03-15 02:20:47 +03:00
Vitaly Turovsky
72028d925d feat: revamp right click experience by reworking block placing prediction and extending activatble items list 2025-03-14 23:25:13 +03:00
Vitaly
897c991a0e
fix: respect main menu links display from config (#308) 2025-03-14 19:53:59 +03:00
Vitaly Turovsky
baa6158872 fix: support custom names search & display in jei 2025-03-14 19:32:02 +03:00
Vitaly Turovsky
a67b9d7aa2 active back all vanilla mechanics like hotbar wheel when replay window is minimized 2025-03-14 19:11:14 +03:00
Vitaly Turovsky
518d6ad866 fix always display reconnect and better last packets display (time) 2025-03-14 02:13:04 +03:00
Vitaly Turovsky
09cd2c3f64 fix(guiRenderer): dont break textures with custom namespaces rendering 2025-03-14 01:50:54 +03:00
Vitaly Turovsky
a8564232f7 fix: make chat arrows work on ios
fix: disable annoying in many cases auto correct on ios (more annoying than useful especially in commands)
fix: make stats dont overlap with chat
fix: fix edgecases when focusing on chat was not possible on mobile
2025-03-14 01:36:14 +03:00
Vitaly Turovsky
91dc4d1007 fix build info alert 2025-03-13 23:27:44 +03:00
Vitaly Turovsky
d921977caf on item giving preserve all metadata & nbt 2025-03-13 23:21:50 +03:00
Vitaly Turovsky
1267dcceae fix: reconnect button sometimes was not displayed 2025-03-13 23:11:12 +03:00
Vitaly Turovsky
c947b285ea fix: fix action bar text was not visible on ios (when landscape) 2025-03-13 23:06:41 +03:00
Vitaly Turovsky
09e61c9aa0 fix: cancel block placements in adventure and when interaction is expected eg crafting table! 2025-03-13 21:57:50 +03:00
Vitaly Turovsky
e1831eea38 fix placing panorama after login (again) 2025-03-13 20:59:07 +03:00
Vitaly Turovsky
214828df0c add option to enable/disable jei even depending on gamemode 2025-03-13 20:46:48 +03:00
Vitaly Turovsky
ad13ab83f2 load panorama earlier 2025-03-12 19:17:02 +03:00
Vitaly Turovsky
fbb3d08bfa use 1.19.4 as version by default because of known issues of configuration stage (like auth doesnt work with some bungee setups, incorrect re-login handling) 2025-03-12 15:59:26 +03:00
Vitaly Turovsky
78313ee225 fix more annoying crash edge cases 2025-03-12 01:55:18 +03:00
Vitaly Turovsky
10ed5b6dfb update to wss 2025-03-12 00:17:24 +03:00
Vitaly Turovsky
87e5ae253d simplify panorama, sfp fixes! 2025-03-12 00:14:16 +03:00
Vitaly
b8b1320258
feat: single file build! (#181) 2025-03-11 23:58:52 +03:00
Vitaly Turovsky
f9c042b00f add groups into UI for future work 2025-03-11 19:55:03 +03:00
Vitaly Turovsky
a6018c6891 remove not working google drive button to avoid confusion 2025-03-11 19:42:13 +03:00
Vitaly Turovsky
ec953fd5d1 feat: never force resort items in the list and allow to resort manually with shift+up/down 2025-03-11 19:37:34 +03:00
Vitaly Turovsky
6263c9ae66 also rename in manifest 2025-03-11 19:22:50 +03:00
Vitaly Turovsky
d5cc6d325e just casually doing a major client rename 2025-03-11 19:21:41 +03:00
Vitaly Turovsky
734d195be0 fix errors spamming from players list 2025-03-11 02:11:36 +03:00
Vitaly Turovsky
14d3bba5f5 rm serverslist storybook 2025-03-11 02:04:22 +03:00
Vitaly Turovsky
e60d10e121 fix panorama seams! 2025-03-11 01:40:56 +03:00
Vitaly Turovsky
8232737a75 fix panorama files order 2025-03-11 01:34:37 +03:00
Vitaly Turovsky
1c2e249031 fix panorama direction 2025-03-10 22:48:51 +03:00
Max Lee
a7e6f9772c
feat: display items in hand of entities (#293) 2025-03-07 15:18:54 +03:00
Vitaly Turovsky
28da4e60f0 hotfix for ui crash on disconnect 2025-03-06 20:12:05 +03:00
Vitaly Turovsky
4cde65e635 hotfix: fix old maps broken regression after gui renderer introduction, fix no connection indicator 2025-03-06 19:35:37 +03:00
Vitaly Turovsky
d35bf41e8c fix: fix displaying of unsigned messages (still need to simplify weird mineflayer parsing) 2025-03-05 23:12:19 +03:00
Vitaly Turovsky
e7b012c08d feat: Display players list on long chat button hold 2025-03-05 22:58:11 +03:00
Vitaly Turovsky
a27fa4cd1d feat: Add interaction hint for touch-based entity targeting 2025-03-05 22:49:36 +03:00
Vitaly Turovsky
c6b8efe4e8 hotfix: should fix edge case when canvas was out of viewport bounds on ios 2025-03-05 22:22:35 +03:00
Vitaly Turovsky
a846eb4500 hotfix: fix world interaction crashes 2025-03-05 20:45:34 +03:00
Vitaly Turovsky
6fb18d4438 fixes & workarounds rendering items in inventory (some were broken since last commit) 2025-03-05 15:26:59 +03:00
Vitaly Turovsky
b9df1bcf9e fix enabling lighting falsey when load for chunks is enabled 2025-03-05 15:12:05 +03:00
Vitaly
0db49e7879
feat: Full support for rendering blocks in inventory GUI powered by deeplsate (#292) 2025-03-05 15:11:42 +03:00
Vitaly Turovsky
998f0f0a85 fix: fix sentry #6092213276 DataCloneError: Cannot decode detached ArrayBuffer 2025-03-05 13:07:21 +03:00
Vitaly Turovsky
465ce35e83 feat: display motd/players info for ws servers (still no icon sadly)
add new server
2025-03-05 13:02:55 +03:00
Vitaly
1c700aac1e
feat(config-json): Only either bundle or load from remote (#291) 2025-03-04 19:00:20 +03:00
Vitaly Turovsky
4b54be637d ci: adjust esbuild build arg syntax for prod 2025-03-03 18:48:27 +03:00
Vitaly Turovsky
1d4dc0ddaa fix define in arg build 2025-03-03 18:45:25 +03:00
Vitaly Turovsky
874cafc75e add self host zip publishing with release 2025-03-03 18:42:08 +03:00
Vitaly Turovsky
2a8f514095 add build zip workflow 2025-03-03 18:24:06 +03:00
Vitaly Turovsky
2619e5da89 fix: was not possible to click notification, make error routing more strict & obvious 2025-03-03 17:42:01 +03:00
Vitaly Turovsky
b0da1e41d6 fix: fix crashes on packets logging recording
fix: make replay panel minmizable
2025-03-03 15:31:25 +03:00
Vitaly Turovsky
10f17063c0 fix: fix whole pipeline of rendering custom items from rp: add them to atlas and update texture propertly. align behavior blocks vs items and gui vs hand/floor 2025-03-03 14:19:38 +03:00
Vitaly
ceb4cb0b66
feat: Refactor mouse controls, fixing all false entity/item interaction issues (#286) 2025-02-27 15:26:38 +03:00
Vitaly Turovsky
fa9c0813c3 fix: seagrass and kelp are always waterlogged 2025-02-27 05:17:18 +03:00
Vitaly Turovsky
dec93c2b64 fix react warning 2025-02-27 04:48:16 +03:00
Vitaly Turovsky
d348a44bb8 add a way to disable recording button on pause menu, refactor 2025-02-27 04:48:03 +03:00
Vitaly Turovsky
dffadbb06c wip jei channel 2025-02-26 23:33:13 +03:00
Vitaly Turovsky
2414111b9c feat: add packets recording control to pause menu, display packets view after recording was started for in real time server packets debug, fix auto captured packets display 2025-02-26 23:29:18 +03:00
Vitaly Turovsky
edad57a225 feat: allow to load client without free space on device (or no write permissions) 2025-02-26 22:56:02 +03:00
Max Lee
52ae41a78d
Add better chat link prompt screen (#290) 2025-02-26 22:33:50 +03:00
Max Lee
8ff05924dd
feat: add config option for pause screen links (#288) 2025-02-26 22:31:22 +03:00
Vitaly Turovsky
322e2f9b44 fix sounds 2025-02-26 22:18:51 +03:00
Vitaly Turovsky
89fd5dde71 add external folder for forks code (ext functionality) 2025-02-26 04:36:15 +03:00
Vitaly Turovsky
deedcda467 correctly merge local config when building 2025-02-26 03:54:25 +03:00
Vitaly Turovsky
ecf55727bc stop publishing UI to npm since no one uses it 2025-02-26 03:52:32 +03:00
Vitaly Turovsky
59cb442225 fix: display notification on user resourecepack enable 2025-02-26 03:33:49 +03:00
Vitaly Turovsky
e8d980b790 add brand new progress reporter 2025-02-26 03:31:02 +03:00
Vitaly Turovsky
acd8144d76 feat: initial config.json is now bundled on build step, which is required for defaultSettings
feat: allow to specify default and locked settings in config.json
feat: allow to specify default app params in config.json
feat: rework how loading progress is reported in app on connect
feat: add setting to wait for chunks to load before starting rendering (which is highly recommended to enable), however not enabled by default because functionality is top priority of the client out of the box, not pleasent ux, so pls enable yourself
2025-02-26 03:29:10 +03:00
Vitaly Turovsky
b7560f716a fix custom block models regression 2025-02-25 23:11:02 +03:00
Vitaly Turovsky
2833b33b4e fix: display err when sound mappings not found 2025-02-25 00:29:34 +03:00
Vitaly Turovsky
077dc9df26 fix: fix hand performance because of unnecessary texture rewrites 2025-02-25 00:02:35 +03:00
Vitaly Turovsky
0b2d676d93 fix: fix a lof of bugs in base transition classes and fix bug with possibly wrong item display in hand 2025-02-25 00:01:56 +03:00
Vitaly Turovsky
6d29413a5d add transparent block model override 2025-02-24 21:53:43 +03:00
Vitaly Turovsky
2f200a876a fix: fix hardcoded sounds played when resource is requested 2025-02-24 21:22:06 +03:00
Vitaly Turovsky
cde239211c fix: change chat completions filtering to be not aggressive 2025-02-24 21:21:48 +03:00
Vitaly Turovsky
2f29a9a5cb fix stupid errors 2025-02-24 07:44:24 +03:00
Vitaly Turovsky
2e3363dce8 a lot of replay code cleanup 2025-02-24 01:18:48 +03:00
Vitaly Turovsky
2cb8bea374 fix types 2025-02-23 21:55:07 +03:00
Vitaly Turovsky
bdea1fc50c a huge progress on packet replay component, fix bugs add switches 2025-02-23 21:52:12 +03:00
Vitaly Turovsky
9613a0e644 fix: fix bossbar flickering 2025-02-23 20:06:47 +03:00
Vitaly Turovsky
75e44407ed fix autocomplete in replay 2025-02-23 20:06:25 +03:00
Vitaly Turovsky
0505b64539 fix: fix shift desync issue, dedupe 2025-02-23 19:08:35 +03:00
Vitaly Turovsky
334e8a502d fix a few bugs in packet, replayer
feat: add a way to ALWAYS inspect 30 packets on disconnect
fix: fix a few server packets
2025-02-23 04:45:33 +03:00
Vitaly
1387cb036b
feat: Replay packets server functionality! (#287)
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-02-23 03:48:15 +03:00
Vitaly Turovsky
81a692272c fix: fix sound id mapping for some versions like 1.16.5 2025-02-22 20:57:40 +03:00
Vitaly Turovsky
78d923d817 a way to have packets logger always enabled 2025-02-21 21:33:21 +03:00
Vitaly Turovsky
f0d5ad616d fix: fix visual issue when loading screen disappears
fix: display loading pregress of server resourecepacks
2025-02-21 20:38:24 +03:00
Vitaly Turovsky
ba6a618443 feat: add support for custom sounds and sounds.json 2025-02-21 20:15:49 +03:00
Vitaly Turovsky
a268c69879 feat: rework atlas packing so now almost any resourepack with different tile sizes can be used 2025-02-21 20:15:38 +03:00
Vitaly
2f81bafd75
fix: improve rendering of armor to closer match game and prevent z-fighting (#285) 2025-02-21 17:31:07 +03:00
Phoenix616
7110b8c66d
fix: improve rendering of armor to closer match game and prevent z-fighting 2025-02-20 14:53:42 +01:00
Vitaly Turovsky
8e4987e685 fix: add resourcepack texture processing error catching 2025-02-20 04:02:15 +03:00
Vitaly Turovsky
2ea74b22fd fix: literally fix the notification component 2025-02-20 04:01:48 +03:00
Vitaly Turovsky
730cf656de feat: add detailed block assets parsing (blockstates + models) in F3 on right side (includes parsing errors! super useful for resourcepacks) 2025-02-20 03:41:44 +03:00
Vitaly Turovsky
fda38bbf59 fix custom channel crash 2025-02-20 02:30:33 +03:00
Max Lee
f1c945d22a
fix: baby mobs are not rendered smaller (#283) 2025-02-19 17:00:43 +03:00
Vitaly Turovsky
3b9503982c fix: fix custom item display on 1.21.4, fix text component potential crash 2025-02-19 04:03:32 +03:00
Max Lee
795f241cbd
fix: floor and ceiling button hitboxes (#282)
Co-authored-by: Vitaly <vital2580@icloud.com>
2025-02-19 03:53:36 +03:00
Vitaly Turovsky
5fd3584823 move reconnect button below disconnect 2025-02-19 03:44:50 +03:00
Vitaly Turovsky
11bfcb8f1a fix wasnt possible to click suggestions 2025-02-19 03:40:02 +03:00
Vitaly Turovsky
bfd88ce544 add stale ping status display 2025-02-19 03:33:39 +03:00
Vitaly Turovsky
0bb6301056 feat: now always focus chat when letter or ctrl+v is pressed (like in discord)
feat: hide completions when input is not focused
2025-02-19 03:26:14 +03:00
Vitaly Turovsky
b7e6793c07 do not display invisible ears on steve (default skin) 2025-02-19 03:13:28 +03:00
Vitaly Turovsky
a0a01d9a3f disable possibly invalid entity checks 2025-02-19 03:01:52 +03:00
Vitaly Turovsky
f8c44ae4f0 add mirror server for play.mcraft.fun 2025-02-19 02:57:21 +03:00
Vitaly Turovsky
44d630b1b3 fix entities apply skin crash
fix websocket ping display for ws
2025-02-18 21:25:18 +03:00
Vitaly Turovsky
0cd11ebe29 regression hand disappeared 2025-02-18 20:58:19 +03:00
Vitaly Turovsky
7196623853 minor visual updates to status component 2025-02-18 20:37:15 +03:00
Vitaly Turovsky
612b35426e feat: add ping display view in pause menu! 2025-02-18 20:36:59 +03:00
Vitaly Turovsky
191043380e fix textures loading regression, fix entity tex resizing 2025-02-18 20:10:33 +03:00
Max Lee
d83fbb0393
fix: maps on invisible Item Frames would have gap (#281) 2025-02-18 17:45:47 +03:00
Vitaly Turovsky
9a8451fff7 fix: fix all known issues with chat auto scroll! 2025-02-18 04:28:48 +03:00
Vitaly Turovsky
5e0ece8288 fix arrow file casing 2025-02-18 04:01:17 +03:00
Vitaly Turovsky
c1bf8bf1d7 add arrow obj 2025-02-18 03:53:13 +03:00
Vitaly Turovsky
4223800fd8 fix: improve rendering of some entites like arrow and sheep 2025-02-18 03:47:24 +03:00
Vitaly Turovsky
4a1138f21c fix: fix memory leak with textures when a lot of entities are loaded at the same time (eg big servers like hypixel), make entities def loader faster 2025-02-18 02:06:00 +03:00
Vitaly
5f6ce69b51
feat: custom block models via custom channel (#277) 2025-02-17 19:40:17 +03:00
Max Lee
a20dca18f8
fix: autoConnect parameter wouldn't do anything (#276) 2025-02-17 16:42:31 +03:00
Max Lee
874a3b3ab0
feat: option to disable ears on all skins (#275) 2025-02-17 16:41:52 +03:00
Vitaly Turovsky
847fed5142 fix: handle unsuccessful block breaking packet (eg spawn protection) 2025-02-15 21:28:37 +03:00
Vitaly
65af9a73c2
feat: rework hand! enable by default, fix bow anim (#261)
* refactor swing animation to controller

* idle animator!!!!

* implelment state switch transition

* a huge fix for UI server edit!

* adjust ui scaling so main menu elements clip less

* view bobbing, new config name, ws:

* EXTREMELY important fixes to entities rendering

* a lot of fixes, add dns resolve fallback

* improve f3 E, fix modal not found edge case

* set correctly target for old browsers, should fix ios 14 crash

* unecessary big refactor, to fix ts err

* fix isWysiwyg check

* fix entities rendering count
2025-02-15 05:14:36 +03:00
Vitaly Turovsky
0f29053ca6 fix(important): save username instantly so it doesn't reset when you reload the page if you didn't change it 2025-02-14 14:31:02 +03:00
Vitaly
df4dd69c80 feat: display reconnect button in pause menu when connection seems to be lost 2025-02-13 23:37:34 +03:00
Vitaly Turovsky
2f70575534 finishing support for mcraft viewer plugin:( 2025-02-13 21:31:55 +03:00
gguio
25c07b14a9
feat: some minimap fixes (#249) 2025-02-13 05:07:19 +03:00
Max Lee
cab9eed5e7
fix: resource pack armor textures and leather armor color bleed (#274) 2025-02-12 21:26:23 +03:00
Vitaly
e2ee1ff133
Fix setting of lastError to avoid false spam into console 2025-02-12 16:06:43 +03:00
Vitaly Turovsky
193616b147 fix npm lib building 2025-02-11 17:19:42 +03:00
Vitaly Turovsky
90e002a3a1 fix lint 2025-02-11 17:12:39 +03:00
Vitaly Turovsky
8595d545a5 Restore support for old browsers! Restore and revisit browserslist 2025-02-11 17:11:41 +03:00
Vitaly Turovsky
cf8d8e51fc fix quickconnect port ignored regression 2025-02-11 16:54:58 +03:00
Vitaly Turovsky
946fc26d86 correctly open release link, show build info 2025-02-11 16:26:17 +03:00
Vitaly Turovsky
d05898ab1c display stack on app crash, more advanced logic on app refresh 2025-02-11 15:51:56 +03:00
Vitaly Turovsky
85fbe0cec0 seems all versions auth is supported, remove restriction 2025-02-11 01:08:41 +03:00
Vitaly Turovsky
0dd7b4d802 plugin: register custom channel later 2025-02-11 00:40:52 +03:00
Vitaly Turovsky
f03046573d hotfix: workaround hotbar crash on 1.21.1 2025-02-10 22:47:43 +03:00
Vitaly Turovsky
208f5b7e0d fix chat placeholder 2025-02-10 21:04:40 +03:00
Vitaly Turovsky
d27f2b4a61 fix(big-perf): do not render entities in not loaded chunks that are far away 2025-02-10 21:04:33 +03:00
Vitaly Turovsky
e9e8641f10 should fix build error, fix viewer memory leak 2025-02-10 20:04:04 +03:00
Vitaly Turovsky
cea4d7f277 feat: implement full support for new mineflayer plugin! add UI customization, motion effect, JS repl console and more! 2025-02-10 19:54:38 +03:00
Vitaly Turovsky
dcbeed42ad fix regression https parsing, improve/fix http, ws parsing in all places of app. finally we parsing a single string in a decent way 2025-02-09 23:36:30 +03:00
Vitaly Turovsky
82693ac80c feat: add fov animation that should have added from first release 2025-02-09 22:12:50 +03:00
Vitaly Turovsky
7ab03e3245 ci: fix crash no deploy 2025-02-09 22:12:50 +03:00
Vitaly Turovsky
f560e952d3 add debugToggle 2025-02-09 19:09:13 +03:00
Vitaly Turovsky
c063ff7244 regression fixes
- Modify proxy connection logic to check for server configuration
- Enhance resource pack handling to deny pack if not accepted
- Improve server address parsing by stripping http/https protocols
2025-02-09 13:58:43 +03:00
Vitaly
f96673bc17
rename prismarine-viewer dir to renderer to avoid confusion (#269) 2025-02-08 14:17:27 +03:00
Vitaly
b196ea5955 should fix storybook build 2025-02-08 12:15:14 +03:00
Vitaly
43580511e2 refactor entitymesh to ts 2025-02-08 12:05:15 +03:00
Vitaly
17dc564ef1 fix playground crash, fix entities crash 2025-02-08 11:54:41 +03:00
Vitaly
c0dc516a2d improve button hover ui on mobile 2025-02-08 11:45:08 +03:00
Vitaly
a0bf1e6a71 fix: fix critical bug with joystick when opening other ui
feat: add history tracking of ever connected servers

- Implement server connection history tracking in localStorage
- Update server connection UI with autocomplete and history suggestions
- Refactor connection logic to use new server address parsing
- Improve touch joystick handling with more robust state management
2025-02-08 11:43:08 +03:00
Max Lee
d295100f34
fix: skins of NPCs would not get applied properly (#265)
feat: heads render texture from properties
2025-02-08 10:33:44 +03:00
Vitaly Turovsky
d7374a5206 fix tsc 2025-02-07 14:23:47 +03:00
Vitaly Turovsky
2993bd2e45 fix bugs with blocks models atlas usage 2025-02-07 14:16:01 +03:00
Vitaly Turovsky
c5a895a18e feat: revamp dropped items & holding blocks/items display! now using flexible item definition resolution 2025-02-07 13:54:38 +03:00
Vitaly
9be5950760
feat: Connections issues icon & reconnect button (#268) 2025-02-07 13:01:22 +03:00
Vitaly
c81da88eb7 up guide!!! add what many didnt know 2025-02-07 10:00:30 +00:00
Vitaly Turovsky
3e00dcb3e9 fix: one of the biggest ios fixes yet: reset viewport state (scale) on unexpected vieport changes and fix possible touch camera movement misaldment 2025-02-07 06:58:33 +03:00
Vitaly Turovsky
1b7ccbd77f up workflows 2025-02-06 08:47:12 +03:00
Vitaly Turovsky
3bfaa2980c up mc-assets to resolve issues with some items custom texture rendering 2025-02-06 08:45:05 +03:00
Vitaly Turovsky
09f91d1491 try to use pnpm action setup and recommend usign corepack 2025-02-06 08:40:24 +03:00
Vitaly Turovsky
42275df14d feedback: remove setting default keybindings to arrows to avoid confusion on accidental arrow keys pressing which might happen in super comfy gamer setups like boxes falling on right part of keyboard which is never used 2025-02-05 14:51:44 +03:00
Vitaly Turovsky
e54c262972 fix dropped blocks rendering 2025-02-05 14:38:18 +03:00
Vitaly Turovsky
20830bc257 fix(resourcepack): fix custom items textures
feat: implement support for custom models tags - new minecraft feature for resourcepack for items
feat: dropped blocks rendered as blocks
feat: add support for new
2025-02-05 14:05:23 +03:00
Vitaly Turovsky
6bf4a4f2a3 hotfix: allow complex messages to be displayed in chat 2025-02-05 07:21:42 +03:00
Vitaly Turovsky
97e1395464 up tech 2025-02-04 16:46:23 +03:00
Vitaly Turovsky
2ccc462679 fix eslint 2025-02-04 08:42:31 +03:00
Max Lee
e2a093baf1
feat: enable bossbar by default & support for custom title texts (#259) 2025-02-04 08:11:11 +03:00
Vitaly Turovsky
50baabad90 dont ignore version override! 2025-02-04 07:04:34 +03:00
Vitaly Turovsky
0d778dad80 fix auto wss 2025-02-04 05:22:22 +03:00
Vitaly Turovsky
d4345b19f1 linting 2025-02-04 05:15:41 +03:00
Vitaly Turovsky
b5a16d520a feat: Add WebSocket server support for direct connections! (no proxy url is used then) 2025-02-04 04:56:37 +03:00
Vitaly Turovsky
32acb55526 better loading msg 2025-02-03 11:04:35 +03:00
Vitaly Turovsky
41489130d6 feat: wait for resource pack install before displaying world to avoid chunks reloading 2025-02-03 11:02:12 +03:00
Vitaly
72058f14f2
feat: reimplement auto version - fix bugs, +config, make faster!! (#262) 2025-02-03 10:19:59 +03:00
Vitaly Turovsky
4a77ba15b6 disable dedupe check until usability and performanse problemas are fixed in pnpm 2025-02-03 05:16:46 +03:00
Vitaly Turovsky
84d38ba21c feat: now keyboard arrows control camera rotation 2025-02-03 04:10:45 +03:00
Max Lee
d4e93d4d39
fix: kick screen would allow going back with lockConnect parameter (#253)
refactor qs getters in codebase
Co-authored-by: Vitaly <vital2580@icloud.com>
2025-02-01 06:24:04 +03:00
Max Lee
6ad1a4d63b
feat: item & maps in frame rendering (#243)
Co-authored-by: Vitaly Turovsky <vital2580@icloud.com>
2025-02-01 05:46:05 +03:00
Vitaly Turovsky
b35685fc13 fix lint 2025-02-01 05:03:13 +03:00
Vitaly Turovsky
5bc55c1bdf adjust config merging 2025-02-01 04:30:20 +03:00
Vitaly Turovsky
4cc6767c78 docker: fix volume path to use with public 2025-02-01 04:23:32 +03:00
Vitaly Turovsky
297d94d419 fix sounds enabling in dockerfile 2025-02-01 03:20:13 +03:00
Vitaly
379484327e
refactor: Rework mobile controls! (#260)
Separate movement and interaction touch controls
Add new options for touch control types (modern/classic)
Refactor touch interaction handling and event management
Create a new GameInteractionOverlay component for touch interactions for less bugs in future
Update touch controls UI and positioning logic

Co-authored-by: qodo-merge-pro-for-open-source[bot] <189517486+qodo-merge-pro-for-open-source[bot]@users.noreply.github.com>
2025-02-01 02:51:56 +03:00
Vitaly Turovsky
b5a8bf16ff fix critical inventory regression 2025-02-01 02:47:08 +03:00
Vitaly Turovsky
978bd16785 ci: rm --offline flag 2025-02-01 02:34:49 +03:00
Vitaly Turovsky
51d8975f5e chore: Update net-browserify dependency to GitHub source 2025-02-01 02:27:43 +03:00
Vitaly Turovsky
28552bd1de feat: Add option to disable service worker during build 2025-02-01 02:23:47 +03:00
Vitaly Turovsky
ad2ba0e24f feat(server): Add server connection validation and username validation on connect server UI with QS 2025-02-01 02:23:24 +03:00
Vitaly Turovsky
b7da6e201f up browserify 2025-02-01 01:52:04 +03:00
Vitaly Turovsky
81aaaab76d fix(proxy): Improve proxy URL handling for non-standard ports 2025-02-01 00:58:46 +03:00
Vitaly Turovsky
8766eaae21 fix signal crash regression 2025-02-01 00:42:49 +03:00
Vitaly Turovsky
92ce4dd0d8 chore(ci): Update GitHub Actions workflow dependencies
- Upgrade actions/upload-artifact to v4
- Minor workflow configuration refinements
2025-02-01 00:35:06 +03:00
Vitaly Turovsky
f755385981 refactor: rm loadedGameState, add a way to copy server resource pack to local 2025-01-31 05:52:55 +03:00
Vitaly Turovsky
14b7cb039a feat(ui): Improve server list UI
- Implement concurrent server status fetching with rate limiting
- Show offline status for servers that cannot be reached
2025-01-31 04:40:15 +03:00
Vitaly Turovsky
28f0546f3b feat(ui): add back, close buttons in settings for mobile 2025-01-31 03:52:07 +03:00
Vitaly Turovsky
317b84943a feat: Add block highlight color option to allow always it to be blue (useful!) 2025-01-29 19:33:55 +03:00
Vitaly Turovsky
2490fbe211 fix(regression): minecity and some other maps broke 2025-01-29 05:26:46 +03:00
Vitaly
df442338f8
feat(sounds): Add sound variants and resource pack support! (#258)
feat: add in-game music support! Enable it with `options.enableMusic = true`
2025-01-29 04:54:51 +03:00
Vitaly Turovsky
a628f64d37 fix: deactivate mouse buttons when window loses focus (edge case) 2025-01-29 04:03:04 +03:00
Vitaly Turovsky
1f5404be9d DOWNLOAD_SOUNDS flag 2025-01-29 04:02:21 +03:00
Vitaly
3de1089a1c
fix: disable block breaking in adventure (#252) 2025-01-29 03:01:08 +03:00
Vitaly
062115c42b
feat: try to bring tab attention or focus it when you are finally joined the server 2025-01-28 21:29:53 +03:00
Max Lee
41684bc028
fix: title would not get rendered and rendering times be wrong (#254) 2025-01-28 19:42:06 +03:00
Vitaly Turovsky
a442acf49a fix build 2025-01-28 19:25:22 +03:00
Vitaly
ff36f75e44
fix regression to quick elem re-focus 2025-01-27 23:09:32 +03:00
Vitaly Turovsky
91ef3ccd3c wip colored button options 2025-01-27 19:49:27 +03:00
Vitaly Turovsky
e6ce6dc268 feat: enhance OptionButton navigation with shift key support for reverse cycling 2025-01-27 19:47:20 +03:00
Vitaly
5a948828b7
Should fix test 2025-01-27 14:35:33 +03:00
Vitaly Turovsky
6d91ad3d41 add lint-fix script 2025-01-27 06:21:42 +03:00
Vitaly Turovsky
ccc1d760e0 run dedupe 2025-01-27 06:19:06 +03:00
Vitaly Turovsky
463b9ef80a feat: enable global arrows navigation in server/saves list, always focus on first server 2025-01-27 06:19:02 +03:00
Vitaly Turovsky
382a685b65 fix: fix light and bubble_column blocks rendering, fix waterlogging for ocean vegetation (plants) 2025-01-27 06:10:31 +03:00
Vitaly Turovsky
5694d14d58 feat: add camera shake effect when player takes damage 2025-01-27 05:57:58 +03:00
Vitaly Turovsky
491b5d66c5 feat: implement player skin display based on texture properties from server packets eg your custom plugin skins! 2025-01-27 03:48:42 +03:00
Vitaly Turovsky
3fc644082a update button disabled state to depend on recorded packets 2025-01-27 03:40:24 +03:00
Vitaly Turovsky
7186b183f1 feat: add packets logger preset option and process packet data based on preset 2025-01-27 02:22:31 +03:00
Vitaly Turovsky
84bc3d5911 add recommended but optional mount volume to dockerfile config 2025-01-26 15:25:50 +03:00
Vitaly Turovsky
540f90fd0c ci: fix build 2025-01-25 22:37:24 +03:00
Vitaly Turovsky
1a45381b3d fix: fix maps display for 1.21+ 2025-01-25 20:25:52 +03:00
Vitaly Turovsky
e0be30b86f fix: adjust FOV calculations for sprinting and flying to increase field of view instead of decreasing it 2025-01-25 20:02:15 +03:00
Vitaly Turovsky
7d6986ccb4 update all deps 2025-01-25 19:38:50 +03:00
Vitaly Turovsky
798eb34466 fix: fix Y keybinding: fix URL on server, add proxy and username parameters 2025-01-25 18:39:26 +03:00
Vitaly Turovsky
77e529b6f4 feat: ctrl or cmd + Escape now closes all modals! 2025-01-25 18:34:18 +03:00
Vitaly Turovsky
f2fde37e6a ci: update patch, should fix dedupe 2025-01-22 21:04:14 +03:00
Vitaly Turovsky
61659d82b4 feat: experimental namespaces support in resource packs 2025-01-22 20:53:30 +03:00
Vitaly Turovsky
4ce95de03f fix: fov changing based on player state was changed incorrectly 2025-01-22 20:03:54 +03:00
Vitaly Turovsky
d0f7f57400 disable parsing of disconnect kick messages that works in the wrong way 2025-01-22 20:02:07 +03:00
Vitaly Turovsky
b052422c99 ci: fix update CI workflow condition for dedupe-check to use pull request head ref 2025-01-16 18:09:49 +07:00
Vitaly Turovsky
ce8e414528 fix: sometimes kick messages where not formatted 2025-01-16 18:04:33 +07:00
Vitaly Turovsky
8f91d282d2 ci: add deduped packages check 2025-01-16 18:01:54 +07:00
Vitaly Turovsky
b946271d87 pnpm dedupe: remove a lot of duplicated packages 2025-01-16 17:35:27 +07:00
Vitaly Turovsky
102a3e499b fix mineflayer, prismarine package versions 2025-01-16 17:27:34 +07:00
Vitaly Turovsky
2edf425ecf fix: fix 1.20.3 and 1.20.4 was not playable, fix issues on these versions 2025-01-16 14:37:05 +07:00
Vitaly Turovsky
5d480d4265 fix: full support for minecity, no more map loading crashes 2025-01-14 17:31:01 +07:00
Vitaly Turovsky
a5bd38619c fix test 2025-01-13 10:39:12 +07:00
Vitaly Turovsky
cbd90aeb53 add release pr auto opening 2025-01-13 10:38:29 +07:00
Vitaly Turovsky
c070fe836f fix type regression 2025-01-12 12:58:36 +07:00
Vitaly Turovsky
30dd64a3c6 feat: add 1.20.4 data
fix: data for some versions were missing like 1.16.4 or 1.16.5
2025-01-10 15:58:13 +07:00
Vitaly Turovsky
87b795bdc4 fix(regression): ip qs param was ignored
fix: add local (gitignored) config that is merged into single config after the build
2025-01-09 21:44:59 +07:00
Vitaly Turovsky
36747e6bef add screen edges debug component (#edges in url)
fix: provide possible fix for rare issue when mobile control elements were going outside of the screen on ios
2024-12-21 00:00:27 +03:00
Vitaly Turovsky
9dad509bc2 feat: improve main menu and sign in UIs. Add QR code so you can login to MS account on mobile 2024-12-20 23:18:16 +03:00
Vitaly Turovsky
d754593849 add christmas textures, update docs, fix singleplayer initial gamemode 2024-12-20 22:37:16 +03:00
Vitaly Turovsky
a064892a9b fix rare crash on auth error (cancel server connect later) 2024-12-20 17:54:38 +03:00
Vitaly Turovsky
dd7c9c172e feat: add modal query parameter for page load
fix: improve servers list UI for small width screens
2024-12-19 20:44:27 +03:00
Max Lee
cb82963b86
feat: Armor Stand and armor model support (#242)
Co-authored-by: Vitaly <vital2580@icloud.com>
2024-12-19 15:24:39 +03:00
Vitaly Turovsky
16fe17edf5 fix lint 2024-12-18 22:49:17 +03:00
Vitaly Turovsky
db7a9b9dd2 feat: add zoom keybinding (Key - C) 2024-12-18 22:22:11 +03:00
Vitaly Turovsky
bf676cdf52 up mc-assets 2024-12-18 14:12:09 +03:00
Vitaly Turovsky
b13c8df469 implement auto version configuration for viewer 2024-12-18 13:59:55 +03:00
Vitaly Turovsky
c289283e7f meaningful errors 2024-12-18 12:57:43 +03:00
Vitaly Turovsky
10e14bd675 add viewer connector impl 2024-12-18 12:53:54 +03:00
Vitaly Turovsky
10ee4c00ae feat: initial support for websocket (direct connection) servers. mcraft-fun-mineflayer plugin 2024-12-18 12:53:27 +03:00
Vitaly Turovsky
961cf01a0e allow inspectFn to be function eg for debugger statement 2024-12-18 11:01:50 +03:00
Vitaly Turovsky
2c0b99ffdb fix wrong cache hit on local dev 2024-12-18 10:23:58 +03:00
Vitaly Turovsky
51c1346456 print debug duration 2024-12-18 09:20:59 +03:00
Vitaly Turovsky
064d70480d fix: a lot of edge case world block updates fixes & improvements. Fix all known visual incosistencies after chunk edge block updates, make them faster 2024-12-18 09:06:31 +03:00
Vitaly Turovsky
7b0ead5595 fix renderer string 2024-12-17 11:00:40 +03:00
Vitaly
ee257d7916
Docs eaglercraft change (#239)
A lot misleading information removed
2024-12-16 19:16:58 +03:00
Vitaly Turovsky
45b8ae6f7c docs: more comparison eagler 2024-12-15 17:54:22 +03:00
Vitaly Turovsky
652120c71b docs: more eaglercraft comparison 2024-12-15 17:48:05 +03:00
Vitaly Turovsky
963a769db3 docs: change description to avoid confustion with eaglercraft project 2024-12-15 17:00:59 +03:00
Vitaly Turovsky
5f87385486 make title selectable 2024-12-14 10:34:28 +03:00
Vitaly Turovsky
372583be7d fix ci build type checks 2024-12-14 10:31:12 +03:00
Vitaly Turovsky
725f6ec364 restore hand display setting 2024-12-14 08:17:01 +03:00
Vitaly Turovsky
ad0502dcb9 recenter edition in ui 2024-12-14 08:09:09 +03:00
Vitaly Turovsky
9b5155d3fe up singleplayer version 2024-12-14 08:08:58 +03:00
Vitaly Turovsky
b10c6809ff fix: fix signs could not be rendered properly because font was not in the build 2024-12-14 08:08:30 +03:00
Vitaly
4d411fe561
feat: true hand display behind the setting (#217) 2024-12-13 21:04:45 +03:00
Vitaly Turovsky
c783094068 suppress test error 2024-12-13 20:56:20 +03:00
Vitaly
13a55b4414 update all latest models! fix test 2024-12-12 14:46:45 +03:00
Vitaly
0624e018dd fix critical but rare bug in new change worker implementation 2024-12-12 14:17:25 +03:00
Vitaly
a4c86d707b fix: update changed blocks quicker in the world by using a dedicated mesher thread reserved for blocks updates only 2024-12-12 09:18:54 +03:00
Vitaly
689bebde3d cleanup eyeHeight code 2024-12-12 07:45:20 +03:00
Vitaly Turovsky
376c358d43 feat: add support for 1.21.3 2024-12-12 07:43:46 +03:00
Vitaly Turovsky
5902918729 fix: finally fix issues with ray casting on small distances. which was easy to notice on blocks like buttons when very close 2024-12-12 07:31:00 +03:00
Vitaly Turovsky
c5f6c087ac ci: restore linkin preview domains 2024-12-12 02:07:36 +03:00
Vitaly Turovsky
75965203fc fix: replace Available Offline text with Downloaded to avoid confustion when the app is Offline 2024-12-12 02:05:53 +03:00
Vitaly Turovsky
2d77bdb9b2 ci: allow setting multiple aliases 2024-12-12 01:30:51 +03:00
Vitaly Turovsky
50d1d37ff3 dev: lock url so app restores the same game state after reloads 2024-12-11 08:38:46 +03:00
Vitaly Turovsky
37b84ae003 ci: fix use pr commit SHA, not base branch 2024-12-10 07:17:11 +03:00
Vitaly Turovsky
74cb815940 feat: support for multiple sources for mapDir url in case if first source can't be fetched 2024-12-10 07:03:55 +03:00
Vitaly Turovsky
b02b250ee8 ci(preview): correctly set pr number in url 2024-12-09 09:48:16 +03:00
Vitaly Turovsky
68dba89bf5 ci: update paths for PR and commit redirect index.html in preview workflow 2024-12-09 09:32:37 +03:00
Vitaly Turovsky
2f21e2b453 write pr & commit redirect 2024-12-09 09:28:14 +03:00
Vitaly
3c5c8b78e3 implement changeBackgroundColor method 2024-12-07 03:27:48 +03:00
Vitaly
18a191a03c feat: add ping display on mobile & via f3, playground ui improvements 2024-12-07 01:57:54 +03:00
Vitaly
3bacef1251 fix: fix race condition when state id change was received from server faster than chunk was processed by mesher: now the change will await for the chunk load first instead of ignoring it. sync common code from webgpu 2024-12-07 01:54:00 +03:00
Vitaly
5783b98e74 fix: spectator: display cursor block in f3, hide hotbar and hunger 2024-12-06 09:53:13 +03:00
Vitaly
84dce0941c write build info to all deploys!
(cherry picked from commit 4afccbefbdf96beb62fbc2ea0d442d9f7cd1c370)
2024-12-04 23:19:37 +03:00
Vitaly Turovsky
cf7c4664f2 copy .map file for mesher in dev for better dx 2024-12-03 10:06:18 +03:00
Vitaly Turovsky
0b8eaa4ad2 fix: fix renderer bug cactus was exposing the world 2024-12-03 03:54:43 +03:00
Max Lee
dd20994f78
feat: Text display orientation and rendering fixes (#235) 2024-11-27 23:12:43 +03:00
gguio
af088d92e1
feat: minimap and full screen map (disabled by default) (#147) 2024-11-27 23:07:28 +03:00
Vitaly Turovsky
b89cf522a0 docs: move general params to top 2024-11-25 16:26:30 +03:00
Max Lee
50f35cf176
Fix rendering of custom names and more text display options (#233)
Fix rendering of custom names of normal entities and add more text display options. Newly supported options for text displays:
- background color
- text opacity

Also exclude `mcDataTypes.ts` from linting and fix a possible exception due to no `entityData` being defined
2024-11-22 21:24:45 +01:00
Vitaly Turovsky
c97dbbde9a feat: add query parameter to specify your custom server lists to display in UI: ?serversList
feat: custom proxy is applied in servers list ui when ?proxy is specified
feat: add a way to quickly copy server list with ctrl+c in servers list ui
2024-11-22 15:44:08 +03:00
Vitaly Turovsky
b881a61610 fix: improve output commands formatting, especially errored ones
feat: add gamerules support: basic data parsing, now possible to control/change them via /gamerule only these gamerules effect implemented: doDaylightCycle
fix: better messaging from output commands (now green)
2024-11-22 15:16:20 +03:00
Vitaly Turovsky
c44ad90acf add finally a way to get entities metadata by key 2024-11-21 14:56:39 +03:00
Max Lee
4dac577dfc
feat: basic support for text_display entity (#230) 2024-11-20 09:04:59 +01:00
Vitaly
f2552e70e1
fix ci (#231) 2024-11-20 09:03:10 +01:00
Vitaly Turovsky
c441792d3f fix failing test ci 2024-11-19 23:00:32 +03:00
Vitaly Turovsky
85ece5b4eb fix: some keybinding names were incorrectly parsed
feat: make fullscreen button (F11) configurable in the keybinding panel
2024-11-18 18:53:31 +03:00
Vitaly Turovsky
0506d9de47 fix(ui): better support formatted minecraft messages (handle packets in general way) 2024-11-14 15:40:33 +03:00
Vitaly Turovsky
a0a2c628b4 fix: preserve server selection after going back to the servers list (eg from edit modal)
fix: was not possible to remove saved MS account by clicking on the profile picture
fix: highlight active zone for interaction to avoid UI confusion
fix: save the proxy to memory immedieately after editing it in the servers list screen
2024-11-14 13:41:28 +03:00
Vitaly Turovsky
e28608f86e also cache manifest.json into offline 2024-11-11 18:04:54 +03:00
Vitaly Turovsky
c8c4e3267d feat: auto save worlds in singleplayer every 2 seconds since crashes might happen. still possible to cancel this behavior via new setting or ?noSave=true
feat: add setting for debugging to disable signs text rendering
2024-11-11 17:49:33 +03:00
Vitaly Turovsky
d32f510744 fix: Error messages were not displayed after unsuccessful Microsoft auth. Fixed all error messages. 2024-11-10 11:44:29 +03:00
Vitaly Turovsky
32931efef0 feat: implement experimental clipWorldBelowY setting for testing 2024-11-09 13:01:37 +03:00
Vitaly Turovsky
574cdfc531 docs: add deploy to Koyeb button 2024-11-09 11:19:40 +03:00
Vitaly Turovsky
c303a0e0d5 feat: allow singleplayer mode to be triggered by 'sp=1' query parameter along with singleplayer=1 2024-11-07 19:09:17 +03:00
Vitaly Turovsky
7284d88ae9 update npm banner to a working one 2024-11-05 13:37:30 +03:00
Vitaly Turovsky
270da682da ci: fix release info writing 2024-11-05 13:12:15 +03:00
Vitaly Turovsky
ab3174a45f hotfix: disable fly after going out of spectator mode 2024-11-05 07:10:57 +03:00
Vitaly Turovsky
0f2bc5c1d4 fox issues with chat on 1.8 2024-11-05 07:07:04 +03:00
Vitaly Turovsky
dcc0960d7f fix the auth check 2024-11-04 02:51:37 +03:00
Vitaly Turovsky
c279b4bbe6 do a restoredData validation 2024-11-04 02:49:23 +03:00
Vitaly Turovsky
b267cb77be send connecting version to auth endpoint 2024-11-04 02:32:26 +03:00
Vitaly Turovsky
b3a089323f fix auth err message 2024-11-04 02:23:15 +03:00
Vitaly Turovsky
2c441c3434 fix: fix display names in inventories on 1.8 2024-11-02 12:45:47 +03:00
Vitaly Turovsky
ad9f5be486 spectator gamemode fixes 2024-11-02 12:45:27 +03:00
Vitaly Turovsky
5405987de3 fix: fix entities display on old versions like 1.8 2024-11-02 12:45:11 +03:00
Vitaly Turovsky
667eff49af feat: write published version name on prod website! 2024-11-01 06:40:37 +03:00
Vitaly Turovsky
26d25b77f5 feat: format disconnect messages from minecraft servers (display correctly)
fixes #26
2024-11-01 02:55:19 +03:00
Vitaly Turovsky
7d0c3643d3 fix: don't reset the world in mesher after resourece pack textures update 2024-10-31 01:34:21 +03:00
Vitaly Turovsky
5791626cc5 fix: (latest) or (offline) status text was not displayed sometimes after the page load 2024-10-31 00:28:54 +03:00
Vitaly Turovsky
84b3f8913d fix: don't make text move in loading screens 2024-10-30 23:53:45 +03:00
Vitaly
03c6a3f724
feat: server resource packs (#215)
Co-authored-by: Mqx <62719703+Mqxx@users.noreply.github.com>
2024-10-30 14:40:18 +03:00
Vitaly Turovsky
f13c4e4581 feat: turn back smooth lighting on old maps 2024-10-30 14:23:45 +03:00
Vitaly Turovsky
2fbfc18d2e feat: optimize slabs render performance by rendering less not visible tiles
Improve performance in Greenfield by 6%
2024-10-30 11:33:26 +03:00
Vitaly Turovsky
7d699f24bb fix: some areas in old world were competely white 2024-10-30 08:42:27 +03:00
Vitaly Turovsky
547525d615 fix: fix some blocks were rendered as transparent in old versions. Speed up Greenfield renderer by 18% in tunnels. 2024-10-30 08:40:44 +03:00
Vitaly Turovsky
900bcb0b56 fix: don't crash and conflict with g663 spyware installed 2024-10-30 07:02:33 +03:00
Vitaly Turovsky
dbd4058912 should fix publishing to npm 2024-10-28 17:57:40 +03:00
Vitaly Turovsky
153101fa6f ci: fix lint 2024-10-28 05:49:27 +03:00
Vitaly Turovsky
d497299235 feat: 1.21.1 support 2024-10-28 05:48:51 +03:00
Vitaly Turovsky
6b23eb6bad fix: never get stuck in unloaded chunks! @sa2urami
feat: fully supported spectator mode & basic creative fly fixes
2024-10-28 05:07:52 +03:00
Vitaly
5fa019e7b3
fix: don’t display advanced stats on prod deploy 2024-10-24 14:36:44 +03:00
Vitaly Turovsky
ebb5056540 ci: refactor deployment workflow in preview.yml by removing unused checks and adding alias retrieval step 2024-10-23 02:31:04 +03:00
Vitaly Turovsky
ece59e1744 ci: update event trigger from pull_request to pull_request_target in preview.yml (fix) 2024-10-23 02:20:44 +03:00
Vitaly
8955075d75
ci: trying to fix auto preview workflow! (#221) 2024-10-23 02:11:11 +03:00
Vitaly Turovsky
427ec21213 check that! 2024-10-23 02:03:05 +03:00
Vitaly Turovsky
42cc0bd818 ci: add push event trigger and refine deployment conditions in preview.yml for improved deployment handling 2024-10-23 01:38:55 +03:00
Vitaly Turovsky
9a7a13c2dd ci: enhance trigger conditions for deployment in preview.yml using variables for issue comments and pull requests 2024-10-23 01:00:34 +03:00
Vitaly Turovsky
e35873e106 ci: refactor variable usage from env to vars for PR check in preview.yml 2024-10-23 00:43:43 +03:00
Vitaly Turovsky
f900d6933c ci: simplify PR number check logic using fromJSON in AUTO_DEPLOY_PRS in preview.yml 2024-10-23 00:42:04 +03:00
Vitaly Turovsky
6354ba6bb8 ci: update variable name from env to vars in AUTO_DEPLOY_PRS check in preview.yml 2024-10-23 00:36:58 +03:00
Vitaly Turovsky
95c185fc0b fix: correct syntax for checking PR numbers in AUTO_DEPLOY_PRS in preview.yml 2024-10-23 00:35:33 +03:00
Vitaly
6c994a54f0
autodeploy PRs (#218) 2024-10-22 23:41:19 +03:00
Vitaly Turovsky
de6e82d94f quickly copy your positions with /pos in singleplayer 2024-10-22 23:11:42 +03:00
Vitaly Turovsky
347d155884 fix: improve playground by allowing sync world for fast iterating of advanced use cases 2024-10-22 19:28:41 +03:00
Vitaly Turovsky
70867564ed restore a way t ocreate worlds by enter 2024-10-21 00:41:35 +03:00
Vitaly Turovsky
a4180500d1 update types after minecraft-data types update 2024-10-21 00:05:33 +03:00
Vitaly Turovsky
b21146b92a fix: fix crafting in singleplayer
fix(generator): leaves were filled with water in new versions
fix(generator): set plains biome for light grass color
2024-10-20 23:54:25 +03:00
Vitaly Turovsky
c53ba87309 feat: add a setting to either completely hide perf stats or enable more advanced display
can be used for demos like: `?setting=renderDebug:"none"`
2024-10-20 20:27:03 +03:00
Vitaly Turovsky
a1c41e8767 fix: up deps to support 1.20.5 and 1.20.6
fix: fix raycast. Wasn't possible to active the block when inside of it
2024-10-20 18:26:05 +03:00
Vitaly
e19980800b
feat: rewrite playground from scratch + extras (#202) 2024-10-18 02:27:45 +03:00
Vitaly Turovsky
0368e12635 feat: add crafting and fall damage in survival 2024-10-17 15:58:45 +03:00
Valery Koultchitzky
bdcde9a4bb
fix: add a way to disable VR button (important for android users) (#209) 2024-10-17 14:31:01 +03:00
Vitaly Turovsky
af0d7d14ec fix: stop requesting server info on connect 2024-10-17 14:09:22 +03:00
Vitaly
bd180ef652 fix: correctly lock URL when connected to a server (Y) 2024-10-01 10:18:52 +03:00
Vitaly
0a0b87bee6
fallback p2p discovery server (#211) 2024-10-01 01:39:15 +03:00
Vitaly Turovsky
5b56518122 should fix build 2024-10-01 01:35:01 +03:00
Vitaly Turovsky
ab5f6ab448 fix: add fallback peerjs discovery server to bypass geo restrictions and because sometimes official server is down
feat: allow to use custom peerjs server via config
2024-10-01 01:34:42 +03:00
Vitaly Turovsky
2953554c53 fix(regression): player walking animation was broken 2024-09-28 03:28:27 +03:00
Vitaly Turovsky
00150dda1d fix: inventory UI crash in some cases with some specific window titles
fix: client messages were not displayed on the latest version
2024-09-28 02:57:18 +03:00
Valery-a
40f81d84cd
server change (#207) 2024-09-28 01:43:47 +03:00
Vitaly Turovsky
3ea95d509a fix: fix github pages main deploy 2024-09-19 02:34:45 +03:00
Vitaly Turovsky
9bac681c29 use correct zombie model 2024-09-12 23:38:45 +03:00
Valery-a
7da41b02c9
fix: fixed zombies and husks not having texture (#203) 2024-09-12 23:00:58 +03:00
Vitaly Turovsky
18a6f2c1f5 fix: rare case where digging animation was not cancelled after actual dig cancel after respawn 2024-09-12 04:32:37 +03:00
Vitaly Turovsky
33437823f3 disable outdated packages check for now 2024-09-11 22:40:41 +03:00
Vitaly Turovsky
d0b921a48e revert update current ref 2024-09-11 22:39:51 +03:00
Vitaly Turovsky
16bb43c7d9 update current ref 2024-09-11 22:16:26 +03:00
Vitaly Turovsky
5a3fb6f225 disable Java integration test because of issues with downloading server 2024-09-11 22:06:14 +03:00
Vitaly Turovsky
755eead976 correctly capture screenshots of cypress 2024-09-11 21:51:12 +03:00
Vitaly Turovsky
25db002bc3 redirect to correct playground url 2024-09-11 21:39:15 +03:00
Vitaly
74fe84e10d fix playground on windows: rsbuild does not implement --root option for win 2024-09-11 19:27:17 +03:00
Vitaly Turovsky
76bed4d496 eslint: ignore dist linting! 2024-09-11 03:34:55 +03:00
Vitaly Turovsky
4d3c92f611 some playground fixes 2024-09-11 03:19:24 +03:00
Vitaly Turovsky
1446ccc0a7 should fix playground build 2024-09-11 02:35:29 +03:00
Vitaly Turovsky
f9b87d5087 move playground to rsbuild! now fast reloads!
reload on workers change

fix cd
2024-09-11 02:28:56 +03:00
Vitaly Turovsky
18bf1aa80a feat: The commit also adds a new keybind action for the 'F4' key, allowing the user to cycle through different game modes. Depending on the current game mode, the bot's chat command is updated accordingly. 2024-09-10 20:00:09 +03:00
Vitaly Turovsky
2c971f331e fix: update entities tracker which should fix playing walking animations when players are standing still 2024-09-10 01:34:26 +03:00
Vitaly Turovsky
a5dddfaad5 up mineflayer-auto-jump 2024-09-10 01:20:39 +03:00
Vitaly Turovsky
c6c25a7bb9 up again 2024-09-10 00:52:39 +03:00
Vitaly Turovsky
3fb872129e fix: update autojump module 2024-09-10 00:29:30 +03:00
Vitaly Turovsky
ad8dc1a21a fix: fix compatibility with some versions of new region format files 2024-09-08 21:03:11 +03:00
Vitaly Turovsky
a3ef16a81a ci: checkout pr before merge 2024-09-08 18:40:37 +03:00
Vitaly Turovsky
f518dce04d ci: checkout pr before merge 2024-09-08 18:39:13 +03:00
Vitaly Turovsky
e89196041e ci: update commands 2024-09-08 18:37:06 +03:00
Vitaly Turovsky
f9a4960c31 ci: try to fix the commands 2024-09-08 18:28:55 +03:00
Vitaly Turovsky
d743981fc2 up workflow files: new commands 2024-09-08 18:20:34 +03:00
Vitaly Turovsky
fad9fd6e3a fix: provide a hack to just render blocks all the blocks even with unknown states for preflat versions 2024-09-07 19:48:08 +03:00
Vitaly Turovsky
a063a0d75b fix: fix cobblestone_wall and player head (skull) rendering in preflat versions 2024-09-07 19:42:50 +03:00
Vitaly Turovsky
1c7fdc21a6 add a way to to disable neighbor chunk updates and all the UI (needed for perf testing) 2024-09-07 19:33:16 +03:00
Vitaly Turovsky
a30106342e feat: add a way to disable some of the UI parts in settings (for testing and other use cases)
feat: re-add bossbars, but it's still disabled by default
2024-09-07 18:37:57 +03:00
Vitaly Turovsky
9160ff33c2 fix: fix joining to some popular servers (since dns was resovle was incorrectly used)
fix: fix some auth issues when starting the app locally
2024-09-07 17:46:34 +03:00
Vitaly Turovsky
cd9ead74d2 fix(regression,critical): chunks were stopped loading after moving to another chunk
fix: fix command blocks parsing for most versions
fix: chat was not working for 1.19+
fix: better range adjustment for plate command block activation
fix: was not possible to change the warp
2024-09-06 10:23:46 +03:00
Vitaly Turovsky
3f761430d7 change name of builtin server 2024-09-06 03:59:07 +03:00
Vitaly Turovsky
8e330c0253 feat(devtools): downloadFile global function
fix: local server wasnt saving the time of the world
feat(advanced): add a way to specify local server options
2024-09-06 03:48:54 +03:00
Vitaly Turovsky
d6964b89eb fix typings 2024-09-05 18:23:05 +03:00
Vitaly Turovsky
ea5a48967b fix: improve signs viewer by allow to copy position and select signs 2024-09-05 16:48:04 +03:00
Vitaly Turovsky
9dfff40afd fix(regression): fix dropped items display (for preflat versions it is still broken like many other entities display) 2024-09-05 15:13:26 +03:00
Vitaly Turovsky
65ba687e08 force make crosshair transparent to avoid issues with old darkreader safari version 2024-09-04 19:17:02 +03:00
Vitaly Turovsky
a9d2104dbf fix: Force disable dark reader as it was making the crosshair (reticle) black 2024-09-04 19:14:09 +03:00
Vitaly Turovsky
c6ea9f79dc fix(regression): instruct the browser that the web app uses dark mode (which fixes some edge-case issues) 2024-09-04 19:02:02 +03:00
Vitaly Turovsky
266d34c1cf fixup for player pos in chunk loader 2024-09-04 05:48:00 +03:00
Vitaly
5aaa687392
feat: display progress of downloading chunks visually (#195) 2024-09-04 05:03:17 +03:00
Vitaly Turovsky
306f894d8c fix(important,singleplayer): stop loading chunks in previous position when teleported to a new pos and always start loading chunks in new pos 2024-09-04 03:18:56 +03:00
Vitaly Turovsky
9e7711e386 add eaglercraft as alternative, fix types again 2024-09-03 03:15:17 +03:00
Vitaly Turovsky
684261e515 fix building, update test types 2024-09-03 03:08:19 +03:00
Vitaly Turovsky
c2a34ea9f1 fix(preflat-worlds): improve mesher performance by 2x by syncing the code from webgpu branch
fixes #191
2024-09-03 02:48:16 +03:00
Vitaly Turovsky
698fb1d388 fix tsc 2024-09-03 01:13:12 +03:00
Vitaly Turovsky
559f535207 don't lie of resoure pack support 2024-09-03 01:11:49 +03:00
Vitaly Turovsky
b2ac80602c feat(important): redirect to origin website from maps.mcraft.fun which makes testing maps so much easier on preview deploys and locally 2024-09-03 01:10:11 +03:00
Vitaly Turovsky
0d3a3affd7 fix recently introduced bug with crafting in singleplayer 2024-09-03 01:00:54 +03:00
Vitaly Turovsky
00dd606091 [skip ci] cleanup starfield code 2024-09-02 23:50:46 +03:00
Vitaly Turovsky
574dbafc28 fix(renderer,important): fix all known rendering issues with starfield by @sa2urami 2024-09-02 23:46:22 +03:00
Valery-a
66d26ad2e6
feat: add visuals for entities damaging (#186) 2024-09-01 17:53:48 +03:00
Vitaly
ee966395c6
feat: Display holding block (experimental setting) (#190) 2024-09-01 03:32:53 +03:00
496 changed files with 56572 additions and 38794 deletions

View 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.

View file

@ -3,4 +3,7 @@ rsbuild.config.ts
*.module.css.d.ts
*.generated.ts
generated
dist
public
**/*/rsbuildSharedConfig.ts
src/mcDataTypes.ts

View file

@ -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
View 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
View 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
View 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

View file

@ -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
View 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
View 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

View file

@ -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!',
});
}

View file

@ -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

View file

@ -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
View 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
View file

@ -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

View file

@ -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

View file

@ -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"]

View file

@ -2,34 +2,66 @@
![banner](./docs-assets/banner.jpg)
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
![docs-assets/singleplayer-future-city-1-10-2.jpg](./docs-assets/singleplayer-future-city-1-10-2.jpg)
### 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:
[![Deploy to Koyeb](https://www.koyeb.com/static/images/deploy/button.svg)](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

View file

@ -6,7 +6,7 @@ Minecraft UI components for React extracted from [mcraft.fun](https://mcraft.fun
pnpm i minecraft-react
```
![demo](https://github-production-user-asset-6210df.s3.amazonaws.com/46503702/346295584-80f3ed4a-cab6-45d2-8896-5e20233cc9b1.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAVCODYLSA53PQK4ZA%2F20240706%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20240706T195400Z&X-Amz-Expires=300&X-Amz-Signature=5b063823a57057c4042c15edd1db3edd107e00940fd0e66a2ba1df4e564a2809&X-Amz-SignedHeaders=host&actor_id=46503702&key_id=0&repo_id=432411890)
![demo](./docs-assets/npm-banner.jpeg)
## Usage

58
TECH.md Normal file
View 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
View 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>

View 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
View 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>

View file

@ -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
View file

@ -0,0 +1,4 @@
<!-- just redirect to /playground/ -->
<script>
window.location.href = `/playground/${window.location.search}`
</script>

View file

@ -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
View file

@ -0,0 +1,5 @@
{
"alwaysReconnectButton": true,
"reportBugButtonWithReconnect": true,
"allowAutoConnect": true
}

View file

@ -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__/*'],
},
})

View 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'))
})
})
})

View file

@ -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')

View file

@ -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 = {

View file

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

1
experiments/state.html Normal file
View file

@ -0,0 +1 @@
<script src="state.ts" type="module"></script>

37
experiments/state.ts Normal file
View 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()

View 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
View 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()

View file

@ -0,0 +1,5 @@
<script type="module" src="three-labels.ts"></script>
<style>
body { margin: 0; }
canvas { display: block; }
</style>

View 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()

View file

@ -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);
})

View file

@ -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>

View file

@ -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"
}

View 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 })
}

View file

@ -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,

View file

@ -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;

View file

@ -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

File diff suppressed because it is too large Load diff

View file

@ -1,4 +1,4 @@
packages:
- "."
- "prismarine-viewer"
- "prismarine-viewer/viewer/sign-renderer/"
- "renderer"
- "renderer/viewer/sign-renderer/"

View file

@ -0,0 +1,5 @@
# Prismarine Viewer
Renamed to `renderer`.
For more info see [CONTRIBUTING.md](../CONTRIBUTING.md).

View file

@ -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()

View file

@ -1 +0,0 @@
export { default as rotation } from './rotation'

View file

@ -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

View file

@ -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

View file

@ -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()

View file

@ -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
}

View file

@ -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()
}
}
}

View file

@ -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)
}
}
}

View file

@ -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

View file

@ -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

View file

@ -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()
}
}
}

View file

@ -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
}

View file

@ -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

View file

@ -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)

View file

@ -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
}

View file

@ -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)
}
}

View file

@ -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 }

View file

@ -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)
})
}

View file

@ -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 }

View file

@ -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()
}
}

View file

@ -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()
}
}

View file

@ -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.

View file

@ -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)
}
}
}

View file

@ -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)
})
}
}

View file

@ -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'}>
}`,
})
}
}

View file

@ -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
}

View file

@ -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

View file

@ -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",

View file

@ -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>

View 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)
}

View 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()
}
}

View 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

View 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>
)
}

View 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)
}
}

View 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, {})
}
}
}
}
}

View 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))
}
}
}
}

View 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()
}
})
}
}

View 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'

View 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, {})
}
}

View 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

View 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 })
}
}

View file

@ -0,0 +1,7 @@
import { BasePlaygroundScene } from '../baseScene'
export default class RotationIssueScene extends BasePlaygroundScene {
setupWorld () {
// todo
}
}

View 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 })
}
}

View 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')
}
}

View 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)
}
}

View 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())
},
},
],
})
)

View 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'
}
}

View 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