Compare commits

...

539 commits

Author SHA1 Message Date
griffi-gh e7d81ce7cc add the old "lighting" thingy 2024-05-13 23:56:31 +02:00
griffi-gh db05b0902d Remove unneded use of nonsendsync 2024-05-13 23:44:48 +02:00
griffi-gh 1b096c07ed add submerge overlay back 2024-05-13 23:43:39 +02:00
griffi-gh 34a0c8c648 wip smoverlay 2024-05-13 19:47:36 +02:00
griffi-gh 1bf61500b3 trans chunks are visible again! 2024-05-08 20:13:19 +02:00
griffi-gh 7928745938 players :3 2024-05-08 15:02:16 +02:00
griffi-gh 438b79e8f0 uwu 2024-05-08 13:35:03 +02:00
griffi-gh eec672d665 kitty? kitty! 2024-05-08 13:12:58 +02:00
griffi-gh 3f0697d573 use latest hui 2024-05-08 03:22:41 +02:00
griffi-gh 7eafea521f kubiui integration update 2024-05-08 03:04:50 +02:00
griffi-gh ec23cedb7a use proper srgb color for background 2024-05-07 21:04:50 +02:00
griffi-gh c98f504310 print adapter info on start 2024-05-07 20:50:59 +02:00
griffi-gh 18e35b3ea4 Selection Box 2024-05-07 20:12:07 +02:00
griffi-gh 75cb0ead6b clean up world renderer 2024-05-07 01:15:44 +02:00
griffi-gh 487cf31843 wip 2024-05-07 01:14:36 +02:00
griffi-gh 7aa91b6414 add cube primitive 2024-05-07 01:11:05 +02:00
griffi-gh 196a7010b6 separate clear_bg pass 2024-05-07 00:13:14 +02:00
griffi-gh c8cacafab5 use default limits 2024-05-06 18:21:19 +02:00
griffi-gh d738b228f2 fix compilation for android 2024-05-06 18:07:29 +02:00
griffi-gh 811bd8d8b4 use linear for min 2024-05-06 17:38:58 +02:00
griffi-gh 99cc2d1e72 enable all backends 2024-05-06 17:21:18 +02:00
griffi-gh 85c64c9064 flip uvs 2024-05-06 17:19:11 +02:00
griffi-gh 6e9e3fa445 enable depth texture 2024-05-06 17:09:13 +02:00
griffi-gh 8a4549efea restructure stuff and fix some warnings 2024-05-06 16:53:40 +02:00
griffi-gh 6f25cb728f remove WindowSize 2024-05-06 16:24:44 +02:00
griffi-gh 9a01ecd6f2 depth, wip 2024-05-06 16:17:01 +02:00
griffi-gh 8fd1930ce6 forgor vec4 type 2024-05-06 15:54:17 +02:00
griffi-gh 674ec97a6e oops 2024-05-06 15:53:47 +02:00
griffi-gh f04542ac02 discard fully transparent 2024-05-06 15:52:59 +02:00
griffi-gh dd386acea1 owo 2024-05-06 15:48:36 +02:00
griffi-gh d14b5e1b40 ok this is better 2024-05-06 14:29:07 +02:00
griffi-gh f4e4886d33 i see... something? 2024-05-06 13:32:44 +02:00
griffi-gh 3f768a8318 do rpass in world.rs 2024-05-05 15:42:25 +02:00
griffi-gh baf037d4a5 use Arc<Window> instead of create_surface_unsafe 2024-05-05 15:20:44 +02:00
griffi-gh 2c2199d520 ... 2024-05-05 15:19:09 +02:00
griffi-gh 1d9fafd408 lifetime errors :є 2024-05-05 13:25:54 +02:00
griffi-gh 58e4eed14e init texture prefab 2024-05-05 13:16:48 +02:00
griffi-gh 90784e21dd minor changes 2024-05-05 02:02:30 +02:00
griffi-gh 260f4b4232 load block textures 2024-05-05 02:00:06 +02:00
griffi-gh 35ff06a439 minor changes to rendering.rs 2024-05-05 01:34:27 +02:00
griffi-gh 7ac045f013 move renderer into it's own file 2024-05-05 01:30:07 +02:00
griffi-gh 0b69377865 copy over world render 2024-05-05 01:25:44 +02:00
griffi-gh 324270ed7d write some dumb shader code 2024-05-05 01:06:43 +02:00
griffi-gh 906f4882a2 throw away glsl shaders 2024-05-05 00:17:27 +02:00
griffi-gh c32568af1c :3 2024-05-05 00:16:55 +02:00
griffi-gh 1d4cbcc2b1 oops forgor to cast trans buffers 2024-05-04 23:18:27 +02:00
griffi-gh 8373d2be54 create buffers 2024-05-04 23:17:12 +02:00
griffi-gh eaad06863f "fix" compile errs 2024-05-04 22:10:40 +02:00
griffi-gh 0e22bccbac init wgpu stuff 2024-05-04 17:24:19 +02:00
griffi-gh 4ff284288e . 2024-05-04 14:32:21 +02:00
griffi-gh e11ee19597 rip out some parts of glium 2024-05-04 14:30:06 +02:00
griffi-gh 8f606b77c0 use single seeder instance 2024-05-04 13:50:58 +02:00
griffi-gh 048e45628a render trans chunks after entities/sel box etc 2024-05-04 00:35:56 +02:00
griffi-gh 7108afa662 remove unused 2024-05-04 00:07:11 +02:00
griffi-gh 11ad2cdc77 preheat chunks on server 2024-05-04 00:05:16 +02:00
griffi-gh dac3c10aee fix mesh not updating 2024-05-03 23:43:01 +02:00
griffi-gh e9000af878 handle unsub requests 2024-05-03 23:39:37 +02:00
griffi-gh 49753ecc4c refactor state transitions 2024-05-03 23:29:52 +02:00
griffi-gh 94fa5268fa remove outdated info from readme 2024-05-03 20:27:39 +02:00
griffi-gh e3692bca94 update 2024-05-03 20:18:45 +02:00
griffi-gh 3f8056b6ea always request gles3 2024-05-03 14:01:05 +02:00
griffi-gh b596deeaed hopefully fix trans rendering bug 2024-05-03 13:36:22 +02:00
griffi-gh ec592951bc add lints 2024-05-03 01:48:12 +02:00
griffi-gh 656f124549 "fix" some warnings 2024-05-03 01:39:47 +02:00
griffi-gh 00b8a253bb
Merge pull request #19 from griffi-gh/rewrite-wgen
Rewrite world generation
2024-05-03 01:23:34 +02:00
griffi-gh 875f131fe1 move stuff, forests! 2024-05-03 01:21:56 +02:00
griffi-gh 91326ce2dc decoration and layers 2024-05-03 00:27:43 +02:00
griffi-gh d620ba3840 add opt lvl 2024-05-02 22:27:49 +02:00
griffi-gh 73695dcd5a check before first step 2024-05-02 18:16:29 +02:00
griffi-gh 8790454b23 switch libs 2024-05-02 18:15:18 +02:00
griffi-gh 650ea55a14 disable borked cave generation 2024-05-02 16:51:03 +02:00
griffi-gh 8c728f9650 abortions and stuff 2024-05-02 16:50:46 +02:00
griffi-gh 64475022e3 rewrite worldgen 2024-05-02 12:42:14 +02:00
griffi-gh 7620717368 drop kubi-pool 2024-05-02 11:14:47 +02:00
griffi-gh f157e2dab3 add overlay when submerged 2024-05-02 11:03:38 +02:00
griffi-gh 8e39fc24fd more settings 2024-05-02 02:43:58 +02:00
griffi-gh a63deb5173 add tip to chat 2024-05-02 02:22:55 +02:00
griffi-gh 8c5b0aa47e integrate hui-winit 2024-05-02 02:20:54 +02:00
griffi-gh 8e907a9fbc if not locked dont move camera 2024-05-02 02:07:49 +02:00
griffi-gh ce5dd6f011 move cursor to center on unlock 2024-05-02 02:07:36 +02:00
griffi-gh a5fae8ad2b wip settings 2024-05-02 02:06:23 +02:00
griffi-gh bb9107e912 ~~scuffed sorting?~~ 2024-05-02 01:42:07 +02:00
griffi-gh dd6f52edb5 scuffed sorting 2024-05-02 01:38:58 +02:00
griffi-gh 6cde878a50 fix shader 2024-05-02 01:24:27 +02:00
griffi-gh 62c3c2105e refactor transparency descriptor 2024-05-02 01:21:17 +02:00
griffi-gh e1f1ba706c set discard_alpha 2024-05-02 01:13:21 +02:00
griffi-gh 204bb882a6 alt shape 2024-05-02 01:05:34 +02:00
griffi-gh 610d309ead dynamic crosshair stuff 2024-05-02 00:43:24 +02:00
griffi-gh 66d3ea656b add trans rendering and crosshair 2024-05-02 00:32:43 +02:00
griffi-gh 772a8ea7db add logging to blk queue 2024-04-25 19:14:56 +02:00
griffi-gh 6f84d9014a implement local server queue 2024-04-25 19:13:05 +02:00
griffi-gh 6ee282e744 fix missing module 2024-04-25 18:39:18 +02:00
griffi-gh 5d8906cfb1 upgrade deps 2024-04-25 18:29:34 +02:00
griffi-gh 8c4ef7f83f downgrade back to rand 0.8 from alpha 2024-04-25 18:28:14 +02:00
griffi-gh b8c7dcc196 revert back to yellow 2024-04-25 17:52:42 +02:00
griffi-gh dc1a641887 Make leave messages red 2024-04-25 17:50:09 +02:00
griffi-gh e373aa758c rename ChatManager to ChatHistory 2024-04-25 17:49:12 +02:00
griffi-gh 907a5845fa randomize usernames 2024-04-25 17:48:39 +02:00
griffi-gh 740da98cbd stuff 2024-04-25 15:16:31 +02:00
griffi-gh af3c938a03 uwu 2024-04-25 13:41:50 +02:00
griffi-gh 50cc36e3d5 add chat 2024-04-25 13:39:23 +02:00
griffi-gh 043bb873c4 Sync client disconnects 2024-04-25 12:30:25 +02:00
griffi-gh ec17768842 Merge branch 'master' of https://github.com/griffi-gh/kubi 2024-04-23 16:16:37 +02:00
griffi-gh 5c39900376 upgrade shipyard (fix compilation on nightly) 2024-04-23 16:16:33 +02:00
griffi-gh 65bfa8fcc1
Create LICENSE 2024-04-21 19:31:29 +02:00
griffi-gh 7ead258028 upgrade to hui = "0.1.0-alpha.4" 2024-03-25 19:06:35 +01:00
griffi-gh f0270e3ce5 handle fuck off request 2024-02-21 04:21:47 +01:00
griffi-gh 00edf0d272 restructure stuff 2024-02-21 03:51:05 +01:00
griffi-gh a63cc4bd17 update the loading screen 2024-02-21 03:31:52 +01:00
griffi-gh 80614e8461 separate loading screen base 2024-02-21 02:28:15 +01:00
griffi-gh 3d8307b124 owo 2024-02-20 22:29:55 +01:00
griffi-gh 6853f4529e update hui to 0.1.0-alpha.3, new loading screen 2024-02-20 22:23:34 +01:00
griffi-gh 5d8cc1433f upgrade deps 2024-02-18 04:15:26 +01:00
griffi-gh 2548884354 upgrade to hui 0.1.0-alpha.1 2024-02-18 04:14:17 +01:00
griffi-gh 2045b26544 separate kubi-ui as hui 2024-02-17 23:14:04 +01:00
griffi-gh 5c40e68d66
Merge pull request #14 from griffi-gh/client-physics
Client physics (wip)
2024-02-17 23:59:12 +03:00
griffi-gh 14e9cb32f7 pass velocity to server move evt 2024-02-17 14:45:30 +01:00
griffi-gh f3a844bf55 clamp? 2024-02-16 15:13:37 +01:00
griffi-gh f97b922943 uwu 2024-02-16 00:43:11 +01:00
griffi-gh 6a96d6c3d3 fun 2024-02-15 19:39:09 +01:00
griffi-gh 778c2b279e use force for plr 2024-02-15 15:51:06 +01:00
griffi-gh 89ccd595ac fps ctl test impl 2024-02-15 14:49:41 +01:00
griffi-gh b34f1a94b1 fix ctl speed 2024-02-15 01:54:31 +01:00
griffi-gh 1466f62b5a pl ctl 2024-02-15 01:51:41 +01:00
griffi-gh 93e34b2c88 refactor stuff 2024-02-14 21:56:59 +01:00
griffi-gh 0ae87b6ddd wip client physics 2024-02-14 01:59:37 +01:00
griffi-gh c162893fd0 remove notics abt multitouch 2024-02-13 11:52:50 +01:00
griffi-gh 4948d85e05 wip cl physics 2024-02-13 00:50:30 +01:00
griffi-gh 47839c03aa normalize rotation in update_movement 2024-02-13 00:00:01 +01:00
griffi-gh a468e764c3 rename proces_glutin_events to process_winit_events 2024-02-12 23:53:14 +01:00
griffi-gh 5e63d1b630 beautify code 2024-02-12 23:50:08 +01:00
griffi-gh f00b2081e6 fix landscape 2024-02-12 22:59:57 +01:00
griffi-gh 748f12a1ac what the fuck :ferrisballSweat: 2024-02-12 22:59:49 +01:00
griffi-gh 685cd43ada fix crash on android by using dynamic buffers for ui 2024-02-12 22:29:40 +01:00
griffi-gh 3f38351356 upgrade deps 2024-02-12 22:20:45 +01:00
griffi-gh 9f5b974f82 increase small 2024-02-01 16:21:10 +01:00
griffi-gh bfdd506ae8 rename guiv2_integration -> kubi_ui_integration 2024-02-01 16:12:51 +01:00
griffi-gh 3a42c293dd refactor net code to use enums 2024-02-01 16:00:32 +01:00
griffi-gh f5fd6d0b05 set protocol id back to zero 2023-12-03 16:50:07 +01:00
griffi-gh ff6f89f92e allow chunk size to be increased past 32 2023-12-03 16:48:48 +01:00
griffi-gh 15d1184451 uwu 2023-12-02 21:32:20 +01:00
griffi-gh 726dfa4b52 owo 2023-12-02 21:20:20 +01:00
griffi-gh f15ae91155 add new text test 2023-12-02 21:12:26 +01:00
griffi-gh a42a3d95c3 separate textured shader 2023-12-02 20:50:25 +01:00
griffi-gh adbab2b2a6 it works 2023-12-02 19:46:08 +01:00
griffi-gh 6b33e9cdc9 use fontdue layout 2023-12-02 19:27:21 +01:00
griffi-gh f19031662f owo 2023-12-02 19:11:41 +01:00
griffi-gh 491bb435cc Vec<UiDrawCommand> -> UiDrawCommands 2023-12-02 19:07:31 +01:00
griffi-gh eaf4d98d65 font handle stuff 2023-12-02 17:47:25 +01:00
griffi-gh e1b41bbc79 text kinda works! 2023-12-02 17:16:45 +01:00
griffi-gh 5823f05943 rename texman.rs to ftm 2023-12-02 17:05:38 +01:00
griffi-gh dea9b7c584 kinda got text rendering working as well as multi draw call 2023-12-02 17:05:03 +01:00
griffi-gh 207f29e0ee run cargo update 2023-12-02 12:50:41 +01:00
griffi-gh 0f93d0ca71 tr 2023-12-01 21:48:17 +01:00
griffi-gh ee1f3ced47 uwu 2023-12-01 20:51:41 +01:00
griffi-gh 5bac932108 add bind_texture field to UiVertex 2023-12-01 00:54:32 +01:00
griffi-gh 200092f52a misc. font rendering changes in kui, minor backend api change 2023-12-01 00:45:56 +01:00
griffi-gh 1c52273ce2 update ftm stuff 2023-11-30 21:15:00 +01:00
griffi-gh 769d7e84e9 demonstrate padding on all 4 sides 2023-11-28 20:18:47 +01:00
griffi-gh 0862ffaf04 fix manifest 2023-11-28 20:18:36 +01:00
griffi-gh f7c1a58748 fix readme link 2023-11-28 20:15:00 +01:00
griffi-gh 747576a6e7 run cargo update 2023-11-28 20:13:30 +01:00
griffi-gh a5612d965f major ui restructure, wip interactable api 2023-11-28 20:13:14 +01:00
griffi-gh 2e91b3a9ee main axis align 2023-11-28 11:32:09 +01:00
griffi-gh eb23bdb448 tst 2023-11-28 10:39:25 +01:00
griffi-gh bf5b05295d fix stuff 2023-11-28 10:31:38 +01:00
griffi-gh 0af548320b owo 2023-11-26 01:47:48 +01:00
griffi-gh 16ffb6f786 wip item system 2023-11-26 01:29:02 +01:00
griffi-gh f4c2bcf998 magic -> subheader 2023-11-25 21:12:29 +01:00
griffi-gh 672007ac7b reduce default render distanc 2023-11-25 16:32:05 +01:00
griffi-gh 9ba3b42991 trigger workflow 2023-11-25 16:23:19 +01:00
griffi-gh a1307b1abc fix ci, hopefully 2023-11-25 16:22:44 +01:00
griffi-gh feac14c79c uwu 2023-11-25 16:19:16 +01:00
griffi-gh f7f0ca4547 ui.ui -> ui.kui 2023-11-25 16:18:40 +01:00
griffi-gh cb22f4db59 . 2023-11-25 16:17:47 +01:00
griffi-gh 72a12b5ad9 upd. readme 2023-11-25 15:55:40 +01:00
griffi-gh 225cecf1fa font cache! 2023-11-24 17:54:23 +01:00
griffi-gh cc32082c21 wip 2023-11-23 23:13:17 +01:00
griffi-gh f8cd34443a wip 2023-11-23 21:27:11 +01:00
griffi-gh f56c7a4b8f build 2023-11-23 18:01:18 +01:00
griffi-gh cf8263fee0 wip texture managment 2023-11-23 17:59:14 +01:00
griffi-gh b17519cbc3 wip text render, minor changes 2023-11-23 17:14:02 +01:00
griffi-gh 0b210eac5d use immut. buffers for primitives 2023-11-23 11:43:48 +01:00
griffi-gh 0fa723fc00 fix padding 2023-11-23 11:26:49 +01:00
griffi-gh a95eb96843 . 2023-11-23 11:11:04 +01:00
griffi-gh 0f4264292c restructure stuff 2023-11-23 11:10:42 +01:00
griffi-gh 4e47901117 add test 2023-11-23 11:10:35 +01:00
griffi-gh 447a848009 add traits 2023-11-23 10:53:44 +01:00
griffi-gh 63f862c2be (todo) ui demo 2023-11-23 01:48:04 +01:00
griffi-gh 4263c1ff6c container impl (wip) 2023-11-23 01:47:54 +01:00
griffi-gh b28dca8721 add rect element 2023-11-23 01:47:27 +01:00
griffi-gh 3d8803f465 spacer default 2023-11-23 01:22:33 +01:00
griffi-gh 711942567f oh fuck. 2023-11-22 22:03:39 +01:00
griffi-gh 2d655ea8f1 discard alpha, use treshold 2023-11-22 22:02:30 +01:00
griffi-gh d909ff7424 separate out buffer handling code 2023-11-22 21:57:13 +01:00
griffi-gh 245db03519 oops forgot to remove shader from prefab too 2023-11-22 21:31:05 +01:00
griffi-gh 828694cf5a remove legacy ui shaders 2023-11-22 20:27:08 +01:00
griffi-gh 547759d6b2 fix crash, ui core multi-draw call 2023-11-22 20:26:44 +01:00
griffi-gh d852c48e4a drop legacy ui system 2023-11-22 19:03:17 +01:00
griffi-gh a53f6f9901 migrate loading screen to guiv2, fix bugs 2023-11-22 19:01:32 +01:00
griffi-gh b601aea288 fix ui shader 2023-11-22 18:40:35 +01:00
griffi-gh a5641e0d2e prefer gles core 2023-11-22 17:22:40 +01:00
griffi-gh 198dfb088e fix missing depth buffer (like wtf) 2023-11-22 17:07:55 +01:00
griffi-gh d848f60301 commit 2023-11-22 16:51:59 +01:00
griffi-gh ac497b0651 log when uploading ui buffers 2023-11-22 14:38:15 +01:00
griffi-gh 567fd1d6aa use cyan for debug/trace 2023-11-22 14:38:02 +01:00
griffi-gh d9d3f78066 log shader compiler release 2023-11-22 14:33:43 +01:00
griffi-gh 22c13eeaea pub progressbar, impl default 2023-11-22 14:33:35 +01:00
griffi-gh 09effab7c3 integrate ui 2023-11-22 14:23:48 +01:00
griffi-gh 467e61cb70 use immutable buffers for chunk data 2023-11-22 12:01:49 +01:00
griffi-gh d75a5fa3cb Merge branch 'master' of https://github.com/griffi-gh/kubi 2023-11-22 11:56:48 +01:00
griffi-gh a3857d0586 glium ui backend, misc gui changes 2023-11-22 11:56:46 +01:00
griffi-gh 447a06e9de
Update README.md 2023-11-22 01:14:47 +01:00
griffi-gh fe1427249b
Merge pull request #8 from griffi-gh/upgrade-deps0
Upgrade to latest `glium`/`glutin`/`winit`, use `android-activity` instead of `ndk_glue`
2023-11-21 22:41:32 +01:00
griffi-gh 6f982d4308 change settings 2023-11-21 22:40:57 +01:00
griffi-gh 3069c1c72f . 2023-11-21 19:39:21 +01:00
griffi-gh 5e62e1781d gitignore logs 2023-11-21 19:39:09 +01:00
griffi-gh f7210e4dec minor changes 2023-11-21 19:33:05 +01:00
griffi-gh 7dc33c2dce stuff 2023-11-21 19:23:48 +01:00
griffi-gh 517838e2ae copy over simple init clode 2023-11-21 18:26:39 +01:00
griffi-gh 5987484452 fix android event loop init 2023-11-21 18:06:34 +01:00
griffi-gh ae7cc718cf fix visibility 2023-11-21 17:51:10 +01:00
griffi-gh 06a49e1b93 wip fix android 2023-11-21 17:49:40 +01:00
griffi-gh bc36bb8ce2 fix mouse input 2023-11-21 17:30:04 +01:00
griffi-gh 612e46f454 reuse window creation code 2023-11-21 17:27:44 +01:00
griffi-gh c69ce9105c fix the rest of stuff
now i need to fix mouse buttons and add init code back
2023-11-21 17:24:37 +01:00
griffi-gh 363dcddeca fix some stuff 2023-11-21 17:19:52 +01:00
griffi-gh 2656f5af21 upgrade more stuff 2023-11-21 17:11:53 +01:00
griffi-gh 3388dd6491 get it to compile 2023-11-21 17:03:50 +01:00
griffi-gh dec20225cb undo 2023-11-21 16:46:18 +01:00
griffi-gh d50f4323b1 start fixing stuff 2023-11-21 16:45:51 +01:00
griffi-gh 014e0e7824 upgrade shipyard/glium 2023-11-21 16:45:41 +01:00
griffi-gh 5678b0c06f update ui system 2023-11-21 16:14:26 +01:00
griffi-gh 927337c86d wip ui 2023-11-21 14:08:22 +01:00
griffi-gh 2803ac03db wip new gui 2023-11-21 01:03:04 +01:00
griffi-gh cab3f667e0 update readme 2023-11-21 00:01:52 +01:00
griffi-gh 13516267cb fix readme with up-to-date info 2023-11-20 23:56:42 +01:00
griffi-gh 4338c536ad add default-members 2023-11-20 23:53:51 +01:00
griffi-gh bd327c86f3 change stuff :3 (will break android builds) 2023-11-20 23:21:50 +01:00
griffi-gh 43ca458896 wip data 2023-11-20 20:59:34 +01:00
griffi-gh e2bec4bf2e . 2023-11-20 19:51:16 +01:00
griffi-gh 7ba5d739e7 upgrade some deps 2023-11-19 19:36:17 +01:00
griffi-gh 9bdc38ccf0 wip kubipool 2023-11-19 19:30:06 +01:00
griffi-gh e40e394bd8 upd data 2023-11-19 19:19:37 +01:00
griffi-gh 2cdc79cb18 wip data 2023-11-19 18:27:20 +01:00
griffi-gh 9affee68c3 oops my screen is hdr only 2023-09-25 11:37:59 +02:00
griffi-gh abeef03d50 upgrade dependencies 2023-07-09 03:03:45 +02:00
griffi-gh cc1490db8a Revert "mess around with srgb (wip)"
This reverts commit 3b87fc6423.
2023-07-09 01:40:40 +02:00
griffi-gh 55f2fc07b3
Update README.md 2023-06-10 09:20:01 +02:00
griffi-gh 3b87fc6423 mess around with srgb (wip) 2023-06-04 19:38:46 +02:00
griffi-gh d1427bfb10 Update readme 2023-06-04 18:51:37 +02:00
griffi-gh cc9a829552 add toml highlight to readme 2023-06-04 18:42:33 +02:00
griffi-gh b60eb42a81 android controls -> touch controls 2023-06-04 18:40:54 +02:00
griffi-gh b3be350047 Add touch controls, switch to tinymap for keyboard input 2023-06-04 18:39:55 +02:00
griffi-gh 3a9a452fda update android metadata 2023-06-04 15:44:27 +02:00
griffi-gh e4ce6f290e remove notes about nightly from android 2023-06-04 15:31:23 +02:00
griffi-gh 01cf42b2bf This is deprecated but required to make cargo-apk work! 2023-06-04 15:30:47 +02:00
griffi-gh e70165ba00 change orientation 2023-06-04 15:29:53 +02:00
griffi-gh 45043ac04b update info around nightly in readme 2023-06-04 15:19:33 +02:00
griffi-gh d5e8b4c6b7 add raw-evt feature, fix clippy warnings
`raw-evt` is enable by default
forcing it off is required for kb input on android
mouse input is not supported without it yet though
2023-06-04 15:16:25 +02:00
griffi-gh 340a140ec9 Merge branch 'master' of https://github.com/griffi-gh/kubi 2023-05-28 23:46:20 +02:00
griffi-gh a9f17b878c use mold as linker on linux 2023-05-28 23:46:14 +02:00
griffi-gh fbe605041b oops, avx2 is not as widely available as i thought 2023-05-27 18:54:00 +02:00
griffi-gh 1625e9cce9 add note to readme about android 2023-05-21 12:41:09 +02:00
griffi-gh 31a959ba1a remove not used yet comment 2023-05-21 12:33:09 +02:00
griffi-gh 2dfc915d41 use ProgramCreationInput 2023-05-21 12:32:06 +02:00
griffi-gh 356455a15e remove geom shader macro 2023-05-21 12:29:01 +02:00
griffi-gh 1987354b83 fix warnings; remove unused imports 2023-05-21 12:24:45 +02:00
griffi-gh 33c306f47a statically link crt 2023-05-21 12:13:18 +02:00
griffi-gh 6718c56370 use libcubi as lib name 2023-05-21 12:08:29 +02:00
griffi-gh 061fe46f2a Merge branch 'master' of https://github.com/griffi-gh/kubi 2023-05-21 12:03:46 +02:00
griffi-gh cfba245e11 add ndk to deps, disable srgb by default 2023-05-21 12:03:41 +02:00
griffi-gh 072ffe6965
Add android build instructions 2023-05-21 06:53:47 +02:00
griffi-gh 6e61fd687a check at runtime 2023-05-21 04:54:02 +02:00
griffi-gh a3e5df0304 skip cursor lock on android 2023-05-21 04:24:54 +02:00
griffi-gh 3eddda0886 allow gilrs to fail (required for android support) 2023-05-21 04:23:10 +02:00
griffi-gh 8aaf64904b cross-platform assets 2023-05-21 04:17:28 +02:00
griffi-gh df3791af17 initial android support (fails on textures) 2023-05-21 03:14:04 +02:00
griffi-gh 50c25dac34 fix warnings 2023-05-21 02:16:34 +02:00
griffi-gh dc8229f929 use specific log version 2023-05-21 02:16:12 +02:00
griffi-gh 821cee541b add some android metadata 2023-05-20 16:43:00 +02:00
griffi-gh 0affc06e09 Fixed timestamps! (use them to fix networking) 2023-05-20 15:59:52 +02:00
griffi-gh 7ab7a36d85 oops 2023-05-20 15:17:33 +02:00
griffi-gh e8e3863d47 minor changes to netwrok code 2023-05-20 15:17:16 +02:00
griffi-gh 4dde1651ab reasonable total connections value 2023-05-20 14:59:48 +02:00
griffi-gh 14c66ee2eb add centered cube 2023-05-20 02:48:17 +02:00
griffi-gh 75c6d127b7 player position sync, refactor some stuff 2023-05-20 02:32:32 +02:00
griffi-gh 73940d61ff disable texture filtering experiment 2023-05-19 13:44:33 +02:00
griffi-gh 4bf2b350c6 wip 2023-05-19 13:22:32 +02:00
griffi-gh 03fbb774b5 add todo comments 2023-05-19 07:04:24 +02:00
griffi-gh 96a6693faa Big refactor + Early wip position sync /connect events in multiplayer 2023-05-19 07:02:20 +02:00
griffi-gh 3a50065cc5 Move ClientIdMap to shared, switch to with_capacity_and_hasher where possible 2023-05-19 04:35:16 +02:00
griffi-gh 56849a82cc test commit 2023-05-19 04:00:37 +02:00
griffi-gh ae4ed5d1f5 Oops, checkout already skips tag creation... 2023-05-19 03:51:32 +02:00
griffi-gh 3e34c19234 re-create tag manually to avoid force push 2023-05-19 03:45:50 +02:00
griffi-gh 8751ad99b7 remove commit field
should fix this:
```
Validation Failed: {"resource":"Release","code":"invalid","field":"target_commitish"}
```
2023-05-19 03:20:41 +02:00
griffi-gh 702cdf1ee6 should be working now 2023-05-19 03:16:56 +02:00
griffi-gh 6795f67481 switch to rickstaa/action-create-tag@v1 2023-05-19 03:00:31 +02:00
griffi-gh b832c9dbec hack: move tag before release 2023-05-19 02:44:15 +02:00
griffi-gh 7e08d2a4c1 fix release commit, prevent creating releases from prs 2023-05-19 02:34:14 +02:00
griffi-gh b038c65e74
Update README.md 2023-05-18 06:09:19 +03:00
griffi-gh 1856e10247 create zip files 2023-05-18 05:00:05 +02:00
griffi-gh 9a8ff135c7 add perms to build.yml 2023-05-18 04:48:02 +02:00
griffi-gh c9990eef6e nightly release workflow 2023-05-18 04:42:52 +02:00
griffi-gh 18d71d2edd move handshake into separate module 2023-05-18 04:34:33 +02:00
griffi-gh 282fd7bdcb always request latest gl, set min physical size to 640x480 2023-05-18 04:26:05 +02:00
griffi-gh 74bb3ee2eb player movement, player connect events 2023-05-18 03:58:40 +02:00
griffi-gh f920d27f3a fix missing value 2023-05-17 15:36:04 +02:00
griffi-gh 2e5dc34507 fix generate_visualizer_data 2023-05-17 13:11:26 +02:00
griffi-gh cef8bd9120 :p 2023-05-17 12:58:05 +02:00
griffi-gh bd6ebde832 fix dependabot alert 2023-05-17 12:53:00 +02:00
griffi-gh 5da64b33db fix artifact name 2023-05-17 12:32:55 +02:00
griffi-gh ba95b1820c update workflow 2023-05-17 12:30:14 +02:00
griffi-gh c1f3fe3db0 add Server.toml to artifact 2023-05-17 00:28:55 +02:00
griffi-gh 6bf051697d fix wrong dir name 2023-05-17 00:21:35 +02:00
griffi-gh 42f9bcc757 fix typo in workflow 2023-05-17 00:17:39 +02:00
griffi-gh 70effb9866 fix artifact structure 2023-05-17 00:16:49 +02:00
griffi-gh 4ccb354e24 fix workflow 2023-05-16 23:50:53 +02:00
griffi-gh 54061fcda5 install libudev-dev in build workflow 2023-05-16 23:42:05 +02:00
griffi-gh a87e069576 update workflow/cargo config 2023-05-16 23:33:17 +02:00
griffi-gh dc9f396908
Create build.yml 2023-05-16 22:45:11 +02:00
griffi-gh 141f3ae0bd fix mistake in readme 2023-05-16 13:44:45 +02:00
griffi-gh b6803895c5 fix builds with nightly, update readme 2023-05-16 13:42:55 +02:00
griffi-gh fdaa43d370 enable more nightly stuff if nightly flag is active 2023-05-16 12:47:07 +02:00
griffi-gh e81ebc75e3 Use git glium 2023-05-16 12:27:57 +02:00
griffi-gh fb68f8d12d add vsync option 2023-05-16 12:20:09 +02:00
griffi-gh ce8ad9098f add fullscreen mode, release shader compiler after loading shader prefabs 2023-05-16 12:08:56 +02:00
griffi-gh f15d2b1510 add pre_startup stage, use msaa settings in window init 2023-05-16 11:30:24 +02:00
griffi-gh 896d80e074 update deps, add options for some graphics-related settings, fix some warnings 2023-05-16 11:21:01 +02:00
griffi-gh 627acd40cc u 2023-03-15 02:32:26 +01:00
griffi-gh 0a590bab22 handle disconnect 2023-03-14 01:36:34 +01:00
griffi-gh e9977c3aa4 enable keepalive 2023-03-13 02:34:09 +01:00
griffi-gh a719427eb5 update 2023-03-13 02:24:37 +01:00
griffi-gh 944737c75c separate networking, fix warnings 2023-03-13 01:56:33 +01:00
griffi-gh 32155ff531 send block updates across network 2023-03-12 17:58:24 +01:00
griffi-gh e29ca7d202 remove irrelevant comment 2023-03-12 16:38:01 +01:00
griffi-gh 5009ce1cfb "fast decompress" turned out to be slower lol (stashed) 2023-03-11 17:19:03 +01:00
griffi-gh 23632c69b3 update notice 2023-03-09 18:26:57 +01:00
griffi-gh a5ed520b40 add queue block messages 2023-03-09 18:24:06 +01:00
griffi-gh d239a59fc4 fix client 2023-03-09 18:00:32 +01:00
griffi-gh 739e53c0a9 fix client decompression 2023-03-09 17:56:30 +01:00
griffi-gh 68bc848cbd asss 2023-03-09 04:29:21 +01:00
griffi-gh 7bcfc75dfe fix 2?? 2023-03-09 04:21:59 +01:00
griffi-gh 80c3f354f1 fix 2023-03-09 04:17:04 +01:00
griffi-gh 5e6e50124a Client decompression? hopefully 2023-03-09 04:15:59 +01:00
griffi-gh 615251f031 Move inj to networking 2023-03-09 04:09:17 +01:00
griffi-gh f6eef9457b world compression (server only for now) 2023-03-09 04:06:46 +01:00
griffi-gh 5a75e44beb Finish upgrading server to uflow 2023-03-09 03:30:37 +01:00
griffi-gh c4767e8be8 Place dirt under trees 2023-03-09 01:33:16 +01:00
griffi-gh 951d8e35e3 Fix river world generation 2023-03-09 01:27:17 +01:00
griffi-gh 74533db52c remove comments 2023-03-09 01:18:29 +01:00
griffi-gh b96d57326f temporary fix: make all workloads sequential :/ 2023-03-09 01:14:53 +01:00
griffi-gh 7bc81fc72a add generate_visualizer_data feature 2023-03-09 00:56:46 +01:00
griffi-gh 09b0a0160b update 2023-03-09 00:46:23 +01:00
griffi-gh ec6402633c
Update README.md 2023-03-09 02:33:35 +03:00
griffi-gh 8cd8cf35a2
Merge pull request #2 from griffi-gh/multiplayer
Multiplayer
2023-03-09 00:33:19 +01:00
griffi-gh 18587217fe
Merge pull request #5 from griffi-gh/use-uflow
Use uflow
2023-03-09 00:32:30 +01:00
griffi-gh 0cb066b02b Disable parallel feature by default, recalc camera matrix 2023-03-09 00:31:19 +01:00
griffi-gh 2189d114c7 wtf 2023-03-08 23:00:24 +01:00
griffi-gh 02d3224740 doesn't work? 2023-03-08 22:53:30 +01:00
griffi-gh 3103b3647d some big changes 2023-03-08 22:34:41 +01:00
griffi-gh 51f113d162 actually check the event type (facepalm) 2023-03-08 22:21:19 +01:00
griffi-gh 45c9b20ede add log 2023-03-08 21:13:50 +01:00
griffi-gh b20c6fe552 restore basic auth services 2023-03-08 21:10:48 +01:00
griffi-gh 56dd1a3bda upgrade some server stuff to uflow 2023-03-08 20:26:51 +01:00
griffi-gh 34a50c62b0 change server stuff 2023-03-08 17:27:53 +01:00
griffi-gh 765e360d44 disable lint 2023-03-08 01:47:46 +01:00
griffi-gh e5041aaa1a Client-side migrated 2023-03-08 01:31:38 +01:00
griffi-gh 39d4fd129a Remove kubi-udp 2023-03-07 23:36:04 +01:00
griffi-gh d7c3b6f6ba ew 2023-03-07 21:29:05 +01:00
griffi-gh ce454f0611 make packet size a const 2023-03-07 21:18:59 +01:00
griffi-gh a470543325 wip server chunks 2023-03-07 17:05:55 +01:00
griffi-gh f4bbfd3e8a fix 2023-03-07 04:56:51 +01:00
griffi-gh 639685e76b server 2023-03-07 04:50:06 +01:00
griffi-gh a46c5baab8 basic netw 2023-03-07 03:05:02 +01:00
griffi-gh 12fee77087 change comment 2023-03-07 02:28:16 +01:00
griffi-gh 16e7b7e588 Make the outer system seq 2023-03-07 01:59:54 +01:00
griffi-gh 71823c7ada wtf 2023-03-07 01:51:19 +01:00
griffi-gh 6933987d7b update glam to 0.23 2023-03-07 00:52:00 +01:00
griffi-gh 95e3de7228 create vscode settings.json 2023-03-07 00:46:12 +01:00
griffi-gh 3f6abb75fd Upgrade packages 2023-03-07 00:34:26 +01:00
griffi-gh ce687e8d36 changes to binary transparency meshing 2023-02-18 01:04:54 +01:00
griffi-gh fc783d52dc river generation 2023-02-18 00:58:31 +01:00
griffi-gh b7ec1d1165 fix order 2023-02-18 00:38:09 +01:00
griffi-gh 9268a293a8 improve worldgen 2023-02-18 00:37:17 +01:00
griffi-gh fd54eb7e4e cave generation (no scaling yet!) 2023-02-17 23:05:55 +01:00
griffi-gh 3448790daf caves 2023-02-17 22:44:01 +01:00
griffi-gh e2b8cbfdd5 Better tree gen 2023-02-17 22:25:53 +01:00
griffi-gh de4938ee6a soft updates 2023-02-17 22:03:45 +01:00
griffi-gh 4a84fec041 even better trees 2023-02-16 04:08:00 +01:00
griffi-gh f9b4628b49 better trees 2023-02-16 04:05:27 +01:00
griffi-gh f8732abb5d Placement queue, binary transparency, WIP trees 2023-02-16 03:54:57 +01:00
griffi-gh 626e9e654d rotate elevation noise 2023-02-16 02:57:04 +01:00
griffi-gh 7612ea8d8d Add planks 2023-02-16 02:44:21 +01:00
griffi-gh 287baf15ea add tall grass 2023-02-16 02:00:09 +01:00
griffi-gh 2c374c6e23 minor refactor 2023-02-15 23:32:06 +01:00
griffi-gh 00b3a90fd4 remove BlockDescriptorSource trait 2023-02-15 22:11:15 +01:00
griffi-gh f5a4e2a532 inline descriptor 2023-02-15 22:08:48 +01:00
griffi-gh c865835e8d move block to shared 2023-02-15 22:07:47 +01:00
griffi-gh 0de70ed1b7 reduce placement dst 2023-02-14 23:48:47 +01:00
griffi-gh 51ed3762cc disable selbox effect 2023-02-14 23:43:05 +01:00
griffi-gh 3b8eddcad1 world gen experimentation 2023-02-14 23:39:14 +01:00
griffi-gh e1cf0a1ed0 wip better world generation 2023-02-14 23:00:08 +01:00
griffi-gh 3209bf45d6 oops Cargo.lock was in .gitignore 2023-02-14 20:01:11 +01:00
griffi-gh 8a4339506e update shipyard to the lastest commit 2023-02-14 19:54:52 +01:00
griffi-gh a8142468a2 remove track(...) 2023-02-14 19:35:47 +01:00
griffi-gh 7b6d50abe5 Migrate to shipyard master 2023-02-14 19:31:20 +01:00
griffi-gh dada84e097 ad entity to shared 2023-02-14 19:09:27 +01:00
griffi-gh 740fa04910 wip 2023-02-14 04:52:11 +01:00
griffi-gh 4440b07bbe add todo 2023-02-14 04:31:17 +01:00
griffi-gh c284d5f7ed basic client auth 2023-02-14 04:27:27 +01:00
griffi-gh 1b1f6c6426 wip connect 2023-02-13 04:12:02 +01:00
griffi-gh 41a1a5d00b set publish to false 2023-02-13 03:01:56 +01:00
griffi-gh bccd3a8cfd implement broadcase and multicast functions 2023-02-13 02:57:09 +01:00
griffi-gh 95c681d0fc kick inactive clients 2023-02-13 02:30:33 +01:00
griffi-gh 32963044f3 add log to disconnect_inner 2023-02-13 02:03:09 +01:00
griffi-gh f19683c1b1 remove pub from functions 2023-02-13 02:01:25 +01:00
griffi-gh cc2175ec3e use AllStoragesView instead of AllStoragesViewMut 2023-02-13 01:57:30 +01:00
griffi-gh b3990e64c9 change default protocol id 2023-02-13 01:56:14 +01:00
griffi-gh 6b9243a556 protocol id 2023-02-13 01:53:55 +01:00
griffi-gh a5b35ffa7d rename to connect_client_if_needed 2023-02-12 22:42:39 +01:00
griffi-gh 455b4580de Disconnect only in multiplayer 2023-02-12 22:41:27 +01:00
griffi-gh bda8e7b94c disconnect on exit 2023-02-12 22:38:51 +01:00
griffi-gh 6b55688dbd change default server port 2023-02-12 22:30:03 +01:00
griffi-gh 80ef55c8b3 better error handling on client 2023-02-12 22:28:44 +01:00
griffi-gh 147fb03566 fix readme link 2023-02-12 22:19:53 +01:00
griffi-gh 45d3bb7e98 update readme 2023-02-12 22:18:11 +01:00
griffi-gh b8447bc121 what the fuck 2023-02-12 22:15:09 +01:00
griffi-gh bf7fc475e2 fix camera updates 2023-02-12 21:50:24 +01:00
griffi-gh eabddf3019 inline client functions 2023-02-12 21:42:53 +01:00
griffi-gh d28069e9e6 replace .into_workload().run_if() with .run_if() 2023-02-12 21:40:42 +01:00
griffi-gh e0872b331f ignore io errors on server 2023-02-12 21:31:21 +01:00
griffi-gh 8758938e7e client networking calls 2023-02-12 20:37:06 +01:00
griffi-gh bcd3066c95 todo 2023-02-12 18:52:17 +01:00
griffi-gh 05444bd4b8 basic networking 2023-02-12 04:04:48 +01:00
griffi-gh 6c27d63bdf it passes 2023-02-12 03:38:18 +01:00
griffi-gh 70aaba01b0 fix 2023-02-12 03:22:42 +01:00
griffi-gh 907e3164d7 oops 2023-02-12 03:15:31 +01:00
griffi-gh 7ecc4d5c24 change things 2023-02-12 02:56:50 +01:00
griffi-gh 00f90156bd test 2023-02-12 02:33:48 +01:00
griffi-gh 22054c6143 client server changes and integration test (fails) 2023-02-12 02:15:00 +01:00
griffi-gh af62a3749b some changes to server and shared crates 2023-02-12 01:32:59 +01:00
griffi-gh f76fa8c70e server 2023-02-12 00:37:24 +01:00
griffi-gh 5307ba0c9f wip 2023-02-10 22:11:18 +01:00
griffi-gh 6b65497263 server 2023-02-10 22:05:10 +01:00
griffi-gh 3f47be435d separate logging into a library 2023-02-10 20:44:34 +01:00
griffi-gh 055e6c3600 ecs server 2023-02-10 20:36:58 +01:00
griffi-gh 28a82cf69e server 2023-02-10 20:26:03 +01:00
griffi-gh aada43e8dc is_changing_state 2023-02-10 03:03:00 +01:00
griffi-gh 8e9eeb1f30 typo 2023-02-10 03:00:08 +01:00
griffi-gh 35886d9bed log after load finish 2023-02-10 02:46:34 +01:00
griffi-gh c06869c5cb slow down chunk ops while loading 2023-02-10 02:44:17 +01:00
griffi-gh dbb92232bc loading screen 2023-02-10 02:36:11 +01:00
griffi-gh 2f23f39604 uwu 2023-02-10 01:47:37 +01:00
griffi-gh 4e357af959 use windows subsystem attach console on release builds 2023-02-10 01:20:54 +01:00
griffi-gh 6354d8bab4 reorder 2023-02-10 00:57:57 +01:00
griffi-gh 06e94ccb46 assert renderer in init stage 2023-02-10 00:56:03 +01:00
griffi-gh 54004ee5de move assert_renderer to 2023-02-10 00:49:05 +01:00
griffi-gh 2fc8db6740 gui changes, use glsl 3.0 es 2023-02-10 00:46:20 +01:00
griffi-gh 7c8a0fb66b progressbar shader fix 2023-02-10 00:08:32 +01:00
griffi-gh 50ef5c2d1e ui transform works 2023-02-09 04:16:02 +01:00
griffi-gh e75693926a why doesnt it work 2023-02-09 04:11:15 +01:00
griffi-gh da4a686100 fix default colors and color_hex function 2023-02-09 03:34:49 +01:00
griffi-gh 6b34352c55 fix color-related code 2023-02-09 03:33:41 +01:00
griffi-gh 8ed0765c9f gui and progressbars 2023-02-09 03:31:36 +01:00
griffi-gh 9f2e47df8c shaders for progressbar 2023-02-08 03:37:58 +01:00
griffi-gh a6a728ba10 gui files 2023-02-08 03:10:35 +01:00
griffi-gh fb8aa9d528 fix 2023-02-08 03:09:04 +01:00
griffi-gh 1ae34def21 changed a lot of stuff, too lazy to write 2023-02-08 03:06:06 +01:00
griffi-gh f88e2733f3 wip 2023-02-08 02:29:29 +01:00
griffi-gh e96ae90b1d serveeeeer 2023-02-08 01:55:50 +01:00
griffi-gh 1df8c89330 wip things 2023-02-06 21:43:22 +01:00
griffi-gh aa4b552e0f server recv 2023-02-06 20:54:30 +01:00
griffi-gh dc06305956 undo 2023-02-06 20:18:16 +01:00
griffi-gh 44088f76e5 serializable trait 2023-02-06 19:30:26 +01:00
griffi-gh d5efff860c wip 2023-02-06 19:19:02 +01:00
griffi-gh 4cece2a876 wip server 2023-02-06 03:48:43 +01:00
griffi-gh b940a9d9d6 disable broadcasting 2023-02-06 02:41:34 +01:00
griffi-gh 324dc5d43e ... 2023-02-06 02:40:45 +01:00
griffi-gh 102bc101e0 update 2023-02-06 02:15:19 +01:00
griffi-gh 01b82b1094 cobblestone 2023-02-05 01:43:47 +01:00
griffi-gh df8640718d don't count discarded ops 2023-02-05 01:22:43 +01:00
griffi-gh 34fa47acbe Update neighbors 2023-02-05 01:18:30 +01:00
griffi-gh 319cffbb4e block queue works 2023-02-05 00:58:25 +01:00
griffi-gh 00f54a2f5f wip block queue 2023-02-05 00:42:37 +01:00
griffi-gh 53fc7dcd27 comment 2023-02-04 22:27:19 +01:00
griffi-gh c1f1ec028f add kick reason 2023-02-04 22:20:19 +01:00
griffi-gh eff0b50546 . 2023-02-04 02:47:09 +01:00
griffi-gh 4ac6250c04 client is almost finished... 2023-02-04 02:46:48 +01:00
griffi-gh 1ca847b4fc use anyhow::{...} 2023-02-03 19:39:22 +01:00
griffi-gh a1ff9e1d30 id packets 2023-02-03 19:36:52 +01:00
griffi-gh 19bc67a78f disconnect reason 2023-02-03 19:29:38 +01:00
griffi-gh 127460347a client wip 2023-02-02 02:13:32 +01:00
griffi-gh 1e1ab9d40e wip 2023-02-02 01:54:12 +01:00
griffi-gh 99b2848775 client 2023-02-02 01:40:48 +01:00
griffi-gh 63b4091ddd only normalize movement if len >= 1 2023-02-01 23:46:40 +01:00
griffi-gh 3593fcd4d5 fix panic 2023-02-01 23:34:27 +01:00
griffi-gh 8ff2a828ad x 2023-02-01 03:25:39 +01:00
griffi-gh 65ecf0de62 no drop check is needed 2023-02-01 03:25:12 +01:00
griffi-gh e6ec24a55c client 2023-02-01 03:24:06 +01:00
griffi-gh 728c468a70 create net lib 2023-02-01 03:16:23 +01:00
griffi-gh a7a20093d6 move config 2023-01-31 03:12:23 +01:00
griffi-gh 1c0409a9cf use bincode derive instead of serde due to potential issues 2023-01-31 03:06:30 +01:00
griffi-gh 5c3062d13f c 2023-01-31 02:51:54 +01:00
griffi-gh 6f45a0ee77 upd 2023-01-31 02:23:44 +01:00
griffi-gh 1f5b5853b2 replace rkyv with serde and bincode
change to serde
2023-01-31 01:29:51 +01:00
griffi-gh b0642028d1 CheckBytes 2023-01-30 20:23:00 +01:00
griffi-gh ab7c181345 fix excessive logging 2023-01-30 04:32:33 +01:00
griffi-gh 1f8c6ea223 maybe this will work? 2023-01-30 04:23:28 +01:00
griffi-gh 56d85252b0 use xinput, invert y 2023-01-30 04:20:13 +01:00
griffi-gh 3af40c28b8 fix stick input 2023-01-30 04:18:29 +01:00
griffi-gh acbb159606 WIP controller support 2023-01-30 04:14:53 +01:00
griffi-gh f294e0e159 use rkyv 2023-01-30 03:42:58 +01:00
griffi-gh 5b93fa7639 move more things 2023-01-30 03:23:39 +01:00
griffi-gh db6c50ad8d separate blocks into common 2023-01-30 02:45:35 +01:00
griffi-gh 59b91bc23d add kubi-shared as dep 2023-01-30 02:37:11 +01:00
griffi-gh c2b446ac7b move assets folder 2023-01-30 01:50:14 +01:00
griffi-gh 478930e37c restructure 2023-01-30 01:46:22 +01:00
griffi-gh af2a01fad7 empty commit to create pull request 2023-01-30 01:28:09 +01:00
griffi-gh 1d55a80404 add chunk borders 2023-01-30 01:24:53 +01:00
griffi-gh 852fee3607 minor refactor, start working on chunk border rendering 2023-01-30 01:04:13 +01:00
griffi-gh 0b4363ef88 move box to primitives 2023-01-30 00:39:31 +01:00
griffi-gh e00af1cefb add rkyv 2023-01-30 00:29:57 +01:00
griffi-gh ec42143154 decrease it a bit 2023-01-30 00:27:47 +01:00
griffi-gh 8d72185a4e Increase "epsilon" value for max pitch 2023-01-30 00:22:16 +01:00
griffi-gh f5a4bd9603 player move events 2023-01-30 00:21:03 +01:00
griffi-gh 71154b07bc normalize 2023-01-29 23:19:24 +01:00
griffi-gh 889c193a05 fix bug in neighbor code 2023-01-29 23:11:36 +01:00
griffi-gh 54449d5457 change opt levels 2023-01-29 22:36:49 +01:00
griffi-gh bace24b13c Always unmark dirty 2023-01-29 02:16:26 +01:00
griffi-gh 419e08aa64 exit on esc and cursor lock 2023-01-29 02:15:12 +01:00
griffi-gh a74296956e Minor refactor, add cursor lock module 2023-01-29 01:58:52 +01:00
griffi-gh 9d2c00b204 Remove empty crosshair module 2023-01-29 01:45:38 +01:00
griffi-gh d50d61100b optimize deps 2023-01-29 01:37:13 +01:00
griffi-gh e11302c4c0 fix a memory leak and most warnings 2023-01-29 01:28:00 +01:00
griffi-gh 04e4b246fe minor refactor 2023-01-29 01:21:28 +01:00
griffi-gh be8a0a4816
Merge pull request #1 from griffi-gh/ecs-rewrite
Merge Ecs rewrite
2023-01-28 23:45:18 +01:00
griffi-gh 42a822494c
Merge branch 'master' into ecs-rewrite 2023-01-28 23:44:45 +01:00
griffi-gh 9bc51d709c add readme 2023-01-20 03:24:25 +01:00
153 changed files with 11893 additions and 1730 deletions

