diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..8e50cde --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,2 @@ +[build] +target = "armv7-linux-androideabi" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2f7896d --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +target/ diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..2cb0f8d --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,639 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "android_log-sys" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85965b6739a430150bdd138e2374a98af0c3ee0d030b3bb7fc3bddff58d0102e" + +[[package]] +name = "bitflags" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" + +[[package]] +name = "bytes" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "getrandom" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasi", +] + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys", + "log", + "thiserror", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + +[[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.171" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" + +[[package]] +name = "libserver" +version = "0.0.1" +dependencies = [ + "jni", + "paste", + "rand", + "tracing", + "tracing-android", + "tracing-subscriber", +] + +[[package]] +name = "log" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" + +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata 0.1.10", +] + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.94" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" + +[[package]] +name = "rand" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94" +dependencies = [ + "rand_chacha", + "rand_core", + "zerocopy", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom", +] + +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata 0.4.9", + "regex-syntax 0.8.5", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax 0.8.5", +] + +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "smallvec" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd" + +[[package]] +name = "syn" +version = "2.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +dependencies = [ + "cfg-if", + "once_cell", +] + +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-android" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12612be8f868a09c0ceae7113ff26afe79d81a24473a393cb9120ece162e86c0" +dependencies = [ + "android_log-sys", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.14.2+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +dependencies = [ + "wit-bindgen-rt", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[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.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[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.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[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.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[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.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[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.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[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.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "wit-bindgen-rt" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags", +] + +[[package]] +name = "zerocopy" +version = "0.8.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2586fea28e186957ef732a5f8b3be2da217d65c5969d4b1e17f973ebbe876879" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a996a8f63c5c4448cd959ac1bab0aaa3306ccfd060472f85943ee0750f0169be" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..5379213 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,16 @@ +[workspace] +members = ["libserver"] +resolver = "2" + +[workspace.package] +version = "0.0.1" +edition = "2024" + +[workspace.dependencies] +jni = "0.21.1" +tracing = "0.1.41" +tracing-android = "0.2.0" +tracing-subscriber = { version = "0.3.19", features = ["env-filter"] } + +rand = "0.9" +paste = "1.0.15" diff --git a/README.md b/README.md index 8446b09..2930478 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,49 @@ -# Scroll +# scroll-rs +Experimental Clash Royale server emulator on top of libg.so (v1.3.2) +# ![Screenshot](screenshot.png) -Experimental Clash Royale server on top of libg.so \ No newline at end of file +# Why? +Supercell's games have their game logic implementation included in both client and server, for independent execution (the `Logic*` family of classes is shared). However, to implement a feature-complete server emulator, you should rewrite the logic entirely. This experimental way is to use logic that is retained in the game, basically turning client into a server. + +# Current features +- All cards are unlocked +- Training battles +- More features are planned to be implemented (including PvP battles), stay tuned + +# Implementation +Server side code is written in Rust. We provide idiomatic bindings for the structures/functions from `libg.so`. + +# Getting started +#### NOTE: you have to use a device with support of armeabi-v7a binaries +### a) Using pre-built apk files +Navigate to the [Releases](https://git.xeondev.com/Supercell/Scroll/releases) page and download both Server and Client APK files (by default, they're targeted to the `127.0.0.1:9339` endpoint). Next, install both of them. Open the server application first (it should stay with black screen), then leave it running in the background. Open the client and play! +### b) Building from sources +#### Requirements: +- [Rust 1.85+](https://www.rust-lang.org/tools/install) +- [Android NDK](https://developer.android.com/ndk/downloads) +- [cargo-ndk](https://docs.rs/crate/cargo-ndk/3.5.4) + +#### Preparing the toolchain +- Install android armv7 target via rustup: +```sh +rustup target add armv7-linux-androideabi +``` +- Install cargo-ndk extension: +```sh +cargo install cargo-ndk +``` + +##### NOTE: make sure you have configured the `ANDROID_NDK_HOME` environment variable before invoking build command. + +#### Compiling server library +```sh +git clone https://git.xeondev.com/Supercell/Scroll +cd Scroll +cargo ndk -t armeabi-v7a build --release +``` + +### Community +[Our Discord Server](https://discord.gg/reversedrooms) is open for everyone who's interested in our projects! + +### Support +Your support for this project is greatly appreciated! If you'd like to contribute, feel free to send a tip [via Boosty](https://boosty.to/xeondev/donate)! diff --git a/libserver/Cargo.toml b/libserver/Cargo.toml new file mode 100644 index 0000000..b02df07 --- /dev/null +++ b/libserver/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "libserver" +version.workspace = true +edition.workspace = true + +[lib] +name = "server" +crate-type = ["cdylib"] + +[dependencies] +jni.workspace = true + +tracing.workspace = true +tracing-android.workspace = true +tracing-subscriber.workspace = true + +rand.workspace = true +paste.workspace = true diff --git a/libserver/src/allocator.rs b/libserver/src/allocator.rs new file mode 100644 index 0000000..d83fb10 --- /dev/null +++ b/libserver/src/allocator.rs @@ -0,0 +1,18 @@ +mod libc_extern { + unsafe extern "C" { + pub fn malloc(amount: usize) -> *const u8; + pub fn free(ptr: *const u8); + } +} + +pub fn malloc(amount: usize) -> *const u8 { + unsafe { libc_extern::malloc(amount) } +} + +pub fn free(ptr: *const T) { + unsafe { libc_extern::free(ptr as *const u8) } +} + +pub fn new() -> *mut T { + malloc(std::mem::size_of::()).cast::().cast_mut() +} diff --git a/libserver/src/array_list.rs b/libserver/src/array_list.rs new file mode 100644 index 0000000..3c5c002 --- /dev/null +++ b/libserver/src/array_list.rs @@ -0,0 +1,15 @@ +#[repr(C)] +pub struct LogicArrayList { + pub data: *const T, + pub _capacity: usize, + pub count: usize, +} + +impl LogicArrayList +where + T: Sized, +{ + pub fn as_slice(&self) -> &[T] { + unsafe { std::slice::from_raw_parts(self.data, self.count) } + } +} diff --git a/libserver/src/ffi_util.rs b/libserver/src/ffi_util.rs new file mode 100644 index 0000000..6f78486 --- /dev/null +++ b/libserver/src/ffi_util.rs @@ -0,0 +1,57 @@ +use std::sync::LazyLock; + +pub static LIBG_BASE: LazyLock = + LazyLock::new(|| get_module_base("libg.so").expect("failed to get libg.so base address")); + +#[repr(transparent)] +pub struct Nullable(*const T); + +impl Nullable { + pub fn get(&self) -> Option<&T> { + (!self.0.is_null()).then(|| unsafe { &*self.0 }) + } + + pub fn get_mut(&mut self) -> Option<&mut T> { + (!self.0.is_null()).then(|| unsafe { &mut *self.0.cast_mut() }) + } + + pub fn set(&mut self, value: *const T) { + self.0 = value; + } +} + +macro_rules! import { + ($name:ident($($arg_name:ident: $arg_type:ty),*) -> $ret_type:ty = $rva:expr) => { + pub fn $name($($arg_name: $arg_type,)*) -> $ret_type { + unsafe { + type FuncType = unsafe extern "cdecl" fn($($arg_type,)*) -> $ret_type; + ::std::mem::transmute::(*crate::ffi_util::LIBG_BASE + $rva)($($arg_name,)*) + } + } + }; +} + +pub(crate) use import; + +pub fn get_module_base(shared_object_name: &str) -> Option { + use std::{ + fs::File, + io::{self, BufRead}, + }; + + let file = File::open("/proc/self/maps").expect("failed to open /proc/self/maps"); + let reader = io::BufReader::new(file); + + for line in reader.lines() { + let line = line.ok()?; + if line.contains(shared_object_name) { + let address_str = line.split_whitespace().next().unwrap_or(""); + let address = + usize::from_str_radix(&address_str.split('-').next().unwrap_or(""), 16).ok()?; + + return Some(address); + } + } + + None +} diff --git a/libserver/src/lib.rs b/libserver/src/lib.rs new file mode 100644 index 0000000..639ac0f --- /dev/null +++ b/libserver/src/lib.rs @@ -0,0 +1,82 @@ +use std::{ + ffi::c_void, + net::{SocketAddr, TcpListener}, + os::fd::IntoRawFd, + sync::LazyLock, + thread, +}; + +use network::{LogicScrollMessageFactory, Messaging, RC4Encrypter}; +use session::PlayerSession; +use tracing::info; + +mod allocator; +mod array_list; +mod ffi_util; +mod logic; +mod network; +mod resources; +mod sc_string; +mod session; +mod util; + +#[allow(non_snake_case)] +#[unsafe(no_mangle)] +pub unsafe extern "system" fn JNI_OnLoad(_vm: jni::JavaVM, _: *mut c_void) -> jni::sys::jint { + init_tracing(); + + thread::spawn(|| server_main()); + + jni::sys::JNI_VERSION_1_6 +} + +fn server_main() { + const TCP_ADDR: &str = "127.0.0.1:9339"; + + info!("starting server..."); + resources::init(); + info!("resources initialized"); + + let listener = TcpListener::bind(TCP_ADDR).unwrap(); + while let Ok((stream, addr)) = listener.accept() { + info!("new connection from {addr}"); + let fd = stream.into_raw_fd(); + + thread::spawn(move || receive_loop(fd, addr)); + } +} + +fn receive_loop(fd: i32, addr: SocketAddr) { + static MESSAGE_FACTORY: LazyLock = + LazyLock::new(|| LogicScrollMessageFactory::new(false)); + + let mut messaging = Messaging::new(80); + + let encrypter = RC4Encrypter::new(LogicScrollMessageFactory::RC4_KEY, "nonce"); + let decrypter = RC4Encrypter::new(LogicScrollMessageFactory::RC4_KEY, "nonce"); + + messaging.get_connection_mut().fd = fd; + messaging.get_connection_mut().is_connected = true; + messaging.set_encrypters(encrypter, decrypter); + messaging.set_message_factory(&MESSAGE_FACTORY); + + let mut session = PlayerSession::new(messaging); + session.work_until_disconnect(); + + info!("client from {addr} disconnected"); +} + +fn init_tracing() { + use tracing::level_filters::LevelFilter; + use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; + + tracing_subscriber::registry() + .with(tracing_subscriber::fmt::layer()) + .with(tracing_android::layer("SCROLL-SERVER").unwrap()) + .with( + tracing_subscriber::EnvFilter::builder() + .with_default_directive(LevelFilter::INFO.into()) + .from_env_lossy(), + ) + .init(); +} diff --git a/libserver/src/logic/avatar.rs b/libserver/src/logic/avatar.rs new file mode 100644 index 0000000..40d6543 --- /dev/null +++ b/libserver/src/logic/avatar.rs @@ -0,0 +1,63 @@ +use super::{data::LogicData, math::LogicLong}; +use crate::{ + allocator, + ffi_util::import, + sc_string::ScString, + util::data_stream::{ByteStream, ChecksumEncoder}, +}; + +#[repr(C)] +pub struct LogicAvatarVTable { + finalizer_1: extern "C" fn(*mut LogicClientAvatar), + finalizer_2: extern "C" fn(*mut LogicClientAvatar), + pub destruct: extern "C" fn(*mut LogicClientAvatar), + pub encode: extern "C" fn(*const LogicClientAvatar, *mut ChecksumEncoder), + pub decode: extern "C" fn(*mut LogicClientAvatar, *mut ByteStream), + set_logic_game_mode: extern "C" fn(*mut LogicClientAvatar, usize), + is_npc_avatar: extern "C" fn(*const LogicClientAvatar) -> bool, + get_checksum: extern "C" fn(*const LogicClientAvatar) -> i32, +} + +#[repr(C)] +pub struct LogicClientAvatar { + pub vtable: &'static LogicAvatarVTable, + pub logic_game_mode: usize, + pub id: LogicLong, // 8 + pub account_id: LogicLong, // 16 + pub home_id: LogicLong, // 24 + unk_gap_1: [u8; 28], // 32 - 60 + pub name: ScString, // 60 + pub score: i32, // 80 + unk_gap_2: [u8; 28], // 84 - 112 + pub level: i32, // 112 + unk_gap_3: [u8; 68], // 116 - 184 + pub npc_win_count: i32, // 184 + pub npc_lose_count: i32, // 188, + unk_192: bool, + pub name_set_by_user: bool, + unk_194: bool, + pub name_change_state: i32, // 196 + pub arena: &'static LogicData, // 200 +} + +impl LogicClientAvatar { + pub fn new() -> &'static mut LogicClientAvatar { + import!(logic_client_avatar_ctor(ptr: *mut LogicClientAvatar) -> () = 0xEAE00 + 1); + let instance = allocator::new::(); + logic_client_avatar_ctor(instance); + unsafe { &mut *instance } + } + + pub fn clone(&self) -> &'static mut LogicClientAvatar { + let mut stream = ByteStream::new(128); + + (self.vtable.encode)(self, &mut stream.parent); + stream.reset_offset(); + + let cloned_avatar = LogicClientAvatar::new(); + (cloned_avatar.vtable.decode)(cloned_avatar, &mut stream); + stream.destruct(); + + cloned_avatar + } +} diff --git a/libserver/src/logic/battle.rs b/libserver/src/logic/battle.rs new file mode 100644 index 0000000..11374f1 --- /dev/null +++ b/libserver/src/logic/battle.rs @@ -0,0 +1,28 @@ +use crate::ffi_util::import; + +use super::{ + data::{LogicData, LogicLocationData, LogicNpcData}, + spell::LogicSpellDeck, +}; + +#[repr(C)] +pub struct LogicBattle { + unk_gap_1: [u8; 8], // 0 - 8, + pub battle_type: i32, // 8 + unk_gap_2: [u8; 32], // 12 - 44 + pub npc_data: &'static LogicNpcData, + pub arena_data: &'static LogicData, +} + +impl LogicBattle { + pub fn set_location(&mut self, location: &LogicLocationData, skip_init: bool, listener: usize) { + import!(logic_battle_set_location(battle: *mut LogicBattle, location: *const LogicLocationData, skip_init: bool, listener: usize) -> () = 0xEE05C + 1); + + logic_battle_set_location(self, location, skip_init, listener); + } + + pub fn set_spell_decks(&mut self, deck_1: &LogicSpellDeck, deck_2: &LogicSpellDeck) { + import!(logic_battle_set_spell_decks(battle: *mut LogicBattle, deck_1: *const LogicSpellDeck, deck_2: *const LogicSpellDeck) -> () = 0xEE17C + 1); + logic_battle_set_spell_decks(self, deck_1, deck_2); + } +} diff --git a/libserver/src/logic/command/manager.rs b/libserver/src/logic/command/manager.rs new file mode 100644 index 0000000..f53d83f --- /dev/null +++ b/libserver/src/logic/command/manager.rs @@ -0,0 +1,15 @@ +use crate::ffi_util::import; + +use super::LogicCommand; + +#[repr(C)] +pub struct LogicCommandManager { + unk_gap_1: [u8; 0x14], +} + +impl LogicCommandManager { + pub fn add_command(&mut self, command: &LogicCommand) { + import!(logic_command_manager_add_command(ptr: *mut LogicCommandManager, command: *const LogicCommand) -> () = 0xF0914 + 1); + logic_command_manager_add_command(self, command); + } +} diff --git a/libserver/src/logic/command/mod.rs b/libserver/src/logic/command/mod.rs new file mode 100644 index 0000000..acc22e7 --- /dev/null +++ b/libserver/src/logic/command/mod.rs @@ -0,0 +1,33 @@ +use crate::util::data_stream::{ByteStream, ChecksumEncoder}; + +use super::{json::LogicJSONNode, math::LogicLong, mode::LogicGameMode}; + +mod manager; +pub use manager::LogicCommandManager; + +#[repr(C)] +pub struct LogicCommandVTable { + pub destruct: extern "C" fn(ptr: *mut LogicCommand), + finalizer_1: extern "C" fn(ptr: *mut LogicCommand), + finalizer_2: extern "C" fn(ptr: *mut LogicCommand), + pub execute: extern "C" fn(ptr: *const LogicCommand, logic_game_mode: *mut LogicGameMode), + pub get_command_type: extern "C" fn() -> i16, + pub encode: extern "C" fn(ptr: *const LogicCommand, encoder: *mut ChecksumEncoder), + pub decode: extern "C" fn(ptr: *mut LogicCommand, stream: *mut ByteStream), + pub get_command_gold_cost: + extern "C" fn(ptr: *const LogicCommand, logic_game_mode: *const LogicGameMode) -> i32, + pub get_command_diamond_cost: + extern "C" fn(ptr: *const LogicCommand, logic_game_mode: *const LogicGameMode) -> i32, + pub get_json_for_replay: extern "C" fn(ptr: *const LogicCommand) -> *const LogicJSONNode, + pub load_from_json: extern "C" fn(ptr: *mut LogicCommand, json: *const LogicJSONNode), + pub is_saved_for_replay: extern "C" fn(ptr: *const LogicCommand) -> bool, +} + +#[repr(C)] +pub struct LogicCommand { + pub vtable: &'static LogicCommandVTable, + unk: bool, + pub execute_tick: i32, + pub tick_when_given: i32, + pub executor_account_id: LogicLong, +} diff --git a/libserver/src/logic/data.rs b/libserver/src/logic/data.rs new file mode 100644 index 0000000..fe315d7 --- /dev/null +++ b/libserver/src/logic/data.rs @@ -0,0 +1,64 @@ +use crate::{ffi_util::import, resources::CSVNode, sc_string::ScString}; + +pub struct LogicDataTables; + +#[repr(C)] +pub struct LogicData { + unk_gap_1: [u8; 16], + pub global_id: i32, // 16 +} + +#[repr(C)] +pub struct LogicNpcData { + pub parent: LogicData, + unk_gap_1: [u8; 12], // 20 - 32 + pub location: &'static LogicLocationData, +} + +#[repr(C)] +pub struct LogicLocationData { + pub parent: LogicData, +} + +#[repr(C)] +pub struct LogicSpellData { + pub parent: LogicData, +} + +impl LogicLocationData { + pub fn get_tilemap_file_name(&self) -> String { + import!(logic_location_data_get_tilemap_file_name(data: *const LogicLocationData) -> *const ScString = 0x103EC4 + 1); + let file_name = logic_location_data_get_tilemap_file_name(self); + unsafe { (&*file_name).to_string() } + } +} + +impl LogicSpellData { + pub fn get_max_level_index(&self) -> i32 { + import!(logic_spell_data_get_max_level_index(data: *const LogicSpellData) -> i32 = 0x10A2F4 + 1); + logic_spell_data_get_max_level_index(self) + } +} + +impl LogicDataTables { + pub fn get_data_by_id(global_id: i32) -> Option<&'static LogicData> { + import!(logic_data_tables_get_data_by_id(global_id: i32) -> *const LogicData = 0xFCB88 + 1); + let data = logic_data_tables_get_data_by_id(global_id); + (!data.is_null()).then(|| unsafe { &*data }) + } + + pub fn get_spell_count() -> usize { + import!(logic_data_tables_get_spell_count() -> usize = 0xFE974 + 1); + logic_data_tables_get_spell_count() + } + + pub fn get_spell_at(index: usize) -> &'static LogicSpellData { + import!(logic_data_tables_get_spell_at(index: usize) -> *const LogicSpellData = 0xFE98C + 1); + unsafe { &*logic_data_tables_get_spell_at(index) } + } + + pub fn init_tilemap(csv: CSVNode, id: i32) { + import!(logic_data_tables_init_tilemap(csv: *const u8, id: i32) -> () = 0xFE49C + 1); + logic_data_tables_init_tilemap(csv.0, id); + } +} diff --git a/libserver/src/logic/home.rs b/libserver/src/logic/home.rs new file mode 100644 index 0000000..878ebce --- /dev/null +++ b/libserver/src/logic/home.rs @@ -0,0 +1,68 @@ +use crate::{ + allocator, + ffi_util::{import, Nullable}, + util::data_stream::{ByteStream, ChecksumEncoder}, +}; + +use super::{ + data::LogicSpellData, + math::LogicLong, + spell::{LogicSpellCollection, LogicSpellDeck}, +}; + +#[repr(C)] +pub struct LogicClientHome { + pub change_listener: &'static LogicHomeChangeListener, + pub logic_game_mode: usize, + pub id: Nullable, // 8 + pub spell_deck: LogicSpellDeck, // 12 + pub spell_collection: LogicSpellCollection, // 44 + unk_gap_1: [u8; 332], // 60 - 392 +} + +#[repr(C)] +pub struct LogicHomeChangeListener { + pub _vtable: usize, +} + +impl LogicClientHome { + pub fn new() -> &'static mut Self { + import!(logic_client_home_ctor(ptr: *mut LogicClientHome) -> () = 0x114D40 + 1); + let instance = allocator::new::(); + logic_client_home_ctor(instance); + unsafe { &mut *instance } + } + + pub fn has_spell(&self, spell_data: &LogicSpellData) -> bool { + import!(logic_client_home_has_spell(ptr: *const LogicClientHome, spell_data: *const LogicSpellData) -> bool = 0x116EDC + 1); + logic_client_home_has_spell(self, spell_data) + } + + pub fn encode(&self, encoder: &mut ChecksumEncoder) { + import!(logic_client_home_encode(ptr: *const LogicClientHome, encoder: *mut ChecksumEncoder) -> () = 0x115CD4 + 1); + logic_client_home_encode(self, encoder); + } + + pub fn decode(&mut self, stream: &mut ByteStream) { + import!(logic_client_home_decode(ptr: *mut LogicClientHome, stream: *mut ByteStream) -> () = 0x115FD0 + 1); + logic_client_home_decode(self, stream); + } + + pub fn destruct(&self) { + import!(logic_client_home_destruct(ptr: *const LogicClientHome) -> () = 0x115058 + 1); + logic_client_home_destruct(self); + } + + pub fn clone(&self) -> &'static mut LogicClientHome { + let mut stream = ByteStream::new(128); + + self.encode(&mut stream.parent); + stream.reset_offset(); + + let cloned_home = LogicClientHome::new(); + cloned_home.decode(&mut stream); + stream.destruct(); + + cloned_home + } +} diff --git a/libserver/src/logic/json.rs b/libserver/src/logic/json.rs new file mode 100644 index 0000000..0a32ef8 --- /dev/null +++ b/libserver/src/logic/json.rs @@ -0,0 +1,16 @@ +use crate::{ffi_util::import, sc_string::ScString}; + +pub struct LogicJSONParser; + +#[repr(C)] +pub struct LogicJSONNode { + vtable: usize, +} + +impl LogicJSONParser { + pub fn parse(data: &str) -> Option<&'static LogicJSONNode> { + import!(logic_json_parser_parse(data: *const ScString) -> *const LogicJSONNode = 0x17D2E4 + 1); + let result = logic_json_parser_parse(ScString::new(data)); + (!result.is_null()).then(|| unsafe { &*result }) + } +} diff --git a/libserver/src/logic/math.rs b/libserver/src/logic/math.rs new file mode 100644 index 0000000..25f4024 --- /dev/null +++ b/libserver/src/logic/math.rs @@ -0,0 +1,31 @@ +use std::fmt; + +use crate::allocator; + +#[repr(C)] +pub struct LogicLong { + pub higher_int: i32, + pub lower_int: i32, +} + +impl LogicLong { + pub fn new(higher_int: i32, lower_int: i32) -> &'static Self { + unsafe { + let instance = &mut *allocator::new::(); + instance.higher_int = higher_int; + instance.lower_int = lower_int; + instance + } + } + + pub fn set(&mut self, higher_int: i32, lower_int: i32) { + self.higher_int = higher_int; + self.lower_int = lower_int; + } +} + +impl fmt::Display for LogicLong { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "LogicLong({},{})", self.higher_int, self.lower_int) + } +} diff --git a/libserver/src/logic/mod.rs b/libserver/src/logic/mod.rs new file mode 100644 index 0000000..d0768a9 --- /dev/null +++ b/libserver/src/logic/mod.rs @@ -0,0 +1,17 @@ +pub mod avatar; +pub mod battle; +pub mod command; +pub mod data; +pub mod home; +pub mod json; +pub mod math; +pub mod mode; +pub mod spell; +pub mod time; + +pub struct LogicVersion; + +impl LogicVersion { + pub const MAJOR: i32 = 2; + pub const BUILD: i32 = 1666; +} diff --git a/libserver/src/logic/mode.rs b/libserver/src/logic/mode.rs new file mode 100644 index 0000000..3ed0dd1 --- /dev/null +++ b/libserver/src/logic/mode.rs @@ -0,0 +1,84 @@ +use crate::{ + allocator, + ffi_util::{import, Nullable}, + util::data_stream::ChecksumEncoder, +}; + +use super::{ + avatar::LogicClientAvatar, battle::LogicBattle, command::LogicCommandManager, + home::LogicClientHome, time::LogicTime, +}; + +#[repr(C)] +pub struct LogicGameMode { + unk_gap_1: [u8; 36], // 0-36 + pub logic_command_manager: &'static mut LogicCommandManager, // 36 + game_object_manager: usize, // 40 // TODO! + achievement_manager: usize, // 44 // TODO! + pub home: Nullable, // 48 + pub player_avatar: Nullable, // 52 + pub time: LogicTime, // 56 + unk_gap_2: [u8; 8], // 116 - 124 + pub battle: Nullable, // 124 + unk_gap_3: [u8; 168], // 128 - 296 +} + +impl LogicGameMode { + pub fn new(is_battle: bool) -> &'static mut Self { + import!(logic_game_mode_ctor(ptr: *mut LogicGameMode, is_battle: bool) -> () = 0x119DAC + 1); + let instance = allocator::new::(); + logic_game_mode_ctor(instance, is_battle); + unsafe { &mut *instance } + } + + pub fn load_home_state( + &mut self, + logic_client_home: *const LogicClientHome, + logic_client_avatar: *const LogicClientAvatar, + home_passed_time_seconds: i32, + home_passed_time_ticks: i32, + home_random_seed: i32, + ) { + import!(logic_game_mode_load_home_state(lgm: *mut LogicGameMode, lch: *const LogicClientHome, lca: *const LogicClientAvatar, hpts: i32, hptt: i32, hrs: i32) -> () = 0x11A274 + 1); + + logic_game_mode_load_home_state( + self, + logic_client_home, + logic_client_avatar, + home_passed_time_seconds, + home_passed_time_ticks, + home_random_seed, + ); + } + + pub fn add_player(&mut self, player_avatar: &LogicClientAvatar) { + import!(logic_game_mode_add_player(lgm: *mut LogicGameMode, player_avatar: *const LogicClientAvatar) -> () = 0x11AD80 + 1); + logic_game_mode_add_player(self, player_avatar); + } + + pub fn update_one_tick(&mut self) { + import!(logic_game_mode_update_one_tick(lgm: *mut LogicGameMode) -> () = 0x11A718 + 1); + logic_game_mode_update_one_tick(self); + } + + pub fn calculate_checksum(&self) -> i32 { + import!(logic_game_mode_calculate_checksum(lgm: *const LogicGameMode) -> i32 = 0x11A85C + 1); + logic_game_mode_calculate_checksum(self) + } + + pub fn encode( + &self, + encoder: &mut ChecksumEncoder, + include_commands: bool, + avatar: &LogicClientAvatar, + unk: bool, + ) { + import!(logic_game_mode_encode(lgm: *const LogicGameMode, encoder: *mut ChecksumEncoder, include_commands: bool, avatar: *const LogicClientAvatar, unk: bool) -> () = 0x11A8D4 + 1); + logic_game_mode_encode(self, encoder, include_commands, avatar, unk); + } + + pub fn destruct(&self) { + import!(logic_game_mode_destruct(lgm: *const LogicGameMode) -> () = 0x11A034 + 1); + logic_game_mode_destruct(self); + } +} diff --git a/libserver/src/logic/spell.rs b/libserver/src/logic/spell.rs new file mode 100644 index 0000000..2f70dbd --- /dev/null +++ b/libserver/src/logic/spell.rs @@ -0,0 +1,69 @@ +use crate::{allocator, array_list::LogicArrayList, ffi_util::import}; + +use super::{data::LogicSpellData, json::LogicJSONNode}; + +#[repr(C)] +pub struct LogicSpellDeck { + vtable: usize, + unk_gap_1: [u8; 28], +} + +#[repr(C)] +pub struct LogicSpellCollection { + pub spells: LogicArrayList<&'static mut LogicSpell>, + pub current_sort: i32, +} + +#[repr(C)] +pub struct LogicSpell { + pub data: &'static LogicSpellData, + pub level_index: i32, + pub create_time: i32, + pub count: i32, + pub listener: &'static LogicSpellListener, + pub new_upgrade: bool, + pub new_flag: bool, + pub new_count: i32, + pub use_count: i32, +} + +#[repr(C)] +pub struct LogicSpellListener { + vtable: usize, +} + +impl LogicSpell { + pub fn new() -> &'static mut Self { + import!(logic_spell_ctor(ptr: *mut LogicSpell) -> () = 0x117FCC + 1); + let instance = allocator::new::(); + logic_spell_ctor(instance); + unsafe { &mut *instance } + } +} + +impl LogicSpellDeck { + #[expect(unused)] + pub fn new() -> &'static mut Self { + import!(logic_spell_deck_ctor(ptr: *mut LogicSpellDeck) -> () = 0x1188FC + 1); + let instance = allocator::new::(); + logic_spell_deck_ctor(instance); + unsafe { &mut *instance } + } + + pub fn load(&mut self, data: &LogicJSONNode) { + import!(logic_spell_deck_load(ptr: *mut LogicSpellDeck, data: *const LogicJSONNode) -> () = 0x118A7C + 1); + logic_spell_deck_load(self, data); + } + + pub fn clone(&self) -> &'static mut Self { + import!(logic_spell_deck_clone(ptr: *const LogicSpellDeck) -> *mut LogicSpellDeck = 0x11890C + 1); + unsafe { &mut *logic_spell_deck_clone(self) } + } +} + +impl LogicSpellCollection { + pub fn add_spell(&mut self, spell: &'static LogicSpell) { + import!(logic_spell_collection_add_spell(ptr: *const LogicSpellCollection, spell: *const LogicSpell) -> () = 0x118708 + 1); + logic_spell_collection_add_spell(self, spell); + } +} diff --git a/libserver/src/logic/time.rs b/libserver/src/logic/time.rs new file mode 100644 index 0000000..a7d223e --- /dev/null +++ b/libserver/src/logic/time.rs @@ -0,0 +1,9 @@ +#[repr(C)] +pub struct LogicTime { + pub tick: i32, // 0 + pub max_tick: i32, // 4 + pub time_multiplier: i32, // 8 + pub real_time: i32, // 12 + pub prev_tick: i32, // (?) 16 + unk_gap_1: [u8; 40], // 20 - 60 +} diff --git a/libserver/src/network/connection.rs b/libserver/src/network/connection.rs new file mode 100644 index 0000000..e4a1976 --- /dev/null +++ b/libserver/src/network/connection.rs @@ -0,0 +1,8 @@ +#[repr(C)] +pub struct Connection { + pub fd: i32, + pub is_connected: bool, + unk_1: i32, + unk_2: i32, + unk_3: i32, +} diff --git a/libserver/src/network/encryption.rs b/libserver/src/network/encryption.rs new file mode 100644 index 0000000..89a4fd2 --- /dev/null +++ b/libserver/src/network/encryption.rs @@ -0,0 +1,18 @@ +use crate::{allocator, ffi_util::import, sc_string::ScString}; + +#[repr(C)] +pub struct RC4Encrypter { + vtable: usize, + x: i32, + y: i32, + key: [u8; 256], +} + +impl RC4Encrypter { + pub fn new(key: &str, nonce: &str) -> *const Self { + import!(rc4_encrypter_ctor(ptr: *const RC4Encrypter, key: *const ScString, nonce: *const ScString) -> () = 0x15C1B0 + 1); + let instance = allocator::new::(); + rc4_encrypter_ctor(instance, ScString::new(key), ScString::new(nonce)); + instance + } +} diff --git a/libserver/src/network/message/account.rs b/libserver/src/network/message/account.rs new file mode 100644 index 0000000..946afd1 --- /dev/null +++ b/libserver/src/network/message/account.rs @@ -0,0 +1,77 @@ +use crate::{ + allocator, + ffi_util::{import, Nullable}, + logic::math::LogicLong, + sc_string::ScString, +}; + +use super::PiranhaMessage; + +#[repr(C)] +pub struct LoginMessage { + pub parent: PiranhaMessage, + pub account_id: &'static LogicLong, + pub pass_token: Nullable, + pub client_major_version: i32, + pub client_minor_version: i32, + pub client_build: i32, +} + +impl LoginMessage { + pub const MESSAGE_TYPE: u16 = 10101; +} + +#[repr(C)] +pub struct KeepAliveMessage { + pub parent: PiranhaMessage, +} + +impl KeepAliveMessage { + pub const MESSAGE_TYPE: u16 = 10108; +} + +#[repr(C)] +pub struct LoginOkMessage { + pub parent: PiranhaMessage, + pub account_id: &'static LogicLong, + pub home_id: &'static LogicLong, + pub pass_token: Nullable, + pub facebook_id: Nullable, + pub gamecenter_id: Nullable, + pub facebook_app_id: Nullable, + pub server_major_version: i32, + pub server_minor_version: i32, + pub server_build: i32, + pub content_version: i32, + pub environment: Nullable, + pub session_count: i32, + pub play_time_seconds: i32, + pub days_since_started_playing: i32, + pub server_time: Nullable, + pub account_created_date: Nullable, + pub google_service_id: Nullable, + pub unk: i32, +} + +impl LoginOkMessage { + pub fn new() -> &'static mut Self { + import!(login_ok_message_ctor(ptr: *mut LoginOkMessage) -> () = 0x12A644 + 1); + let instance = allocator::new::(); + login_ok_message_ctor(instance); + unsafe { &mut *instance } + } +} + +#[repr(C)] +pub struct KeepAliveServerMessage { + pub parent: PiranhaMessage, +} + +impl KeepAliveServerMessage { + pub fn new() -> &'static mut Self { + import!(keep_alive_server_message_ctor(ptr: *mut KeepAliveServerMessage) -> () = 0x129E7C + 1); + let instance = allocator::new::(); + keep_alive_server_message_ctor(instance); + unsafe { &mut *instance } + } +} diff --git a/libserver/src/network/message/home.rs b/libserver/src/network/message/home.rs new file mode 100644 index 0000000..a8d626e --- /dev/null +++ b/libserver/src/network/message/home.rs @@ -0,0 +1,76 @@ +use crate::{ + allocator, + array_list::LogicArrayList, + ffi_util::{import, Nullable}, + logic::{ + avatar::LogicClientAvatar, command::LogicCommand, data::LogicNpcData, home::LogicClientHome, + }, +}; + +use super::PiranhaMessage; + +#[repr(C)] +pub struct GoHomeMessage { + pub parent: PiranhaMessage, + unk_bool: bool, +} + +impl GoHomeMessage { + pub const MESSAGE_TYPE: u16 = 14101; +} + +#[repr(C)] +pub struct EndClientTurnMessage { + pub parent: PiranhaMessage, + pub commands: &'static LogicArrayList<&'static LogicCommand>, + pub tick: i32, + pub checksum: i32, +} + +impl EndClientTurnMessage { + pub const MESSAGE_TYPE: u16 = 14102; +} + +#[repr(C)] +pub struct StartMissionMessage { + pub parent: PiranhaMessage, + pub npc: &'static LogicNpcData, +} + +impl StartMissionMessage { + pub const MESSAGE_TYPE: u16 = 14104; +} + +#[repr(C)] +pub struct OwnHomeDataMessage { + pub parent: PiranhaMessage, + pub logic_client_home: Nullable, + pub logic_client_avatar: Nullable, + pub random_seed: i32, +} + +impl OwnHomeDataMessage { + pub fn new() -> &'static mut Self { + import!(own_home_data_message_ctor(ptr: *mut OwnHomeDataMessage) -> () = 0x130480 + 1); + let instance = allocator::new::(); + own_home_data_message_ctor(instance); + unsafe { &mut *instance } + } +} + +#[repr(C)] +pub struct OutOfSyncMessage { + pub parent: PiranhaMessage, + pub server_checksum: i32, + pub client_checksum: i32, + pub tick: i32, +} + +impl OutOfSyncMessage { + pub fn new() -> &'static mut Self { + import!(out_of_sync_message_ctor(ptr: *mut OutOfSyncMessage) -> () = 0x130330 + 1); + let instance = allocator::new::(); + out_of_sync_message_ctor(instance); + unsafe { &mut *instance } + } +} diff --git a/libserver/src/network/message/mod.rs b/libserver/src/network/message/mod.rs new file mode 100644 index 0000000..58422c8 --- /dev/null +++ b/libserver/src/network/message/mod.rs @@ -0,0 +1,27 @@ +mod account; +mod home; +mod sector; + +pub use account::*; +pub use home::*; +pub use sector::*; + +use crate::util::data_stream::ByteStream; + +#[repr(C)] +pub struct PiranhaMessageVTable { + finalizer_1: extern "C" fn(ptr: *const PiranhaMessage), + finalizer_2: extern "C" fn(ptr: *const PiranhaMessage), + encode: extern "C" fn(ptr: *const PiranhaMessage), + decode: extern "C" fn(ptr: *const PiranhaMessage), + pub get_service_node_type: extern "C" fn() -> i32, + pub get_message_type: extern "C" fn() -> u16, +} + +#[repr(C)] +pub struct PiranhaMessage { + pub vtable: &'static PiranhaMessageVTable, + pub version: i32, + pub stream: ByteStream, + pub proxy_session_id: i32, +} diff --git a/libserver/src/network/message/sector.rs b/libserver/src/network/message/sector.rs new file mode 100644 index 0000000..2281533 --- /dev/null +++ b/libserver/src/network/message/sector.rs @@ -0,0 +1,17 @@ +use crate::{allocator, ffi_util::import}; + +use super::PiranhaMessage; + +#[repr(C)] +pub struct SectorStateMessage { + pub parent: PiranhaMessage, +} + +impl SectorStateMessage { + pub fn new() -> &'static mut Self { + import!(sector_state_message_ctor(ptr: *mut SectorStateMessage) -> () = 0x132DA0 + 1); + let instance = allocator::new::(); + sector_state_message_ctor(instance); + unsafe { &mut *instance } + } +} diff --git a/libserver/src/network/message_factory.rs b/libserver/src/network/message_factory.rs new file mode 100644 index 0000000..342218e --- /dev/null +++ b/libserver/src/network/message_factory.rs @@ -0,0 +1,23 @@ +use std::mem::MaybeUninit; + +use crate::ffi_util::import; + +#[repr(C)] +pub struct LogicScrollMessageFactory { + vtable: usize, + is_udp_factory: bool, +} + +impl LogicScrollMessageFactory { + pub const RC4_KEY: &str = "fhsd6f86f67rt8fw78fw789we78r9789wer6re"; + + pub fn new(is_udp_factory: bool) -> Self { + import!(logic_scroll_message_factory_ctor(ptr: *const LogicScrollMessageFactory, is_udp: bool) -> () = 0x118F60 + 1); + + unsafe { + let instance = MaybeUninit::zeroed().assume_init(); + logic_scroll_message_factory_ctor(&instance, is_udp_factory); + instance + } + } +} diff --git a/libserver/src/network/messaging.rs b/libserver/src/network/messaging.rs new file mode 100644 index 0000000..e28d9de --- /dev/null +++ b/libserver/src/network/messaging.rs @@ -0,0 +1,57 @@ +use crate::{allocator::malloc, ffi_util::import}; + +use super::{message::PiranhaMessage, Connection, LogicScrollMessageFactory, RC4Encrypter}; + +#[repr(transparent)] +pub struct Messaging(pub *const u8); + +impl Messaging { + pub fn new(queue_size: usize) -> Self { + import!(messaging_ctor(ptr: *const u8, queue_size: i32) -> () = 0x138C2C + 1); + let instance = malloc(300); + messaging_ctor(instance, queue_size as i32); + Self(instance) + } + + pub fn set_encrypters( + &mut self, + encrypter: *const RC4Encrypter, + decrypter: *const RC4Encrypter, + ) { + import!(messaging_set_encrypters(ptr: *const u8, en: *const RC4Encrypter, de: *const RC4Encrypter, a4: bool) -> () = 0x138BFC + 1); + messaging_set_encrypters(self.0, encrypter, decrypter, false); + } + + pub fn on_receive(&self, connection: &Connection) { + import!(messaging_on_receive(ptr: *const u8, connection: *const Connection) -> () = 0x139650 + 1); + messaging_on_receive(self.0, connection); + } + + pub fn next_message(&self) -> Option<&'static PiranhaMessage> { + import!(messaging_next_message(ptr: *const u8) -> *const PiranhaMessage = 0x1390B4 + 1); + let message = messaging_next_message(self.0); + (!message.is_null()).then(|| unsafe { &*message }) + } + + pub fn send(&self, message: *const T) { + import!(messaging_send(ptr: *const u8, message: usize) -> () = 0x138FB8 + 1); + messaging_send(self.0, message as usize); + } + + pub fn on_wakeup(&self, connection: &Connection) { + import!(messaging_on_wakeup(ptr: *const u8, connection: *const Connection) -> () = 0x1394C4 + 1); + messaging_on_wakeup(self.0, connection); + } + + pub fn get_connection(&self) -> &Connection { + unsafe { &*(self.0.wrapping_add(64) as *const Connection) } + } + + pub fn get_connection_mut(&self) -> &mut Connection { + unsafe { &mut *(self.0.wrapping_add(64) as *mut Connection) } + } + + pub fn set_message_factory(&mut self, factory: &'static LogicScrollMessageFactory) { + unsafe { *(self.0.wrapping_add(4) as *mut *const LogicScrollMessageFactory) = factory } + } +} diff --git a/libserver/src/network/mod.rs b/libserver/src/network/mod.rs new file mode 100644 index 0000000..510f32c --- /dev/null +++ b/libserver/src/network/mod.rs @@ -0,0 +1,11 @@ +mod connection; +mod encryption; +mod message_factory; +mod messaging; + +pub mod message; + +pub use connection::Connection; +pub use encryption::RC4Encrypter; +pub use message_factory::LogicScrollMessageFactory; +pub use messaging::Messaging; diff --git a/libserver/src/resources.rs b/libserver/src/resources.rs new file mode 100644 index 0000000..726ecf0 --- /dev/null +++ b/libserver/src/resources.rs @@ -0,0 +1,142 @@ +use crate::{ + allocator::malloc, + array_list::LogicArrayList, + ffi_util::import, + logic::data::{LogicDataTables, LogicLocationData}, + sc_string::ScString, +}; + +#[repr(transparent)] +pub struct DataLoaderFactory(*const u8); + +impl DataLoaderFactory { + pub fn new() -> Self { + import!(data_loader_factory_ctor(ptr: *const u8) -> () = 0x15EC38 + 1); + + let instance = malloc(4); + data_loader_factory_ctor(instance); + Self(instance) + } +} + +struct LogicResources; + +impl LogicResources { + pub fn create_data_table_resources_array() -> &'static LogicArrayList { + import!(logic_resources_create_data_table_resources_array() -> *const u8 = 0x1078E8 + 1); + unsafe { + &*(logic_resources_create_data_table_resources_array() + as *const LogicArrayList) + } + } + + pub fn load( + resources_array: &LogicArrayList, + index: usize, + csv: &CSVNode, + ) { + import!(logic_resources_load(r: *const LogicArrayList, i: i32, csv: *const u8, a4: i32, a5: i32) -> () = 0x10833C + 1); + logic_resources_load( + resources_array as *const LogicArrayList, + index as i32, + csv.0, + 0, + 0, + ); + } +} + +#[repr(transparent)] +struct LogicDataTableResource(*const u8); + +impl LogicDataTableResource { + pub fn get_file_name(&self) -> String { + unsafe { (&*(self.0.wrapping_add(8) as *const ScString)).to_string() } + } +} + +#[repr(transparent)] +pub struct CSVNode(pub *const u8); + +pub struct ResourceManager; + +impl ResourceManager { + pub fn init(data_loader_factory: &DataLoaderFactory) { + import!(resource_manager_init(dlf: *const u8, data_path: *const u8) -> () = 0x15D668 + 1); + resource_manager_init(data_loader_factory.0, [0x00].as_ptr()); + } + + pub fn resource_to_load() -> i32 { + import!(resource_manager_resource_to_load() -> i32 = 0x15E068 + 1); + resource_manager_resource_to_load() + } + + pub fn load_next_resource() { + import!(resource_manager_load_next_resource() -> () = 0x15DC10 + 1); + resource_manager_load_next_resource(); + } + + pub fn get_csv(file_name: &str) -> CSVNode { + import!(resource_manager_get_csv(file_name: *const ScString) -> *const u8 = 0x15E08C + 1); + CSVNode(resource_manager_get_csv(ScString::new(file_name))) + } +} + +#[repr(transparent)] +struct ResourceListener(*const u8); + +impl ResourceListener { + pub fn new() -> Self { + import!(resource_listener_ctor(ptr: *const u8) -> () = 0x15C370 + 1); + + let instance = malloc(20); + resource_listener_ctor(instance); + Self(instance) + } + + pub fn add_file(&self, name: &str) { + import!(resource_listener_add_file(ptr: *const u8, name: *const ScString, a3: i32, a4: i32, a5: i32, a6: i32) -> () = 0x15C51C + 1); + resource_listener_add_file(self.0, ScString::new(name), -1, -1, -1, -1); + } + + pub fn start_loading(&self) { + import!(resource_listener_start_loading(ptr: *const u8) -> () = 0x15C7BC + 1); + resource_listener_start_loading(self.0); + } +} + +pub fn init() { + let data_loader_factory = DataLoaderFactory::new(); + ResourceManager::init(&data_loader_factory); + + let listener = ResourceListener::new(); + let data_table_resources_array = LogicResources::create_data_table_resources_array(); + + for resource in data_table_resources_array.as_slice().iter() { + listener.add_file(&resource.get_file_name()); + } + + listener.start_loading(); + + while ResourceManager::resource_to_load() != 0 { + ResourceManager::load_next_resource(); + } + + for (index, resource) in data_table_resources_array.as_slice().iter().enumerate() { + let csv = ResourceManager::get_csv(&resource.get_file_name()); + LogicResources::load(data_table_resources_array, index, &csv); + } +} + +pub fn prepare_location_data(location: &LogicLocationData) { + let tilemap_file_name = location.get_tilemap_file_name(); + load_one_resource(&tilemap_file_name); + + let csv = ResourceManager::get_csv(&tilemap_file_name); + LogicDataTables::init_tilemap(csv, location.parent.global_id % 1_000_000); +} + +pub fn load_one_resource(path: &str) { + import!(game_main_load_asset(path: *const ScString) -> () = 0x5A73C + 1); + game_main_load_asset(ScString::new(path)); +} diff --git a/libserver/src/sc_string.rs b/libserver/src/sc_string.rs new file mode 100644 index 0000000..18dd25f --- /dev/null +++ b/libserver/src/sc_string.rs @@ -0,0 +1,72 @@ +use std::{ + ffi::{CStr, CString}, + fmt, +}; + +use crate::{allocator, ffi_util::import}; + +#[repr(C)] +pub struct ScString { + unk_1: i32, + length: i32, + content: [u8; 8], + unk_2: i32, +} + +impl From for &'static ScString +where + S: AsRef, +{ + fn from(value: S) -> Self { + import!(string_ctor(ptr: *const ScString, data: *const u8) -> () = 0x13AF4C + 1); + let sc_string = unsafe { &*allocator::new::() }; + + string_ctor(sc_string, CString::new(value.as_ref()).unwrap().as_ptr()); + sc_string + } +} + +impl ScString { + pub fn new>(value: S) -> &'static Self { + From::from(value) + } + + pub fn assign(&mut self, content: &str) { + import!(string_assignment_operator(lhs: *mut ScString, rhs: *const ScString) -> () = 0x13B16C + 1); + string_assignment_operator(self, ScString::new(content)); + } +} + +impl fmt::Display for ScString { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let length = self.length as usize; + if length + 1 > 8 { + let c_string = usize::from_le_bytes(self.content[..4].try_into().unwrap()) as *const u8; + unsafe { + write!( + f, + "{}", + CStr::from_ptr(c_string).to_string_lossy().to_string() + ) + } + } else if length > 0 { + unsafe { + write!( + f, + "{}", + CStr::from_ptr(self.content.as_ptr()) + .to_string_lossy() + .to_string() + ) + } + } else { + Ok(()) + } + } +} + +impl fmt::Debug for ScString { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "\"{}\"", self) + } +} diff --git a/libserver/src/session/handlers/account.rs b/libserver/src/session/handlers/account.rs new file mode 100644 index 0000000..8fd6945 --- /dev/null +++ b/libserver/src/session/handlers/account.rs @@ -0,0 +1,51 @@ +use tracing::info; + +use crate::{ + logic::{ + math::LogicLong, + LogicVersion, + }, + network::message::{ + KeepAliveMessage, KeepAliveServerMessage, LoginMessage, LoginOkMessage, + }, + sc_string::ScString, + session::{PlayerSaveData, PlayerSession}, +}; + +pub fn on_login_message(session: &mut PlayerSession, message: &LoginMessage) { + info!( + "LoginMessage received: account_id: {}, pass_token: {:?}, client version: {}.{}.{}", + message.account_id, + message.pass_token.get(), + message.client_major_version, + message.client_build, + message.client_minor_version + ); + + let login_ok_message = LoginOkMessage::new(); + + login_ok_message.account_id = LogicLong::new(0, 1); + login_ok_message.home_id = LogicLong::new(0, 1); + login_ok_message + .pass_token + .set(ScString::new("MostSecureTokenEver")); + + login_ok_message.server_major_version = LogicVersion::MAJOR; + login_ok_message.server_build = LogicVersion::BUILD; + login_ok_message.environment.set(ScString::new("dev")); + + let account_id = LogicLong { + higher_int: 0, + lower_int: 1, + }; + + session.player_save_data = Some(PlayerSaveData::create_default_data(&account_id)); + session.account_id = Some(account_id); + + session.send_message(login_ok_message); + session.go_home(); +} + +pub fn on_keep_alive_message(session: &mut PlayerSession, _message: &KeepAliveMessage) { + session.send_message(KeepAliveServerMessage::new()); +} diff --git a/libserver/src/session/handlers/home.rs b/libserver/src/session/handlers/home.rs new file mode 100644 index 0000000..bb133df --- /dev/null +++ b/libserver/src/session/handlers/home.rs @@ -0,0 +1,71 @@ +use tracing::{error, info}; + +use crate::{ + network::message::{ + EndClientTurnMessage, GoHomeMessage, OutOfSyncMessage, + StartMissionMessage, + }, + session::{PlayerSaveData, PlayerSession}, +}; + +pub fn on_go_home_message(session: &mut PlayerSession, _message: &GoHomeMessage) { + session.go_home(); +} + +pub fn on_end_client_turn_message(session: &mut PlayerSession, message: &EndClientTurnMessage) { + let Some(logic_game_mode) = session.logic_game_mode.as_mut() else { + error!("EndClientTurnMessage received but LogicGameMode is NULL!"); + return; + }; + + info!( + "EndClientTurnMessage received: tick: {}, checksum: {}, command count: {}", + message.tick, message.checksum, message.commands.count + ); + + while logic_game_mode.time.tick < message.tick { + for command in message.commands.as_slice() { + if command.execute_tick == logic_game_mode.time.tick { + info!( + "received command of type: {}", + (command.vtable.get_command_type)() + ); + logic_game_mode.logic_command_manager.add_command(command); + } + } + + logic_game_mode.update_one_tick(); + } + + let checksum = logic_game_mode.calculate_checksum(); + if checksum != message.checksum { + error!( + "client and server are out of sync! tick: {}, server checksum: {}, client checksum: {}", + logic_game_mode.time.tick, checksum, message.checksum + ); + + let out_of_sync_message = OutOfSyncMessage::new(); + out_of_sync_message.server_checksum = checksum; + out_of_sync_message.client_checksum = message.checksum; + out_of_sync_message.tick = logic_game_mode.time.tick; + session.messaging.send(out_of_sync_message); + } + + if let Some(logic_client_home) = logic_game_mode.home.get() { + // Save data if we're in home state + + session.player_save_data = Some(PlayerSaveData::new( + logic_client_home, + logic_game_mode.player_avatar.get().unwrap(), + )); + } +} + +pub fn on_start_mission_message(session: &mut PlayerSession, message: &StartMissionMessage) { + info!( + "StartMissionMessage received: npc: {}, location: {}", + message.npc.parent.global_id, message.npc.location.parent.global_id + ); + + session.start_mission(message.npc); +} diff --git a/libserver/src/session/handlers/mod.rs b/libserver/src/session/handlers/mod.rs new file mode 100644 index 0000000..6ebf9df --- /dev/null +++ b/libserver/src/session/handlers/mod.rs @@ -0,0 +1,30 @@ +mod account; +mod home; + +use account::*; +use home::*; + +macro_rules! messages { + ($($name:ident),*) => { + pub fn handle_message( + session: &mut super::PlayerSession, + message: &crate::network::message::PiranhaMessage, + ) { + use crate::network::message::*; + + match (message.vtable.get_message_type)() { + $( + $name::MESSAGE_TYPE => ::paste::paste!([]( + session, + unsafe { ::std::mem::transmute(message) } + )), + )* + unhandled => ::tracing::warn!("unhandled message: {unhandled}"), + } + } + }; +} + +messages! { + LoginMessage, KeepAliveMessage, GoHomeMessage, EndClientTurnMessage, StartMissionMessage +} diff --git a/libserver/src/session/mod.rs b/libserver/src/session/mod.rs new file mode 100644 index 0000000..c310fff --- /dev/null +++ b/libserver/src/session/mod.rs @@ -0,0 +1,118 @@ +use rand::RngCore; +use save::PlayerSaveData; + +use crate::{ + allocator, + logic::{ + avatar::LogicClientAvatar, + data::{LogicDataTables, LogicNpcData}, + math::LogicLong, + mode::LogicGameMode, + }, + network::{message::PiranhaMessage, Messaging}, + resources, +}; + +mod handlers; +mod protocol_util; +mod save; + +pub struct PlayerSession { + messaging: Messaging, + pub account_id: Option, + pub player_save_data: Option, + pub logic_game_mode: Option<&'static mut LogicGameMode>, +} + +impl PlayerSession { + pub fn new(messaging: Messaging) -> Self { + Self { + messaging, + account_id: None, + player_save_data: None, + logic_game_mode: None, + } + } + + pub fn send_message(&self, message: *const T) { + self.messaging.send(message); + } + + pub fn go_home(&mut self) { + self.destruct_game_mode(); + + let player_save_data = self.player_save_data.as_ref().unwrap(); + + let logic_client_home = player_save_data.get_logic_client_home(); + let logic_client_avatar = player_save_data.get_logic_client_avatar(); + let random_seed = rand::rng().next_u32() as i32; + + let own_home_data_message = + protocol_util::build_own_home_data(logic_client_home, logic_client_avatar, random_seed); + + let logic_game_mode = LogicGameMode::new(false); + logic_game_mode.load_home_state(logic_client_home, logic_client_avatar, 0, -1, random_seed); + + self.logic_game_mode = Some(logic_game_mode); + self.send_message(own_home_data_message); + } + + pub fn start_mission(&mut self, npc: &'static LogicNpcData) { + const TRAINING_ARENA_ID: i32 = 54000001; + + self.destruct_game_mode(); + + resources::prepare_location_data(npc.location); + let arena = LogicDataTables::get_data_by_id(TRAINING_ARENA_ID).unwrap(); + let player_save_data = self.player_save_data.as_ref().unwrap(); + let player_avatar = player_save_data.get_logic_client_avatar(); + + let player_deck = player_save_data.get_spell_deck(); + let npc_deck = player_deck.clone(); + + let npc_avatar = LogicClientAvatar::new(); + npc_avatar.id.set(-1, -1); + npc_avatar.account_id.set(-1, -1); + npc_avatar.home_id.set(-1, -1); + npc_avatar.arena = arena; + npc_avatar.level = player_avatar.level; + + let battle_mode = LogicGameMode::new(true); + let battle = battle_mode.battle.get_mut().unwrap(); + + battle.set_location(npc.location, false, 0); + battle.battle_type = 1; + battle.npc_data = npc; + battle.arena_data = arena; + + battle.set_spell_decks(player_deck, npc_deck); + battle_mode.add_player(player_avatar); + battle_mode.add_player(npc_avatar); + + let sector_state_message = protocol_util::build_sector_state(battle_mode, player_avatar); + + self.logic_game_mode = Some(battle_mode); + self.send_message(sector_state_message); + } + + pub fn destruct_game_mode(&mut self) { + if let Some(logic_game_mode) = self.logic_game_mode.take() { + logic_game_mode.destruct(); + allocator::free(logic_game_mode); + } + } + + pub fn work_until_disconnect(&mut self) { + while self.messaging.get_connection().is_connected { + self.messaging.on_receive(self.messaging.get_connection()); + while let Some(message) = self.messaging.next_message() { + self.receive_message(message); + } + } + } + + fn receive_message(&mut self, message: &PiranhaMessage) { + handlers::handle_message(self, message); + self.messaging.on_wakeup(self.messaging.get_connection()); + } +} diff --git a/libserver/src/session/protocol_util.rs b/libserver/src/session/protocol_util.rs new file mode 100644 index 0000000..26b4149 --- /dev/null +++ b/libserver/src/session/protocol_util.rs @@ -0,0 +1,40 @@ +use crate::{ + logic::{avatar::LogicClientAvatar, home::LogicClientHome, mode::LogicGameMode}, + network::message::{OwnHomeDataMessage, SectorStateMessage}, +}; + +pub fn build_own_home_data( + logic_client_home: &LogicClientHome, + logic_client_avatar: &LogicClientAvatar, + random_seed: i32, +) -> &'static mut OwnHomeDataMessage { + let own_home_data_message = OwnHomeDataMessage::new(); + + own_home_data_message + .logic_client_home + .set(LogicClientHome::clone(logic_client_home)); + + own_home_data_message + .logic_client_avatar + .set(LogicClientAvatar::clone(logic_client_avatar)); + + own_home_data_message.random_seed = random_seed; + own_home_data_message +} + +pub fn build_sector_state( + logic_game_mode: &LogicGameMode, + client_avatar: &LogicClientAvatar, +) -> &'static mut SectorStateMessage { + let sector_state_message = SectorStateMessage::new(); + sector_state_message.parent.stream.write_byte(0); + sector_state_message.parent.stream.parent.checksum = 0; + logic_game_mode.encode( + &mut sector_state_message.parent.stream.parent, + true, + client_avatar, + false, + ); + + sector_state_message +} diff --git a/libserver/src/session/save.rs b/libserver/src/session/save.rs new file mode 100644 index 0000000..db419b8 --- /dev/null +++ b/libserver/src/session/save.rs @@ -0,0 +1,112 @@ +use crate::{ + allocator, + logic::{ + avatar::LogicClientAvatar, + data::LogicDataTables, + home::LogicClientHome, + json::LogicJSONParser, + math::LogicLong, + spell::{LogicSpell, LogicSpellDeck}, + }, + util::data_stream::ByteStream, +}; + +pub struct PlayerSaveData { + pub logic_client_home: Vec, + pub logic_client_avatar: Vec, +} + +impl PlayerSaveData { + pub fn new( + logic_client_home: &LogicClientHome, + logic_client_avatar: &LogicClientAvatar, + ) -> Self { + let mut stream = ByteStream::new(64); + logic_client_home.encode(&mut stream.parent); + let logic_client_home = stream.get_data().to_vec(); + stream.destruct(); + + let mut stream = ByteStream::new(64); + (logic_client_avatar.vtable.encode)(logic_client_avatar, &mut stream.parent); + let logic_client_avatar = stream.get_data().to_vec(); + stream.destruct(); + + Self { + logic_client_home, + logic_client_avatar, + } + } + + pub fn create_default_data(id: &LogicLong) -> Self { + const STARTING_DECK_JSON: &str = r#"[{"d":26000000,"t":0,"s":0,"l":12,"c":1},{"d":26000001,"t":0,"s":0,"l":12,"c":1},{"d":26000013,"t":0,"s":0,"l":12,"c":1},{"d":28000001,"t":0,"s":0,"l":12,"c":1},{"d":28000000,"t":0,"s":0,"l":10,"c":1},{"d":26000003,"t":0,"s":0,"l":10,"c":1},{"d":26000004,"t":0,"s":0,"l":7,"c":1},{"d":26000005,"t":0,"s":0,"l":12,"c":1}]"#; + + let logic_client_home = LogicClientHome::new(); + logic_client_home + .id + .set(LogicLong::new(id.higher_int, id.lower_int)); + + let starting_deck = LogicJSONParser::parse(STARTING_DECK_JSON).unwrap(); + logic_client_home.spell_deck.load(starting_deck); + + for i in 0..LogicDataTables::get_spell_count() { + let spell_data = LogicDataTables::get_spell_at(i); + if !logic_client_home.has_spell(spell_data) { + let spell = LogicSpell::new(); + spell.data = spell_data; + spell.level_index = spell_data.get_max_level_index(); + logic_client_home.spell_collection.add_spell(spell); + } + } + + let logic_client_avatar = LogicClientAvatar::new(); + logic_client_avatar.id.set(id.higher_int, id.lower_int); + logic_client_avatar + .account_id + .set(id.higher_int, id.lower_int); + logic_client_avatar.home_id.set(id.higher_int, id.lower_int); + logic_client_avatar.name.assign("xeondev"); + logic_client_avatar.name_change_state = 0; + logic_client_avatar.name_set_by_user = true; + logic_client_avatar.arena = LogicDataTables::get_data_by_id(54000007).unwrap(); + logic_client_avatar.npc_win_count = 8; + logic_client_avatar.score = 3000; + logic_client_avatar.level = 13; + + let instance = Self::new(logic_client_home, logic_client_avatar); + + logic_client_home.destruct(); + allocator::free(logic_client_home as *const _); + + (logic_client_avatar.vtable.destruct)(logic_client_avatar); + allocator::free(logic_client_avatar as *const _); + + instance + } + + pub fn get_logic_client_home(&self) -> &'static mut LogicClientHome { + let logic_client_home = LogicClientHome::new(); + let mut stream = ByteStream::from(&self.logic_client_home); + logic_client_home.decode(&mut stream); + stream.destruct(); + + logic_client_home + } + + pub fn get_logic_client_avatar(&self) -> &'static mut LogicClientAvatar { + let logic_client_avatar = LogicClientAvatar::new(); + let mut stream = ByteStream::from(&self.logic_client_avatar); + (logic_client_avatar.vtable.decode)(logic_client_avatar, &mut stream); + stream.destruct(); + + logic_client_avatar + } + + pub fn get_spell_deck(&self) -> &'static mut LogicSpellDeck { + let logic_client_home = self.get_logic_client_home(); + let cloned_deck = logic_client_home.spell_deck.clone(); + logic_client_home.destruct(); + allocator::free(logic_client_home); + + cloned_deck + } +} diff --git a/libserver/src/util/data_stream.rs b/libserver/src/util/data_stream.rs new file mode 100644 index 0000000..98c766b --- /dev/null +++ b/libserver/src/util/data_stream.rs @@ -0,0 +1,75 @@ +use std::mem::MaybeUninit; + +use crate::ffi_util::import; + +#[repr(C)] +pub struct ChecksumEncoder { + pub vtable: usize, + pub is_checksum_enabled: bool, + pub checksum: i32, + pub checksum_snapshot: i32, +} + +#[repr(C)] +pub struct ByteStream { + pub parent: ChecksumEncoder, + pub offset: i32, + pub length: i32, + pub bit_idx: i32, + pub data: *const u8, + pub capacity: i32, +} + +impl ByteStream { + pub fn new(initial_capacity: usize) -> Self { + import!(byte_stream_ctor(ptr: *mut ByteStream, initial_capacity: i32) -> () = 0x17B098 + 1); + unsafe { + let mut instance = MaybeUninit::zeroed().assume_init(); + byte_stream_ctor(&mut instance, initial_capacity as i32); + instance + } + } + + pub fn write_byte(&mut self, value: u8) { + import!(byte_stream_write_byte(ptr: *mut ByteStream, value: u8) -> () = 0x17B7EC + 1); + byte_stream_write_byte(self, value); + } + + pub fn reset_offset(&mut self) { + self.offset = 0; + self.bit_idx = 0; + } + + pub fn get_length(&self) -> usize { + std::cmp::max(self.offset, self.length) as usize + } + + pub fn get_data(&self) -> &[u8] { + unsafe { std::slice::from_raw_parts(self.data, self.get_length()) } + } + + pub fn destruct(mut self) { + import!(byte_stream_destruct(ptr: *mut ByteStream) -> () = 0x17BC20 + 1); + byte_stream_destruct(&mut self); + } +} + +impl From for ByteStream +where + T: AsRef<[u8]>, +{ + fn from(value: T) -> Self { + import!(byte_stream_ctor(ptr: *mut ByteStream, data: *const u8, length: i32) -> () = 0x17B0E0 + 1); + + unsafe { + let mut instance = MaybeUninit::zeroed().assume_init(); + byte_stream_ctor( + &mut instance, + value.as_ref().as_ptr(), + value.as_ref().len() as i32, + ); + + instance + } + } +} diff --git a/libserver/src/util/mod.rs b/libserver/src/util/mod.rs new file mode 100644 index 0000000..31aec06 --- /dev/null +++ b/libserver/src/util/mod.rs @@ -0,0 +1 @@ +pub mod data_stream; diff --git a/screenshot.png b/screenshot.png new file mode 100644 index 0000000..76b72fd Binary files /dev/null and b/screenshot.png differ