diff --git a/lib/rbx_loader/.gitignore b/lib/rbx_loader/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/lib/rbx_loader/.gitignore @@ -0,0 +1 @@ +/target diff --git a/lib/rbx_loader/Cargo.lock b/lib/rbx_loader/Cargo.lock new file mode 100644 index 0000000..192af3a --- /dev/null +++ b/lib/rbx_loader/Cargo.lock @@ -0,0 +1,792 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "array-init" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d62b7694a562cdf5a74227903507c56ab2cc8bdd1f781ed5cb4cf9c9f810bfc" + +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + +[[package]] +name = "binrw" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f36b7cb3ab9ff6a2858650d8dc360e783a5d14dc29594db48c56a3c233cc265" +dependencies = [ + "array-init", + "binrw_derive", + "bytemuck", +] + +[[package]] +name = "binrw_derive" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20ea7a8c5c8eeffffac6d54d172444e15beffac6f817fac714460a9a9aa88da3" +dependencies = [ + "either", + "owo-colors", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + +[[package]] +name = "blake3" +version = "1.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d82033247fd8e890df8f740e407ad4d038debb9eb1f40533fffb32e7d17dc6f7" +dependencies = [ + "arrayref", + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq", +] + +[[package]] +name = "bnum" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50202def95bf36cb7d1d7a7962cea1c36a3f8ad42425e5d2b71d7acb8041b5b8" + +[[package]] +name = "bstr" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40723b8fb387abc38f4f4a37c09073622e41dd12327033091ef8950659e6dc0c" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "bytemuck" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94bbb0ad554ad961ddc5da507a12a29b14e4ae5bda06b19f575a3e6079d2e2ae" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "cc" +version = "1.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812acba72f0a070b003d3697490d2b55b837230ae7c6c6497f05cc2ddbb8d938" +dependencies = [ + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "constant_time_eq" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" + +[[package]] +name = "either" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" + +[[package]] +name = "fixed_wide" +version = "0.1.1" +source = "sparse+https://git.itzana.me/api/packages/strafesnet/cargo/" +checksum = "d9c2cf115b3785ede870fada07e8b1aeba3378345b4ca86fe3c772ecabc05c0f" +dependencies = [ + "arrayvec", + "bnum", + "paste", + "ratio_ops", +] + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "glam" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28091a37a5d09b555cb6628fd954da299b536433834f5b8e59eba78e0cbbf8a" + +[[package]] +name = "id" +version = "0.1.0" +source = "sparse+https://git.itzana.me/api/packages/strafesnet/cargo/" +checksum = "2337e7a6c273082b672e377e159d7a168fb51438461b7c4033c79a515dd7a25a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.79", +] + +[[package]] +name = "lazy-regex" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d8e41c97e6bc7ecb552016274b99fbb5d035e8de288c582d9b933af6677bfda" +dependencies = [ + "lazy-regex-proc_macros", + "once_cell", + "regex", +] + +[[package]] +name = "lazy-regex-proc_macros" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76e1d8b05d672c53cb9c7b920bbba8783845ae4f0b076e02a3db1d02c81b4163" +dependencies = [ + "proc-macro2", + "quote", + "regex", + "syn 2.0.79", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.159" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5" + +[[package]] +name = "libloading" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4" +dependencies = [ + "cfg-if", + "windows-targets", +] + +[[package]] +name = "linear_ops" +version = "0.1.0" +source = "sparse+https://git.itzana.me/api/packages/strafesnet/cargo/" +checksum = "b2e6977ac24f47086d8a7a2d4ae1c720e86dfdc8407cf5e34c18bfa01053c456" +dependencies = [ + "fixed_wide", + "paste", + "ratio_ops", +] + +[[package]] +name = "log" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" + +[[package]] +name = "luau0-src" +version = "0.10.3+luau640" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f39d12b514a676c943990cfbe6200fedcb9c293c8c9219d29be512a6969be92" +dependencies = [ + "cc", +] + +[[package]] +name = "lz4" +version = "1.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d1febb2b4a79ddd1980eede06a8f7902197960aa0383ffcfdd62fe723036725" +dependencies = [ + "lz4-sys", +] + +[[package]] +name = "lz4-sys" +version = "1.11.1+lz4-1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bd8c0d6c6ed0cd30b3652886bb8711dc4bb01d637a68105a3d5158039b418e6" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "mlua" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d111deb18a9c9bd33e1541309f4742523bfab01d276bfa9a27519f6de9c11dc7" +dependencies = [ + "bstr", + "libloading", + "mlua-sys", + "num-traits", + "once_cell", + "rustc-hash", +] + +[[package]] +name = "mlua-sys" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe026d6bd1583a9cf9080e189030ddaea7e6f5f0deb366a8e26f8a26c4135b8" +dependencies = [ + "cc", + "cfg-if", + "luau0-src", + "pkg-config", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82881c4be219ab5faaf2ad5e5e5ecdff8c66bd7402ca3160975c93b24961afd1" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "owo-colors" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f" + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pkg-config" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" + +[[package]] +name = "portable-atomic" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc9c68a3f6da06753e9335d63e27f6b9754dd1920d941135b7ea8224f141adb2" + +[[package]] +name = "ppv-lite86" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "profiling" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d84d1d7a6ac92673717f9f6d1518374ef257669c24ebc5ac25d5033828be58" +dependencies = [ + "profiling-procmacros", +] + +[[package]] +name = "profiling-procmacros" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8021cf59c8ec9c432cfc2526ac6b8aa508ecaf29cd415f271b8406c1b851c3fd" +dependencies = [ + "quote", + "syn 2.0.79", +] + +[[package]] +name = "quote" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "ratio_ops" +version = "0.1.0" +source = "sparse+https://git.itzana.me/api/packages/strafesnet/cargo/" +checksum = "01239195d6afe0509e7e3511b716c0540251dfe7ece0a9a5a27116afb766c42c" + +[[package]] +name = "rbx_binary" +version = "0.7.4" +source = "sparse+https://git.itzana.me/api/packages/strafesnet/cargo/" +checksum = "18b401155b93f7151217bf51e36bdfa7bddcaf5f0d26b563c9ac3b08a3701c27" +dependencies = [ + "log", + "lz4", + "profiling", + "rbx_dom_weak", + "rbx_reflection", + "rbx_reflection_database", + "thiserror", +] + +[[package]] +name = "rbx_dom_weak" +version = "2.9.0" +source = "sparse+https://git.itzana.me/api/packages/strafesnet/cargo/" +checksum = "2a6b916687c98aaea36f9c03e80906bfafab057bebee248628c8c04def807f43" +dependencies = [ + "rbx_types", + "serde", +] + +[[package]] +name = "rbx_mesh" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "864ead0e98afce28c960f653d6203483834890d07f87b60e2f01415530a2fe9d" +dependencies = [ + "binrw", + "lazy-regex", +] + +[[package]] +name = "rbx_reflection" +version = "4.7.0" +source = "sparse+https://git.itzana.me/api/packages/strafesnet/cargo/" +checksum = "c1b43fe592a4ce6fe54eb215fb82735efbb516d2cc045a94e3dc0234ff293620" +dependencies = [ + "rbx_types", + "serde", + "thiserror", +] + +[[package]] +name = "rbx_reflection_database" +version = "0.2.12+roblox-638" +source = "sparse+https://git.itzana.me/api/packages/strafesnet/cargo/" +checksum = "2e772bb9e1bc0ebe65d338f876d1bb1ea22e15a8f9a82e8245028010c2fea3c9" +dependencies = [ + "lazy_static", + "rbx_reflection", + "rmp-serde", + "serde", +] + +[[package]] +name = "rbx_types" +version = "1.10.0" +source = "sparse+https://git.itzana.me/api/packages/strafesnet/cargo/" +checksum = "d7a390c44034fa448c53bd0983dfc2d70d8d6b2f65be4f164d4bec8b6a2a2d09" +dependencies = [ + "base64", + "bitflags 1.3.2", + "blake3", + "lazy_static", + "rand", + "serde", + "thiserror", +] + +[[package]] +name = "rbx_xml" +version = "0.13.3" +source = "sparse+https://git.itzana.me/api/packages/strafesnet/cargo/" +checksum = "d6d1a15f58a1e4b4f578abe6eb5e1461cb16eea82fb4a147d5995c9b79f08d1f" +dependencies = [ + "base64", + "log", + "rbx_dom_weak", + "rbx_reflection", + "rbx_reflection_database", + "xml-rs", +] + +[[package]] +name = "regex" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38200e5ee88914975b69f657f0801b6f6dccafd44fd9326302a4aaeecfacb1d8" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "rmp" +version = "0.8.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "228ed7c16fa39782c3b3468e974aec2795e9089153cd08ee2e9aefb3613334c4" +dependencies = [ + "byteorder", + "num-traits", + "paste", +] + +[[package]] +name = "rmp-serde" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52e599a477cf9840e92f2cde9a7189e67b42c57532749bf90aea6ec10facd4db" +dependencies = [ + "byteorder", + "rmp", + "serde", +] + +[[package]] +name = "roblox_emulator" +version = "0.4.0" +source = "sparse+https://git.itzana.me/api/packages/strafesnet/cargo/" +checksum = "df647d25a9bc8871a838a830ae90776cd31c44d1a74268bd783d1cadcd8fcd93" +dependencies = [ + "glam", + "mlua", + "rbx_dom_weak", + "rbx_reflection", + "rbx_reflection_database", + "rbx_types", +] + +[[package]] +name = "rustc-hash" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "583034fd73374156e66797ed8e5b0d5690409c9226b22d87cb7f19821c05d152" + +[[package]] +name = "serde" +version = "1.0.210" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.210" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.79", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "strafesnet_common" +version = "0.5.2" +source = "sparse+https://git.itzana.me/api/packages/strafesnet/cargo/" +checksum = "91cc1f3699bd8248da18bf5d11273264396a257b5d47b8558acb2cb4e1761219" +dependencies = [ + "arrayvec", + "bitflags 2.6.0", + "fixed_wide", + "glam", + "id", + "linear_ops", + "ratio_ops", +] + +[[package]] +name = "strafesnet_rbx_loader" +version = "0.5.2" +dependencies = [ + "bytemuck", + "glam", + "lazy-regex", + "rbx_binary", + "rbx_dom_weak", + "rbx_mesh", + "rbx_reflection_database", + "rbx_xml", + "roblox_emulator", + "strafesnet_common", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89132cd0bf050864e1d38dc3bbc07a0eb8e7530af26344d3d2bbbef83499f590" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "1.0.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.79", +] + +[[package]] +name = "unicode-ident" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "xml-rs" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af4e2e2f7cba5a093896c1e150fbfe177d1883e7448200efb81d40b9d339ef26" + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "byteorder", + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.79", +] diff --git a/lib/rbx_loader/Cargo.toml b/lib/rbx_loader/Cargo.toml new file mode 100644 index 0000000..13096e0 --- /dev/null +++ b/lib/rbx_loader/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "strafesnet_rbx_loader" +version = "0.5.2" +edition = "2021" +repository = "https://git.itzana.me/StrafesNET/rbx_loader" +license = "MIT OR Apache-2.0" +description = "Convert Roblox place and model files to StrafesNET data structures." +authors = ["Rhys Lloyd "] + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +bytemuck = "1.14.3" +glam = "0.29.0" +lazy-regex = "3.1.0" +rbx_binary = { version = "0.7.4", registry = "strafesnet" } +rbx_dom_weak = { version = "2.7.0", registry = "strafesnet" } +rbx_mesh = "0.1.2" +rbx_reflection_database = { version = "0.2.10", registry = "strafesnet" } +rbx_xml = { version = "0.13.3", registry = "strafesnet" } +roblox_emulator = { version = "0.4.0", registry = "strafesnet" } +strafesnet_common = { version = "0.5.1", registry = "strafesnet" } diff --git a/lib/rbx_loader/LICENSE-APACHE b/lib/rbx_loader/LICENSE-APACHE new file mode 100644 index 0000000..a7e77cb --- /dev/null +++ b/lib/rbx_loader/LICENSE-APACHE @@ -0,0 +1,176 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS \ No newline at end of file diff --git a/lib/rbx_loader/LICENSE-MIT b/lib/rbx_loader/LICENSE-MIT new file mode 100644 index 0000000..468cd79 --- /dev/null +++ b/lib/rbx_loader/LICENSE-MIT @@ -0,0 +1,23 @@ +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. \ No newline at end of file diff --git a/lib/rbx_loader/README.md b/lib/rbx_loader/README.md new file mode 100644 index 0000000..ad62509 --- /dev/null +++ b/lib/rbx_loader/README.md @@ -0,0 +1,19 @@ +StrafesNET Roblox Loader +======================== + +## Convert Roblox files into StrafesNET data structures + +#### License + + +Licensed under either of Apache License, Version +2.0 or MIT license at your option. + + +
+ + +Unless you explicitly state otherwise, any contribution intentionally submitted +for inclusion in this crate by you, as defined in the Apache-2.0 license, shall +be dual licensed as above, without any additional terms or conditions. + \ No newline at end of file diff --git a/lib/rbx_loader/src/lib.rs b/lib/rbx_loader/src/lib.rs new file mode 100644 index 0000000..06fbb07 --- /dev/null +++ b/lib/rbx_loader/src/lib.rs @@ -0,0 +1,107 @@ +use std::io::Read; +use rbx_dom_weak::WeakDom; + +mod rbx; +mod mesh; +mod primitives; + +pub mod data{ + pub struct RobloxMeshBytes(Vec); + impl RobloxMeshBytes{ + pub fn new(bytes:Vec)->Self{ + Self(bytes) + } + pub(crate) fn cursor(self)->std::io::Cursor>{ + std::io::Cursor::new(self.0) + } + } +} + +pub struct Model{ + dom:WeakDom, +} +impl Model{ + fn new(dom:WeakDom)->Self{ + Self{dom} + } + pub fn into_place(self)->Place{ + let Self{mut dom}=self; + let context=roblox_emulator::context::Context::from_mut(&mut dom); + let services=context.convert_into_place(); + Place{dom,services} + } +} +impl AsRef for Model{ + fn as_ref(&self)->&WeakDom{ + &self.dom + } +} + +pub struct Place{ + dom:WeakDom, + services:roblox_emulator::context::Services, +} +impl Place{ + fn new(dom:WeakDom)->Option{ + let context=roblox_emulator::context::Context::from_ref(&dom); + Some(Self{ + services:context.find_services()?, + dom, + }) + } + pub fn run_scripts(&mut self){ + let Place{dom,services}=self; + let runner=roblox_emulator::runner::Runner::new().unwrap(); + let context=roblox_emulator::context::Context::from_mut(dom); + let scripts=context.scripts(); + let runnable=runner.runnable_context_with_services(context,services).unwrap(); + for script in scripts{ + if let Err(e)=runnable.run_script(script){ + println!("runner error: {e}"); + } + } + } +} +impl AsRef for Place{ + fn as_ref(&self)->&WeakDom{ + &self.dom + } +} + +#[derive(Debug)] +pub enum ReadError{ + RbxBinary(rbx_binary::DecodeError), + RbxXml(rbx_xml::DecodeError), + Io(std::io::Error), + UnknownFileFormat, +} +impl std::fmt::Display for ReadError{ + fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{ + write!(f,"{self:?}") + } +} +impl std::error::Error for ReadError{} + +pub fn read(input:R)->Result{ + let mut buf=std::io::BufReader::new(input); + let peek=std::io::BufRead::fill_buf(&mut buf).map_err(ReadError::Io)?; + match &peek[0..8]{ + b"rbx_binary::from_reader(buf).map(Model::new).map_err(ReadError::RbxBinary), + b"rbx_xml::from_reader_default(buf).map(Model::new).map_err(ReadError::RbxXml), + _=>Err(ReadError::UnknownFileFormat), + } +} + +//ConvertError + +pub fn convert( + dom:impl AsRef, + acquire_render_config_id:AcquireRenderConfigId, + acquire_mesh_id:AcquireMeshId +)->rbx::PartialMap1 +where + AcquireRenderConfigId:FnMut(Option<&str>)->strafesnet_common::model::RenderConfigId, + AcquireMeshId:FnMut(&str)->strafesnet_common::model::MeshId, +{ + rbx::convert(&dom.as_ref(),acquire_render_config_id,acquire_mesh_id) +} diff --git a/lib/rbx_loader/src/mesh.rs b/lib/rbx_loader/src/mesh.rs new file mode 100644 index 0000000..ae5d161 --- /dev/null +++ b/lib/rbx_loader/src/mesh.rs @@ -0,0 +1,210 @@ +use std::collections::HashMap; + +use rbx_mesh::mesh::{Vertex2, Vertex2Truncated}; +use strafesnet_common::{integer::vec3,model::{self, ColorId, IndexedVertex, NormalId, PolygonGroup, PolygonList, PositionId, TextureCoordinateId, VertexId}}; + +#[derive(Debug)] +pub enum Error{ + Planar64Vec3(strafesnet_common::integer::Planar64TryFromFloatError), + RbxMesh(rbx_mesh::mesh::Error) +} +impl std::fmt::Display for Error{ + fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{ + write!(f,"{self:?}") + } +} +impl std::error::Error for Error{} + +fn ingest_vertices2< + AcquirePosId, + AcquireTexId, + AcquireNormalId, + AcquireColorId, + AcquireVertexId, +>( + vertices:Vec, + acquire_pos_id:&mut AcquirePosId, + acquire_tex_id:&mut AcquireTexId, + acquire_normal_id:&mut AcquireNormalId, + acquire_color_id:&mut AcquireColorId, + acquire_vertex_id:&mut AcquireVertexId, +)->Result,Error> +where + AcquirePosId:FnMut([f32;3])->Result, + AcquireTexId:FnMut([f32;2])->TextureCoordinateId, + AcquireNormalId:FnMut([f32;3])->Result, + AcquireColorId:FnMut([f32;4])->ColorId, + AcquireVertexId:FnMut(IndexedVertex)->VertexId, +{ + //this monster is collecting a map of old_vertices_index -> unique_vertices_index + //while also doing the inserting unique entries into lists simultaneously + Ok(vertices.into_iter().enumerate().map(|(vertex_id,vertex)|Ok(( + rbx_mesh::mesh::VertexId2(vertex_id as u32), + acquire_vertex_id(IndexedVertex{ + pos:acquire_pos_id(vertex.pos)?, + tex:acquire_tex_id(vertex.tex), + normal:acquire_normal_id(vertex.norm)?, + color:acquire_color_id(vertex.color.map(|f|f as f32/255.0f32)) + }), + ))).collect::>()?) +} +fn ingest_vertices_truncated2< + AcquirePosId, + AcquireTexId, + AcquireNormalId, + AcquireVertexId, +>( + vertices:Vec, + acquire_pos_id:&mut AcquirePosId, + acquire_tex_id:&mut AcquireTexId, + acquire_normal_id:&mut AcquireNormalId, + static_color_id:ColorId,//pick one color and fill everything with it + acquire_vertex_id:&mut AcquireVertexId, +)->Result,Error> +where + AcquirePosId:FnMut([f32;3])->Result, + AcquireTexId:FnMut([f32;2])->TextureCoordinateId, + AcquireNormalId:FnMut([f32;3])->Result, + AcquireVertexId:FnMut(IndexedVertex)->VertexId, +{ + //this monster is collecting a map of old_vertices_index -> unique_vertices_index + //while also doing the inserting unique entries into lists simultaneously + Ok(vertices.into_iter().enumerate().map(|(vertex_id,vertex)|Ok(( + rbx_mesh::mesh::VertexId2(vertex_id as u32), + acquire_vertex_id(IndexedVertex{ + pos:acquire_pos_id(vertex.pos)?, + tex:acquire_tex_id(vertex.tex), + normal:acquire_normal_id(vertex.norm)?, + color:static_color_id + }), + ))).collect::>()?) +} + +fn ingest_faces2_lods3( + polygon_groups:&mut Vec, + vertex_id_map:&HashMap, + faces:&Vec, + lods:&Vec +){ + //faces have to be split into polygon groups based on lod + polygon_groups.extend(lods.windows(2).map(|lod_pair| + PolygonGroup::PolygonList(PolygonList::new(faces[lod_pair[0].0 as usize..lod_pair[1].0 as usize].iter().map(|face| + vec![vertex_id_map[&face.0],vertex_id_map[&face.1],vertex_id_map[&face.2]] + ).collect())) + )) +} + +pub fn convert(roblox_mesh_bytes:crate::data::RobloxMeshBytes)->Result{ + //generate that mesh boi + let mut unique_pos=Vec::new(); + let mut pos_id_from=HashMap::new(); + let mut unique_tex=Vec::new(); + let mut tex_id_from=HashMap::new(); + let mut unique_normal=Vec::new(); + let mut normal_id_from=HashMap::new(); + let mut unique_color=Vec::new(); + let mut color_id_from=HashMap::new(); + let mut unique_vertices=Vec::new(); + let mut vertex_id_from=HashMap::new(); + let mut polygon_groups=Vec::new(); + let mut acquire_pos_id=|pos|{ + let p=vec3::try_from_f32_array(pos).map_err(Error::Planar64Vec3)?; + Ok(PositionId::new(*pos_id_from.entry(p).or_insert_with(||{ + let pos_id=unique_pos.len(); + unique_pos.push(p); + pos_id + }) as u32)) + }; + let mut acquire_tex_id=|tex|{ + let h=bytemuck::cast::<[f32;2],[u32;2]>(tex); + TextureCoordinateId::new(*tex_id_from.entry(h).or_insert_with(||{ + let tex_id=unique_tex.len(); + unique_tex.push(glam::Vec2::from_array(tex)); + tex_id + }) as u32) + }; + let mut acquire_normal_id=|normal|{ + let n=vec3::try_from_f32_array(normal).map_err(Error::Planar64Vec3)?; + Ok(NormalId::new(*normal_id_from.entry(n).or_insert_with(||{ + let normal_id=unique_normal.len(); + unique_normal.push(n); + normal_id + }) as u32)) + }; + let mut acquire_color_id=|color|{ + let h=bytemuck::cast::<[f32;4],[u32;4]>(color); + ColorId::new(*color_id_from.entry(h).or_insert_with(||{ + let color_id=unique_color.len(); + unique_color.push(glam::Vec4::from_array(color)); + color_id + }) as u32) + }; + let mut acquire_vertex_id=|vertex:IndexedVertex|{ + VertexId::new(*vertex_id_from.entry(vertex.clone()).or_insert_with(||{ + let vertex_id=unique_vertices.len(); + unique_vertices.push(vertex); + vertex_id + }) as u32) + }; + match rbx_mesh::read_versioned(roblox_mesh_bytes.cursor()).map_err(Error::RbxMesh)?{ + rbx_mesh::mesh::VersionedMesh::Version1(mesh)=>{ + let color_id=acquire_color_id([1.0f32;4]); + polygon_groups.push(PolygonGroup::PolygonList(PolygonList::new(mesh.vertices.chunks_exact(3).map(|trip|{ + let mut ingest_vertex1=|vertex:&rbx_mesh::mesh::Vertex1|Ok(acquire_vertex_id(IndexedVertex{ + pos:acquire_pos_id(vertex.pos)?, + tex:acquire_tex_id([vertex.tex[0],vertex.tex[1]]), + normal:acquire_normal_id(vertex.norm)?, + color:color_id, + })); + Ok(vec![ingest_vertex1(&trip[0])?,ingest_vertex1(&trip[1])?,ingest_vertex1(&trip[2])?]) + }).collect::>()?))); + }, + rbx_mesh::mesh::VersionedMesh::Version2(mesh)=>{ + let vertex_id_map=match mesh.header.sizeof_vertex{ + rbx_mesh::mesh::SizeOfVertex2::Truncated=>{ + //pick white and make all the vertices white + let color_id=acquire_color_id([1.0f32;4]); + ingest_vertices_truncated2(mesh.vertices_truncated,&mut acquire_pos_id,&mut acquire_tex_id,&mut acquire_normal_id,color_id,&mut acquire_vertex_id) + }, + rbx_mesh::mesh::SizeOfVertex2::Full=>ingest_vertices2(mesh.vertices,&mut acquire_pos_id,&mut acquire_tex_id,&mut acquire_normal_id,&mut acquire_color_id,&mut acquire_vertex_id), + }?; + //one big happy group for all the faces + polygon_groups.push(PolygonGroup::PolygonList(PolygonList::new(mesh.faces.into_iter().map(|face| + vec![vertex_id_map[&face.0],vertex_id_map[&face.1],vertex_id_map[&face.2]] + ).collect()))); + }, + rbx_mesh::mesh::VersionedMesh::Version3(mesh)=>{ + let vertex_id_map=match mesh.header.sizeof_vertex{ + rbx_mesh::mesh::SizeOfVertex2::Truncated=>{ + let color_id=acquire_color_id([1.0f32;4]); + ingest_vertices_truncated2(mesh.vertices_truncated,&mut acquire_pos_id,&mut acquire_tex_id,&mut acquire_normal_id,color_id,&mut acquire_vertex_id) + }, + rbx_mesh::mesh::SizeOfVertex2::Full=>ingest_vertices2(mesh.vertices,&mut acquire_pos_id,&mut acquire_tex_id,&mut acquire_normal_id,&mut acquire_color_id,&mut acquire_vertex_id), + }?; + ingest_faces2_lods3(&mut polygon_groups,&vertex_id_map,&mesh.faces,&mesh.lods); + }, + rbx_mesh::mesh::VersionedMesh::Version4(mesh)=>{ + let vertex_id_map=ingest_vertices2( + mesh.vertices,&mut acquire_pos_id,&mut acquire_tex_id,&mut acquire_normal_id,&mut acquire_color_id,&mut acquire_vertex_id + )?; + ingest_faces2_lods3(&mut polygon_groups,&vertex_id_map,&mesh.faces,&mesh.lods); + }, + rbx_mesh::mesh::VersionedMesh::Version5(mesh)=>{ + let vertex_id_map=ingest_vertices2( + mesh.vertices,&mut acquire_pos_id,&mut acquire_tex_id,&mut acquire_normal_id,&mut acquire_color_id,&mut acquire_vertex_id + )?; + ingest_faces2_lods3(&mut polygon_groups,&vertex_id_map,&mesh.faces,&mesh.lods); + }, + } + Ok(model::Mesh{ + unique_pos, + unique_normal, + unique_tex, + unique_color, + unique_vertices, + polygon_groups, + //these should probably be moved to the model... + graphics_groups:Vec::new(), + physics_groups:Vec::new(), + }) +} diff --git a/lib/rbx_loader/src/primitives.rs b/lib/rbx_loader/src/primitives.rs new file mode 100644 index 0000000..b70c859 --- /dev/null +++ b/lib/rbx_loader/src/primitives.rs @@ -0,0 +1,510 @@ +use strafesnet_common::model::{Color4,TextureCoordinate,Mesh,IndexedGraphicsGroup,IndexedPhysicsGroup,IndexedVertex,PolygonGroupId,PolygonGroup,PolygonList,IndexedVertexList,PositionId,TextureCoordinateId,NormalId,ColorId,VertexId,RenderConfigId}; +use strafesnet_common::integer::{vec3,Planar64Vec3}; + +#[derive(Debug)] +pub enum Primitives{ + Sphere, + Cube, + Cylinder, + Wedge, + CornerWedge, +} +#[derive(Hash,PartialEq,Eq)] +pub enum CubeFace{ + Right, + Top, + Back, + Left, + Bottom, + Front, +} +const CUBE_DEFAULT_TEXTURE_COORDS:[TextureCoordinate;4]=[ + TextureCoordinate::new(0.0,0.0), + TextureCoordinate::new(1.0,0.0), + TextureCoordinate::new(1.0,1.0), + TextureCoordinate::new(0.0,1.0), +]; +const CUBE_DEFAULT_VERTICES:[Planar64Vec3;8]=[ + vec3::int(-1,-1, 1),//0 left bottom back + vec3::int( 1,-1, 1),//1 right bottom back + vec3::int( 1, 1, 1),//2 right top back + vec3::int(-1, 1, 1),//3 left top back + vec3::int(-1, 1,-1),//4 left top front + vec3::int( 1, 1,-1),//5 right top front + vec3::int( 1,-1,-1),//6 right bottom front + vec3::int(-1,-1,-1),//7 left bottom front +]; +const CUBE_DEFAULT_NORMALS:[Planar64Vec3;6]=[ + vec3::int( 1, 0, 0),//CubeFace::Right + vec3::int( 0, 1, 0),//CubeFace::Top + vec3::int( 0, 0, 1),//CubeFace::Back + vec3::int(-1, 0, 0),//CubeFace::Left + vec3::int( 0,-1, 0),//CubeFace::Bottom + vec3::int( 0, 0,-1),//CubeFace::Front +]; +const CUBE_DEFAULT_POLYS:[[[u32;3];4];6]=[ + // right (1, 0, 0) + [ + [6,2,0],//[vertex,tex,norm] + [5,1,0], + [2,0,0], + [1,3,0], + ], + // top (0, 1, 0) + [ + [5,3,1], + [4,2,1], + [3,1,1], + [2,0,1], + ], + // back (0, 0, 1) + [ + [0,3,2], + [1,2,2], + [2,1,2], + [3,0,2], + ], + // left (-1, 0, 0) + [ + [0,2,3], + [3,1,3], + [4,0,3], + [7,3,3], + ], + // bottom (0,-1, 0) + [ + [1,1,4], + [0,0,4], + [7,3,4], + [6,2,4], + ], + // front (0, 0,-1) + [ + [4,1,5], + [5,0,5], + [6,3,5], + [7,2,5], + ], +]; + +#[derive(Hash,PartialEq,Eq)] +pub enum WedgeFace{ + Right, + TopFront, + Back, + Left, + Bottom, +} +const WEDGE_DEFAULT_NORMALS:[Planar64Vec3;5]=[ + vec3::int( 1, 0, 0),//Wedge::Right + vec3::int( 0, 1,-1),//Wedge::TopFront + vec3::int( 0, 0, 1),//Wedge::Back + vec3::int(-1, 0, 0),//Wedge::Left + vec3::int( 0,-1, 0),//Wedge::Bottom +]; +/* +local cornerWedgeVerticies = { + Vector3.new(-1/2,-1/2,-1/2),7 + Vector3.new(-1/2,-1/2, 1/2),0 + Vector3.new( 1/2,-1/2,-1/2),6 + Vector3.new( 1/2,-1/2, 1/2),1 + Vector3.new( 1/2, 1/2,-1/2),5 +} +*/ +#[derive(Hash,PartialEq,Eq)] +pub enum CornerWedgeFace{ + Right, + TopBack, + TopLeft, + Bottom, + Front, +} +const CORNERWEDGE_DEFAULT_NORMALS:[Planar64Vec3;5]=[ + vec3::int( 1, 0, 0),//CornerWedge::Right + vec3::int( 0, 1, 1),//CornerWedge::BackTop + vec3::int(-1, 1, 0),//CornerWedge::LeftTop + vec3::int( 0,-1, 0),//CornerWedge::Bottom + vec3::int( 0, 0,-1),//CornerWedge::Front +]; +pub fn unit_sphere(render:RenderConfigId)->Mesh{ + unit_cube(render) +} +#[derive(Default)] +pub struct CubeFaceDescription([Option;6]); +impl CubeFaceDescription{ + pub fn insert(&mut self,index:CubeFace,value:FaceDescription){ + self.0[index as usize]=Some(value); + } + pub fn pairs(self)->std::iter::FilterMap,6>>,impl FnMut((usize,Option))->Option<(usize,FaceDescription)>>{ + self.0.into_iter().enumerate().filter_map(|v|v.1.map(|u|(v.0,u))) + } +} +pub fn unit_cube(render:RenderConfigId)->Mesh{ + let mut t=CubeFaceDescription::default(); + t.insert(CubeFace::Right,FaceDescription::new_with_render_id(render)); + t.insert(CubeFace::Top,FaceDescription::new_with_render_id(render)); + t.insert(CubeFace::Back,FaceDescription::new_with_render_id(render)); + t.insert(CubeFace::Left,FaceDescription::new_with_render_id(render)); + t.insert(CubeFace::Bottom,FaceDescription::new_with_render_id(render)); + t.insert(CubeFace::Front,FaceDescription::new_with_render_id(render)); + generate_partial_unit_cube(t) +} +pub fn unit_cylinder(render:RenderConfigId)->Mesh{ + //lmao + unit_cube(render) +} +#[derive(Default)] +pub struct WedgeFaceDescription([Option;5]); +impl WedgeFaceDescription{ + pub fn insert(&mut self,index:WedgeFace,value:FaceDescription){ + self.0[index as usize]=Some(value); + } + pub fn pairs(self)->std::iter::FilterMap,5>>,impl FnMut((usize,Option))->Option<(usize,FaceDescription)>>{ + self.0.into_iter().enumerate().filter_map(|v|v.1.map(|u|(v.0,u))) + } +} +pub fn unit_wedge(render:RenderConfigId)->Mesh{ + let mut t=WedgeFaceDescription::default(); + t.insert(WedgeFace::Right,FaceDescription::new_with_render_id(render)); + t.insert(WedgeFace::TopFront,FaceDescription::new_with_render_id(render)); + t.insert(WedgeFace::Back,FaceDescription::new_with_render_id(render)); + t.insert(WedgeFace::Left,FaceDescription::new_with_render_id(render)); + t.insert(WedgeFace::Bottom,FaceDescription::new_with_render_id(render)); + generate_partial_unit_wedge(t) +} +#[derive(Default)] +pub struct CornerWedgeFaceDescription([Option;5]); +impl CornerWedgeFaceDescription{ + pub fn insert(&mut self,index:CornerWedgeFace,value:FaceDescription){ + self.0[index as usize]=Some(value); + } + pub fn pairs(self)->std::iter::FilterMap,5>>,impl FnMut((usize,Option))->Option<(usize,FaceDescription)>>{ + self.0.into_iter().enumerate().filter_map(|v|v.1.map(|u|(v.0,u))) + } +} +pub fn unit_cornerwedge(render:RenderConfigId)->Mesh{ + let mut t=CornerWedgeFaceDescription::default(); + t.insert(CornerWedgeFace::Right,FaceDescription::new_with_render_id(render)); + t.insert(CornerWedgeFace::TopBack,FaceDescription::new_with_render_id(render)); + t.insert(CornerWedgeFace::TopLeft,FaceDescription::new_with_render_id(render)); + t.insert(CornerWedgeFace::Bottom,FaceDescription::new_with_render_id(render)); + t.insert(CornerWedgeFace::Front,FaceDescription::new_with_render_id(render)); + generate_partial_unit_cornerwedge(t) +} + +#[derive(Clone)] +pub struct FaceDescription{ + pub render:RenderConfigId, + pub transform:glam::Affine2, + pub color:Color4, +} +impl FaceDescription{ + pub fn new_with_render_id(render:RenderConfigId)->Self { + Self{ + render, + transform:glam::Affine2::IDENTITY, + color:Color4::new(1.0,1.0,1.0,0.0),//zero alpha to hide the default texture + } + } +} +pub fn generate_partial_unit_cube(face_descriptions:CubeFaceDescription)->Mesh{ + let mut generated_pos=Vec::new(); + let mut generated_tex=Vec::new(); + let mut generated_normal=Vec::new(); + let mut generated_color=Vec::new(); + let mut generated_vertices=Vec::new(); + let mut polygon_groups=Vec::new(); + let mut graphics_groups=Vec::new(); + let mut physics_group=IndexedPhysicsGroup::default(); + let mut transforms=Vec::new(); + //note that on a cube every vertex is guaranteed to be unique, so there's no need to hash them against existing vertices. + for (face_id,face_description) in face_descriptions.pairs(){ + //assume that scanning short lists is faster than hashing. + let transform_index=if let Some(transform_index)=transforms.iter().position(|&transform|transform==face_description.transform){ + transform_index + }else{ + //create new transform_index + let transform_index=transforms.len(); + transforms.push(face_description.transform); + generated_tex.extend(CUBE_DEFAULT_TEXTURE_COORDS.map(|tex| + face_description.transform.transform_point2(tex) + )); + transform_index + } as u32; + let color_index=if let Some(color_index)=generated_color.iter().position(|&color|color==face_description.color){ + color_index + }else{ + //create new color_index + let color_index=generated_color.len(); + generated_color.push(face_description.color); + color_index + } as u32; + //always push normal + let normal_index=generated_normal.len() as u32; + generated_normal.push(CUBE_DEFAULT_NORMALS[face_id]); + //push vertices as they are needed + let group_id=PolygonGroupId::new(polygon_groups.len() as u32); + polygon_groups.push(PolygonGroup::PolygonList(PolygonList::new(vec![ + CUBE_DEFAULT_POLYS[face_id].map(|tup|{ + let pos=CUBE_DEFAULT_VERTICES[tup[0] as usize]; + let pos_index=if let Some(pos_index)=generated_pos.iter().position(|&p|p==pos){ + pos_index + }else{ + //create new pos_index + let pos_index=generated_pos.len(); + generated_pos.push(pos); + pos_index + } as u32; + //always push vertex + let vertex=IndexedVertex{ + pos:PositionId::new(pos_index), + tex:TextureCoordinateId::new(tup[1]+4*transform_index), + normal:NormalId::new(normal_index), + color:ColorId::new(color_index), + }; + let vert_index=generated_vertices.len(); + generated_vertices.push(vertex); + VertexId::new(vert_index as u32) + }).to_vec(), + ]))); + graphics_groups.push(IndexedGraphicsGroup{ + render:face_description.render, + groups:vec![group_id], + }); + physics_group.groups.push(group_id); + } + Mesh{ + unique_pos:generated_pos, + unique_tex:generated_tex, + unique_normal:generated_normal, + unique_color:generated_color, + unique_vertices:generated_vertices, + polygon_groups, + graphics_groups, + physics_groups:vec![physics_group], + } +} +//don't think too hard about the copy paste because this is all going into the map tool eventually... +pub fn generate_partial_unit_wedge(face_descriptions:WedgeFaceDescription)->Mesh{ + let wedge_default_polys=[ + // right (1, 0, 0) + vec![ + [6,2,0],//[vertex,tex,norm] + [2,0,0], + [1,3,0], + ], + // FrontTop (0, 1, -1) + vec![ + [3,1,1], + [2,0,1], + [6,3,1], + [7,2,1], + ], + // back (0, 0, 1) + vec![ + [0,3,2], + [1,2,2], + [2,1,2], + [3,0,2], + ], + // left (-1, 0, 0) + vec![ + [0,2,3], + [3,1,3], + [7,3,3], + ], + // bottom (0,-1, 0) + vec![ + [1,1,4], + [0,0,4], + [7,3,4], + [6,2,4], + ], + ]; + let mut generated_pos=Vec::new(); + let mut generated_tex=Vec::new(); + let mut generated_normal=Vec::new(); + let mut generated_color=Vec::new(); + let mut generated_vertices=Vec::new(); + let mut polygon_groups=Vec::new(); + let mut graphics_groups=Vec::new(); + let mut physics_group=IndexedPhysicsGroup::default(); + let mut transforms=Vec::new(); + //note that on a cube every vertex is guaranteed to be unique, so there's no need to hash them against existing vertices. + for (face_id,face_description) in face_descriptions.pairs(){ + //assume that scanning short lists is faster than hashing. + let transform_index=if let Some(transform_index)=transforms.iter().position(|&transform|transform==face_description.transform){ + transform_index + }else{ + //create new transform_index + let transform_index=transforms.len(); + transforms.push(face_description.transform); + generated_tex.extend(CUBE_DEFAULT_TEXTURE_COORDS.map(|tex| + face_description.transform.transform_point2(tex) + )); + transform_index + } as u32; + let color_index=if let Some(color_index)=generated_color.iter().position(|&color|color==face_description.color){ + color_index + }else{ + //create new color_index + let color_index=generated_color.len(); + generated_color.push(face_description.color); + color_index + } as u32; + //always push normal + let normal_index=generated_normal.len() as u32; + generated_normal.push(WEDGE_DEFAULT_NORMALS[face_id]); + //push vertices as they are needed + let group_id=PolygonGroupId::new(polygon_groups.len() as u32); + polygon_groups.push(PolygonGroup::PolygonList(PolygonList::new(vec![ + wedge_default_polys[face_id].iter().map(|tup|{ + let pos=CUBE_DEFAULT_VERTICES[tup[0] as usize]; + let pos_index=if let Some(pos_index)=generated_pos.iter().position(|&p|p==pos){ + pos_index + }else{ + //create new pos_index + let pos_index=generated_pos.len(); + generated_pos.push(pos); + pos_index + } as u32; + //always push vertex + let vertex=IndexedVertex{ + pos:PositionId::new(pos_index), + tex:TextureCoordinateId::new(tup[1]+4*transform_index), + normal:NormalId::new(normal_index), + color:ColorId::new(color_index), + }; + let vert_index=generated_vertices.len(); + generated_vertices.push(vertex); + VertexId::new(vert_index as u32) + }).collect() + ]))); + graphics_groups.push(IndexedGraphicsGroup{ + render:face_description.render, + groups:vec![group_id], + }); + physics_group.groups.push(group_id); + } + Mesh{ + unique_pos:generated_pos, + unique_tex:generated_tex, + unique_normal:generated_normal, + unique_color:generated_color, + unique_vertices:generated_vertices, + polygon_groups, + graphics_groups, + physics_groups:vec![physics_group], + } +} + +pub fn generate_partial_unit_cornerwedge(face_descriptions:CornerWedgeFaceDescription)->Mesh{ + let cornerwedge_default_polys=[ + // right (1, 0, 0) + vec![ + [6,2,0],//[vertex,tex,norm] + [5,1,0], + [1,3,0], + ], + // BackTop (0, 1, 1) + vec![ + [5,3,1], + [0,1,1], + [1,0,1], + ], + // LeftTop (-1, 1, 0) + vec![ + [5,3,2], + [7,2,2], + [0,1,2], + ], + // bottom (0,-1, 0) + vec![ + [1,1,3], + [0,0,3], + [7,3,3], + [6,2,3], + ], + // front (0, 0,-1) + vec![ + [5,0,4], + [6,3,4], + [7,2,4], + ], + ]; + let mut generated_pos=Vec::new(); + let mut generated_tex=Vec::new(); + let mut generated_normal=Vec::new(); + let mut generated_color=Vec::new(); + let mut generated_vertices=Vec::new(); + let mut polygon_groups=Vec::new(); + let mut graphics_groups=Vec::new(); + let mut physics_group=IndexedPhysicsGroup::default(); + let mut transforms=Vec::new(); + //note that on a cube every vertex is guaranteed to be unique, so there's no need to hash them against existing vertices. + for (face_id,face_description) in face_descriptions.pairs(){ + //assume that scanning short lists is faster than hashing. + let transform_index=if let Some(transform_index)=transforms.iter().position(|&transform|transform==face_description.transform){ + transform_index + }else{ + //create new transform_index + let transform_index=transforms.len(); + transforms.push(face_description.transform); + generated_tex.extend(CUBE_DEFAULT_TEXTURE_COORDS.map(|tex| + face_description.transform.transform_point2(tex) + )); + transform_index + } as u32; + let color_index=if let Some(color_index)=generated_color.iter().position(|&color|color==face_description.color){ + color_index + }else{ + //create new color_index + let color_index=generated_color.len(); + generated_color.push(face_description.color); + color_index + } as u32; + //always push normal + let normal_index=generated_normal.len() as u32; + generated_normal.push(CORNERWEDGE_DEFAULT_NORMALS[face_id]); + //push vertices as they are needed + let group_id=PolygonGroupId::new(polygon_groups.len() as u32); + polygon_groups.push(PolygonGroup::PolygonList(PolygonList::new(vec![ + cornerwedge_default_polys[face_id].iter().map(|tup|{ + let pos=CUBE_DEFAULT_VERTICES[tup[0] as usize]; + let pos_index=if let Some(pos_index)=generated_pos.iter().position(|&p|p==pos){ + pos_index + }else{ + //create new pos_index + let pos_index=generated_pos.len(); + generated_pos.push(pos); + pos_index + } as u32; + //always push vertex + let vertex=IndexedVertex{ + pos:PositionId::new(pos_index), + tex:TextureCoordinateId::new(tup[1]+4*transform_index), + normal:NormalId::new(normal_index), + color:ColorId::new(color_index), + }; + let vert_index=generated_vertices.len(); + generated_vertices.push(vertex); + VertexId::new(vert_index as u32) + }).collect(), + ]))); + graphics_groups.push(IndexedGraphicsGroup{ + render:face_description.render, + groups:vec![group_id], + }); + physics_group.groups.push(group_id); + } + Mesh{ + unique_pos:generated_pos, + unique_tex:generated_tex, + unique_normal:generated_normal, + unique_color:generated_color, + unique_vertices:generated_vertices, + polygon_groups, + graphics_groups, + physics_groups:vec![physics_group], + } +} diff --git a/lib/rbx_loader/src/rbx.rs b/lib/rbx_loader/src/rbx.rs new file mode 100644 index 0000000..0535724 --- /dev/null +++ b/lib/rbx_loader/src/rbx.rs @@ -0,0 +1,909 @@ +use std::collections::HashMap; +use crate::primitives; +use strafesnet_common::map; +use strafesnet_common::model; +use strafesnet_common::gameplay_modes; +use strafesnet_common::gameplay_style; +use strafesnet_common::gameplay_attributes as attr; +use strafesnet_common::integer::{self,vec3,Planar64,Planar64Vec3,Planar64Mat3,Planar64Affine3}; +use strafesnet_common::model::RenderConfigId; +use strafesnet_common::updatable::Updatable; + +fn class_is_a(class: &str, superclass: &str) -> bool { + if class==superclass { + return true + } + let class_descriptor=rbx_reflection_database::get().classes.get(class); + if let Some(descriptor) = &class_descriptor { + if let Some(class_super) = &descriptor.superclass { + return class_is_a(&class_super, superclass) + } + } + false +} +fn recursive_collect_superclass(objects: &mut std::vec::Vec,dom: &rbx_dom_weak::WeakDom, instance: &rbx_dom_weak::Instance, superclass: &str){ + let mut stack=vec![instance]; + while let Some(item)=stack.pop(){ + for &referent in item.children(){ + if let Some(c)=dom.get_by_ref(referent){ + if class_is_a(c.class.as_str(),superclass){ + objects.push(c.referent());//copy ref + } + stack.push(c); + } + } + } +} +fn planar64_affine3_from_roblox(cf:&rbx_dom_weak::types::CFrame,size:&rbx_dom_weak::types::Vector3)->Planar64Affine3{ + Planar64Affine3::new( + Planar64Mat3::from_cols([ + vec3::try_from_f32_array([cf.orientation.x.x,cf.orientation.y.x,cf.orientation.z.x]).unwrap() + *integer::try_from_f32(size.x/2.0).unwrap(), + vec3::try_from_f32_array([cf.orientation.x.y,cf.orientation.y.y,cf.orientation.z.y]).unwrap() + *integer::try_from_f32(size.y/2.0).unwrap(), + vec3::try_from_f32_array([cf.orientation.x.z,cf.orientation.y.z,cf.orientation.z.z]).unwrap() + *integer::try_from_f32(size.z/2.0).unwrap(), + ].map(|t|t.fix_1())), + vec3::try_from_f32_array([cf.position.x,cf.position.y,cf.position.z]).unwrap() + ) +} +struct ModeBuilder{ + mode:gameplay_modes::Mode, + final_stage_id_from_builder_stage_id:HashMap, +} +#[derive(Default)] +struct ModesBuilder{ + modes:HashMap, + stages:HashMap>, + mode_updates:Vec<(gameplay_modes::ModeId,gameplay_modes::ModeUpdate)>, + stage_updates:Vec<(gameplay_modes::ModeId,gameplay_modes::StageId,gameplay_modes::StageUpdate)>, +} +impl ModesBuilder{ + fn build(mut self)->gameplay_modes::Modes{ + //collect modes and stages into contiguous arrays + let mut unique_modes:Vec<(gameplay_modes::ModeId,gameplay_modes::Mode)> + =self.modes.into_iter().collect(); + unique_modes.sort_by_key(|&(mode_id,_)|mode_id); + let (mut modes,final_mode_id_from_builder_mode_id):(Vec,HashMap) + =unique_modes.into_iter().enumerate() + .map(|(final_mode_id,(builder_mode_id,mut mode))|{ + ( + ModeBuilder{ + final_stage_id_from_builder_stage_id:self.stages.remove(&builder_mode_id).map_or_else(||HashMap::new(),|stages|{ + let mut unique_stages:Vec<(gameplay_modes::StageId,gameplay_modes::Stage)> + =stages.into_iter().collect(); + unique_stages.sort_by(|a,b|a.0.cmp(&b.0)); + unique_stages.into_iter().enumerate() + .map(|(final_stage_id,(builder_stage_id,stage))|{ + mode.push_stage(stage); + (builder_stage_id,gameplay_modes::StageId::new(final_stage_id as u32)) + }).collect() + }), + mode, + }, + ( + builder_mode_id, + gameplay_modes::ModeId::new(final_mode_id as u32) + ) + ) + }).unzip(); + //TODO: failure messages or errors or something + //push stage updates + for (builder_mode_id,builder_stage_id,stage_update) in self.stage_updates{ + if let Some(final_mode_id)=final_mode_id_from_builder_mode_id.get(&builder_mode_id){ + if let Some(mode)=modes.get_mut(final_mode_id.get() as usize){ + if let Some(&final_stage_id)=mode.final_stage_id_from_builder_stage_id.get(&builder_stage_id){ + if let Some(stage)=mode.mode.get_stage_mut(final_stage_id){ + stage.update(stage_update); + } + } + } + } + } + //push mode updates + for (builder_mode_id,mut mode_update) in self.mode_updates{ + if let Some(final_mode_id)=final_mode_id_from_builder_mode_id.get(&builder_mode_id){ + if let Some(mode)=modes.get_mut(final_mode_id.get() as usize){ + //map stage id on stage elements + mode_update.map_stage_element_ids(|stage_id| + //walk down one stage id at a time until a stage is found + //TODO use better logic like BTreeMap::upper_bound instead of walking + // final_stage_id_from_builder_stage_id.upper_bound(Bound::Included(&stage_id)) + // .value().copied().unwrap_or(gameplay_modes::StageId::FIRST) + (0..=stage_id.get()).rev().find_map(|builder_stage_id| + //map the stage element to that stage + mode.final_stage_id_from_builder_stage_id.get(&gameplay_modes::StageId::new(builder_stage_id)).copied() + ).unwrap_or(gameplay_modes::StageId::FIRST) + ); + mode.mode.update(mode_update); + } + } + } + gameplay_modes::Modes::new(modes.into_iter().map(|mode_builder|mode_builder.mode).collect()) + } + fn insert_mode(&mut self,mode_id:gameplay_modes::ModeId,mode:gameplay_modes::Mode){ + assert!(self.modes.insert(mode_id,mode).is_none(),"Cannot replace existing mode"); + } + fn insert_stage(&mut self,mode_id:gameplay_modes::ModeId,stage_id:gameplay_modes::StageId,stage:gameplay_modes::Stage){ + assert!(self.stages.entry(mode_id).or_insert(HashMap::new()).insert(stage_id,stage).is_none(),"Cannot replace existing stage"); + } + fn push_mode_update(&mut self,mode_id:gameplay_modes::ModeId,mode_update:gameplay_modes::ModeUpdate){ + self.mode_updates.push((mode_id,mode_update)); + } + fn push_stage_update(&mut self,mode_id:gameplay_modes::ModeId,stage_id:gameplay_modes::StageId,stage_update:gameplay_modes::StageUpdate){ + self.stage_updates.push((mode_id,stage_id,stage_update)); + } +} +fn get_attributes(name:&str,can_collide:bool,velocity:Planar64Vec3,model_id:model::ModelId,modes_builder:&mut ModesBuilder,wormhole_in_model_to_id:&mut HashMap,wormhole_id_to_out_model:&mut HashMap)->attr::CollisionAttributes{ + let mut general=attr::GeneralAttributes::default(); + let mut intersecting=attr::IntersectingAttributes::default(); + let mut contacting=attr::ContactingAttributes::default(); + let mut force_can_collide=can_collide; + let mut force_intersecting=false; + match name{ + "Water"=>{ + force_can_collide=false; + //TODO: read stupid CustomPhysicalProperties + intersecting.water=Some(attr::IntersectingWater{density:Planar64::ONE,viscosity:Planar64::ONE/10,velocity}); + }, + "Accelerator"=>{ + //although the new game supports collidable accelerators, this is a roblox compatability map loader + force_can_collide=false; + general.accelerator=Some(attr::Accelerator{acceleration:velocity}); + }, + // "UnorderedCheckpoint"=>general.teleport_behaviour=Some(model::TeleportBehaviour::StageElement(attr::StageElement{ + // mode_id:0, + // stage_id:0, + // force:false, + // behaviour:model::StageElementBehaviour::Unordered + // })), + "SetVelocity"=>general.trajectory=Some(attr::SetTrajectory::Velocity(velocity)), + "MapStart"=>{ + force_can_collide=false; + force_intersecting=true; + modes_builder.insert_mode( + gameplay_modes::ModeId::MAIN, + gameplay_modes::Mode::empty( + gameplay_style::StyleModifiers::roblox_bhop(), + model_id + ) + ); + }, + "MapFinish"=>{ + force_can_collide=false; + force_intersecting=true; + modes_builder.push_mode_update( + gameplay_modes::ModeId::MAIN, + gameplay_modes::ModeUpdate::zone( + model_id, + gameplay_modes::Zone::Finish, + ), + ); + }, + "MapAnticheat"=>{ + force_can_collide=false; + force_intersecting=true; + modes_builder.push_mode_update( + gameplay_modes::ModeId::MAIN, + gameplay_modes::ModeUpdate::zone( + model_id, + gameplay_modes::Zone::Anticheat, + ), + ); + }, + "Platform"=>{ + modes_builder.push_mode_update( + gameplay_modes::ModeId::MAIN, + gameplay_modes::ModeUpdate::element( + model_id, + gameplay_modes::StageElement::new(gameplay_modes::StageId::FIRST,false,gameplay_modes::StageElementBehaviour::Platform,None),//roblox does not know which stage the platform belongs to + ), + ); + }, + other=>{ + let regman=lazy_regex::regex!(r"^(BonusStart|WormholeOut)(\d+)$"); + if let Some(captures)=regman.captures(other){ + match &captures[1]{ + "BonusStart"=>{ + force_can_collide=false; + force_intersecting=true; + modes_builder.insert_mode( + gameplay_modes::ModeId::new(captures[2].parse::().unwrap()), + gameplay_modes::Mode::empty( + gameplay_style::StyleModifiers::roblox_bhop(), + model_id + ) + ); + }, + "WormholeOut"=>{ + //the PhysicsModelId has to exist for it to be teleported to! + force_intersecting=true; + //this object is not special in strafe client, but the roblox mapping needs to be converted to model id + assert!(wormhole_id_to_out_model.insert(captures[2].parse::().unwrap(),model_id).is_none(),"Cannot have multiple WormholeOut with same id"); + }, + _=>(), + } + }else if let Some(captures)=lazy_regex::regex!(r"^(Force)?(Spawn|SpawnAt|Trigger|Teleport|Platform)(\d+)$") + .captures(other){ + force_intersecting=true; + let stage_id=gameplay_modes::StageId::new(captures[3].parse::().unwrap()); + let stage_element=gameplay_modes::StageElement::new( + //stage_id: + stage_id, + //force: + match captures.get(1){ + Some(m)=>m.as_str()=="Force", + None=>false, + }, + //behaviour: + match &captures[2]{ + "Spawn"=>{ + modes_builder.insert_stage( + gameplay_modes::ModeId::MAIN, + stage_id, + gameplay_modes::Stage::empty(model_id), + ); + //TODO: let denormalize handle this + gameplay_modes::StageElementBehaviour::SpawnAt + }, + "SpawnAt"=>gameplay_modes::StageElementBehaviour::SpawnAt, + //cancollide false so you don't hit the side + //NOT a decoration + "Trigger"=>{force_can_collide=false;gameplay_modes::StageElementBehaviour::Trigger}, + "Teleport"=>{force_can_collide=false;gameplay_modes::StageElementBehaviour::Teleport}, + "Platform"=>gameplay_modes::StageElementBehaviour::Platform, + _=>panic!("regex1[2] messed up bad"), + }, + None + ); + modes_builder.push_mode_update( + gameplay_modes::ModeId::MAIN, + gameplay_modes::ModeUpdate::element( + model_id, + stage_element, + ), + ); + }else if let Some(captures)=lazy_regex::regex!(r"^(Jump|WormholeIn)(\d+)$") + .captures(other){ + match &captures[1]{ + "Jump"=>modes_builder.push_mode_update( + gameplay_modes::ModeId::MAIN, + gameplay_modes::ModeUpdate::element( + model_id, + //jump_limit: + gameplay_modes::StageElement::new( + gameplay_modes::StageId::FIRST, + false, + gameplay_modes::StageElementBehaviour::Check, + Some(captures[2].parse::().unwrap()) + ) + ), + ), + "WormholeIn"=>{ + force_can_collide=false; + force_intersecting=true; + assert!(wormhole_in_model_to_id.insert(model_id,captures[2].parse::().unwrap()).is_none(),"Impossible"); + }, + _=>panic!("regex2[1] messed up bad"), + } + }else if let Some(captures)=lazy_regex::regex!(r"^Bonus(Finish|Anticheat)(\d+)$") + .captures(other){ + force_can_collide=false; + force_intersecting=true; + modes_builder.push_mode_update( + gameplay_modes::ModeId::new(captures[2].parse::().unwrap()), + gameplay_modes::ModeUpdate::zone( + model_id, + //zone: + match &captures[1]{ + "Finish"=>gameplay_modes::Zone::Finish, + "Anticheat"=>gameplay_modes::Zone::Anticheat, + _=>panic!("regex3[1] messed up bad"), + }, + ), + ); + } + // else if let Some(captures)=lazy_regex::regex!(r"^Stage(\d+)OrderedCheckpoint(\d+)$") + // .captures(other){ + // match &captures[1]{ + // "OrderedCheckpoint"=>modes_builder.push_stage_update( + // gameplay_modes::ModeId::MAIN, + // gameplay_modes::StageId::new(0), + // gameplay_modes::StageUpdate::ordered_checkpoint(captures[2].parse::().unwrap()), + // ), + // _=>panic!("regex3[1] messed up bad"), + // } + // } + } + } + //need some way to skip this + if velocity!=vec3::ZERO{ + general.booster=Some(attr::Booster::Velocity(velocity)); + } + match force_can_collide{ + true=>{ + match name{ + "Bounce"=>contacting.contact_behaviour=Some(attr::ContactingBehaviour::Elastic(u32::MAX)), + "Surf"=>contacting.contact_behaviour=Some(attr::ContactingBehaviour::Surf), + "Ladder"=>contacting.contact_behaviour=Some(attr::ContactingBehaviour::Ladder(attr::ContactingLadder{sticky:true})), + _=>(), + } + attr::CollisionAttributes::Contact(attr::ContactAttributes{contacting,general}) + }, + false=>if force_intersecting + ||general.any() + ||intersecting.any() + { + attr::CollisionAttributes::Intersect(attr::IntersectAttributes{intersecting,general}) + }else{ + attr::CollisionAttributes::Decoration + }, + } +} + +#[derive(Clone,Copy,PartialEq)] +struct RobloxTextureTransform{ + offset_u:f32, + offset_v:f32, + scale_u:f32, + scale_v:f32, +} +impl std::cmp::Eq for RobloxTextureTransform{}//???? +impl std::default::Default for RobloxTextureTransform{ + fn default()->Self{ + Self{offset_u:0.0,offset_v:0.0,scale_u:1.0,scale_v:1.0} + } +} +impl std::hash::Hash for RobloxTextureTransform{ + fn hash(&self,state:&mut H) { + self.offset_u.to_ne_bytes().hash(state); + self.offset_v.to_ne_bytes().hash(state); + self.scale_u.to_ne_bytes().hash(state); + self.scale_v.to_ne_bytes().hash(state); + } +} +#[derive(Clone,PartialEq)] +struct RobloxFaceTextureDescription{ + render:RenderConfigId, + color:glam::Vec4, + transform:RobloxTextureTransform, +} +impl std::cmp::Eq for RobloxFaceTextureDescription{}//???? +impl std::hash::Hash for RobloxFaceTextureDescription{ + fn hash(&self,state:&mut H){ + self.render.hash(state); + self.transform.hash(state); + for &el in self.color.as_ref().iter(){ + el.to_ne_bytes().hash(state); + } + } +} +impl RobloxFaceTextureDescription{ + fn to_face_description(&self)->primitives::FaceDescription{ + primitives::FaceDescription{ + render:self.render, + transform:glam::Affine2::from_translation( + glam::vec2(self.transform.offset_u,self.transform.offset_v) + ) + *glam::Affine2::from_scale( + glam::vec2(self.transform.scale_u,self.transform.scale_v) + ), + color:self.color, + } + } +} +type RobloxPartDescription=[Option;6]; +type RobloxWedgeDescription=[Option;5]; +type RobloxCornerWedgeDescription=[Option;5]; +#[derive(Clone,Eq,Hash,PartialEq)] +enum RobloxBasePartDescription{ + Sphere(RobloxPartDescription), + Part(RobloxPartDescription), + Cylinder(RobloxPartDescription), + Wedge(RobloxWedgeDescription), + CornerWedge(RobloxCornerWedgeDescription), +} +enum Shape{ + Primitive(primitives::Primitives), + MeshPart, +} +enum MeshAvailability{ + Immediate, + Deferred(RenderConfigId), +} +struct DeferredModelDeferredAttributes{ + render:RenderConfigId, + model:ModelDeferredAttributes, +} +struct ModelDeferredAttributes{ + mesh:model::MeshId, + deferred_attributes:GetAttributesArgs, + color:model::Color4,//transparency is in here + transform:Planar64Affine3, +} +struct ModelOwnedAttributes{ + mesh:model::MeshId, + attributes:attr::CollisionAttributes, + color:model::Color4,//transparency is in here + transform:Planar64Affine3, +} +struct GetAttributesArgs{ + name:Box, + can_collide:bool, + velocity:Planar64Vec3, +} +pub fn convert( + dom:&rbx_dom_weak::WeakDom, + mut acquire_render_config_id:AcquireRenderConfigId, + mut acquire_mesh_id:AcquireMeshId, +)->PartialMap1 +where + AcquireRenderConfigId:FnMut(Option<&str>)->model::RenderConfigId, + AcquireMeshId:FnMut(&str)->model::MeshId, +{ + + let mut deferred_models_deferred_attributes=Vec::new(); + let mut primitive_models_deferred_attributes=Vec::new(); + let mut primitive_meshes=Vec::new(); + let mut mesh_id_from_description=HashMap::new(); + + //just going to leave it like this for now instead of reworking the data structures for this whole thing + let textureless_render_group=acquire_render_config_id(None); + + let mut object_refs=Vec::new(); + let mut temp_objects=Vec::new(); + recursive_collect_superclass(&mut object_refs, &dom, dom.root(),"BasePart"); + for object_ref in object_refs { + if let Some(object)=dom.get_by_ref(object_ref){ + if let ( + Some(rbx_dom_weak::types::Variant::CFrame(cf)), + Some(rbx_dom_weak::types::Variant::Vector3(size)), + Some(rbx_dom_weak::types::Variant::Vector3(velocity)), + Some(rbx_dom_weak::types::Variant::Float32(transparency)), + Some(rbx_dom_weak::types::Variant::Color3uint8(color3)), + Some(rbx_dom_weak::types::Variant::Bool(can_collide)), + ) = ( + object.properties.get("CFrame"), + object.properties.get("Size"), + object.properties.get("Velocity"), + object.properties.get("Transparency"), + object.properties.get("Color"), + object.properties.get("CanCollide"), + ) + { + let model_transform=planar64_affine3_from_roblox(cf,size); + + if model_transform.matrix3.det().is_zero(){ + let mut parent_ref=object.parent(); + let mut full_path=object.name.clone(); + while let Some(parent)=dom.get_by_ref(parent_ref){ + full_path=format!("{}.{}",parent.name,full_path); + parent_ref=parent.parent(); + } + println!("Zero determinant CFrame at location {}",full_path); + println!("matrix3:{}",model_transform.matrix3); + continue; + } + + //at this point a new model is going to be generated for sure. + let model_id=model::ModelId::new(primitive_models_deferred_attributes.len() as u32); + + //TODO: also detect "CylinderMesh" etc here + let shape=match object.class.as_str(){ + "Part"=>if let Some(rbx_dom_weak::types::Variant::Enum(shape))=object.properties.get("Shape"){ + Shape::Primitive(match shape.to_u32(){ + 0=>primitives::Primitives::Sphere, + 1=>primitives::Primitives::Cube, + 2=>primitives::Primitives::Cylinder, + 3=>primitives::Primitives::Wedge, + 4=>primitives::Primitives::CornerWedge, + other=>panic!("Funky roblox PartType={};",other), + }) + }else{ + panic!("Part has no Shape!"); + }, + "TrussPart"=>Shape::Primitive(primitives::Primitives::Cube), + "WedgePart"=>Shape::Primitive(primitives::Primitives::Wedge), + "CornerWedgePart"=>Shape::Primitive(primitives::Primitives::CornerWedge), + "MeshPart"=>Shape::MeshPart, + _=>{ + println!("Unsupported BasePart ClassName={}; defaulting to cube",object.class); + Shape::Primitive(primitives::Primitives::Cube) + } + }; + + let (availability,mesh_id)=match shape{ + Shape::Primitive(primitive_shape)=>{ + //TODO: TAB TAB + //use the biggest one and cut it down later... + let mut part_texture_description:RobloxPartDescription=[None,None,None,None,None,None]; + temp_objects.clear(); + recursive_collect_superclass(&mut temp_objects, &dom, object,"Decal"); + for &decal_ref in &temp_objects{ + if let Some(decal)=dom.get_by_ref(decal_ref){ + if let ( + Some(rbx_dom_weak::types::Variant::Content(content)), + Some(rbx_dom_weak::types::Variant::Enum(normalid)), + Some(rbx_dom_weak::types::Variant::Color3(decal_color3)), + Some(rbx_dom_weak::types::Variant::Float32(decal_transparency)), + ) = ( + decal.properties.get("Texture"), + decal.properties.get("Face"), + decal.properties.get("Color3"), + decal.properties.get("Transparency"), + ) { + let render_id=acquire_render_config_id(Some(content.as_ref())); + let normal_id=normalid.to_u32(); + if normal_id<6{ + let (roblox_texture_color,roblox_texture_transform)=if decal.class=="Texture"{ + //generate tranform + if let ( + Some(rbx_dom_weak::types::Variant::Float32(ox)), + Some(rbx_dom_weak::types::Variant::Float32(oy)), + Some(rbx_dom_weak::types::Variant::Float32(sx)), + Some(rbx_dom_weak::types::Variant::Float32(sy)), + ) = ( + decal.properties.get("OffsetStudsU"), + decal.properties.get("OffsetStudsV"), + decal.properties.get("StudsPerTileU"), + decal.properties.get("StudsPerTileV"), + ) + { + let (size_u,size_v)=match normal_id{ + 0=>(size.z,size.y),//right + 1=>(size.x,size.z),//top + 2=>(size.x,size.y),//back + 3=>(size.z,size.y),//left + 4=>(size.x,size.z),//bottom + 5=>(size.x,size.y),//front + _=>unreachable!(), + }; + ( + glam::vec4(decal_color3.r,decal_color3.g,decal_color3.b,1.0-*decal_transparency), + RobloxTextureTransform{ + offset_u:*ox/(*sx),offset_v:*oy/(*sy), + scale_u:size_u/(*sx),scale_v:size_v/(*sy), + } + ) + }else{ + (glam::Vec4::ONE,RobloxTextureTransform::default()) + } + }else{ + (glam::Vec4::ONE,RobloxTextureTransform::default()) + }; + part_texture_description[normal_id as usize]=Some(RobloxFaceTextureDescription{ + render:render_id, + color:roblox_texture_color, + transform:roblox_texture_transform, + }); + }else{ + println!("NormalId={} unsupported for shape={:?}",normal_id,primitive_shape); + } + } + } + } + //obscure rust syntax "slice pattern" + let [ + f0,//Cube::Right + f1,//Cube::Top + f2,//Cube::Back + f3,//Cube::Left + f4,//Cube::Bottom + f5,//Cube::Front + ]=part_texture_description; + let basepart_description=match primitive_shape{ + primitives::Primitives::Sphere=>RobloxBasePartDescription::Sphere([f0,f1,f2,f3,f4,f5]), + primitives::Primitives::Cube=>RobloxBasePartDescription::Part([f0,f1,f2,f3,f4,f5]), + primitives::Primitives::Cylinder=>RobloxBasePartDescription::Cylinder([f0,f1,f2,f3,f4,f5]), + //use front face texture first and use top face texture as a fallback + primitives::Primitives::Wedge=>RobloxBasePartDescription::Wedge([ + f0,//Cube::Right->Wedge::Right + if f5.is_some(){f5}else{f1},//Cube::Front|Cube::Top->Wedge::TopFront + f2,//Cube::Back->Wedge::Back + f3,//Cube::Left->Wedge::Left + f4,//Cube::Bottom->Wedge::Bottom + ]), + //TODO: fix Left+Back texture coordinates to match roblox when not overwridden by Top + primitives::Primitives::CornerWedge=>RobloxBasePartDescription::CornerWedge([ + f0,//Cube::Right->CornerWedge::Right + if f2.is_some(){f2}else{f1.clone()},//Cube::Back|Cube::Top->CornerWedge::TopBack + if f3.is_some(){f3}else{f1},//Cube::Left|Cube::Top->CornerWedge::TopLeft + f4,//Cube::Bottom->CornerWedge::Bottom + f5,//Cube::Front->CornerWedge::Front + ]), + }; + //make new model if unit cube has not been created before + let mesh_id=if let Some(&mesh_id)=mesh_id_from_description.get(&basepart_description){ + //push to existing texture model + mesh_id + }else{ + let mesh_id=model::MeshId::new(primitive_meshes.len() as u32); + mesh_id_from_description.insert(basepart_description.clone(),mesh_id);//borrow checker going crazy + let mesh=match basepart_description{ + RobloxBasePartDescription::Sphere(part_texture_description) + |RobloxBasePartDescription::Cylinder(part_texture_description) + |RobloxBasePartDescription::Part(part_texture_description)=>{ + let mut cube_face_description=primitives::CubeFaceDescription::default(); + for (face_id,roblox_face_description) in part_texture_description.iter().enumerate(){ + cube_face_description.insert( + match face_id{ + 0=>primitives::CubeFace::Right, + 1=>primitives::CubeFace::Top, + 2=>primitives::CubeFace::Back, + 3=>primitives::CubeFace::Left, + 4=>primitives::CubeFace::Bottom, + 5=>primitives::CubeFace::Front, + _=>unreachable!(), + }, + match roblox_face_description{ + Some(roblox_texture_transform)=>roblox_texture_transform.to_face_description(), + None=>primitives::FaceDescription::new_with_render_id(textureless_render_group), + }); + } + primitives::generate_partial_unit_cube(cube_face_description) + }, + RobloxBasePartDescription::Wedge(wedge_texture_description)=>{ + let mut wedge_face_description=primitives::WedgeFaceDescription::default(); + for (face_id,roblox_face_description) in wedge_texture_description.iter().enumerate(){ + wedge_face_description.insert( + match face_id{ + 0=>primitives::WedgeFace::Right, + 1=>primitives::WedgeFace::TopFront, + 2=>primitives::WedgeFace::Back, + 3=>primitives::WedgeFace::Left, + 4=>primitives::WedgeFace::Bottom, + _=>unreachable!(), + }, + match roblox_face_description{ + Some(roblox_texture_transform)=>roblox_texture_transform.to_face_description(), + None=>primitives::FaceDescription::new_with_render_id(textureless_render_group), + }); + } + primitives::generate_partial_unit_wedge(wedge_face_description) + }, + RobloxBasePartDescription::CornerWedge(cornerwedge_texture_description)=>{ + let mut cornerwedge_face_description=primitives::CornerWedgeFaceDescription::default(); + for (face_id,roblox_face_description) in cornerwedge_texture_description.iter().enumerate(){ + cornerwedge_face_description.insert( + match face_id{ + 0=>primitives::CornerWedgeFace::Right, + 1=>primitives::CornerWedgeFace::TopBack, + 2=>primitives::CornerWedgeFace::TopLeft, + 3=>primitives::CornerWedgeFace::Bottom, + 4=>primitives::CornerWedgeFace::Front, + _=>unreachable!(), + }, + match roblox_face_description{ + Some(roblox_texture_transform)=>roblox_texture_transform.to_face_description(), + None=>primitives::FaceDescription::new_with_render_id(textureless_render_group), + }); + } + primitives::generate_partial_unit_cornerwedge(cornerwedge_face_description) + }, + }; + primitive_meshes.push(mesh); + mesh_id + }; + (MeshAvailability::Immediate,mesh_id) + }, + Shape::MeshPart=>if let ( + Some(rbx_dom_weak::types::Variant::Content(mesh_asset_id)), + Some(rbx_dom_weak::types::Variant::Content(texture_asset_id)), + )=( + object.properties.get("MeshId"), + object.properties.get("TextureID"), + ){ + ( + MeshAvailability::Deferred(acquire_render_config_id(Some(texture_asset_id.as_ref()))), + acquire_mesh_id(mesh_asset_id.as_ref()), + ) + }else{ + panic!("Mesh has no Mesh or Texture"); + }, + }; + let model_deferred_attributes=ModelDeferredAttributes{ + mesh:mesh_id, + transform:model_transform, + color:glam::vec4(color3.r as f32/255f32, color3.g as f32/255f32, color3.b as f32/255f32, 1.0-*transparency), + deferred_attributes:GetAttributesArgs{ + name:object.name.as_str().into(), + can_collide:*can_collide, + velocity:vec3::try_from_f32_array([velocity.x,velocity.y,velocity.z]).unwrap(), + }, + }; + match availability{ + MeshAvailability::Immediate=>primitive_models_deferred_attributes.push(model_deferred_attributes), + MeshAvailability::Deferred(render)=>deferred_models_deferred_attributes.push(DeferredModelDeferredAttributes{ + render, + model:model_deferred_attributes + }), + } + } + } + } + PartialMap1{ + primitive_meshes, + primitive_models_deferred_attributes, + deferred_models_deferred_attributes, + } +} +struct MeshWithAabb{ + mesh:model::Mesh, + aabb:strafesnet_common::aabb::Aabb, +} +pub struct PartialMap1{ + primitive_meshes:Vec, + primitive_models_deferred_attributes:Vec, + deferred_models_deferred_attributes:Vec, +} +impl PartialMap1{ + pub fn add_meshpart_meshes_and_calculate_attributes( + mut self, + meshpart_meshes:impl IntoIterator, + )->PartialMap2{ + //calculate attributes + let mut modes_builder=ModesBuilder::default(); + let mut unique_attributes=Vec::new(); + let mut attributes_id_from_attributes=HashMap::new(); + + let mut wormhole_in_model_to_id=HashMap::new(); + let mut wormhole_id_to_out_model=HashMap::new(); + + //decode roblox meshes + //generate mesh_id_map based on meshes that failed to load + let loaded_meshes:HashMap= + meshpart_meshes.into_iter().flat_map(|(old_mesh_id,roblox_mesh_bytes)| + match crate::mesh::convert(roblox_mesh_bytes){ + Ok(mesh)=>{ + let mut aabb=strafesnet_common::aabb::Aabb::default(); + for &pos in &mesh.unique_pos{ + aabb.grow(pos); + } + Some((old_mesh_id,MeshWithAabb{ + mesh, + aabb, + })) + }, + Err(e)=>{ + println!("Error converting mesh: {e:?}"); + None + }, + } + ).collect(); + + let mut mesh_id_from_render_config_id=HashMap::new(); + //ignore meshes that fail to load completely for now + let mut acquire_mesh_id_from_render_config_id=|old_mesh_id,render|{ + loaded_meshes.get(&old_mesh_id).map(|mesh_with_aabb|( + *mesh_id_from_render_config_id.entry(old_mesh_id).or_insert_with(||HashMap::new()) + .entry(render).or_insert_with(||{ + let mesh_id=model::MeshId::new(self.primitive_meshes.len() as u32); + let mut mesh_clone=mesh_with_aabb.mesh.clone(); + //add a render group lool + mesh_clone.graphics_groups.push(model::IndexedGraphicsGroup{ + render, + //the lowest lod is highest quality + groups:vec![model::PolygonGroupId::new(0)] + }); + self.primitive_meshes.push(mesh_clone); + mesh_id + }), + &mesh_with_aabb.aabb, + )) + }; + //now that the meshes are loaded, these models can be generated + let models_owned_attributes:Vec= + self.deferred_models_deferred_attributes.into_iter().flat_map(|deferred_model_deferred_attributes|{ + //meshes need to be cloned from loaded_meshes with a new id when they are used with a new render_id + //insert into primitive_meshes + let (mesh,aabb)=acquire_mesh_id_from_render_config_id( + deferred_model_deferred_attributes.model.mesh, + deferred_model_deferred_attributes.render + )?; + let size=aabb.size(); + Some(ModelDeferredAttributes{ + mesh, + deferred_attributes:deferred_model_deferred_attributes.model.deferred_attributes, + color:deferred_model_deferred_attributes.model.color, + transform:Planar64Affine3::new( + Planar64Mat3::from_cols([ + (deferred_model_deferred_attributes.model.transform.matrix3.x_axis*2/size.x).divide().fix_1(), + (deferred_model_deferred_attributes.model.transform.matrix3.y_axis*2/size.y).divide().fix_1(), + (deferred_model_deferred_attributes.model.transform.matrix3.z_axis*2/size.z).divide().fix_1() + ]), + deferred_model_deferred_attributes.model.transform.translation + ), + }) + }).chain(self.primitive_models_deferred_attributes.into_iter()) + .enumerate().map(|(model_id,model_deferred_attributes)|{ + let model_id=model::ModelId::new(model_id as u32); + ModelOwnedAttributes{ + mesh:model_deferred_attributes.mesh, + attributes:get_attributes( + &model_deferred_attributes.deferred_attributes.name, + model_deferred_attributes.deferred_attributes.can_collide, + model_deferred_attributes.deferred_attributes.velocity, + model_id, + &mut modes_builder, + &mut wormhole_in_model_to_id, + &mut wormhole_id_to_out_model, + ), + color:model_deferred_attributes.color, + transform:model_deferred_attributes.transform, + } + }).collect(); + let models=models_owned_attributes.into_iter().enumerate().map(|(model_id,mut model_owned_attributes)|{ + //TODO: TAB + let model_id=model::ModelId::new(model_id as u32); + //update attributes with wormhole id + //TODO: errors/prints + if let Some(wormhole_id)=wormhole_in_model_to_id.get(&model_id){ + if let Some(&wormhole_out_model_id)=wormhole_id_to_out_model.get(wormhole_id){ + match &mut model_owned_attributes.attributes{ + attr::CollisionAttributes::Contact(attr::ContactAttributes{contacting:_,general}) + |attr::CollisionAttributes::Intersect(attr::IntersectAttributes{intersecting:_,general}) + =>general.wormhole=Some(attr::Wormhole{destination_model:wormhole_out_model_id}), + attr::CollisionAttributes::Decoration=>println!("Not a wormhole"), + } + } + } + + //index the attributes + let attributes_id=if let Some(&attributes_id)=attributes_id_from_attributes.get(&model_owned_attributes.attributes){ + attributes_id + }else{ + let attributes_id=attr::CollisionAttributesId::new(unique_attributes.len() as u32); + attributes_id_from_attributes.insert(model_owned_attributes.attributes.clone(),attributes_id); + unique_attributes.push(model_owned_attributes.attributes); + attributes_id + }; + model::Model{ + mesh:model_owned_attributes.mesh, + transform:model_owned_attributes.transform, + color:model_owned_attributes.color, + attributes:attributes_id, + } + }).collect(); + PartialMap2{ + meshes:self.primitive_meshes, + models, + modes:modes_builder.build(), + attributes:unique_attributes, + } + } +} + +pub struct PartialMap2{ + meshes:Vec, + models:Vec, + modes:gameplay_modes::Modes, + attributes:Vec, +} +impl PartialMap2{ + pub fn add_render_configs_and_textures( + self, + render_configs:impl IntoIterator, + textures:impl IntoIterator)>, + )->map::CompleteMap{ + let (textures,texture_id_map):(Vec>,HashMap) + =textures.into_iter().enumerate().map(|(new_texture_id,(old_texture_id,texture))|{ + (texture,(old_texture_id,model::TextureId::new(new_texture_id as u32))) + }).unzip(); + let render_configs=render_configs.into_iter().map(|(render_config_id,mut render_config)|{ + //this may generate duplicate no-texture render configs but idc + render_config.texture=render_config.texture.and_then(|texture_id| + texture_id_map.get(&texture_id).copied() + ); + render_config + }).collect(); + map::CompleteMap{ + modes:self.modes, + attributes:self.attributes, + meshes:self.meshes, + models:self.models, + //the roblox legacy texture thing always works + textures, + render_configs, + } + } +}