11
.cargo/config.toml Normal file
View file

@ -0,0 +1,11 @@
[target.x86_64-pc-windows-msvc]
rustflags = ["-Ctarget-feature=+crt-static"]
[target.i686-pc-windows-msvc]
rustflags = ["-Ctarget-feature=+crt-static"]
[target.i586-pc-windows-msvc]
rustflags = ["-Ctarget-feature=+crt-static"]
[target.'cfg(target_arch = "x86_64")']
rustflags = "-Ctarget-feature=+sse,+sse2,+avx"

115
.github/workflows/build.yml vendored Normal file
View file

@ -0,0 +1,115 @@
name: Build
on:
push:
branches: [ "master" ]
tags:
- 'v*'
pull_request:
branches: [ "master" ]
env:
CARGO_TERM_COLOR: always
CARGO_TERM_PROGRESS_WHEN: never
jobs:
build:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v3
- uses: rui314/setup-mold@v1
if: runner.os == 'Linux'
- uses: awalsh128/cache-apt-pkgs-action@v1.3.0
if: runner.os == 'Linux'
with:
packages: libudev-dev
version: 1.0
- uses: Swatinem/rust-cache@v2.3.0
- name: Build
run: cargo build
--release
--package kubi
--bin kubi
--package kubi-server
--bin kubi-server
- name: Create artifact
shell: bash
run: |
mkdir artifact;
cp ./target/release/kubi ./artifact;
cp ./target/release/kubi-server ./artifact;
cp -r ./assets ./artifact;
cp ./Server.toml ./artifact;
- uses: actions/upload-artifact@v3
with:
name: ${{ runner.os }}_${{ runner.arch }}
path: ./artifact/*
if-no-files-found: error
publish-nightly:
needs: build
permissions: write-all
if: (github.event_name == 'push') && (github.ref == 'refs/heads/master')
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/download-artifact@v3
with:
path: ./artifacts
- name: Delete existing tag
continue-on-error: true
run: |
git push --delete origin refs/tags/nightly;
- name: Create nightly tag
continue-on-error: true
run: |
git tag nightly;
git push origin nightly;
- name: Create zip files
run: |
cd ./artifacts;
for folder in */; do
zip -r "${folder%/}.zip" "$folder"*;
rm -rf "$folder";
done;
cd ..;
- uses: ncipollo/release-action@v1
with:
name: nightly
tag: nightly
allowUpdates: true
removeArtifacts: true
replacesArtifacts: true
prerelease: true
generateReleaseNotes: true
updateOnlyUnreleased: true
artifacts: ./artifacts/*
publish-release:
needs: build
permissions: write-all
if: (github.event_name == 'push') && startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v3
with:
path: ./artifacts
- name: Create zip files
run: |
cd ./artifacts;
for folder in */; do
zip -r "${folder%/}.zip" "$folder"*;
rm -rf "$folder";
done;
cd ..;
- uses: rlespinasse/github-slug-action@v4.4.1
- uses: ncipollo/release-action@v1
with:
tag: ${{ env.GITHUB_REF_SLUG_URL }}
generateReleaseNotes: true
makeLatest: true
artifacts: ./artifacts/*

53
.gitignore vendored
View file

@ -1,17 +1,36 @@
# Generated by Cargo
# will have compiled files and executables
debug/
target/
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
Cargo.lock
# These are backup files generated by rustfmt
**/*.rs.bk
# MSVC Windows builds of rustc generate these, which store debugging information
*.pdb
#old source
_src
# Generated by Cargo
# will have compiled files and executables
debug/
target/
# These are backup files generated by rustfmt
**/*.rs.bk
# MSVC Windows builds of rustc generate these, which store debugging information
*.pdb
#old source
_src
_visualizer.json
*.kubi
/*_log*.txt
/*.log
# make sure build artifacts and binaries are not committed
*.d
*.pdb
*.exe
*.dll
*.so
*.dylib
*.rlib
#but keep the dxcompiler.dll/dxil.dll
!dxcompiler.dll
!dxil.dll
# blender backup files
*.blend1

BIN
.readme/game.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 MiB

BIN
.readme/touch_controls.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

9
.vscode/settings.json vendored Normal file
View file

@ -0,0 +1,9 @@
{
"editor.tabSize": 2,
"rust-analyzer.diagnostics.disabled": [
//rust-analyzer issue #14269,
"unresolved-method",
"unresolved-import",
"unresolved-field"
]
}

3272
Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,20 +1,47 @@
[package]
name = "kubi"
version = "0.1.0"
edition = "2021"
[workspace]
members = [
"kubi",
"kubi-server",
"kubi-shared",
"kubi-logging",
]
default-members = ["kubi"]
resolver = "2"
[dependencies]
glium = "0.32"
image = { version = "0.24", default_features = false, features = ["png"] }
log = "0.4"
env_logger = "0.10"
strum = { version = "0.24", features = ["derive"] }
glam = { version = "0.22", features = ["debug-glam-assert", "mint", "fast-math"] }
hashbrown = "0.13"
rayon = "1.6"
shipyard = { version = "0.6", features = ["thread_local"] }
nohash-hasher = "0.2.0"
anyhow = "1.0"
flume = "0.10"
#once_cell = "1.17"
bracket-noise = "0.8"
[profile.release-with-debug]
inherits = "release"
debug = true
[profile.dev]
opt-level = 1
[profile.dev.package."*"]
opt-level = 1
[profile.dev.package.uflow]
opt-level = 3
[profile.dev.package.wgpu]
opt-level = 3
[profile.dev.package.wgpu-core]
opt-level = 3
[profile.dev.package.wgpu-hal]
opt-level = 3
[profile.dev.package.fastnoise-lite]
opt-level = 3
[profile.dev.package.rayon]
opt-level = 3
#this is cursed as fuck
#enabling debug assertions here causes the game to abort
[profile.dev.package.android-activity]
debug-assertions = false
# [patch.'https://github.com/griffi-gh/hui']
# hui = { path = "X:/Projects/hui/hui" }
# hui-winit = { path = "X:/Projects/hui/hui-winit" }
# hui-wgpu = { path = "X:/Projects/hui/hui-wgpu" }

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 griffi-gh
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

124
README.md
View file

@ -1,3 +1,121 @@
<h1 align="center">Kubi</h1>
work in progress
<h6 align="right"><i>~ uwu</i></h6>
<h1 align="center">Kubi</h1>
<p align="center">
Voxel engine written in Rust
</p>
<div align="center">
<img src=".readme/game.gif" width="512">
</div>
<h2>features</h2>
<p>
<ul>
<li>multithreaded procedural world generation</li>
<li>procedural structures and block queue</li>
<li>multithreaded mesh generation</li>
<li>cubic chunks (32x32x32)</li>
<li>low-level OpenGL renderer, targetting OpenGL ES 3.0</li>
<li>frustum culling</li>
<li>multiplayer networking</li>
<li>immediate ui system <sup><code>[1]</code></sup></li>
<li>cross platform: windows, linux, osx, android <sup><code>[2]</code></sup></li>
<li>universal input system: supports keyboard, mouse, gamepad and touch input <sup><code>[3]</code></sup></li>
<li>support for semi-transparet blocks<sup><code>[4]</code></sup></li>
</ul>
<h6>
<code>[1]</code> - developed out-of-tree <a href="https://github.com/griffi-gh/hUI">here</a> since 2024<br>
<code>[2]</code> - android support is experimental<br>
<code>[3]</code> - mouse and gamepad input is not supported on android<br>
<code>[4]</code> - work in progress, may cause issues<br>
</h6>
</p>
<h2>download</h2>
<a href="https://github.com/griffi-gh/kubi/releases/tag/nightly">Latest nightly release</a>
<h2>build for windows/linux</h2>
```bash
cargo build -p kubi
cargo run -p kubi
#or, build with release mode optimizations:
cargo run -p kubi --release
```
<h2>build for android</h2>
please note that android support is highly experimental!\
gamepad, mouse input is currently borked, as well as srgb, which leads to dark textures.
prerequisites: Android SDK, NDK, command line tools, platform-tools, latest JDK\
(make sure that your `PATH`, `ANDROID_HOME` and `ANDROID_NDK_ROOT` variables are configured properly)
**Setup:**
latest unpublished (git) version of cargo-apk is required
```bash
cargo install --git https://github.com/rust-mobile/cargo-apk cargo-apk
rustup target add aarch64-linux-android
```
**Build:**
`--no-default-features` is required for keyboard input!\
(`prefer-raw-events` feature *must* be disabled on android)\
Mouse input is not implemented, touch only!
```bash
cargo apk build -p kubi --lib --no-default-features
```
**Run on device (using adb):**
```bash
cargo apk run -p kubi --lib --no-default-features
```
<h2>touch controls</h2>
<img src=".readme/touch_controls.png" alt="touch control scheme" width="300">
- Left side: **Movement**
- Rigth side: **Camera controls**
- Bottom right corner:
- **B** (e.g. place blocks)
- **A** (e.g. break, attack)
<h2>mutiplayer</h2>
to join a multiplayer server, just pass the ip address as the first argument
```sh
cargo run -p kubi -- 127.0.0.1:1234
```
<h2>server configuration</h2>
```toml
[server]
address = "0.0.0.0:12345" # ip address to bind to
max_clients = 32 # max amount of connected clients
timeout_ms = 10000 # client timeout in ms
[world]
seed = 0xfeb_face_dead_cafe # worldgen seed to use
[query]
name = "Kubi Server" # server name
```
<h2>"In-house" libraries</h2>
- [`hui`, `hui-glium`, `hui-winit`](https://github.com/griffi-gh/hui): semi-imm.mode backend-agnostic ui system\
- [`kubi-logging`](kubi-logging) fancy custom formatter for `env-logger`
deprecated:
- ~~`kubi-udp`~~ eventually got replaced by `uflow` (https://github.com/lowquark/uflow) in #5
- ~~`kubi-pool`~~ decided there's no need to replace rayon for now
<h6 align="right"><i>~ uwu</i></h6>

11
Server.toml Normal file
View file

@ -0,0 +1,11 @@
[server]
address = "0.0.0.0:12345"
max_clients = 32
timeout_ms = 10000
[world]
seed = 0xfeb_face_dead_cafe
preheat_radius = 8
[query]
name = "Kubi Server"

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 578 B

BIN
assets/blocks/planks.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 247 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 210 B

BIN
assets/blocks/water.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 341 B

BIN
assets/fonts/Crisp.ttf Normal file

Binary file not shown.

64
assets/playermodel1.obj Normal file
View file

@ -0,0 +1,64 @@
# Blender 4.0.0 Beta
# www.blender.org
o Cube
v -0.500000 0.708333 -0.666667
v -0.500000 -0.291667 -0.666667
v 0.500000 0.708333 -0.666667
v 0.500000 -0.291667 -0.666667
v -0.500000 0.708333 0.333333
v -0.500000 -0.291667 0.333333
v 0.500000 0.708333 0.333333
v 0.500000 -0.291667 0.333333
v -0.500000 0.958333 -0.666667
v 0.500000 0.958333 -0.666667
v -0.166667 0.708333 -0.666667
v 0.166667 0.708333 -0.666667
vn -0.0000 1.0000 -0.0000
vn 1.0000 -0.0000 -0.0000
vn -0.0000 -0.0000 1.0000
vn -0.0000 -1.0000 -0.0000
vn -0.0000 -0.0000 -1.0000
vn -1.0000 -0.0000 -0.0000
vt 0.204555 0.291387
vt 0.043204 0.462923
vt 0.043204 0.291387
vt 0.259623 0.207472
vt 0.024467 0.457472
vt 0.024467 0.207472
vt 0.177715 0.183914
vt 0.010921 0.538561
vt 0.010921 0.183914
vt 0.246583 0.218979
vt 0.011426 0.468979
vt 0.011426 0.218979
vt 0.896961 0.811182
vt 0.168955 0.037222
vt 0.896961 0.037221
vt 0.177715 0.538561
vt 0.010921 0.361238
vt 0.168955 0.811182
vt 0.411624 0.811182
vt 0.204555 0.462923
vt 0.259623 0.457472
vt 0.246583 0.468979
vt 0.177715 0.361238
vt 0.896961 0.990308
vt 0.654292 0.811182
vt 0.168955 0.990308
s 0
f 5/1/1 3/2/1 1/3/1
f 3/4/2 8/5/2 4/6/2
f 7/7/3 6/8/3 8/9/3
f 2/10/4 8/11/4 6/12/4
f 1/13/5 4/14/5 2/15/5
f 5/16/6 2/17/6 6/8/6
f 3/18/3 1/13/3 12/19/3
f 5/1/1 7/20/1 3/2/1
f 3/4/2 7/21/2 8/5/2
f 7/7/3 5/16/3 6/8/3
f 2/10/4 4/22/4 8/11/4
f 1/13/5 3/18/5 4/14/5
f 5/16/6 1/23/6 2/17/6
f 1/13/5 9/24/5 11/25/5
f 12/19/5 10/26/5 3/18/5
f 1/13/5 11/25/5 12/19/5

BIN
assets/playermodel1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 KiB

View file

@ -1 +0,0 @@
sorry no crabs here

14
kubi-logging/Cargo.toml Normal file
View file

@ -0,0 +1,14 @@
[package]
name = "kubi-logging"
version = "0.1.0"
edition = "2021"
publish = false
[dependencies]
log = "0.4"
[target.'cfg(not(target_os = "android"))'.dependencies]
env_logger = "0.10"
[target.'cfg(target_os = "android")'.dependencies]
android_logger = "0.13"

64
kubi-logging/src/lib.rs Normal file
View file

@ -0,0 +1,64 @@
//! Custom env_logger options and styling
/// Custom env_logger options and styling
#[inline]
#[cfg(not(target_os = "android"))]
pub fn init() {
use log::Level;
use std::io::Write;
use env_logger::{fmt::Color, Builder, Env};
let env = Env::default()
.filter_or("RUST_LOG", "trace,gilrs=warn,rusty_xinput=warn,wgpu=warn,wgpu_core=warn,wgpu_hal=warn,hui=info,hui-winit=info,hui-glium=info,hui-wgpu=info,naga=warn");
Builder::from_env(env)
.format(|buf, record| {
let mut level_style = buf.style();
level_style.set_color(match record.level() {
Level::Error => Color::Red,
Level::Warn => Color::Yellow,
Level::Debug | Level::Trace => Color::Cyan,
_ => Color::Blue
}).set_bold(true);
let mut bold_style = buf.style();
bold_style.set_bold(true);
let mut location_style = buf.style();
location_style.set_bold(true);
location_style.set_dimmed(true);
let mut location_line_style = buf.style();
location_line_style.set_dimmed(true);
let text = format!("{}", record.args());
writeln!(
buf,
"{} {:<50}\t{}{}{}{}",
level_style.value(match record.level() {
Level::Error => "[e]",
Level::Warn => "[w]",
Level::Info => "[i]",
Level::Debug => "[d]",
Level::Trace => "[t]",
}),
text,
bold_style.value((text.len() > 50).then_some("\n ╰─ ").unwrap_or_default()),
location_style.value(record.target()),
location_line_style.value(" :"),
location_line_style.value(record.line().unwrap_or(0))
)
})
.init();
}
/// Custom env_logger options and styling
#[inline]
#[cfg(target_os = "android")]
pub fn init() {
use log::LevelFilter;
use android_logger::Config;
android_logger::init_once(
Config::default().with_max_level(LevelFilter::Trace),
);
}

29
kubi-server/Cargo.toml Normal file
View file

@ -0,0 +1,29 @@
[package]
name = "kubi-server"
version = "0.0.0"
edition = "2021"
publish = false
[dependencies]
kubi-shared = { path = "../kubi-shared" }
kubi-logging = { path = "../kubi-logging" }
log = "0.4"
shipyard = { git = "https://github.com/leudz/shipyard", rev = "aacf3b1df5", default-features = false, features = ["std", "proc", "thread_local"] }
serde = { version = "1.0", default-features = false, features = ["alloc", "derive"] }
toml = "0.8"
glam = { version = "0.27", features = ["debug-glam-assert", "fast-math"] }
hashbrown = "0.14"
nohash-hasher = "0.2"
anyhow = "1.0"
rayon = "1.10"
flume = "0.11"
rand = "0.8"
uflow = "0.7"
postcard = { version = "1.0", features = ["alloc"] }
lz4_flex = { version = "0.11", default-features = false, features = ["std"] }
[features]
default = ["parallel"]
parallel = ["shipyard/parallel"]
safe_lz4 = ["lz4_flex/safe-encode", "lz4_flex/safe-decode"]
nightly = ["hashbrown/nightly", "rand/nightly", "rand/simd_support", "glam/core-simd", "kubi-shared/nightly"]

184
kubi-server/src/auth.rs Normal file
View file

@ -0,0 +1,184 @@
use glam::{Vec3, Mat4, vec3};
use shipyard::{UniqueView, NonSendSync, EntitiesViewMut, ViewMut, UniqueViewMut, AllStoragesView, IntoIter};
use uflow::{server::Event as ServerEvent, SendMode};
use kubi_shared::{
networking::{
messages::{
ClientToServerMessage,
ServerToClientMessage,
InitData,
ClientInitData,
ClientToServerMessageType,
},
client::{Client, ClientId, Username},
channels::Channel,
},
player::{Player, PLAYER_HEALTH},
transform::Transform, entity::{Entity, Health}
};
use crate::{
config::ConfigTable,
server::{ServerEvents, UdpServer, IsMessageOfType},
client::{ClientAddress, ClientAddressMap}
};
pub use kubi_shared::networking::client::ClientIdMap;
pub fn authenticate_players(
storages: AllStoragesView,
) {
let mut client_entity_map = storages.borrow::<UniqueViewMut<ClientIdMap>>().unwrap();
let mut client_addr_map = storages.borrow::<UniqueViewMut<ClientAddressMap>>().unwrap();
let server = storages.borrow::<NonSendSync<UniqueView<UdpServer>>>().unwrap();
let events = storages.borrow::<UniqueView<ServerEvents>>().unwrap();
let config = storages.borrow::<UniqueView<ConfigTable>>().unwrap();
for event in &events.0 {
// NOT using `check_message_auth` here because the user is not authed yet!
let ServerEvent::Receive(client_addr, data) = event else{
continue
};
if !event.is_message_of_type::<{ClientToServerMessageType::ClientHello as u8}>() {
continue
}
let Some(client) = server.0.client(client_addr) else {
log::error!("Client doesn't exist");
continue
};
let Ok(parsed_message) = postcard::from_bytes(data) else {
log::error!("Malformed message");
continue
};
let ClientToServerMessage::ClientHello { username, password } = parsed_message else {
unreachable!()
};
log::info!("ClientHello; username={} password={:?}", username, password);
// Handle password auth
if let Some(server_password) = &config.server.password {
if let Some(user_password) = &password {
if server_password != user_password {
client.borrow_mut().send(
postcard::to_allocvec(&ServerToClientMessage::ServerFuckOff {
reason: "Incorrect password".into()
}).unwrap().into_boxed_slice(),
Channel::Auth as usize,
SendMode::Reliable
);
continue
}
} else {
client.borrow_mut().send(
postcard::to_allocvec(&ServerToClientMessage::ServerFuckOff {
reason: "This server is password protected".into()
}).unwrap().into_boxed_slice(),
Channel::Auth as usize,
SendMode::Reliable
);
continue
}
}
//Find the player ID
let max_clients = config.server.max_clients as ClientId;
let Some(client_id) = (0..max_clients).find(|id| {
!client_entity_map.0.contains_key(id)
}) else {
client.borrow_mut().send(
postcard::to_allocvec(&ServerToClientMessage::ServerFuckOff {
reason: "Can't find a free spot for you!".into()
}).unwrap().into_boxed_slice(),
Channel::Auth as usize,
SendMode::Reliable
);
continue
};
//Spawn the user
let entity_id = {
storages.borrow::<EntitiesViewMut>().unwrap().add_entity((
&mut storages.borrow::<ViewMut<Entity>>().unwrap(),
&mut storages.borrow::<ViewMut<Player>>().unwrap(),
&mut storages.borrow::<ViewMut<Health>>().unwrap(),
&mut storages.borrow::<ViewMut<Client>>().unwrap(),
&mut storages.borrow::<ViewMut<ClientAddress>>().unwrap(),
&mut storages.borrow::<ViewMut<Transform>>().unwrap(),
&mut storages.borrow::<ViewMut<Username>>().unwrap(),
), (
Entity,
Player,
Health::new(PLAYER_HEALTH),
Client(client_id),
ClientAddress(*client_addr),
Transform(Mat4::from_translation(vec3(0., 60., 0.))),
Username(username.clone()),
))
};
//Add the user to the ClientIdMap and ClientAddressMap
client_entity_map.0.insert(client_id, entity_id);
client_addr_map.0.insert(*client_addr, entity_id);
//Create init data
let init_data = {
let mut user = None;
let mut users = Vec::with_capacity(client_entity_map.0.len() - 1);
for (client, username, transform, &health) in (
&storages.borrow::<ViewMut<Client>>().unwrap(),
&storages.borrow::<ViewMut<Username>>().unwrap(),
&storages.borrow::<ViewMut<Transform>>().unwrap(),
&storages.borrow::<ViewMut<Health>>().unwrap(),
).iter() {
let (_, direction, position) = transform.0.to_scale_rotation_translation();
let idata = ClientInitData {
client_id: client.0,
username: username.0.clone(),
position,
velocity: Vec3::ZERO,
direction,
health,
};
if client_id == client.0 {
user = Some(idata);
} else {
users.push(idata);
}
}
InitData {
user: user.unwrap(),
users
}
};
//Announce new player to other clients
{
let message = &ServerToClientMessage::PlayerConnected {
init: init_data.user.clone()
};
for (other_client_addr, _) in client_addr_map.0.iter() {
//TODO: ONLY JOINED CLIENTS HERE! USE URL AS REFERENCE
// https://github.com/griffi-gh/kubi/blob/96a6693faa14580fca560f4a64f0e88e595a8ca0/kubi-server/src/world.rs#L144
let Some(other_client) = server.0.client(other_client_addr) else {
log::error!("Other client doesn't exist");
continue
};
other_client.borrow_mut().send(
postcard::to_allocvec(&message).unwrap().into_boxed_slice(),
Channel::SysEvt as usize,
SendMode::Reliable
);
}
}
//Approve the user and send init data
client.borrow_mut().send(
postcard::to_allocvec(&ServerToClientMessage::ServerHello {
init: init_data
}).unwrap().into_boxed_slice(),
Channel::Auth as usize,
SendMode::Reliable
);
log::info!("{username}({client_id}) joined the game!")
}
}

135
kubi-server/src/client.rs Normal file
View file

@ -0,0 +1,135 @@
use glam::Mat4;
use shipyard::{AllStoragesView, AllStoragesViewMut, Component, EntityId, Get, IntoIter, NonSendSync, Unique, UniqueView, UniqueViewMut, View, ViewMut};
use hashbrown::HashMap;
use uflow::{server::Event, SendMode};
use std::net::SocketAddr;
use kubi_shared::{
networking::{
client::{ClientIdMap, Client},
messages::{ClientToServerMessage, ServerToClientMessage, ClientToServerMessageType},
channels::Channel
},
transform::Transform
};
use crate::{
server::{ServerEvents, UdpServer},
util::check_message_auth, world::ChunkManager
};
#[derive(Component, Clone, Copy)]
pub struct ClientAddress(pub SocketAddr);
#[derive(Unique, Default)]
pub struct ClientAddressMap(pub HashMap<SocketAddr, EntityId>);
impl ClientAddressMap {
pub fn new() -> Self { Self::default() }
}
pub fn init_client_maps(
storages: AllStoragesView
) {
storages.add_unique(ClientIdMap::new());
storages.add_unique(ClientAddressMap::new());
}
pub fn sync_client_positions(
server: NonSendSync<UniqueView<UdpServer>>,
events: UniqueView<ServerEvents>,
addr_map: UniqueView<ClientAddressMap>,
clients: View<Client>,
mut transforms: ViewMut<Transform>,
addrs: View<ClientAddress>,
) {
for event in &events.0 {
let Some(message) = check_message_auth
::<{ClientToServerMessageType::PositionChanged as u8}>
(&server, event, &clients, &addr_map) else { continue };
let ClientToServerMessage::PositionChanged { position, velocity: _, direction } = message.message else {
unreachable!()
};
//log movement (annoying duh)
log::debug!("dbg: player moved id: {} coords: {} quat: {}", message.client_id, position, direction);
//Apply position to server-side client
let mut trans = (&mut transforms).get(message.entity_id).unwrap();
trans.0 = Mat4::from_rotation_translation(direction, position);
//Transmit the change to other players
for (other_client, other_client_address) in (&clients, &addrs).iter() {
if other_client.0 == message.client_id {
continue
}
let Some(client) = server.0.client(&other_client_address.0) else {
log::error!("Client with address not found");
continue
};
client.borrow_mut().send(
postcard::to_allocvec(
&ServerToClientMessage::PlayerPositionChanged {
client_id: message.client_id,
position,
direction
}
).unwrap().into_boxed_slice(),
Channel::Move as usize,
SendMode::Reliable
);
}
}
}
pub fn on_client_disconnect(
mut all_storages: AllStoragesViewMut,
) {
let mut to_delete = Vec::new();
{
let server = all_storages.borrow::<NonSendSync<UniqueView<UdpServer>>>().unwrap();
let events = all_storages.borrow::<UniqueView<ServerEvents>>().unwrap();
let mut addr_map = all_storages.borrow::<UniqueViewMut<ClientAddressMap>>().unwrap();
let mut id_map = all_storages.borrow::<UniqueViewMut<ClientIdMap>>().unwrap();
let clients = all_storages.borrow::<View<Client>>().unwrap();
let mut chunk_manager = all_storages.borrow::<UniqueViewMut<ChunkManager>>().unwrap();
let addrs = all_storages.borrow::<View<ClientAddress>>().unwrap();
for event in &events.0 {
if let Event::Disconnect(addr) = event {
//XXX: do sth with this:
//let net_client = server.0.client(addr).unwrap();
let Some(&entity_id) = addr_map.0.get(addr) else {
log::error!("Disconnected client not authenticated, moving on");
continue;
};
let client_id = clients.get(entity_id).unwrap().0;
log::info!("Client disconnected: ID {}", client_id);
addr_map.0.remove(addr);
id_map.0.remove(&client_id);
to_delete.push(entity_id);
//unsubscribe from chunks
chunk_manager.unsubscribe_all(client_id);
//send disconnect message to other clients
for (_, other_client_address) in (&clients, &addrs).iter() {
let Some(client) = server.0.client(&other_client_address.0) else {
log::error!("Client with address not found");
continue
};
client.borrow_mut().send(
postcard::to_allocvec(
&ServerToClientMessage::PlayerDisconnected { id: client_id }
).unwrap().into_boxed_slice(),
Channel::SysEvt as usize,
SendMode::Reliable
);
}
}
}
}
for entity_id in to_delete {
all_storages.delete_entity(entity_id);
}
}

38
kubi-server/src/config.rs Normal file
View file

@ -0,0 +1,38 @@
use shipyard::{AllStoragesView, Unique};
use serde::{Serialize, Deserialize};
use std::{fs, net::SocketAddr};
#[derive(Serialize, Deserialize)]
pub struct ConfigTableServer {
pub address: SocketAddr,
pub max_clients: usize,
pub timeout_ms: u64,
pub password: Option<String>,
}
#[derive(Serialize, Deserialize)]
pub struct ConfigTableWorld {
pub seed: u64,
pub preheat_radius: u32,
}
#[derive(Serialize, Deserialize)]
pub struct ConfigTableQuery {
pub name: Option<String>
}
#[derive(Unique, Serialize, Deserialize)]
pub struct ConfigTable {
pub server: ConfigTableServer,
pub world: ConfigTableWorld,
pub query: ConfigTableQuery,
}
pub fn read_config(
storages: AllStoragesView,
) {
log::info!("Reading config...");
let config_str = fs::read_to_string("Server.toml").expect("No config file found");
let config: ConfigTable = toml::from_str(&config_str).expect("Invalid configuration file");
storages.add_unique(config);
}

50
kubi-server/src/main.rs Normal file
View file

@ -0,0 +1,50 @@
use shipyard::{IntoWorkload, Workload, WorkloadModificator, World};
use std::{thread, time::Duration};
mod util;
mod config;
mod server;
mod client;
mod world;
mod auth;
use config::read_config;
use server::{bind_server, update_server, log_server_errors};
use client::{init_client_maps, on_client_disconnect, sync_client_positions};
use auth::authenticate_players;
use world::{update_world, init_world};
fn initialize() -> Workload {
(
read_config,
bind_server,
init_client_maps,
init_world.after_all(read_config),
).into_workload()
}
fn update() -> Workload {
(
update_server,
(
log_server_errors,
authenticate_players,
update_world,
sync_client_positions,
on_client_disconnect,
).into_workload()
).into_sequential_workload()
}
fn main() {
kubi_logging::init();
let world = World::new();
world.add_workload(initialize);
world.add_workload(update);
world.run_workload(initialize).unwrap();
log::info!("The server is now running");
loop {
world.run_workload(update).unwrap();
thread::sleep(Duration::from_millis(16));
}
}

64
kubi-server/src/server.rs Normal file
View file

@ -0,0 +1,64 @@
use shipyard::{AllStoragesView, Unique, UniqueView, UniqueViewMut, NonSendSync};
use uflow::{server::{Server, Event as ServerEvent, Config as ServerConfig}, EndpointConfig};
use crate::config::ConfigTable;
#[derive(Unique)]
#[repr(transparent)]
pub struct UdpServer(pub Server);
#[derive(Unique, Default)]
pub struct ServerEvents(pub Vec<ServerEvent>);
pub trait IsMessageOfType {
///Checks if postcard-encoded message has a type
fn is_message_of_type<const T: u8>(&self) -> bool;
}
impl IsMessageOfType for ServerEvent {
fn is_message_of_type<const T: u8>(&self) -> bool {
let ServerEvent::Receive(_, data) = &self else { return false };
if data.len() == 0 { return false }
data[0] == T
}
}
pub fn bind_server(
storages: AllStoragesView,
) {
log::info!("Creating server...");
let config = storages.borrow::<UniqueView<ConfigTable>>().unwrap();
let server = Server::bind(
config.server.address,
ServerConfig {
max_total_connections: config.server.max_clients * 2,
max_active_connections: config.server.max_clients,
enable_handshake_errors: true,
endpoint_config: EndpointConfig {
active_timeout_ms: config.server.timeout_ms,
keepalive: true,
keepalive_interval_ms: 5000,
..Default::default()
},
}
).expect("Failed to create the server");
storages.add_unique_non_send_sync(UdpServer(server));
storages.add_unique(ServerEvents::default());
}
pub fn update_server(
mut server: NonSendSync<UniqueViewMut<UdpServer>>,
mut events: UniqueViewMut<ServerEvents>,
) {
server.0.flush();
events.0.clear();
events.0.extend(server.0.step());
}
pub fn log_server_errors(
events: UniqueView<ServerEvents>,
) {
for event in &events.0 {
if let ServerEvent::Error(addr, error) = event {
log::error!("Server error addr: {addr} error: {error:?}");
}
}
}

72
kubi-server/src/util.rs Normal file
View file

@ -0,0 +1,72 @@
use std::{net::SocketAddr, rc::Rc, cell::RefCell};
use shipyard::{View, Get, EntityId};
use uflow::server::{Event as ServerEvent, RemoteClient};
use kubi_shared::networking::{
messages::ClientToServerMessage,
client::{Client, ClientId}
};
use crate::{
server::{IsMessageOfType, UdpServer},
client::ClientAddressMap
};
#[derive(Clone)]
pub struct CtsMessageMetadata<'a> {
pub message: ClientToServerMessage,
pub client_id: ClientId,
pub entity_id: EntityId,
pub client_addr: SocketAddr,
pub client: &'a Rc<RefCell<RemoteClient>>,
}
impl From<CtsMessageMetadata<'_>> for ClientToServerMessage {
fn from(value: CtsMessageMetadata) -> Self { value.message }
}
impl From<CtsMessageMetadata<'_>> for ClientId {
fn from(value: CtsMessageMetadata) -> Self { value.client_id }
}
impl From<CtsMessageMetadata<'_>> for EntityId {
fn from(value: CtsMessageMetadata) -> Self { value.entity_id }
}
impl From<CtsMessageMetadata<'_>> for SocketAddr {
fn from(value: CtsMessageMetadata) -> Self { value.client_addr }
}
impl<'a> From<CtsMessageMetadata<'a>> for &'a Rc<RefCell<RemoteClient>> {
fn from(value: CtsMessageMetadata<'a>) -> Self { value.client }
}
pub fn check_message_auth<'a, const C_MSG: u8>(
server: &'a UdpServer,
event: &ServerEvent,
clients: &View<Client>,
addr_map: &ClientAddressMap
) -> Option<CtsMessageMetadata<'a>> {
let ServerEvent::Receive(client_addr, data) = event else{
return None
};
if !event.is_message_of_type::<C_MSG>() {
return None
}
let Some(client) = server.0.client(client_addr) else {
log::error!("Client doesn't exist");
return None
};
let Some(&entity_id) = addr_map.0.get(client_addr) else {
log::error!("Client not authenticated");
return None
};
let Ok(&Client(client_id)) = clients.get(entity_id) else {
log::error!("Entity ID is invalid");
return None
};
let Ok(message) = postcard::from_bytes(data) else {
log::error!("Malformed message");
return None
};
Some(CtsMessageMetadata {
message,
client_id,
entity_id,
client_addr: *client_addr,
client
})
}

306
kubi-server/src/world.rs Normal file
View file

@ -0,0 +1,306 @@
use shipyard::{AllStoragesView, Get, IntoIter, IntoWorkload, NonSendSync, SystemModificator, Unique, UniqueView, UniqueViewMut, View, Workload};
use glam::IVec3;
use hashbrown::HashMap;
use kubi_shared::{
chunk::CHUNK_SIZE,
queue::QueuedBlock,
networking::{
channels::Channel,
client::{Client, ClientId},
messages::{ClientToServerMessage, ClientToServerMessageType, ServerToClientMessage}
},
};
use uflow::{server::RemoteClient, SendMode};
use lz4_flex::compress_prepend_size as lz4_compress;
use anyhow::Result;
use std::{cell::RefCell, rc::Rc};
use kubi_shared::networking::client::ClientIdMap;
use crate::{
server::{UdpServer, ServerEvents},
config::ConfigTable,
client::{ClientAddress, ClientAddressMap},
util::check_message_auth,
};
pub mod chunk;
pub mod tasks;
use chunk::Chunk;
use self::{
tasks::{ChunkTaskManager, ChunkTask, ChunkTaskResponse, init_chunk_task_manager},
chunk::ChunkState
};
#[derive(Unique, Default)]
pub struct LocalBlockQueue {
pub queue: Vec<QueuedBlock>,
}
#[derive(Unique, Default)]
pub struct ChunkManager {
pub chunks: HashMap<IVec3, Chunk>
}
impl ChunkManager {
pub fn unsubscribe_all(&mut self, client_id: ClientId) {
for chunk in self.chunks.values_mut() {
chunk.subscriptions.remove(&client_id);
}
}
pub fn new() -> Self {
Self::default()
}
}
///Sends a compressed chunk packet
pub fn send_chunk_compressed(
client: &Rc<RefCell<RemoteClient>>,
message: &ServerToClientMessage
) -> Result<()> {
let mut ser_message = postcard::to_allocvec(&message)?;
let mut compressed = lz4_compress(&ser_message[1..]);
ser_message.truncate(1);
ser_message.append(&mut compressed);
let ser_message = ser_message.into_boxed_slice();
client.borrow_mut().send(
ser_message,
Channel::WorldData as usize,
SendMode::Reliable
);
Ok(())
}
fn process_chunk_requests(
server: NonSendSync<UniqueView<UdpServer>>,
events: UniqueView<ServerEvents>,
mut chunk_manager: UniqueViewMut<ChunkManager>,
task_manager: UniqueView<ChunkTaskManager>,
config: UniqueView<ConfigTable>,
addr_map: UniqueView<ClientAddressMap>,
clients: View<Client>
) {
for event in &events.0 {
let Some(message) = check_message_auth
::<{ClientToServerMessageType::ChunkSubRequest as u8}>
(&server, event, &clients, &addr_map) else { continue };
let ClientToServerMessage::ChunkSubRequest { chunk: chunk_position } = message.message else {
unreachable!()
};
if let Some(chunk) = chunk_manager.chunks.get_mut(&chunk_position) {
chunk.subscriptions.insert(message.client_id);
//TODO Start task here if status is "Nothing"
if let Some(blocks) = &chunk.blocks {
send_chunk_compressed(
message.client,
&ServerToClientMessage::ChunkResponse {
chunk: chunk_position,
data: blocks.clone(),
queued: Vec::with_capacity(0)
}
).unwrap();
}
} else {
let mut chunk = Chunk::new();
chunk.state = ChunkState::Loading;
chunk.subscriptions.insert(message.client_id);
chunk_manager.chunks.insert(chunk_position, chunk);
task_manager.spawn_task(ChunkTask::LoadChunk {
position: chunk_position,
seed: config.world.seed,
});
}
}
}
fn process_finished_tasks(
server: NonSendSync<UniqueView<UdpServer>>,
task_manager: UniqueView<ChunkTaskManager>,
mut chunk_manager: UniqueViewMut<ChunkManager>,
id_map: UniqueView<ClientIdMap>,
client_addr: View<ClientAddress>,
mut local_queue: UniqueViewMut<LocalBlockQueue>,
) {
'outer: while let Some(res) = task_manager.receive() {
let ChunkTaskResponse::ChunkLoaded { chunk_position, blocks, queue } = res;
let Some(chunk) = chunk_manager.chunks.get_mut(&chunk_position) else {
log::warn!("Chunk discarded: Doesn't exist");
continue
};
if chunk.state != ChunkState::Loading {
log::warn!("Chunk discarded: Not Loading");
continue
}
chunk.state = ChunkState::Loaded;
chunk.blocks = Some(blocks.clone());
local_queue.queue.extend_from_slice(&queue);
log::debug!("Chunk {chunk_position} loaded, {} subs", chunk.subscriptions.len());
let chunk_packet = &ServerToClientMessage::ChunkResponse {
chunk: chunk_position,
data: blocks,
queued: queue //should this be here?
};
for &subscriber in &chunk.subscriptions {
let Some(&entity_id) = id_map.0.get(&subscriber) else {
log::error!("Invalid subscriber client id");
continue 'outer;
};
let Ok(&ClientAddress(client_addr)) = (&client_addr).get(entity_id) else {
log::error!("Invalid subscriber entity id");
continue 'outer;
};
let Some(client) = server.0.client(&client_addr) else {
log::error!("Client not connected");
continue 'outer;
};
send_chunk_compressed(client, chunk_packet).unwrap();
// client.borrow_mut().send(
// chunk_packet.clone(),
// CHANNEL_WORLD,
// SendMode::Reliable,
// );
}
}
}
fn process_chunk_unsubscribe_events(
server: NonSendSync<UniqueView<UdpServer>>,
events: UniqueView<ServerEvents>,
mut chunk_manager: UniqueViewMut<ChunkManager>,
addr_map: UniqueView<ClientAddressMap>,
clients: View<Client>
) {
for event in &events.0 {
let Some(message) = check_message_auth
::<{ClientToServerMessageType::ChunkUnsubscribe as u8}>
(&server, event, &clients, &addr_map) else { continue };
let ClientToServerMessage::ChunkUnsubscribe { chunk: chunk_position } = message.message else {
unreachable!()
};
let Some(chunk) = chunk_manager.chunks.get_mut(&chunk_position) else {
log::warn!("tried to unsubscribe from non-existent chunk");
continue
};
chunk.subscriptions.remove(&message.client_id);
//TODO unload chunk if no more subscribers
}
}
fn process_block_queue_messages(
server: NonSendSync<UniqueView<UdpServer>>,
events: UniqueView<ServerEvents>,
addr_map: UniqueView<ClientAddressMap>,
clients: View<Client>,
addrs: View<ClientAddress>,
mut queue: UniqueViewMut<LocalBlockQueue>,
) {
for event in &events.0 {
let Some(message) = check_message_auth
::<{ClientToServerMessageType::QueueBlock as u8}>
(&server, event, &clients, &addr_map) else { continue };
let ClientToServerMessage::QueueBlock { item } = message.message else { unreachable!() };
//place in our local world
queue.queue.push(item);
log::info!("Placed block {:?} at {}", item.block_type, item.position);
for (other_client, other_client_address) in (&clients, &addrs).iter() {
//No need to send the event back
if message.client_id == other_client.0 {
continue
}
//Get client
let Some(client) = server.0.client(&other_client_address.0) else {
log::error!("Client with address not found");
continue
};
//Send the message
client.borrow_mut().send(
postcard::to_allocvec(
&ServerToClientMessage::QueueBlock { item }
).unwrap().into_boxed_slice(),
Channel::Block as usize,
SendMode::Reliable,
);
}
}
}
fn process_block_queue(
mut chunk_manager: UniqueViewMut<ChunkManager>,
mut queue: UniqueViewMut<LocalBlockQueue>,
) {
let initial_len = queue.queue.len();
queue.queue.retain(|item| {
let chunk_position = item.position.div_euclid(IVec3::splat(CHUNK_SIZE as i32));
let block_position = item.position.rem_euclid(IVec3::splat(CHUNK_SIZE as i32));
let Some(chunk) = chunk_manager.chunks.get_mut(&chunk_position) else {
return true
};
let Some(blocks) = &mut chunk.blocks else {
return true
};
blocks[block_position.x as usize][block_position.y as usize][block_position.z as usize] = item.block_type;
false
});
if initial_len != queue.queue.len() {
log::debug!("queue processed {}/{} items", initial_len - queue.queue.len(), initial_len);
}
}
/// init local block queue and chunk manager
fn init_chunk_manager_and_block_queue(
storages: AllStoragesView
) {
storages.add_unique(ChunkManager::new());
storages.add_unique(LocalBlockQueue::default());
}
pub fn preheat_world(
mut chunk_manager: UniqueViewMut<ChunkManager>,
task_manager: UniqueView<ChunkTaskManager>,
config: UniqueView<ConfigTable>,
) {
let r = config.world.preheat_radius as i32;
for x in -r..=r {
for y in -r..=r {
for z in -r..=r {
let chunk_position = IVec3::new(x, y, z);
let mut chunk = Chunk::new();
chunk.state = ChunkState::Loading;
chunk_manager.chunks.insert(chunk_position, chunk);
task_manager.spawn_task(ChunkTask::LoadChunk {
position: chunk_position,
seed: config.world.seed,
});
}
}
}
}
pub fn init_world() -> Workload {
(
init_chunk_manager_and_block_queue.before_all(preheat_world),
init_chunk_task_manager.before_all(preheat_world),
preheat_world,
).into_workload()
}
pub fn update_world() -> Workload {
(
process_finished_tasks,
process_block_queue_messages,
process_block_queue,
process_chunk_unsubscribe_events,
process_chunk_requests,
).into_sequential_workload()
}

View file

@ -0,0 +1,28 @@
use hashbrown::HashSet;
use nohash_hasher::BuildNoHashHasher;
use kubi_shared::{
chunk::BlockData,
networking::client::ClientId
};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ChunkState {
Nothing,
Loading,
Loaded,
}
pub struct Chunk {
pub state: ChunkState,
pub blocks: Option<BlockData>,
pub subscriptions: HashSet<ClientId, BuildNoHashHasher<ClientId>>,
}
impl Chunk {
pub fn new() -> Self {
Self {
state: ChunkState::Nothing,
blocks: None,
subscriptions: HashSet::with_capacity_and_hasher(4, BuildNoHashHasher::default()),
}
}
}

View file

@ -0,0 +1,60 @@
use shipyard::{Unique, AllStoragesView};
use flume::{unbounded, Sender, Receiver};
use glam::IVec3;
use rayon::{ThreadPool, ThreadPoolBuilder};
use anyhow::Result;
use kubi_shared::{
chunk::BlockData,
worldgen::generate_world,
queue::QueuedBlock,
};
pub enum ChunkTask {
LoadChunk {
position: IVec3,
seed: u64,
}
}
pub enum ChunkTaskResponse {
ChunkLoaded {
chunk_position: IVec3,
blocks: BlockData,
queue: Vec<QueuedBlock>
}
}
#[derive(Unique)]
pub struct ChunkTaskManager {
channel: (Sender<ChunkTaskResponse>, Receiver<ChunkTaskResponse>),
pool: ThreadPool,
}
impl ChunkTaskManager {
pub fn new() -> Result<Self> {
Ok(Self {
channel: unbounded(),
pool: ThreadPoolBuilder::new().build()?
})
}
pub fn spawn_task(&self, task: ChunkTask) {
let sender = self.channel.0.clone();
self.pool.spawn(move || {
sender.send(match task {
ChunkTask::LoadChunk { position: chunk_position, seed } => {
//unwrap is fine because abort is not possible
let (blocks, queue) = generate_world(chunk_position, seed, None).unwrap();
ChunkTaskResponse::ChunkLoaded { chunk_position, blocks, queue }
}
}).unwrap()
})
}
pub fn receive(&self) -> Option<ChunkTaskResponse> {
self.channel.1.try_recv().ok()
}
}
pub fn init_chunk_task_manager(
storages: AllStoragesView
) {
storages.add_unique(ChunkTaskManager::new().expect("ChunkTaskManager Init failed"));
}

29
kubi-shared/Cargo.toml Normal file
View file

@ -0,0 +1,29 @@
[package]
name = "kubi-shared"
version = "0.0.0"
edition = "2021"
publish = false
[dependencies]
glam = { version = "0.27", features = ["debug-glam-assert", "fast-math", "serde"] }
shipyard = { git = "https://github.com/leudz/shipyard", rev = "aacf3b1df5", default-features = false, features = ["std"] }
strum = { version = "0.26", features = ["derive"] }
num_enum = "0.7"
postcard = { version = "1.0", features = ["alloc"] }
serde = { version = "1.0", default-features = false, features = ["alloc", "derive"] }
serde_with = "3.4"
bincode = "1.3"
anyhow = "1.0"
fastnoise-lite = { version = "1.1", features = ["std", "f64"] }
rand = { version = "0.8", default_features = false, features = ["std", "min_const_gen"] }
rand_xoshiro = "0.6"
hashbrown = { version = "0.14", features = ["serde"] }
nohash-hasher = "0.2"
bytemuck = { version = "1.14", features = ["derive"] }
static_assertions = "1.1"
nz = "0.3"
atomic = "0.6"
[features]
default = []
nightly = ["hashbrown/nightly", "rand/nightly", "rand/simd_support", "glam/core-simd"]

272
kubi-shared/src/block.rs Normal file
View file

@ -0,0 +1,272 @@
use glam::{vec4, Vec4};
use serde::{Serialize, Deserialize};
use strum::EnumIter;
use num_enum::TryFromPrimitive;
use crate::item::Item;
#[derive(Serialize, Deserialize, Clone, Copy, Debug, EnumIter)]
#[repr(u8)]
pub enum BlockTexture {
Stone,
Dirt,
GrassTop,
GrassSide,
Sand,
Bedrock,
Wood,
WoodTop,
Leaf,
Torch,
TallGrass,
Snow,
GrassSideSnow,
Cobblestone,
Planks,
WaterSolid,
Water,
}
#[derive(Serialize, Deserialize, Clone, Copy, Debug, Default, PartialEq, Eq, EnumIter, TryFromPrimitive)]
#[repr(u8)]
pub enum Block {
#[default]
Air,
Marker,
Stone,
Dirt,
Grass,
Sand,
Cobblestone,
TallGrass,
Planks,
Torch,
Wood,
Leaf,
Water,
}
impl Block {
#[inline]
pub const fn descriptor(self) -> BlockDescriptor {
match self {
Self::Air => BlockDescriptor {
name: "air",
render: RenderType::None,
collision: CollisionType::None,
raycast_collision: false,
drops: None,
submerge: None,
},
Self::Marker => BlockDescriptor {
name: "marker",
render: RenderType::None,
collision: CollisionType::None,
raycast_collision: false,
drops: None,
submerge: None,
},
Self::Stone => BlockDescriptor {
name: "stone",
render: RenderType::Cube(
Transparency::Solid,
CubeTexture::all(BlockTexture::Stone)
),
collision: CollisionType::Solid,
raycast_collision: true,
drops: None,
submerge: None,
},
Self::Dirt => BlockDescriptor {
name: "dirt",
render: RenderType::Cube(
Transparency::Solid,
CubeTexture::all(BlockTexture::Dirt)
),
collision: CollisionType::Solid,
raycast_collision: true,
drops: None,
submerge: None,
},
Self::Grass => BlockDescriptor {
name: "grass",
render: RenderType::Cube(
Transparency::Solid,
CubeTexture::top_sides_bottom(
BlockTexture::GrassTop,
BlockTexture::GrassSide,
BlockTexture::Dirt
)
),
collision: CollisionType::Solid,
raycast_collision: true,
drops: None,
submerge: None,
},
Self::Sand => BlockDescriptor {
name: "sand",
render: RenderType::Cube(
Transparency::Solid,
CubeTexture::all(BlockTexture::Sand)
),
collision: CollisionType::Solid,
raycast_collision: true,
drops: None,
submerge: None,
},
Self::Cobblestone => BlockDescriptor {
name: "cobblestone",
render: RenderType::Cube(
Transparency::Solid,
CubeTexture::all(BlockTexture::Cobblestone)
),
collision: CollisionType::Solid,
raycast_collision: true,
drops: None,
submerge: None,
},
Self::TallGrass => BlockDescriptor {
name: "tall grass",
render: RenderType::Cross(CrossTexture::all(BlockTexture::TallGrass)),
collision: CollisionType::None,
raycast_collision: true,
drops: None,
submerge: None,
},
Self::Planks => BlockDescriptor {
name: "planks",
render: RenderType::Cube(
Transparency::Solid,
CubeTexture::all(BlockTexture::Planks)
),
collision: CollisionType::Solid,
raycast_collision: true,
drops: None,
submerge: None,
},
Self::Torch => BlockDescriptor {
name: "torch",
render: RenderType::Cross(CrossTexture::all(BlockTexture::Torch)),
collision: CollisionType::None,
raycast_collision: true,
drops: None,
submerge: None,
},
Self::Wood => BlockDescriptor {
name: "leaf",
render: RenderType::Cube(
Transparency::Solid,
CubeTexture::horizontal_vertical(BlockTexture::Wood, BlockTexture::WoodTop)
),
collision: CollisionType::Solid,
raycast_collision: true,
drops: None,
submerge: None,
},
Self::Leaf => BlockDescriptor {
name: "leaf",
render: RenderType::Cube(
Transparency::Binary,
CubeTexture::all(BlockTexture::Leaf)
),
collision: CollisionType::Solid,
raycast_collision: true,
drops: None,
submerge: None,
},
Self::Water => BlockDescriptor {
name: "water",
render: RenderType::Cube(
Transparency::Trans,
CubeTexture::all(BlockTexture::Water)
),
collision: CollisionType::None,
raycast_collision: true,
drops: None,
submerge: Some(vec4(0., 0., 0.25, 0.75)),
},
}
}
}
#[derive(Clone, Copy, Debug)]
pub struct BlockDescriptor {
pub name: &'static str,
pub render: RenderType,
pub collision: CollisionType,
pub raycast_collision: bool,
pub drops: Option<Item>,
pub submerge: Option<Vec4>,
}
#[derive(Clone, Copy, Debug)]
pub struct CubeTexture {
pub top: BlockTexture,
pub bottom: BlockTexture,
pub left: BlockTexture,
pub right: BlockTexture,
pub front: BlockTexture,
pub back: BlockTexture,
}
impl CubeTexture {
pub const fn top_sides_bottom(top: BlockTexture, sides: BlockTexture, bottom: BlockTexture) -> Self {
Self {
top,
bottom,
left: sides,
right: sides,
front: sides,
back: sides,
}
}
pub const fn horizontal_vertical(horizontal: BlockTexture, vertical: BlockTexture) -> Self {
Self::top_sides_bottom(vertical, horizontal, vertical)
}
pub const fn all(texture: BlockTexture) -> Self {
Self::horizontal_vertical(texture, texture)
}
}
#[derive(Clone, Copy, Debug)]
pub struct CrossTextureSides {
pub front: BlockTexture,
pub back: BlockTexture
}
impl CrossTextureSides {
pub const fn all(texture: BlockTexture) -> Self {
Self {
front: texture,
back: texture
}
}
}
#[derive(Clone, Copy, Debug)]
pub struct CrossTexture(pub CrossTextureSides, pub CrossTextureSides);
impl CrossTexture {
pub const fn all(texture: BlockTexture) -> Self {
Self(
CrossTextureSides::all(texture),
CrossTextureSides::all(texture)
)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum CollisionType {
None,
Solid,
}
#[derive(Clone, Copy, Debug)]
pub enum Transparency {
Solid,
Binary,
Trans,
}
#[derive(Clone, Copy, Debug)]
pub enum RenderType {
None,
Cube(Transparency, CubeTexture),
Cross(CrossTexture),
}

5
kubi-shared/src/chunk.rs Normal file
View file

@ -0,0 +1,5 @@
use crate::block::Block;
pub const CHUNK_SIZE: usize = 32;
pub type BlockData = Box<[[[Block; CHUNK_SIZE]; CHUNK_SIZE]; CHUNK_SIZE]>;
pub type BlockDataRef = [[[Block; CHUNK_SIZE]; CHUNK_SIZE]; CHUNK_SIZE];

169
kubi-shared/src/data.rs Normal file
View file

@ -0,0 +1,169 @@
use std::{
fs::File,
mem::size_of,
io::{Read, Seek, SeekFrom, Write},
borrow::Cow,
sync::{Arc, RwLock}
};
use num_enum::TryFromPrimitive;
use serde::{Serialize, Deserialize};
use glam::IVec3;
use hashbrown::HashMap;
use anyhow::Result;
use shipyard::Unique;
use static_assertions::const_assert_eq;
use crate::{
block::Block,
chunk::{CHUNK_SIZE, BlockDataRef, BlockData}
};
const SECTOR_SIZE: usize = CHUNK_SIZE * CHUNK_SIZE * CHUNK_SIZE * size_of::<Block>();
const RESERVED_SIZE: usize = 1048576; //~1mb (16 sectors assuming 32x32x32 world of 1byte blocks)
const RESERVED_SECTOR_COUNT: usize = RESERVED_SIZE / SECTOR_SIZE;
//magic = "KUBI" + IDENTITY (4 bytes)
const SUBHEADER_SIZE: usize = 8;
const SUBHEADER_MAGIC: [u8; 4] = *b"KUBI";
const SUBHEADER_IDENTITY: u32 = 1;
// #[repr(transparent)]
// struct IVec3Hash(IVec3);
#[derive(Serialize, Deserialize)]
pub struct WorldSaveDataHeader {
pub name: Cow<'static, str>,
pub seed: u64,
sector_count: u32,
chunk_map: HashMap<IVec3, u32>,
}
impl Default for WorldSaveDataHeader {
fn default() -> Self {
Self {
name: "World".into(),
seed: 0,
sector_count: RESERVED_SECTOR_COUNT as u32,
chunk_map: HashMap::new()
}
}
}
#[derive(Unique)]
pub struct WorldSaveFile {
pub file: File,
pub header: WorldSaveDataHeader,
}
pub type SharedSaveFile = Arc<RwLock<WorldSaveFile>>;
impl WorldSaveFile {
pub fn new(file: File) -> Self {
WorldSaveFile {
file,
header: WorldSaveDataHeader::default()
}
}
fn read_header(&mut self) -> Result<()> {
self.file.rewind()?;
let mut subheader = [0u8; SUBHEADER_SIZE];
self.file.read_exact(&mut subheader)?;
if subheader[0..4] != SUBHEADER_MAGIC {
return Err(anyhow::anyhow!("invalid file header"));
}
if subheader[4..8] != SUBHEADER_IDENTITY.to_be_bytes() {
return Err(anyhow::anyhow!("this save file cannot be loaded by this version of the game"));
}
let limit = (RESERVED_SIZE - SUBHEADER_SIZE) as u64;
self.header = bincode::deserialize_from((&self.file).take(limit))?;
Ok(())
}
fn write_header(&mut self) -> Result<()> {
self.file.rewind()?;
self.file.write_all(&SUBHEADER_MAGIC)?;
self.file.write_all(&SUBHEADER_IDENTITY.to_be_bytes())?;
//XXX: this can cause the header to destroy chunk data (if it's WAY too long)
// read has checks against this, but write doesn't
// 1mb is pretty generous tho, so it's not a *big* deal
bincode::serialize_into(&self.file, &self.header)?;
Ok(())
}
pub fn initialize(&mut self) -> Result<()> {
self.write_header()?;
Ok(())
}
pub fn load_data(&mut self) -> Result<()> {
self.read_header()?;
Ok(())
}
fn allocate_sector(&mut self) -> u32 {
let value = self.header.sector_count + 1;
self.header.sector_count += 1;
value
}
pub fn save_chunk(&mut self, position: IVec3, data: &BlockDataRef) -> Result<()> {
let mut header_modified = false;
let sector = self.header.chunk_map.get(&position).copied().unwrap_or_else(|| {
header_modified = true;
self.allocate_sector()
});
let offset = sector as u64 * SECTOR_SIZE as u64;
const_assert_eq!(size_of::<Block>(), 1);
let data: &[u8; SECTOR_SIZE] = unsafe { std::mem::transmute(data) };
self.file.seek(SeekFrom::Start(offset))?;
self.file.write_all(data)?;
if header_modified {
self.write_header()?;
}
self.file.sync_data()?;
Ok(())
}
///TODO partial chunk commit (No need to write whole 32kb for a single block change!)
pub fn chunk_set_block() {
todo!()
}
pub fn chunk_exists(&self, position: IVec3) -> bool {
self.header.chunk_map.contains_key(&position)
}
pub fn load_chunk(&mut self, position: IVec3) -> Result<Option<BlockData>> {
let Some(&sector) = self.header.chunk_map.get(&position) else {
return Ok(None);
};
let mut buffer = Box::new([0u8; CHUNK_SIZE * CHUNK_SIZE * CHUNK_SIZE * size_of::<Block>()]);
let offset = sector as u64 * SECTOR_SIZE as u64;
self.file.seek(SeekFrom::Start(offset))?;
self.file.read_exact(&mut buffer[..])?;
//should be safe under these conditions:
//Block is a single byte
//All block data bytes are in valid range
const_assert_eq!(size_of::<Block>(), 1);
for &byte in &buffer[..] {
let block = Block::try_from_primitive(byte);
match block {
//Sanity check, not actually required: (should NEVER happen)
Ok(block) => debug_assert_eq!(byte, block as u8),
Err(_) => anyhow::bail!("invalid block data"),
}
}
let data: BlockData = unsafe { std::mem::transmute(buffer) };
Ok(Some(data))
}
}

36
kubi-shared/src/entity.rs Normal file
View file

@ -0,0 +1,36 @@
use shipyard::Component;
use serde::{Serialize, Deserialize};
#[derive(Component)]
pub struct Entity;
#[derive(Component, Serialize, Deserialize, Clone, Copy, Debug)]
pub struct Health {
pub current: u8,
pub max: u8,
}
impl Health {
pub fn new(health: u8) -> Self {
Self {
current: health,
max: health
}
}
}
// impl PartialEq for Health {
// fn eq(&self, other: &Self) -> bool {
// self.current == other.current
// }
// }
// impl Eq for Health {}
// impl PartialOrd for Health {
// fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
// self.current.partial_cmp(&other.current)
// }
// }
// impl Ord for Health {
// fn cmp(&self, other: &Self) -> std::cmp::Ordering {
// self.current.cmp(&other.current)
// }
// }

66
kubi-shared/src/item.rs Normal file
View file

@ -0,0 +1,66 @@
use std::num::NonZeroU8;
use num_enum::TryFromPrimitive;
use serde::{Serialize, Deserialize};
use strum::EnumIter;
use crate::block::Block;
#[derive(Clone, Copy)]
pub enum ItemUsage {
AsBlock(Block)
}
pub struct ItemDescriptor {
pub name: &'static str,
pub usage: Option<ItemUsage>,
pub stack_size: NonZeroU8,
}
#[derive(Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Hash, Debug, EnumIter, TryFromPrimitive)]
#[repr(u8)]
pub enum Item {
TestItem,
}
impl Item {
#[inline]
pub const fn descriptor(self) -> ItemDescriptor {
match self {
Self::TestItem => ItemDescriptor {
name: "Test Item",
usage: None,
stack_size: nz::u8!(32),
},
}
}
}
#[derive(Clone, Copy)]
pub struct ItemCollection(Option<(Item, NonZeroU8)>);
impl ItemCollection {
pub const fn new(item: Item, amount: NonZeroU8) -> Self {
Self(Some((item, amount)))
}
pub const fn new_single(item: Item) -> Self {
Self(Some((item, nz::u8!(1))))
}
pub const fn new_empty() -> Self {
Self(None)
}
pub const fn with_amount(&self, amount: NonZeroU8) -> Self {
Self(match self.0 {
Some((item, _)) => Some((item, amount)),
None => None,
})
}
/// Add items from another slot, copying them\
/// Returns the leftover items
pub fn add(&mut self, from: &Self) -> Self {
let Some((item, count)) = from.0 else { return Self::new_empty() };
todo!() //TODO finish item slot system
}
}

10
kubi-shared/src/lib.rs Normal file
View file

@ -0,0 +1,10 @@
pub mod block;
pub mod item;
pub mod networking;
pub mod worldgen;
pub mod chunk;
pub mod transform;
pub mod entity;
pub mod player;
pub mod queue;
pub mod data;

View file

@ -0,0 +1,4 @@
pub mod messages;
pub mod state;
pub mod client;
pub mod channels;

View file

@ -0,0 +1,17 @@
#[repr(u8)]
pub enum Channel {
#[deprecated]
Generic = 0,
/// Used during the initial handshake process
Auth = 1,
/// Used for sending chunk data from server to client
WorldData = 2,
/// Used for sending/receiving block place events
Block = 3,
/// Used for sending/receiving player movements
Move = 4,
/// Used for system events, like players joining or leaving
SysEvt = 5,
/// Used for subscribing and unsubscribing from chunks
SubReq = 6,
}

View file

@ -0,0 +1,29 @@
use shipyard::{Component, Unique, EntityId};
use hashbrown::HashMap;
use nohash_hasher::BuildNoHashHasher;
pub type ClientId = u16;
#[derive(Component, Clone, Debug)]
#[repr(transparent)]
pub struct Username(pub String);
#[derive(Component, Clone, Copy, Debug)]
#[repr(transparent)]
pub struct Client(pub ClientId);
#[derive(Unique)]
#[repr(transparent)]
pub struct ClientIdMap(pub HashMap<ClientId, EntityId, BuildNoHashHasher<ClientId>>);
impl ClientIdMap {
pub fn new() -> Self {
Self(HashMap::with_capacity_and_hasher(16, BuildNoHashHasher::default()))
}
}
impl Default for ClientIdMap {
fn default() -> Self {
Self::new()
}
}

View file

@ -0,0 +1,143 @@
use glam::{Vec3, IVec3, Quat};
use serde::{Serialize, Deserialize};
use crate::{chunk::{BlockData, CHUNK_SIZE}, queue::QueuedBlock, entity::Health};
use super::client::ClientId;
pub const PROTOCOL_ID: u16 = 0;
pub trait ToMessageType<T> {
fn message_type(&self) -> T;
}
#[repr(u8)]
#[non_exhaustive]
pub enum ClientToServerMessageType {
ClientHello = 0,
PositionChanged = 1,
ChunkSubRequest = 2,
ChunkUnsubscribe = 3,
QueueBlock = 4,
}
#[derive(Serialize, Deserialize, Clone)]
#[repr(u8)]
#[non_exhaustive]
pub enum ClientToServerMessage {
ClientHello {
username: String,
password: Option<String>,
} = ClientToServerMessageType::ClientHello as u8,
PositionChanged {
position: Vec3,
velocity: Vec3,
direction: Quat,
} = ClientToServerMessageType::PositionChanged as u8,
ChunkSubRequest {
chunk: IVec3,
} = ClientToServerMessageType::ChunkSubRequest as u8,
ChunkUnsubscribe {
chunk: IVec3,
} = ClientToServerMessageType::ChunkUnsubscribe as u8,
QueueBlock {
item: QueuedBlock
} = ClientToServerMessageType::QueueBlock as u8,
}
impl ToMessageType<ClientToServerMessageType> for ClientToServerMessage {
fn message_type(&self) -> ClientToServerMessageType {
match self {
ClientToServerMessage::ClientHello { .. } => ClientToServerMessageType::ClientHello,
ClientToServerMessage::PositionChanged { .. } => ClientToServerMessageType::PositionChanged,
ClientToServerMessage::ChunkSubRequest { .. } => ClientToServerMessageType::ChunkSubRequest,
ClientToServerMessage::ChunkUnsubscribe { .. } => ClientToServerMessageType::ChunkUnsubscribe,
ClientToServerMessage::QueueBlock { .. } => ClientToServerMessageType::QueueBlock,
}
}
}
#[repr(u8)]
#[non_exhaustive]
pub enum ServerToClientMessageType {
ServerHello = 0,
ServerFuckOff = 1,
PlayerPositionChanged = 2,
ChunkResponse = 3,
QueueBlock = 4,
PlayerConnected = 5,
PlayerDisconnected = 6,
}
#[serde_with::serde_as]
#[derive(Serialize, Deserialize, Clone)]
#[repr(u8)]
#[non_exhaustive]
pub enum ServerToClientMessage {
ServerHello {
init: InitData
} = ServerToClientMessageType::ServerHello as u8,
ServerFuckOff {
reason: String,
} = ServerToClientMessageType::ServerFuckOff as u8,
PlayerPositionChanged {
client_id: ClientId,
position: Vec3,
direction: Quat,
} = ServerToClientMessageType::PlayerPositionChanged as u8,
///## WARNING: THIS IS COMPRESSED
///MESSAGES OF THIS TYPE ARE FULLY
///COMPRESSED ***EXCEPT THE FIRST BYTE***
///TO REDUCE NETWORK USAGE
ChunkResponse {
chunk: IVec3,
#[serde_as(as = "Box<[[[_; CHUNK_SIZE]; CHUNK_SIZE]; CHUNK_SIZE]>")]
data: BlockData,
queued: Vec<QueuedBlock>,
} = ServerToClientMessageType::ChunkResponse as u8,
QueueBlock {
item: QueuedBlock
} = ServerToClientMessageType::QueueBlock as u8,
PlayerConnected {
init: ClientInitData
} = ServerToClientMessageType::PlayerConnected as u8,
PlayerDisconnected {
id: ClientId
} = ServerToClientMessageType::PlayerDisconnected as u8,
}
impl ToMessageType<ServerToClientMessageType> for ServerToClientMessage {
fn message_type(&self) -> ServerToClientMessageType {
match self {
ServerToClientMessage::ServerHello { .. } => ServerToClientMessageType::ServerHello,
ServerToClientMessage::ServerFuckOff { .. } => ServerToClientMessageType::ServerFuckOff,
ServerToClientMessage::PlayerPositionChanged { .. } => ServerToClientMessageType::PlayerPositionChanged,
ServerToClientMessage::ChunkResponse { .. } => ServerToClientMessageType::ChunkResponse,
ServerToClientMessage::QueueBlock { .. } => ServerToClientMessageType::QueueBlock,
ServerToClientMessage::PlayerConnected { .. } => ServerToClientMessageType::PlayerConnected,
ServerToClientMessage::PlayerDisconnected { .. } => ServerToClientMessageType::PlayerDisconnected,
}
}
}
//---
#[derive(Serialize, Deserialize, Clone)]
pub struct ClientInitData {
pub client_id: ClientId,
pub username: String,
pub position: Vec3,
pub velocity: Vec3,
pub direction: Quat,
pub health: Health,
}
#[derive(Serialize, Deserialize, Clone)]
pub struct InitData {
pub user: ClientInitData,
pub users: Vec<ClientInitData>
}

View file

@ -0,0 +1,15 @@
use shipyard::{Unique, Component};
// disconnected => connect => join => load => ingame
#[derive(Unique, Component, PartialEq, Eq, Clone, Copy, Debug)]
#[repr(u8)]
pub enum ClientJoinState {
/// Not connected yet
Disconnected,
/// Client has connected to the game, but hasn't authenticated yet
Connected,
/// Client has joined the game, but hasn't loaded the world yet
Joined,
/// Client is currently ingame
InGame,
}

11
kubi-shared/src/player.rs Normal file
View file

@ -0,0 +1,11 @@
use shipyard::Component;
use crate::block::Block;
pub const PLAYER_HEALTH: u8 = 20;
#[derive(Component)]
pub struct Player;
#[derive(Component, Clone, Copy, Debug, Default, PartialEq, Eq)]
#[repr(transparent)]
pub struct PlayerHolding(pub Option<Block>);

11
kubi-shared/src/queue.rs Normal file
View file

@ -0,0 +1,11 @@
use glam::IVec3;
use serde::{Serialize, Deserialize};
use crate::block::Block;
#[derive(Serialize, Deserialize, Clone, Copy, Debug)]
pub struct QueuedBlock {
pub position: IVec3,
pub block_type: Block,
/// Only replace air blocks
pub soft: bool,
}

View file

@ -0,0 +1,10 @@
use shipyard::Component;
use glam::{Mat4, Mat3};
#[derive(Component, Clone, Copy, Debug, Default)]
#[repr(transparent)]
pub struct Transform(pub Mat4);
#[derive(Component, Clone, Copy, Debug, Default)]
#[repr(transparent)]
pub struct Transform2d(pub Mat3);

200
kubi-shared/src/worldgen.rs Normal file
View file

@ -0,0 +1,200 @@
use std::sync::Arc;
use atomic::Atomic;
use bytemuck::{CheckedBitPattern, NoUninit};
use glam::IVec3;
use static_assertions::const_assert;
use crate::{
block::Block,
chunk::{BlockData, CHUNK_SIZE},
queue::QueuedBlock,
};
pub mod steps;
pub mod structures;
#[repr(u8)]
#[derive(Clone, Copy, Debug, Default, NoUninit, CheckedBitPattern)]
pub enum AbortState {
#[default]
Continue,
Abort,
Aborted,
}
const_assert!(Atomic::<AbortState>::is_lock_free());
pub struct SeedThingy {
pseed: u64,
iseed: i32,
iter: u8,
}
impl SeedThingy {
pub fn new(seed: u64) -> Self {
Self {
pseed: seed,
iseed: (seed & 0x7fffffffu64) as i32,
iter: 0,
}
}
pub fn next_seed(&mut self) -> i32 {
self.iter += 1;
self.iseed = (
self.pseed
.rotate_left((3 * self.iter) as _)
& 0x7fffffff
) as i32;
self.iseed
}
}
trait WorldGenStep {
fn initialize(generator: &WorldGenerator, seeder: &mut SeedThingy) -> Self;
fn generate(&mut self, generator: &mut WorldGenerator);
}
macro_rules! run_steps {
($gen: expr, $abort: expr, [$($step:ty),* $(,)?]) => {
(||{
let _abort: ::std::sync::Arc<::atomic::Atomic<$crate::worldgen::AbortState>> =
$abort.unwrap_or_else(|| ::std::sync::Arc::new(::atomic::Atomic::new($crate::worldgen::AbortState::Continue)));
let _chkabt = || _abort.compare_exchange(
$crate::worldgen::AbortState::Abort,
$crate::worldgen::AbortState::Aborted,
::atomic::Ordering::Relaxed,
::atomic::Ordering::Relaxed
).is_ok();
if _chkabt() { return false }
let mut _seeder = $crate::worldgen::SeedThingy::new($gen.seed);
$({
let _ensure_ref: &mut $crate::worldgen::WorldGenerator = $gen;
struct _Ensure0<T: $crate::worldgen::WorldGenStep>(T);
type _Ensure1 = _Ensure0<$step>;
let mut step: _Ensure1 = _Ensure0(<$step>::initialize(&*_ensure_ref, &mut _seeder));
if _chkabt() { return false }
step.0.generate(_ensure_ref);
if _chkabt() { return false }
})*
true
})()
};
}
#[derive(Default)]
pub struct WorldGeneratorData {
pub master_height_map: Option<Vec<Vec<i32>>>,
}
pub struct WorldGenerator {
seed: u64,
chunk_position: IVec3,
blocks: BlockData,
queue: Vec<QueuedBlock>,
pub data: WorldGeneratorData,
}
impl WorldGenerator {
fn offset(&self) -> IVec3 {
self.chunk_position * CHUNK_SIZE as i32
}
fn query(&self, position: IVec3) -> Block {
// let offset = self.offset();
// let event_pos = offset + position;
// if let Some(block) = self.queue.iter().find(|block| block.position == event_pos) {
// block.block_type
// } else {
// self.blocks[position.x as usize][position.y as usize][position.z as usize]
// }
self.blocks[position.x as usize][position.y as usize][position.z as usize]
}
fn place(&mut self, position: IVec3, block: Block) {
// let offset = self.offset();
// let event_pos = offset + position;
// self.queue.retain(|block: &QueuedBlock| {
// block.position != event_pos
// });
self.blocks[position.x as usize][position.y as usize][position.z as usize] = block;
}
fn place_if_empty(&mut self, position: IVec3, block: Block) {
if self.query(position) == Block::Air {
self.place(position, block);
}
}
fn place_or_queue(&mut self, position: IVec3, block: Block) {
let offset = self.offset();
if position.to_array().iter().any(|&x| !(0..CHUNK_SIZE).contains(&(x as usize))) {
let event_pos = offset + position;
self.queue.retain(|block: &QueuedBlock| {
block.position != event_pos
});
self.queue.push(QueuedBlock {
position: event_pos,
block_type: block,
soft: true
});
} else {
self.blocks[position.x as usize][position.y as usize][position.z as usize] = block;
}
}
fn global_position(&self, position: IVec3) -> IVec3 {
self.offset() + position
}
fn local_height(&self, height: i32) -> i32 {
let offset = self.chunk_position * CHUNK_SIZE as i32;
(height - offset.y).clamp(0, CHUNK_SIZE as i32)
}
fn local_y_position(&self, y: i32) -> Option<i32> {
let offset = self.chunk_position * CHUNK_SIZE as i32;
let position = y - offset.y;
(0..CHUNK_SIZE as i32).contains(&position).then_some(position)
}
/// crude hash of self.seed and x
fn seeded_hash(&self, x: impl std::hash::Hash) -> u64 {
//use std::hash to hash the seed and x
use std::hash::{Hash, Hasher};
let mut hasher = std::collections::hash_map::DefaultHasher::new();
x.hash(&mut hasher);
self.seed.hash(&mut hasher);
hasher.finish()
}
pub fn new(chunk_position: IVec3, seed: u64) -> Self {
Self {
seed,
chunk_position,
blocks: Box::new([[[Block::Air; CHUNK_SIZE]; CHUNK_SIZE]; CHUNK_SIZE]),
queue: Vec::with_capacity(0),
data: Default::default(),
}
}
/// Generate the chunk.
///
/// Will return `None` only if the generation was aborted.
pub fn generate(mut self, abort: Option<Arc<Atomic<AbortState>>>) -> Option<(BlockData, Vec<QueuedBlock>)> {
run_steps!(&mut self, abort, [
steps::_01_terrain::TerrainStep,
steps::_02_water::WaterStep,
steps::_03_caves::CaveStep,
steps::_04_layers::LayersStep,
steps::_05_decorate::DecorateStep,
steps::_06_trees::TreesStep,
]).then_some((self.blocks, self.queue))
}
}
pub fn generate_world(chunk_position: IVec3, seed: u64, abort: Option<Arc<Atomic<AbortState>>>) -> Option<(BlockData, Vec<QueuedBlock>)> {
//TODO: pass through None for abort
WorldGenerator::new(chunk_position, seed).generate(abort)
}

View file

@ -0,0 +1,6 @@
pub mod _01_terrain;
pub mod _02_water;
pub mod _03_caves;
pub mod _04_layers;
pub mod _05_decorate;
pub mod _06_trees;

View file

@ -0,0 +1,33 @@
use fastnoise_lite::{FastNoiseLite, FractalType};
use glam::ivec3;
use crate::{block::Block, chunk::CHUNK_SIZE};
use super::super::{SeedThingy, WorldGenStep, WorldGenerator};
pub struct TerrainStep {
noise: FastNoiseLite,
}
impl WorldGenStep for TerrainStep {
fn initialize(_: &WorldGenerator, seeder: &mut SeedThingy) -> Self {
let mut noise = FastNoiseLite::with_seed(seeder.next_seed());
noise.set_fractal_type(Some(FractalType::FBm));
noise.set_fractal_octaves(Some(4));
noise.set_frequency(Some(0.003));
Self { noise }
}
fn generate(&mut self, gen: &mut WorldGenerator) {
let mut height_map = vec![vec![0; CHUNK_SIZE]; CHUNK_SIZE];
for x in 0..CHUNK_SIZE as i32 {
for z in 0..CHUNK_SIZE as i32 {
let global_xz = gen.global_position(ivec3(x, 0, z));
let height = (self.noise.get_noise_2d(global_xz.x as f64, global_xz.z as f64) * 32.0) as i32;
height_map[x as usize][z as usize] = height;
for y in 0..gen.local_height(height) {
gen.place(ivec3(x, y, z), Block::Stone);
}
}
}
gen.data.master_height_map = Some(height_map);
}
}

View file

@ -0,0 +1,20 @@
use glam::ivec3;
use crate::{block::Block, chunk::CHUNK_SIZE, worldgen::SeedThingy};
use super::super::{WorldGenerator, WorldGenStep};
pub const WATER_LEVEL: i32 = 0;
pub struct WaterStep;
impl WorldGenStep for WaterStep {
fn initialize(_: &WorldGenerator, _: &mut SeedThingy) -> Self { Self }
fn generate(&mut self, gen: &mut WorldGenerator) {
for x in 0..CHUNK_SIZE as i32 {
for z in 0..CHUNK_SIZE as i32 {
for y in 0..gen.local_height(WATER_LEVEL) {
gen.place_if_empty(ivec3(x, y, z), Block::Water);
}
}
}
}
}

View file

@ -0,0 +1,47 @@
use fastnoise_lite::{FastNoiseLite, FractalType};
use glam::ivec3;
use crate::{block::Block, chunk::CHUNK_SIZE};
use super::super::{SeedThingy, WorldGenStep, WorldGenerator};
pub struct CaveStep {
a: FastNoiseLite,
b: FastNoiseLite,
}
impl WorldGenStep for CaveStep {
fn initialize(_: &WorldGenerator, seeder: &mut SeedThingy) -> Self {
let mut a = FastNoiseLite::with_seed(seeder.next_seed());
a.set_fractal_type(Some(FractalType::FBm));
a.set_fractal_octaves(Some(2));
let mut b = FastNoiseLite::with_seed(seeder.next_seed());
b.set_fractal_type(Some(FractalType::FBm));
b.set_fractal_octaves(Some(2));
Self { a, b }
}
fn generate(&mut self, gen: &mut WorldGenerator) {
for x in 0..CHUNK_SIZE as i32 {
for y in 0..CHUNK_SIZE as i32 {
for z in 0..CHUNK_SIZE as i32 {
let cave_size = ((gen.offset().y + y - 50) as f64 / -200.).clamp(0., 1.) as f32;
let inv_cave_size = 1. - cave_size;
if cave_size < 0.1 { continue }
let pos = ivec3(x, y, z);
if gen.query(pos) != Block::Stone { continue }
let pos_global = gen.global_position(pos);
let noise_a = self.a.get_noise_3d(pos_global.x as f64, pos_global.y as f64, pos_global.z as f64) * 0.5 + 0.5;
let noise_b = self.b.get_noise_3d(pos_global.x as f64, pos_global.y as f64, pos_global.z as f64) * 0.5 + 0.5;
if noise_a.min(noise_b) > (0.6 + 0.4 * inv_cave_size) {
gen.place(pos, Block::Air);
}
//TODO
}
}
}
}
}

View file

@ -0,0 +1,36 @@
use glam::ivec3;
use crate::{block::Block, chunk::CHUNK_SIZE, worldgen::SeedThingy};
use super::{
_02_water::WATER_LEVEL,
super::{WorldGenStep, WorldGenerator}
};
pub struct LayersStep;
impl WorldGenStep for LayersStep {
fn initialize(_: &WorldGenerator, _: &mut SeedThingy) -> Self { Self }
fn generate(&mut self, gen: &mut WorldGenerator) {
for x in 0..CHUNK_SIZE as i32 {
for z in 0..CHUNK_SIZE as i32 {
let terrain_height = gen.data.master_height_map.as_ref().unwrap()[x as usize][z as usize];
// Dirt layer height, naturally gets thinner as height gets deeper
let mut dirt_layer_height = (((terrain_height as f32 + 15.) / 20.).clamp(0., 1.) * 8.).round() as i32;
dirt_layer_height -= (gen.seeded_hash((x, z, 0x040)) & 1) as i32; //+ (gen.seeded_hash((x, z, 0x041)) & 1) as i32;
// Place dirt layer
for y in gen.local_height(terrain_height - dirt_layer_height)..gen.local_height(terrain_height) {
gen.place(ivec3(x, y, z), Block::Dirt);
}
// If above water level, place grass
if terrain_height >= WATER_LEVEL {
if let Some(local_y) = gen.local_y_position(terrain_height - 1) {
gen.place(ivec3(x, local_y, z), Block::Grass);
}
}
}
}
}
}

View file

@ -0,0 +1,31 @@
use glam::ivec3;
use crate::{block::Block, chunk::CHUNK_SIZE, worldgen::SeedThingy};
use super::{
_02_water::WATER_LEVEL,
super::{WorldGenStep, WorldGenerator},
};
pub struct DecorateStep;
impl WorldGenStep for DecorateStep {
fn initialize(_: &WorldGenerator, _: &mut SeedThingy) -> Self { Self }
fn generate(&mut self, gen: &mut WorldGenerator) {
for x in 0..CHUNK_SIZE as i32 {
for z in 0..CHUNK_SIZE as i32 {
let global_xz = gen.global_position(ivec3(x, 0, z));
let terrain_height = gen.data.master_height_map.as_ref().unwrap()[x as usize][z as usize];
//Place tall grass
if terrain_height >= WATER_LEVEL {
if let Some(local_y) = gen.local_y_position(terrain_height) {
if (gen.seeded_hash((global_xz.x, global_xz.z, 0x050)) & 0xf) == 0xf {
gen.place_if_empty(ivec3(x, local_y, z), Block::TallGrass);
}
}
}
}
}
}
}

View file

@ -0,0 +1,43 @@
use fastnoise_lite::{FastNoiseLite, NoiseType};
use glam::ivec3;
use crate::{chunk::CHUNK_SIZE, worldgen::SeedThingy};
use super::_02_water::WATER_LEVEL;
use crate::worldgen::{
WorldGenStep, WorldGenerator,
structures::{Structure, TreeStructure},
};
pub struct TreesStep {
density_noise: FastNoiseLite,
}
impl WorldGenStep for TreesStep {
fn initialize(_: &WorldGenerator, seeder: &mut SeedThingy) -> Self {
let mut density_noise = FastNoiseLite::with_seed(seeder.next_seed());
density_noise.set_noise_type(Some(NoiseType::OpenSimplex2));
density_noise.set_frequency(Some(0.008));
Self { density_noise }
}
fn generate(&mut self, gen: &mut WorldGenerator) {
for x in 0..CHUNK_SIZE as i32 {
for z in 0..CHUNK_SIZE as i32 {
let terrain_height = gen.data.master_height_map.as_ref().unwrap()[x as usize][z as usize];
if terrain_height < WATER_LEVEL { continue }
let global_xz = gen.global_position(ivec3(x, 0, z));
let mut density = self.density_noise.get_noise_2d(global_xz.x as f64, global_xz.z as f64) * 0.5 + 0.5;
density = density.powi(3);
if gen.seeded_hash((global_xz.x, global_xz.z, 0x060)) & 0xff >= (density * 7.).round() as u64 {
continue
}
let tree = TreeStructure::default();
if let Some(local_y) = gen.local_y_position(terrain_height) {
tree.place(gen, ivec3(x, local_y, z));
}
}
}
}
}

View file

@ -0,0 +1,9 @@
use glam::IVec3;
use super::WorldGenerator;
mod tree;
pub use tree::TreeStructure;
pub trait Structure {
fn place(&self, gen: &mut WorldGenerator, root_pos: IVec3);
}

View file

@ -0,0 +1,58 @@
use glam::IVec3;
use super::Structure;
use crate::{block::Block, worldgen::WorldGenerator};
#[derive(Clone, Copy, Debug)]
pub struct TreeStructure {
pub height: i32,
}
impl Default for TreeStructure {
fn default() -> Self {
Self { height: 5 }
}
}
impl Structure for TreeStructure {
fn place(&self, gen: &mut WorldGenerator, root: IVec3) {
//check the block below the tree, if it's grass, replace it with dirt
//XXX: This won't work if root.y == 0
if root.y != 0 && gen.query(root - IVec3::Y) == Block::Grass {
gen.place(root - IVec3::Y, Block::Dirt);
}
//Tree stem
for y in root.y..root.y + self.height {
gen.place_or_queue(IVec3::new(root.x, y, root.z), Block::Wood);
}
//Tree leaves
//Try to create the following shape:
//(a 5x2x5 cube that wraps around the stem with a 3x1x3 cube on top)
// xxx
// xx|xx
// xx|xx
// |
for y in 0..=4_i32 {
for x in -2..=2_i32 {
for z in -2..=2_i32 {
//Do not overwrite the stem
if y < 3 && x == 0 && z == 0 {
continue
}
// Cut off the corners of the top layer
if y >= 3 && (x.abs() > 1 || z.abs() > 1) {
continue
}
let position = IVec3::new(
root.x + x,
root.y + self.height - 3 + y,
root.z + z
);
gen.place_or_queue(position, Block::Leaf);
}
}
}
}
}

93
kubi/Cargo.toml Normal file
View file

@ -0,0 +1,93 @@
[package]
name = "kubi"
version = "0.0.0"
edition = "2021"
publish = false
[lib]
name = "kubilib"
crate-type = ["lib", "cdylib"]
[dependencies]
kubi-shared = { path = "../kubi-shared" }
kubi-logging = { path = "../kubi-logging" }
hui = { git = "https://github.com/griffi-gh/hui", rev = "6eb3d98ad4" }
hui-wgpu = { git = "https://github.com/griffi-gh/hui", rev = "6eb3d98ad4" }
hui-winit = { git = "https://github.com/griffi-gh/hui", rev = "6eb3d98ad4", features = ["winit_30"] }
log = "0.4"
wgpu = { version = "0.20", features = ["webgl"] }
pollster = "0.3"
bytemuck = { version = "1.15", features = ["derive"] }
winit = { version = "0.30", features = ["android-native-activity"] }
raw-window-handle = "0.6"
glam = { version = "0.27", features = ["debug-glam-assert", "fast-math"] }
image = { version = "0.25", default_features = false, features = ["png"] }
strum = { version = "0.26", features = ["derive"] }
hashbrown = "0.14"
nohash-hasher = "0.2"
rayon = "1.10"
shipyard = { git = "https://github.com/leudz/shipyard", rev = "aacf3b1df5", default-features = false, features = ["std", "proc", "thread_local"] }
anyhow = "1.0"
flume = "0.11"
gilrs = { version = "0.10", default_features = false, features = ["xinput"] }
uflow = "0.7"
postcard = { version = "1.0", features = ["alloc"] }
lz4_flex = { version = "0.11", default-features = false, features = ["std"] }
static_assertions = "1.1"
tinyset = "0.4"
serde_json = { version = "1.0", optional = true } #only used for `generate_visualizer_data`
rand = { version = "0.8", features = ["alloc", "small_rng"]}
atomic = "0.6"
tobj = "4.0"
[target.'cfg(target_os = "android")'.dependencies]
android-activity = "0.6"
ndk = "0.9"
[target.'cfg(target_os = "windows")'.dependencies]
winapi = { version = "0.3", features = ["wincon"] }
[features]
default = ["raw-evt"]
raw-evt = [] #required for mouse input, but breaks keyboard on android
generate_visualizer_data = ["dep:serde_json", "shipyard/serde1"]
safe_lz4 = ["lz4_flex/safe-encode", "lz4_flex/safe-decode"]
parallel = ["shipyard/parallel"] # causes some serious issues!
nightly = ["hashbrown/nightly", "glam/core-simd", "static_assertions/nightly", "lz4_flex/nightly", "kubi-shared/nightly", "rand/nightly"]
#part of wip android support
[package.metadata.android]
package = "com.ggh.kubi"
build_targets = ["aarch64-linux-android"]
assets = "../assets"
apk_name = "kubi"
theme = "@android:style/Theme.DeviceDefault.NoActionBar.Fullscreen"
label = "Kubi"
[package.metadata.android.sdk]
min_sdk_version = 16
target_sdk_version = 30
[[package.metadata.android.uses_feature]]
glEsVersion = 0x00030000
required = true
[[package.metadata.android.uses_feature]]
name = "android.hardware.touchscreen.multitouch"
required = true
[[package.metadata.android.uses_feature]]
name = "android.hardware.touchscreen.multitouch.distinct"
required = true
[package.metadata.android.application.activity]
label = "Kubi"
launch_mode = "singleTop"
orientation = "sensorLandscape"
config_changes = "orientation|keyboardHidden|screenLayout|screenSize"
exported = true
resizeable_activity = true
# [package.metadata.android.signing.release]
# path = "$HOME/.android/debug.keystore"
# keystore_password = "android"

14
kubi/shaders/c2d.wgsl Normal file
View file

@ -0,0 +1,14 @@
@group(0) @binding(0)
var<uniform> color: vec4<f32>;
@vertex
fn vs_main(
@location(0) position: vec2<f32>,
) -> @builtin(position) vec4<f32> {
return vec4<f32>(position, 0.0, 1.0);
}
@fragment
fn fs_main() -> @location(0) vec4<f32> {
return color;
}

View file

@ -0,0 +1,50 @@
struct CameraUniform {
view_proj: mat4x4<f32>,
};
@group(1) @binding(0)
var<uniform> camera: CameraUniform;
struct VertexInput {
@location(0) uv: vec2<f32>,
@location(1) position: vec3<f32>,
@location(2) normal: vec3<f32>,
@location(3) mat_row0: vec4<f32>,
@location(4) mat_row1: vec4<f32>,
@location(5) mat_row2: vec4<f32>,
@location(6) mat_row3: vec4<f32>,
}
struct VertexOutput {
@builtin(position) clip_position: vec4<f32>,
@location(0) uv: vec2<f32>,
@location(1) normal: vec3<f32>,
};
@vertex
fn vs_main(
in: VertexInput,
) -> VertexOutput {
let inst_mat = mat4x4<f32>(
in.mat_row0,
in.mat_row1,
in.mat_row2,
in.mat_row3,
);
var out: VertexOutput;
out.clip_position = camera.view_proj * (inst_mat * vec4<f32>(in.position, 1.0));
out.uv = in.uv;
out.normal = in.normal;
return out;
}
@group(0) @binding(0)
var t_diffuse: texture_2d<f32>;
@group(0) @binding(1)
var s_diffuse: sampler;
@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
return textureSample(t_diffuse, s_diffuse, in.uv);
}

View file

@ -0,0 +1,27 @@
struct CameraUniform {
view_proj: mat4x4<f32>,
};
@group(0) @binding(0)
var<uniform> camera: CameraUniform;
struct SboxUniform {
position: vec3<f32>,
};
@group(1) @binding(0)
var<uniform> sbox: SboxUniform;
@vertex
fn vs_main(
@location(0) position: vec3<f32>,
) -> @builtin(position) vec4<f32> {
return camera.view_proj * vec4<f32>(position + sbox.position, 1.0);
}
@fragment
fn fs_main(
@builtin(position) in: vec4<f32>,
) -> @location(0) vec4<f32> {
return vec4<f32>(0.0, 0.0, 0.0, 0.5);
}

62
kubi/shaders/world.wgsl Normal file
View file

@ -0,0 +1,62 @@
struct CameraUniform {
view_proj: mat4x4<f32>,
};
@group(1) @binding(0)
var<uniform> camera: CameraUniform;
struct VertexInput {
@location(0) position: vec3<f32>,
@location(1) normal: vec3<f32>,
@location(2) uv: vec2<f32>,
@location(3) tex_index: u32,
}
struct VertexOutput {
@builtin(position) clip_position: vec4<f32>,
@location(0) uv: vec2<f32>,
@location(1) normal: vec3<f32>,
@location(2) @interpolate(flat)tex_index: u32,
};
@vertex
fn vs_main(
in: VertexInput,
) -> VertexOutput {
var out: VertexOutput;
out.uv = in.uv;
out.normal = in.normal;
out.tex_index = in.tex_index;
out.clip_position = camera.view_proj * vec4<f32>(in.position, 1.0);
return out;
}
@group(0) @binding(0)
var t_diffuse: texture_2d_array<f32>;
@group(0) @binding(1)
var s_diffuse: sampler;
@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
// not actual lighting; makes it easier to distinguish unlit faces
let light: f32 =
abs(in.normal.x) + .85 *
abs(in.normal.y) + .65 *
abs(in.normal.z);
let color: vec4<f32> =
textureSample(t_diffuse, s_diffuse, in.uv, in.tex_index)
* vec4<f32>(light, light, light, 1.0);
if (color.a < 0.5) {
discard;
}
return color;
}
@fragment
fn fs_main_trans(in: VertexOutput) -> @location(0) vec4<f32> {
return textureSample(t_diffuse, s_diffuse, in.uv, in.tex_index);
}

View file

@ -0,0 +1,93 @@
use shipyard::{UniqueViewMut, UniqueView, View, IntoIter, ViewMut, EntitiesViewMut, Workload, IntoWorkload};
use winit::keyboard::KeyCode;
use kubi_shared::{
block::Block,
queue::QueuedBlock,
player::PlayerHolding,
};
use crate::{
player::MainPlayer,
world::{
raycast::{LookingAtBlock, RAYCAST_STEP},
queue::BlockUpdateQueue
},
input::{Inputs, PrevInputs, RawKbmInputState},
events::{
EventComponent,
player_actions::PlayerActionEvent
},
};
const BLOCK_KEY_MAP: &[(KeyCode, Block)] = &[
(KeyCode::Digit1, Block::Cobblestone),
(KeyCode::Digit2, Block::Planks),
(KeyCode::Digit3, Block::Dirt),
(KeyCode::Digit4, Block::Grass),
(KeyCode::Digit5, Block::Sand),
(KeyCode::Digit6, Block::Stone),
(KeyCode::Digit7, Block::Torch),
(KeyCode::Digit8, Block::Leaf),
];
fn pick_block_with_number_keys(
main_player: View<MainPlayer>,
mut holding: ViewMut<PlayerHolding>,
input: UniqueView<RawKbmInputState>,
) {
let Some((_, mut holding)) = (&main_player, &mut holding).iter().next() else { return };
for &(key, block) in BLOCK_KEY_MAP {
if input.keyboard_state.contains(key as u32) {
holding.0 = Some(block);
return
}
}
}
fn block_placement_system(
main_player: View<MainPlayer>,
holding: View<PlayerHolding>,
raycast: View<LookingAtBlock>,
input: UniqueView<Inputs>,
prev_input: UniqueView<PrevInputs>,
mut block_event_queue: UniqueViewMut<BlockUpdateQueue>,
mut entities: EntitiesViewMut,
mut events: ViewMut<EventComponent>,
mut player_events: ViewMut<PlayerActionEvent>,
) {
let action_place = input.action_b && !prev_input.0.action_b;
let action_break = input.action_a && !prev_input.0.action_a;
if action_place ^ action_break {
//get components
let Some((_, ray, block)) = (&main_player, &raycast, &holding).iter().next() else { return };
let Some(ray) = ray.0 else { return };
//get coord and block type
let (place_position, place_block) = if action_place {
if block.0.is_none() { return }
let position = (ray.position - ray.direction * (RAYCAST_STEP + 0.001)).floor().as_ivec3();
(position, block.0.unwrap())
} else {
(ray.block_position, Block::Air)
};
//queue place
block_event_queue.0.push(QueuedBlock {
position: place_position,
block_type: place_block,
soft: place_block != Block::Air,
});
//send event
entities.add_entity(
(&mut events, &mut player_events),
(EventComponent, PlayerActionEvent::UpdatedBlock {
position: place_position,
block: place_block,
})
);
}
}
pub fn update_block_placement() -> Workload {
(
pick_block_with_number_keys,
block_placement_system
).into_sequential_workload()
}

View file

@ -39,5 +39,5 @@ pub fn compute_cameras() -> Workload {
(
update_matrices,
update_frustum,
).into_workload()
).into_sequential_workload()
}

View file

@ -8,7 +8,7 @@
// three layers of stolen code, yay!
use glam::{Vec3A, Vec4, Mat3A, vec3a, Vec3, vec4};
use shipyard::{ViewMut, IntoIter, View};
use shipyard::{ViewMut, IntoIter, View, track};
use crate::transform::Transform;
use super::Camera;
@ -122,9 +122,9 @@ fn intersection<const A: usize, const B: usize, const C: usize>(planes: &[Vec4;
pub fn update_frustum(
mut cameras: ViewMut<Camera>,
transforms: View<Transform>
transforms: View<Transform, track::All>
) {
for (camera, _) in (&mut cameras, transforms.inserted_or_modified()).iter() {
camera.frustum = Frustum::compute(camera);
for (mut camera, _) in (&mut cameras, transforms.inserted_or_modified()).iter() {
camera.frustum = Frustum::compute(&camera);
}
}

View file

@ -0,0 +1,50 @@
use glam::{Vec3, Mat4};
use shipyard::{ViewMut, View, IntoIter, Workload, IntoWorkload, track, UniqueView, SystemModificator};
use crate::{transform::Transform, rendering::Renderer, events::WindowResizedEvent};
use super::Camera;
//maybe parallelize these two?
fn update_view_matrix(
mut vm_camera: ViewMut<Camera>,
v_transform: View<Transform, track::All>
) {
for (mut camera, transform) in (&mut vm_camera, v_transform.inserted_or_modified()).iter() {
let (_, rotation, translation) = transform.0.to_scale_rotation_translation();
let direction = (rotation.normalize() * Vec3::NEG_Z).normalize();
camera.view_matrix = Mat4::look_to_rh(translation, direction, camera.up);
}
}
fn update_perspective_matrix(
mut vm_camera: ViewMut<Camera>,
ren: UniqueView<Renderer>,
) {
let sz = ren.size_vec2();
for mut camera in (&mut vm_camera).iter() {
camera.perspective_matrix = Mat4::perspective_rh(
camera.fov,
sz.x / sz.y,
camera.z_near,
camera.z_far,
)
}
}
fn need_perspective_calc(
v_camera: View<Camera>,
resize_event: View<WindowResizedEvent>,
) -> bool {
(resize_event.len() > 0) ||
(v_camera.iter().any(|camera| {
camera.perspective_matrix == Mat4::default()
}))
}
pub fn update_matrices() -> Workload {
(
update_view_matrix,
//update_perspective_matrix,
update_perspective_matrix.run_if(need_perspective_calc),
).into_sequential_workload()
}

65
kubi/src/chat.rs Normal file
View file

@ -0,0 +1,65 @@
use kubi_shared::networking::client::ClientId;
use shipyard::{AllStoragesView, Unique};
pub enum ChatMessage {
PlayerMessage {
id: ClientId,
username: String,
message: String,
},
PlayerJoin {
id: ClientId,
username: String,
},
PlayerLeave {
id: ClientId,
username: String,
},
System(String),
}
// impl ChatMessage {
// pub fn render() -> String {
// todo!() //TODO
// }
// }
#[derive(Unique, Default)]
pub struct ChatHistory {
pub messages: Vec<ChatMessage>,
}
impl ChatHistory {
pub fn add_message(&mut self, message: ChatMessage) {
self.messages.push(message);
}
pub fn add_chat_message(&mut self, id: ClientId, username: String, message: String,) {
self.messages.push(ChatMessage::PlayerMessage { id, username, message });
}
pub fn add_player_join(&mut self, id: ClientId, username: String) {
self.messages.push(ChatMessage::PlayerJoin { id, username });
}
pub fn add_player_leave(&mut self, id: ClientId, username: String) {
self.messages.push(ChatMessage::PlayerLeave { id, username });
}
pub fn add_system_message(&mut self, message: String) {
self.messages.push(ChatMessage::System(message));
}
pub fn get_messages(&self) -> &[ChatMessage] {
&self.messages[..]
}
}
pub fn init_chat_manager(
storages: AllStoragesView,
) {
let mut chat_manager = ChatHistory::default();
chat_manager.add_system_message("Welcome to Kubi! Chat messages will appear here".to_string());
chat_manager.add_system_message("F1 (Hold): Settings; F3: Release cursor; F4/F5: Gamemode".to_string());
storages.add_unique(chat_manager);
}

188
kubi/src/client_physics.rs Normal file
View file

@ -0,0 +1,188 @@
//TODO client-side physics
//TODO move this to shared
use glam::{vec3, Mat4, Vec3, Vec3Swizzles};
use shipyard::{track, AllStoragesView, Component, IntoIter, Unique, UniqueView, ViewMut};
use kubi_shared::{block::{Block, CollisionType}, transform::Transform};
use crate::{delta_time::DeltaTime, world::ChunkStorage};
#[derive(Unique)]
pub struct GlobalClPhysicsConfig {
pub gravity: Vec3,
///XXX: currenly unused:
pub iterations: usize,
}
impl Default for GlobalClPhysicsConfig {
fn default() -> Self {
Self {
gravity: Vec3::new(0., -9.8, 0.),
iterations: 10,
}
}
}
//TODO: actors should be represented by a vertical line, not a point.
//XXX: maybe a capsule? (or configurable hull?)
//TODO: per block friction
#[derive(Component)]
pub struct ClPhysicsActor {
pub disable: bool,
pub offset: Vec3,
pub forces: Vec3,
pub frame_velocity: Vec3,
pub velocity: Vec3,
pub decel: Vec3,
pub gravity_scale: f32,
pub max_velocity: (Option<f32>, Option<f32>, Option<f32>),
pub hack_xz_circular: bool,
flag_ground: bool,
flag_collision: bool,
}
impl ClPhysicsActor {
pub fn apply_force(&mut self, force: Vec3) {
self.forces += force;
}
pub fn add_frame_velocity(&mut self, force: Vec3) {
self.frame_velocity += force;
}
pub fn on_ground(&self) -> bool {
self.flag_ground
}
}
impl Default for ClPhysicsActor {
fn default() -> Self {
Self {
//HACK: for player
disable: false,
offset: vec3(0., 1.5, 0.),
forces: Vec3::ZERO,
frame_velocity: Vec3::ZERO,
velocity: Vec3::ZERO,
//constant deceleration, in ratio per second. e.g. value of 1 should stop the actor in 1 second.
decel: vec3(1., 0., 1.),
gravity_scale: 1.,
max_velocity: (Some(20.), None, Some(20.)),
hack_xz_circular: true,
flag_ground: false,
flag_collision: false,
}
}
}
trait BlockCollisionExt {
fn collision_type(&self) -> CollisionType;
fn is_solid(&self) -> bool {
self.collision_type() == CollisionType::Solid
}
}
impl BlockCollisionExt for Option<Block> {
fn collision_type(&self) -> CollisionType {
self.unwrap_or(Block::Air).descriptor().collision
}
}
impl BlockCollisionExt for Block {
fn collision_type(&self) -> CollisionType {
self.descriptor().collision
}
}
pub fn init_client_physics(
storages: AllStoragesView,
) {
storages.add_unique(GlobalClPhysicsConfig::default());
}
pub fn update_client_physics_late(
mut actors: ViewMut<ClPhysicsActor>,
mut transforms: ViewMut<Transform, track::All>,
conf: UniqueView<GlobalClPhysicsConfig>,
world: UniqueView<ChunkStorage>,
dt: UniqueView<DeltaTime>,
) {
for (mut actor, mut transform) in (&mut actors, &mut transforms).iter() {
if actor.disable {
actor.forces = Vec3::ZERO;
continue;
}
//apply forces
let actor_forces = actor.forces;
actor.velocity += (actor_forces + conf.gravity) * dt.0.as_secs_f32();
actor.forces = Vec3::ZERO;
//get position
let (scale, rotation, mut actor_position) = transform.0.to_scale_rotation_translation();
actor_position -= actor.offset;
//get grid-aligned pos and blocks
let actor_block_pos = actor_position.floor().as_ivec3();
let actor_block = world.get_block(actor_block_pos);
let actor_block_pos_slightly_below = (actor_position + Vec3::NEG_Y * 0.01).floor().as_ivec3();
let actor_block_below = world.get_block(actor_block_pos_slightly_below);
//update flags
actor.flag_collision = actor_block.is_solid();
actor.flag_ground = actor.flag_collision || actor_block_below.is_solid();
//push actor back out of the block
if actor.flag_collision {
//first, compute restitution, based on position inside the block
// let block_center = actor_block_pos.as_f32() + Vec3::ONE * 0.5;
// let to_block_center = actor_position - block_center;
//then, based on normal:
//push the actor back
//actor_position += normal * 0.5;
//cancel out velocity in the direction of the normal
// let dot = actor.velocity.dot(normal);
// if dot > 0. {
// //actor.velocity -= normal * dot;
// actor.velocity = Vec3::ZERO;
// }
//HACK: for now, just stop the vertical velocity if on ground altogether,
//as we don't have proper collision velocity resolution yet (we need to compute dot product or sth)
if actor.flag_ground {
actor.velocity.y = actor.velocity.y.max(0.);
}
}
//clamp velocity
let max_velocity = actor.max_velocity;
if actor.hack_xz_circular && actor.max_velocity.0.is_some() && (actor.max_velocity.0 == actor.max_velocity.2) {
actor.velocity.y = actor.velocity.y.clamp(-max_velocity.1.unwrap_or(f32::MAX), max_velocity.1.unwrap_or(f32::MAX));
let clamped = actor.velocity.xz().clamp_length_max(actor.max_velocity.0.unwrap_or(f32::MAX));
actor.velocity.x = clamped.x;
actor.velocity.z = clamped.y;
} else {
actor.velocity = vec3(
actor.velocity.x.clamp(-max_velocity.0.unwrap_or(f32::MAX), max_velocity.0.unwrap_or(f32::MAX)),
actor.velocity.y.clamp(-max_velocity.1.unwrap_or(f32::MAX), max_velocity.1.unwrap_or(f32::MAX)),
actor.velocity.z.clamp(-max_velocity.2.unwrap_or(f32::MAX), max_velocity.2.unwrap_or(f32::MAX)),
);
}
//Apply velocity
actor_position += (actor.velocity + actor.frame_velocity) * dt.0.as_secs_f32();
actor.frame_velocity = Vec3::ZERO;
actor_position += actor.offset;
transform.0 = Mat4::from_scale_rotation_translation(scale, rotation.normalize(), actor_position);
//Apply "friction"
let actor_velocity = actor.velocity;
let actor_decel = actor.decel;
actor.velocity -= actor_velocity * actor_decel * dt.0.as_secs_f32();
}
// for (_, mut transform) in (&controllers, &mut transforms).iter() {
// let (scale, rotation, mut translation) = transform.0.to_scale_rotation_translation();
// translation.y -= dt.0.as_secs_f32() * 100.;
// transform.0 = Mat4::from_scale_rotation_translation(scale, rotation, translation);
// }
}

12
kubi/src/color.rs Normal file
View file

@ -0,0 +1,12 @@
use glam::{Vec4, vec4};
#[inline(always)]
pub fn color_rgba(r: u8, g: u8, b: u8, a: u8) -> Vec4 {
vec4(r as f32 / 255., g as f32 / 255., b as f32 / 255., a as f32 / 255.)
}
#[inline(always)]
pub fn color_hex(c: u32) -> Vec4 {
let c = c.to_be_bytes();
color_rgba(c[0], c[1], c[2], c[3])
}

21
kubi/src/control_flow.rs Normal file
View file

@ -0,0 +1,21 @@
use shipyard::{UniqueView, UniqueViewMut, Unique, AllStoragesView};
use winit::{keyboard::KeyCode, event_loop::ControlFlow};
use crate::input::RawKbmInputState;
#[derive(Unique)]
pub struct RequestExit(pub bool);
pub fn exit_on_esc(
raw_inputs: UniqueView<RawKbmInputState>,
mut exit: UniqueViewMut<RequestExit>
) {
if raw_inputs.keyboard_state.contains(KeyCode::Escape as u32) {
exit.0 = true;
}
}
pub fn insert_control_flow_unique(
storages: AllStoragesView
) {
storages.add_unique(RequestExit(false))
}

58
kubi/src/cursor_lock.rs Normal file
View file

@ -0,0 +1,58 @@
use shipyard::{AllStoragesView, IntoIter, Unique, UniqueView, UniqueViewMut, View};
use crate::{events::InputDeviceEvent, rendering::Renderer};
use winit::{
dpi::PhysicalPosition, event::{DeviceEvent, ElementState, RawKeyEvent}, keyboard::{KeyCode, PhysicalKey}, window::CursorGrabMode
};
#[derive(Unique)]
pub struct CursorLock(pub bool);
pub fn update_cursor_lock_state(
lock: UniqueView<CursorLock>,
display: UniqueView<Renderer>
) {
if cfg!(target_os = "android") {
return
}
if lock.is_inserted_or_modified() {
//TODO MIGRATION
let window = display.window();
window.set_cursor_grab(match lock.0 {
true => CursorGrabMode::Confined,
false => CursorGrabMode::None,
}).expect("Failed to change cursor grab state");
window.set_cursor_visible(!lock.0);
}
}
pub fn insert_lock_state(
storages: AllStoragesView
) {
storages.add_unique(CursorLock(false))
}
pub fn lock_cursor_now(
mut lock: UniqueViewMut<CursorLock>
) {
lock.0 = true
}
/// XXX: this is a huge hack
pub fn debug_toggle_lock(
mut lock: UniqueViewMut<CursorLock>,
device_events: View<InputDeviceEvent>,
ren: UniqueView<Renderer>,
) {
for evt in device_events.iter() {
if let DeviceEvent::Key(RawKeyEvent {
physical_key: PhysicalKey::Code(KeyCode::F3),
state: ElementState::Pressed,
}) = evt.event {
lock.0 = !lock.0;
if !lock.0 {
let center = PhysicalPosition::new(ren.size().width as f64 / 2., ren.size().height as f64 / 2.);
let _ = ren.window().set_cursor_position(center);
}
}
}
}

12
kubi/src/delta_time.rs Normal file
View file

@ -0,0 +1,12 @@
use std::time::Duration;
use shipyard::{Unique, AllStoragesView};
#[derive(Unique, Default)]
pub(crate) struct DeltaTime(pub Duration);
pub fn init_delta_time(
storages: AllStoragesView
) {
storages.add_unique(DeltaTime::default())
}

106
kubi/src/events.rs Normal file
View file

@ -0,0 +1,106 @@
use glam::UVec2;
use shipyard::{World, Component, AllStoragesViewMut, SparseSet, NonSendSync, UniqueView};
use winit::event::{Event, DeviceEvent, DeviceId, WindowEvent, Touch};
use crate::rendering::Renderer;
pub mod player_actions;
#[derive(Component, Clone, Copy, Debug, Default)]
pub struct EventComponent;
#[derive(Component, Clone, Copy, Debug, Default)]
pub struct OnBeforeExitEvent;
#[derive(Component, Clone, Debug)]
pub struct InputDeviceEvent{
pub device_id: DeviceId,
pub event: DeviceEvent
}
#[derive(Component, Clone, Copy, Debug)]
#[repr(transparent)]
pub struct TouchEvent(pub Touch);
#[derive(Component, Clone, Copy, Debug, Default)]
pub struct WindowResizedEvent(pub UVec2);
pub fn process_winit_events(world: &mut World, event: &Event<()>) {
#[allow(clippy::collapsible_match, clippy::single_match)]
match event {
Event::WindowEvent { window_id: _, event } => match event {
WindowEvent::Resized(size) => {
world.add_entity((
EventComponent,
WindowResizedEvent(UVec2::new(size.width as _, size.height as _))
));
},
#[cfg(not(feature = "raw-evt"))]
WindowEvent::KeyboardInput { device_id, event, .. } => {
world.add_entity((
EventComponent,
InputDeviceEvent {
device_id: *device_id,
event: DeviceEvent::Key(winit::event::RawKeyEvent {
physical_key: event.physical_key,
state: event.state,
})
}
));
}
WindowEvent::Touch(touch) => {
// if matches!(touch.phase, TouchPhase::Started | TouchPhase::Cancelled | TouchPhase::Ended) {
// println!("TOUCH ==================== {:#?}", touch);
// } else {
// println!("TOUCH MOVED {:?} {}", touch.phase, touch.id);
// }
world.add_entity((
EventComponent,
TouchEvent(*touch)
));
}
_ => ()
},
#[cfg(feature = "raw-evt")]
Event::DeviceEvent { device_id, event } => {
world.add_entity((
EventComponent,
InputDeviceEvent {
device_id: *device_id,
event: event.clone()
}
));
},
Event::LoopExiting => {
world.add_entity((
EventComponent,
OnBeforeExitEvent
));
},
_ => (),
}
}
// pub fn initial_resize_event(
// mut storages: AllStoragesViewMut,
// ) {
// let (w, h) = {
// let renderer = storages.borrow::<UniqueView<Renderer>>().unwrap();
// (renderer.size().width, renderer.size().height)
// };
// storages.add_entity((
// EventComponent,
// WindowResizedEvent(UVec2::new(w, h))
// ));
// }
pub fn clear_events(
mut all_storages: AllStoragesViewMut,
) {
all_storages.delete_any::<SparseSet<EventComponent>>();
}

View file

@ -0,0 +1,39 @@
use shipyard::{Component, View, ViewMut, EntitiesViewMut, IntoIter, track};
use glam::{IVec3, Quat, Vec3};
use kubi_shared::block::Block;
use crate::{
client_physics::ClPhysicsActor, player::MainPlayer, transform::Transform
};
use super::EventComponent;
#[derive(Component, Clone, Copy, Debug)]
pub enum PlayerActionEvent {
PositionChanged {
position: Vec3,
//XXX: should this even be here?
velocity: Vec3,
direction: Quat
},
UpdatedBlock {
position: IVec3,
block: Block,
},
}
pub fn generate_move_events(
transforms: View<Transform, track::All>,
player: View<MainPlayer>,
actors: View<ClPhysicsActor>,
mut entities: EntitiesViewMut,
mut events: ViewMut<EventComponent>,
mut actions: ViewMut<PlayerActionEvent>,
) {
let Some((_, transform, actor)) = (&player, transforms.inserted_or_modified(), &actors).iter().next() else { return };
let (_, direction, position) = transform.0.to_scale_rotation_translation();
//HACK: if the actor is disabled, the velocity is irrelevant, so we just set it to zero.
let velocity = if actor.disable { Vec3::ZERO } else { actor.velocity };
entities.add_entity(
(&mut events, &mut actions),
(EventComponent, PlayerActionEvent::PositionChanged { position, velocity, direction })
);
}

29
kubi/src/filesystem.rs Normal file
View file

@ -0,0 +1,29 @@
use std::{fs::File, path::Path, io::{Read, Seek}};
use anyhow::Result;
use shipyard::{Unique, AllStoragesView};
pub trait ReadOnly: Read + Seek {}
impl<T: Read + Seek> ReadOnly for T {}
#[derive(Unique)]
pub struct AssetManager {
#[cfg(target_os = "android")]
pub(crate) app: android_activity::AndroidApp,
}
impl AssetManager {
pub fn open_asset(&self, path: &Path) -> Result<Box<dyn ReadOnly>> {
#[cfg(target_os = "android")] {
use anyhow::Context;
use std::ffi::CString;
let asset_manager = self.app.asset_manager();
let path_cstr = CString::new(path.to_string_lossy().as_bytes())?;
let handle = asset_manager.open(&path_cstr).context("Asset doesn't exist")?;
Ok(Box::new(handle))
}
#[cfg(not(target_os = "android"))] {
let asset_path = Path::new("./assets/").join(path);
Ok(Box::new(File::open(asset_path)?))
}
}
}

View file

@ -0,0 +1,45 @@
use shipyard::{Workload, WorkloadModificator, Unique, AllStoragesView, UniqueViewMut, IntoWorkload};
use hashbrown::HashMap;
use std::time::{Duration, Instant};
use nohash_hasher::BuildNoHashHasher;
#[derive(Unique)]
#[repr(transparent)]
struct FixedTimestampStorage(HashMap<u32, Instant, BuildNoHashHasher<u32>>);
impl FixedTimestampStorage {
pub fn new() -> Self {
Self(HashMap::with_capacity_and_hasher(16, BuildNoHashHasher::default()))
}
}
impl Default for FixedTimestampStorage {
fn default() -> Self {
Self::new()
}
}
pub trait FixedTimestamp {
fn make_fixed(self, rate_millis: u16, unique_id: u16) -> Self;
}
impl FixedTimestamp for Workload {
fn make_fixed(self, rate_millis: u16, unique_id: u16) -> Self {
let key = (rate_millis as u32) | ((unique_id as u32) << 16);
let duration = Duration::from_millis(rate_millis as u64);
(self,).into_workload().run_if(move |mut timestamps: UniqueViewMut<FixedTimestampStorage>| {
let Some(t) = timestamps.0.get_mut(&key) else {
timestamps.0.insert_unique_unchecked(key, Instant::now());
return true
};
if t.elapsed() >= duration {
*t = Instant::now();
return true
}
false
})
}
}
pub fn init_fixed_timestamp_storage(
storages: AllStoragesView
) {
storages.add_unique(FixedTimestampStorage::new());
}

View file

@ -0,0 +1,51 @@
use hui::UiInstance;
use hui_wgpu::WgpuUiRenderer;
//use hui_glium::GliumUiRenderer;
use shipyard::{AllStoragesView, Unique, UniqueView, NonSendSync, UniqueViewMut};
use crate::rendering::{RenderCtx, Renderer};
#[derive(Unique)]
pub struct UiState {
pub hui: UiInstance,
pub renderer: WgpuUiRenderer,
}
pub fn kubi_ui_init(
storages: AllStoragesView
) {
let renderer = storages.borrow::<UniqueView<Renderer>>().unwrap();
storages.add_unique_non_send_sync(UiState {
hui: UiInstance::new(),
renderer: WgpuUiRenderer::new(renderer.device(), renderer.surface_config().format),
});
}
pub fn kubi_ui_begin(
mut ui: NonSendSync<UniqueViewMut<UiState>>
) {
ui.hui.begin();
}
pub fn kubi_ui_end(
mut ui: NonSendSync<UniqueViewMut<UiState>>,
renderer: UniqueView<Renderer>,
) {
let ui: &mut UiState = &mut ui;
let UiState { hui, renderer: ui_renderer } = ui;
hui.end();
ui_renderer.update(hui, renderer.queue(), renderer.device(), renderer.size_vec2());
}
pub fn kubi_ui_draw(
ctx: &mut RenderCtx,
ui: NonSendSync<UniqueView<UiState>>,
) {
ui.renderer.draw(ctx.encoder, ctx.surface_view);
}
pub fn hui_process_winit_events(
event: &winit::event::Event<()>,
mut ui: NonSendSync<UniqueViewMut<UiState>>,
) {
hui_winit::handle_winit_event(&mut ui.hui, event);
}

38
kubi/src/init.rs Normal file
View file

@ -0,0 +1,38 @@
use shipyard::{AllStoragesView, UniqueViewMut};
use std::{env, net::SocketAddr, fs::OpenOptions, path::Path};
use anyhow::Result;
use crate::{
networking::{GameType, ServerAddress},
state::{GameState, NextState}
};
use kubi_shared::data::WorldSaveFile;
fn open_local_save_file(path: &Path) -> Result<WorldSaveFile> {
let mut save_file = WorldSaveFile::new({
OpenOptions::new()
.read(true)
.write(true)
.open("world.kbi")?
});
if save_file.file.metadata().unwrap().len() == 0 {
save_file.initialize()?;
} else {
save_file.load_data()?;
}
Ok(save_file)
}
pub fn initialize_from_args(
all_storages: AllStoragesView,
) {
let args: Vec<String> = env::args().collect();
if args.len() > 1 {
let address = args[1].parse::<SocketAddr>().expect("invalid address");
all_storages.add_unique(GameType::Muliplayer);
all_storages.add_unique(ServerAddress(address));
all_storages.borrow::<UniqueViewMut<NextState>>().unwrap().0 = Some(GameState::Connecting);
} else {
all_storages.add_unique(GameType::Singleplayer);
all_storages.borrow::<UniqueViewMut<NextState>>().unwrap().0 = Some(GameState::LoadingWorld);
}
}

301
kubi/src/input.rs Normal file
View file

@ -0,0 +1,301 @@
use gilrs::{Gilrs, GamepadId, Button, Event, Axis};
use glam::{Vec2, DVec2, vec2, dvec2};
use winit::{
keyboard::{KeyCode, PhysicalKey},
event::{DeviceEvent, DeviceId, ElementState, TouchPhase}
};
use hashbrown::HashMap;
use tinyset::{SetU32, SetU64};
use nohash_hasher::BuildNoHashHasher;
use shipyard::{AllStoragesView, Unique, View, IntoIter, UniqueViewMut, Workload, IntoWorkload, UniqueView, NonSendSync};
use crate::{
events::{InputDeviceEvent, TouchEvent},
rendering::Renderer,
};
#[derive(Unique, Clone, Copy, Default, Debug)]
pub struct Inputs {
pub movement: Vec2,
pub look: Vec2,
pub action_a: bool,
pub action_b: bool,
pub jump: bool,
}
#[derive(Unique, Clone, Copy, Default, Debug)]
pub struct PrevInputs(pub Inputs);
#[derive(Unique, Clone, Default, Debug)]
pub struct RawKbmInputState {
pub keyboard_state: SetU32,
pub button_state: [bool; 32],
pub mouse_delta: DVec2
}
#[derive(Clone, Copy, Debug, Default)]
pub enum FingerCheck {
#[default]
Start,
Current,
StartOrCurrent,
StartAndCurrent,
NotMoved,
}
#[derive(Clone, Copy, Debug)]
pub struct Finger {
pub id: u64,
pub device_id: DeviceId,
pub prev_position: DVec2,
pub start_position: DVec2,
pub current_position: DVec2,
pub has_moved: bool,
}
impl Finger {
pub fn within_area(&self, area_pos: DVec2, area_size: DVec2, check: FingerCheck) -> bool {
let within_area = |pos: DVec2| -> bool {
((pos - area_pos).min_element() >= 0.) &&
((pos - (area_pos + area_size)).max_element() <= 0.)
};
let start = within_area(self.start_position);
let current = within_area(self.current_position);
match check {
FingerCheck::Start => start,
FingerCheck::Current => current,
FingerCheck::StartOrCurrent => start || current,
FingerCheck::StartAndCurrent => start && current,
FingerCheck::NotMoved => current && !self.has_moved,
}
}
}
#[derive(Unique, Clone, Default, Debug)]
pub struct RawTouchState {
//TODO: handle multiple touch devices somehow
pub fingers: HashMap<u64, Finger, BuildNoHashHasher<u64>>
}
impl RawTouchState {
pub fn query_area(&self, area_pos: DVec2, area_size: DVec2, check: FingerCheck) -> impl Iterator<Item = Finger> + '_ {
self.fingers.iter().filter_map(move |(_, &finger)| {
finger.within_area(area_pos, area_size, check).then_some(finger)
})
}
}
#[derive(Unique)]
pub struct GilrsWrapper(Option<Gilrs>);
#[derive(Unique, Default, Clone, Copy)]
pub struct ActiveGamepad(Option<GamepadId>);
//maybe we should manage gamepad state ourselves just like keyboard?
//at least for the sake of consitency
fn process_events(
device_events: View<InputDeviceEvent>,
mut input_state: UniqueViewMut<RawKbmInputState>,
) {
input_state.mouse_delta = DVec2::ZERO;
for event in device_events.iter() {
match &event.event {
DeviceEvent::MouseMotion { delta } => {
input_state.mouse_delta = DVec2::from(*delta);
},
DeviceEvent::Key(input) => {
if let PhysicalKey::Code(code) = input.physical_key {
match input.state {
ElementState::Pressed => input_state.keyboard_state.insert(code as u32),
ElementState::Released => input_state.keyboard_state.remove(code as u32),
};
}
},
DeviceEvent::Button { button, state } => {
if *button < 32 {
input_state.button_state[*button as usize] = matches!(*state, ElementState::Pressed);
}
},
_ => ()
}
}
}
fn process_touch_events(
touch_events: View<TouchEvent>,
mut touch_state: UniqueViewMut<RawTouchState>,
) {
for (_, finger) in &mut touch_state.fingers {
finger.prev_position = finger.current_position;
}
for event in touch_events.iter() {
let position = dvec2(event.0.location.x, event.0.location.y);
match event.0.phase {
TouchPhase::Started => {
//println!("touch started: finger {}", event.0.id);
touch_state.fingers.insert(event.0.id, Finger {
id: event.0.id,
device_id: event.0.device_id,
start_position: position,
current_position: position,
prev_position: position,
has_moved: false
});
},
TouchPhase::Moved => {
if let Some(finger) = touch_state.fingers.get_mut(&event.0.id) {
finger.has_moved = true;
finger.current_position = position;
}
},
TouchPhase::Ended | TouchPhase::Cancelled => {
//println!("touch ended: finger {}", event.0.id);
touch_state.fingers.remove(&event.0.id);
},
}
}
}
fn process_gilrs_events(
mut gilrs: NonSendSync<UniqueViewMut<GilrsWrapper>>,
mut active_gamepad: UniqueViewMut<ActiveGamepad>
) {
if let Some(gilrs) = &mut gilrs.0 {
while let Some(Event { id, event: _, time: _ }) = gilrs.next_event() {
active_gamepad.0 = Some(id);
}
}
}
fn input_start(
mut inputs: UniqueViewMut<Inputs>,
mut prev_inputs: UniqueViewMut<PrevInputs>,
) {
prev_inputs.0 = *inputs;
*inputs = Inputs::default();
}
fn update_input_state (
raw_inputs: UniqueView<RawKbmInputState>,
mut inputs: UniqueViewMut<Inputs>,
) {
inputs.movement += Vec2::new(
raw_inputs.keyboard_state.contains(KeyCode::KeyD as u32) as u32 as f32 -
raw_inputs.keyboard_state.contains(KeyCode::KeyA as u32) as u32 as f32,
raw_inputs.keyboard_state.contains(KeyCode::KeyW as u32) as u32 as f32 -
raw_inputs.keyboard_state.contains(KeyCode::KeyS as u32) as u32 as f32
);
inputs.look += raw_inputs.mouse_delta.as_vec2();
inputs.action_a |= raw_inputs.button_state[0];
inputs.action_b |= raw_inputs.button_state[1];
inputs.jump |= raw_inputs.keyboard_state.contains(KeyCode::Space as u32);
}
fn update_input_state_gamepad (
gilrs: NonSendSync<UniqueView<GilrsWrapper>>,
active_gamepad: UniqueView<ActiveGamepad>,
mut inputs: UniqueViewMut<Inputs>,
) {
if let Some(gilrs) = &gilrs.0 {
if let Some(gamepad) = active_gamepad.0.map(|id| gilrs.gamepad(id)) {
let left_stick = vec2(gamepad.value(Axis::LeftStickX), gamepad.value(Axis::LeftStickY));
let right_stick = vec2(gamepad.value(Axis::RightStickX), -gamepad.value(Axis::RightStickY));
inputs.movement += left_stick;
//HACK: for now, we multiply look by 2 to make it feel more responsive
inputs.look += right_stick * 2.;
inputs.action_a |= gamepad.is_pressed(Button::West);
inputs.action_b |= gamepad.is_pressed(Button::East);
inputs.jump |= gamepad.is_pressed(Button::South);
}
}
}
fn update_input_state_touch (
touch_state: UniqueView<RawTouchState>,
renderer: UniqueView<Renderer>,
mut inputs: UniqueViewMut<Inputs>,
) {
let w = renderer.size_uvec2().as_dvec2();
//Movement
if let Some(finger) = touch_state.query_area(
dvec2(0., 0.),
dvec2(w.x / 2., w.y),
FingerCheck::Start
).next() {
inputs.movement += (((finger.current_position - finger.start_position) / (w.x / 4.)) * dvec2(1., -1.)).as_vec2();
}
//Action buttons
let action_button_fingers = {
let mut action_button_fingers = SetU64::new();
//Creates iterator of fingers that started within action button area
let action_finger_iter = || touch_state.query_area(
dvec2(w.x * 0.75, w.y * 0.666),
dvec2(w.x * 0.25, w.y * 0.333),
FingerCheck::Start
);
//Action button A
inputs.action_a |= action_finger_iter().filter(|finger| finger.within_area(
dvec2(w.x * (0.75 + 0.125), w.y * 0.666),
dvec2(w.x * 0.125, w.y * 0.333),
FingerCheck::StartOrCurrent
)).map(|x| action_button_fingers.insert(x.id)).next().is_some();
//Action button B
inputs.action_b |= action_finger_iter().filter(|finger| finger.within_area(
dvec2(w.x * 0.75, w.y * 0.666),
dvec2(w.x * 0.125, w.y * 0.333),
FingerCheck::StartOrCurrent
)).map(|x| action_button_fingers.insert(x.id)).next().is_some();
action_button_fingers
};
//Camera controls
if let Some(finger) = touch_state.query_area(
dvec2(w.x / 2., 0.),
dvec2(w.x / 2., w.y),
FingerCheck::Start
).find(|x| !action_button_fingers.contains(x.id)) {
inputs.look += (((finger.current_position - finger.prev_position) / (w.x / 4.)) * 300.).as_vec2();
}
}
fn input_end(
mut inputs: UniqueViewMut<Inputs>,
) {
if inputs.movement.length() >= 1. {
inputs.movement = inputs.movement.normalize();
}
}
pub fn init_input (
storages: AllStoragesView
) {
storages.add_unique_non_send_sync(GilrsWrapper(
Gilrs::new().map_err(|x| {
log::error!("Failed to initialize Gilrs");
x
}).ok()
));
storages.add_unique(ActiveGamepad::default());
storages.add_unique(Inputs::default());
storages.add_unique(PrevInputs::default());
storages.add_unique(RawKbmInputState::default());
storages.add_unique(RawTouchState::default());
}
pub fn process_inputs() -> Workload {
(
process_events,
process_touch_events,
process_gilrs_events,
input_start,
update_input_state,
update_input_state_touch,
update_input_state_gamepad,
input_end,
).into_sequential_workload()
}

336
kubi/src/lib.rs Normal file
View file

@ -0,0 +1,336 @@
//TODO move lints to workspace Cargo.toml
#![allow(
clippy::too_many_arguments, // allowed because systems often need a lot of argumentss
clippy::enum_variant_names,
clippy::type_complexity
)]
#![forbid(
static_mut_refs,
unsafe_op_in_unsafe_fn,
rust_2024_compatibility,
)]
use shipyard::{
World, Workload, IntoWorkload,
UniqueView, UniqueViewMut,
WorkloadModificator,
SystemModificator
};
use winit::{
event_loop::{EventLoop, ControlFlow},
event::{Event, WindowEvent}
};
use glam::vec3;
use std::time::Instant;
pub(crate) use kubi_shared::transform;
mod ui;
pub(crate) use ui::{
loading_screen,
connecting_screen,
chat_ui,
crosshair_ui,
settings_ui,
};
pub(crate) mod rendering;
pub(crate) mod world;
pub(crate) mod player;
pub(crate) mod prefabs;
pub(crate) mod settings;
pub(crate) mod camera;
pub(crate) mod events;
pub(crate) mod input;
pub(crate) mod player_controller;
pub(crate) mod block_placement;
pub(crate) mod delta_time;
pub(crate) mod cursor_lock;
pub(crate) mod control_flow;
pub(crate) mod state;
pub(crate) mod hui_integration;
pub(crate) mod networking;
pub(crate) mod init;
pub(crate) mod color;
pub(crate) mod fixed_timestamp;
pub(crate) mod filesystem;
pub(crate) mod client_physics;
pub(crate) mod chat;
use world::{
init_game_world,
loading::update_loaded_world_around_player,
raycast::update_raycasts,
queue::apply_queued_blocks,
tasks::ChunkTaskManager,
};
use player::{spawn_player, MainPlayer};
use prefabs::load_prefabs;
use settings::{load_settings, GameSettings};
use camera::compute_cameras;
use events::{clear_events, process_winit_events, player_actions::generate_move_events};
use input::{init_input, process_inputs};
use player_controller::{debug_switch_ctl_type, update_player_controllers};
use rendering::{BackgroundColor, Renderer, init_rendering, render_master, update_rendering_early, update_rendering_late};
use block_placement::update_block_placement;
use delta_time::{DeltaTime, init_delta_time};
use cursor_lock::{debug_toggle_lock, insert_lock_state, lock_cursor_now, update_cursor_lock_state};
use control_flow::{exit_on_esc, insert_control_flow_unique, RequestExit};
use state::{is_ingame, is_ingame_or_loading, is_loading, init_state, update_state, is_connecting};
use networking::{update_networking, update_networking_late, is_multiplayer, disconnect_on_exit, is_singleplayer};
use init::initialize_from_args;
use hui_integration::{kubi_ui_begin, /*kubi_ui_draw,*/ kubi_ui_end, kubi_ui_init};
use loading_screen::update_loading_screen;
use connecting_screen::update_connecting_screen;
use fixed_timestamp::init_fixed_timestamp_storage;
use filesystem::AssetManager;
use client_physics::{init_client_physics, update_client_physics_late};
use chat_ui::render_chat;
use chat::init_chat_manager;
use crosshair_ui::{init_crosshair_image, draw_crosshair};
use settings_ui::render_settings_ui;
use hui_integration::hui_process_winit_events;
/// stuff required to init the renderer and other basic systems
fn pre_startup() -> Workload {
(
load_settings,
).into_sequential_workload()
}
fn startup() -> Workload {
(
init_fixed_timestamp_storage,
kubi_ui_init,
load_prefabs,
init_rendering,
insert_lock_state,
init_state,
initialize_from_args,
lock_cursor_now,
init_input,
insert_control_flow_unique,
init_delta_time,
init_client_physics,
init_chat_manager,
init_crosshair_image,
).into_sequential_workload()
}
fn update() -> Workload {
(
update_rendering_early,
debug_toggle_lock,
update_cursor_lock_state,
process_inputs,
kubi_ui_begin,
(
init_game_world.run_if_missing_unique::<ChunkTaskManager>(),
(
spawn_player.run_if_storage_empty::<MainPlayer>(),
).into_sequential_workload().run_if(is_singleplayer),
).into_sequential_workload().run_if(is_ingame_or_loading),
update_networking().run_if(is_multiplayer),
(
update_connecting_screen,
).into_sequential_workload().run_if(is_connecting),
(
update_loading_screen,
).into_sequential_workload().run_if(is_loading),
(
update_loaded_world_around_player,
).into_sequential_workload().run_if(is_ingame_or_loading),
(
debug_switch_ctl_type,
update_player_controllers,
update_client_physics_late,
generate_move_events,
update_raycasts,
update_block_placement,
apply_queued_blocks,
//UI:
render_chat,
draw_crosshair,
render_settings_ui,
).into_sequential_workload().run_if(is_ingame),
update_networking_late.run_if(is_multiplayer),
compute_cameras,
kubi_ui_end,
update_state,
exit_on_esc,
disconnect_on_exit.run_if(is_multiplayer),
update_rendering_late,
).into_sequential_workload()
}
// fn render() -> Workload {
// (
// clear_background,
// (
// draw_world,
// draw_current_chunk_border,
// render_selection_box,
// render_entities,
// draw_world_trans,
// render_submerged_view,
// ).into_sequential_workload().run_if(is_ingame),
// kubi_ui_draw,
// ).into_sequential_workload()
// }
fn after_render() -> Workload {
(
clear_events,
).into_sequential_workload()
}
#[cfg(all(windows, not(debug_assertions)))]
fn attach_console() {
use winapi::um::wincon::{AttachConsole, ATTACH_PARENT_PROCESS};
unsafe { AttachConsole(ATTACH_PARENT_PROCESS); }
}
#[no_mangle]
#[cfg(target_os = "android")]
pub fn android_main(app: android_activity::AndroidApp) {
use android_activity::WindowManagerFlags;
app.set_window_flags(WindowManagerFlags::FULLSCREEN, WindowManagerFlags::empty());
kubi_main(app)
}
#[no_mangle]
pub fn kubi_main(
#[cfg(target_os = "android")]
app: android_activity::AndroidApp
) {
//Attach console on release builds on windows
#[cfg(all(windows, not(debug_assertions)))]
attach_console();
//Print version
println!("{:─^54}", format!("[ ▄▀ Kubi client v. {} ]", env!("CARGO_PKG_VERSION")));
//Init env_logger
kubi_logging::init();
//Create a shipyard world
let mut world = World::new();
//Init assman
world.add_unique(AssetManager {
#[cfg(target_os = "android")]
app: app.clone()
});
//Register workloads
world.add_workload(pre_startup);
world.add_workload(startup);
world.add_workload(update);
//world.add_workload(render);
world.add_workload(after_render);
//Save _visualizer.json
#[cfg(feature = "generate_visualizer_data")]
std::fs::write(
"_visualizer.json",
serde_json::to_string(&world.workloads_info()).unwrap(),
).unwrap();
//Run pre-startup procedure
world.run_workload(pre_startup).unwrap();
//Create event loop
let event_loop ={
#[cfg(not(target_os = "android"))] { EventLoop::new().unwrap() }
#[cfg(target_os = "android")] {
use winit::{
platform::android::EventLoopBuilderExtAndroid,
event_loop::EventLoopBuilder
};
EventLoopBuilder::new().with_android_app(app).build().unwrap()
}
};
//Run the event loop
let mut last_update = Instant::now();
let mut ready = false;
event_loop.run(move |event, window_target| {
//Wait for the window to become active (required for android)
if !ready {
if Event::Resumed != event {
window_target.set_control_flow(ControlFlow::Wait);
return
}
//Initialize renderer
{
let settings = world.borrow::<UniqueView<GameSettings>>().unwrap();
world.add_unique_non_send_sync(Renderer::init(window_target, &settings));
}
world.add_unique(BackgroundColor(vec3(0.21, 0.21, 1.)));
//Run startup systems
world.run_workload(startup).unwrap();
ready = true;
}
window_target.set_control_flow(ControlFlow::Poll);
world.run_with_data(hui_process_winit_events, &event);
process_winit_events(&mut world, &event);
#[allow(clippy::collapsible_match, clippy::single_match)]
match event {
#[cfg(target_os = "android")]
Event::Suspended => {
window_target.exit();
}
Event::WindowEvent { event, .. } => match event {
WindowEvent::CloseRequested => {
log::info!("exit requested");
window_target.exit();
},
_ => (),
},
Event::AboutToWait => {
//Update delta time (maybe move this into a system?)
{
let mut dt_view = world.borrow::<UniqueViewMut<DeltaTime>>().unwrap();
let now = Instant::now();
dt_view.0 = now - last_update;
last_update = now;
}
//Run update workflows
world.run_workload(update).unwrap();
world.run(render_master);
//Start rendering (maybe use custom views for this?)
// let target = {
// let renderer = world.borrow::<UniqueView<Renderer>>().unwrap();
// renderer.display.draw()
// };
// world.add_unique_non_send_sync(RenderTarget(target));
//Run render workflow
//world.run_workload(render).unwrap();
//Finish rendering
// let target = world.remove_unique::<RenderTarget>().unwrap();
// target.0.finish().unwrap();
//After frame end
world.run_workload(after_render).unwrap();
//Process control flow changes
if world.borrow::<UniqueView<RequestExit>>().unwrap().0 {
window_target.exit();
}
},
_ => (),
};
}).unwrap();
}

