Compare commits
539 commits
254103975b
...
e7d81ce7cc
Author | SHA1 | Date | |
---|---|---|---|
griffi-gh | e7d81ce7cc | ||
griffi-gh | db05b0902d | ||
griffi-gh | 1b096c07ed | ||
griffi-gh | 34a0c8c648 | ||
griffi-gh | 1bf61500b3 | ||
griffi-gh | 7928745938 | ||
griffi-gh | 438b79e8f0 | ||
griffi-gh | eec672d665 | ||
griffi-gh | 3f0697d573 | ||
griffi-gh | 7eafea521f | ||
griffi-gh | ec23cedb7a | ||
griffi-gh | c98f504310 | ||
griffi-gh | 18e35b3ea4 | ||
griffi-gh | 75cb0ead6b | ||
griffi-gh | 487cf31843 | ||
griffi-gh | 7aa91b6414 | ||
griffi-gh | 196a7010b6 | ||
griffi-gh | c8cacafab5 | ||
griffi-gh | d738b228f2 | ||
griffi-gh | 811bd8d8b4 | ||
griffi-gh | 99cc2d1e72 | ||
griffi-gh | 85c64c9064 | ||
griffi-gh | 6e9e3fa445 | ||
griffi-gh | 8a4549efea | ||
griffi-gh | 6f25cb728f | ||
griffi-gh | 9a01ecd6f2 | ||
griffi-gh | 8fd1930ce6 | ||
griffi-gh | 674ec97a6e | ||
griffi-gh | f04542ac02 | ||
griffi-gh | dd386acea1 | ||
griffi-gh | d14b5e1b40 | ||
griffi-gh | f4e4886d33 | ||
griffi-gh | 3f768a8318 | ||
griffi-gh | baf037d4a5 | ||
griffi-gh | 2c2199d520 | ||
griffi-gh | 1d9fafd408 | ||
griffi-gh | 58e4eed14e | ||
griffi-gh | 90784e21dd | ||
griffi-gh | 260f4b4232 | ||
griffi-gh | 35ff06a439 | ||
griffi-gh | 7ac045f013 | ||
griffi-gh | 0b69377865 | ||
griffi-gh | 324270ed7d | ||
griffi-gh | 906f4882a2 | ||
griffi-gh | c32568af1c | ||
griffi-gh | 1d4cbcc2b1 | ||
griffi-gh | 8373d2be54 | ||
griffi-gh | eaad06863f | ||
griffi-gh | 0e22bccbac | ||
griffi-gh | 4ff284288e | ||
griffi-gh | e11ee19597 | ||
griffi-gh | 8f606b77c0 | ||
griffi-gh | 048e45628a | ||
griffi-gh | 7108afa662 | ||
griffi-gh | 11ad2cdc77 | ||
griffi-gh | dac3c10aee | ||
griffi-gh | e9000af878 | ||
griffi-gh | 49753ecc4c | ||
griffi-gh | 94fa5268fa | ||
griffi-gh | e3692bca94 | ||
griffi-gh | 3f8056b6ea | ||
griffi-gh | b596deeaed | ||
griffi-gh | ec592951bc | ||
griffi-gh | 656f124549 | ||
griffi-gh | 00b8a253bb | ||
griffi-gh | 875f131fe1 | ||
griffi-gh | 91326ce2dc | ||
griffi-gh | d620ba3840 | ||
griffi-gh | 73695dcd5a | ||
griffi-gh | 8790454b23 | ||
griffi-gh | 650ea55a14 | ||
griffi-gh | 8c728f9650 | ||
griffi-gh | 64475022e3 | ||
griffi-gh | 7620717368 | ||
griffi-gh | f157e2dab3 | ||
griffi-gh | 8e39fc24fd | ||
griffi-gh | a63deb5173 | ||
griffi-gh | 8c5b0aa47e | ||
griffi-gh | 8e907a9fbc | ||
griffi-gh | ce5dd6f011 | ||
griffi-gh | a5fae8ad2b | ||
griffi-gh | bb9107e912 | ||
griffi-gh | dd6f52edb5 | ||
griffi-gh | 6cde878a50 | ||
griffi-gh | 62c3c2105e | ||
griffi-gh | e1f1ba706c | ||
griffi-gh | 204bb882a6 | ||
griffi-gh | 610d309ead | ||
griffi-gh | 66d3ea656b | ||
griffi-gh | 772a8ea7db | ||
griffi-gh | 6f84d9014a | ||
griffi-gh | 6ee282e744 | ||
griffi-gh | 5d8906cfb1 | ||
griffi-gh | 8c4ef7f83f | ||
griffi-gh | b8c7dcc196 | ||
griffi-gh | dc1a641887 | ||
griffi-gh | e373aa758c | ||
griffi-gh | 907a5845fa | ||
griffi-gh | 740da98cbd | ||
griffi-gh | af3c938a03 | ||
griffi-gh | 50cc36e3d5 | ||
griffi-gh | 043bb873c4 | ||
griffi-gh | ec17768842 | ||
griffi-gh | 5c39900376 | ||
griffi-gh | 65bfa8fcc1 | ||
griffi-gh | 7ead258028 | ||
griffi-gh | f0270e3ce5 | ||
griffi-gh | 00edf0d272 | ||
griffi-gh | a63cc4bd17 | ||
griffi-gh | 80614e8461 | ||
griffi-gh | 3d8307b124 | ||
griffi-gh | 6853f4529e | ||
griffi-gh | 5d8cc1433f | ||
griffi-gh | 2548884354 | ||
griffi-gh | 2045b26544 | ||
griffi-gh | 5c40e68d66 | ||
griffi-gh | 14e9cb32f7 | ||
griffi-gh | f3a844bf55 | ||
griffi-gh | f97b922943 | ||
griffi-gh | 6a96d6c3d3 | ||
griffi-gh | 778c2b279e | ||
griffi-gh | 89ccd595ac | ||
griffi-gh | b34f1a94b1 | ||
griffi-gh | 1466f62b5a | ||
griffi-gh | 93e34b2c88 | ||
griffi-gh | 0ae87b6ddd | ||
griffi-gh | c162893fd0 | ||
griffi-gh | 4948d85e05 | ||
griffi-gh | 47839c03aa | ||
griffi-gh | a468e764c3 | ||
griffi-gh | 5e63d1b630 | ||
griffi-gh | f00b2081e6 | ||
griffi-gh | 748f12a1ac | ||
griffi-gh | 685cd43ada | ||
griffi-gh | 3f38351356 | ||
griffi-gh | 9f5b974f82 | ||
griffi-gh | bfdd506ae8 | ||
griffi-gh | 3a42c293dd | ||
griffi-gh | f5fd6d0b05 | ||
griffi-gh | ff6f89f92e | ||
griffi-gh | 15d1184451 | ||
griffi-gh | 726dfa4b52 | ||
griffi-gh | f15ae91155 | ||
griffi-gh | a42a3d95c3 | ||
griffi-gh | adbab2b2a6 | ||
griffi-gh | 6b33e9cdc9 | ||
griffi-gh | f19031662f | ||
griffi-gh | 491bb435cc | ||
griffi-gh | eaf4d98d65 | ||
griffi-gh | e1b41bbc79 | ||
griffi-gh | 5823f05943 | ||
griffi-gh | dea9b7c584 | ||
griffi-gh | 207f29e0ee | ||
griffi-gh | 0f93d0ca71 | ||
griffi-gh | ee1f3ced47 | ||
griffi-gh | 5bac932108 | ||
griffi-gh | 200092f52a | ||
griffi-gh | 1c52273ce2 | ||
griffi-gh | 769d7e84e9 | ||
griffi-gh | 0862ffaf04 | ||
griffi-gh | f7c1a58748 | ||
griffi-gh | 747576a6e7 | ||
griffi-gh | a5612d965f | ||
griffi-gh | 2e91b3a9ee | ||
griffi-gh | eb23bdb448 | ||
griffi-gh | bf5b05295d | ||
griffi-gh | 0af548320b | ||
griffi-gh | 16ffb6f786 | ||
griffi-gh | f4c2bcf998 | ||
griffi-gh | 672007ac7b | ||
griffi-gh | 9ba3b42991 | ||
griffi-gh | a1307b1abc | ||
griffi-gh | feac14c79c | ||
griffi-gh | f7f0ca4547 | ||
griffi-gh | cb22f4db59 | ||
griffi-gh | 72a12b5ad9 | ||
griffi-gh | 225cecf1fa | ||
griffi-gh | cc32082c21 | ||
griffi-gh | f8cd34443a | ||
griffi-gh | f56c7a4b8f | ||
griffi-gh | cf8263fee0 | ||
griffi-gh | b17519cbc3 | ||
griffi-gh | 0b210eac5d | ||
griffi-gh | 0fa723fc00 | ||
griffi-gh | a95eb96843 | ||
griffi-gh | 0f4264292c | ||
griffi-gh | 4e47901117 | ||
griffi-gh | 447a848009 | ||
griffi-gh | 63f862c2be | ||
griffi-gh | 4263c1ff6c | ||
griffi-gh | b28dca8721 | ||
griffi-gh | 3d8803f465 | ||
griffi-gh | 711942567f | ||
griffi-gh | 2d655ea8f1 | ||
griffi-gh | d909ff7424 | ||
griffi-gh | 245db03519 | ||
griffi-gh | 828694cf5a | ||
griffi-gh | 547759d6b2 | ||
griffi-gh | d852c48e4a | ||
griffi-gh | a53f6f9901 | ||
griffi-gh | b601aea288 | ||
griffi-gh | a5641e0d2e | ||
griffi-gh | 198dfb088e | ||
griffi-gh | d848f60301 | ||
griffi-gh | ac497b0651 | ||
griffi-gh | 567fd1d6aa | ||
griffi-gh | d9d3f78066 | ||
griffi-gh | 22c13eeaea | ||
griffi-gh | 09effab7c3 | ||
griffi-gh | 467e61cb70 | ||
griffi-gh | d75a5fa3cb | ||
griffi-gh | a3857d0586 | ||
griffi-gh | 447a06e9de | ||
griffi-gh | fe1427249b | ||
griffi-gh | 6f982d4308 | ||
griffi-gh | 3069c1c72f | ||
griffi-gh | 5e62e1781d | ||
griffi-gh | f7210e4dec | ||
griffi-gh | 7dc33c2dce | ||
griffi-gh | 517838e2ae | ||
griffi-gh | 5987484452 | ||
griffi-gh | ae7cc718cf | ||
griffi-gh | 06a49e1b93 | ||
griffi-gh | bc36bb8ce2 | ||
griffi-gh | 612e46f454 | ||
griffi-gh | c69ce9105c | ||
griffi-gh | 363dcddeca | ||
griffi-gh | 2656f5af21 | ||
griffi-gh | 3388dd6491 | ||
griffi-gh | dec20225cb | ||
griffi-gh | d50f4323b1 | ||
griffi-gh | 014e0e7824 | ||
griffi-gh | 5678b0c06f | ||
griffi-gh | 927337c86d | ||
griffi-gh | 2803ac03db | ||
griffi-gh | cab3f667e0 | ||
griffi-gh | 13516267cb | ||
griffi-gh | 4338c536ad | ||
griffi-gh | bd327c86f3 | ||
griffi-gh | 43ca458896 | ||
griffi-gh | e2bec4bf2e | ||
griffi-gh | 7ba5d739e7 | ||
griffi-gh | 9bdc38ccf0 | ||
griffi-gh | e40e394bd8 | ||
griffi-gh | 2cdc79cb18 | ||
griffi-gh | 9affee68c3 | ||
griffi-gh | abeef03d50 | ||
griffi-gh | cc1490db8a | ||
griffi-gh | 55f2fc07b3 | ||
griffi-gh | 3b87fc6423 | ||
griffi-gh | d1427bfb10 | ||
griffi-gh | cc9a829552 | ||
griffi-gh | b60eb42a81 | ||
griffi-gh | b3be350047 | ||
griffi-gh | 3a9a452fda | ||
griffi-gh | e4ce6f290e | ||
griffi-gh | 01cf42b2bf | ||
griffi-gh | e70165ba00 | ||
griffi-gh | 45043ac04b | ||
griffi-gh | d5e8b4c6b7 | ||
griffi-gh | 340a140ec9 | ||
griffi-gh | a9f17b878c | ||
griffi-gh | fbe605041b | ||
griffi-gh | 1625e9cce9 | ||
griffi-gh | 31a959ba1a | ||
griffi-gh | 2dfc915d41 | ||
griffi-gh | 356455a15e | ||
griffi-gh | 1987354b83 | ||
griffi-gh | 33c306f47a | ||
griffi-gh | 6718c56370 | ||
griffi-gh | 061fe46f2a | ||
griffi-gh | cfba245e11 | ||
griffi-gh | 072ffe6965 | ||
griffi-gh | 6e61fd687a | ||
griffi-gh | a3e5df0304 | ||
griffi-gh | 3eddda0886 | ||
griffi-gh | 8aaf64904b | ||
griffi-gh | df3791af17 | ||
griffi-gh | 50c25dac34 | ||
griffi-gh | dc8229f929 | ||
griffi-gh | 821cee541b | ||
griffi-gh | 0affc06e09 | ||
griffi-gh | 7ab7a36d85 | ||
griffi-gh | e8e3863d47 | ||
griffi-gh | 4dde1651ab | ||
griffi-gh | 14c66ee2eb | ||
griffi-gh | 75c6d127b7 | ||
griffi-gh | 73940d61ff | ||
griffi-gh | 4bf2b350c6 | ||
griffi-gh | 03fbb774b5 | ||
griffi-gh | 96a6693faa | ||
griffi-gh | 3a50065cc5 | ||
griffi-gh | 56849a82cc | ||
griffi-gh | ae4ed5d1f5 | ||
griffi-gh | 3e34c19234 | ||
griffi-gh | 8751ad99b7 | ||
griffi-gh | 702cdf1ee6 | ||
griffi-gh | 6795f67481 | ||
griffi-gh | b832c9dbec | ||
griffi-gh | 7e08d2a4c1 | ||
griffi-gh | b038c65e74 | ||
griffi-gh | 1856e10247 | ||
griffi-gh | 9a8ff135c7 | ||
griffi-gh | c9990eef6e | ||
griffi-gh | 18d71d2edd | ||
griffi-gh | 282fd7bdcb | ||
griffi-gh | 74bb3ee2eb | ||
griffi-gh | f920d27f3a | ||
griffi-gh | 2e5dc34507 | ||
griffi-gh | cef8bd9120 | ||
griffi-gh | bd6ebde832 | ||
griffi-gh | 5da64b33db | ||
griffi-gh | ba95b1820c | ||
griffi-gh | c1f3fe3db0 | ||
griffi-gh | 6bf051697d | ||
griffi-gh | 42f9bcc757 | ||
griffi-gh | 70effb9866 | ||
griffi-gh | 4ccb354e24 | ||
griffi-gh | 54061fcda5 | ||
griffi-gh | a87e069576 | ||
griffi-gh | dc9f396908 | ||
griffi-gh | 141f3ae0bd | ||
griffi-gh | b6803895c5 | ||
griffi-gh | fdaa43d370 | ||
griffi-gh | e81ebc75e3 | ||
griffi-gh | fb68f8d12d | ||
griffi-gh | ce8ad9098f | ||
griffi-gh | f15d2b1510 | ||
griffi-gh | 896d80e074 | ||
griffi-gh | 627acd40cc | ||
griffi-gh | 0a590bab22 | ||
griffi-gh | e9977c3aa4 | ||
griffi-gh | a719427eb5 | ||
griffi-gh | 944737c75c | ||
griffi-gh | 32155ff531 | ||
griffi-gh | e29ca7d202 | ||
griffi-gh | 5009ce1cfb | ||
griffi-gh | 23632c69b3 | ||
griffi-gh | a5ed520b40 | ||
griffi-gh | d239a59fc4 | ||
griffi-gh | 739e53c0a9 | ||
griffi-gh | 68bc848cbd | ||
griffi-gh | 7bcfc75dfe | ||
griffi-gh | 80c3f354f1 | ||
griffi-gh | 5e6e50124a | ||
griffi-gh | 615251f031 | ||
griffi-gh | f6eef9457b | ||
griffi-gh | 5a75e44beb | ||
griffi-gh | c4767e8be8 | ||
griffi-gh | 951d8e35e3 | ||
griffi-gh | 74533db52c | ||
griffi-gh | b96d57326f | ||
griffi-gh | 7bc81fc72a | ||
griffi-gh | 09b0a0160b | ||
griffi-gh | ec6402633c | ||
griffi-gh | 8cd8cf35a2 | ||
griffi-gh | 18587217fe | ||
griffi-gh | 0cb066b02b | ||
griffi-gh | 2189d114c7 | ||
griffi-gh | 02d3224740 | ||
griffi-gh | 3103b3647d | ||
griffi-gh | 51f113d162 | ||
griffi-gh | 45c9b20ede | ||
griffi-gh | b20c6fe552 | ||
griffi-gh | 56dd1a3bda | ||
griffi-gh | 34a50c62b0 | ||
griffi-gh | 765e360d44 | ||
griffi-gh | e5041aaa1a | ||
griffi-gh | 39d4fd129a | ||
griffi-gh | d7c3b6f6ba | ||
griffi-gh | ce454f0611 | ||
griffi-gh | a470543325 | ||
griffi-gh | f4bbfd3e8a | ||
griffi-gh | 639685e76b | ||
griffi-gh | a46c5baab8 | ||
griffi-gh | 12fee77087 | ||
griffi-gh | 16e7b7e588 | ||
griffi-gh | 71823c7ada | ||
griffi-gh | 6933987d7b | ||
griffi-gh | 95e3de7228 | ||
griffi-gh | 3f6abb75fd | ||
griffi-gh | ce687e8d36 | ||
griffi-gh | fc783d52dc | ||
griffi-gh | b7ec1d1165 | ||
griffi-gh | 9268a293a8 | ||
griffi-gh | fd54eb7e4e | ||
griffi-gh | 3448790daf | ||
griffi-gh | e2b8cbfdd5 | ||
griffi-gh | de4938ee6a | ||
griffi-gh | 4a84fec041 | ||
griffi-gh | f9b4628b49 | ||
griffi-gh | f8732abb5d | ||
griffi-gh | 626e9e654d | ||
griffi-gh | 7612ea8d8d | ||
griffi-gh | 287baf15ea | ||
griffi-gh | 2c374c6e23 | ||
griffi-gh | 00b3a90fd4 | ||
griffi-gh | f5a4e2a532 | ||
griffi-gh | c865835e8d | ||
griffi-gh | 0de70ed1b7 | ||
griffi-gh | 51ed3762cc | ||
griffi-gh | 3b8eddcad1 | ||
griffi-gh | e1cf0a1ed0 | ||
griffi-gh | 3209bf45d6 | ||
griffi-gh | 8a4339506e | ||
griffi-gh | a8142468a2 | ||
griffi-gh | 7b6d50abe5 | ||
griffi-gh | dada84e097 | ||
griffi-gh | 740fa04910 | ||
griffi-gh | 4440b07bbe | ||
griffi-gh | c284d5f7ed | ||
griffi-gh | 1b1f6c6426 | ||
griffi-gh | 41a1a5d00b | ||
griffi-gh | bccd3a8cfd | ||
griffi-gh | 95c681d0fc | ||
griffi-gh | 32963044f3 | ||
griffi-gh | f19683c1b1 | ||
griffi-gh | cc2175ec3e | ||
griffi-gh | b3990e64c9 | ||
griffi-gh | 6b9243a556 | ||
griffi-gh | a5b35ffa7d | ||
griffi-gh | 455b4580de | ||
griffi-gh | bda8e7b94c | ||
griffi-gh | 6b55688dbd | ||
griffi-gh | 80ef55c8b3 | ||
griffi-gh | 147fb03566 | ||
griffi-gh | 45d3bb7e98 | ||
griffi-gh | b8447bc121 | ||
griffi-gh | bf7fc475e2 | ||
griffi-gh | eabddf3019 | ||
griffi-gh | d28069e9e6 | ||
griffi-gh | e0872b331f | ||
griffi-gh | 8758938e7e | ||
griffi-gh | bcd3066c95 | ||
griffi-gh | 05444bd4b8 | ||
griffi-gh | 6c27d63bdf | ||
griffi-gh | 70aaba01b0 | ||
griffi-gh | 907e3164d7 | ||
griffi-gh | 7ecc4d5c24 | ||
griffi-gh | 00f90156bd | ||
griffi-gh | 22054c6143 | ||
griffi-gh | af62a3749b | ||
griffi-gh | f76fa8c70e | ||
griffi-gh | 5307ba0c9f | ||
griffi-gh | 6b65497263 | ||
griffi-gh | 3f47be435d | ||
griffi-gh | 055e6c3600 | ||
griffi-gh | 28a82cf69e | ||
griffi-gh | aada43e8dc | ||
griffi-gh | 8e9eeb1f30 | ||
griffi-gh | 35886d9bed | ||
griffi-gh | c06869c5cb | ||
griffi-gh | dbb92232bc | ||
griffi-gh | 2f23f39604 | ||
griffi-gh | 4e357af959 | ||
griffi-gh | 6354d8bab4 | ||
griffi-gh | 06e94ccb46 | ||
griffi-gh | 54004ee5de | ||
griffi-gh | 2fc8db6740 | ||
griffi-gh | 7c8a0fb66b | ||
griffi-gh | 50ef5c2d1e | ||
griffi-gh | e75693926a | ||
griffi-gh | da4a686100 | ||
griffi-gh | 6b34352c55 | ||
griffi-gh | 8ed0765c9f | ||
griffi-gh | 9f2e47df8c | ||
griffi-gh | a6a728ba10 | ||
griffi-gh | fb8aa9d528 | ||
griffi-gh | 1ae34def21 | ||
griffi-gh | f88e2733f3 | ||
griffi-gh | e96ae90b1d | ||
griffi-gh | 1df8c89330 | ||
griffi-gh | aa4b552e0f | ||
griffi-gh | dc06305956 | ||
griffi-gh | 44088f76e5 | ||
griffi-gh | d5efff860c | ||
griffi-gh | 4cece2a876 | ||
griffi-gh | b940a9d9d6 | ||
griffi-gh | 324dc5d43e | ||
griffi-gh | 102bc101e0 | ||
griffi-gh | 01b82b1094 | ||
griffi-gh | df8640718d | ||
griffi-gh | 34fa47acbe | ||
griffi-gh | 319cffbb4e | ||
griffi-gh | 00f54a2f5f | ||
griffi-gh | 53fc7dcd27 | ||
griffi-gh | c1f1ec028f | ||
griffi-gh | eff0b50546 | ||
griffi-gh | 4ac6250c04 | ||
griffi-gh | 1ca847b4fc | ||
griffi-gh | a1ff9e1d30 | ||
griffi-gh | 19bc67a78f | ||
griffi-gh | 127460347a | ||
griffi-gh | 1e1ab9d40e | ||
griffi-gh | 99b2848775 | ||
griffi-gh | 63b4091ddd | ||
griffi-gh | 3593fcd4d5 | ||
griffi-gh | 8ff2a828ad | ||
griffi-gh | 65ecf0de62 | ||
griffi-gh | e6ec24a55c | ||
griffi-gh | 728c468a70 | ||
griffi-gh | a7a20093d6 | ||
griffi-gh | 1c0409a9cf | ||
griffi-gh | 5c3062d13f | ||
griffi-gh | 6f45a0ee77 | ||
griffi-gh | 1f5b5853b2 | ||
griffi-gh | b0642028d1 | ||
griffi-gh | ab7c181345 | ||
griffi-gh | 1f8c6ea223 | ||
griffi-gh | 56d85252b0 | ||
griffi-gh | 3af40c28b8 | ||
griffi-gh | acbb159606 | ||
griffi-gh | f294e0e159 | ||
griffi-gh | 5b93fa7639 | ||
griffi-gh | db6c50ad8d | ||
griffi-gh | 59b91bc23d | ||
griffi-gh | c2b446ac7b | ||
griffi-gh | 478930e37c | ||
griffi-gh | af2a01fad7 | ||
griffi-gh | 1d55a80404 | ||
griffi-gh | 852fee3607 | ||
griffi-gh | 0b4363ef88 | ||
griffi-gh | e00af1cefb | ||
griffi-gh | ec42143154 | ||
griffi-gh | 8d72185a4e | ||
griffi-gh | f5a4bd9603 | ||
griffi-gh | 71154b07bc | ||
griffi-gh | 889c193a05 | ||
griffi-gh | 54449d5457 | ||
griffi-gh | bace24b13c | ||
griffi-gh | 419e08aa64 | ||
griffi-gh | a74296956e | ||
griffi-gh | 9d2c00b204 | ||
griffi-gh | d50d61100b | ||
griffi-gh | e11302c4c0 | ||
griffi-gh | 04e4b246fe | ||
griffi-gh | be8a0a4816 | ||
griffi-gh | 42a822494c | ||
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
|
||||
# will have compiled files and executables
|
||||
debug/
|
||||
target/
|
||||
|
||||
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
|
||||
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
|
||||
Cargo.lock
|
||||
|
||||
# These are backup files generated by rustfmt
|
||||
**/*.rs.bk
|
||||
|
||||
# MSVC Windows builds of rustc generate these, which store debugging information
|
||||
*.pdb
|
||||
|
||||
#old source
|
||||
_src
|
||||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
debug/
|
||||
target/
|
||||
|
||||
# These are backup files generated by rustfmt
|
||||
**/*.rs.bk
|
||||
|
||||
# MSVC Windows builds of rustc generate these, which store debugging information
|
||||
*.pdb
|
||||
|
||||
#old source
|
||||
_src
|
||||
|
||||
_visualizer.json
|
||||
|
||||
*.kubi
|
||||
|
||||
/*_log*.txt
|
||||
/*.log
|
||||
|
||||
# make sure build artifacts and binaries are not committed
|
||||
*.d
|
||||
*.pdb
|
||||
*.exe
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
*.rlib
|
||||
|
||||
#but keep the dxcompiler.dll/dxil.dll
|
||||
!dxcompiler.dll
|
||||
!dxil.dll
|
||||
|
||||
# blender backup files
|
||||
*.blend1
|
||||
|
|
BIN
.readme/game.gif
Normal file
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]
|
||||
name = "kubi"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
[workspace]
|
||||
members = [
|
||||
"kubi",
|
||||
"kubi-server",
|
||||
"kubi-shared",
|
||||
"kubi-logging",
|
||||
]
|
||||
default-members = ["kubi"]
|
||||
resolver = "2"
|
||||
|
||||
[dependencies]
|
||||
glium = "0.32"
|
||||
image = { version = "0.24", default_features = false, features = ["png"] }
|
||||
log = "0.4"
|
||||
env_logger = "0.10"
|
||||
strum = { version = "0.24", features = ["derive"] }
|
||||
glam = { version = "0.22", features = ["debug-glam-assert", "mint", "fast-math"] }
|
||||
hashbrown = "0.13"
|
||||
rayon = "1.6"
|
||||
shipyard = { version = "0.6", features = ["thread_local"] }
|
||||
nohash-hasher = "0.2.0"
|
||||
anyhow = "1.0"
|
||||
flume = "0.10"
|
||||
#once_cell = "1.17"
|
||||
bracket-noise = "0.8"
|
||||
[profile.release-with-debug]
|
||||
inherits = "release"
|
||||
debug = true
|
||||
|
||||
[profile.dev]
|
||||
opt-level = 1
|
||||
|
||||
[profile.dev.package."*"]
|
||||
opt-level = 1
|
||||
|
||||
[profile.dev.package.uflow]
|
||||
opt-level = 3
|
||||
|
||||
[profile.dev.package.wgpu]
|
||||
opt-level = 3
|
||||
|
||||
[profile.dev.package.wgpu-core]
|
||||
opt-level = 3
|
||||
|
||||
[profile.dev.package.wgpu-hal]
|
||||
opt-level = 3
|
||||
|
||||
[profile.dev.package.fastnoise-lite]
|
||||
opt-level = 3
|
||||
|
||||
[profile.dev.package.rayon]
|
||||
opt-level = 3
|
||||
|
||||
#this is cursed as fuck
|
||||
#enabling debug assertions here causes the game to abort
|
||||
[profile.dev.package.android-activity]
|
||||
debug-assertions = false
|
||||
|
||||
# [patch.'https://github.com/griffi-gh/hui']
|
||||
# hui = { path = "X:/Projects/hui/hui" }
|
||||
# hui-winit = { path = "X:/Projects/hui/hui-winit" }
|
||||
# hui-wgpu = { path = "X:/Projects/hui/hui-wgpu" }
|
||||
|
|
21
LICENSE
Normal file
|
@ -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>
|
||||
work in progress
|
||||
<h6 align="right"><i>~ uwu</i></h6>
|
||||
<h1 align="center">Kubi</h1>
|
||||
<p align="center">
|
||||
Voxel engine written in Rust
|
||||
</p>
|
||||
<div align="center">
|
||||
<img src=".readme/game.gif" width="512">
|
||||
</div>
|
||||
|
||||
<h2>features</h2>
|
||||
|
||||
<p>
|
||||
<ul>
|
||||
<li>multithreaded procedural world generation</li>
|
||||
<li>procedural structures and block queue</li>
|
||||
<li>multithreaded mesh generation</li>
|
||||
<li>cubic chunks (32x32x32)</li>
|
||||
<li>low-level OpenGL renderer, targetting OpenGL ES 3.0</li>
|
||||
<li>frustum culling</li>
|
||||
<li>multiplayer networking</li>
|
||||
<li>immediate ui system <sup><code>[1]</code></sup></li>
|
||||
<li>cross platform: windows, linux, osx, android <sup><code>[2]</code></sup></li>
|
||||
<li>universal input system: supports keyboard, mouse, gamepad and touch input <sup><code>[3]</code></sup></li>
|
||||
<li>support for semi-transparet blocks<sup><code>[4]</code></sup></li>
|
||||
</ul>
|
||||
<h6>
|
||||
<code>[1]</code> - developed out-of-tree <a href="https://github.com/griffi-gh/hUI">here</a> since 2024<br>
|
||||
<code>[2]</code> - android support is experimental<br>
|
||||
<code>[3]</code> - mouse and gamepad input is not supported on android<br>
|
||||
<code>[4]</code> - work in progress, may cause issues<br>
|
||||
</h6>
|
||||
</p>
|
||||
|
||||
<h2>download</h2>
|
||||
<a href="https://github.com/griffi-gh/kubi/releases/tag/nightly">Latest nightly release</a>
|
||||
|
||||
<h2>build for windows/linux</h2>
|
||||
|
||||
```bash
|
||||
cargo build -p kubi
|
||||
cargo run -p kubi
|
||||
#or, build with release mode optimizations:
|
||||
cargo run -p kubi --release
|
||||
```
|
||||
|
||||
<h2>build for android</h2>
|
||||
|
||||
please note that android support is highly experimental!\
|
||||
gamepad, mouse input is currently borked, as well as srgb, which leads to dark textures.
|
||||
|
||||
prerequisites: Android SDK, NDK, command line tools, platform-tools, latest JDK\
|
||||
(make sure that your `PATH`, `ANDROID_HOME` and `ANDROID_NDK_ROOT` variables are configured properly)
|
||||
|
||||
**Setup:**
|
||||
|
||||
latest unpublished (git) version of cargo-apk is required
|
||||
|
||||
```bash
|
||||
cargo install --git https://github.com/rust-mobile/cargo-apk cargo-apk
|
||||
rustup target add aarch64-linux-android
|
||||
```
|
||||
|
||||
**Build:**
|
||||
|
||||
`--no-default-features` is required for keyboard input!\
|
||||
(`prefer-raw-events` feature *must* be disabled on android)\
|
||||
Mouse input is not implemented, touch only!
|
||||
|
||||
```bash
|
||||
cargo apk build -p kubi --lib --no-default-features
|
||||
```
|
||||
|
||||
**Run on device (using adb):**
|
||||
|
||||
```bash
|
||||
cargo apk run -p kubi --lib --no-default-features
|
||||
```
|
||||
|
||||
<h2>touch controls</h2>
|
||||
|
||||
<img src=".readme/touch_controls.png" alt="touch control scheme" width="300">
|
||||
|
||||
- Left side: **Movement**
|
||||
- Rigth side: **Camera controls**
|
||||
- Bottom right corner:
|
||||
- **B** (e.g. place blocks)
|
||||
- **A** (e.g. break, attack)
|
||||
|
||||
<h2>mutiplayer</h2>
|
||||
|
||||
to join a multiplayer server, just pass the ip address as the first argument
|
||||
|
||||
```sh
|
||||
cargo run -p kubi -- 127.0.0.1:1234
|
||||
```
|
||||
|
||||
<h2>server configuration</h2>
|
||||
|
||||
```toml
|
||||
[server]
|
||||
address = "0.0.0.0:12345" # ip address to bind to
|
||||
max_clients = 32 # max amount of connected clients
|
||||
timeout_ms = 10000 # client timeout in ms
|
||||
|
||||
[world]
|
||||
seed = 0xfeb_face_dead_cafe # worldgen seed to use
|
||||
|
||||
[query]
|
||||
name = "Kubi Server" # server name
|
||||
```
|
||||
|
||||
<h2>"In-house" libraries</h2>
|
||||
|
||||
- [`hui`, `hui-glium`, `hui-winit`](https://github.com/griffi-gh/hui): semi-imm.mode backend-agnostic ui system\
|
||||
- [`kubi-logging`](kubi-logging) fancy custom formatter for `env-logger`
|
||||
|
||||
deprecated:
|
||||
|
||||
- ~~`kubi-udp`~~ eventually got replaced by `uflow` (https://github.com/lowquark/uflow) in #5
|
||||
- ~~`kubi-pool`~~ decided there's no need to replace rayon for now
|
||||
|
||||
<h6 align="right"><i>~ uwu</i></h6>
|
||||
|
|
11
Server.toml
Normal file
|
@ -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_frustum,
|
||||
).into_workload()
|
||||
).into_sequential_workload()
|
||||
}
|
|
@ -8,7 +8,7 @@
|
|||
// three layers of stolen code, yay!
|
||||
|
||||
use glam::{Vec3A, Vec4, Mat3A, vec3a, Vec3, vec4};
|
||||
use shipyard::{ViewMut, IntoIter, View};
|
||||
use shipyard::{ViewMut, IntoIter, View, track};
|
||||
use crate::transform::Transform;
|
||||
use super::Camera;
|
||||
|
||||
|
@ -122,9 +122,9 @@ fn intersection<const A: usize, const B: usize, const C: usize>(planes: &[Vec4;
|
|||
|
||||
pub fn update_frustum(
|
||||
mut cameras: ViewMut<Camera>,
|
||||
transforms: View<Transform>
|
||||
transforms: View<Transform, track::All>
|
||||
) {
|
||||
for (camera, _) in (&mut cameras, transforms.inserted_or_modified()).iter() {
|
||||
camera.frustum = Frustum::compute(camera);
|
||||
for (mut camera, _) in (&mut cameras, transforms.inserted_or_modified()).iter() {
|
||||
camera.frustum = Frustum::compute(&camera);
|
||||
}
|
||||
}
|
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
|
||||
}
|
||||
}
|