Compare commits

...

539 commits

Author SHA1 Message Date
griffi-gh 254103975b add the old "lighting" thingy 2024-05-13 23:56:31 +02:00
griffi-gh 7067f1202e Remove unneded use of nonsendsync 2024-05-13 23:44:48 +02:00
griffi-gh 06e23d463b add submerge overlay back 2024-05-13 23:43:39 +02:00
griffi-gh 13ac29c9a2 wip smoverlay 2024-05-13 19:47:36 +02:00
griffi-gh fe25df309e trans chunks are visible again! 2024-05-08 20:13:19 +02:00
griffi-gh afe9dcf378 players :3 2024-05-08 15:02:16 +02:00
griffi-gh b1e3e91bc4 uwu 2024-05-08 13:35:03 +02:00
griffi-gh 282c27962b kitty? kitty! 2024-05-08 13:12:58 +02:00
griffi-gh 1e319de573 use latest hui 2024-05-08 03:22:41 +02:00
griffi-gh d21104decc kubiui integration update 2024-05-08 03:04:50 +02:00
griffi-gh ba406d812e use proper srgb color for background 2024-05-07 21:04:50 +02:00
griffi-gh df750c8901 print adapter info on start 2024-05-07 20:50:59 +02:00
griffi-gh 0d41a9f14d Selection Box 2024-05-07 20:12:07 +02:00
griffi-gh 241dc8ea7e clean up world renderer 2024-05-07 01:15:44 +02:00
griffi-gh 6431f4e7fe wip 2024-05-07 01:14:36 +02:00
griffi-gh 6654c0a69f add cube primitive 2024-05-07 01:11:05 +02:00
griffi-gh c4f60e2634 separate clear_bg pass 2024-05-07 00:13:14 +02:00
griffi-gh d34fda3cd3 use default limits 2024-05-06 18:21:19 +02:00
griffi-gh a2a39e1edf fix compilation for android 2024-05-06 18:07:29 +02:00
griffi-gh 8ae89f37b0 use linear for min 2024-05-06 17:38:58 +02:00
griffi-gh dcfb9865eb enable all backends 2024-05-06 17:21:18 +02:00
griffi-gh b0c47d8e82 flip uvs 2024-05-06 17:19:11 +02:00
griffi-gh 641e3c921c enable depth texture 2024-05-06 17:09:13 +02:00
griffi-gh 66afd3057e restructure stuff and fix some warnings 2024-05-06 16:53:40 +02:00
griffi-gh db8243cb90 remove WindowSize 2024-05-06 16:24:44 +02:00
griffi-gh 5e1499cbf8 depth, wip 2024-05-06 16:17:01 +02:00
griffi-gh 4cc1754951 forgor vec4 type 2024-05-06 15:54:17 +02:00
griffi-gh e09f5405d8 oops 2024-05-06 15:53:47 +02:00
griffi-gh f5f5d8b9b9 discard fully transparent 2024-05-06 15:52:59 +02:00
griffi-gh a6e92e041c owo 2024-05-06 15:48:36 +02:00
griffi-gh 4f114be002 ok this is better 2024-05-06 14:29:07 +02:00
griffi-gh c071986f2a i see... something? 2024-05-06 13:32:44 +02:00
griffi-gh 0440f207cc do rpass in world.rs 2024-05-05 15:42:25 +02:00
griffi-gh a7b403b0a5 use Arc<Window> instead of create_surface_unsafe 2024-05-05 15:20:44 +02:00
griffi-gh 840529511c ... 2024-05-05 15:19:09 +02:00
griffi-gh 3f359f433c lifetime errors :є 2024-05-05 13:25:54 +02:00
griffi-gh df808610ef init texture prefab 2024-05-05 13:16:48 +02:00
griffi-gh 230f10fc57 minor changes 2024-05-05 02:02:30 +02:00
griffi-gh 33c418ff6f load block textures 2024-05-05 02:00:06 +02:00
griffi-gh 72d03962f5 minor changes to rendering.rs 2024-05-05 01:34:27 +02:00
griffi-gh bf4922c4b1 move renderer into it's own file 2024-05-05 01:30:07 +02:00
griffi-gh 525ce0cd40 copy over world render 2024-05-05 01:25:44 +02:00
griffi-gh e341a84b85 write some dumb shader code 2024-05-05 01:06:43 +02:00
griffi-gh eeb650eac0 throw away glsl shaders 2024-05-05 00:17:27 +02:00
griffi-gh 535f7d6112 :3 2024-05-05 00:16:55 +02:00
griffi-gh 7d8c2b14c3 oops forgor to cast trans buffers 2024-05-04 23:18:27 +02:00
griffi-gh f457f1034b create buffers 2024-05-04 23:17:12 +02:00
griffi-gh 4307a50a76 "fix" compile errs 2024-05-04 22:10:40 +02:00
griffi-gh bcaf011b88 init wgpu stuff 2024-05-04 17:24:19 +02:00
griffi-gh bae7af39d5 . 2024-05-04 14:32:21 +02:00
griffi-gh faecf5a4a4 rip out some parts of glium 2024-05-04 14:30:06 +02:00
griffi-gh 877e603fed use single seeder instance 2024-05-04 13:50:58 +02:00
griffi-gh b665d1c004 render trans chunks after entities/sel box etc 2024-05-04 00:35:56 +02:00
griffi-gh f0270aead3 remove unused 2024-05-04 00:07:11 +02:00
griffi-gh 3ed8d82a04 preheat chunks on server 2024-05-04 00:05:16 +02:00
griffi-gh c32d97b636 fix mesh not updating 2024-05-03 23:43:01 +02:00
griffi-gh f067a07d90 handle unsub requests 2024-05-03 23:39:37 +02:00
griffi-gh 1d06621504 refactor state transitions 2024-05-03 23:29:52 +02:00
griffi-gh 1558169c7d remove outdated info from readme 2024-05-03 20:27:39 +02:00
griffi-gh da790a932a update 2024-05-03 20:18:45 +02:00
griffi-gh 6a6ef1bfec always request gles3 2024-05-03 14:01:05 +02:00
griffi-gh 338f11afc6 hopefully fix trans rendering bug 2024-05-03 13:36:22 +02:00
griffi-gh 80abfc2768 add lints 2024-05-03 01:48:12 +02:00
griffi-gh 6a218352ab "fix" some warnings 2024-05-03 01:39:47 +02:00
griffi-gh 0f55ff6210 Merge pull request #19 from griffi-gh/rewrite-wgen
Rewrite world generation
2024-05-03 01:23:34 +02:00
griffi-gh cfffb403a3 move stuff, forests! 2024-05-03 01:21:56 +02:00
griffi-gh 0fac4531e0 decoration and layers 2024-05-03 00:27:43 +02:00
griffi-gh b1de67339e add opt lvl 2024-05-02 22:27:49 +02:00
griffi-gh b487da6474 check before first step 2024-05-02 18:16:29 +02:00
griffi-gh 56fe70765f switch libs 2024-05-02 18:15:18 +02:00
griffi-gh b6c314e8f3 disable borked cave generation 2024-05-02 16:51:03 +02:00
griffi-gh a3bc619cb6 abortions and stuff 2024-05-02 16:50:46 +02:00
griffi-gh 7d5b706d0d rewrite worldgen 2024-05-02 12:42:14 +02:00
griffi-gh 6ee3130f8a drop kubi-pool 2024-05-02 11:14:47 +02:00
griffi-gh dac6e56516 add overlay when submerged 2024-05-02 11:03:38 +02:00
griffi-gh 931eb572fc more settings 2024-05-02 02:43:58 +02:00
griffi-gh aaf576a9f1 add tip to chat 2024-05-02 02:22:55 +02:00
griffi-gh 42c06cba33 integrate hui-winit 2024-05-02 02:20:54 +02:00
griffi-gh 2f7dcfabc8 if not locked dont move camera 2024-05-02 02:07:49 +02:00
griffi-gh ca74c7799c move cursor to center on unlock 2024-05-02 02:07:36 +02:00
griffi-gh 0a352ac291 wip settings 2024-05-02 02:06:23 +02:00
griffi-gh 69e7ae23db ~~scuffed sorting?~~ 2024-05-02 01:42:07 +02:00
griffi-gh 4906e1bada scuffed sorting 2024-05-02 01:38:58 +02:00
griffi-gh 5357cf0595 fix shader 2024-05-02 01:24:27 +02:00
griffi-gh 4a24f4ac56 refactor transparency descriptor 2024-05-02 01:21:17 +02:00
griffi-gh 8ebc94fd9e set discard_alpha 2024-05-02 01:13:21 +02:00
griffi-gh 184be9393c alt shape 2024-05-02 01:05:34 +02:00
griffi-gh 748a0bf6d6 dynamic crosshair stuff 2024-05-02 00:43:24 +02:00
griffi-gh d57787059a add trans rendering and crosshair 2024-05-02 00:32:43 +02:00
griffi-gh b95be7fb2a add logging to blk queue 2024-04-25 19:14:56 +02:00
griffi-gh af5ed58442 implement local server queue 2024-04-25 19:13:05 +02:00
griffi-gh dc1d8db27d fix missing module 2024-04-25 18:39:18 +02:00
griffi-gh f2114a45c2 upgrade deps 2024-04-25 18:29:34 +02:00
griffi-gh 266c13d30e downgrade back to rand 0.8 from alpha 2024-04-25 18:28:14 +02:00
griffi-gh 3b12a3260d revert back to yellow 2024-04-25 17:52:42 +02:00
griffi-gh 6931118931 Make leave messages red 2024-04-25 17:50:09 +02:00
griffi-gh 61b63a55d2 rename ChatManager to ChatHistory 2024-04-25 17:49:12 +02:00
griffi-gh 6a721b0400 randomize usernames 2024-04-25 17:48:39 +02:00
griffi-gh fb0ff88c10 stuff 2024-04-25 15:16:31 +02:00
griffi-gh caa7cf9aeb uwu 2024-04-25 13:41:50 +02:00
griffi-gh 09af18dda0 add chat 2024-04-25 13:39:23 +02:00
griffi-gh ee6a5dd2f9 Sync client disconnects 2024-04-25 12:30:25 +02:00
griffi-gh fd8ec3478a Merge branch 'master' of https://github.com/griffi-gh/kubi 2024-04-23 16:16:37 +02:00
griffi-gh 62618f4486 upgrade shipyard (fix compilation on nightly) 2024-04-23 16:16:33 +02:00
griffi-gh 31b082b0c1 Create LICENSE 2024-04-21 19:31:29 +02:00
griffi-gh 3006640be5 upgrade to hui = "0.1.0-alpha.4" 2024-03-25 19:06:35 +01:00
griffi-gh d63966e618 handle fuck off request 2024-02-21 04:21:47 +01:00
griffi-gh 8f5f8b07fb restructure stuff 2024-02-21 03:51:05 +01:00
griffi-gh e2bfa0c6f0 update the loading screen 2024-02-21 03:31:52 +01:00
griffi-gh b9c721f9b6 separate loading screen base 2024-02-21 02:28:15 +01:00
griffi-gh bd6fef60a0 owo 2024-02-20 22:29:55 +01:00
griffi-gh 83353b4fd6 update hui to 0.1.0-alpha.3, new loading screen 2024-02-20 22:23:34 +01:00
griffi-gh d5b23f62a4 upgrade deps 2024-02-18 04:15:26 +01:00
griffi-gh 35b7d74129 upgrade to hui 0.1.0-alpha.1 2024-02-18 04:14:17 +01:00
griffi-gh 895da317e8 separate kubi-ui as hui 2024-02-17 23:14:04 +01:00
griffi-gh 4cae827233 Merge pull request #14 from griffi-gh/client-physics
Client physics (wip)
2024-02-17 23:59:12 +03:00
griffi-gh 81decc6202 pass velocity to server move evt 2024-02-17 14:45:30 +01:00
griffi-gh bc3db2c95e clamp? 2024-02-16 15:13:37 +01:00
griffi-gh c799d94e0a uwu 2024-02-16 00:43:11 +01:00
griffi-gh 1b87553f79 fun 2024-02-15 19:39:09 +01:00
griffi-gh 8d673ed82f use force for plr 2024-02-15 15:51:06 +01:00
griffi-gh f84fe5d022 fps ctl test impl 2024-02-15 14:49:41 +01:00
griffi-gh aa5a0c09cc fix ctl speed 2024-02-15 01:54:31 +01:00
griffi-gh 93b99d2306 pl ctl 2024-02-15 01:51:41 +01:00
griffi-gh 36cec2b1e5 refactor stuff 2024-02-14 21:56:59 +01:00
griffi-gh 71d089c784 wip client physics 2024-02-14 01:59:37 +01:00
griffi-gh af2ae5da88 remove notics abt multitouch 2024-02-13 11:52:50 +01:00
griffi-gh 5f2f27396b wip cl physics 2024-02-13 00:50:30 +01:00
griffi-gh d49c858cd8 normalize rotation in update_movement 2024-02-13 00:00:01 +01:00
griffi-gh f928970afd rename proces_glutin_events to process_winit_events 2024-02-12 23:53:14 +01:00
griffi-gh 2ba011a89b beautify code 2024-02-12 23:50:08 +01:00
griffi-gh 01f642fcbb fix landscape 2024-02-12 22:59:57 +01:00
griffi-gh 12a9d799d4 what the fuck :ferrisballSweat: 2024-02-12 22:59:49 +01:00
griffi-gh 4727488fcd fix crash on android by using dynamic buffers for ui 2024-02-12 22:29:40 +01:00
griffi-gh 04d0b59dcf upgrade deps 2024-02-12 22:20:45 +01:00
griffi-gh ad4254e0d4 increase small 2024-02-01 16:21:10 +01:00
griffi-gh 37d67bafb1 rename guiv2_integration -> kubi_ui_integration 2024-02-01 16:12:51 +01:00
griffi-gh 52d4560bd0 refactor net code to use enums 2024-02-01 16:00:32 +01:00
griffi-gh 63e002364e set protocol id back to zero 2023-12-03 16:50:07 +01:00
griffi-gh 362c880c64 allow chunk size to be increased past 32 2023-12-03 16:48:48 +01:00
griffi-gh 3882e096bb uwu 2023-12-02 21:32:20 +01:00
griffi-gh 9ffe7a795f owo 2023-12-02 21:20:20 +01:00
griffi-gh 82dd665008 add new text test 2023-12-02 21:12:26 +01:00
griffi-gh 1c0dc2a3dd separate textured shader 2023-12-02 20:50:25 +01:00
griffi-gh c9400343dd it works 2023-12-02 19:46:08 +01:00
griffi-gh c9ca85900d use fontdue layout 2023-12-02 19:27:21 +01:00
griffi-gh 37e643e319 owo 2023-12-02 19:11:41 +01:00
griffi-gh c85b76acc1 Vec<UiDrawCommand> -> UiDrawCommands 2023-12-02 19:07:31 +01:00
griffi-gh 84e79cf93f font handle stuff 2023-12-02 17:47:25 +01:00
griffi-gh 521b7f3ef2 text kinda works! 2023-12-02 17:16:45 +01:00
griffi-gh 31a2499d26 rename texman.rs to ftm 2023-12-02 17:05:38 +01:00
griffi-gh b120e0449b kinda got text rendering working as well as multi draw call 2023-12-02 17:05:03 +01:00
griffi-gh a6bb48dddd run cargo update 2023-12-02 12:50:41 +01:00
griffi-gh ad55d93b6e tr 2023-12-01 21:48:17 +01:00
griffi-gh 175ca1a877 uwu 2023-12-01 20:51:41 +01:00
griffi-gh 64e4dcfe01 add bind_texture field to UiVertex 2023-12-01 00:54:32 +01:00
griffi-gh ce1cb63c01 misc. font rendering changes in kui, minor backend api change 2023-12-01 00:45:56 +01:00
griffi-gh f783917945 update ftm stuff 2023-11-30 21:15:00 +01:00
griffi-gh d955445dbd demonstrate padding on all 4 sides 2023-11-28 20:18:47 +01:00
griffi-gh b57467f7de fix manifest 2023-11-28 20:18:36 +01:00
griffi-gh 894946f5b1 fix readme link 2023-11-28 20:15:00 +01:00
griffi-gh 0ed687211d run cargo update 2023-11-28 20:13:30 +01:00
griffi-gh 2e7cdceec7 major ui restructure, wip interactable api 2023-11-28 20:13:14 +01:00
griffi-gh a9eba8cc29 main axis align 2023-11-28 11:32:09 +01:00
griffi-gh 736c0e09d5 tst 2023-11-28 10:39:25 +01:00
griffi-gh 7cd04247b8 fix stuff 2023-11-28 10:31:38 +01:00
griffi-gh 519a4a2e0d owo 2023-11-26 01:47:48 +01:00
griffi-gh cbefcffb93 wip item system 2023-11-26 01:29:02 +01:00
griffi-gh c746c19068 magic -> subheader 2023-11-25 21:12:29 +01:00
griffi-gh b3233915e6 reduce default render distanc 2023-11-25 16:32:05 +01:00
griffi-gh a0df0cc643 trigger workflow 2023-11-25 16:23:19 +01:00
griffi-gh 990f3787f7 fix ci, hopefully 2023-11-25 16:22:44 +01:00
griffi-gh aa4f40a767 uwu 2023-11-25 16:19:16 +01:00
griffi-gh 257f7bb130 ui.ui -> ui.kui 2023-11-25 16:18:40 +01:00
griffi-gh 356bfa1acb . 2023-11-25 16:17:47 +01:00
griffi-gh cf26489f34 upd. readme 2023-11-25 15:55:40 +01:00
griffi-gh 921b9fbb75 font cache! 2023-11-24 17:54:23 +01:00
griffi-gh c986e36f88 wip 2023-11-23 23:13:17 +01:00
griffi-gh b893193753 wip 2023-11-23 21:27:11 +01:00
griffi-gh 09a7426ac2 build 2023-11-23 18:01:18 +01:00
griffi-gh de7c83adbc wip texture managment 2023-11-23 17:59:14 +01:00
griffi-gh a18537a915 wip text render, minor changes 2023-11-23 17:14:02 +01:00
griffi-gh 3231a292ad use immut. buffers for primitives 2023-11-23 11:43:48 +01:00
griffi-gh e78400ae82 fix padding 2023-11-23 11:26:49 +01:00
griffi-gh 4b840c5ff0 . 2023-11-23 11:11:04 +01:00
griffi-gh cf56bf29fe restructure stuff 2023-11-23 11:10:42 +01:00
griffi-gh fef14911d5 add test 2023-11-23 11:10:35 +01:00
griffi-gh a6d091f4da add traits 2023-11-23 10:53:44 +01:00
griffi-gh 7ff83b3cfc (todo) ui demo 2023-11-23 01:48:04 +01:00
griffi-gh 1b9d13b80b container impl (wip) 2023-11-23 01:47:54 +01:00
griffi-gh 99feb1eae7 add rect element 2023-11-23 01:47:27 +01:00
griffi-gh 0106409c2e spacer default 2023-11-23 01:22:33 +01:00
griffi-gh 3aa64143e3 oh fuck. 2023-11-22 22:03:39 +01:00
griffi-gh fe50cd1af9 discard alpha, use treshold 2023-11-22 22:02:30 +01:00
griffi-gh de44cd67b2 separate out buffer handling code 2023-11-22 21:57:13 +01:00
griffi-gh 36363ac15d oops forgot to remove shader from prefab too 2023-11-22 21:31:05 +01:00
griffi-gh a064d571ac remove legacy ui shaders 2023-11-22 20:27:08 +01:00
griffi-gh 0941ca5cda fix crash, ui core multi-draw call 2023-11-22 20:26:44 +01:00
griffi-gh 60d05782bd drop legacy ui system 2023-11-22 19:03:17 +01:00
griffi-gh c849c0bfd6 migrate loading screen to guiv2, fix bugs 2023-11-22 19:01:32 +01:00
griffi-gh a8d5b4a752 fix ui shader 2023-11-22 18:40:35 +01:00
griffi-gh fcc19a58a5 prefer gles core 2023-11-22 17:22:40 +01:00
griffi-gh d4d7066526 fix missing depth buffer (like wtf) 2023-11-22 17:07:55 +01:00
griffi-gh 6e0d93c23e commit 2023-11-22 16:51:59 +01:00
griffi-gh f42b1ea1bc log when uploading ui buffers 2023-11-22 14:38:15 +01:00
griffi-gh 82d1034f67 use cyan for debug/trace 2023-11-22 14:38:02 +01:00
griffi-gh fc9da59300 log shader compiler release 2023-11-22 14:33:43 +01:00
griffi-gh 277fd4c27d pub progressbar, impl default 2023-11-22 14:33:35 +01:00
griffi-gh a78e01435f integrate ui 2023-11-22 14:23:48 +01:00
griffi-gh 2a09002b46 use immutable buffers for chunk data 2023-11-22 12:01:49 +01:00
griffi-gh f6fd4ed20f Merge branch 'master' of https://github.com/griffi-gh/kubi 2023-11-22 11:56:48 +01:00
griffi-gh 701ab7434a glium ui backend, misc gui changes 2023-11-22 11:56:46 +01:00
griffi-gh 87f4bd8df7 Update README.md 2023-11-22 01:14:47 +01:00
griffi-gh 388614d92d 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 792a2b7c55 change settings 2023-11-21 22:40:57 +01:00
griffi-gh f36df25c0e . 2023-11-21 19:39:21 +01:00
griffi-gh e5d1b1471a gitignore logs 2023-11-21 19:39:09 +01:00
griffi-gh 5de03912d2 minor changes 2023-11-21 19:33:05 +01:00
griffi-gh a6d642ce7a stuff 2023-11-21 19:23:48 +01:00
griffi-gh d949d86ea8 copy over simple init clode 2023-11-21 18:26:39 +01:00
griffi-gh 8c0004e990 fix android event loop init 2023-11-21 18:06:34 +01:00
griffi-gh 50aeb64c11 fix visibility 2023-11-21 17:51:10 +01:00
griffi-gh fc23e8b827 wip fix android 2023-11-21 17:49:40 +01:00
griffi-gh 5dbc52bba2 fix mouse input 2023-11-21 17:30:04 +01:00
griffi-gh 8183f4b0d0 reuse window creation code 2023-11-21 17:27:44 +01:00
griffi-gh 11ec9d3ea9 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 8287a48f83 fix some stuff 2023-11-21 17:19:52 +01:00
griffi-gh bdcdd4ebd2 upgrade more stuff 2023-11-21 17:11:53 +01:00
griffi-gh 02009e6b59 get it to compile 2023-11-21 17:03:50 +01:00
griffi-gh 003f2fd460 undo 2023-11-21 16:46:18 +01:00
griffi-gh 13816a6bd3 start fixing stuff 2023-11-21 16:45:51 +01:00
griffi-gh 6864e65814 upgrade shipyard/glium 2023-11-21 16:45:41 +01:00
griffi-gh 2777204982 update ui system 2023-11-21 16:14:26 +01:00
griffi-gh ad61b7db94 wip ui 2023-11-21 14:08:22 +01:00
griffi-gh 6eb7a3f690 wip new gui 2023-11-21 01:03:04 +01:00
griffi-gh a1369b3520 update readme 2023-11-21 00:01:52 +01:00
griffi-gh c1d1e512f0 fix readme with up-to-date info 2023-11-20 23:56:42 +01:00
griffi-gh bbba9c6f6d add default-members 2023-11-20 23:53:51 +01:00
griffi-gh 3e9e914df3 change stuff :3 (will break android builds) 2023-11-20 23:21:50 +01:00
griffi-gh 264c72aeb0 wip data 2023-11-20 20:59:34 +01:00
griffi-gh 93f3066ee0 . 2023-11-20 19:51:16 +01:00
griffi-gh 298c959d73 upgrade some deps 2023-11-19 19:36:17 +01:00
griffi-gh fa02f0bfe4 wip kubipool 2023-11-19 19:30:06 +01:00
griffi-gh 70fa0e1ebb upd data 2023-11-19 19:19:37 +01:00
griffi-gh 63f27cbb71 wip data 2023-11-19 18:27:20 +01:00
griffi-gh 5494dc61ed oops my screen is hdr only 2023-09-25 11:37:59 +02:00
griffi-gh 503e005fcf upgrade dependencies 2023-07-09 03:03:45 +02:00
griffi-gh b760a0f5b8 Revert "mess around with srgb (wip)"
This reverts commit 4f37e7bfc6.
2023-07-09 01:40:40 +02:00
griffi-gh 8ec98d639a Update README.md 2023-06-10 09:20:01 +02:00
griffi-gh 4f37e7bfc6 mess around with srgb (wip) 2023-06-04 19:38:46 +02:00
griffi-gh 260f7e83b8 Update readme 2023-06-04 18:51:37 +02:00
griffi-gh e5613ba05e add toml highlight to readme 2023-06-04 18:42:33 +02:00
griffi-gh 0a2ed9a9a2 android controls -> touch controls 2023-06-04 18:40:54 +02:00
griffi-gh bbea1659e1 Add touch controls, switch to tinymap for keyboard input 2023-06-04 18:39:55 +02:00
griffi-gh 48497c6e17 update android metadata 2023-06-04 15:44:27 +02:00
griffi-gh da69fe2a0a remove notes about nightly from android 2023-06-04 15:31:23 +02:00
griffi-gh ea7fc85c6d This is deprecated but required to make cargo-apk work! 2023-06-04 15:30:47 +02:00
griffi-gh 3c9da07bc0 change orientation 2023-06-04 15:29:53 +02:00
griffi-gh 7b214b8ebb update info around nightly in readme 2023-06-04 15:19:33 +02:00
griffi-gh 601e55fb9f 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 c919aeb81b Merge branch 'master' of https://github.com/griffi-gh/kubi 2023-05-28 23:46:20 +02:00
griffi-gh db2bb1b53e use mold as linker on linux 2023-05-28 23:46:14 +02:00
griffi-gh 1c2f21d028 oops, avx2 is not as widely available as i thought 2023-05-27 18:54:00 +02:00
griffi-gh 184fca2726 add note to readme about android 2023-05-21 12:41:09 +02:00
griffi-gh 864ae29b3b remove not used yet comment 2023-05-21 12:33:09 +02:00
griffi-gh fe29489f7a use ProgramCreationInput 2023-05-21 12:32:06 +02:00
griffi-gh e42475570b remove geom shader macro 2023-05-21 12:29:01 +02:00
griffi-gh 3f77dfc099 fix warnings; remove unused imports 2023-05-21 12:24:45 +02:00
griffi-gh c8ff48d34b statically link crt 2023-05-21 12:13:18 +02:00
griffi-gh c48afa107f use libcubi as lib name 2023-05-21 12:08:29 +02:00
griffi-gh b3a85a85aa Merge branch 'master' of https://github.com/griffi-gh/kubi 2023-05-21 12:03:46 +02:00
griffi-gh 986c80111f add ndk to deps, disable srgb by default 2023-05-21 12:03:41 +02:00
griffi-gh 2c4009f35c Add android build instructions 2023-05-21 06:53:47 +02:00
griffi-gh 3159273495 check at runtime 2023-05-21 04:54:02 +02:00
griffi-gh f33d1589b3 skip cursor lock on android 2023-05-21 04:24:54 +02:00
griffi-gh ec7cddec8d allow gilrs to fail (required for android support) 2023-05-21 04:23:10 +02:00
griffi-gh 0c0619a54b cross-platform assets 2023-05-21 04:17:28 +02:00
griffi-gh 09920a2e55 initial android support (fails on textures) 2023-05-21 03:14:04 +02:00
griffi-gh 51919ed327 fix warnings 2023-05-21 02:16:34 +02:00
griffi-gh edd43d3684 use specific log version 2023-05-21 02:16:12 +02:00
griffi-gh 2745a8c823 add some android metadata 2023-05-20 16:43:00 +02:00
griffi-gh aeaa3d175f Fixed timestamps! (use them to fix networking) 2023-05-20 15:59:52 +02:00
griffi-gh 76ec9fba32 oops 2023-05-20 15:17:33 +02:00
griffi-gh 7fbee66880 minor changes to netwrok code 2023-05-20 15:17:16 +02:00
griffi-gh 991601dfaf reasonable total connections value 2023-05-20 14:59:48 +02:00
griffi-gh 8e2bc5d35d add centered cube 2023-05-20 02:48:17 +02:00
griffi-gh 78689bbe1c player position sync, refactor some stuff 2023-05-20 02:32:32 +02:00
griffi-gh af356565a3 disable texture filtering experiment 2023-05-19 13:44:33 +02:00
griffi-gh b18756bb7e wip 2023-05-19 13:22:32 +02:00
griffi-gh 8d7b51707f add todo comments 2023-05-19 07:04:24 +02:00
griffi-gh 163298fe38 Big refactor + Early wip position sync /connect events in multiplayer 2023-05-19 07:02:20 +02:00
griffi-gh c0ce1c71d9 Move ClientIdMap to shared, switch to with_capacity_and_hasher where possible 2023-05-19 04:35:16 +02:00
griffi-gh cb18be7e2a test commit 2023-05-19 04:00:37 +02:00
griffi-gh 0edcc475ea Oops, checkout already skips tag creation... 2023-05-19 03:51:32 +02:00
griffi-gh 7b438de4f1 re-create tag manually to avoid force push 2023-05-19 03:45:50 +02:00
griffi-gh bbbd57516d 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 5dc6687461 should be working now 2023-05-19 03:16:56 +02:00
griffi-gh 6f2f37308e switch to rickstaa/action-create-tag@v1 2023-05-19 03:00:31 +02:00
griffi-gh 3936fbd40e hack: move tag before release 2023-05-19 02:44:15 +02:00
griffi-gh bc373dc05b fix release commit, prevent creating releases from prs 2023-05-19 02:34:14 +02:00
griffi-gh 0129f87825 Update README.md 2023-05-18 06:09:19 +03:00
griffi-gh 8c45e6c45c create zip files 2023-05-18 05:00:05 +02:00
griffi-gh 60ae7177b8 add perms to build.yml 2023-05-18 04:48:02 +02:00
griffi-gh 928fd2956c nightly release workflow 2023-05-18 04:42:52 +02:00
griffi-gh 13c6fce6e5 move handshake into separate module 2023-05-18 04:34:33 +02:00
griffi-gh 22f38298a0 always request latest gl, set min physical size to 640x480 2023-05-18 04:26:05 +02:00
griffi-gh f6e2f961c6 player movement, player connect events 2023-05-18 03:58:40 +02:00
griffi-gh 422613275c fix missing value 2023-05-17 15:36:04 +02:00
griffi-gh 95ed5f67b4 fix generate_visualizer_data 2023-05-17 13:11:26 +02:00
griffi-gh a74dfd6d00 :p 2023-05-17 12:58:05 +02:00
griffi-gh 6c8a7f8072 fix dependabot alert 2023-05-17 12:53:00 +02:00
griffi-gh 163d391ef9 fix artifact name 2023-05-17 12:32:55 +02:00
griffi-gh 11554c6b4b update workflow 2023-05-17 12:30:14 +02:00
griffi-gh 61df7072f0 add Server.toml to artifact 2023-05-17 00:28:55 +02:00
griffi-gh 967388f6c6 fix wrong dir name 2023-05-17 00:21:35 +02:00
griffi-gh 002613332d fix typo in workflow 2023-05-17 00:17:39 +02:00
griffi-gh f05caaa6b0 fix artifact structure 2023-05-17 00:16:49 +02:00
griffi-gh 0f13a2a239 fix workflow 2023-05-16 23:50:53 +02:00
griffi-gh 2b97bc1a8d install libudev-dev in build workflow 2023-05-16 23:42:05 +02:00
griffi-gh 885b69be39 update workflow/cargo config 2023-05-16 23:33:17 +02:00
griffi-gh e034ad6b5a Create build.yml 2023-05-16 22:45:11 +02:00
griffi-gh 8b3f463ada fix mistake in readme 2023-05-16 13:44:45 +02:00
griffi-gh 97260cb327 fix builds with nightly, update readme 2023-05-16 13:42:55 +02:00
griffi-gh 7ec922e480 enable more nightly stuff if nightly flag is active 2023-05-16 12:47:07 +02:00
griffi-gh 3e828855e6 Use git glium 2023-05-16 12:27:57 +02:00
griffi-gh 33a5b8c6ef add vsync option 2023-05-16 12:20:09 +02:00
griffi-gh c142b2fd47 add fullscreen mode, release shader compiler after loading shader prefabs 2023-05-16 12:08:56 +02:00
griffi-gh 41a97cbcef add pre_startup stage, use msaa settings in window init 2023-05-16 11:30:24 +02:00
griffi-gh 46a5da3b26 update deps, add options for some graphics-related settings, fix some warnings 2023-05-16 11:21:01 +02:00
griffi-gh 6f621d20a8 u 2023-03-15 02:32:26 +01:00
griffi-gh 7251dacedc handle disconnect 2023-03-14 01:36:34 +01:00
griffi-gh 5d8f253c3c enable keepalive 2023-03-13 02:34:09 +01:00
griffi-gh db1a48f876 update 2023-03-13 02:24:37 +01:00
griffi-gh 4361eff76f separate networking, fix warnings 2023-03-13 01:56:33 +01:00
griffi-gh 6375b397e9 send block updates across network 2023-03-12 17:58:24 +01:00
griffi-gh 30b636c88e remove irrelevant comment 2023-03-12 16:38:01 +01:00
griffi-gh aee4e5a2bf "fast decompress" turned out to be slower lol (stashed) 2023-03-11 17:19:03 +01:00
griffi-gh b6fa90602f update notice 2023-03-09 18:26:57 +01:00
griffi-gh d5d1b9ea27 add queue block messages 2023-03-09 18:24:06 +01:00
griffi-gh 4d0f8038be fix client 2023-03-09 18:00:32 +01:00
griffi-gh 940a43330a fix client decompression 2023-03-09 17:56:30 +01:00
griffi-gh 9cf4e81818 asss 2023-03-09 04:29:21 +01:00
griffi-gh 321e69f976 fix 2?? 2023-03-09 04:21:59 +01:00
griffi-gh 2a2e22cf72 fix 2023-03-09 04:17:04 +01:00
griffi-gh acbf1cbe77 Client decompression? hopefully 2023-03-09 04:15:59 +01:00
griffi-gh 87ca9a0f50 Move inj to networking 2023-03-09 04:09:17 +01:00
griffi-gh e151ee7a17 world compression (server only for now) 2023-03-09 04:06:46 +01:00
griffi-gh 3f39f11860 Finish upgrading server to uflow 2023-03-09 03:30:37 +01:00
griffi-gh 5723f07a26 Place dirt under trees 2023-03-09 01:33:16 +01:00
griffi-gh 7da7a3dc50 Fix river world generation 2023-03-09 01:27:17 +01:00
griffi-gh 647a458ff5 remove comments 2023-03-09 01:18:29 +01:00
griffi-gh f53f019659 temporary fix: make all workloads sequential :/ 2023-03-09 01:14:53 +01:00
griffi-gh e2c4d315d3 add generate_visualizer_data feature 2023-03-09 00:56:46 +01:00
griffi-gh 083f89c52a update 2023-03-09 00:46:23 +01:00
griffi-gh 112b74d864 Update README.md 2023-03-09 02:33:35 +03:00
griffi-gh 30c81a906f Merge pull request #2 from griffi-gh/multiplayer
Multiplayer
2023-03-09 00:33:19 +01:00
griffi-gh f57845671c Merge pull request #5 from griffi-gh/use-uflow
Use uflow
2023-03-09 00:32:30 +01:00
griffi-gh 93457ecdcf Disable parallel feature by default, recalc camera matrix 2023-03-09 00:31:19 +01:00
griffi-gh f5d1de39c6 wtf 2023-03-08 23:00:24 +01:00
griffi-gh 0c75c71f51 doesn't work? 2023-03-08 22:53:30 +01:00
griffi-gh 9b112e9b70 some big changes 2023-03-08 22:34:41 +01:00
griffi-gh 06521c27cc actually check the event type (facepalm) 2023-03-08 22:21:19 +01:00
griffi-gh c823aa12cc add log 2023-03-08 21:13:50 +01:00
griffi-gh d89d4bf4ca restore basic auth services 2023-03-08 21:10:48 +01:00
griffi-gh 1725c5a7c9 upgrade some server stuff to uflow 2023-03-08 20:26:51 +01:00
griffi-gh 56d9044203 change server stuff 2023-03-08 17:27:53 +01:00
griffi-gh aeba49f03a disable lint 2023-03-08 01:47:46 +01:00
griffi-gh a07aad39ca Client-side migrated 2023-03-08 01:31:38 +01:00
griffi-gh 9d122961b5 Remove kubi-udp 2023-03-07 23:36:04 +01:00
griffi-gh 91b0ef3193 ew 2023-03-07 21:29:05 +01:00
griffi-gh 6a9dc49529 make packet size a const 2023-03-07 21:18:59 +01:00
griffi-gh 4c31eab0f4 wip server chunks 2023-03-07 17:05:55 +01:00
griffi-gh 38d686e583 fix 2023-03-07 04:56:51 +01:00
griffi-gh 5754dc7485 server 2023-03-07 04:50:06 +01:00
griffi-gh ac5f39a49e basic netw 2023-03-07 03:05:02 +01:00
griffi-gh 1bb785df1d change comment 2023-03-07 02:28:16 +01:00
griffi-gh 8aa4de1e4c Make the outer system seq 2023-03-07 01:59:54 +01:00
griffi-gh 0e4c6a94ef wtf 2023-03-07 01:51:19 +01:00
griffi-gh ce4cc08d2f update glam to 0.23 2023-03-07 00:52:00 +01:00
griffi-gh a3f2eda52c create vscode settings.json 2023-03-07 00:46:12 +01:00
griffi-gh 552e039f3b Upgrade packages 2023-03-07 00:34:26 +01:00
griffi-gh 5f855bdaf1 changes to binary transparency meshing 2023-02-18 01:04:54 +01:00
griffi-gh 80635cd596 river generation 2023-02-18 00:58:31 +01:00
griffi-gh 4ccdb56803 fix order 2023-02-18 00:38:09 +01:00
griffi-gh 0c2e61bf26 improve worldgen 2023-02-18 00:37:17 +01:00
griffi-gh 8dc682c9e0 cave generation (no scaling yet!) 2023-02-17 23:05:55 +01:00
griffi-gh 91bc3f50a8 caves 2023-02-17 22:44:01 +01:00
griffi-gh 32a293922a Better tree gen 2023-02-17 22:25:53 +01:00
griffi-gh 7d46e46e88 soft updates 2023-02-17 22:03:45 +01:00
griffi-gh c908294004 even better trees 2023-02-16 04:08:00 +01:00
griffi-gh 257c75fd9d better trees 2023-02-16 04:05:27 +01:00
griffi-gh 58344cf838 Placement queue, binary transparency, WIP trees 2023-02-16 03:54:57 +01:00
griffi-gh 916f9058be rotate elevation noise 2023-02-16 02:57:04 +01:00
griffi-gh 9a7cbd4e2c Add planks 2023-02-16 02:44:21 +01:00
griffi-gh d4fbbc490f add tall grass 2023-02-16 02:00:09 +01:00
griffi-gh 4451461993 minor refactor 2023-02-15 23:32:06 +01:00
griffi-gh dd382c899f remove BlockDescriptorSource trait 2023-02-15 22:11:15 +01:00
griffi-gh b63f2e4075 inline descriptor 2023-02-15 22:08:48 +01:00
griffi-gh bcffaa4562 move block to shared 2023-02-15 22:07:47 +01:00
griffi-gh 496de05f27 reduce placement dst 2023-02-14 23:48:47 +01:00
griffi-gh 16a570803f disable selbox effect 2023-02-14 23:43:05 +01:00
griffi-gh 1739ce1488 world gen experimentation 2023-02-14 23:39:14 +01:00
griffi-gh 5924c2aa51 wip better world generation 2023-02-14 23:00:08 +01:00
griffi-gh e32ac5a2f1 oops Cargo.lock was in .gitignore 2023-02-14 20:01:11 +01:00
griffi-gh 5ad21e9f08 update shipyard to the lastest commit 2023-02-14 19:54:52 +01:00
griffi-gh cfd50d03af remove track(...) 2023-02-14 19:35:47 +01:00
griffi-gh 0dbb7e21ee Migrate to shipyard master 2023-02-14 19:31:20 +01:00
griffi-gh d583673081 ad entity to shared 2023-02-14 19:09:27 +01:00
griffi-gh 4a7c985ba1 wip 2023-02-14 04:52:11 +01:00
griffi-gh 539b6a1f91 add todo 2023-02-14 04:31:17 +01:00
griffi-gh e89747794b basic client auth 2023-02-14 04:27:27 +01:00
griffi-gh 71f89e8f28 wip connect 2023-02-13 04:12:02 +01:00
griffi-gh 72d8e17b43 set publish to false 2023-02-13 03:01:56 +01:00
griffi-gh 3cd3634618 implement broadcase and multicast functions 2023-02-13 02:57:09 +01:00
griffi-gh 7bb7b9b131 kick inactive clients 2023-02-13 02:30:33 +01:00
griffi-gh ed721ea91c add log to disconnect_inner 2023-02-13 02:03:09 +01:00
griffi-gh 1b476819d8 remove pub from functions 2023-02-13 02:01:25 +01:00
griffi-gh 2818e157b1 use AllStoragesView instead of AllStoragesViewMut 2023-02-13 01:57:30 +01:00
griffi-gh 48bc85835f change default protocol id 2023-02-13 01:56:14 +01:00
griffi-gh 8505ee2a0f protocol id 2023-02-13 01:53:55 +01:00
griffi-gh 944279f09f rename to connect_client_if_needed 2023-02-12 22:42:39 +01:00
griffi-gh 657661749a Disconnect only in multiplayer 2023-02-12 22:41:27 +01:00
griffi-gh 742bd11773 disconnect on exit 2023-02-12 22:38:51 +01:00
griffi-gh c52895f86d change default server port 2023-02-12 22:30:03 +01:00
griffi-gh 04d94383d0 better error handling on client 2023-02-12 22:28:44 +01:00
griffi-gh c08e02132e fix readme link 2023-02-12 22:19:53 +01:00
griffi-gh c769eaac85 update readme 2023-02-12 22:18:11 +01:00
griffi-gh c9fcf889c3 what the fuck 2023-02-12 22:15:09 +01:00
griffi-gh f3cdb841ea fix camera updates 2023-02-12 21:50:24 +01:00
griffi-gh 125b870a8b inline client functions 2023-02-12 21:42:53 +01:00
griffi-gh 053c199f51 replace .into_workload().run_if() with .run_if() 2023-02-12 21:40:42 +01:00
griffi-gh f9bd7c1992 ignore io errors on server 2023-02-12 21:31:21 +01:00
griffi-gh 80e9a344ff client networking calls 2023-02-12 20:37:06 +01:00
griffi-gh 7aafd95f5e todo 2023-02-12 18:52:17 +01:00
griffi-gh c2a9609da5 basic networking 2023-02-12 04:04:48 +01:00
griffi-gh 0c725d9408 it passes 2023-02-12 03:38:18 +01:00
griffi-gh 82a79c40ed fix 2023-02-12 03:22:42 +01:00
griffi-gh 22b4eff9f0 oops 2023-02-12 03:15:31 +01:00
griffi-gh e27e8de1aa change things 2023-02-12 02:56:50 +01:00
griffi-gh 34eba9c2bc test 2023-02-12 02:33:48 +01:00
griffi-gh b0b3587dcb client server changes and integration test (fails) 2023-02-12 02:15:00 +01:00
griffi-gh 55876fb796 some changes to server and shared crates 2023-02-12 01:32:59 +01:00
griffi-gh f0f17bdf74 server 2023-02-12 00:37:24 +01:00
griffi-gh 209dc15f81 wip 2023-02-10 22:11:18 +01:00
griffi-gh 0256c954f4 server 2023-02-10 22:05:10 +01:00
griffi-gh d5abf53ee0 separate logging into a library 2023-02-10 20:44:34 +01:00
griffi-gh 1c35573bba ecs server 2023-02-10 20:36:58 +01:00
griffi-gh 6922c7635e server 2023-02-10 20:26:03 +01:00
griffi-gh 2544386417 is_changing_state 2023-02-10 03:03:00 +01:00
griffi-gh 3ab78fa7bf typo 2023-02-10 03:00:08 +01:00
griffi-gh f4c93ed29a log after load finish 2023-02-10 02:46:34 +01:00
griffi-gh 1c2557d83f slow down chunk ops while loading 2023-02-10 02:44:17 +01:00
griffi-gh 41d7803a64 loading screen 2023-02-10 02:36:11 +01:00
griffi-gh 1ce3b94cd5 uwu 2023-02-10 01:47:37 +01:00
griffi-gh 1d82cf69ba use windows subsystem attach console on release builds 2023-02-10 01:20:54 +01:00
griffi-gh 63b0fad27b reorder 2023-02-10 00:57:57 +01:00
griffi-gh 205f67a2e8 assert renderer in init stage 2023-02-10 00:56:03 +01:00
griffi-gh 7edf3529db move assert_renderer to 2023-02-10 00:49:05 +01:00
griffi-gh 39b97eb990 gui changes, use glsl 3.0 es 2023-02-10 00:46:20 +01:00
griffi-gh c9cfa478d3 progressbar shader fix 2023-02-10 00:08:32 +01:00
griffi-gh fdb7cf80e7 ui transform works 2023-02-09 04:16:02 +01:00
griffi-gh 2f3eab4d1e why doesnt it work 2023-02-09 04:11:15 +01:00
griffi-gh 55d70cc02d fix default colors and color_hex function 2023-02-09 03:34:49 +01:00
griffi-gh 144cff3268 fix color-related code 2023-02-09 03:33:41 +01:00
griffi-gh ba2d2dea2c gui and progressbars 2023-02-09 03:31:36 +01:00
griffi-gh d974f8326e shaders for progressbar 2023-02-08 03:37:58 +01:00
griffi-gh bdab783360 gui files 2023-02-08 03:10:35 +01:00
griffi-gh 628457b0de fix 2023-02-08 03:09:04 +01:00
griffi-gh dc07f891b0 changed a lot of stuff, too lazy to write 2023-02-08 03:06:06 +01:00
griffi-gh e8d9eec0d4 wip 2023-02-08 02:29:29 +01:00
griffi-gh 3b79f996e2 serveeeeer 2023-02-08 01:55:50 +01:00
griffi-gh 197bb7b784 wip things 2023-02-06 21:43:22 +01:00
griffi-gh 99605766d9 server recv 2023-02-06 20:54:30 +01:00
griffi-gh 48e56f5275 undo 2023-02-06 20:18:16 +01:00
griffi-gh c8ff99e98c serializable trait 2023-02-06 19:30:26 +01:00
griffi-gh b44e08e954 wip 2023-02-06 19:19:02 +01:00
griffi-gh f62eaf2dc1 wip server 2023-02-06 03:48:43 +01:00
griffi-gh 9f861999e1 disable broadcasting 2023-02-06 02:41:34 +01:00
griffi-gh 2e66d0bda8 ... 2023-02-06 02:40:45 +01:00
griffi-gh 9eae5d780b update 2023-02-06 02:15:19 +01:00
griffi-gh c4fb530258 cobblestone 2023-02-05 01:43:47 +01:00
griffi-gh 1b85340d9c don't count discarded ops 2023-02-05 01:22:43 +01:00
griffi-gh 816d2f6077 Update neighbors 2023-02-05 01:18:30 +01:00
griffi-gh 392189fc14 block queue works 2023-02-05 00:58:25 +01:00
griffi-gh f05e221658 wip block queue 2023-02-05 00:42:37 +01:00
griffi-gh 5914d10498 comment 2023-02-04 22:27:19 +01:00
griffi-gh bbfde7f0eb add kick reason 2023-02-04 22:20:19 +01:00
griffi-gh da8ca65a3d . 2023-02-04 02:47:09 +01:00
griffi-gh 78edba4c02 client is almost finished... 2023-02-04 02:46:48 +01:00
griffi-gh 4a699c34f3 use anyhow::{...} 2023-02-03 19:39:22 +01:00
griffi-gh 0bd48bf46f id packets 2023-02-03 19:36:52 +01:00
griffi-gh f79d3c8e4d disconnect reason 2023-02-03 19:29:38 +01:00
griffi-gh d7b4951ac9 client wip 2023-02-02 02:13:32 +01:00
griffi-gh 9a29366322 wip 2023-02-02 01:54:12 +01:00
griffi-gh 557e738976 client 2023-02-02 01:40:48 +01:00
griffi-gh f9bb72b232 only normalize movement if len >= 1 2023-02-01 23:46:40 +01:00
griffi-gh a7e21406bc fix panic 2023-02-01 23:34:27 +01:00
griffi-gh 01341c1b71 x 2023-02-01 03:25:39 +01:00
griffi-gh ad811de4ca no drop check is needed 2023-02-01 03:25:12 +01:00
griffi-gh de1f2c4b16 client 2023-02-01 03:24:06 +01:00
griffi-gh 060533f831 create net lib 2023-02-01 03:16:23 +01:00
griffi-gh dfebd1d5a1 move config 2023-01-31 03:12:23 +01:00
griffi-gh f1a8b87672 use bincode derive instead of serde due to potential issues 2023-01-31 03:06:30 +01:00
griffi-gh a61f419f3b c 2023-01-31 02:51:54 +01:00
griffi-gh b855e57fae upd 2023-01-31 02:23:44 +01:00
griffi-gh fae78e32dd replace rkyv with serde and bincode
change to serde
2023-01-31 01:29:51 +01:00
griffi-gh 343b307aa6 CheckBytes 2023-01-30 20:23:00 +01:00
griffi-gh b765a74415 fix excessive logging 2023-01-30 04:32:33 +01:00
griffi-gh 97122bbd2b maybe this will work? 2023-01-30 04:23:28 +01:00
griffi-gh 00839d6b2e use xinput, invert y 2023-01-30 04:20:13 +01:00
griffi-gh 88b115732b fix stick input 2023-01-30 04:18:29 +01:00
griffi-gh d62aca68ad WIP controller support 2023-01-30 04:14:53 +01:00
griffi-gh 87abc6c215 use rkyv 2023-01-30 03:42:58 +01:00
griffi-gh 0ae2a8ac17 move more things 2023-01-30 03:23:39 +01:00
griffi-gh 80be3673a2 separate blocks into common 2023-01-30 02:45:35 +01:00
griffi-gh 6d41a5717b add kubi-shared as dep 2023-01-30 02:37:11 +01:00
griffi-gh 0fc668ae00 move assets folder 2023-01-30 01:50:14 +01:00
griffi-gh a791d7c3dc restructure 2023-01-30 01:46:22 +01:00
griffi-gh 4f700df1c5 empty commit to create pull request 2023-01-30 01:28:09 +01:00
griffi-gh 813f3ffb73 add chunk borders 2023-01-30 01:24:53 +01:00
griffi-gh 2c0638fca2 minor refactor, start working on chunk border rendering 2023-01-30 01:04:13 +01:00
griffi-gh 86af5b8195 move box to primitives 2023-01-30 00:39:31 +01:00
griffi-gh 708f532834 add rkyv 2023-01-30 00:29:57 +01:00
griffi-gh 04a239d04f decrease it a bit 2023-01-30 00:27:47 +01:00
griffi-gh ae55ad7359 Increase "epsilon" value for max pitch 2023-01-30 00:22:16 +01:00
griffi-gh ed300fc0b6 player move events 2023-01-30 00:21:03 +01:00
griffi-gh 74a7a9710d normalize 2023-01-29 23:19:24 +01:00
griffi-gh fc7792382c fix bug in neighbor code 2023-01-29 23:11:36 +01:00
griffi-gh 8e6c4e9a15 change opt levels 2023-01-29 22:36:49 +01:00
griffi-gh aa855b8089 Always unmark dirty 2023-01-29 02:16:26 +01:00
griffi-gh 63eb8682d2 exit on esc and cursor lock 2023-01-29 02:15:12 +01:00
griffi-gh 3b08f3e13d Minor refactor, add cursor lock module 2023-01-29 01:58:52 +01:00
griffi-gh 7e828c4f39 Remove empty crosshair module 2023-01-29 01:45:38 +01:00
griffi-gh 17497ce95a optimize deps 2023-01-29 01:37:13 +01:00
griffi-gh 021d79d568 fix a memory leak and most warnings 2023-01-29 01:28:00 +01:00
griffi-gh 17dbdc2757 minor refactor 2023-01-29 01:21:28 +01:00
griffi-gh 155e3b1e36 Merge pull request #1 from griffi-gh/ecs-rewrite
Merge Ecs rewrite
2023-01-28 23:45:18 +01:00
griffi-gh 6664aa60c6 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 # Generated by Cargo
# will have compiled files and executables # will have compiled files and executables
debug/ debug/
target/ target/
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries # These are backup files generated by rustfmt
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html **/*.rs.bk
Cargo.lock
# MSVC Windows builds of rustc generate these, which store debugging information
# These are backup files generated by rustfmt *.pdb
**/*.rs.bk
#old source
# MSVC Windows builds of rustc generate these, which store debugging information _src
*.pdb
_visualizer.json
#old source
_src *.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] [workspace]
name = "kubi" members = [
version = "0.1.0" "kubi",
edition = "2021" "kubi-server",
"kubi-shared",
"kubi-logging",
]
default-members = ["kubi"]
resolver = "2"
[dependencies] [profile.release-with-debug]
glium = "0.32" inherits = "release"
image = { version = "0.24", default_features = false, features = ["png"] } debug = true
log = "0.4"
env_logger = "0.10" [profile.dev]
strum = { version = "0.24", features = ["derive"] } opt-level = 1
glam = { version = "0.22", features = ["debug-glam-assert", "mint", "fast-math"] }
hashbrown = "0.13" [profile.dev.package."*"]
rayon = "1.6" opt-level = 1
shipyard = { version = "0.6", features = ["thread_local"] }
nohash-hasher = "0.2.0" [profile.dev.package.uflow]
anyhow = "1.0" opt-level = 3
flume = "0.10"
#once_cell = "1.17" [profile.dev.package.wgpu]
bracket-noise = "0.8" 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> <h1 align="center">Kubi</h1>
work in progress <p align="center">
<h6 align="right"><i>~ uwu</i></h6> 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_matrices,
update_frustum, update_frustum,
).into_workload() ).into_sequential_workload()
} }

View file

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