8
kubi/src/main.rs Normal file
View file

@ -0,0 +1,8 @@
#![cfg_attr(
all(windows, not(debug_assertions)),
windows_subsystem = "windows"
)]
fn main() {
kubilib::kubi_main();
}

202
kubi/src/networking.rs Normal file
View file

@ -0,0 +1,202 @@
use shipyard::{Unique, AllStoragesView, UniqueView, UniqueViewMut, Workload, IntoWorkload, EntitiesViewMut, Component, ViewMut, SystemModificator, View, IntoIter, WorkloadModificator};
use std::net::SocketAddr;
use uflow::{
client::{Client, Config as ClientConfig, Event as ClientEvent},
EndpointConfig
};
use kubi_shared::networking::{
messages::ServerToClientMessage,
state::ClientJoinState,
client::ClientIdMap,
};
use crate::{
events::EventComponent,
control_flow::RequestExit,
world::tasks::ChunkTaskManager,
state::is_ingame_or_loading,
fixed_timestamp::FixedTimestamp
};
mod handshake;
mod world;
mod player;
pub use handshake::ConnectionRejectionReason;
use handshake::{
set_client_join_state_to_connected,
say_hello,
check_server_hello_response,
check_server_fuck_off_response,
};
use world::{
inject_network_responses_into_manager_queue,
send_block_place_events,
recv_block_place_events,
};
use player::{
init_client_map,
send_player_movement_events,
receive_player_movement_events,
receive_player_connect_events,
receive_player_disconnect_events,
};
const NET_TICKRATE: u16 = 33;
#[derive(Unique, Clone, Copy, PartialEq, Eq)]
pub enum GameType {
Singleplayer,
Muliplayer
}
#[derive(Unique, Clone, Copy, PartialEq, Eq)]
pub struct ServerAddress(pub SocketAddr);
#[derive(Unique)]
pub struct UdpClient(pub Client);
#[derive(Component)]
pub struct NetworkEvent(pub ClientEvent);
impl NetworkEvent {
///Checks if postcard-encoded message has a type
pub fn is_message_of_type<const T: u8>(&self) -> bool {
let ClientEvent::Receive(data) = &self.0 else { return false };
if data.len() == 0 { return false }
data[0] == T
}
}
#[derive(Component)]
pub struct NetworkMessageEvent(pub ServerToClientMessage);
fn connect_client(
storages: AllStoragesView
) {
log::info!("Creating client");
let address = storages.borrow::<UniqueView<ServerAddress>>().unwrap();
let client = Client::connect(address.0, ClientConfig {
endpoint_config: EndpointConfig {
active_timeout_ms: 10000,
keepalive: true,
keepalive_interval_ms: 5000,
..Default::default()
},
}).expect("Client connection failed");
storages.add_unique(UdpClient(client));
storages.add_unique(ClientJoinState::Disconnected);
}
fn poll_client(
mut client: UniqueViewMut<UdpClient>,
mut entities: EntitiesViewMut,
mut events: ViewMut<EventComponent>,
mut network_events: ViewMut<NetworkEvent>,
) {
entities.bulk_add_entity((
&mut events,
&mut network_events,
), client.0.step().map(|event| {
(EventComponent, NetworkEvent(event))
}));
}
fn flush_client(
mut client: UniqueViewMut<UdpClient>,
) {
client.0.flush();
}
fn handle_disconnect(
network_events: View<NetworkEvent>,
mut join_state: UniqueViewMut<ClientJoinState>
) {
for event in network_events.iter() {
if matches!(event.0, ClientEvent::Disconnect) {
log::warn!("Disconnected from server");
*join_state = ClientJoinState::Disconnected;
return;
}
}
}
pub fn update_networking() -> Workload {
(
init_client_map.run_if_missing_unique::<ClientIdMap>(),
connect_client.run_if_missing_unique::<UdpClient>(),
poll_client.into_workload().make_fixed(NET_TICKRATE, 0),
(
set_client_join_state_to_connected,
say_hello,
).into_sequential_workload().run_if(if_just_connected),
(
check_server_hello_response,
check_server_fuck_off_response,
handle_disconnect,
).into_sequential_workload().run_if(is_join_state::<{ClientJoinState::Connected as u8}>),
(
(
receive_player_connect_events,
receive_player_disconnect_events,
).into_workload(),
(
recv_block_place_events,
receive_player_movement_events,
).into_workload()
).into_sequential_workload().run_if(is_join_state::<{ClientJoinState::Joined as u8}>).run_if(is_ingame_or_loading),
inject_network_responses_into_manager_queue.run_if(is_ingame_or_loading).skip_if_missing_unique::<ChunkTaskManager>(),
).into_sequential_workload()
}
pub fn update_networking_late() -> Workload {
(
(
send_block_place_events,
send_player_movement_events,
).into_workload().run_if(is_join_state::<{ClientJoinState::Joined as u8}>),
flush_client.into_workload().make_fixed(NET_TICKRATE, 1)
).into_sequential_workload()
}
pub fn disconnect_on_exit(
exit: UniqueView<RequestExit>,
mut client: UniqueViewMut<UdpClient>,
) {
//TODO check if this works
if exit.0 {
if client.0.is_active() {
client.0.flush();
client.0.disconnect();
while client.0.is_active() { client.0.step().for_each(|_|()); }
log::info!("Client disconnected");
} else {
log::info!("Client inactive")
}
}
}
// conditions
fn if_just_connected(
network_events: View<NetworkEvent>,
) -> bool {
network_events.iter().any(|event| matches!(&event.0, ClientEvent::Connect))
}
fn is_join_state<const STATE: u8>(
join_state: UniqueView<ClientJoinState>
) -> bool {
(*join_state as u8) == STATE
}
pub fn is_multiplayer(
game_type: UniqueView<GameType>
) -> bool {
*game_type == GameType::Muliplayer
}
pub fn is_singleplayer(
game_type: UniqueView<GameType>
) -> bool {
*game_type == GameType::Singleplayer
}

