Compare commits
539 commits
e7d81ce7cc
...
254103975b
Author | SHA1 | Date | |
---|---|---|---|
griffi-gh | 254103975b | ||
griffi-gh | 7067f1202e | ||
griffi-gh | 06e23d463b | ||
griffi-gh | 13ac29c9a2 | ||
griffi-gh | fe25df309e | ||
griffi-gh | afe9dcf378 | ||
griffi-gh | b1e3e91bc4 | ||
griffi-gh | 282c27962b | ||
griffi-gh | 1e319de573 | ||
griffi-gh | d21104decc | ||
griffi-gh | ba406d812e | ||
griffi-gh | df750c8901 | ||
griffi-gh | 0d41a9f14d | ||
griffi-gh | 241dc8ea7e | ||
griffi-gh | 6431f4e7fe | ||
griffi-gh | 6654c0a69f | ||
griffi-gh | c4f60e2634 | ||
griffi-gh | d34fda3cd3 | ||
griffi-gh | a2a39e1edf | ||
griffi-gh | 8ae89f37b0 | ||
griffi-gh | dcfb9865eb | ||
griffi-gh | b0c47d8e82 | ||
griffi-gh | 641e3c921c | ||
griffi-gh | 66afd3057e | ||
griffi-gh | db8243cb90 | ||
griffi-gh | 5e1499cbf8 | ||
griffi-gh | 4cc1754951 | ||
griffi-gh | e09f5405d8 | ||
griffi-gh | f5f5d8b9b9 | ||
griffi-gh | a6e92e041c | ||
griffi-gh | 4f114be002 | ||
griffi-gh | c071986f2a | ||
griffi-gh | 0440f207cc | ||
griffi-gh | a7b403b0a5 | ||
griffi-gh | 840529511c | ||
griffi-gh | 3f359f433c | ||
griffi-gh | df808610ef | ||
griffi-gh | 230f10fc57 | ||
griffi-gh | 33c418ff6f | ||
griffi-gh | 72d03962f5 | ||
griffi-gh | bf4922c4b1 | ||
griffi-gh | 525ce0cd40 | ||
griffi-gh | e341a84b85 | ||
griffi-gh | eeb650eac0 | ||
griffi-gh | 535f7d6112 | ||
griffi-gh | 7d8c2b14c3 | ||
griffi-gh | f457f1034b | ||
griffi-gh | 4307a50a76 | ||
griffi-gh | bcaf011b88 | ||
griffi-gh | bae7af39d5 | ||
griffi-gh | faecf5a4a4 | ||
griffi-gh | 877e603fed | ||
griffi-gh | b665d1c004 | ||
griffi-gh | f0270aead3 | ||
griffi-gh | 3ed8d82a04 | ||
griffi-gh | c32d97b636 | ||
griffi-gh | f067a07d90 | ||
griffi-gh | 1d06621504 | ||
griffi-gh | 1558169c7d | ||
griffi-gh | da790a932a | ||
griffi-gh | 6a6ef1bfec | ||
griffi-gh | 338f11afc6 | ||
griffi-gh | 80abfc2768 | ||
griffi-gh | 6a218352ab | ||
griffi-gh | 0f55ff6210 | ||
griffi-gh | cfffb403a3 | ||
griffi-gh | 0fac4531e0 | ||
griffi-gh | b1de67339e | ||
griffi-gh | b487da6474 | ||
griffi-gh | 56fe70765f | ||
griffi-gh | b6c314e8f3 | ||
griffi-gh | a3bc619cb6 | ||
griffi-gh | 7d5b706d0d | ||
griffi-gh | 6ee3130f8a | ||
griffi-gh | dac6e56516 | ||
griffi-gh | 931eb572fc | ||
griffi-gh | aaf576a9f1 | ||
griffi-gh | 42c06cba33 | ||
griffi-gh | 2f7dcfabc8 | ||
griffi-gh | ca74c7799c | ||
griffi-gh | 0a352ac291 | ||
griffi-gh | 69e7ae23db | ||
griffi-gh | 4906e1bada | ||
griffi-gh | 5357cf0595 | ||
griffi-gh | 4a24f4ac56 | ||
griffi-gh | 8ebc94fd9e | ||
griffi-gh | 184be9393c | ||
griffi-gh | 748a0bf6d6 | ||
griffi-gh | d57787059a | ||
griffi-gh | b95be7fb2a | ||
griffi-gh | af5ed58442 | ||
griffi-gh | dc1d8db27d | ||
griffi-gh | f2114a45c2 | ||
griffi-gh | 266c13d30e | ||
griffi-gh | 3b12a3260d | ||
griffi-gh | 6931118931 | ||
griffi-gh | 61b63a55d2 | ||
griffi-gh | 6a721b0400 | ||
griffi-gh | fb0ff88c10 | ||
griffi-gh | caa7cf9aeb | ||
griffi-gh | 09af18dda0 | ||
griffi-gh | ee6a5dd2f9 | ||
griffi-gh | fd8ec3478a | ||
griffi-gh | 62618f4486 | ||
griffi-gh | 31b082b0c1 | ||
griffi-gh | 3006640be5 | ||
griffi-gh | d63966e618 | ||
griffi-gh | 8f5f8b07fb | ||
griffi-gh | e2bfa0c6f0 | ||
griffi-gh | b9c721f9b6 | ||
griffi-gh | bd6fef60a0 | ||
griffi-gh | 83353b4fd6 | ||
griffi-gh | d5b23f62a4 | ||
griffi-gh | 35b7d74129 | ||
griffi-gh | 895da317e8 | ||
griffi-gh | 4cae827233 | ||
griffi-gh | 81decc6202 | ||
griffi-gh | bc3db2c95e | ||
griffi-gh | c799d94e0a | ||
griffi-gh | 1b87553f79 | ||
griffi-gh | 8d673ed82f | ||
griffi-gh | f84fe5d022 | ||
griffi-gh | aa5a0c09cc | ||
griffi-gh | 93b99d2306 | ||
griffi-gh | 36cec2b1e5 | ||
griffi-gh | 71d089c784 | ||
griffi-gh | af2ae5da88 | ||
griffi-gh | 5f2f27396b | ||
griffi-gh | d49c858cd8 | ||
griffi-gh | f928970afd | ||
griffi-gh | 2ba011a89b | ||
griffi-gh | 01f642fcbb | ||
griffi-gh | 12a9d799d4 | ||
griffi-gh | 4727488fcd | ||
griffi-gh | 04d0b59dcf | ||
griffi-gh | ad4254e0d4 | ||
griffi-gh | 37d67bafb1 | ||
griffi-gh | 52d4560bd0 | ||
griffi-gh | 63e002364e | ||
griffi-gh | 362c880c64 | ||
griffi-gh | 3882e096bb | ||
griffi-gh | 9ffe7a795f | ||
griffi-gh | 82dd665008 | ||
griffi-gh | 1c0dc2a3dd | ||
griffi-gh | c9400343dd | ||
griffi-gh | c9ca85900d | ||
griffi-gh | 37e643e319 | ||
griffi-gh | c85b76acc1 | ||
griffi-gh | 84e79cf93f | ||
griffi-gh | 521b7f3ef2 | ||
griffi-gh | 31a2499d26 | ||
griffi-gh | b120e0449b | ||
griffi-gh | a6bb48dddd | ||
griffi-gh | ad55d93b6e | ||
griffi-gh | 175ca1a877 | ||
griffi-gh | 64e4dcfe01 | ||
griffi-gh | ce1cb63c01 | ||
griffi-gh | f783917945 | ||
griffi-gh | d955445dbd | ||
griffi-gh | b57467f7de | ||
griffi-gh | 894946f5b1 | ||
griffi-gh | 0ed687211d | ||
griffi-gh | 2e7cdceec7 | ||
griffi-gh | a9eba8cc29 | ||
griffi-gh | 736c0e09d5 | ||
griffi-gh | 7cd04247b8 | ||
griffi-gh | 519a4a2e0d | ||
griffi-gh | cbefcffb93 | ||
griffi-gh | c746c19068 | ||
griffi-gh | b3233915e6 | ||
griffi-gh | a0df0cc643 | ||
griffi-gh | 990f3787f7 | ||
griffi-gh | aa4f40a767 | ||
griffi-gh | 257f7bb130 | ||
griffi-gh | 356bfa1acb | ||
griffi-gh | cf26489f34 | ||
griffi-gh | 921b9fbb75 | ||
griffi-gh | c986e36f88 | ||
griffi-gh | b893193753 | ||
griffi-gh | 09a7426ac2 | ||
griffi-gh | de7c83adbc | ||
griffi-gh | a18537a915 | ||
griffi-gh | 3231a292ad | ||
griffi-gh | e78400ae82 | ||
griffi-gh | 4b840c5ff0 | ||
griffi-gh | cf56bf29fe | ||
griffi-gh | fef14911d5 | ||
griffi-gh | a6d091f4da | ||
griffi-gh | 7ff83b3cfc | ||
griffi-gh | 1b9d13b80b | ||
griffi-gh | 99feb1eae7 | ||
griffi-gh | 0106409c2e | ||
griffi-gh | 3aa64143e3 | ||
griffi-gh | fe50cd1af9 | ||
griffi-gh | de44cd67b2 | ||
griffi-gh | 36363ac15d | ||
griffi-gh | a064d571ac | ||
griffi-gh | 0941ca5cda | ||
griffi-gh | 60d05782bd | ||
griffi-gh | c849c0bfd6 | ||
griffi-gh | a8d5b4a752 | ||
griffi-gh | fcc19a58a5 | ||
griffi-gh | d4d7066526 | ||
griffi-gh | 6e0d93c23e | ||
griffi-gh | f42b1ea1bc | ||
griffi-gh | 82d1034f67 | ||
griffi-gh | fc9da59300 | ||
griffi-gh | 277fd4c27d | ||
griffi-gh | a78e01435f | ||
griffi-gh | 2a09002b46 | ||
griffi-gh | f6fd4ed20f | ||
griffi-gh | 701ab7434a | ||
griffi-gh | 87f4bd8df7 | ||
griffi-gh | 388614d92d | ||
griffi-gh | 792a2b7c55 | ||
griffi-gh | f36df25c0e | ||
griffi-gh | e5d1b1471a | ||
griffi-gh | 5de03912d2 | ||
griffi-gh | a6d642ce7a | ||
griffi-gh | d949d86ea8 | ||
griffi-gh | 8c0004e990 | ||
griffi-gh | 50aeb64c11 | ||
griffi-gh | fc23e8b827 | ||
griffi-gh | 5dbc52bba2 | ||
griffi-gh | 8183f4b0d0 | ||
griffi-gh | 11ec9d3ea9 | ||
griffi-gh | 8287a48f83 | ||
griffi-gh | bdcdd4ebd2 | ||
griffi-gh | 02009e6b59 | ||
griffi-gh | 003f2fd460 | ||
griffi-gh | 13816a6bd3 | ||
griffi-gh | 6864e65814 | ||
griffi-gh | 2777204982 | ||
griffi-gh | ad61b7db94 | ||
griffi-gh | 6eb7a3f690 | ||
griffi-gh | a1369b3520 | ||
griffi-gh | c1d1e512f0 | ||
griffi-gh | bbba9c6f6d | ||
griffi-gh | 3e9e914df3 | ||
griffi-gh | 264c72aeb0 | ||
griffi-gh | 93f3066ee0 | ||
griffi-gh | 298c959d73 | ||
griffi-gh | fa02f0bfe4 | ||
griffi-gh | 70fa0e1ebb | ||
griffi-gh | 63f27cbb71 | ||
griffi-gh | 5494dc61ed | ||
griffi-gh | 503e005fcf | ||
griffi-gh | b760a0f5b8 | ||
griffi-gh | 8ec98d639a | ||
griffi-gh | 4f37e7bfc6 | ||
griffi-gh | 260f7e83b8 | ||
griffi-gh | e5613ba05e | ||
griffi-gh | 0a2ed9a9a2 | ||
griffi-gh | bbea1659e1 | ||
griffi-gh | 48497c6e17 | ||
griffi-gh | da69fe2a0a | ||
griffi-gh | ea7fc85c6d | ||
griffi-gh | 3c9da07bc0 | ||
griffi-gh | 7b214b8ebb | ||
griffi-gh | 601e55fb9f | ||
griffi-gh | c919aeb81b | ||
griffi-gh | db2bb1b53e | ||
griffi-gh | 1c2f21d028 | ||
griffi-gh | 184fca2726 | ||
griffi-gh | 864ae29b3b | ||
griffi-gh | fe29489f7a | ||
griffi-gh | e42475570b | ||
griffi-gh | 3f77dfc099 | ||
griffi-gh | c8ff48d34b | ||
griffi-gh | c48afa107f | ||
griffi-gh | b3a85a85aa | ||
griffi-gh | 986c80111f | ||
griffi-gh | 2c4009f35c | ||
griffi-gh | 3159273495 | ||
griffi-gh | f33d1589b3 | ||
griffi-gh | ec7cddec8d | ||
griffi-gh | 0c0619a54b | ||
griffi-gh | 09920a2e55 | ||
griffi-gh | 51919ed327 | ||
griffi-gh | edd43d3684 | ||
griffi-gh | 2745a8c823 | ||
griffi-gh | aeaa3d175f | ||
griffi-gh | 76ec9fba32 | ||
griffi-gh | 7fbee66880 | ||
griffi-gh | 991601dfaf | ||
griffi-gh | 8e2bc5d35d | ||
griffi-gh | 78689bbe1c | ||
griffi-gh | af356565a3 | ||
griffi-gh | b18756bb7e | ||
griffi-gh | 8d7b51707f | ||
griffi-gh | 163298fe38 | ||
griffi-gh | c0ce1c71d9 | ||
griffi-gh | cb18be7e2a | ||
griffi-gh | 0edcc475ea | ||
griffi-gh | 7b438de4f1 | ||
griffi-gh | bbbd57516d | ||
griffi-gh | 5dc6687461 | ||
griffi-gh | 6f2f37308e | ||
griffi-gh | 3936fbd40e | ||
griffi-gh | bc373dc05b | ||
griffi-gh | 0129f87825 | ||
griffi-gh | 8c45e6c45c | ||
griffi-gh | 60ae7177b8 | ||
griffi-gh | 928fd2956c | ||
griffi-gh | 13c6fce6e5 | ||
griffi-gh | 22f38298a0 | ||
griffi-gh | f6e2f961c6 | ||
griffi-gh | 422613275c | ||
griffi-gh | 95ed5f67b4 | ||
griffi-gh | a74dfd6d00 | ||
griffi-gh | 6c8a7f8072 | ||
griffi-gh | 163d391ef9 | ||
griffi-gh | 11554c6b4b | ||
griffi-gh | 61df7072f0 | ||
griffi-gh | 967388f6c6 | ||
griffi-gh | 002613332d | ||
griffi-gh | f05caaa6b0 | ||
griffi-gh | 0f13a2a239 | ||
griffi-gh | 2b97bc1a8d | ||
griffi-gh | 885b69be39 | ||
griffi-gh | e034ad6b5a | ||
griffi-gh | 8b3f463ada | ||
griffi-gh | 97260cb327 | ||
griffi-gh | 7ec922e480 | ||
griffi-gh | 3e828855e6 | ||
griffi-gh | 33a5b8c6ef | ||
griffi-gh | c142b2fd47 | ||
griffi-gh | 41a97cbcef | ||
griffi-gh | 46a5da3b26 | ||
griffi-gh | 6f621d20a8 | ||
griffi-gh | 7251dacedc | ||
griffi-gh | 5d8f253c3c | ||
griffi-gh | db1a48f876 | ||
griffi-gh | 4361eff76f | ||
griffi-gh | 6375b397e9 | ||
griffi-gh | 30b636c88e | ||
griffi-gh | aee4e5a2bf | ||
griffi-gh | b6fa90602f | ||
griffi-gh | d5d1b9ea27 | ||
griffi-gh | 4d0f8038be | ||
griffi-gh | 940a43330a | ||
griffi-gh | 9cf4e81818 | ||
griffi-gh | 321e69f976 | ||
griffi-gh | 2a2e22cf72 | ||
griffi-gh | acbf1cbe77 | ||
griffi-gh | 87ca9a0f50 | ||
griffi-gh | e151ee7a17 | ||
griffi-gh | 3f39f11860 | ||
griffi-gh | 5723f07a26 | ||
griffi-gh | 7da7a3dc50 | ||
griffi-gh | 647a458ff5 | ||
griffi-gh | f53f019659 | ||
griffi-gh | e2c4d315d3 | ||
griffi-gh | 083f89c52a | ||
griffi-gh | 112b74d864 | ||
griffi-gh | 30c81a906f | ||
griffi-gh | f57845671c | ||
griffi-gh | 93457ecdcf | ||
griffi-gh | f5d1de39c6 | ||
griffi-gh | 0c75c71f51 | ||
griffi-gh | 9b112e9b70 | ||
griffi-gh | 06521c27cc | ||
griffi-gh | c823aa12cc | ||
griffi-gh | d89d4bf4ca | ||
griffi-gh | 1725c5a7c9 | ||
griffi-gh | 56d9044203 | ||
griffi-gh | aeba49f03a | ||
griffi-gh | a07aad39ca | ||
griffi-gh | 9d122961b5 | ||
griffi-gh | 91b0ef3193 | ||
griffi-gh | 6a9dc49529 | ||
griffi-gh | 4c31eab0f4 | ||
griffi-gh | 38d686e583 | ||
griffi-gh | 5754dc7485 | ||
griffi-gh | ac5f39a49e | ||
griffi-gh | 1bb785df1d | ||
griffi-gh | 8aa4de1e4c | ||
griffi-gh | 0e4c6a94ef | ||
griffi-gh | ce4cc08d2f | ||
griffi-gh | a3f2eda52c | ||
griffi-gh | 552e039f3b | ||
griffi-gh | 5f855bdaf1 | ||
griffi-gh | 80635cd596 | ||
griffi-gh | 4ccdb56803 | ||
griffi-gh | 0c2e61bf26 | ||
griffi-gh | 8dc682c9e0 | ||
griffi-gh | 91bc3f50a8 | ||
griffi-gh | 32a293922a | ||
griffi-gh | 7d46e46e88 | ||
griffi-gh | c908294004 | ||
griffi-gh | 257c75fd9d | ||
griffi-gh | 58344cf838 | ||
griffi-gh | 916f9058be | ||
griffi-gh | 9a7cbd4e2c | ||
griffi-gh | d4fbbc490f | ||
griffi-gh | 4451461993 | ||
griffi-gh | dd382c899f | ||
griffi-gh | b63f2e4075 | ||
griffi-gh | bcffaa4562 | ||
griffi-gh | 496de05f27 | ||
griffi-gh | 16a570803f | ||
griffi-gh | 1739ce1488 | ||
griffi-gh | 5924c2aa51 | ||
griffi-gh | e32ac5a2f1 | ||
griffi-gh | 5ad21e9f08 | ||
griffi-gh | cfd50d03af | ||
griffi-gh | 0dbb7e21ee | ||
griffi-gh | d583673081 | ||
griffi-gh | 4a7c985ba1 | ||
griffi-gh | 539b6a1f91 | ||
griffi-gh | e89747794b | ||
griffi-gh | 71f89e8f28 | ||
griffi-gh | 72d8e17b43 | ||
griffi-gh | 3cd3634618 | ||
griffi-gh | 7bb7b9b131 | ||
griffi-gh | ed721ea91c | ||
griffi-gh | 1b476819d8 | ||
griffi-gh | 2818e157b1 | ||
griffi-gh | 48bc85835f | ||
griffi-gh | 8505ee2a0f | ||
griffi-gh | 944279f09f | ||
griffi-gh | 657661749a | ||
griffi-gh | 742bd11773 | ||
griffi-gh | c52895f86d | ||
griffi-gh | 04d94383d0 | ||
griffi-gh | c08e02132e | ||
griffi-gh | c769eaac85 | ||
griffi-gh | c9fcf889c3 | ||
griffi-gh | f3cdb841ea | ||
griffi-gh | 125b870a8b | ||
griffi-gh | 053c199f51 | ||
griffi-gh | f9bd7c1992 | ||
griffi-gh | 80e9a344ff | ||
griffi-gh | 7aafd95f5e | ||
griffi-gh | c2a9609da5 | ||
griffi-gh | 0c725d9408 | ||
griffi-gh | 82a79c40ed | ||
griffi-gh | 22b4eff9f0 | ||
griffi-gh | e27e8de1aa | ||
griffi-gh | 34eba9c2bc | ||
griffi-gh | b0b3587dcb | ||
griffi-gh | 55876fb796 | ||
griffi-gh | f0f17bdf74 | ||
griffi-gh | 209dc15f81 | ||
griffi-gh | 0256c954f4 | ||
griffi-gh | d5abf53ee0 | ||
griffi-gh | 1c35573bba | ||
griffi-gh | 6922c7635e | ||
griffi-gh | 2544386417 | ||
griffi-gh | 3ab78fa7bf | ||
griffi-gh | f4c93ed29a | ||
griffi-gh | 1c2557d83f | ||
griffi-gh | 41d7803a64 | ||
griffi-gh | 1ce3b94cd5 | ||
griffi-gh | 1d82cf69ba | ||
griffi-gh | 63b0fad27b | ||
griffi-gh | 205f67a2e8 | ||
griffi-gh | 7edf3529db | ||
griffi-gh | 39b97eb990 | ||
griffi-gh | c9cfa478d3 | ||
griffi-gh | fdb7cf80e7 | ||
griffi-gh | 2f3eab4d1e | ||
griffi-gh | 55d70cc02d | ||
griffi-gh | 144cff3268 | ||
griffi-gh | ba2d2dea2c | ||
griffi-gh | d974f8326e | ||
griffi-gh | bdab783360 | ||
griffi-gh | 628457b0de | ||
griffi-gh | dc07f891b0 | ||
griffi-gh | e8d9eec0d4 | ||
griffi-gh | 3b79f996e2 | ||
griffi-gh | 197bb7b784 | ||
griffi-gh | 99605766d9 | ||
griffi-gh | 48e56f5275 | ||
griffi-gh | c8ff99e98c | ||
griffi-gh | b44e08e954 | ||
griffi-gh | f62eaf2dc1 | ||
griffi-gh | 9f861999e1 | ||
griffi-gh | 2e66d0bda8 | ||
griffi-gh | 9eae5d780b | ||
griffi-gh | c4fb530258 | ||
griffi-gh | 1b85340d9c | ||
griffi-gh | 816d2f6077 | ||
griffi-gh | 392189fc14 | ||
griffi-gh | f05e221658 | ||
griffi-gh | 5914d10498 | ||
griffi-gh | bbfde7f0eb | ||
griffi-gh | da8ca65a3d | ||
griffi-gh | 78edba4c02 | ||
griffi-gh | 4a699c34f3 | ||
griffi-gh | 0bd48bf46f | ||
griffi-gh | f79d3c8e4d | ||
griffi-gh | d7b4951ac9 | ||
griffi-gh | 9a29366322 | ||
griffi-gh | 557e738976 | ||
griffi-gh | f9bb72b232 | ||
griffi-gh | a7e21406bc | ||
griffi-gh | 01341c1b71 | ||
griffi-gh | ad811de4ca | ||
griffi-gh | de1f2c4b16 | ||
griffi-gh | 060533f831 | ||
griffi-gh | dfebd1d5a1 | ||
griffi-gh | f1a8b87672 | ||
griffi-gh | a61f419f3b | ||
griffi-gh | b855e57fae | ||
griffi-gh | fae78e32dd | ||
griffi-gh | 343b307aa6 | ||
griffi-gh | b765a74415 | ||
griffi-gh | 97122bbd2b | ||
griffi-gh | 00839d6b2e | ||
griffi-gh | 88b115732b | ||
griffi-gh | d62aca68ad | ||
griffi-gh | 87abc6c215 | ||
griffi-gh | 0ae2a8ac17 | ||
griffi-gh | 80be3673a2 | ||
griffi-gh | 6d41a5717b | ||
griffi-gh | 0fc668ae00 | ||
griffi-gh | a791d7c3dc | ||
griffi-gh | 4f700df1c5 | ||
griffi-gh | 813f3ffb73 | ||
griffi-gh | 2c0638fca2 | ||
griffi-gh | 86af5b8195 | ||
griffi-gh | 708f532834 | ||
griffi-gh | 04a239d04f | ||
griffi-gh | ae55ad7359 | ||
griffi-gh | ed300fc0b6 | ||
griffi-gh | 74a7a9710d | ||
griffi-gh | fc7792382c | ||
griffi-gh | 8e6c4e9a15 | ||
griffi-gh | aa855b8089 | ||
griffi-gh | 63eb8682d2 | ||
griffi-gh | 3b08f3e13d | ||
griffi-gh | 7e828c4f39 | ||
griffi-gh | 17497ce95a | ||
griffi-gh | 021d79d568 | ||
griffi-gh | 17dbdc2757 | ||
griffi-gh | 155e3b1e36 | ||
griffi-gh | 6664aa60c6 | ||
griffi-gh | 9bc51d709c |
11
.cargo/config.toml
Normal 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
|
@ -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
|
@ -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
After Width: | Height: | Size: 2.7 MiB |
BIN
.readme/touch_controls.png
Normal file
After Width: | Height: | Size: 12 KiB |
9
.vscode/settings.json
vendored
Normal 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
65
Cargo.toml
|
@ -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
|
@ -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
|
@ -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
|
@ -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"
|
BIN
assets-src/playermodel1.blend
Normal file
BIN
assets/blocks/cobblestone.png
Normal file
After Width: | Height: | Size: 578 B |
BIN
assets/blocks/planks.png
Normal file
After Width: | Height: | Size: 247 B |
BIN
assets/blocks/solid_water.png
Normal file
After Width: | Height: | Size: 210 B |
BIN
assets/blocks/water.png
Normal file
After Width: | Height: | Size: 341 B |
BIN
assets/fonts/Crisp.ttf
Normal file
64
assets/playermodel1.obj
Normal 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
After Width: | Height: | Size: 137 KiB |
14
kubi-logging/Cargo.toml
Normal 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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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()
|
||||||
|
}
|
28
kubi-server/src/world/chunk.rs
Normal 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()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
60
kubi-server/src/world/tasks.rs
Normal 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
|
@ -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
|
@ -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
|
@ -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
|
@ -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(§or) = 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
|
@ -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
|
@ -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
|
@ -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;
|
4
kubi-shared/src/networking.rs
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
pub mod messages;
|
||||||
|
pub mod state;
|
||||||
|
pub mod client;
|
||||||
|
pub mod channels;
|
17
kubi-shared/src/networking/channels.rs
Normal 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,
|
||||||
|
}
|
29
kubi-shared/src/networking/client.rs
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
143
kubi-shared/src/networking/messages.rs
Normal 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>
|
||||||
|
}
|
15
kubi-shared/src/networking/state.rs
Normal 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
|
@ -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
|
@ -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,
|
||||||
|
}
|
10
kubi-shared/src/transform.rs
Normal 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
|
@ -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)
|
||||||
|
}
|
6
kubi-shared/src/worldgen/steps.rs
Normal 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;
|
33
kubi-shared/src/worldgen/steps/_01_terrain.rs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
20
kubi-shared/src/worldgen/steps/_02_water.rs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
47
kubi-shared/src/worldgen/steps/_03_caves.rs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
36
kubi-shared/src/worldgen/steps/_04_layers.rs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
31
kubi-shared/src/worldgen/steps/_05_decorate.rs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
43
kubi-shared/src/worldgen/steps/_06_trees.rs
Normal 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
9
kubi-shared/src/worldgen/structures.rs
Normal 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);
|
||||||
|
}
|
58
kubi-shared/src/worldgen/structures/tree.rs
Normal 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
|
@ -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
|
@ -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;
|
||||||
|
}
|
50
kubi/shaders/entities.wgsl
Normal 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);
|
||||||
|
}
|
27
kubi/shaders/selection_box.wgsl
Normal 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
|
@ -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);
|
||||||
|
}
|
93
kubi/src/block_placement.rs
Normal 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()
|
||||||
|
}
|
|
@ -39,5 +39,5 @@ pub fn compute_cameras() -> Workload {
|
||||||
(
|
(
|
||||||
update_matrices,
|
update_matrices,
|
||||||
update_frustum,
|
update_frustum,
|
||||||
).into_workload()
|
).into_sequential_workload()
|
||||||
}
|
}
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
50
kubi/src/camera/matrices.rs
Normal 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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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>>();
|
||||||
|
}
|
39
kubi/src/events/player_actions.rs
Normal 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
|
@ -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)?))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
45
kubi/src/fixed_timestamp.rs
Normal 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());
|
||||||
|
}
|
51
kubi/src/hui_integration.rs
Normal 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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
||||||
|
}
|
131
kubi/src/networking/handshake.rs
Normal 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 });
|
||||||
|
}
|
146
kubi/src/networking/player.rs
Normal 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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
91
kubi/src/networking/world.rs
Normal 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
|
@ -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);
|
||||||
|
}
|
133
kubi/src/player_controller.rs
Normal 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
|
@ -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
|
@ -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
|
@ -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
|
||||||
|
// }
|
25
kubi/src/rendering/background.rs
Normal 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()
|
||||||
|
});
|
||||||
|
}
|
81
kubi/src/rendering/camera_uniform.rs
Normal 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() });
|
||||||
|
}
|
72
kubi/src/rendering/depth.rs
Normal 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);
|
||||||
|
}
|
67
kubi/src/rendering/entities.rs
Normal 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);
|
||||||
|
}
|
78
kubi/src/rendering/entities/instance.rs
Normal 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)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
66
kubi/src/rendering/entities/pipeline.rs
Normal 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,
|
||||||
|
})
|
||||||
|
}
|
42
kubi/src/rendering/primitives.rs
Normal 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],
|
||||||
|
};
|
||||||
|
}
|
50
kubi/src/rendering/primitives/cube.rs
Normal 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,
|
||||||
|
}));
|
||||||
|
}
|
24
kubi/src/rendering/primitives/fstri.rs
Normal 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));
|
||||||
|
}
|
177
kubi/src/rendering/renderer.rs
Normal 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
|
||||||
|
}
|
||||||
|
}
|