View file

@ -0,0 +1,131 @@
use shipyard::{AllStoragesView, AllStoragesViewMut, IntoIter, Unique, UniqueViewMut, View};
use uflow::{client::Event as ClientEvent, SendMode};
use kubi_shared::networking::{
messages::{ClientToServerMessage, ServerToClientMessage, ServerToClientMessageType},
state::ClientJoinState,
channels::Channel,
};
use rand::prelude::*;
use crate::{chat::ChatHistory, player::{spawn_local_player_multiplayer, spawn_remote_player_multiplayer}};
use super::{UdpClient, NetworkEvent};
const USERNAME_BANK: &[&str] = &[
"XxX-FishFucker-69420",
"Sbeve34",
"ShadowBladeX",
"CyberNinja92",
"sputnik1",
"dumbpotato",
"FortNiteNinja",
"MinecraftMiner",
];
#[derive(Unique)]
pub struct ConnectionRejectionReason {
pub reason: String,
}
pub fn set_client_join_state_to_connected(
mut join_state: UniqueViewMut<ClientJoinState>
) {
log::info!("Setting ClientJoinState");
*join_state = ClientJoinState::Connected;
}
pub fn say_hello(
mut client: UniqueViewMut<UdpClient>,
) {
let mut rng = thread_rng();
let username = (*USERNAME_BANK.choose(&mut rng).unwrap()).to_owned();
let password = None;
log::info!("Authenticating");
client.0.send(
postcard::to_allocvec(
&ClientToServerMessage::ClientHello { username, password }
).unwrap().into_boxed_slice(),
Channel::Auth as usize,
SendMode::Reliable
);
}
pub fn check_server_hello_response(
mut storages: AllStoragesViewMut,
) {
//Check if we got the message and extract the init data from it
let Some(init) = storages.borrow::<View<NetworkEvent>>().unwrap().iter().find_map(|event| {
let ClientEvent::Receive(data) = &event.0 else {
return None
};
if !event.is_message_of_type::<{ServerToClientMessageType::ServerHello as u8}>() {
return None
}
let Ok(parsed_message) = postcard::from_bytes(data) else {
log::error!("Malformed message");
return None
};
let ServerToClientMessage::ServerHello { init } = parsed_message else {
unreachable!()
};
Some(init)
}) else { return };
// struct ClientInitData {
// client_id: ClientId,
// username: String,
// position: Vec3,
// velocity: Vec3,
// direction: Quat,
// health: Health,
// }
let client_id = init.user.client_id;
let username = init.user.username.clone();
//Add components to main player
spawn_local_player_multiplayer(&mut storages, init.user);
//Init players
for init_data in init.users {
spawn_remote_player_multiplayer(&mut storages, init_data);
}
// Set state to connected
let mut join_state = storages.borrow::<UniqueViewMut<ClientJoinState>>().unwrap();
*join_state = ClientJoinState::Joined;
log::info!("Joined the server!");
// Send chat message
let mut chat = storages.borrow::<UniqueViewMut<ChatHistory>>().unwrap();
chat.add_player_join(client_id, username);
}
pub fn check_server_fuck_off_response(
storages: AllStoragesView,
) {
//Check if we got the message and extract the init data from it
let Some(reason) = storages.borrow::<View<NetworkEvent>>().unwrap().iter().find_map(|event| {
let ClientEvent::Receive(data) = &event.0 else {
return None
};
if !event.is_message_of_type::<{ServerToClientMessageType::ServerFuckOff as u8}>() {
return None
}
let Ok(parsed_message) = postcard::from_bytes(data) else {
log::error!("Malformed message");
return None
};
let ServerToClientMessage::ServerFuckOff { reason } = parsed_message else {
unreachable!()
};
Some(reason)
}) else { return };
let mut client = storages.borrow::<UniqueViewMut<UdpClient>>().unwrap();
client.0.disconnect_now();
let mut join_state = storages.borrow::<UniqueViewMut<ClientJoinState>>().unwrap();
*join_state = ClientJoinState::Disconnected;
storages.add_unique(ConnectionRejectionReason { reason });
}

View file

@ -0,0 +1,146 @@
use glam::{Vec3, Mat4};
use shipyard::{UniqueViewMut, View, IntoIter, AllStoragesView, AllStoragesViewMut, UniqueView, ViewMut, Get};
use uflow::{SendMode, client::Event as ClientEvent};
use kubi_shared::{
transform::Transform,
networking::{
channels::Channel,
client::{ClientIdMap, Username},
messages::{ClientToServerMessage, ServerToClientMessage, ServerToClientMessageType},
},
};
use crate::{
chat::ChatHistory,
events::player_actions::PlayerActionEvent,
player::spawn_remote_player_multiplayer,
};
use super::{UdpClient, NetworkEvent};
pub fn init_client_map(
storages: AllStoragesView,
) {
storages.add_unique(ClientIdMap::new());
}
pub fn send_player_movement_events(
actions: View<PlayerActionEvent>,
mut client: UniqueViewMut<UdpClient>,
) {
for event in actions.iter() {
let PlayerActionEvent::PositionChanged { position, velocity, direction } = event else {
continue
};
client.0.send(
postcard::to_allocvec(&ClientToServerMessage::PositionChanged {
position: *position,
velocity: *velocity,
direction: *direction
}).unwrap().into_boxed_slice(),
Channel::Move as usize,
SendMode::TimeSensitive
);
}
}
pub fn receive_player_movement_events(
mut transforms: ViewMut<Transform>,
network_events: View<NetworkEvent>,
id_map: UniqueView<ClientIdMap>
) {
for event in network_events.iter() {
let ClientEvent::Receive(data) = &event.0 else {
continue
};
if !event.is_message_of_type::<{ServerToClientMessageType::PlayerPositionChanged as u8}>() {
continue
}
let Ok(parsed_message) = postcard::from_bytes(data) else {
log::error!("Malformed message");
continue
};
let ServerToClientMessage::PlayerPositionChanged {
client_id, position, direction
} = parsed_message else { unreachable!() };
let Some(&ent_id) = id_map.0.get(&client_id) else {
log::error!("Not in client-id map");
continue
};
let mut transform = (&mut transforms).get(ent_id)
.expect("invalid player entity id");
transform.0 = Mat4::from_rotation_translation(direction, position);
}
}
pub fn receive_player_connect_events(
mut storages: AllStoragesViewMut,
) {
let messages: Vec<ServerToClientMessage> = storages.borrow::<View<NetworkEvent>>().unwrap().iter().filter_map(|event| {
let ClientEvent::Receive(data) = &event.0 else {
return None
};
if !event.is_message_of_type::<{ServerToClientMessageType::PlayerConnected as u8}>() {
return None
};
let Ok(parsed_message) = postcard::from_bytes(data) else {
log::error!("Malformed message");
return None
};
Some(parsed_message)
}).collect();
for message in messages {
let ServerToClientMessage::PlayerConnected { init } = message else { unreachable!() };
log::info!("player connected: {} (id {})", init.username, init.client_id);
let mut chat = storages.borrow::<UniqueViewMut<ChatHistory>>().unwrap();
chat.add_player_join(init.client_id, init.username.clone());
drop(chat);
spawn_remote_player_multiplayer(&mut storages, init);
}
}
pub fn receive_player_disconnect_events(
mut storages: AllStoragesViewMut,
) {
let messages: Vec<ServerToClientMessage> = storages.borrow::<View<NetworkEvent>>().unwrap().iter().filter_map(|event| {
let ClientEvent::Receive(data) = &event.0 else {
return None
};
if !event.is_message_of_type::<{ServerToClientMessageType::PlayerDisconnected as u8}>() {
return None
};
let Ok(parsed_message) = postcard::from_bytes(data) else {
log::error!("Malformed message");
return None
};
Some(parsed_message)
}).collect();
for message in messages {
let ServerToClientMessage::PlayerDisconnected { id } = message else { unreachable!() };
log::info!("player disconnected: {}", id);
let mut id_map = storages.borrow::<UniqueViewMut<ClientIdMap>>().unwrap();
let Some(ent_id) = id_map.0.remove(&id) else {
log::warn!("Disconnected player entity not found in client-id map");
continue
};
let username = storages.get::<&Username>(ent_id).unwrap();
let mut chat = storages.borrow::<UniqueViewMut<ChatHistory>>().unwrap();
chat.add_player_leave(id, username.0.to_string());
drop(chat);
drop(id_map);
drop(username);
if !storages.delete_entity(ent_id) {
log::warn!("Disconnected player entity not found in storage");
}
}
}

View file

@ -0,0 +1,91 @@
use shipyard::{UniqueView, UniqueViewMut, View, IntoIter};
use uflow::{client::Event as ClientEvent, SendMode};
use lz4_flex::decompress_size_prepended;
use anyhow::{Result, Context};
use kubi_shared::{
networking::{
messages::{ClientToServerMessage, ServerToClientMessage, ServerToClientMessageType},
channels::Channel,
},
queue::QueuedBlock
};
use crate::{
events::player_actions::PlayerActionEvent,
world::{
tasks::{ChunkTaskResponse, ChunkTaskManager},
queue::BlockUpdateQueue
},
};
use super::{NetworkEvent, UdpClient};
//TODO multithreaded decompression
fn decompress_chunk_packet(data: &[u8]) -> Result<ServerToClientMessage> {
let mut decompressed = decompress_size_prepended(&data[1..])?;
decompressed.insert(0, data[0]);
postcard::from_bytes(&decompressed).ok().context("Deserialization failed")
}
//TODO get rid of this, this is awfulll
pub fn inject_network_responses_into_manager_queue(
manager: UniqueView<ChunkTaskManager>,
events: View<NetworkEvent>
) {
for event in events.iter() {
if event.is_message_of_type::<{ServerToClientMessageType::ChunkResponse as u8}>() {
let NetworkEvent(ClientEvent::Receive(data)) = &event else { unreachable!() };
let packet = decompress_chunk_packet(data).expect("Chunk decode failed");
let ServerToClientMessage::ChunkResponse {
chunk, data, queued
} = packet else { unreachable!() };
manager.add_sussy_response(ChunkTaskResponse::LoadedChunk {
position: chunk,
chunk_data: data,
queued
});
}
}
}
pub fn send_block_place_events(
action_events: View<PlayerActionEvent>,
mut client: UniqueViewMut<UdpClient>,
) {
for event in action_events.iter() {
let PlayerActionEvent::UpdatedBlock { position, block } = event else {
continue
};
client.0.send(
postcard::to_allocvec(&ClientToServerMessage::QueueBlock {
item: QueuedBlock {
position: *position,
block_type: *block,
soft: false
}
}).unwrap().into_boxed_slice(),
Channel::Block as usize,
SendMode::Reliable,
);
}
}
pub fn recv_block_place_events(
mut queue: UniqueViewMut<BlockUpdateQueue>,
network_events: View<NetworkEvent>,
) {
for event in network_events.iter() {
let ClientEvent::Receive(data) = &event.0 else {
continue
};
if !event.is_message_of_type::<{ServerToClientMessageType::QueueBlock as u8}>() {
continue
}
let Ok(parsed_message) = postcard::from_bytes(data) else {
log::error!("Malformed message");
continue
};
let ServerToClientMessage::QueueBlock { item } = parsed_message else {
unreachable!()
};
queue.0.push(item);
}
}

89
kubi/src/player.rs Normal file
View file

@ -0,0 +1,89 @@
use glam::Mat4;
use shipyard::{Component, AllStoragesViewMut, UniqueViewMut};
use kubi_shared::{
entity::{Entity, Health},
player::{Player, PLAYER_HEALTH, PlayerHolding},
block::Block,
networking::{
client::{Username, Client, ClientIdMap},
messages::ClientInitData
}
};
use crate::{
camera::Camera,
client_physics::ClPhysicsActor,
player_controller::PlayerController,
transform::Transform,
world::raycast::LookingAtBlock
};
#[derive(Component)]
pub struct MainPlayer;
pub fn spawn_player (
mut storages: AllStoragesViewMut,
) {
log::info!("spawning player");
storages.add_entity(((
Player,
MainPlayer,
Entity,
Health::new(PLAYER_HEALTH),
Transform::default(),
Camera::default(),
PlayerController::DEFAULT_FPS_CTL,
LookingAtBlock::default(),
PlayerHolding(Some(Block::Cobblestone)),
Username("LocalPlayer".into()),
),(
ClPhysicsActor::default(),
)));
}
pub fn spawn_local_player_multiplayer (
storages: &mut AllStoragesViewMut,
init: ClientInitData
) {
log::info!("spawning local multiplayer player");
let entity_id = storages.add_entity(((
Player,
Client(init.client_id),
MainPlayer,
Entity,
init.health,
Transform(Mat4::from_rotation_translation(init.direction, init.position)),
Camera::default(),
PlayerController::DEFAULT_FPS_CTL,
LookingAtBlock::default(),
PlayerHolding::default(),
),(
Username(init.username),
ClPhysicsActor::default(),
)));
//Add ourself to the client id map
let mut client_id_map = storages.borrow::<UniqueViewMut<ClientIdMap>>().unwrap();
client_id_map.0.insert(init.client_id, entity_id);
}
pub fn spawn_remote_player_multiplayer(
storages: &mut AllStoragesViewMut,
init: ClientInitData
) {
log::info!("spawning remote multiplayer player");
//Spawn player locally
let entity_id = storages.add_entity((
Username(init.username),
Client(init.client_id),
Player,
Entity,
init.health,
Transform(Mat4::from_rotation_translation(init.direction, init.position)),
PlayerHolding::default(),
));
//Add it to the client id map
let mut client_id_map = storages.borrow::<UniqueViewMut<ClientIdMap>>().unwrap();
client_id_map.0.insert(init.client_id, entity_id);
}

View file

@ -0,0 +1,133 @@
use glam::{vec3, EulerRot, Mat4, Quat, Vec2, Vec2Swizzles, Vec3, Vec3Swizzles};
use shipyard::{track, Component, Get, IntoIter, IntoWithId, IntoWorkload, Unique, UniqueView, View, ViewMut, Workload};
use winit::keyboard::KeyCode;
use std::f32::consts::PI;
use crate::{client_physics::ClPhysicsActor, cursor_lock::CursorLock, delta_time::DeltaTime, input::{Inputs, PrevInputs, RawKbmInputState}, settings::GameSettings, transform::Transform};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum PlayerControllerType {
FlyCam,
FpsCtl,
}
#[derive(Component)]
pub struct PlayerController {
pub control_type: PlayerControllerType,
pub speed: f32,
}
impl PlayerController {
pub const DEFAULT_FLY_CAM: Self = Self {
control_type: PlayerControllerType::FlyCam,
speed: 50.,
};
pub const DEFAULT_FPS_CTL: Self = Self {
control_type: PlayerControllerType::FpsCtl,
speed: 10.,
};
}
pub fn update_player_controllers() -> Workload {
(
update_look,
update_movement
).into_sequential_workload()
}
const MAX_PITCH: f32 = PI/2. - 0.05;
fn update_look(
controllers: View<PlayerController>,
mut transforms: ViewMut<Transform, track::All>,
inputs: UniqueView<Inputs>,
settings: UniqueView<GameSettings>,
dt: UniqueView<DeltaTime>,
lock: UniqueView<CursorLock>,
) {
//Only update if the cursor is locked
if !lock.0 { return }
let look = inputs.look * settings.mouse_sensitivity * dt.0.as_secs_f32();
if look == Vec2::ZERO { return }
for (_, mut transform) in (&controllers, &mut transforms).iter() {
let (scale, mut rotation, translation) = transform.0.to_scale_rotation_translation();
let (mut yaw, mut pitch, _roll) = rotation.to_euler(EulerRot::YXZ);
yaw -= look.x;
pitch -= look.y;
pitch = pitch.clamp(-MAX_PITCH, MAX_PITCH);
rotation = Quat::from_euler(EulerRot::YXZ, yaw, pitch, 0.).normalize();
transform.0 = Mat4::from_scale_rotation_translation(scale, rotation, translation);
}
}
fn update_movement(
controllers: View<PlayerController>,
mut transforms: ViewMut<Transform, track::All>,
mut actors: ViewMut<ClPhysicsActor>,
inputs: UniqueView<Inputs>,
prev_inputs: UniqueView<PrevInputs>,
dt: UniqueView<DeltaTime>,
) {
let jump = inputs.jump && !prev_inputs.0.jump;
if (inputs.movement == Vec2::ZERO) && !jump { return }
let movement = inputs.movement.extend(jump as u32 as f32).xzy();
for (id, (ctl, mut transform)) in (&controllers, &mut transforms).iter().with_id() {
let (scale, rotation, mut translation) = transform.0.to_scale_rotation_translation();
let rotation_norm = rotation.normalize();
match ctl.control_type {
PlayerControllerType::FlyCam => {
translation += (rotation_norm * Vec3::NEG_Z).normalize() * movement.z * ctl.speed * dt.0.as_secs_f32();
translation += (rotation_norm * Vec3::X).normalize() * movement.x * ctl.speed * dt.0.as_secs_f32();
translation += Vec3::Y * movement.y * ctl.speed * dt.0.as_secs_f32();
transform.0 = Mat4::from_scale_rotation_translation(scale, rotation_norm, translation);
},
PlayerControllerType::FpsCtl => {
let mut actor = (&mut actors).get(id).unwrap();
let actor_on_ground = actor.on_ground();
let euler = rotation_norm.to_euler(EulerRot::YZX);
let right = Vec2::from_angle(-euler.0).extend(0.).xzy();
let forward = Vec2::from_angle(-(euler.0 + PI/2.)).extend(0.).xzy();
//TODO: remove hardcoded jump force
// actor.apply_constant_force(ctl.speed * (
// (forward * movement.z) +
// (right * movement.x)
// ));
actor.apply_force(
ctl.speed * (
(forward * movement.z) +
(right * movement.x)
) +
Vec3::Y * movement.y * 1250. * (actor_on_ground as u8 as f32)
);
// actor.decel =
// (right * (1. - inputs.movement.x.abs()) * 10.) +
// (forward * (1. - inputs.movement.y.abs()) * 10.);
// translation += forward * movement.z * ctl.speed * dt.0.as_secs_f32();
// translation += right * movement.x * ctl.speed * dt.0.as_secs_f32();
// translation += Vec3::Y * movement.y * ctl.speed * dt.0.as_secs_f32();
// transform.0 = Mat4::from_scale_rotation_translation(scale, rotation_norm, translation);
}
}
}
}
pub fn debug_switch_ctl_type(
mut controllers: ViewMut<PlayerController>,
mut actors: ViewMut<ClPhysicsActor>,
kbm_state: UniqueView<RawKbmInputState>,
) {
for (mut controller, mut actor) in (&mut controllers, &mut actors).iter() {
if kbm_state.keyboard_state.contains(KeyCode::F4 as u32) {
*controller = PlayerController::DEFAULT_FPS_CTL;
actor.disable = false;
} else if kbm_state.keyboard_state.contains(KeyCode::F5 as u32) {
*controller = PlayerController::DEFAULT_FLY_CAM;
actor.disable = true;
}
}
}

245
kubi/src/prefabs.rs Normal file
View file

@ -0,0 +1,245 @@
use std::{io::{BufReader, Read}, path::{Path, PathBuf}};
use bytemuck::{Pod, Zeroable};
use hui::text::FontHandle;
use shipyard::{AllStoragesView, NonSendSync, Unique, UniqueView, UniqueViewMut};
use kubi_shared::block::BlockTexture;
use crate::{filesystem::AssetManager, hui_integration::UiState, rendering::{BufferPair, Renderer}};
//TODO move to rendering module
mod loader;
use loader::{load_texture2darray_prefab, load_texture2d_prefab, load_obj_prefab};
#[derive(Clone, Copy, Default, Pod, Zeroable)]
#[repr(C, packed)]
pub struct ModelVertex {
pub tex_coords: [f32; 2],
pub position: [f32; 3],
pub _padding: u32,
pub normal: [f32; 3],
}
impl ModelVertex {
pub const LAYOUT: wgpu::VertexBufferLayout<'static> = wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<ModelVertex>() as wgpu::BufferAddress,
step_mode: wgpu::VertexStepMode::Vertex,
attributes: &wgpu::vertex_attr_array![
0 => Float32x2,
1 => Float32x3,
2 => Float32x3,
],
};
}
pub trait AssetPaths {
fn file_name(self) -> &'static str;
}
impl AssetPaths for BlockTexture {
fn file_name(self) -> &'static str {
match self {
Self::Stone => "stone.png",
Self::Dirt => "dirt.png",
Self::GrassTop => "grass_top.png",
Self::GrassSide => "grass_side.png",
Self::Sand => "sand.png",
Self::Bedrock => "bedrock.png",
Self::Wood => "wood.png",
Self::WoodTop => "wood_top.png",
Self::Leaf => "leaf.png",
Self::Torch => "torch.png",
Self::TallGrass => "tall_grass.png",
Self::Snow => "snow.png",
Self::GrassSideSnow => "grass_side_snow.png",
Self::Cobblestone => "cobblestone.png",
Self::Planks => "planks.png",
Self::WaterSolid => "solid_water.png",
Self::Water => "water.png",
}
}
}
#[derive(Unique)]
pub struct GpuPrefabs {
pub block_diffuse_texture: wgpu::Texture,
pub block_diffuse_bind_group_layout: wgpu::BindGroupLayout,
pub block_diffuse_bind_group: wgpu::BindGroup,
pub player_model_diffuse_texture: wgpu::Texture,
pub player_model_diffuse_bind_group_layout: wgpu::BindGroupLayout,
pub player_model_diffuse_bind_group: wgpu::BindGroup,
pub player_model: BufferPair,
}
#[derive(Unique)]
#[repr(transparent)]
pub struct UiFontPrefab(pub FontHandle);
pub fn load_prefabs(
storages: AllStoragesView,
renderer: UniqueView<Renderer>,
mut ui: NonSendSync<UniqueViewMut<UiState>>,
assman: UniqueView<AssetManager>
) {
log::info!("Loading textures...");
let block_diffuse_texture = load_texture2darray_prefab::<BlockTexture>(
&renderer,
&assman,
"blocks".into(),
);
log::info!("Creating bing groups");
let block_diffuse_view = block_diffuse_texture.create_view(&wgpu::TextureViewDescriptor {
label: Some("block_texture_view"),
..Default::default()
});
let block_diffuse_sampler = renderer.device().create_sampler(&wgpu::SamplerDescriptor {
label: Some("block_diffuse_sampler"),
address_mode_u: wgpu::AddressMode::ClampToEdge,
address_mode_v: wgpu::AddressMode::ClampToEdge,
address_mode_w: wgpu::AddressMode::ClampToEdge,
mag_filter: wgpu::FilterMode::Nearest,
min_filter: wgpu::FilterMode::Linear,
mipmap_filter: wgpu::FilterMode::Nearest,
..Default::default()
});
let block_diffuse_bind_group_layout = renderer.device()
.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("block_diffuse_bind_group_layout"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: wgpu::TextureViewDimension::D2Array,
multisampled: false,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
}
]
});
let block_diffuse_bind_group = renderer.device().create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("block_diffuse_bind_group"),
layout: &block_diffuse_bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(&block_diffuse_view),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(&block_diffuse_sampler),
}
]
});
let player_model_diffuse_texture = load_texture2d_prefab(&renderer, &assman, &PathBuf::from("playermodel1.png"));
let player_model_diffuse_view = player_model_diffuse_texture.create_view(&wgpu::TextureViewDescriptor {
label: Some("player_model_texture_view"),
..Default::default()
});
let player_model_diffuse_sampler = renderer.device().create_sampler(&wgpu::SamplerDescriptor {
label: Some("player_model_sampler"),
address_mode_u: wgpu::AddressMode::ClampToEdge,
address_mode_v: wgpu::AddressMode::ClampToEdge,
address_mode_w: wgpu::AddressMode::ClampToEdge,
mag_filter: wgpu::FilterMode::Linear,
min_filter: wgpu::FilterMode::Linear,
mipmap_filter: wgpu::FilterMode::Nearest,
..Default::default()
});
let player_model_diffuse_bind_group_layout = renderer.device()
.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("player_model_bind_group_layout"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
}
]
});
let player_model_diffuse_bind_group = renderer.device().create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("player_model_bind_group"),
layout: &player_model_diffuse_bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(&player_model_diffuse_view),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(&player_model_diffuse_sampler),
}
]
});
let player_model = load_obj_prefab(&renderer, &assman, &PathBuf::from("playermodel1.obj"));
storages.add_unique_non_send_sync(GpuPrefabs {
block_diffuse_texture,
block_diffuse_bind_group_layout,
block_diffuse_bind_group,
player_model_diffuse_texture,
player_model_diffuse_bind_group_layout,
player_model_diffuse_bind_group,
player_model,
});
log::info!("Loading the UI stuff...");
{
let asset_handle = assman.open_asset(Path::new("fonts/Crisp.ttf")).unwrap();
let mut font_data = vec![];
BufReader::new(asset_handle).read_to_end(&mut font_data).unwrap();
let font_handle = ui.hui.add_font(&font_data);
ui.hui.push_font(font_handle);
storages.add_unique(UiFontPrefab(font_handle));
}
//log::info!("Compiling shaders...");
// storages.add_unique_non_send_sync(ChunkShaderPrefab(
// include_shader_prefab!(
// "world",
// "../shaders/world.vert",
// "../shaders/world.frag",
// &renderer.display
// )
// ));
// storages.add_unique_non_send_sync(ColoredShaderPrefab(
// include_shader_prefab!(
// "colored",
// "../shaders/colored.vert",
// "../shaders/colored.frag",
// &renderer.display
// )
// ));
// storages.add_unique_non_send_sync(Colored2ShaderPrefab(
// include_shader_prefab!(
// "colored",
// "../shaders/colored2.vert",
// "../shaders/colored2.frag",
// &renderer.display
// )
// ));
//log::info!("releasing shader compiler");
//renderer.display.release_shader_compiler();
}

168
kubi/src/prefabs/loader.rs Normal file
View file

@ -0,0 +1,168 @@
use glam::UVec2;
use strum::IntoEnumIterator;
use rayon::prelude::*;
use wgpu::util::{DeviceExt, TextureDataOrder};
use std::{io::{BufReader, Read}, path::{Path, PathBuf}};
use crate::{filesystem::AssetManager, prefabs::ModelVertex, rendering::{BufferPair, Renderer}};
use super::AssetPaths;
pub fn load_texture2darray_prefab<T: AssetPaths + IntoEnumIterator>(
renderer: &Renderer,
assman: &AssetManager,
directory: PathBuf,
) -> wgpu::Texture {
log::info!("started loading {}", directory.as_os_str().to_str().unwrap());
//Load raw images
let tex_files: Vec<&'static str> = T::iter().map(|x| x.file_name()).collect();
let raw_images: Vec<(Vec<u8>, UVec2)> = tex_files.par_iter().map(|&file_name| {
log::info!("loading texture {}", file_name);
//Get path to the image and open the file
let reader = {
let path = directory.join(file_name);
BufReader::new(assman.open_asset(&path).expect("Failed to open texture file"))
};
//Parse image data
let (image_data, dimensions) = {
let image = image::load(
reader,
image::ImageFormat::Png
).unwrap().to_rgba8();
let dimensions = image.dimensions();
(image.into_raw(), dimensions)
};
(image_data, UVec2::from(dimensions))
}).collect();
assert!(!raw_images.is_empty(), "no images loaded");
//TODO: check same size
log::info!("done loading texture files, uploading to the gpu");
let size = raw_images[0].1;
let layers = raw_images.len() as u32;
//Concat data into a single vec
let mut data = Vec::with_capacity((size.x * size.y * layers * 4) as usize);
for (layer_data, _) in raw_images {
data.extend_from_slice(&layer_data);
}
//Upload images to the GPU
let label = format!("texture2darray_prefab_{}", directory.as_os_str().to_str().unwrap());
let desc = &wgpu::TextureDescriptor {
label: Some(&label),
size: wgpu::Extent3d {
width: size.x,
height: size.y,
depth_or_array_layers: layers,
},
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rgba8UnormSrgb,
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
mip_level_count: 1,
sample_count: 1,
view_formats: &[],
};
renderer.device().create_texture_with_data(
renderer.queue(),
desc,
TextureDataOrder::MipMajor,
&data
)
}
pub fn load_texture2d_prefab(
renderer: &Renderer,
assman: &AssetManager,
path: &Path,
) -> wgpu::Texture {
log::info!("loading texture2d: {path:?}");
let image = image::load(
BufReader::new(assman.open_asset(path).expect("Failed to open texture file")),
image::ImageFormat::Png
).unwrap().to_rgba8();
let size = image.dimensions();
let data = image.into_raw();
let label = format!("texture2d_prefab_{}", path.file_name().unwrap().to_str().unwrap());
let desc = wgpu::TextureDescriptor {
label: Some(&label),
size: wgpu::Extent3d {
width: size.0,
height: size.1,
depth_or_array_layers: 1,
},
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rgba8UnormSrgb,
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
mip_level_count: 1,
sample_count: 1,
view_formats: &[],
};
renderer.device().create_texture_with_data(
renderer.queue(),
&desc,
TextureDataOrder::MipMajor,
&data
)
}
pub fn load_obj_prefab(
renderer: &Renderer,
assman: &AssetManager,
path: &Path,
) -> BufferPair {
log::info!("loading obj prefab: {path:?}");
let mut reader = BufReader::new(
assman.open_asset(path).expect("Failed to open texture file")
);
let (model, _) = tobj::load_obj_buf(
&mut reader,
&tobj::GPU_LOAD_OPTIONS,
|_| unimplemented!()
).unwrap();
assert_eq!(model.len(), 1, "only single model supported at the moment, sowwy :3");
let mesh = &model[0].mesh;
debug_assert!(mesh.normal_indices.is_empty() && mesh.texcoord_indices.is_empty(), "forgor single_index");
let tex_coords = bytemuck::cast_slice::<f32, [f32; 2]>(&mesh.texcoords);
let positions = bytemuck::cast_slice::<f32, [f32; 3]>(&mesh.positions);
let normals = bytemuck::cast_slice::<f32, [f32; 3]>(&mesh.normals);
let vertex_buffer: Vec<_> = (0..positions.len()).map(|i| {
ModelVertex {
tex_coords: [tex_coords[i][0], 1. - tex_coords[i][1]],
position: positions[i],
_padding: 0,
normal: normals[i],
}
}).collect();
let vertex_buffer = renderer.device().create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("obj_vertex_buffer"),
contents: bytemuck::cast_slice(&vertex_buffer),
usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::VERTEX,
});
let index_buffer = renderer.device().create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("obj_index_buffer"),
contents: bytemuck::cast_slice(&mesh.indices),
usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::INDEX,
});
BufferPair {
vertex: vertex_buffer,
vertex_len: positions.len() as u32,
index: index_buffer,
index_len: mesh.indices.len() as u32,
}
}

106
kubi/src/rendering.rs Normal file
View file

@ -0,0 +1,106 @@
use shipyard::{AllStoragesViewMut, IntoIter, IntoWorkload, SystemModificator, Unique, UniqueView, UniqueViewMut, View, Workload, WorkloadModificator};
use winit::dpi::PhysicalSize;
use glam::Vec3;
use crate::{events::WindowResizedEvent, hui_integration::kubi_ui_draw, state::is_ingame};
mod renderer;
mod primitives;
mod selection_box;
mod entities;
pub use renderer::Renderer;
pub mod background;
pub mod world;
pub mod camera_uniform;
pub mod depth;
pub mod smoverlay;
pub struct BufferPair {
pub index: wgpu::Buffer,
pub index_len: u32,
pub vertex: wgpu::Buffer,
pub vertex_len: u32,
}
#[derive(Unique)]
pub struct BackgroundColor(pub Vec3);
pub struct RenderCtx<'a> {
//pub renderer: &'a Renderer,
pub encoder: &'a mut wgpu::CommandEncoder,
pub surface_view: &'a wgpu::TextureView,
}
//TODO run init_world_render_state, init_selection_box_state, etc. only once ingame?
pub fn init_rendering() -> Workload {
(
depth::init_depth_texture,
camera_uniform::init_camera_uniform_buffer,
primitives::init_primitives,
world::init_world_render_state, //req: depth, camera
entities::init_entities_render_state, //req: depth, camera
selection_box::init_selection_box_render_state, //req: depth, camera, primitives
smoverlay::init_smoverlay_render_state, //req: primitives
).into_sequential_workload()
}
pub fn update_rendering_early() -> Workload {
(
resize_renderer,
depth::resize_depth_texture,
).into_sequential_workload()
}
pub fn update_rendering_late() -> Workload {
(
camera_uniform::update_camera_uniform_buffer,
(
selection_box::update_selection_box_render_state,
entities::update_entities_render_state,
smoverlay::update_smoverlay_render_state,
).into_workload().run_if(is_ingame),
).into_workload()
}
pub fn render_master(storages: AllStoragesViewMut) {
let renderer = storages.borrow::<UniqueView<Renderer>>().unwrap();
let mut encoder = renderer.device().create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("main_encoder"),
});
let surface_texture = renderer.surface().get_current_texture().unwrap();
let surface_view = surface_texture.texture.create_view(&wgpu::TextureViewDescriptor::default());
let mut data = RenderCtx {
encoder: &mut encoder,
surface_view: &surface_view,
};
storages.run_with_data(background::clear_bg, &mut data);
if storages.run(is_ingame) {
storages.run_with_data(world::draw_world, &mut data);
storages.run_with_data(selection_box::draw_selection_box, &mut data);
storages.run_with_data(entities::render_entities, &mut data);
storages.run_with_data(world::rpass_submit_trans_bundle, &mut data);
storages.run_with_data(smoverlay::render_submerged_view, &mut data);
}
storages.run_with_data(kubi_ui_draw, &mut data);
renderer.queue().submit([encoder.finish()]);
surface_texture.present();
}
/// Resize the renderer when the window is resized
pub fn resize_renderer(
mut renderer: UniqueViewMut<Renderer>,
resize: View<WindowResizedEvent>,
) {
if let Some(size) = resize.iter().last() {
renderer.resize(PhysicalSize::new(size.0.x, size.0.y));
}
}
// pub fn if_resized (resize: View<WindowResizedEvent>,) -> bool {
// resize.len() > 0
// }

View file

@ -0,0 +1,25 @@
use shipyard::UniqueView;
use super::{BackgroundColor, RenderCtx};
pub fn clear_bg(
ctx: &mut RenderCtx,
bg: UniqueView<BackgroundColor>,
) {
let _rpass = ctx.encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("clear_bg"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: ctx.surface_view,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color {
r: bg.0.x as f64,
g: bg.0.y as f64,
b: bg.0.z as f64,
a: 1.0,
}),
store: wgpu::StoreOp::Store,
},
})],
..Default::default()
});
}

View file

@ -0,0 +1,81 @@
use bytemuck::{Pod, Zeroable};
use shipyard::{AllStoragesView, IntoIter, Unique, UniqueView, View};
use wgpu::util::DeviceExt;
use crate::camera::Camera;
use super::Renderer;
#[derive(Debug, Clone, Copy, Default, Pod, Zeroable)]
#[repr(C, packed)]
pub struct CameraUniformData {
pub view_proj: [f32; 4 * 4],
}
//TODO if multiple cameras, buffer per camera
#[derive(Unique)]
pub struct CameraUniformBuffer {
pub camera_uniform_buffer: wgpu::Buffer,
pub camera_bind_group_layout: wgpu::BindGroupLayout,
pub camera_bind_group: wgpu::BindGroup,
}
impl CameraUniformBuffer {
pub fn init(renderer: &Renderer, data: CameraUniformData) -> Self {
let camera_uniform_buffer = renderer.device().create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("camera_uniform_buffer"),
contents: bytemuck::cast_slice(&[data]),
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
});
let camera_bind_group_layout = renderer.device().create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("camera_bind_group_layout"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::VERTEX,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
],
});
let camera_bind_group = renderer.device().create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("camera_bind_group"),
layout: &camera_bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: camera_uniform_buffer.as_entire_binding(),
},
],
});
Self { camera_uniform_buffer, camera_bind_group_layout, camera_bind_group }
}
pub fn init_default(renderer: &Renderer) -> Self {
Self::init(renderer, CameraUniformData::default())
}
pub fn update(&self, renderer: &Renderer, data: CameraUniformData) {
renderer.queue().write_buffer(&self.camera_uniform_buffer, 0, bytemuck::cast_slice(&[data]));
}
}
pub fn init_camera_uniform_buffer(storages: AllStoragesView) {
let renderer = storages.borrow::<UniqueView<Renderer>>().unwrap();
storages.add_unique(CameraUniformBuffer::init_default(&renderer));
}
pub fn update_camera_uniform_buffer(
renderer: UniqueView<Renderer>,
camera_uniform_buffer: UniqueView<CameraUniformBuffer>,
camera: View<Camera>,
) {
let Some(camera) = camera.iter().next() else { return };
let proj = camera.perspective_matrix * camera.view_matrix;
camera_uniform_buffer.update(&renderer, CameraUniformData { view_proj: proj.to_cols_array() });
}

View file

@ -0,0 +1,72 @@
use glam::{uvec2, UVec2};
use shipyard::{AllStoragesView, Unique, UniqueView, UniqueViewMut};
use super::Renderer;
#[derive(Unique)]
pub struct DepthTexture {
pub depth_texture: wgpu::Texture,
pub depth_view: wgpu::TextureView,
pub depth_sampler: wgpu::Sampler,
}
impl DepthTexture {
fn desc(size: UVec2) -> wgpu::TextureDescriptor<'static> {
wgpu::TextureDescriptor {
label: Some("depth_texture"),
size: wgpu::Extent3d {
width: size.x,
height: size.y,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Depth32Float,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
view_formats: &[wgpu::TextureFormat::Depth32Float],
}
}
pub fn init(renderer: &Renderer) -> Self {
let size = uvec2(renderer.size().width, renderer.size().height);
let depth_texture_desc = Self::desc(size);
let depth_texture = renderer.device().create_texture(&depth_texture_desc);
let depth_view = depth_texture.create_view(&wgpu::TextureViewDescriptor::default());
let depth_sampler = renderer.device().create_sampler(&wgpu::SamplerDescriptor {
label: Some("depth_sampler"),
address_mode_u: wgpu::AddressMode::ClampToEdge,
address_mode_v: wgpu::AddressMode::ClampToEdge,
address_mode_w: wgpu::AddressMode::ClampToEdge,
mag_filter: wgpu::FilterMode::Nearest,
min_filter: wgpu::FilterMode::Nearest,
mipmap_filter: wgpu::FilterMode::Nearest,
compare: Some(wgpu::CompareFunction::LessEqual),
..Default::default()
});
Self { depth_texture, depth_view, depth_sampler }
}
pub fn resize(&mut self, renderer: &Renderer) {
let old_size = uvec2(self.depth_texture.size().width, self.depth_texture.size().height);
let new_size = uvec2(renderer.size().width, renderer.size().height);
if old_size == new_size { return }
let depth_texture_desc = Self::desc(new_size);
self.depth_texture = renderer.device().create_texture(&depth_texture_desc);
self.depth_view = self.depth_texture.create_view(&wgpu::TextureViewDescriptor::default());
}
}
pub fn init_depth_texture(
storages: AllStoragesView,
) {
let renderer = storages.borrow::<UniqueView<Renderer>>().unwrap();
storages.add_unique(DepthTexture::init(&renderer));
}
pub fn resize_depth_texture(
mut depth_texture: UniqueViewMut<DepthTexture>,
renderer: UniqueView<Renderer>,
) {
depth_texture.resize(&renderer);
}

View file

@ -0,0 +1,67 @@
use shipyard::{AllStoragesView, IntoIter, IntoWithId, Unique, UniqueView, View};
use kubi_shared::{entity::Entity, transform::Transform};
use crate::{
camera::Camera, prefabs::GpuPrefabs, settings::GameSettings
};
use super::{camera_uniform::CameraUniformBuffer, depth::DepthTexture, RenderCtx};
mod instance;
mod pipeline;
#[derive(Unique)]
pub struct EntitiesRenderState {
pub pipeline: wgpu::RenderPipeline,
pub instance_buffer: instance::InstanceBuffer,
}
pub fn init_entities_render_state(storages: AllStoragesView) {
storages.add_unique(EntitiesRenderState {
pipeline: storages.run(pipeline::init_entities_pipeline),
instance_buffer: storages.run(instance::create_instance_buffer),
});
}
pub use instance::update_instance_buffer as update_entities_render_state;
// TODO: entity models
pub fn render_entities(
ctx: &mut RenderCtx,
state: UniqueView<EntitiesRenderState>,
depth: UniqueView<DepthTexture>,
prefabs: UniqueView<GpuPrefabs>,
camera_ubo: UniqueView<CameraUniformBuffer>,
) {
if state.instance_buffer.count == 0 {
return
}
let mut rpass = ctx.encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("rpass_draw_entities"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: ctx.surface_view,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Load,
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
view: &depth.depth_view,
depth_ops: Some(wgpu::Operations {
load: wgpu::LoadOp::Load,
store: wgpu::StoreOp::Store,
}),
stencil_ops: None,
}),
..Default::default()
});
rpass.set_pipeline(&state.pipeline);
rpass.set_bind_group(0, &prefabs.player_model_diffuse_bind_group, &[]);
rpass.set_bind_group(1, &camera_ubo.camera_bind_group, &[]);
rpass.set_vertex_buffer(0, prefabs.player_model.vertex.slice(..));
rpass.set_vertex_buffer(1, state.instance_buffer.buffer.slice(..));
rpass.set_index_buffer(prefabs.player_model.index.slice(..), wgpu::IndexFormat::Uint32);
rpass.draw_indexed(0..prefabs.player_model.index_len, 0, 0..state.instance_buffer.count);
}

View file

@ -0,0 +1,78 @@
use bytemuck::{Pod, Zeroable};
use kubi_shared::{entity::Entity, transform::Transform};
use renderer::Renderer;
use shipyard::{EntityId, IntoIter, IntoWithId, UniqueView, UniqueViewMut, View};
use crate::{camera::Camera, rendering::renderer};
use super::EntitiesRenderState;
#[derive(Clone, Copy, Pod, Zeroable)]
#[repr(C, packed)]
pub struct InstanceData {
pub mat: [f32; 4 * 4],
}
impl InstanceData {
pub const LAYOUT: wgpu::VertexBufferLayout<'static> = wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<InstanceData>() as wgpu::BufferAddress,
step_mode: wgpu::VertexStepMode::Instance,
attributes: &wgpu::vertex_attr_array![
3 => Float32x4,
4 => Float32x4,
5 => Float32x4,
6 => Float32x4,
],
};
}
pub struct InstanceBuffer {
pub count: u32,
pub buffer: wgpu::Buffer,
}
pub fn create_instance_buffer(
renderer: UniqueView<Renderer>,
) -> InstanceBuffer {
log::info!("entities: create_instance_buffer");
let buffer = renderer.device().create_buffer(&wgpu::BufferDescriptor {
label: Some("instance_buffer"),
size: 255 * std::mem::size_of::<InstanceData>() as u64,
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
InstanceBuffer { count: 0, buffer }
}
pub fn update_instance_buffer(
renderer: UniqueView<Renderer>,
mut state: UniqueViewMut<EntitiesRenderState>,
entities: View<Entity>,
transforms: View<Transform>,
camera: View<Camera>,
) {
//Get id of the camera entity (this assumes a single camera entity)
let cam_id = (&camera)
.iter().with_id().next()
.map(|(x, _)| x)
.unwrap_or(EntityId::dead());
// Create a list of instance data for all entities except ones that have camera attached
let mut instances = Vec::with_capacity(entities.len() - 1);
for (id, (_, trans)) in (&entities, &transforms).iter().with_id() {
if id == cam_id { continue }
instances.push(InstanceData {
mat: trans.0.to_cols_array(),
});
}
state.instance_buffer.count = instances.len() as u32;
if !instances.is_empty() {
renderer.queue().write_buffer(
&state.instance_buffer.buffer,
0,
bytemuck::cast_slice(&instances)
);
}
}

View file

@ -0,0 +1,66 @@
use shipyard::UniqueView;
use wgpu::include_wgsl;
use crate::{prefabs::{GpuPrefabs, ModelVertex}, rendering::{camera_uniform::CameraUniformBuffer, Renderer}};
use super::instance::InstanceData;
pub fn init_entities_pipeline(
renderer: UniqueView<Renderer>,
prefabs: UniqueView<GpuPrefabs>,
camera_ubo: UniqueView<CameraUniformBuffer>,
) -> wgpu::RenderPipeline {
log::info!("init_entities_pipeline");
let module = renderer.device().create_shader_module(include_wgsl!("../../../shaders/entities.wgsl"));
let pipeline_layout = renderer.device().create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("entities_pipeline_layout"),
bind_group_layouts: &[
&prefabs.player_model_diffuse_bind_group_layout,
&camera_ubo.camera_bind_group_layout,
],
push_constant_ranges: &[],
});
renderer.device().create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("entities_pipeline"),
layout: Some(&pipeline_layout),
vertex: wgpu::VertexState {
module: &module,
compilation_options: wgpu::PipelineCompilationOptions::default(),
entry_point: "vs_main",
buffers: &[
ModelVertex::LAYOUT,
InstanceData::LAYOUT,
],
},
fragment: Some(wgpu::FragmentState {
module: &module,
compilation_options: wgpu::PipelineCompilationOptions::default(),
entry_point: "fs_main",
targets: &[Some(wgpu::ColorTargetState {
format: renderer.surface_config().format,
blend: Some(wgpu::BlendState::REPLACE),
write_mask: wgpu::ColorWrites::COLOR,
})],
}),
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
strip_index_format: None,
front_face: wgpu::FrontFace::Ccw,
cull_mode: None, // Some(wgpu::Face::Back), //XXX: this culls their majestic ears! :(
polygon_mode: wgpu::PolygonMode::Fill,
conservative: false,
unclipped_depth: false,
},
depth_stencil: Some(wgpu::DepthStencilState {
format: wgpu::TextureFormat::Depth32Float,
depth_write_enabled: true,
depth_compare: wgpu::CompareFunction::Less,
bias: wgpu::DepthBiasState::default(),
stencil: wgpu::StencilState::default(),
}),
multisample: wgpu::MultisampleState::default(),
multiview: None,
})
}

View file

@ -0,0 +1,42 @@
use bytemuck::{Pod, Zeroable};
use shipyard::{IntoWorkload, Workload};
mod cube;
mod fstri;
pub use cube::CubePrimitive;
pub use fstri::FstriPrimitive;
pub fn init_primitives() -> Workload {
(
cube::init_cube_primitive,
fstri::init_fstri_primitive,
).into_workload()
}
#[derive(Clone, Copy, Default, Pod, Zeroable)]
#[repr(C, packed)]
pub struct PrimitiveVertex {
pub position: [f32; 3],
}
impl PrimitiveVertex {
pub const LAYOUT: wgpu::VertexBufferLayout<'static> = wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<PrimitiveVertex>() as wgpu::BufferAddress,
step_mode: wgpu::VertexStepMode::Vertex,
attributes: &wgpu::vertex_attr_array![0 => Float32x3],
};
}
#[derive(Clone, Copy, Default, Pod, Zeroable)]
#[repr(C, packed)]
pub struct PrimitiveVertex2 {
pub position: [f32; 2],
}
impl PrimitiveVertex2 {
pub const LAYOUT: wgpu::VertexBufferLayout<'static> = wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<PrimitiveVertex2>() as wgpu::BufferAddress,
step_mode: wgpu::VertexStepMode::Vertex,
attributes: &wgpu::vertex_attr_array![0 => Float32x2],
};
}

View file

@ -0,0 +1,50 @@
use shipyard::{AllStoragesView, Unique, UniqueView};
use wgpu::util::DeviceExt;
use crate::rendering::{BufferPair, Renderer};
use super::PrimitiveVertex;
#[derive(Unique)]
pub struct CubePrimitive(pub BufferPair);
/// Vertices for a centered cube with a side length of 1
const CUBE_VERTICES: &[PrimitiveVertex] = &[
// front
PrimitiveVertex { position: [-0.5, -0.5, 0.5] },
PrimitiveVertex { position: [ 0.5, -0.5, 0.5] },
PrimitiveVertex { position: [ 0.5, 0.5, 0.5] },
PrimitiveVertex { position: [-0.5, 0.5, 0.5] },
// back
PrimitiveVertex { position: [-0.5, -0.5, -0.5] },
PrimitiveVertex { position: [ 0.5, -0.5, -0.5] },
PrimitiveVertex { position: [ 0.5, 0.5, -0.5] },
PrimitiveVertex { position: [-0.5, 0.5, -0.5] },
];
/// Indices for a cube primitive
const CUBE_INDICES: &[u16] = &[
0, 1, 2, 2, 3, 0, // front
1, 5, 6, 6, 2, 1, // right
7, 6, 5, 5, 4, 7, // back
4, 0, 3, 3, 7, 4, // left
4, 5, 1, 1, 0, 4, // bottom
3, 2, 6, 6, 7, 3, // top
];
pub fn init_cube_primitive(storages: AllStoragesView) {
log::info!("init_cube_primitive");
let renderer = storages.borrow::<UniqueView<Renderer>>().unwrap();
storages.add_unique(CubePrimitive(BufferPair {
index: renderer.device().create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("cube_index_buffer"),
contents: bytemuck::cast_slice(CUBE_INDICES),
usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::INDEX,
}),
index_len: CUBE_INDICES.len() as u32,
vertex: renderer.device().create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("cube_vertex_buffer"),
contents: bytemuck::cast_slice(CUBE_VERTICES),
usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::VERTEX,
}),
vertex_len: CUBE_VERTICES.len() as u32,
}));
}

View file

@ -0,0 +1,24 @@
use shipyard::{AllStoragesView, Unique, UniqueView};
use wgpu::util::DeviceExt;
use crate::rendering::Renderer;
use super::PrimitiveVertex2;
pub const FSTRI_VERTICES: &[PrimitiveVertex2] = &[
PrimitiveVertex2 { position: [-1.0, -1.0] },
PrimitiveVertex2 { position: [ 3.0, -1.0] },
PrimitiveVertex2 { position: [-1.0, 3.0] },
];
#[derive(Unique)]
pub struct FstriPrimitive(pub wgpu::Buffer);
pub fn init_fstri_primitive(storages: AllStoragesView) {
log::info!("init_fstri_primitive");
let renderer = storages.borrow::<UniqueView<Renderer>>().unwrap();
let buffer = renderer.device().create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("fstri_vertex_buffer"),
contents: bytemuck::cast_slice(FSTRI_VERTICES),
usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::VERTEX,
});
storages.add_unique(FstriPrimitive(buffer));
}

View file

@ -0,0 +1,177 @@
use std::sync::Arc;
use pollster::FutureExt;
use shipyard::Unique;
use winit::{
event_loop::ActiveEventLoop,
window::{Fullscreen, Window},
dpi::PhysicalSize
};
use crate::settings::{GameSettings, FullscreenMode};
#[derive(Unique)]
pub struct Renderer {
window: Arc<Window>,
instance: wgpu::Instance,
surface: wgpu::Surface<'static>,
device: wgpu::Device,
queue: wgpu::Queue,
surface_config: wgpu::SurfaceConfiguration,
size: PhysicalSize<u32>,
// pub depth_texture: wgpu::Texture,
}
impl Renderer {
pub fn init(event_loop: &ActiveEventLoop, settings: &GameSettings) -> Self {
log::info!("initializing display");
let window_attributes = Window::default_attributes()
.with_title("kubi")
.with_maximized(true)
.with_min_inner_size(PhysicalSize::new(640, 480))
.with_fullscreen({
//this has no effect on android, so skip this pointless stuff
#[cfg(target_os = "android")] {
None
}
#[cfg(not(target_os = "android"))]
if let Some(fs_settings) = &settings.fullscreen {
let monitor = event_loop.primary_monitor().or_else(|| {
event_loop.available_monitors().next()
});
if let Some(monitor) = monitor {
log::info!("monitor: {}", monitor.name().unwrap_or_else(|| "generic".into()));
match fs_settings.mode {
FullscreenMode::Borderless => {
log::info!("starting in borderless fullscreen mode");
Some(Fullscreen::Borderless(Some(monitor)))
},
FullscreenMode::Exclusive => {
log::warn!("exclusive fullscreen mode is experimental");
log::info!("starting in exclusive fullscreen mode");
//TODO: grabbing the first video mode is probably not the best idea...
monitor.video_modes().next()
.map(|vmode| {
log::info!("video mode: {}", vmode.to_string());
Some(Fullscreen::Exclusive(vmode))
})
.unwrap_or_else(|| {
log::warn!("no valid video modes found, falling back to windowed mode instead");
None
})
}
}
} else {
log::warn!("no monitors found, falling back to windowed mode");
None
}
} else {
log::info!("starting in windowed mode");
None
}
});
let window = Arc::new(event_loop.create_window(window_attributes).unwrap());
let size = window.inner_size();
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
backends: wgpu::Backends::all(),
//Disable validation layer
flags: wgpu::InstanceFlags::default() & !wgpu::InstanceFlags::VALIDATION,
//we're using vulkan on windows
// #[cfg(all(target_os = "windows", target_arch = "x86_64"))]
// dx12_shader_compiler: wgpu::Dx12Compiler::Dxc {
// dxil_path: Some("./dxil.dll".into()),
// dxc_path: Some("./dxcompiler.dll".into()),
// },
..Default::default()
});
// Create a surface with `create_surface_unsafe` to get a surface with 'static lifetime
// It should never outlive the window it's created from
// let surface = unsafe {
// let target = wgpu::SurfaceTargetUnsafe::from_window(&window).unwrap();
// instance.create_surface_unsafe(target).unwrap()
// };
let surface = instance.create_surface(Arc::clone(&window)).unwrap();
let adapter = instance.request_adapter(
&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::HighPerformance,
compatible_surface: Some(&surface),
force_fallback_adapter: false,
},
).block_on().unwrap();
log::info!("Adapter: {:?}", adapter.get_info());
log::info!("Features: {:?}", adapter.features());
log::info!("Limits: {:?}", adapter.limits());
let (device, queue) = adapter.request_device(
&wgpu::DeviceDescriptor {
label: None,
required_features: wgpu::Features::empty(),
required_limits: wgpu::Limits::downlevel_webgl2_defaults().using_resolution(adapter.limits()),
},
None,
).block_on().unwrap();
let surface_config = surface.get_default_config(&adapter, size.width, size.height).unwrap();
surface.configure(&device, &surface_config);
Self { window, instance, surface, device, queue, surface_config, size }
}
pub fn resize(&mut self, size: PhysicalSize<u32>) {
if size.width == 0 || size.height == 0 {
log::warn!("Ignoring resize event with zero width or height");
return
}
if self.size == size {
log::warn!("Ignoring resize event with same size");
return
}
log::debug!("resizing surface to {:?}", size);
self.size = size;
self.surface_config.width = size.width;
self.surface_config.height = size.height;
self.surface.configure(&self.device, &self.surface_config);
}
pub fn reconfigure(&self) {
self.surface.configure(&self.device, &self.surface_config);
}
//getters:
pub fn size(&self) -> PhysicalSize<u32> {
self.size
}
pub fn size_uvec2(&self) -> glam::UVec2 {
glam::UVec2::new(self.size.width, self.size.height)
}
pub fn size_vec2(&self) -> glam::Vec2 {
glam::Vec2::new(self.size.width as f32, self.size.height as f32)
}
pub fn window(&self) -> &Window {
&self.window
}
pub fn surface(&self) -> &wgpu::Surface<'static> {
&self.surface
}
pub fn device(&self) -> &wgpu::Device {
&self.device
}
pub fn queue(&self) -> &wgpu::Queue {
&self.queue
}
pub fn surface_config(&self) -> &wgpu::SurfaceConfiguration {
&self.surface_config
}
}

Some files were not shown because too many files have changed in this diff Show more