├── data ├── .gitignore ├── Maps │ ├── .gitignore │ ├── README.md │ ├── Maps.csv │ └── Portals.csv └── GameMaps │ ├── .gitignore │ └── README.md ├── .gitignore ├── crates ├── bindings │ ├── .cargo │ │ └── config.toml │ └── Cargo.toml ├── db │ ├── src │ │ ├── lib.rs │ │ ├── error.rs │ │ ├── realm.rs │ │ ├── portal.rs │ │ ├── npc.rs │ │ ├── map.rs │ │ ├── account.rs │ │ └── character.rs │ └── Cargo.toml ├── crypto │ ├── Cargo.toml │ └── src │ │ ├── nop.rs │ │ └── lib.rs ├── math │ └── Cargo.toml ├── primitives │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── tracing-wasm │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── wasm-builder │ ├── Cargo.toml │ └── src │ │ └── prerequisites.rs ├── serde │ ├── Cargo.toml │ └── src │ │ ├── lib.rs │ │ └── errors.rs ├── codec │ └── Cargo.toml ├── server │ ├── Cargo.toml │ └── src │ │ └── error.rs └── network │ ├── Cargo.toml │ └── src │ ├── error.rs │ ├── lib.rs │ └── actor.rs ├── .env.example ├── server ├── game │ ├── src │ │ ├── world │ │ │ ├── mod.rs │ │ │ └── portal.rs │ │ ├── systems │ │ │ ├── mod.rs │ │ │ └── commands.rs │ │ ├── lib.rs │ │ ├── entities │ │ │ ├── floor_item.rs │ │ │ ├── mod.rs │ │ │ └── npc.rs │ │ ├── packets │ │ │ ├── msg_item_info.rs │ │ │ ├── mod.rs │ │ │ ├── msg_transfer.rs │ │ │ ├── msg_npc_info.rs │ │ │ ├── msg_data.rs │ │ │ ├── msg_player.rs │ │ │ ├── msg_map_info.rs │ │ │ ├── msg_connect.rs │ │ │ ├── msg_weather.rs │ │ │ ├── msg_walk.rs │ │ │ ├── msg_user_info.rs │ │ │ ├── msg_npc.rs │ │ │ ├── msg_item.rs │ │ │ ├── msg_talk.rs │ │ │ └── msg_register.rs │ │ ├── utils.rs │ │ ├── constants.rs │ │ ├── test_utils.rs │ │ ├── state │ │ │ └── actor_state.rs │ │ └── main.rs │ └── Cargo.toml └── auth │ ├── src │ ├── state.rs │ ├── error.rs │ └── main.rs │ └── Cargo.toml ├── rust-toolchain.toml ├── tools ├── externref │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── hash-pwd │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── rust-windbg.cmd ├── gamemap-decoder │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── benchbot │ ├── src │ │ ├── error.rs │ │ └── state.rs │ └── Cargo.toml ├── txt-to-csv.py └── npcs.py ├── packets ├── account │ ├── build.rs │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── connect │ ├── build.rs │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── transfer │ ├── build.rs │ ├── Cargo.toml │ └── src │ │ └── lib.rs └── connect-ex │ ├── Cargo.toml │ └── src │ └── lib.rs ├── migrations ├── 7_default_realms.sql ├── 0_accounts.sql ├── 2_realms.sql ├── 4_maps.sql ├── 5_portals.sql ├── 10_npcs.sql ├── 6_default_accounts.sql └── 1_characters.sql ├── .github ├── dependabot.yml ├── workflows │ ├── audit.yml │ ├── lints.yml │ └── ci.yml ├── CONTRIBUTING.md └── CODE_OF_CONDUCT.md ├── macros ├── derive-packetid │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── derive-packetprocessor │ ├── Cargo.toml │ └── src │ │ └── lib.rs └── derive-packethandler │ ├── Cargo.toml │ └── src │ └── lib.rs ├── .envrc ├── rustfmt.toml ├── .cargo └── config.toml ├── .vscode ├── settings.json └── launch.json ├── flake.nix ├── flake.lock └── Cargo.toml /data/.gitignore: -------------------------------------------------------------------------------- 1 | *.db* 2 | -------------------------------------------------------------------------------- /data/Maps/.gitignore: -------------------------------------------------------------------------------- 1 | *.cmap -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | .env 3 | .direnv 4 | *.DS_Store 5 | -------------------------------------------------------------------------------- /crates/bindings/.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | target = "wasm32-unknown-unknown" 3 | -------------------------------------------------------------------------------- /data/Maps/README.md: -------------------------------------------------------------------------------- 1 | Contains the Compressed Maps converted from `GameMaps` folder. 2 | -------------------------------------------------------------------------------- /data/GameMaps/.gitignore: -------------------------------------------------------------------------------- 1 | **/*.DMap 2 | **/*.pul 3 | **/*.scene 4 | **/*.Part 5 | GameMap.dat 6 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | RUST_BACKRACE=1 2 | LOG_VERBOSITY=2 3 | AUTH_PORT=9958 4 | 5 | DATA_LOCATION=./data 6 | -------------------------------------------------------------------------------- /server/game/src/world/mod.rs: -------------------------------------------------------------------------------- 1 | mod map; 2 | pub use map::{Map, Maps}; 3 | 4 | mod portal; 5 | pub use portal::Portal; 6 | -------------------------------------------------------------------------------- /server/game/src/systems/mod.rs: -------------------------------------------------------------------------------- 1 | mod floor; 2 | pub use floor::*; 3 | 4 | mod screen; 5 | pub use screen::*; 6 | 7 | pub mod commands; 8 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "nightly" 3 | components = ["rustfmt", "clippy", "rust-src"] 4 | targets = ["wasm32-unknown-unknown"] 5 | -------------------------------------------------------------------------------- /crates/db/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod account; 2 | pub mod character; 3 | pub mod error; 4 | pub mod map; 5 | pub mod npc; 6 | pub mod portal; 7 | pub mod realm; 8 | 9 | pub use error::Error; 10 | -------------------------------------------------------------------------------- /tools/externref/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "externref-cli" 3 | version = "0.1.0" 4 | edition.workspace = true 5 | 6 | 7 | [dependencies] 8 | externref = { workspace = true, features = ["processor"] } 9 | -------------------------------------------------------------------------------- /packets/account/build.rs: -------------------------------------------------------------------------------- 1 | use tq_wasm_builder::WasmBuilder; 2 | 3 | fn main() { 4 | WasmBuilder::selector() 5 | .with_current_project() 6 | .enable_feature("std") 7 | .build(); 8 | } 9 | -------------------------------------------------------------------------------- /packets/connect/build.rs: -------------------------------------------------------------------------------- 1 | use tq_wasm_builder::WasmBuilder; 2 | 3 | fn main() { 4 | WasmBuilder::selector() 5 | .with_current_project() 6 | .enable_feature("std") 7 | .build(); 8 | } 9 | -------------------------------------------------------------------------------- /packets/transfer/build.rs: -------------------------------------------------------------------------------- 1 | use tq_wasm_builder::WasmBuilder; 2 | 3 | fn main() { 4 | WasmBuilder::selector() 5 | .with_current_project() 6 | .enable_feature("std") 7 | .build(); 8 | } 9 | -------------------------------------------------------------------------------- /tools/hash-pwd/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "hash-pwd" 3 | version = "0.1.0" 4 | authors = ["Shady Khalifa "] 5 | edition.workspace = true 6 | 7 | [dependencies] 8 | bcrypt.workspace = true 9 | -------------------------------------------------------------------------------- /migrations/7_default_realms.sql: -------------------------------------------------------------------------------- 1 | -- Add migration script here 2 | INSERT INTO realms (name, game_ip_address, game_port) 3 | VALUES ( 4 | 'CoEmu', 5 | '192.168.0.200', 6 | -- Change this to your server's IP address 7 | 5816 8 | ) ON CONFLICT(name) DO NOTHING; 9 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "cargo" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | 8 | - package-ecosystem: "github-actions" 9 | directory: "/" 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /tools/rust-windbg.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | setlocal 3 | 4 | for /f "delims=" %%i in ('rustc --print=sysroot') do set rustc_sysroot=%%i 5 | 6 | set rust_etc=%rustc_sysroot%\lib\rustlib\etc 7 | 8 | windbg -c ".nvload %rust_etc%\intrinsic.natvis; .nvload %rust_etc%\liballoc.natvis; .nvload %rust_etc%\libcore.natvis;" %* 9 | -------------------------------------------------------------------------------- /macros/derive-packetid/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "derive-packetid" 3 | version = "0.1.0" 4 | authors = ["Shady Khalifa "] 5 | edition.workspace = true 6 | 7 | [lib] 8 | proc-macro = true 9 | 10 | [dependencies] 11 | syn = { version = "2.0", features = ["full"] } 12 | quote = "1.0" 13 | -------------------------------------------------------------------------------- /.envrc: -------------------------------------------------------------------------------- 1 | if ! has nix_direnv_version || ! nix_direnv_version 2.3.0; then 2 | source_url "https://raw.githubusercontent.com/nix-community/nix-direnv/2.3.0/direnvrc" "sha256-Dmd+j63L84wuzgyjITIfSxSD57Tx7v51DMxVZOsiUD8=" 3 | fi 4 | dotenv_if_exists 5 | watch_file flake.nix 6 | watch_file flake.lock 7 | use flake 8 | # vi: ft=sh 9 | -------------------------------------------------------------------------------- /crates/crypto/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tq-crypto" 3 | version = "0.1.0" 4 | authors = ["Shady Khalifa "] 5 | edition.workspace = true 6 | 7 | [dependencies] 8 | bytes.workspace = true 9 | parking_lot.workspace = true 10 | 11 | [features] 12 | default = ["std"] 13 | std = ["bytes/std"] 14 | -------------------------------------------------------------------------------- /macros/derive-packetprocessor/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "derive-packetprocessor" 3 | version = "0.1.0" 4 | authors = ["Shady Khalifa "] 5 | edition.workspace = true 6 | 7 | [lib] 8 | proc-macro = true 9 | 10 | [dependencies] 11 | syn = { version = "2.0", features = ["full"] } 12 | quote = "1.0" 13 | -------------------------------------------------------------------------------- /migrations/0_accounts.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS accounts ( 2 | account_id INTEGER PRIMARY KEY, 3 | username TEXT NOT NULL UNIQUE CHECK (length(username) <= 16), 4 | password TEXT NOT NULL, 5 | name TEXT DEFAULT NULL CHECK (length(name) <= 32), 6 | email TEXT DEFAULT NULL CHECK (length(email) <= 64) 7 | ); 8 | -------------------------------------------------------------------------------- /tools/gamemap-decoder/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "gamemap-decoder" 3 | version = "0.1.0" 4 | authors = ["Shady Khalifa "] 5 | edition.workspace = true 6 | 7 | [dependencies] 8 | dotenvy.workspace = true 9 | bytes.workspace = true 10 | anyhow.workspace = true 11 | serde.workspace = true 12 | csv = "1" 13 | -------------------------------------------------------------------------------- /crates/math/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tq-math" 3 | version = "0.1.0" 4 | authors = ["Shady Khalifa "] 5 | edition.workspace = true 6 | 7 | [dependencies] 8 | libm = { version = "0.2", default-features = false, optional = true } 9 | 10 | [features] 11 | default = ["std"] 12 | std = [] 13 | libm = ["dep:libm"] 14 | -------------------------------------------------------------------------------- /tools/hash-pwd/src/main.rs: -------------------------------------------------------------------------------- 1 | const SHOW_USAGE: &str = "usage hash-pwd "; 2 | 3 | fn main() -> Result<(), String> { 4 | let pwd = std::env::args().nth(1).ok_or_else(|| SHOW_USAGE.to_owned())?; 5 | let hashed = bcrypt::hash(pwd, bcrypt::DEFAULT_COST).map_err(|e| e.to_string())?; 6 | println!("{}", hashed); 7 | Ok(()) 8 | } 9 | -------------------------------------------------------------------------------- /macros/derive-packethandler/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "derive-packethandler" 3 | version = "0.1.0" 4 | authors = ["Shady Khalifa "] 5 | edition.workspace = true 6 | 7 | [lib] 8 | proc-macro = true 9 | 10 | [dependencies] 11 | syn = { version = "2.0", features = ["full"] } 12 | quote = "1.0" 13 | proc-macro2 = "1.0" 14 | -------------------------------------------------------------------------------- /migrations/2_realms.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS realms ( 2 | realm_id INTEGER PRIMARY KEY, 3 | name TEXT UNIQUE NOT NULL CHECK (length(name) <= 16), 4 | game_ip_address TEXT NOT NULL CHECK (length(game_ip_address) <= 16), 5 | game_port INTEGER NOT NULL CHECK ( 6 | game_port >= 0 7 | AND game_port <= 65535 8 | ) 9 | ); 10 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | max_width = 120 2 | tab_spaces = 4 3 | fn_single_line = false 4 | match_block_trailing_comma = true 5 | normalize_comments = true 6 | wrap_comments = true 7 | imports_granularity = "Module" 8 | reorder_impl_items = true 9 | use_field_init_shorthand = true 10 | use_try_shorthand = true 11 | normalize_doc_attributes = true 12 | edition = "2021" 13 | -------------------------------------------------------------------------------- /server/game/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![allow(non_upper_case_globals)] 2 | 3 | pub mod constants; 4 | pub mod entities; 5 | pub mod systems; 6 | pub mod utils; 7 | pub mod world; 8 | 9 | #[cfg(test)] 10 | pub mod test_utils; 11 | 12 | pub mod state; 13 | pub use state::{ActorState, State}; 14 | 15 | pub mod error; 16 | pub use error::Error; 17 | 18 | pub mod packets; 19 | -------------------------------------------------------------------------------- /packets/connect-ex/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "msg-connect-ex" 3 | version = "0.1.0" 4 | edition.workspace = true 5 | 6 | [dependencies] 7 | tq-serde.workspace = true 8 | tq-network.workspace = true 9 | 10 | serde.workspace = true 11 | num_enum.workspace = true 12 | 13 | [features] 14 | default = ["std"] 15 | std = [ 16 | "tq-serde/std", 17 | "tq-network/std", 18 | ] 19 | -------------------------------------------------------------------------------- /crates/primitives/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "primitives" 3 | version = "0.1.0" 4 | authors = ["Shady Khalifa "] 5 | edition.workspace = true 6 | 7 | [dependencies] 8 | num-traits = { version = "0.2", default-features = false } 9 | bytemuck = { workspace = true, default-features = false } 10 | 11 | [features] 12 | default = ["std"] 13 | std = ["num-traits/std"] 14 | -------------------------------------------------------------------------------- /crates/tracing-wasm/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tracing-wasm" 3 | version = "0.1.0" 4 | edition.workspace = true 5 | 6 | [dependencies] 7 | tracing-core = { workspace = true, default-features = false } 8 | tracing-subscriber = { workspace = true, default-features = false, features = ["alloc"] } 9 | 10 | [features] 11 | default = [] 12 | std = ["tracing-subscriber/std", "tracing-subscriber/fmt"] 13 | -------------------------------------------------------------------------------- /migrations/4_maps.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS maps( 2 | id INTEGER PRIMARY KEY, 3 | map_id INTEGER NOT NULL, 4 | path TEXT NOT NULL, 5 | revive_point_x INTEGER NOT NULL, 6 | revive_point_y INTEGER NOT NULL, 7 | flags INTEGER NOT NULL DEFAULT 0, 8 | weather INTEGER NOT NULL DEFAULT 0, 9 | reborn_map INTEGER NOT NULL DEFAULT 0, 10 | color INTEGER NOT NULL DEFAULT 4294967295 11 | ); 12 | -------------------------------------------------------------------------------- /migrations/5_portals.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS portals ( 2 | id INTEGER PRIMARY KEY, 3 | from_map_id INTEGER NOT NULL CONSTRAINT fk_map_from REFERENCES maps(id) ON DELETE CASCADE, 4 | from_x INTEGER NOT NULL, 5 | from_y INTEGER NOT NULL, 6 | to_map_id INTEGER NOT NULL CONSTRAINT fk_map_to REFERENCES maps(id) ON DELETE CASCADE, 7 | to_x INTEGER NOT NULL, 8 | to_y INTEGER NOT NULL 9 | ); 10 | -------------------------------------------------------------------------------- /crates/crypto/src/nop.rs: -------------------------------------------------------------------------------- 1 | /// Nop Cipher, does almost no work, Could be useful for testing or for using 2 | /// internally between local servers to act as RPC. 3 | #[derive(Copy, Clone, Debug, Default)] 4 | pub struct NopCipher; 5 | 6 | impl crate::Cipher for NopCipher { 7 | fn generate_keys(&self, _seed: u64) {} 8 | 9 | fn decrypt(&self, _data: &mut [u8]) {} 10 | 11 | fn encrypt(&self, _data: &mut [u8]) {} 12 | } 13 | -------------------------------------------------------------------------------- /crates/wasm-builder/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tq-wasm-builder" 3 | version = "0.1.0" 4 | edition.workspace = true 5 | 6 | [dependencies] 7 | build-helper = "0.1" 8 | cargo_metadata = "0.18" 9 | console = "0.15" 10 | strum = { version = "0.25", features = ["derive"] } 11 | tempfile = "3.1" 12 | toml = "0.8" 13 | walkdir = "2.3" 14 | filetime = "0.2" 15 | wasm-opt = "0.116" 16 | externref = { workspace = true, features = ["processor"] } 17 | -------------------------------------------------------------------------------- /crates/db/src/error.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, thiserror::Error)] 2 | pub enum Error { 3 | #[cfg(feature = "sqlx")] 4 | #[error(transparent)] 5 | Db(#[from] sqlx::Error), 6 | #[error(transparent)] 7 | Bcrypt(#[from] bcrypt::BcryptError), 8 | #[error("Account not found")] 9 | AccountNotFound, 10 | #[error("Invalid password")] 11 | InvalidPassword, 12 | #[error("Creating account failed")] 13 | CreateAccountFailed, 14 | } 15 | -------------------------------------------------------------------------------- /crates/serde/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tq-serde" 3 | version = "0.1.0" 4 | authors = ["Shady Khalifa "] 5 | edition.workspace = true 6 | 7 | [dependencies] 8 | bytes.workspace = true 9 | serde.workspace = true 10 | tq-crypto.workspace = true 11 | 12 | [dev-dependencies] 13 | serde = { workspace = true, features = ["derive"] } 14 | 15 | [features] 16 | default = ["std"] 17 | std = ["bytes/std", "serde/std", "tq-crypto/std"] 18 | -------------------------------------------------------------------------------- /data/GameMaps/README.md: -------------------------------------------------------------------------------- 1 | To convert TQ's `.dmaps` into compressed maps `.cmap` for the server's use, place the 2 | contents of the client's map folder inside this folder. 3 | The contents of the folder should include the map and scene folders. 4 | 5 | Also don't forget to copy the `GameMap.dat` from `ini` folder here too, it will be used to construct the maps into the database. 6 | 7 | After being converted, the `.dmap` files and scene files from this folder, they can be 8 | removed. 9 | -------------------------------------------------------------------------------- /migrations/10_npcs.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS npcs ( 2 | id INTEGER PRIMARY KEY, 3 | name TEXT NOT NULL, 4 | kind INTEGER NOT NULL, 5 | look INTEGER NOT NULL, 6 | map_id INTEGER NOT NULL CONSTRAINT fk_npc_map REFERENCES maps(id) ON DELETE CASCADE, 7 | x INTEGER NOT NULL CHECK(x >= 0), 8 | y INTEGER NOT NULL CHECK(y >= 0), 9 | base INTEGER NOT NULL, 10 | sort INTEGER NOT NULL, 11 | level INTEGER NOT NULL, 12 | life INTEGER NOT NULL, 13 | defense INTEGER NOT NULL, 14 | magic_defense INTEGER NOT NULL 15 | ); 16 | -------------------------------------------------------------------------------- /packets/connect/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "msg-connect" 3 | version = "0.1.0" 4 | edition.workspace = true 5 | 6 | [dependencies] 7 | tq-serde.workspace = true 8 | tq-network.workspace = true 9 | tq-bindings.workspace = true 10 | 11 | serde.workspace = true 12 | bytes.workspace = true 13 | thiserror.workspace = true 14 | tracing.workspace = true 15 | 16 | [build-dependencies] 17 | tq-wasm-builder.workspace = true 18 | 19 | [features] 20 | default = ["std"] 21 | std = ["tq-serde/std", "tq-network/std", "tq-bindings/std"] 22 | 23 | -------------------------------------------------------------------------------- /server/game/src/entities/floor_item.rs: -------------------------------------------------------------------------------- 1 | pub trait FloorItem: Default { 2 | fn money(&self) -> u32; 3 | fn map_id(&self) -> u32; 4 | fn x(&self) -> u16; 5 | fn y(&self) -> u16; 6 | } 7 | 8 | #[derive(Debug, Clone, Default)] 9 | pub struct Item; 10 | 11 | impl FloorItem for Item { 12 | fn money(&self) -> u32 { 13 | 0 14 | } 15 | 16 | fn map_id(&self) -> u32 { 17 | 0 18 | } 19 | 20 | fn x(&self) -> u16 { 21 | 0 22 | } 23 | 24 | fn y(&self) -> u16 { 25 | 0 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /migrations/6_default_accounts.sql: -------------------------------------------------------------------------------- 1 | -- Add migration script here 2 | -- shekohex password is 123456 3 | INSERT INTO accounts (username, password) 4 | VALUES ( 5 | 'shekohex', 6 | -- username 7 | '$2b$12$yrHThFrB2K2fozb4cchAke6oov7HGnGVQe0W0TJ7mdyT5i4rsd9gG' 8 | ) ON CONFLICT(username) DO NOTHING; 9 | -- test1 password is 123456 10 | INSERT INTO accounts (username, password) 11 | VALUES ( 12 | 'test1', 13 | -- username 14 | '$2b$12$gdikPiJmesxrkUFyKQRh1ushZFv.urQhd1st8H9R5OxyQe4nzK5cq' 15 | ) ON CONFLICT(username) DO NOTHING; 16 | -------------------------------------------------------------------------------- /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [alias] 2 | auth = "run --bin auth-server --features=server" 3 | game = "run --bin game-server" 4 | hash-pwd = "run --bin hash-pwd" 5 | externref = "run --bin externref-cli" 6 | 7 | [target.aarch64-apple-darwin] 8 | linker = "clang" 9 | rustflags = ["-Zshare-generics=y", "--cfg", "tokio_unstable"] 10 | 11 | [target.x86_64-unknown-linux-gnu] 12 | linker = "clang" 13 | rustflags = ["-C", "link-arg=-fuse-ld=mold", "-Zshare-generics=y", "--cfg", "tokio_unstable"] 14 | 15 | [target.x86_64-pc-windows-msvc] 16 | linker = "rust-lld.exe" 17 | rustflags = ["-Zshare-generics=n", "--cfg", "tokio_unstable"] 18 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "rust-analyzer.lens.run.enable": true, 3 | "rust-analyzer.lens.references.adt.enable": true, 4 | "rust-analyzer.lens.references.method.enable": true, 5 | "rust-analyzer.lens.implementations.enable": true, 6 | "rust-analyzer.check.allTargets": true, 7 | "rust-analyzer.checkOnSave": true, 8 | "sqltools.useNodeRuntime": true, 9 | "sqltools.connections": [ 10 | { 11 | "previewLimit": 50, 12 | "driver": "SQLite", 13 | "name": "CoEmu", 14 | "group": "Conquer Server Emulators", 15 | "database": "${workspaceFolder:coemu}/data/coemu.db" 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /.github/workflows/audit.yml: -------------------------------------------------------------------------------- 1 | name: Security audit 2 | 3 | on: 4 | schedule: 5 | - cron: "0 0 * * *" 6 | push: 7 | branches: [master] 8 | paths: 9 | - "**/Cargo.toml" 10 | - "**/Cargo.lock" 11 | pull_request: 12 | 13 | jobs: 14 | audit: 15 | runs-on: ubuntu-latest 16 | concurrency: 17 | group: ${{ github.workflow }}-${{ github.ref }}-ubuntu-latest 18 | cancel-in-progress: true 19 | steps: 20 | - uses: actions/checkout@v4 21 | - uses: rustsec/audit-check@v1.4.1 22 | with: 23 | token: ${{ secrets.GITHUB_TOKEN }} 24 | ignore: RUSTSEC-2023-0071 25 | -------------------------------------------------------------------------------- /crates/codec/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tq-codec" 3 | version = "0.1.0" 4 | authors = ["Shady Khalifa "] 5 | edition.workspace = true 6 | 7 | [dependencies] 8 | bytes.workspace = true 9 | tq-crypto.workspace = true 10 | tracing = { workspace = true, features = ["attributes"] } 11 | tokio-stream = { workspace = true, features = ["io-util"] } 12 | tokio = { workspace = true, default-features = false, features = ["io-util"] } 13 | pretty-hex = { version = "0.4", default-features = false } 14 | 15 | [features] 16 | default = ["std"] 17 | std = ["bytes/std", "tq-crypto/std", "tracing/std", "pretty-hex/alloc"] 18 | -------------------------------------------------------------------------------- /crates/db/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tq-db" 3 | version = "0.1.0" 4 | authors = ["Shady Khalifa "] 5 | edition.workspace = true 6 | 7 | [dependencies] 8 | bcrypt.workspace = true 9 | thiserror.workspace = true 10 | tokio-stream.workspace = true 11 | tracing.workspace = true 12 | futures.workspace = true 13 | rkyv = { workspace = true, default-features = false, features = ["alloc", "size_32"] } 14 | 15 | # Database 16 | [dependencies.sqlx] 17 | workspace = true 18 | default-features = false 19 | optional = true 20 | features = ["sqlite", "macros"] 21 | 22 | [features] 23 | default = ["sqlx"] 24 | sqlx = ["dep:sqlx"] 25 | -------------------------------------------------------------------------------- /crates/serde/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! This Crate is used to create a Binary Serialization and Deserialization on 2 | //! top of [serde](https://serde.rs). 3 | //! It will be use to Serialize and Deserialize Conquer Online Binary Packets. 4 | 5 | #![cfg_attr(not(feature = "std"), no_std)] 6 | 7 | #[cfg(not(feature = "std"))] 8 | extern crate alloc; 9 | 10 | mod errors; 11 | pub use errors::TQSerdeError; 12 | 13 | mod fixed_string; 14 | pub use fixed_string::{String10, String16, TQMaskedPassword, TQPassword}; 15 | 16 | mod string_list; 17 | pub use string_list::StringList; 18 | 19 | mod ser; 20 | pub use ser::to_bytes; 21 | 22 | mod de; 23 | pub use de::from_bytes; 24 | -------------------------------------------------------------------------------- /crates/server/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tq-server" 3 | version = "0.1.0" 4 | authors = ["Shady Khalifa "] 5 | edition.workspace = true 6 | 7 | [dependencies] 8 | serde.workspace = true 9 | bytes.workspace = true 10 | tq-serde.workspace = true 11 | tq-codec.workspace = true 12 | tq-crypto.workspace = true 13 | tq-network.workspace = true 14 | async-trait.workspace = true 15 | tracing.workspace = true 16 | futures.workspace = true 17 | 18 | [dependencies.tokio-stream] 19 | workspace = true 20 | default-features = false 21 | features = ["io-util", "net"] 22 | 23 | [dependencies.tokio] 24 | workspace = true 25 | default-features = false 26 | features = ["io-util", "sync", "tracing"] 27 | -------------------------------------------------------------------------------- /packets/transfer/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "msg-transfer" 3 | version = "0.1.0" 4 | edition.workspace = true 5 | 6 | [dependencies] 7 | tq-serde.workspace = true 8 | tq-codec.workspace = true 9 | tq-crypto.workspace = true 10 | tq-network.workspace = true 11 | tq-bindings.workspace = true 12 | tq-db = { workspace = true, default-features = false } 13 | 14 | serde.workspace = true 15 | tracing.workspace = true 16 | bytes.workspace = true 17 | thiserror.workspace = true 18 | 19 | msg-connect-ex.workspace = true 20 | 21 | 22 | [build-dependencies] 23 | tq-wasm-builder.workspace = true 24 | 25 | [features] 26 | default = ["std"] 27 | std = [ 28 | "tq-serde/std", 29 | "tq-codec/std", 30 | "tq-crypto/std", 31 | "tq-network/std", 32 | "tq-bindings/std", 33 | 34 | "tracing/std", 35 | 36 | "msg-connect-ex/std", 37 | ] 38 | -------------------------------------------------------------------------------- /crates/bindings/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tq-bindings" 3 | version = "0.1.0" 4 | edition.workspace = true 5 | 6 | [dependencies] 7 | tq-network.workspace = true 8 | tracing-wasm.workspace = true 9 | tq-db = { workspace = true, default-features = false } 10 | rkyv = { workspace = true, default-features = false, features = ["alloc", "size_32"] } 11 | 12 | tracing = { workspace = true, default-features = false, optional = true } 13 | tracing-subscriber = { workspace = true, default-features = false, features = ["alloc"], optional = true } 14 | externref = { workspace = true, default-features = false, features = ["macro"] } 15 | getrandom = { workspace = true, default-features = false, features = ["custom"] } 16 | 17 | 18 | [features] 19 | default = ["std"] 20 | std = ["tq-network/std", "tracing-wasm/std", "dep:tracing", "dep:tracing-subscriber"] 21 | -------------------------------------------------------------------------------- /packets/account/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "msg-account" 3 | version = "0.1.0" 4 | edition.workspace = true 5 | 6 | [dependencies] 7 | tq-serde.workspace = true 8 | tq-codec.workspace = true 9 | tq-crypto.workspace = true 10 | tq-network.workspace = true 11 | tq-bindings.workspace = true 12 | tq-db = { workspace = true, default-features = false } 13 | 14 | 15 | serde.workspace = true 16 | bytes.workspace = true 17 | tracing.workspace = true 18 | thiserror.workspace = true 19 | 20 | 21 | msg-connect-ex.workspace = true 22 | msg-transfer.workspace = true 23 | 24 | [build-dependencies] 25 | tq-wasm-builder.workspace = true 26 | 27 | [features] 28 | default = ["std"] 29 | std = [ 30 | "tq-serde/std", 31 | "tq-codec/std", 32 | "tq-crypto/std", 33 | "tq-network/std", 34 | "tq-bindings/std", 35 | 36 | "tracing/std", 37 | 38 | "msg-connect-ex/std", 39 | "msg-transfer/std", 40 | ] 41 | -------------------------------------------------------------------------------- /tools/benchbot/src/error.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, thiserror::Error)] 2 | pub enum Error { 3 | #[error(transparent)] 4 | Auth(#[from] auth::Error), 5 | #[error(transparent)] 6 | Network(#[from] tq_network::Error), 7 | #[error(transparent)] 8 | IO(#[from] std::io::Error), 9 | #[error(transparent)] 10 | DotEnv(#[from] dotenvy::Error), 11 | #[error(transparent)] 12 | Env(#[from] std::env::VarError), 13 | #[error(transparent)] 14 | Sqlx(#[from] sqlx::Error), 15 | #[error(transparent)] 16 | Db(#[from] tq_db::Error), 17 | #[error("Realm not found")] 18 | RealmNotFound, 19 | #[error("Server timed out")] 20 | ServerTimedOut, 21 | #[error("Invalid password")] 22 | InvalidPassword, 23 | #[error("Character name already taken")] 24 | CharacterNameAlreadyTaken, 25 | #[error("Token not found")] 26 | AccountTokenNotFound, 27 | } 28 | -------------------------------------------------------------------------------- /crates/db/src/realm.rs: -------------------------------------------------------------------------------- 1 | /// Realms are configured instances of the game server. This struct defines 2 | /// routing details for authenticated clients to be redirected to. Redirection 3 | /// involves access token leasing, provided by the game server via RPC. 4 | #[derive(Clone, Debug, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)] 5 | #[cfg_attr(feature = "sqlx", derive(sqlx::FromRow))] 6 | pub struct Realm { 7 | pub realm_id: i32, 8 | pub name: String, 9 | pub game_ip_address: String, 10 | pub game_port: i16, 11 | } 12 | 13 | #[cfg(feature = "sqlx")] 14 | impl Realm { 15 | pub async fn by_name(pool: &sqlx::SqlitePool, name: &str) -> Result, crate::Error> { 16 | let realm = sqlx::query_as::<_, Self>("SELECT * FROM realms WHERE name = ?;") 17 | .bind(name) 18 | .fetch_optional(pool) 19 | .await?; 20 | Ok(realm) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tools/externref/src/main.rs: -------------------------------------------------------------------------------- 1 | use externref::processor::Processor; 2 | 3 | fn main() { 4 | let args = std::env::args().collect::>(); 5 | let mut args = args.iter().skip(1); 6 | let input = args.next().unwrap_or_else(print_usage); 7 | let output = args.next().unwrap_or_else(print_usage); 8 | let module = std::fs::read(input).unwrap_or_else(|err| { 9 | eprintln!("Failed to read input file: {}", err); 10 | std::process::exit(1); 11 | }); 12 | let processed: Vec = Processor::default().process_bytes(&module).unwrap(); 13 | std::fs::write(output, processed).unwrap_or_else(|err| { 14 | eprintln!("Failed to write output file: {}", err); 15 | std::process::exit(1); 16 | }); 17 | } 18 | 19 | fn print_usage<'a>() -> &'a String { 20 | eprintln!("Usage: {} ", std::env::args().next().unwrap()); 21 | std::process::exit(1); 22 | } 23 | -------------------------------------------------------------------------------- /server/auth/src/state.rs: -------------------------------------------------------------------------------- 1 | use crate::error::Error; 2 | use sqlx::sqlite::{SqlitePool, SqlitePoolOptions}; 3 | 4 | #[derive(Debug, Clone)] 5 | pub struct State { 6 | pool: SqlitePool, 7 | } 8 | 9 | impl State { 10 | /// Init The State. 11 | /// Should only get called once. 12 | pub async fn init() -> Result { 13 | let data_dir = dotenvy::var("DATA_LOCATION")?; 14 | let default_db_location = format!("sqlite://{data_dir}/coemu.db?mode=rwc"); 15 | let db_url = dotenvy::var("DATABASE_URL").unwrap_or(default_db_location); 16 | let pool = SqlitePoolOptions::new() 17 | .max_connections(42) 18 | .min_connections(4) 19 | .connect(&db_url) 20 | .await?; 21 | let state = Self { pool }; 22 | Ok(state) 23 | } 24 | 25 | /// Get access to the database pool 26 | pub fn pool(&self) -> &SqlitePool { 27 | &self.pool 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /crates/network/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tq-network" 3 | version = "0.1.0" 4 | authors = ["Shady Khalifa "] 5 | edition.workspace = true 6 | 7 | [dependencies] 8 | serde.workspace = true 9 | bytes.workspace = true 10 | tq-serde.workspace = true 11 | tq-codec.workspace = true 12 | tq-crypto.workspace = true 13 | async-trait.workspace = true 14 | tracing.workspace = true 15 | futures.workspace = true 16 | 17 | # macros 18 | derive-packetid.workspace = true 19 | derive-packethandler.workspace = true 20 | derive-packetprocessor.workspace = true 21 | 22 | [dependencies.tokio-stream] 23 | workspace = true 24 | default-features = false 25 | optional = true 26 | features = ["io-util", "net"] 27 | 28 | [dependencies.tokio] 29 | workspace = true 30 | default-features = false 31 | features = ["io-util", "sync"] 32 | 33 | [features] 34 | default = [] 35 | std = [ 36 | "tq-serde/std", 37 | "tq-codec/std", 38 | "tq-crypto/std", 39 | "tracing/std", 40 | "futures/std" 41 | ] 42 | -------------------------------------------------------------------------------- /crates/network/src/error.rs: -------------------------------------------------------------------------------- 1 | #[cfg(not(feature = "std"))] 2 | use alloc::string::String; 3 | 4 | #[derive(Debug, Clone)] 5 | pub enum Error { 6 | TQSerde(tq_serde::TQSerdeError), 7 | SendError, 8 | Other(String), 9 | } 10 | 11 | impl core::fmt::Display for Error { 12 | fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 13 | match self { 14 | Self::TQSerde(e) => write!(f, "TQSerde Error: {}", e), 15 | Self::SendError => write!(f, "Send Error"), 16 | Self::Other(e) => write!(f, "{}", e), 17 | } 18 | } 19 | } 20 | 21 | impl From for Error { 22 | fn from(e: tq_serde::TQSerdeError) -> Self { 23 | Self::TQSerde(e) 24 | } 25 | } 26 | 27 | impl From> for Error { 28 | fn from(_: tokio::sync::mpsc::error::SendError) -> Self { 29 | Self::SendError 30 | } 31 | } 32 | 33 | #[cfg(feature = "std")] 34 | impl std::error::Error for Error {} 35 | -------------------------------------------------------------------------------- /tools/benchbot/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "benchbot" 3 | version = "0.1.0" 4 | edition.workspace = true 5 | 6 | 7 | [dependencies] 8 | dotenvy.workspace = true 9 | thiserror.workspace = true 10 | tracing.workspace = true 11 | futures.workspace = true 12 | tokio-stream.workspace = true 13 | parking_lot.workspace = true 14 | rand.workspace = true 15 | 16 | tq-codec.workspace = true 17 | tq-crypto.workspace = true 18 | tq-network.workspace = true 19 | tq-db.workspace = true 20 | game.workspace = true 21 | auth.workspace = true 22 | 23 | pretty-hex = "0.4" 24 | local-ip-address = "0.5" 25 | 26 | 27 | [dependencies.tracing-subscriber] 28 | version = "0.3" 29 | default-features = false 30 | features = ["env-filter", "ansi", "fmt", "smallvec"] 31 | 32 | # Runtime 33 | [dependencies.tokio] 34 | workspace = true 35 | default-features = false 36 | features = ["rt-multi-thread", "macros", "signal"] 37 | 38 | # Database 39 | [dependencies.sqlx] 40 | workspace = true 41 | default-features = false 42 | features = ["runtime-tokio-rustls", "sqlite", "macros"] 43 | -------------------------------------------------------------------------------- /server/game/src/packets/msg_item_info.rs: -------------------------------------------------------------------------------- 1 | use num_enum::FromPrimitive; 2 | use serde::Serialize; 3 | use tq_network::PacketID; 4 | 5 | #[derive(Debug, FromPrimitive)] 6 | #[repr(u8)] 7 | pub enum ItemInfoAction { 8 | #[num_enum(default)] 9 | None = 0, 10 | AddItem = 1, 11 | Trade = 2, 12 | Update = 3, 13 | OtherPlayerEquipement = 4, 14 | } 15 | 16 | /// This packet is sent server>client to add or update the attributes of a 17 | /// specific item. 18 | #[derive(Debug, Serialize, Clone, PacketID, Default)] 19 | #[packet(id = 1008)] 20 | pub struct MsgItemInfo { 21 | character_id: u32, 22 | item_id: u32, 23 | durability: u16, 24 | max_durability: u16, 25 | action: u8, 26 | ident: u8, // always 0 27 | position: u8, 28 | /// Unknown 29 | reserved0: u8, 30 | reserved1: u32, 31 | gem_one: u8, 32 | gem_two: u8, 33 | reborn_effect: u8, 34 | magic: u8, 35 | plus: u8, 36 | blees: u8, 37 | enchant: u8, 38 | reserved2: u8, 39 | restrain: u32, 40 | reserved3: u32, 41 | reserved4: u32, 42 | } 43 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "lldb", 9 | "request": "launch", 10 | "name": "Debug executable 'auth-server'", 11 | "cargo": { 12 | "args": ["build", "--bin=auth-server", "--package=auth"], 13 | "filter": { 14 | "name": "auth-server", 15 | "kind": "bin" 16 | } 17 | }, 18 | "args": [], 19 | "cwd": "${workspaceFolder}" 20 | }, 21 | { 22 | "type": "lldb", 23 | "request": "launch", 24 | "name": "Debug executable 'game-server'", 25 | "cargo": { 26 | "args": ["build", "--bin=game-server", "--package=game"], 27 | "filter": { 28 | "name": "game-server", 29 | "kind": "bin" 30 | } 31 | }, 32 | "args": [], 33 | "cwd": "${workspaceFolder}" 34 | } 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /server/game/src/packets/mod.rs: -------------------------------------------------------------------------------- 1 | mod msg_connect; 2 | pub use msg_connect::MsgConnect; 3 | 4 | mod msg_talk; 5 | pub use msg_talk::{MsgTalk, TalkChannel, TalkStyle}; 6 | 7 | mod msg_user_info; 8 | pub use msg_user_info::MsgUserInfo; 9 | 10 | mod msg_action; 11 | pub use msg_action::{ActionType, MsgAction}; 12 | 13 | mod msg_item; 14 | pub use msg_item::MsgItem; 15 | 16 | mod msg_transfer; 17 | pub use msg_transfer::MsgTransfer; 18 | 19 | mod msg_register; 20 | pub use msg_register::{BaseClass, BodyType, MsgRegister}; 21 | 22 | mod msg_walk; 23 | pub use msg_walk::{MovementType, MsgWalk}; 24 | 25 | mod msg_player; 26 | pub use msg_player::MsgPlayer; 27 | 28 | mod msg_item_info; 29 | pub use msg_item_info::*; 30 | 31 | mod msg_data; 32 | pub use msg_data::MsgData; 33 | 34 | mod msg_weather; 35 | pub use msg_weather::{MsgWeather, WeatherKind}; 36 | 37 | mod msg_map_info; 38 | pub use msg_map_info::{MapFlags, MsgMapInfo}; 39 | 40 | mod msg_npc_info; 41 | pub use msg_npc_info::MsgNpcInfo; 42 | 43 | mod msg_npc; 44 | pub use msg_npc::MsgNpc; 45 | 46 | mod msg_task_dialog; 47 | pub use msg_task_dialog::MsgTaskDialog; 48 | -------------------------------------------------------------------------------- /packets/connect/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(not(feature = "std"), no_std)] 2 | 3 | #[cfg(not(feature = "std"))] 4 | extern crate alloc; 5 | 6 | include!(concat!(env!("OUT_DIR"), "/wasm.rs")); 7 | 8 | use tq_bindings::{host, Resource}; 9 | use tq_network::ActorHandle; 10 | use tq_serde::String16; 11 | 12 | /// Message containing a connection request to the game server. Contains the 13 | /// player's access token from the Account server, and the patch and language 14 | /// versions of the game client. 15 | #[derive(Debug, serde::Serialize, serde::Deserialize, tq_network::PacketID)] 16 | #[packet(id = 1052)] 17 | pub struct MsgConnect { 18 | pub id: u32, 19 | pub file_contents: u32, 20 | pub file_name: String16, 21 | } 22 | 23 | #[derive(Debug, thiserror::Error)] 24 | pub enum Error { 25 | #[error(transparent)] 26 | Network(#[from] tq_network::Error), 27 | } 28 | 29 | #[tq_network::packet_processor(MsgConnect)] 30 | pub fn process(msg: MsgConnect, actor: &Resource) -> Result<(), crate::Error> { 31 | tracing::debug!(?msg, "Shutting down actor!"); 32 | host::network::actor::shutdown(actor); 33 | Ok(()) 34 | } 35 | -------------------------------------------------------------------------------- /crates/server/src/error.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug)] 2 | pub enum Error { 3 | TQNetwork(tq_network::Error), 4 | AddrParseError(std::net::AddrParseError), 5 | IO(std::io::Error), 6 | Internal(Box), 7 | } 8 | 9 | impl core::fmt::Display for Error { 10 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 11 | match self { 12 | Self::TQNetwork(e) => write!(f, "TQNetwork Error: {}", e), 13 | Self::AddrParseError(e) => write!(f, "AddrParse Error: {}", e), 14 | Self::IO(e) => write!(f, "IO Error: {}", e), 15 | Self::Internal(s) => write!(f, "Internal Error: {}", s), 16 | } 17 | } 18 | } 19 | 20 | impl From for Error { 21 | fn from(e: tq_network::Error) -> Self { 22 | Self::TQNetwork(e) 23 | } 24 | } 25 | 26 | impl From for Error { 27 | fn from(e: std::net::AddrParseError) -> Self { 28 | Self::AddrParseError(e) 29 | } 30 | } 31 | 32 | impl From for Error { 33 | fn from(e: std::io::Error) -> Self { 34 | Self::IO(e) 35 | } 36 | } 37 | 38 | impl std::error::Error for Error {} 39 | -------------------------------------------------------------------------------- /server/game/src/packets/msg_transfer.rs: -------------------------------------------------------------------------------- 1 | use crate::{ActorState, Error, State}; 2 | use async_trait::async_trait; 3 | use serde::{Deserialize, Serialize}; 4 | use tq_network::{Actor, PacketID, PacketProcess}; 5 | 6 | /// Defines account parameters to be transferred from the account server to the 7 | /// game server. Account information is supplied from the account database, and 8 | /// used on the game server to transfer authentication and authority level. 9 | #[derive(Clone, Debug, Deserialize, Serialize, PacketID)] 10 | #[packet(id = 4001)] 11 | pub struct MsgTransfer { 12 | account_id: u32, 13 | realm_id: u32, 14 | #[serde(skip_deserializing)] 15 | token: u64, 16 | } 17 | 18 | #[async_trait] 19 | impl PacketProcess for MsgTransfer { 20 | type ActorState = ActorState; 21 | type Error = Error; 22 | type State = State; 23 | 24 | async fn process(&self, state: &Self::State, actor: &Actor) -> Result<(), Self::Error> { 25 | let generated = state.generate_login_token(self.account_id, self.realm_id)?; 26 | let mut msg = self.clone(); 27 | msg.token = generated.token; 28 | actor.send(msg).await?; 29 | actor.shutdown().await?; 30 | Ok(()) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /crates/db/src/portal.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, Clone, Default)] 2 | #[cfg_attr(feature = "sqlx", derive(sqlx::FromRow))] 3 | pub struct Portal { 4 | pub id: i32, 5 | pub from_map_id: i32, 6 | pub from_x: i16, 7 | pub from_y: i16, 8 | pub to_map_id: i32, 9 | pub to_x: i16, 10 | pub to_y: i16, 11 | } 12 | 13 | #[cfg(feature = "sqlx")] 14 | impl Portal { 15 | #[tracing::instrument] 16 | pub async fn by_map(pool: &sqlx::SqlitePool, from: i32) -> Result, crate::Error> { 17 | use tokio_stream::StreamExt; 18 | 19 | let mut portals = Vec::new(); 20 | let mut s = sqlx::query_as::<_, Self>("SELECT * FROM portals WHERE from_map_id = ?;") 21 | .bind(from) 22 | .fetch(pool); 23 | while let Some(maybe_portal) = s.next().await { 24 | match maybe_portal { 25 | Ok(portal) => portals.push(portal), 26 | Err(error) => { 27 | tracing::error!( 28 | %error, 29 | from_map_id = %from, 30 | "Error while loading a portal" 31 | ); 32 | }, 33 | } 34 | } 35 | Ok(portals) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /server/game/src/packets/msg_npc_info.rs: -------------------------------------------------------------------------------- 1 | use crate::entities::Npc; 2 | use serde::Serialize; 3 | use tq_network::PacketID; 4 | 5 | /// This packet is used to spawn NPCs to players. 6 | #[derive(Debug, Serialize, Clone, PacketID, Default)] 7 | #[packet(id = 2030)] 8 | pub struct MsgNpcInfo { 9 | /// UniqueID 10 | id: u32, 11 | x: u16, 12 | y: u16, 13 | look: u16, 14 | kind: u16, 15 | sort: u16, 16 | /// * 0 if not sending any name 17 | /// * 1 if sending name 18 | list_count: u8, 19 | /// The name of the NPC 20 | name: Option, 21 | } 22 | 23 | impl MsgNpcInfo { 24 | pub fn new(npc: &Npc) -> Self { 25 | let loc = npc.entity().location(); 26 | Self { 27 | id: npc.id(), 28 | x: loc.x, 29 | y: loc.y, 30 | look: npc.entity().mesh() as u16, 31 | kind: npc.kind() as u16, 32 | sort: npc.sort() as u16, 33 | list_count: 0, 34 | name: None, 35 | } 36 | } 37 | 38 | pub fn from_npc_with_name(npc: &Npc) -> Self { 39 | let mut this = Self::new(npc); 40 | this.list_count = 1; 41 | this.name = Some(npc.entity().name().to_string()); 42 | this 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /tools/benchbot/src/state.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::sync::Arc; 3 | 4 | use crate::Error; 5 | use parking_lot::RwLock; 6 | use sqlx::sqlite::{SqlitePool, SqlitePoolOptions}; 7 | 8 | type Shared = Arc>; 9 | 10 | #[derive(Debug, Clone)] 11 | pub struct State { 12 | pool: SqlitePool, 13 | /// Maps Account ID to Token value 14 | tokens: Shared>, 15 | } 16 | 17 | impl State { 18 | /// Init The State. 19 | /// Should only get called once. 20 | pub async fn init() -> Result { 21 | let data_dir = dotenvy::var("DATA_LOCATION")?; 22 | let default_db_location = 23 | format!("sqlite://{data_dir}/coemu.db?mode=rwc"); 24 | let db_url = 25 | dotenvy::var("DATABASE_URL").unwrap_or(default_db_location); 26 | let pool = SqlitePoolOptions::new() 27 | .max_connections(42) 28 | .min_connections(4) 29 | .connect(&db_url) 30 | .await?; 31 | let state = Self { 32 | pool, 33 | tokens: Default::default(), 34 | }; 35 | Ok(state) 36 | } 37 | 38 | /// Get access to the database pool 39 | pub fn pool(&self) -> &SqlitePool { &self.pool } 40 | 41 | pub fn tokens(&self) -> &Shared> { &self.tokens } 42 | } 43 | -------------------------------------------------------------------------------- /crates/db/src/npc.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, Clone, Default)] 2 | #[cfg_attr(feature = "sqlx", derive(sqlx::FromRow))] 3 | pub struct Npc { 4 | pub id: i32, 5 | pub name: String, 6 | pub kind: i8, 7 | pub look: i32, 8 | pub map_id: i32, 9 | pub x: i16, 10 | pub y: i16, 11 | pub base: i8, 12 | pub sort: i8, 13 | pub level: i32, 14 | pub life: i32, 15 | pub defense: i32, 16 | pub magic_defense: i32, 17 | } 18 | 19 | #[cfg(feature = "sqlx")] 20 | impl Npc { 21 | #[tracing::instrument] 22 | pub async fn by_map(pool: &sqlx::SqlitePool, id: i32) -> Result, crate::Error> { 23 | use tokio_stream::StreamExt; 24 | 25 | let mut npcs = Vec::new(); 26 | let mut s = sqlx::query_as::<_, Self>("SELECT * FROM npcs WHERE map_id = ?;") 27 | .bind(id) 28 | .fetch(pool); 29 | while let Some(maybe_npc) = s.next().await { 30 | match maybe_npc { 31 | Ok(npc) => npcs.push(npc), 32 | Err(error) => { 33 | tracing::error!( 34 | %error, 35 | map_id = %id, 36 | "Error while loading a npc from the database" 37 | ); 38 | }, 39 | } 40 | } 41 | Ok(npcs) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /crates/crypto/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! This crate contains cipher algorithms used between the Conquer 2 | //! Online game client and server, it Defines generalized methods for ciphers 3 | //! used by `Server` for encrypting and 4 | //! decrypting data to and from the game client. 5 | 6 | #![cfg_attr(not(feature = "std"), no_std)] 7 | 8 | #[cfg(not(feature = "std"))] 9 | extern crate alloc; 10 | 11 | mod rc5; 12 | pub use rc5::TQRC5; 13 | 14 | mod tq_cipher; 15 | pub use tq_cipher::TQCipher; 16 | 17 | mod nop; 18 | pub use nop::NopCipher; 19 | 20 | mod cq_cipher; 21 | pub use cq_cipher::CQCipher; 22 | 23 | /// Defines generalized methods for ciphers used by 24 | /// `Server` for encrypting and decrypting 25 | /// data to and from the game client. 26 | /// Can be used to switch between ciphers easily for 27 | /// seperate states of the game client connection. 28 | pub trait Cipher: Clone + Default + Send + Sync + Unpin + 'static { 29 | /// Generates keys using key derivation variables. 30 | fn generate_keys(&self, seed: u64); 31 | /// Decrypts data from the client. 32 | /// 33 | /// * `data` - data that requires decrypting. 34 | fn decrypt(&self, data: &mut [u8]); 35 | 36 | /// Encrypts data to send to the client. 37 | /// 38 | /// * `data` - data that requires encrypting. 39 | fn encrypt(&self, data: &mut [u8]); 40 | } 41 | -------------------------------------------------------------------------------- /tools/txt-to-csv.py: -------------------------------------------------------------------------------- 1 | # Converts the Input text file to a CSV file 2 | # Input: Text file 3 | # Output: CSV file 4 | 5 | # Example Input: 6 | # 601 OfflineTG 601 0074 0 65 55 601 4294967295 7 | # Output: 8 | # 601,OfflineTG,601,0074,0,65,55,601,4294967295 9 | 10 | import argparse 11 | import csv 12 | 13 | parser = argparse.ArgumentParser( 14 | prog="TextToCSV", 15 | description="Converts the Input text file to a CSV file", 16 | epilog="Example: python txt-to-csv.py --input input.txt --output output.csv", 17 | ) 18 | parser.add_argument("-i", "--input", help="Input text file", type=str, required=True) 19 | parser.add_argument("-o", "--output", help="Output CSV file", type=str, required=True) 20 | args = parser.parse_args() 21 | 22 | input_file = args.input 23 | output_file = args.output 24 | 25 | 26 | # Open input text file for reading 27 | with open(input_file, "r") as f: 28 | lines = f.readlines() 29 | # Filter out empty lines 30 | lines = list(filter(lambda x: x.strip(), lines)) 31 | 32 | # Open output CSV file for writing 33 | with open(output_file, "w") as csvfile: 34 | 35 | # Create CSV writer 36 | writer = csv.writer(csvfile, dialect=csv.unix_dialect, quoting=csv.QUOTE_NONE) 37 | 38 | # Loop through lines and write to CSV 39 | for line in lines: 40 | row = line.strip().split(" ") 41 | if len(row) > 0: 42 | writer.writerow(row) 43 | -------------------------------------------------------------------------------- /.github/workflows/lints.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: [master] 4 | paths-ignore: 5 | - README.md 6 | - CHANGELOG.md 7 | - .gitignore 8 | - .github/** 9 | pull_request: 10 | branches: [master] 11 | types: [opened, synchronize, closed] 12 | workflow_dispatch: 13 | 14 | name: Nightly lints 15 | 16 | jobs: 17 | combo: 18 | name: Clippy + rustfmt 19 | runs-on: ubuntu-latest 20 | concurrency: 21 | group: ${{ github.workflow }}-${{ github.ref }}-ubuntu-latest 22 | cancel-in-progress: true 23 | steps: 24 | - name: Checkout sources 25 | uses: actions/checkout@v4 26 | 27 | - name: Install nightly toolchain 28 | uses: actions-rs/toolchain@v1 29 | with: 30 | profile: minimal 31 | toolchain: nightly 32 | override: true 33 | components: rustfmt, clippy 34 | 35 | - name: Rust Cache 36 | uses: Swatinem/rust-cache@v2 37 | with: 38 | shared-key: "rust" 39 | 40 | - name: Setup mold Linker 41 | uses: rui314/setup-mold@v1 42 | 43 | - name: Run cargo fmt 44 | uses: actions-rs/cargo@v1 45 | with: 46 | command: fmt 47 | args: --all -- --check 48 | 49 | - name: Run cargo clippy 50 | uses: actions-rs/cargo@v1 51 | env: 52 | SQLX_OFFLINE: true 53 | with: 54 | command: clippy 55 | args: -- -D warnings 56 | -------------------------------------------------------------------------------- /crates/db/src/map.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, Clone, Default)] 2 | #[cfg_attr(feature = "sqlx", derive(sqlx::FromRow))] 3 | pub struct Map { 4 | pub id: i32, 5 | pub map_id: i32, 6 | pub path: String, 7 | pub revive_point_x: i32, 8 | pub revive_point_y: i32, 9 | pub flags: i32, 10 | pub weather: i8, 11 | pub reborn_map: i32, 12 | pub color: i32, 13 | } 14 | 15 | #[cfg(feature = "sqlx")] 16 | impl Map { 17 | /// Loads all maps from the database to add them to the state. 18 | #[tracing::instrument] 19 | pub async fn load_all(pool: &sqlx::SqlitePool) -> Result, crate::Error> { 20 | use tokio_stream::StreamExt; 21 | 22 | let mut maps = Vec::new(); 23 | let mut s = sqlx::query_as::<_, Self>("SELECT * FROM maps;").fetch(pool); 24 | while let Some(maybe_map) = s.next().await { 25 | match maybe_map { 26 | Ok(map) => maps.push(map), 27 | Err(error) => { 28 | tracing::error!( 29 | %error, 30 | "Error while loading a map" 31 | ); 32 | }, 33 | } 34 | } 35 | Ok(maps) 36 | } 37 | 38 | pub async fn load(pool: &sqlx::SqlitePool, id: i32) -> Result, crate::Error> { 39 | let maybe_map = sqlx::query_as::<_, Self>("SELECT * FROM maps WHERE id = ?;") 40 | .bind(id) 41 | .fetch_optional(pool) 42 | .await?; 43 | Ok(maybe_map) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /server/game/src/utils.rs: -------------------------------------------------------------------------------- 1 | pub fn current_ts() -> u32 { 2 | let start = std::time::SystemTime::now(); 3 | let since_the_epoch = start 4 | .duration_since(std::time::UNIX_EPOCH) 5 | .expect("Time went backwards"); 6 | since_the_epoch.as_secs() as u32 7 | } 8 | 9 | pub trait LoHi { 10 | type Output; 11 | 12 | fn lo(&self) -> Self::Output; 13 | fn hi(&self) -> Self::Output; 14 | fn constract(hi: Self::Output, lo: Self::Output) -> Self; 15 | } 16 | 17 | impl LoHi for u16 { 18 | type Output = u8; 19 | 20 | fn lo(&self) -> Self::Output { 21 | *self as u8 22 | } 23 | 24 | fn hi(&self) -> Self::Output { 25 | (*self >> 8) as u8 26 | } 27 | 28 | fn constract(hi: Self::Output, lo: Self::Output) -> Self { 29 | lo as u16 | (hi as u16) << 8 30 | } 31 | } 32 | 33 | impl LoHi for u32 { 34 | type Output = u16; 35 | 36 | fn lo(&self) -> Self::Output { 37 | *self as u16 38 | } 39 | 40 | fn hi(&self) -> Self::Output { 41 | (*self >> 16) as u16 42 | } 43 | 44 | fn constract(hi: Self::Output, lo: Self::Output) -> Self { 45 | lo as u32 | (hi as u32) << 16 46 | } 47 | } 48 | 49 | impl LoHi for u64 { 50 | type Output = u32; 51 | 52 | fn lo(&self) -> Self::Output { 53 | *self as u32 54 | } 55 | 56 | fn hi(&self) -> Self::Output { 57 | (*self >> 32) as u32 58 | } 59 | 60 | fn constract(hi: Self::Output, lo: Self::Output) -> Self { 61 | lo as u64 | (hi as u64) << 32 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /crates/serde/src/errors.rs: -------------------------------------------------------------------------------- 1 | //! Handle Errors. 2 | use core::fmt::Display; 3 | use serde::{de, ser}; 4 | 5 | #[cfg(not(feature = "std"))] 6 | use alloc::string::{String, ToString}; 7 | 8 | /// Represents Any Errors that could happens while Serializing/Deserializing 9 | /// Binary Packets. 10 | #[derive(Clone, Debug)] 11 | pub enum TQSerdeError { 12 | Message(String), 13 | Utf8Error(core::str::Utf8Error), 14 | InvalidBool, 15 | Eof, 16 | DeserializeAnyNotSupported, 17 | Unspported, 18 | } 19 | 20 | impl Display for TQSerdeError { 21 | fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 22 | match self { 23 | TQSerdeError::Message(msg) => write!(f, "{}", msg), 24 | TQSerdeError::Utf8Error(err) => write!(f, "{}", err), 25 | TQSerdeError::InvalidBool => write!(f, "Invalid Boolean Value"), 26 | TQSerdeError::Eof => write!(f, "EOF"), 27 | TQSerdeError::DeserializeAnyNotSupported => { 28 | write!(f, "Deserializing Any Not Supported") 29 | }, 30 | TQSerdeError::Unspported => write!(f, "Unspported Type"), 31 | } 32 | } 33 | } 34 | 35 | #[cfg(feature = "std")] 36 | impl std::error::Error for TQSerdeError {} 37 | 38 | impl ser::Error for TQSerdeError { 39 | fn custom(msg: T) -> Self { 40 | TQSerdeError::Message(msg.to_string()) 41 | } 42 | } 43 | 44 | impl de::Error for TQSerdeError { 45 | fn custom(msg: T) -> Self { 46 | TQSerdeError::Message(msg.to_string()) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /migrations/1_characters.sql: -------------------------------------------------------------------------------- 1 | -- Add migration script here 2 | CREATE TABLE IF NOT EXISTS characters ( 3 | character_id INTEGER PRIMARY KEY, 4 | account_id INTEGER NOT NULL CONSTRAINT fk_account REFERENCES accounts(account_id) ON DELETE CASCADE, 5 | realm_id INTEGER NOT NULL CONSTRAINT fk_realm REFERENCES realms(realm_id), 6 | name TEXT UNIQUE NOT NULL CHECK (length(name) <= 32), 7 | mesh INTEGER NOT NULL DEFAULT 1003, 8 | avatar INTEGER NOT NULL DEFAULT 1, 9 | hair_style INTEGER NOT NULL DEFAULT 535, 10 | silver INTEGER NOT NULL DEFAULT 1000 CHECK (silver >= 0), 11 | cps INTEGER NOT NULL DEFAULT 0 CHECK (cps >= 0), 12 | current_class INTEGER NOT NULL DEFAULT 10 CHECK (current_class > 0), 13 | previous_class INTEGER NOT NULL DEFAULT 0 CHECK (previous_class >= 0), 14 | rebirths INTEGER NOT NULL DEFAULT 0 CHECK (rebirths >= 0), 15 | level INTEGER NOT NULL DEFAULT 1 CHECK (level > 0), 16 | experience INTEGER NOT NULL DEFAULT 0 CHECK (experience >= 0), 17 | map_id INTEGER NOT NULL DEFAULT 1002, 18 | x INTEGER NOT NULL DEFAULT 430, 19 | y INTEGER NOT NULL DEFAULT 380, 20 | virtue INTEGER NOT NULL DEFAULT 0, 21 | strength INTEGER NOT NULL DEFAULT 4, 22 | agility INTEGER NOT NULL DEFAULT 6, 23 | vitality INTEGER NOT NULL DEFAULT 12, 24 | spirit INTEGER NOT NULL DEFAULT 0, 25 | attribute_points INTEGER NOT NULL DEFAULT 0 CHECK (attribute_points >= 0), 26 | health_points INTEGER NOT NULL DEFAULT 12 CHECK (health_points >= 0), 27 | mana_points INTEGER NOT NULL DEFAULT 0 CHECK (mana_points >= 0), 28 | kill_points INTEGER NOT NULL DEFAULT 0 CHECK (kill_points >= 0) 29 | ); 30 | -------------------------------------------------------------------------------- /server/game/src/constants.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused)] 2 | 3 | pub const SYSTEM: &str = "SYSTEM"; 4 | pub const ALL_USERS: &str = "ALLUSERS"; 5 | pub const ANSWER_OK: &str = "ANSWER_OK"; 6 | pub const NEW_ROLE: &str = "NEW_ROLE"; 7 | 8 | pub const MAX_TXT_LEN: usize = 250; 9 | 10 | pub const HAIR_STYLES: [i16; 12] = [10, 11, 13, 14, 15, 24, 30, 35, 37, 38, 39, 40]; 11 | 12 | pub const WALK_XCOORDS: [i8; 8] = [0, -1, -1, -1, 0, 1, 1, 1]; 13 | pub const WALK_YCOORDS: [i8; 8] = [1, 1, 0, -1, -1, -1, 0, 1]; 14 | 15 | pub const NPC_ID_MIN: u32 = 1; 16 | pub const DYN_NPC_ID_MIN: u32 = 100001; 17 | pub const DYN_NPC_ID_MAX: u32 = 199999; 18 | pub const MONSTER_ID_MIN: u32 = 400001; 19 | pub const MONSTER_ID_MAX: u32 = 499999; 20 | pub const PET_ID_MIN: u32 = 500001; 21 | pub const PET_ID_MAX: u32 = 599999; 22 | pub const NPC_ID_MAX: u32 = 700000; 23 | pub const CALL_PET_ID_MIN: u32 = 700001; 24 | pub const CALL_PET_ID_MAX: u32 = 799999; 25 | pub const CHARACTER_ID_MIN: u32 = 1000000; 26 | pub const CHARACTER_ID_MAX: u32 = 10000000; 27 | 28 | pub const fn is_npc(id: u32) -> bool { 29 | id >= NPC_ID_MIN && id <= NPC_ID_MAX 30 | } 31 | 32 | pub const fn is_terrain_npc(id: u32) -> bool { 33 | id >= DYN_NPC_ID_MIN && id <= DYN_NPC_ID_MAX 34 | } 35 | 36 | pub const fn is_monster(id: u32) -> bool { 37 | id >= MONSTER_ID_MIN && id <= MONSTER_ID_MAX 38 | } 39 | 40 | pub const fn is_pet(id: u32) -> bool { 41 | id >= PET_ID_MIN && id <= PET_ID_MAX 42 | } 43 | 44 | pub const fn is_call_pet(id: u32) -> bool { 45 | id >= CALL_PET_ID_MIN && id <= CALL_PET_ID_MAX 46 | } 47 | 48 | pub const fn is_character(id: u32) -> bool { 49 | id >= CHARACTER_ID_MIN && id <= CHARACTER_ID_MAX 50 | } 51 | -------------------------------------------------------------------------------- /server/game/src/world/portal.rs: -------------------------------------------------------------------------------- 1 | use crate::utils::LoHi; 2 | use std::hash::Hash; 3 | use std::ops::Deref; 4 | 5 | #[derive(Debug)] 6 | pub struct Portal { 7 | inner: tq_db::portal::Portal, 8 | } 9 | 10 | impl Deref for Portal { 11 | type Target = tq_db::portal::Portal; 12 | 13 | fn deref(&self) -> &Self::Target { 14 | &self.inner 15 | } 16 | } 17 | 18 | impl Portal { 19 | pub fn new(inner: tq_db::portal::Portal) -> Self { 20 | Self { inner } 21 | } 22 | 23 | pub fn uid(&self) -> u32 { 24 | self.inner.id as u32 25 | } 26 | 27 | pub fn id(&self) -> u32 { 28 | u32::constract(self.from_y(), self.from_x()) 29 | } 30 | 31 | #[allow(clippy::wrong_self_convention)] 32 | pub fn from_map_id(&self) -> u32 { 33 | self.inner.from_map_id as u32 34 | } 35 | 36 | pub fn to_map_id(&self) -> u32 { 37 | self.inner.to_map_id as u32 38 | } 39 | 40 | #[allow(clippy::wrong_self_convention)] 41 | pub fn from_x(&self) -> u16 { 42 | self.inner.from_x as u16 43 | } 44 | 45 | #[allow(clippy::wrong_self_convention)] 46 | pub fn from_y(&self) -> u16 { 47 | self.inner.from_y as u16 48 | } 49 | 50 | pub fn to_x(&self) -> u16 { 51 | self.inner.to_x as u16 52 | } 53 | 54 | pub fn to_y(&self) -> u16 { 55 | self.inner.to_y as u16 56 | } 57 | } 58 | 59 | impl PartialEq for Portal { 60 | fn eq(&self, other: &Self) -> bool { 61 | self.id == other.id 62 | } 63 | } 64 | 65 | impl Eq for Portal {} 66 | 67 | impl Hash for Portal { 68 | fn hash(&self, state: &mut H) { 69 | state.write_u32(self.id()); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "CoEmu development environment"; 3 | inputs = { 4 | nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; 5 | flake-utils.url = "github:numtide/flake-utils"; 6 | # Rust 7 | rust-overlay = { 8 | url = "github:oxalica/rust-overlay"; 9 | inputs = { 10 | nixpkgs.follows = "nixpkgs"; 11 | flake-utils.follows = "flake-utils"; 12 | }; 13 | }; 14 | }; 15 | 16 | outputs = { self, nixpkgs, rust-overlay, flake-utils }: 17 | flake-utils.lib.eachDefaultSystem (system: 18 | let 19 | overlays = [ (import rust-overlay) ]; 20 | pkgs = import nixpkgs { 21 | inherit system overlays; 22 | }; 23 | lib = pkgs.lib; 24 | toolchain = pkgs.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml; 25 | in 26 | { 27 | devShells.default = pkgs.mkShell { 28 | name = "coemu"; 29 | nativeBuildInputs = [ 30 | pkgs.pkg-config 31 | pkgs.clang 32 | pkgs.openssl 33 | # Mold Linker for faster builds (only on Linux) 34 | (lib.optionals pkgs.stdenv.isLinux pkgs.mold) 35 | (lib.optionals pkgs.stdenv.isDarwin pkgs.darwin.apple_sdk.frameworks.Security) 36 | ]; 37 | buildInputs = [ 38 | # We want the unwrapped version, wrapped comes with nixpkgs' toolchain 39 | pkgs.rust-analyzer-unwrapped 40 | # Finally the toolchain 41 | toolchain 42 | ]; 43 | packages = [ 44 | pkgs.cargo-nextest 45 | pkgs.cargo-expand 46 | ]; 47 | # Environment variables 48 | RUST_SRC_PATH = "${toolchain}/lib/rustlib/src/rust/library"; 49 | LD_LIBRARY_PATH = lib.makeLibraryPath [ ]; 50 | }; 51 | }); 52 | } 53 | -------------------------------------------------------------------------------- /server/game/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "game" 3 | version = "0.1.0" 4 | authors = ["Shady Khalifa "] 5 | edition.workspace = true 6 | 7 | 8 | [[bin]] 9 | name = "game-server" 10 | path = "src/main.rs" 11 | 12 | [lib] 13 | name = "game" 14 | path = "src/lib.rs" 15 | 16 | [dependencies] 17 | thiserror.workspace = true 18 | serde.workspace = true 19 | bytes.workspace = true 20 | tq-network = { workspace = true, features = ["std"] } 21 | tq-serde = { workspace = true, features = ["std"] } 22 | tq-math.workspace = true 23 | tq-db.workspace = true 24 | tq-server.workspace = true 25 | primitives.workspace = true 26 | async-trait.workspace = true 27 | tracing.workspace = true 28 | dotenvy.workspace = true 29 | tokio-stream.workspace = true 30 | rand.workspace = true 31 | chrono.workspace = true 32 | futures.workspace = true 33 | arc-swap.workspace = true 34 | atomic.workspace = true 35 | parking_lot.workspace = true 36 | 37 | bitflags = { workspace = true, features = ["serde"] } 38 | argh = "0.1" 39 | 40 | # Utils 41 | num_enum = { workspace = true, default-features = false } 42 | 43 | [dependencies.tracing-subscriber] 44 | version = "0.3" 45 | default-features = false 46 | features = ["env-filter", "ansi", "fmt", "smallvec"] 47 | 48 | # Debugging 49 | [dependencies.console-subscriber] 50 | version = "0.2" 51 | optional = true 52 | default-features = false 53 | features = ["env-filter", "parking_lot"] 54 | 55 | # Runtime 56 | [dependencies.tokio] 57 | workspace = true 58 | default-features = false 59 | features = ["rt-multi-thread", "macros", "signal", "sync", "parking_lot", "tracing"] 60 | 61 | # Database 62 | [dependencies.sqlx] 63 | workspace = true 64 | default-features = false 65 | features = ["runtime-tokio-rustls", "sqlite", "time"] 66 | 67 | [dev-dependencies.sqlx] 68 | workspace = true 69 | default-features = false 70 | features = ["runtime-tokio-rustls", "sqlite", "time", "migrate"] 71 | 72 | [features] 73 | default = [] 74 | console = ["dep:console-subscriber"] 75 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: [master] 4 | paths-ignore: 5 | - README.md 6 | - CHANGELOG.md 7 | - .gitignore 8 | - .github/** 9 | pull_request: 10 | branches: [master] 11 | types: [opened, synchronize, closed] 12 | workflow_dispatch: 13 | 14 | name: CI 15 | 16 | env: 17 | S3_ENDPOINT: https://pub-a119292fa58c4241b30d6ba460bf8231.r2.dev 18 | RUST_BACKTRACE: 1 19 | 20 | jobs: 21 | test: 22 | name: Compile and Test 23 | runs-on: ubuntu-latest 24 | concurrency: 25 | group: ${{ github.workflow }}-${{ github.ref }}-ubuntu-latest 26 | cancel-in-progress: true 27 | steps: 28 | - name: Checkout sources 29 | uses: actions/checkout@v4 30 | 31 | - name: Install nightly toolchain 32 | uses: actions-rs/toolchain@v1 33 | with: 34 | profile: minimal 35 | toolchain: nightly 36 | override: true 37 | 38 | - name: Rust Cache 39 | uses: Swatinem/rust-cache@v2 40 | with: 41 | shared-key: "rust" 42 | 43 | - name: Fixtures Cache 44 | uses: actions/cache@v3 45 | with: 46 | path: | 47 | data/GameMaps/GameMap.dat 48 | data/GameMaps/map/*.DMap 49 | data/GameMaps/Scene/*.scene 50 | data/GameMaps/ScenePart/*.Part 51 | key: fixtures-${{ hashFiles('scripts/fetch-test-fixtures.bash') }} 52 | restore-keys: | 53 | fixtures- 54 | 55 | - name: Setup mold Linker 56 | uses: rui314/setup-mold@v1 57 | 58 | - name: Download Test Fixtures 59 | run: bash ./scripts/fetch-test-fixtures.bash 60 | 61 | - name: Run cargo build 62 | uses: actions-rs/cargo@v1 63 | env: 64 | DATABASE_URL: file::memory:?cache=shared 65 | with: 66 | command: build 67 | 68 | - name: Run cargo test 69 | uses: actions-rs/cargo@v1 70 | with: 71 | command: test 72 | -------------------------------------------------------------------------------- /server/game/src/packets/msg_data.rs: -------------------------------------------------------------------------------- 1 | use std::time::{SystemTime, UNIX_EPOCH}; 2 | 3 | use crate::state::State; 4 | use crate::{ActorState, Error}; 5 | use chrono::{Datelike, NaiveDateTime, Timelike}; 6 | use num_enum::{FromPrimitive, IntoPrimitive}; 7 | use serde::{Deserialize, Serialize}; 8 | use tq_network::{Actor, PacketID, PacketProcess}; 9 | 10 | /// Enumeration type for defining data actions that may used by the client. 11 | #[derive(Debug, FromPrimitive, IntoPrimitive)] 12 | #[repr(u32)] 13 | pub enum DataAction { 14 | #[default] 15 | SetServerTime = 0, 16 | SetMountMovePoint = 2, 17 | AntiCheatAnswerMsgTypeCount = 3, 18 | AntiCheatAskMsgTypeCount = 4, 19 | } 20 | 21 | /// Message containing the current date and time. This is sent to the client 22 | /// to synchronize the client's clock with the server's clock. 23 | #[derive(Debug, Default, Deserialize, Serialize, PacketID)] 24 | #[packet(id = 1033)] 25 | pub struct MsgData { 26 | action: u32, 27 | year: i32, 28 | month: i32, 29 | day: i32, 30 | hour: i32, 31 | minute: i32, 32 | second: i32, 33 | } 34 | 35 | impl MsgData { 36 | pub fn now() -> Self { 37 | let now = SystemTime::now() 38 | .duration_since(UNIX_EPOCH) 39 | .expect("system time before Unix epoch"); 40 | let naive = NaiveDateTime::from_timestamp_opt(now.as_secs() as i64, now.subsec_nanos()).unwrap(); 41 | let now = chrono::TimeZone::from_utc_datetime(&chrono::Utc, &naive); 42 | Self { 43 | action: DataAction::SetServerTime.into(), 44 | year: now.year() - 1900, 45 | month: (now.month() - 1) as i32, 46 | day: now.day() as i32, 47 | hour: now.hour() as i32, 48 | minute: now.minute() as i32, 49 | second: now.second() as i32, 50 | } 51 | } 52 | } 53 | 54 | #[async_trait::async_trait] 55 | impl PacketProcess for MsgData { 56 | type ActorState = ActorState; 57 | type Error = Error; 58 | type State = State; 59 | 60 | async fn process(&self, _state: &Self::State, _actor: &Actor) -> Result<(), Self::Error> { 61 | Ok(()) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /macros/derive-packetid/src/lib.rs: -------------------------------------------------------------------------------- 1 | use proc_macro::TokenStream; 2 | use quote::quote; 3 | use syn::parse::{Parse, ParseStream}; 4 | use syn::{parse_macro_input, DeriveInput, Ident, Lit, LitInt, Token}; 5 | 6 | struct Args { 7 | id: LitInt, 8 | } 9 | 10 | impl Parse for Args { 11 | fn parse(input: ParseStream) -> syn::Result { 12 | let ident: Ident = input.parse()?; 13 | let _: Token!(=) = input.parse()?; 14 | let id: Lit = input.parse()?; 15 | if ident != "id" { 16 | return Err(syn::Error::new( 17 | ident.span(), 18 | format!("expected `id` but got {}", ident), 19 | )); 20 | } 21 | let id = if let Lit::Int(v) = id { 22 | v 23 | } else { 24 | let e = syn::Error::new(ident.span(), "Expected u16"); 25 | return Err(e); 26 | }; 27 | let args = Self { id }; 28 | Ok(args) 29 | } 30 | } 31 | 32 | fn derive_packet_id(input: DeriveInput) -> syn::Result { 33 | // Used in the quasi-quotation below as `#name`. 34 | let name = input.ident; 35 | let generics = input.generics; 36 | let attr = input 37 | .attrs 38 | .iter() 39 | .find(|a| a.path().is_ident("packet")) 40 | .ok_or_else(|| { 41 | syn::Error::new( 42 | name.span(), 43 | "Missing Packet id! please add #[packet(id = ..)] on the struct", 44 | ) 45 | })?; 46 | let args: Args = attr.parse_args()?; 47 | let id = args.id; 48 | let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); 49 | // Build the output, possibly using quasi-quotation 50 | let expanded = quote! { 51 | impl #impl_generics tq_network::PacketID for #name #ty_generics #where_clause { 52 | const PACKET_ID: u16 = #id; 53 | } 54 | }; 55 | Ok(expanded.into()) 56 | } 57 | 58 | #[proc_macro_derive(PacketID, attributes(packet))] 59 | pub fn derive(input: TokenStream) -> TokenStream { 60 | // Parse the input tokens into a syntax tree 61 | let input = parse_macro_input!(input as DeriveInput); 62 | derive_packet_id(input).unwrap_or_else(|err| err.to_compile_error().into()) 63 | } 64 | -------------------------------------------------------------------------------- /server/auth/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "auth" 3 | version = "0.1.0" 4 | authors = ["Shady Khalifa "] 5 | edition.workspace = true 6 | 7 | [[bin]] 8 | name = "auth-server" 9 | path = "src/main.rs" 10 | required-features = ["server"] 11 | 12 | [lib] 13 | name = "auth" 14 | path = "src/lib.rs" 15 | 16 | [dependencies] 17 | thiserror.workspace = true 18 | serde.workspace = true 19 | bytes.workspace = true 20 | tq-db = { workspace = true, features = ["sqlx"] } 21 | tq-network.workspace = true 22 | tq-serde.workspace = true 23 | async-trait.workspace = true 24 | tracing.workspace = true 25 | dotenvy.workspace = true 26 | tokio-stream.workspace = true 27 | num_enum.workspace = true 28 | futures.workspace = true 29 | rand.workspace = true 30 | rkyv = { workspace = true, default-features = false, features = ["alloc", "size_32"] } 31 | 32 | 33 | # Packets 34 | msg-account.workspace = true 35 | msg-connect.workspace = true 36 | msg-transfer.workspace = true 37 | 38 | 39 | [dependencies.tq-server] 40 | workspace = true 41 | optional = true 42 | 43 | [dependencies.wasmtime] 44 | workspace = true 45 | default-features = false 46 | features = ["async", "cranelift", "coredump", "cache", "pooling-allocator"] 47 | 48 | [dependencies.tracing-subscriber] 49 | workspace = true 50 | optional = true 51 | default-features = false 52 | features = ["env-filter", "ansi", "fmt", "smallvec"] 53 | 54 | # Runtime 55 | [dependencies.tokio] 56 | workspace = true 57 | default-features = false 58 | features = [] 59 | 60 | # Database 61 | [dependencies.sqlx] 62 | workspace = true 63 | default-features = false 64 | features = ["sqlite"] 65 | 66 | [dev-dependencies] 67 | tokio = { workspace = true, features = ["full"] } 68 | sqlx = { workspace = true, features = ["sqlite", "runtime-tokio", "migrate"] } 69 | tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt", "ansi"] } 70 | 71 | msg-connect-ex.workspace = true 72 | 73 | [features] 74 | default = [] 75 | server = [ 76 | "tq-network/std", 77 | "tq-serde/std", 78 | "msg-account/std", 79 | "msg-connect/std", 80 | "msg-transfer/std", 81 | "tokio/rt-multi-thread", 82 | "tokio/macros", 83 | "tokio/signal", 84 | "dep:tq-server", 85 | "sqlx/runtime-tokio", 86 | "dep:tracing-subscriber", 87 | ] 88 | -------------------------------------------------------------------------------- /server/game/src/packets/msg_player.rs: -------------------------------------------------------------------------------- 1 | use crate::entities::Character; 2 | use serde::{Deserialize, Serialize}; 3 | use tq_network::PacketID; 4 | 5 | /// This packet is sent to the observing clients on the map 6 | /// server when the actor enters their screen or an acting client observes the 7 | /// character as they enter its screen. The packet contains the player's 8 | /// character spawn information. This class only encapsulates constants related 9 | /// to writing data to the packet buffer. The character class handles writing to 10 | /// the packet as data changes. 11 | #[derive(Debug, Serialize, Deserialize, Clone, PacketID, Default)] 12 | #[packet(id = 1014)] 13 | pub struct MsgPlayer { 14 | pub character_id: i32, 15 | mesh: i32, 16 | status_flags: i64, 17 | syndicate_id: i16, 18 | /// Unknown 19 | reserved0: u8, 20 | syndicate_member_rank: u8, 21 | germent: i32, 22 | helment: i32, 23 | armor: i32, 24 | right_hand: i32, 25 | left_hand: i32, 26 | reserved1: i32, 27 | health_points: u16, 28 | level: i16, 29 | pub x: u16, 30 | pub y: u16, 31 | hair_style: i16, 32 | direction: u8, 33 | action: u8, 34 | metempsychosis: i16, 35 | level2: i16, 36 | reserved2: i32, 37 | nobility_rank: i32, 38 | character_id2: i32, 39 | nobility_position: i32, 40 | list_count: u8, 41 | pub character_name: String, 42 | } 43 | 44 | impl From<&Character> for MsgPlayer { 45 | fn from(c: &Character) -> Self { 46 | let loc = c.entity().location(); 47 | Self { 48 | character_id: c.id() as i32, 49 | character_id2: c.id() as i32, 50 | mesh: (c.entity().mesh() + (c.avatar() as u32 * 10_000)) as i32, 51 | health_points: c.entity().hp().current(), 52 | hair_style: c.hair_style() as i16, 53 | level: c.entity().level() as i16, 54 | level2: c.entity().level() as i16, 55 | x: loc.x, 56 | y: loc.y, 57 | direction: loc.direction, 58 | list_count: 1, 59 | character_name: c.entity().name().to_owned(), 60 | status_flags: c.entity().flags().bits() as i64, 61 | action: c.entity().action() as u8, 62 | ..Default::default() 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-utils": { 4 | "inputs": { 5 | "systems": "systems" 6 | }, 7 | "locked": { 8 | "lastModified": 1689068808, 9 | "narHash": "sha256-6ixXo3wt24N/melDWjq70UuHQLxGV8jZvooRanIHXw0=", 10 | "owner": "numtide", 11 | "repo": "flake-utils", 12 | "rev": "919d646de7be200f3bf08cb76ae1f09402b6f9b4", 13 | "type": "github" 14 | }, 15 | "original": { 16 | "owner": "numtide", 17 | "repo": "flake-utils", 18 | "type": "github" 19 | } 20 | }, 21 | "nixpkgs": { 22 | "locked": { 23 | "lastModified": 1690083312, 24 | "narHash": "sha256-I3egwgNXavad1eIjWu1kYyi0u73di/sMmlnQIuzQASk=", 25 | "owner": "NixOS", 26 | "repo": "nixpkgs", 27 | "rev": "af8cd5ded7735ca1df1a1174864daab75feeb64a", 28 | "type": "github" 29 | }, 30 | "original": { 31 | "owner": "NixOS", 32 | "ref": "nixpkgs-unstable", 33 | "repo": "nixpkgs", 34 | "type": "github" 35 | } 36 | }, 37 | "root": { 38 | "inputs": { 39 | "flake-utils": "flake-utils", 40 | "nixpkgs": "nixpkgs", 41 | "rust-overlay": "rust-overlay" 42 | } 43 | }, 44 | "rust-overlay": { 45 | "inputs": { 46 | "flake-utils": [ 47 | "flake-utils" 48 | ], 49 | "nixpkgs": [ 50 | "nixpkgs" 51 | ] 52 | }, 53 | "locked": { 54 | "lastModified": 1690165277, 55 | "narHash": "sha256-P3X8iSAu12z+UFxquuntZnR8sXjKwgYHf0wTzgO8I7M=", 56 | "owner": "oxalica", 57 | "repo": "rust-overlay", 58 | "rev": "317c523c09218f27f1da1ec0d06bbd2cbc0c1939", 59 | "type": "github" 60 | }, 61 | "original": { 62 | "owner": "oxalica", 63 | "repo": "rust-overlay", 64 | "type": "github" 65 | } 66 | }, 67 | "systems": { 68 | "locked": { 69 | "lastModified": 1681028828, 70 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 71 | "owner": "nix-systems", 72 | "repo": "default", 73 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 74 | "type": "github" 75 | }, 76 | "original": { 77 | "owner": "nix-systems", 78 | "repo": "default", 79 | "type": "github" 80 | } 81 | } 82 | }, 83 | "root": "root", 84 | "version": 7 85 | } 86 | -------------------------------------------------------------------------------- /packets/connect-ex/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(not(feature = "std"), no_std)] 2 | 3 | use num_enum::IntoPrimitive; 4 | use serde::Serialize; 5 | use tq_network::PacketID; 6 | use tq_serde::String16; 7 | 8 | /// Rejection codes are sent to the client in offset 8 of this packet when the 9 | /// client has failed authentication with the account server. These codes define 10 | /// which error message will be displayed in the client. 11 | #[derive(Debug, IntoPrimitive, Copy, Clone)] 12 | #[repr(u32)] 13 | pub enum RejectionCode { 14 | Clear = 0, 15 | InvalidPassword = 1, 16 | Ready = 2, 17 | ServerDown = 10, 18 | TryAgainLater = 11, 19 | AccountBanned = 12, 20 | ServerBusy = 20, 21 | AccountLocked = 22, 22 | AccountNotActivated = 30, 23 | AccountActivationFailed = 31, 24 | ServerTimedOut = 42, 25 | AccountMaxLoginAttempts = 51, 26 | ServerLocked = 70, 27 | ServerOldProtocol = 73, 28 | } 29 | 30 | impl RejectionCode { 31 | pub fn packet(self) -> MsgConnectRejection { 32 | MsgConnectEx::from_code(self) 33 | } 34 | } 35 | 36 | #[derive(Debug, Serialize, PacketID)] 37 | #[packet(id = 1055)] 38 | pub struct MsgConnectEx { 39 | token: u64, 40 | game_server_ip: String16, 41 | game_server_port: u32, 42 | } 43 | 44 | #[derive(Debug, Serialize, PacketID)] 45 | #[packet(id = 1055)] 46 | pub struct MsgConnectRejection { 47 | reserved: u32, 48 | rejection_code: u32, 49 | message: String16, 50 | } 51 | 52 | #[derive(Debug, Clone)] 53 | pub struct AccountCredentials { 54 | pub token: u64, 55 | pub server_ip: String, 56 | pub server_port: u32, 57 | } 58 | 59 | impl MsgConnectEx { 60 | /// Instantiates a new instance of `MsgConnectRejection` for rejecting a 61 | /// client connection using a rejection code. The rejection code spawns an 62 | /// error dialog in the client with a respective error message. 63 | #[allow(unused)] 64 | pub fn from_code(code: RejectionCode) -> MsgConnectRejection { 65 | MsgConnectRejection { 66 | reserved: 0, 67 | rejection_code: code.into(), 68 | message: String::new().into(), 69 | } 70 | } 71 | 72 | pub fn forword_connection(acc_credentials: AccountCredentials) -> Self { 73 | MsgConnectEx { 74 | token: acc_credentials.token, 75 | game_server_ip: acc_credentials.server_ip.into(), 76 | game_server_port: acc_credentials.server_port, 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /tools/npcs.py: -------------------------------------------------------------------------------- 1 | # Given the following csv file with structure: 2 | # UniqId,Name,Type,Look,MapId,X,Y,Base,Sort,Level,Life,Defence,MagicDef 3 | # 1,Storekeeper,0001,10,1002,0415,0351,0000,0000,0000,0000,0000,0000 4 | # 5 | # This script will generate SQL statements to insert the NPCs into the database. 6 | # One Input also is the Maps.csv file to get the map_id or to check if the map_id exists. 7 | # 8 | # Usage: python npcs.py --npcs npcs.csv --maps maps.csv --output npcs.sql 9 | # Example: python npcs.py --npcs npcs.csv --maps maps.csv --output npcs.sql 10 | 11 | import argparse 12 | import csv 13 | 14 | parser = argparse.ArgumentParser(description='Generate SQL statements to insert NPCs into the database.') 15 | parser.add_argument('--npcs', dest='csv_file', help='CSV file with the NPCs') 16 | parser.add_argument('--maps', dest='maps_file', help='CSV file with the Maps') 17 | parser.add_argument('--output', dest='output_file', help='Output file with the SQL statements') 18 | args = parser.parse_args() 19 | 20 | # Read the maps file to get the map_id 21 | maps = {} 22 | with open(args.maps_file, 'r') as csvfile: 23 | reader = csv.reader(csvfile, delimiter=',', skipinitialspace=True, dialect=csv.unix_dialect) 24 | next(reader) # skip header 25 | for row in reader: 26 | map_id = row[0] 27 | maps[map_id] = True 28 | 29 | statms = [] 30 | with open(args.csv_file, 'r') as csvfile: 31 | reader = csv.reader(csvfile, delimiter=',', skipinitialspace=True, dialect=csv.unix_dialect) 32 | next(reader) # skip header 33 | for row in reader: 34 | # We are using SQLITE so we don't need to specify the columns 35 | uniq_id = row[0] 36 | name = row[1] 37 | npc_type = row[2] 38 | look = row[3] 39 | map_id = row[4] 40 | x = row[5] 41 | y = row[6] 42 | base = row[7] 43 | sort = row[8] 44 | level = row[9] 45 | life = row[10] 46 | defence = row[11] 47 | magic_def = row[12] 48 | smtm = f"INSERT INTO npcs VALUES ({uniq_id}, '{name}', {npc_type}, {look}, {map_id}, {x}, {y}, {base}, {sort}, {level}, {life}, {defence}, {magic_def});" 49 | if map_id in maps: 50 | statms.append(smtm) 51 | else: 52 | # Add a comment to the SQL statement 53 | statms.append(f"-- {smtm}") 54 | 55 | # Write the SQL statements to the output file 56 | with open(args.output_file, 'w') as f: 57 | for smtm in statms: 58 | f.write(smtm + "\n") 59 | -------------------------------------------------------------------------------- /server/game/src/packets/msg_map_info.rs: -------------------------------------------------------------------------------- 1 | use serde::Serialize; 2 | use tq_network::PacketID; 3 | 4 | use crate::world::Map; 5 | 6 | bitflags::bitflags! { 7 | #[repr(transparent)] 8 | #[derive(Default, Serialize, Debug, Clone, Copy, PartialEq, Eq, Hash)] 9 | pub struct MapFlags: u32 { 10 | const NONE = 0; 11 | /// No PkPoints, Not Flashing. 12 | const PK_FIELD = 1 << 0; 13 | /// No Change Map. 14 | const CHANGE_MAP_DISABLED = 1 << 1; 15 | /// Do not save this position, save the previous 16 | const RECORD_DISABLED = 1 << 2; 17 | /// No PK. 18 | const PK_DISABLED = 1 << 3; 19 | /// Booth enabled. 20 | const BOOTH_ENABLED = 1 << 4; 21 | /// Team Disabled. 22 | const TEAM_DISABLED = 1 << 5; 23 | /// Teleport Disabled. 24 | const TELEPORT_DISABLED = 1 << 6; 25 | /// Syndicate Map. 26 | const SYNDICATE_MAP = 1 << 7; 27 | /// Prison Map 28 | const PRISON_MAP = 1 << 8; 29 | /// Can't fly. 30 | const FLY_DISABLED = 1 << 9; 31 | /// Family Map. 32 | const FAMILY_MAP = 1 << 10; 33 | /// Mine Map. 34 | const MINE_FIELD = 1 << 11; 35 | /// Free For All Map. (No Newbie Protection) 36 | const FFA_MAP = 1 << 12; 37 | /// Blessed reborn map. 38 | const BLESSED_REBORN_MAP = 1 << 13; 39 | /// Neobiess Protection. 40 | const NEWBIE_PROTECTION = 1 << 14; 41 | } 42 | } 43 | 44 | /// This packet is sent from the game server to the game client to set game map 45 | /// rules and details. The packet may be used to set game map rules, such as 46 | /// no-pk and no-jump. It may also be used to create dynamic map copies of 47 | /// static maps, or static copies of static maps. The game identity comes from 48 | /// GameMap.dat. The unique map identity will be the same as the game map 49 | /// identity if the map is static (not a copy). 50 | #[derive(Debug, Serialize, Clone, PacketID)] 51 | #[packet(id = 1110)] 52 | pub struct MsgMapInfo { 53 | uid: u32, 54 | map_id: u32, 55 | flags: MapFlags, 56 | } 57 | 58 | impl MsgMapInfo { 59 | pub fn from_map(map: &Map) -> Self { 60 | Self { 61 | uid: map.id(), 62 | map_id: map.map_id(), 63 | flags: map.flags(), 64 | } 65 | } 66 | 67 | pub fn is_static(&self) -> bool { 68 | self.uid == self.map_id 69 | } 70 | 71 | pub fn is_copy(&self) -> bool { 72 | self.uid != self.map_id 73 | } 74 | } 75 | 76 | impl From<&Map> for MsgMapInfo { 77 | fn from(map: &Map) -> Self { 78 | Self::from_map(map) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /server/game/src/packets/msg_connect.rs: -------------------------------------------------------------------------------- 1 | use super::{MsgTalk, MsgUserInfo}; 2 | use crate::entities::Character; 3 | use crate::packets::MsgData; 4 | use crate::systems::Screen; 5 | use crate::{ActorState, Error, State}; 6 | use serde::{Deserialize, Serialize}; 7 | use tq_network::{Actor, IntoErrorPacket, PacketID, PacketProcess}; 8 | use tq_serde::String10; 9 | 10 | /// Message containing a connection request to the game server. Contains the 11 | /// player's access token from the Account server, and the patch and language 12 | /// versions of the game client. 13 | #[derive(Debug, Default, Serialize, Deserialize, PacketID)] 14 | #[packet(id = 1052)] 15 | #[allow(dead_code)] 16 | pub struct MsgConnect { 17 | pub token: u64, 18 | pub build_version: u16, 19 | pub language: String10, 20 | pub file_contents: u32, 21 | } 22 | 23 | #[async_trait::async_trait] 24 | impl PacketProcess for MsgConnect { 25 | type ActorState = ActorState; 26 | type Error = Error; 27 | type State = State; 28 | 29 | async fn process(&self, state: &Self::State, actor: &Actor) -> Result<(), Self::Error> { 30 | let info = state 31 | .remove_login_token(self.token) 32 | .map_err(|_| MsgTalk::login_invalid().error_packet())?; 33 | actor.generate_keys(self.token).await?; 34 | actor.set_id(info.account_id as usize); 35 | let maybe_character = tq_db::character::Character::from_account(state.pool(), info.account_id).await?; 36 | match maybe_character { 37 | Some(character) => { 38 | let me = Character::new(actor.handle(), character); 39 | let mymap_id = me.entity().map_id(); 40 | let screen = Screen::new(actor.handle()); 41 | let msg = MsgUserInfo::from(&me); 42 | actor.update(me, screen); 43 | let mymap = state 44 | .try_map(mymap_id) 45 | .map_err(|_| MsgTalk::login_invalid().error_packet())?; 46 | mymap.insert_entity(actor.entity()).await?; 47 | state.insert_entity(actor.entity()); 48 | actor.send(MsgTalk::login_ok()).await?; 49 | actor.send(msg).await?; 50 | actor.send(MsgData::now()).await?; 51 | }, 52 | None => { 53 | state.store_creation_token(self.token as u32, info.account_id, info.realm_id)?; 54 | actor.send(MsgTalk::login_new_role()).await?; 55 | }, 56 | }; 57 | Ok(()) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /packets/account/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(not(feature = "std"), no_std)] 2 | 3 | #[cfg(not(feature = "std"))] 4 | extern crate alloc; 5 | 6 | include!(concat!(env!("OUT_DIR"), "/wasm.rs")); 7 | 8 | use msg_connect_ex::{MsgConnectEx, RejectionCode}; 9 | use msg_transfer::MsgTransfer; 10 | use serde::{Deserialize, Serialize}; 11 | use tq_bindings::{host, Resource}; 12 | use tq_network::PacketID; 13 | use tq_serde::{String16, TQPassword}; 14 | 15 | use tq_network::ActorHandle; 16 | 17 | #[derive(Default, Debug, Serialize, Deserialize, PacketID)] 18 | #[packet(id = 1051)] 19 | pub struct MsgAccount { 20 | pub username: String16, 21 | pub password: TQPassword, 22 | pub realm: String16, 23 | #[serde(skip)] 24 | pub rejection_code: u32, 25 | #[serde(skip)] 26 | pub account_id: i32, 27 | } 28 | 29 | /// Possible errors that can occur while processing a packet. 30 | #[derive(Debug, thiserror::Error)] 31 | pub enum Error { 32 | /// User has entered an invalid username or password. 33 | #[error("Invalid username or password")] 34 | InvalidUsernameOrPassword, 35 | /// Internal Network error. 36 | #[error(transparent)] 37 | Network(#[from] tq_network::Error), 38 | #[error(transparent)] 39 | Db(#[from] tq_db::Error), 40 | } 41 | 42 | #[tq_network::packet_processor(MsgAccount)] 43 | pub fn process(msg: MsgAccount, actor: &Resource) -> Result<(), crate::Error> { 44 | let maybe_accont_id = host::db::account::auth(&msg.username, &msg.password); 45 | let account_id = match maybe_accont_id { 46 | Ok(id) => id, 47 | Err(e) => { 48 | let res = match e { 49 | tq_db::Error::AccountNotFound | tq_db::Error::InvalidPassword => { 50 | RejectionCode::InvalidPassword.packet() 51 | }, 52 | _ => { 53 | tracing::error!("Error authenticating account: {e}"); 54 | RejectionCode::TryAgainLater.packet() 55 | }, 56 | }; 57 | host::network::actor::send(actor, res)?; 58 | return Ok(()); 59 | }, 60 | }; 61 | host::network::actor::set_id(actor, account_id); 62 | let res = match MsgTransfer::handle(actor, &msg.realm) { 63 | Ok(res) => res, 64 | _ => { 65 | tracing::warn!( 66 | %account_id, 67 | "Failed to transfer account" 68 | ); 69 | return Ok(()); 70 | }, 71 | }; 72 | let res = MsgConnectEx::forword_connection(res); 73 | host::network::actor::send(actor, res)?; 74 | Ok(()) 75 | } 76 | -------------------------------------------------------------------------------- /crates/db/src/account.rs: -------------------------------------------------------------------------------- 1 | /// Account information for a registered player. The account server uses this 2 | /// information to authenticate the player on login. Passwords are hashed using 3 | /// bcrypt 4 | #[derive(Default, Debug)] 5 | #[cfg_attr(feature = "sqlx", derive(sqlx::FromRow))] 6 | pub struct Account { 7 | pub account_id: i32, 8 | pub username: String, 9 | pub password: String, 10 | pub name: Option, 11 | pub email: Option, 12 | } 13 | 14 | #[cfg(feature = "sqlx")] 15 | impl Account { 16 | pub async fn auth(pool: &sqlx::SqlitePool, username: &str, password: &str) -> Result { 17 | let maybe_account = sqlx::query_as::<_, Self>("SELECT * FROM accounts WHERE username = ?;") 18 | .bind(username) 19 | .fetch_optional(pool) 20 | .await?; 21 | match maybe_account { 22 | Some(account) => { 23 | let matched = bcrypt::verify(password, &account.password)?; 24 | if matched { 25 | Ok(account) 26 | } else { 27 | Err(crate::Error::InvalidPassword) 28 | } 29 | }, 30 | None => Err(crate::Error::AccountNotFound), 31 | } 32 | } 33 | 34 | /// Returns all accounts in the database. 35 | /// 36 | /// Useful for testing purposes. 37 | pub async fn all( 38 | pool: &sqlx::SqlitePool, 39 | limit: Option, 40 | offset: Option, 41 | ) -> Result, crate::Error> { 42 | use futures::TryFutureExt; 43 | sqlx::query_as::<_, Self>("SELECT * FROM accounts LIMIT ? OFFSET ?;") 44 | .bind(limit.unwrap_or(100)) 45 | .bind(offset.unwrap_or(0)) 46 | .fetch_all(pool) 47 | .map_err(Into::into) 48 | .await 49 | } 50 | 51 | // === Methods === 52 | 53 | /// Creates a new account in the database. 54 | pub async fn create(mut self, pool: &sqlx::SqlitePool) -> Result { 55 | let password = bcrypt::hash(&self.password, bcrypt::DEFAULT_COST)?; 56 | let res = sqlx::query("INSERT INTO accounts (username, password, name, email) VALUES (?, ?, ?, ?);") 57 | .bind(&self.username) 58 | .bind(&password) 59 | .bind(&self.name) 60 | .bind(&self.email) 61 | .execute(pool) 62 | .await?; 63 | if res.rows_affected() == 0 { 64 | Err(crate::Error::CreateAccountFailed) 65 | } else { 66 | self.account_id = res.last_insert_rowid() as i32; 67 | Ok(self) 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /server/game/src/entities/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::Error; 2 | use tq_network::ActorHandle; 3 | 4 | mod floor_item; 5 | pub use floor_item::{FloorItem, Item}; 6 | 7 | mod basic; 8 | pub use basic::Entity; 9 | 10 | mod character; 11 | pub use character::Character; 12 | 13 | mod npc; 14 | pub use npc::{Npc, NpcBase, NpcKind, NpcSort}; 15 | 16 | #[derive(Debug)] 17 | pub enum GameEntity { 18 | Character(Character), 19 | Npc(Npc), 20 | } 21 | 22 | impl From for GameEntity { 23 | fn from(v: Character) -> Self { 24 | Self::Character(v) 25 | } 26 | } 27 | 28 | impl From for GameEntity { 29 | fn from(v: Npc) -> Self { 30 | Self::Npc(v) 31 | } 32 | } 33 | 34 | impl GameEntity { 35 | /// Returns the ID of the Game Entity. 36 | pub fn id(&self) -> u32 { 37 | match self { 38 | Self::Character(v) => v.id(), 39 | Self::Npc(v) => v.id(), 40 | } 41 | } 42 | 43 | /// Returns the Owner of the Game Entity. 44 | /// 45 | /// Only [`Character`]s have an owner. 46 | pub fn owner(&self) -> Option { 47 | match self { 48 | Self::Character(v) => Some(v.owner()), 49 | Self::Npc(..) => None, 50 | } 51 | } 52 | 53 | pub fn basic(&self) -> &Entity { 54 | match self { 55 | Self::Character(v) => v.entity(), 56 | Self::Npc(v) => v.entity(), 57 | } 58 | } 59 | 60 | /// This method sends the spawn packet to another entity 61 | pub async fn send_spawn(&self, to: &Self) -> Result<(), Error> { 62 | match (self, to) { 63 | (Self::Character(from), Self::Character(to)) => from.send_spawn(&to.owner()).await, 64 | (Self::Npc(from), Self::Character(to)) => from.send_spawn(&to.owner()).await, 65 | _ => todo!("send_spawn for non-character entities"), 66 | } 67 | } 68 | 69 | /// Returns `true` if the game entity is [`Character`]. 70 | /// 71 | /// [`Character`]: GameEntity::Character 72 | #[must_use] 73 | pub fn is_character(&self) -> bool { 74 | matches!(self, Self::Character(..)) 75 | } 76 | 77 | pub fn as_character(&self) -> Option<&Character> { 78 | if let Self::Character(v) = self { 79 | Some(v) 80 | } else { 81 | None 82 | } 83 | } 84 | 85 | /// Returns `true` if the game entity is [`Npc`]. 86 | /// 87 | /// [`Npc`]: GameEntity::Npc 88 | #[must_use] 89 | pub fn is_npc(&self) -> bool { 90 | matches!(self, Self::Npc(..)) 91 | } 92 | 93 | pub fn as_npc(&self) -> Option<&Npc> { 94 | if let Self::Npc(v) = self { 95 | Some(v) 96 | } else { 97 | None 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions include code, documentation, answering user questions, running the 4 | project's infrastructure, and advocating for all types of users. 5 | 6 | The project welcomes all contributions from anyone willing to work in good faith 7 | with other contributors and the community. No contribution is too small and all 8 | contributions are valued. 9 | 10 | This guide explains the process for contributing to the project's GitHub 11 | Repository. 12 | 13 | - [Code of Conduct](#code-of-conduct) 14 | - [Bad Actors](#bad-actors) 15 | 16 | ## Code of Conduct 17 | 18 | The project has a [Code of Conduct](./CODE_OF_CONDUCT.md) that _all_ 19 | contributors are expected to follow. This code describes the _minimum_ behavior 20 | expectations for all contributors. 21 | 22 | As a contributor, how you choose to act and interact towards your 23 | fellow contributors, as well as to the community, will reflect back not only 24 | on yourself but on the project as a whole. The Code of Conduct is designed and 25 | intended, above all else, to help establish a culture within the project that 26 | allows anyone and everyone who wants to contribute to feel safe doing so. 27 | 28 | Should any individual act in any way that is considered in violation of the 29 | [Code of Conduct](./CODE_OF_CONDUCT.md), corrective actions will be taken. It is 30 | possible, however, for any individual to _act_ in such a manner that is not in 31 | violation of the strict letter of the Code of Conduct guidelines while still 32 | going completely against the spirit of what that Code is intended to accomplish. 33 | 34 | Open, diverse, and inclusive communities live and die on the basis of trust. 35 | Contributors can disagree with one another so long as they trust that those 36 | disagreements are in good faith and everyone is working towards a common 37 | goal. 38 | 39 | ## Bad Actors 40 | 41 | All contributors to tacitly agree to abide by both the letter and 42 | spirit of the [Code of Conduct](./CODE_OF_CONDUCT.md). Failure, or 43 | unwillingness, to do so will result in contributions being respectfully 44 | declined. 45 | 46 | A _bad actor_ is someone who repeatedly violates the _spirit_ of the Code of 47 | Conduct through consistent failure to self-regulate the way in which they 48 | interact with other contributors in the project. In doing so, bad actors 49 | alienate other contributors, discourage collaboration, and generally reflect 50 | poorly on the project as a whole. 51 | 52 | Being a bad actor may be intentional or unintentional. Typically, unintentional 53 | bad behavior can be easily corrected by being quick to apologize and correct 54 | course _even if you are not entirely convinced you need to_. Giving other 55 | contributors the benefit of the doubt and having a sincere willingness to admit 56 | that you _might_ be wrong is critical for any successful open collaboration. 57 | 58 | Don't be a bad actor. 59 | -------------------------------------------------------------------------------- /server/game/src/packets/msg_weather.rs: -------------------------------------------------------------------------------- 1 | use num_enum::{FromPrimitive, IntoPrimitive}; 2 | use rand::Rng; 3 | use serde::Serialize; 4 | use tq_network::PacketID; 5 | 6 | /// These enumeration type values are hard-coded into the client and server, 7 | /// sent when the [`MsgWeather`] packet. 8 | #[derive(Debug, Clone, Copy, PartialEq, Eq, FromPrimitive, IntoPrimitive)] 9 | #[repr(u32)] 10 | pub enum WeatherKind { 11 | #[num_enum(default)] 12 | Unknwon = 0, 13 | None = 1, 14 | Rain = 2, 15 | Snow = 3, 16 | RainWind = 4, 17 | AutumnLeaves = 5, 18 | CherryBlossomPetals = 7, 19 | CherryBlossomPetalsWind = 8, 20 | BlowingCotten = 9, 21 | Atoms = 10, 22 | } 23 | 24 | impl WeatherKind { 25 | /// Returns `true` if the weather kind is [`None`]. 26 | /// 27 | /// [`None`]: WeatherKind::None 28 | #[must_use] 29 | pub fn is_none(&self) -> bool { 30 | matches!(self, Self::None) 31 | } 32 | 33 | /// Returns `true` if the weather kind is [`Unknwon`]. 34 | /// 35 | /// [`Unknwon`]: WeatherKind::Unknwon 36 | #[must_use] 37 | pub fn is_unknwon(&self) -> bool { 38 | matches!(self, Self::Unknwon) 39 | } 40 | } 41 | 42 | /// This packet is sent from the game server to the client for invoking weather 43 | /// on a map. This packet must be sent when the player changes maps; otherwise, 44 | /// the weather will be cleared by the client. 45 | #[derive(Debug, Serialize, Clone, PacketID)] 46 | #[packet(id = 1016)] 47 | pub struct MsgWeather { 48 | /// Weather type 49 | kind: u32, 50 | /// Range: 0 - 999 51 | intensity: u32, 52 | /// Range: 0 - 359 53 | direction: u32, 54 | /// Color in ARGB (Default: 0x00FFFFFF) 55 | color: u32, 56 | } 57 | 58 | impl MsgWeather { 59 | pub fn new(kind: WeatherKind) -> Self { 60 | let mut rng = rand::thread_rng(); 61 | Self { 62 | kind: kind.into(), 63 | intensity: rng.gen_range(1..=999), 64 | direction: rng.gen_range(1..=359), 65 | color: 0x00FFFFFF, 66 | } 67 | } 68 | 69 | pub fn none() -> Self { 70 | Self::new(WeatherKind::None) 71 | } 72 | 73 | pub fn rain() -> Self { 74 | Self::new(WeatherKind::Rain) 75 | } 76 | 77 | pub fn snow() -> Self { 78 | Self::new(WeatherKind::Snow) 79 | } 80 | 81 | pub fn rain_wind() -> Self { 82 | Self::new(WeatherKind::RainWind) 83 | } 84 | 85 | pub fn autumn_leaves() -> Self { 86 | Self::new(WeatherKind::AutumnLeaves) 87 | } 88 | 89 | pub fn cherry_blossom_petals() -> Self { 90 | Self::new(WeatherKind::CherryBlossomPetals) 91 | } 92 | 93 | pub fn cherry_blossom_petals_wind() -> Self { 94 | Self::new(WeatherKind::CherryBlossomPetalsWind) 95 | } 96 | 97 | pub fn blowing_cotten() -> Self { 98 | Self::new(WeatherKind::BlowingCotten) 99 | } 100 | 101 | pub fn atoms() -> Self { 102 | Self::new(WeatherKind::Atoms) 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace.package] 2 | authors = ["Shady Khalifa "] 3 | edition = "2021" 4 | repository = "https://github.com/shekohex/coemu.git" 5 | license = "GPL-3.0-only" 6 | 7 | [workspace] 8 | resolver = "2" 9 | members = [ 10 | "crates/*", 11 | # Game Packets 12 | "packets/*", 13 | # Servers 14 | "server/*", 15 | # Utils 16 | "macros/*", 17 | # Cli Tools 18 | "tools/*", 19 | ] 20 | exclude = ["tools/benchbot"] 21 | 22 | [workspace.dependencies] 23 | # Local Dependencies 24 | primitives = { path = "crates/primitives" } 25 | tq-network = { path = "crates/network" } 26 | tq-serde = { path = "crates/serde" } 27 | tq-math = { path = "crates/math" } 28 | tq-crypto = { path = "crates/crypto" } 29 | tq-codec = { path = "crates/codec" } 30 | tq-db = { path = "crates/db", default-features = false } 31 | tq-server = { path = "crates/server" } 32 | tq-bindings = { path = "crates/bindings" } 33 | tq-wasm-builder = { path = "crates/wasm-builder" } 34 | tracing-wasm = { path = "crates/tracing-wasm" } 35 | 36 | derive-packetid = { path = "macros/derive-packetid" } 37 | derive-packethandler = { path = "macros/derive-packethandler" } 38 | derive-packetprocessor = { path = "macros/derive-packetprocessor" } 39 | 40 | # Servers & Libs 41 | auth = { path = "server/auth" } 42 | game = { path = "server/game" } 43 | 44 | # Packets 45 | msg-account = { path = "packets/account" } 46 | msg-connect-ex = { path = "packets/connect-ex" } 47 | msg-connect = { path = "packets/connect" } 48 | msg-transfer = { path = "packets/transfer" } 49 | 50 | futures = { version = "0.3", default-features = false } 51 | thiserror = "1.0" 52 | anyhow = "1.0" 53 | serde = { version = "1.0", default-features = false, features = ["alloc", "derive"] } 54 | bytes = { version = "1.5", default-features = false } 55 | async-trait = { version = "0.1", default-features = false } 56 | tracing = { version = "0.1", default-features = false } 57 | tracing-core = { version = "0.1", default-features = false } 58 | tracing-subscriber = { version = "0.3", default-features = false } 59 | dotenvy = "0.15" 60 | bitflags = { version = "2.4", default-features = false } 61 | argh = "0.1" 62 | tokio-stream = { version = "0.1.8", default-features = false } 63 | parking_lot = { version = "0.12.1", default-features = false, features = [] } 64 | rand = "0.8" 65 | getrandom = { version = "0.2", default-features = false } 66 | arc-swap = { version = "1.6", features = ["weak"] } 67 | atomic = { version = "0.6", default-features = false } 68 | bytemuck = { version = "1.13", default-features = false, features = ["derive"] } 69 | chrono = { version = "0.4", default-features = false, features = [] } 70 | num_enum = { version = "0.7", default-features = false } 71 | rkyv = { version = "0.7", default-features = false } 72 | bcrypt = "0.15" 73 | 74 | # WASM deps 75 | externref = { version = "0.2.0", default-features = false } 76 | 77 | [workspace.dependencies.wasmtime] 78 | version = "17.0.0" 79 | default-features = false 80 | 81 | [workspace.dependencies.tokio] 82 | version = "1.21.2" 83 | default-features = false 84 | 85 | [workspace.dependencies.sqlx] 86 | default-features = false 87 | version = "0.7.3" 88 | 89 | [profile.dev] 90 | split-debuginfo = 'packed' 91 | -------------------------------------------------------------------------------- /server/game/src/packets/msg_walk.rs: -------------------------------------------------------------------------------- 1 | use crate::constants::{WALK_XCOORDS, WALK_YCOORDS}; 2 | use crate::state::State; 3 | use crate::systems::TileType; 4 | use crate::{ActorState, Error}; 5 | use async_trait::async_trait; 6 | use num_enum::FromPrimitive; 7 | use primitives::Location; 8 | use serde::{Deserialize, Serialize}; 9 | use tq_network::{Actor, PacketID, PacketProcess}; 10 | 11 | use super::{MsgTalk, TalkChannel}; 12 | 13 | #[derive(Debug, FromPrimitive, Copy, Clone)] 14 | #[repr(u8)] 15 | pub enum MovementType { 16 | Walk = 0, 17 | Run = 1, 18 | Shift = 2, 19 | #[num_enum(default)] 20 | Unknwon, 21 | } 22 | 23 | /// This packet encapsulates a character's ground movement on a map. The 24 | /// movement packet specifies the type of movement being performed 25 | /// and the direction the player as it moves on the map. The packet shows 26 | /// movements from actors on the server, and should be sent back to the actor to 27 | /// complete the movement. 28 | #[derive(Debug, Serialize, Deserialize, Clone, PacketID)] 29 | #[packet(id = 1005)] 30 | pub struct MsgWalk { 31 | character_id: u32, 32 | direction: u8, 33 | movement_type: u8, 34 | } 35 | 36 | #[async_trait] 37 | impl PacketProcess for MsgWalk { 38 | type ActorState = ActorState; 39 | type Error = Error; 40 | type State = State; 41 | 42 | /// processes a character movement for the actor. It checks if 43 | /// the movement is valid, then distributes it to observing players. if 44 | /// the movement is invalid, the packet will not be sent back and the actor 45 | /// will be teleported back to the character's original position. 46 | async fn process(&self, state: &Self::State, actor: &Actor) -> Result<(), Self::Error> { 47 | let direction = (self.direction % 8) as usize; 48 | let entity = actor.entity(); 49 | let me = entity.as_character().ok_or(Error::CharacterNotFound)?; 50 | let current_location = me.entity().location(); 51 | let offset = ((WALK_XCOORDS[direction] as u16), (WALK_YCOORDS[direction] as u16)); 52 | let x = current_location.x.wrapping_add(offset.0); 53 | let y = current_location.y.wrapping_add(offset.1); 54 | let map = state.try_map(me.entity().map_id())?; 55 | match map.tile(x, y) { 56 | Some(tile) if tile.access > TileType::Npc => { 57 | // The packet is valid. Assign character data: 58 | // Send the movement back to the message server and client: 59 | me.entity().set_location(Location::new(x, y, direction as _)); 60 | me.set_elevation(tile.elevation); 61 | actor.send(self.clone()).await?; 62 | map.update_region_for(actor.entity()); 63 | let myscreen = actor.screen(); 64 | myscreen.send_movement(state, self.clone()).await?; 65 | }, 66 | Some(_) | None => { 67 | let msg = MsgTalk::from_system(me.id(), TalkChannel::TopLeft, "Invalid Location"); 68 | actor.send(msg).await?; 69 | me.kick_back().await?; 70 | return Ok(()); 71 | }, 72 | }; 73 | Ok(()) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /server/game/src/test_utils.rs: -------------------------------------------------------------------------------- 1 | use futures::future::BoxFuture; 2 | use sqlx::sqlite::SqlitePoolOptions; 3 | use tq_network::Actor; 4 | use tracing_subscriber::prelude::*; 5 | 6 | use crate::entities::Character; 7 | use crate::packets::MsgRegister; 8 | use crate::systems::Screen; 9 | use crate::ActorState; 10 | 11 | pub async fn with_test_env<'a, F>(log_level: tracing::Level, f: F) -> Result<(), crate::Error> 12 | where 13 | F: FnOnce(crate::State, [Actor; 2]) -> BoxFuture<'a, Result<(), crate::Error>>, 14 | { 15 | let root_dir = std::process::Command::new("git") 16 | .args(["rev-parse", "--show-toplevel"]) 17 | .output() 18 | .expect("Failed to run git command") 19 | .stdout; 20 | let root_dir = std::str::from_utf8(&root_dir)?.trim(); 21 | let root_dir = std::path::Path::new(root_dir); 22 | let data_dir = root_dir.join("data"); 23 | std::env::set_var("DATA_LOCATION", data_dir); 24 | 25 | let env_filter = tracing_subscriber::EnvFilter::from_default_env() 26 | .add_directive(format!("tq_db={}", log_level).parse().unwrap()) 27 | .add_directive(format!("tq_serde={}", log_level).parse().unwrap()) 28 | .add_directive(format!("tq_crypto={}", log_level).parse().unwrap()) 29 | .add_directive(format!("tq_codec={}", log_level).parse().unwrap()) 30 | .add_directive(format!("tq_network={}", log_level).parse().unwrap()) 31 | .add_directive(format!("game={}", log_level).parse().unwrap()) 32 | .add_directive(format!("game_server={}", log_level).parse().unwrap()); 33 | let logger = tracing_subscriber::fmt::layer() 34 | .pretty() 35 | .with_target(true) 36 | .with_test_writer(); 37 | tracing_subscriber::registry().with(env_filter).with(logger).init(); 38 | 39 | let pool = SqlitePoolOptions::new() 40 | .max_connections(42) 41 | .min_connections(4) 42 | .connect("sqlite::memory:") 43 | .await?; 44 | // Run database migrations 45 | sqlx::migrate!("../../migrations") 46 | .run(&pool) 47 | .await 48 | .expect("Failed to migrate database"); 49 | let state = crate::State::with_pool(pool).await?; 50 | let actors = [make_test_actor(&state, 1).await?, make_test_actor(&state, 2).await?]; 51 | f(state, actors).await 52 | } 53 | 54 | pub async fn make_test_actor(state: &crate::State, id: usize) -> Result, crate::Error> { 55 | let (tx, _rx) = tokio::sync::mpsc::channel(50); 56 | let actor = Actor::::new(tx); 57 | actor.set_id(id); 58 | let inner_character = MsgRegister::build_character_with( 59 | format!("test{id}"), 60 | crate::packets::BodyType::MuscularMale, 61 | crate::packets::BaseClass::Trojan, 62 | id as _, 63 | 1, 64 | )?; 65 | inner_character.save(state.pool()).await?; 66 | let inner_character = tq_db::character::Character::from_account(state.pool(), id as _) 67 | .await? 68 | .expect("Failed to load character"); 69 | let character = Character::new(actor.handle(), inner_character); 70 | let screen = Screen::new(actor.handle()); 71 | actor.update(character, screen); 72 | state.insert_entity(actor.entity()); 73 | Ok(actor) 74 | } 75 | -------------------------------------------------------------------------------- /server/game/src/state/actor_state.rs: -------------------------------------------------------------------------------- 1 | use std::sync::{Arc, Weak}; 2 | 3 | use arc_swap::ArcSwapOption; 4 | 5 | use crate::entities::{Character, GameEntity}; 6 | use crate::systems::Screen; 7 | use crate::Error; 8 | 9 | #[derive(Debug)] 10 | pub struct ActorState { 11 | entity: ArcSwapOption, 12 | screen: ArcSwapOption, 13 | } 14 | 15 | #[async_trait::async_trait] 16 | impl tq_network::ActorState for ActorState { 17 | fn init() -> Self { 18 | ActorState { 19 | entity: Default::default(), 20 | screen: Default::default(), 21 | } 22 | } 23 | } 24 | 25 | impl ActorState { 26 | pub fn update(&self, character: Character, screen: Screen) { 27 | let screen = Arc::new(screen); 28 | character.set_screen(Arc::downgrade(&screen)); 29 | let character = Arc::new(GameEntity::Character(character)); 30 | // We use Weak references to avoid a circular reference between the 31 | // character and the screen. The screen needs to know about the 32 | // character, and the character needs to know about the screen. However, 33 | // if the character holds a strong reference to the screen, then the 34 | // screen will never be dropped. and the other way around. 35 | // hence, we use a weak reference to the screen and the character. This 36 | // if the screen is dropped, then the character will be dropped as well. 37 | screen.set_character(Arc::downgrade(&character)); 38 | self.entity.store(Some(character)); 39 | self.screen.store(Some(screen)); 40 | } 41 | 42 | pub fn entity(&self) -> Arc { 43 | self.entity.load().clone().expect("state is not empty") 44 | } 45 | 46 | pub fn entity_weak(&self) -> Weak { 47 | let e = self.entity.load().clone(); 48 | match e { 49 | Some(e) => Arc::downgrade(&e), 50 | None => Weak::new(), 51 | } 52 | } 53 | 54 | pub fn try_entity(&self) -> Result, Error> { 55 | self.entity.load().clone().ok_or(Error::CharacterNotFound) 56 | } 57 | 58 | pub fn try_entity_weak(&self) -> Result, Error> { 59 | let character = self.entity.load().clone(); 60 | match character { 61 | Some(character) => Ok(Arc::downgrade(&character)), 62 | None => Err(Error::CharacterNotFound), 63 | } 64 | } 65 | 66 | pub fn screen(&self) -> Arc { 67 | self.screen.load().clone().expect("state is not empty") 68 | } 69 | 70 | pub fn screen_weak(&self) -> Weak { 71 | let screen = self.screen.load().clone(); 72 | match screen { 73 | Some(screen) => Arc::downgrade(&screen), 74 | None => Weak::new(), 75 | } 76 | } 77 | 78 | pub fn try_screen(&self) -> Result, Error> { 79 | self.screen.load().clone().ok_or(Error::ScreenNotFound) 80 | } 81 | 82 | pub fn try_screen_weak(&self) -> Result, Error> { 83 | let screen = self.screen.load().clone(); 84 | match screen { 85 | Some(screen) => Ok(Arc::downgrade(&screen)), 86 | None => Err(Error::ScreenNotFound), 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /server/game/src/packets/msg_user_info.rs: -------------------------------------------------------------------------------- 1 | use crate::entities::Character; 2 | use serde::{Deserialize, Serialize}; 3 | use tq_network::PacketID; 4 | 5 | /// Message defining character information, used to initialize the client 6 | /// interface and game state. Character information is loaded from the game 7 | /// database on login if a character exists. 8 | #[derive(Debug, Serialize, Deserialize, PacketID)] 9 | #[packet(id = 1006)] 10 | pub struct MsgUserInfo { 11 | pub character_id: u32, 12 | mesh: u32, 13 | hair_style: u16, 14 | silver: u32, 15 | cps: u32, 16 | experience: u64, 17 | reserved0: u64, 18 | reserved1: u64, 19 | strength: u16, 20 | agility: u16, 21 | vitality: u16, 22 | spirit: u16, 23 | attribute_points: u16, 24 | health_points: u16, 25 | mana_points: u16, 26 | kill_points: u16, 27 | level: u8, 28 | current_class: u8, 29 | previous_class: u8, 30 | rebirths: u8, 31 | show_name: bool, 32 | /// Number of Strings to follow 33 | /// 1: Character Name 34 | /// 2: Spouse Name 35 | /// Total: 2 36 | list_count: u8, 37 | pub character_name: String, 38 | spouse: String, 39 | } 40 | 41 | impl Default for MsgUserInfo { 42 | fn default() -> Self { 43 | Self { 44 | character_id: 1, 45 | mesh: 1003 + 10000, 46 | hair_style: (3 * 100) + 11, 47 | silver: 100, 48 | cps: 0, 49 | experience: 0, 50 | reserved0: 0, 51 | reserved1: 0, 52 | strength: 4, 53 | agility: 6, 54 | vitality: 12, 55 | spirit: 6, 56 | attribute_points: 0, 57 | health_points: 318, 58 | mana_points: 0, 59 | kill_points: 0, 60 | level: 1, 61 | current_class: 10, 62 | previous_class: 0, 63 | rebirths: 0, 64 | show_name: true, 65 | list_count: 2, 66 | character_name: "Test".into(), 67 | spouse: "None".to_string(), 68 | } 69 | } 70 | } 71 | 72 | impl From<&Character> for MsgUserInfo { 73 | fn from(c: &Character) -> Self { 74 | Self { 75 | character_id: c.id(), 76 | mesh: (c.entity().mesh() + (c.avatar() as u32 * 10_000)), 77 | hair_style: c.hair_style(), 78 | silver: c.silver() as u32, 79 | cps: c.cps() as u32, 80 | experience: c.experience(), 81 | reserved0: 0, 82 | reserved1: 0, 83 | strength: c.strength(), 84 | agility: c.agility(), 85 | vitality: c.vitality(), 86 | spirit: c.spirit(), 87 | attribute_points: c.attribute_points(), 88 | health_points: c.health_points(), 89 | mana_points: c.mana_points(), 90 | kill_points: c.kill_points(), 91 | level: c.entity().level() as u8, 92 | current_class: c.current_class(), 93 | previous_class: c.previous_class(), 94 | rebirths: c.rebirths(), 95 | show_name: true, 96 | list_count: 2, 97 | character_name: c.entity().name().to_owned(), 98 | spouse: "None".to_owned(), 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /server/game/src/packets/msg_npc.rs: -------------------------------------------------------------------------------- 1 | use num_enum::{FromPrimitive, IntoPrimitive}; 2 | use serde::Deserialize; 3 | use tq_network::{Actor, PacketID, PacketProcess}; 4 | 5 | use crate::entities::NpcKind; 6 | use crate::packets::{MsgAction, MsgTalk, MsgTaskDialog}; 7 | 8 | #[derive(Default, Debug, Clone, Copy, FromPrimitive, IntoPrimitive)] 9 | #[repr(u16)] 10 | pub enum NpcActionKind { 11 | #[default] 12 | Activate = 0, 13 | AddNpc = 1, 14 | LeaveMap = 2, 15 | DeleteNpc = 3, 16 | ChangePosition = 4, 17 | LayNpc = 5, 18 | CancelInteraction = 255, 19 | } 20 | 21 | /// This packet is used to interact with a NPC and contains multiple 22 | /// DialogAction types that are used to determine the type of interaction. 23 | #[derive(Debug, Deserialize, Clone, PacketID)] 24 | #[packet(id = 2031)] 25 | pub struct MsgNpc { 26 | npc_id: u32, 27 | data: u32, 28 | action: u16, 29 | kind: u16, 30 | } 31 | 32 | #[async_trait::async_trait] 33 | impl PacketProcess for MsgNpc { 34 | type ActorState = crate::ActorState; 35 | type Error = crate::Error; 36 | type State = crate::State; 37 | 38 | async fn process(&self, state: &Self::State, actor: &Actor) -> Result<(), Self::Error> { 39 | tracing::debug!( 40 | npc_id = self.npc_id, 41 | data = self.data, 42 | action = ?NpcActionKind::from(self.action), 43 | kind = ?NpcKind::from(self.kind as u8), 44 | "MsgNpc received" 45 | ); 46 | let me = actor.entity(); 47 | let mycharacter = me.as_character().ok_or(crate::Error::CharacterNotFound)?; 48 | let mymap = state.try_map(me.basic().map_id())?; 49 | let npc = match mymap.npc(self.npc_id) { 50 | Some(npc) => npc, 51 | None => { 52 | actor 53 | .send(MsgTalk::from_system( 54 | me.id(), 55 | super::TalkChannel::System, 56 | format!("NPC {} not found", self.npc_id), 57 | )) 58 | .await?; 59 | return Ok(()); 60 | }, 61 | }; 62 | let my_loc = me.basic().location(); 63 | let npc_loc = npc.entity().location(); 64 | let in_screen = tq_math::in_screen(my_loc.into(), npc_loc.into()); 65 | if !in_screen { 66 | // TODO: Player is using some kind of hack to interact with NPCs 67 | // that are not in the screen. 68 | return Ok(()); 69 | } 70 | // Storage NPCs 71 | if npc.is_storage() { 72 | actor 73 | .send(MsgAction::from_character( 74 | mycharacter, 75 | 4, // WHAT IS THIS? 76 | super::ActionType::OpenDialog, 77 | )) 78 | .await?; 79 | return Ok(()); 80 | } 81 | let npc_id = npc.id(); 82 | let npc_name = npc.entity().name(); 83 | // For now, lets try sending a dummy dialog 84 | let dialog = MsgTaskDialog::builder() 85 | .text(format!("Hello, My name is {npc_name} and my id is {npc_id}",)) 86 | .with_edit(1, "What is your name?") 87 | .with_option(255, "Nice to meet you") 88 | .and() 89 | .with_avatar(47) 90 | .build(); 91 | actor.send_all(dialog).await?; 92 | Ok(()) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of 9 | experience, 10 | education, socio-economic status, nationality, personal appearance, race, 11 | religion, or sexual identity and orientation. 12 | 13 | ## Our Standards 14 | 15 | Examples of behavior that contributes to creating a positive environment 16 | include: 17 | 18 | - Using welcoming and inclusive language 19 | - Being respectful of differing viewpoints and experiences 20 | - Gracefully accepting constructive criticism 21 | - Focusing on what is best for the community 22 | - Showing empathy towards other community members 23 | 24 | Examples of unacceptable behavior by participants include: 25 | 26 | - The use of sexualized language or imagery and unwelcome sexual attention or 27 | advances 28 | - Trolling, insulting/derogatory comments, and personal or political attacks 29 | - Public or private harassment 30 | - Publishing others' private information, such as a physical or electronic 31 | address, without explicit permission 32 | - Other conduct which could reasonably be considered inappropriate in a 33 | professional setting 34 | 35 | ## Our Responsibilities 36 | 37 | Project maintainers are responsible for clarifying the standards of acceptable 38 | behavior and are expected to take appropriate and fair corrective action in 39 | response to any instances of unacceptable behavior. 40 | 41 | Project maintainers have the right and responsibility to remove, edit, or 42 | reject comments, commits, code, wiki edits, issues, and other contributions 43 | that are not aligned to this Code of Conduct, or to ban temporarily or 44 | permanently any contributor for other behaviors that they deem inappropriate, 45 | threatening, offensive, or harmful. 46 | 47 | ## Scope 48 | 49 | This Code of Conduct applies both within project spaces and in public spaces 50 | when an individual is representing the project or its community. Examples of 51 | representing a project or community include using an official project e-mail 52 | address, posting via an official social media account, or acting as an appointed 53 | representative at an online or offline event. Representation of a project may be 54 | further defined and clarified by project maintainers. 55 | 56 | ## Enforcement 57 | 58 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 59 | reported by contacting the project team at yoshuawuyts@gmail.com, or through 60 | IRC. All complaints will be reviewed and investigated and will result in a 61 | response that is deemed necessary and appropriate to the circumstances. The 62 | project team is obligated to maintain confidentiality with regard to the 63 | reporter of an incident. 64 | Further details of specific enforcement policies may be posted separately. 65 | 66 | Project maintainers who do not follow or enforce the Code of Conduct in good 67 | faith may face temporary or permanent repercussions as determined by other 68 | members of the project's leadership. 69 | 70 | ## Attribution 71 | 72 | This Code of Conduct is adapted from the Contributor Covenant, version 1.4, 73 | available at 74 | https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 75 | -------------------------------------------------------------------------------- /server/auth/src/error.rs: -------------------------------------------------------------------------------- 1 | use bytes::Bytes; 2 | use tq_network::{ErrorPacket, PacketEncode}; 3 | 4 | #[derive(Debug)] 5 | pub enum Error { 6 | Wasmtime(wasmtime::Error), 7 | Network(tq_network::Error), 8 | #[cfg(feature = "server")] 9 | Server(tq_server::Error), 10 | IO(std::io::Error), 11 | DotEnv(dotenvy::Error), 12 | Env(std::env::VarError), 13 | Sqlx(sqlx::Error), 14 | Db(tq_db::Error), 15 | State(&'static str), 16 | Other(String), 17 | Msg(u16, Bytes), 18 | ActorNotFound, 19 | InvalidPacket, 20 | } 21 | 22 | impl From for Error { 23 | fn from(v: tq_db::Error) -> Self { 24 | Self::Db(v) 25 | } 26 | } 27 | 28 | impl From for Error { 29 | fn from(v: sqlx::Error) -> Self { 30 | Self::Sqlx(v) 31 | } 32 | } 33 | 34 | impl From for Error { 35 | fn from(v: std::env::VarError) -> Self { 36 | Self::Env(v) 37 | } 38 | } 39 | 40 | impl From for Error { 41 | fn from(v: dotenvy::Error) -> Self { 42 | Self::DotEnv(v) 43 | } 44 | } 45 | 46 | impl From for Error { 47 | fn from(v: std::io::Error) -> Self { 48 | Self::IO(v) 49 | } 50 | } 51 | 52 | #[cfg(feature = "server")] 53 | impl From for Error { 54 | fn from(v: tq_server::Error) -> Self { 55 | Self::Server(v) 56 | } 57 | } 58 | 59 | impl From for Error { 60 | fn from(v: tq_network::Error) -> Self { 61 | Self::Network(v) 62 | } 63 | } 64 | 65 | impl From for Error { 66 | fn from(v: wasmtime::Error) -> Self { 67 | Self::Wasmtime(v) 68 | } 69 | } 70 | 71 | impl std::error::Error for Error {} 72 | 73 | impl core::fmt::Display for Error { 74 | fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 75 | match self { 76 | Self::Wasmtime(e) => write!(f, "Wasmtime error: {}", e), 77 | Self::Network(e) => write!(f, "Network error: {}", e), 78 | #[cfg(feature = "server")] 79 | Self::Server(e) => write!(f, "Server error: {}", e), 80 | Self::IO(e) => write!(f, "IO error: {}", e), 81 | Self::DotEnv(e) => write!(f, "DotEnv error: {}", e), 82 | Self::Env(e) => write!(f, "Env error: {}", e), 83 | Self::Sqlx(e) => write!(f, "Sqlx error: {}", e), 84 | Self::Db(e) => write!(f, "Db error: {}", e), 85 | Self::State(e) => write!(f, "State error: {}", e), 86 | Self::Other(e) => write!(f, "{}", e), 87 | Self::Msg(id, bytes) => { 88 | write!(f, "Error packet: id = {}, body = {:?}", id, bytes) 89 | }, 90 | Self::ActorNotFound => write!(f, "Actor Not Found"), 91 | Self::InvalidPacket => write!(f, "Invalid Packet"), 92 | } 93 | } 94 | } 95 | 96 | impl From> for Error { 97 | fn from(v: ErrorPacket) -> Self { 98 | let (id, bytes) = v.0.encode().unwrap(); 99 | Self::Msg(id, bytes) 100 | } 101 | } 102 | 103 | impl PacketEncode for Error { 104 | type Error = Self; 105 | type Packet = (); 106 | 107 | fn encode(&self) -> Result<(u16, Bytes), Self::Error> { 108 | match self { 109 | Self::Msg(id, bytes) => Ok((*id, bytes.clone())), 110 | e => Err(Self::Other(e.to_string())), 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /server/game/src/entities/npc.rs: -------------------------------------------------------------------------------- 1 | use crate::entities::Entity; 2 | use crate::packets::MsgNpcInfo; 3 | use crate::Error; 4 | use num_enum::{FromPrimitive, IntoPrimitive}; 5 | use tq_network::ActorHandle; 6 | 7 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Default, FromPrimitive, IntoPrimitive)] 8 | #[repr(u8)] 9 | pub enum NpcKind { 10 | #[default] 11 | None = 0, 12 | ShopKeeper = 1, 13 | Task = 2, 14 | Storage = 3, 15 | Trunck = 4, 16 | Face = 5, 17 | Forge = 6, 18 | Embed = 7, 19 | 20 | Statuary = 9, 21 | SynFlag = 10, 22 | 23 | Booth = 14, 24 | SynTrans = 15, 25 | BoothFlag = 16, 26 | 27 | Dice = 19, // Game 28 | 29 | WeaponGoal = 21, 30 | MagicGoal = 22, 31 | BowGoal = 23, 32 | Target = 24, 33 | Furniture = 25, 34 | CityGate = 26, 35 | NeighbourGate = 27, 36 | } 37 | 38 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Default, FromPrimitive, IntoPrimitive)] 39 | #[repr(u8)] 40 | pub enum NpcSort { 41 | #[default] 42 | None = 0, 43 | Task = 1 << 0, 44 | Recycle = 1 << 1, 45 | Scene = 1 << 2, 46 | LinkMap = 1 << 3, 47 | DieAction = 1 << 4, 48 | DelEnable = 1 << 5, 49 | Event = 1 << 6, 50 | Table = 1 << 7, 51 | } 52 | 53 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Default, FromPrimitive, IntoPrimitive)] 54 | #[repr(u8)] 55 | pub enum NpcBase { 56 | #[default] 57 | None = 0, 58 | Lamp = 1, 59 | LowShelf = 2, 60 | Cabinet = 3, 61 | HighShelf = 4, 62 | BombeChest = 5, 63 | RosewoodCabinet = 6, 64 | HighCabinet = 7, 65 | FoldingScreen = 8, 66 | Dresser = 9, 67 | BasinRack = 10, 68 | Chair = 11, 69 | EndTable = 12, 70 | LeftGate = 24, 71 | RightGate = 27, 72 | } 73 | 74 | #[derive(Debug)] 75 | pub struct Npc { 76 | inner: tq_db::npc::Npc, 77 | entity: Entity, 78 | kind: NpcKind, 79 | sort: NpcSort, 80 | base: NpcBase, 81 | } 82 | 83 | impl Npc { 84 | pub fn new(inner: tq_db::npc::Npc) -> Self { 85 | let entity = Entity::from(&inner); 86 | let kind = NpcKind::from(inner.kind as u8); 87 | let sort = NpcSort::from(inner.sort as u8); 88 | let base = NpcBase::from(inner.base as u8); 89 | Self { 90 | entity, 91 | inner, 92 | kind, 93 | sort, 94 | base, 95 | } 96 | } 97 | 98 | #[inline] 99 | pub fn id(&self) -> u32 { 100 | self.inner.id as _ 101 | } 102 | 103 | pub fn kind(&self) -> NpcKind { 104 | self.kind 105 | } 106 | 107 | pub fn sort(&self) -> NpcSort { 108 | self.sort 109 | } 110 | 111 | pub fn base(&self) -> NpcBase { 112 | self.base 113 | } 114 | 115 | #[inline] 116 | pub fn entity(&self) -> &Entity { 117 | &self.entity 118 | } 119 | 120 | pub fn is_shopkeeper(&self) -> bool { 121 | self.kind == NpcKind::ShopKeeper 122 | } 123 | 124 | pub fn is_storage(&self) -> bool { 125 | self.kind == NpcKind::Storage 126 | } 127 | 128 | pub fn is_booth(&self) -> bool { 129 | self.kind == NpcKind::Booth 130 | } 131 | 132 | #[tracing::instrument(skip(self, to), fields(npc = self.entity.id()))] 133 | pub(super) async fn send_spawn(&self, to: &ActorHandle) -> Result<(), Error> { 134 | let msg = if self.is_booth() { 135 | MsgNpcInfo::from_npc_with_name(self) 136 | } else { 137 | MsgNpcInfo::new(self) 138 | }; 139 | to.send(msg).await?; 140 | tracing::trace!("sent spawn"); 141 | Ok(()) 142 | } 143 | } 144 | 145 | impl From for Npc { 146 | fn from(value: tq_db::npc::Npc) -> Self { 147 | Self::new(value) 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /data/Maps/Maps.csv: -------------------------------------------------------------------------------- 1 | Uid,Name,Id,Flags,Weather,PortalX,PortalY,RebornMap,Color 2 | 601,OfflineTG,601,0074,0,65,55,601,4294967295 3 | 700,Lottery,700,0588,0,40,47,700,4294967295 4 | 1000,Desert,1000,8192,0,499,650,1000,4294967295 5 | 1001,AncientCity,1001,8192,0,0,0,1000,4294967295 6 | 1002,Plain,1002,24576,0,430,380,1002,4294967295 7 | 1004,JobCenter,1004,8200,0,0,0,1002,4294967295 8 | 1005,Arena,1005,0001,0,51,50,1005,4294967295 9 | 1006,Stable,1006,8200,0,37,31,1002,4294967295 10 | 1007,Blachsmith,1007,8192,0,25,24,1002,4294967295 11 | 1008,Grocery,1008,8206,0,22,25,1002,4294967295 12 | 1010,BirthVillage,1010,0,0,111,98,1010,4294967295 13 | 1011,Forest,1011,24576,0,193,266,1011,4294967295 14 | 1012,Dreamland,1012,8192,0,25,24,1020,4294967295 15 | 1013,TigerCave,1013,8192,0,22,25,1011,4294967295 16 | 1014,DragonPool,1014,8192,0,22,25,1011,4294967295 17 | 1015,BirdIsland,1015,8192,2,717,577,1015,4294967295 18 | 1016,KylinCave,1016,8192,0,22,25,1011,4294967295 19 | 1020,ApeMountain,1020,24576,3,566,565,1020,4294967295 20 | 1023,CrystalMine-1,1023,10242,10,0,0,1000,4294967295 21 | 1024,CrystalMine-2,1024,10242,10,0,0,1000,4294967295 22 | 1025,CopperMine,1025,10242,10,0,0,1011,4294967295 23 | 1026,SilverMine,1026,10242,10,0,0,1020,4294967295 24 | 1027,GoldMine,1027,10242,10,0,0,1000,4294967295 25 | 1028,MineCave,1028,10242,10,0,0,1002,4294967295 26 | 1036,Market,1036,0030,3,303,237,1036,4294967295 27 | 1038,GuildArea,1038,0130,0,0,0,1002,4294967295 28 | 1039,TrainingGround,1039,0013,0,29,72,1002,4294967295 29 | 1042,MoonBox,1042,8192,0,60,108,1002,4294967295 30 | 1043,Peace,1043,8258,0,60,108,1002,4294967295 31 | 1044,Chaos,1043,8258,0,60,108,1002,4294967295 32 | 1045,Deserted,1043,8258,0,60,108,1002,4294967295 33 | 1046,Prosperous,1043,8258,0,60,108,1002,4294967295 34 | 1047,Disturbed,1043,8258,0,60,108,1002,4294967295 35 | 1048,Calmed,1043,8258,0,60,108,1002,4294967295 36 | 1052,AncientDevil,1082,8192,0,724,573,1015,4294967295 37 | 1063,SnakeIslands,1063,492,0,449,358,1015,4294967295 38 | 1075,NewCanyon,1075,8192,3,566,565,1020,4294967295 39 | 1076,NewForest,1076,8192,3,301,488,1076,4294967295 40 | 1077,NewDesert,1077,8192,2,447,347,1077,4294967295 41 | 1081,PKTournement,1018,0007,0,430,380,1002,4294967295 42 | 1090,PKTournement,1090,0007,0,430,380,1002,4294967295 43 | 1091,PKTournement,1091,0007,0,430,380,1002,4294967295 44 | 1201,Adventure-4,1201,0000,0,447,347,1077,4294967295 45 | 1202,Adventure-3,1202,0000,0,447,347,1077,4294967295 46 | 1204,Adventure-6,1204,0000,0,448,272,1213,4294967295 47 | 1205,Adventure-2,1205,0000,0,447,347,1077,4294967295 48 | 1207,Adventure-7.4,1207,0000,0,448,272,1213,4294967295 49 | 1208,Adventure-7.5,1208,0000,0,448,272,1213,4294967295 50 | 1210,Adventure-8,1210,0064,0,1017,1294,1219,4294967295 51 | 1211,Adventure-9,1211,0064,0,1017,1294,1219,4294967295 52 | 1212,Adventure-10,1212,0064,0,1017,1294,1219,4294967295 53 | 1213,Adventure-5,1213,0000,0,448,272,1213,4294967295 54 | 1214,Adventure-7.1,1214,0000,0,448,272,1213,4294967295 55 | 1215,Adventure-7.3,1215,0000,0,448,272,1213,4294967295 56 | 1216,Adventure-7,1216,0000,0,448,272,1213,4294967295 57 | 1217,Adventure-7.2,1075,0000,0,448,272,1213,4294967295 58 | 1218,Adventure-8.1,1025,2114,10,1003,1278,1219,4294967295 59 | 1219,Adventure-11,1212,0064,0,1003,1278,1213,4294967295 60 | 1300,MysticCave,1001,8194,0,0,0,1000,4294967295 61 | 1351,Labyrinth1F,1351,8194,0,430,380,1002,4294967295 62 | 1352,Labyrinth2F,1352,8194,0,430,380,1002,4294967295 63 | 1353,Labyrinth3F,1353,8194,0,430,380,1002,4294967295 64 | 1354,Labyrinth4F,1354,8194,0,430,380,1002,4294967295 65 | 1511,FurnitureStore,1004,8200,0,430,380,1002,4294967295 66 | 2021,HellGate,2021,8198,0,566,565,1020,4294967295 67 | 2022,HellHall,2022,8198,0,566,565,1020,4294967295 68 | 2023,Cloister,2023,8198,0,566,565,1020,4294967295 69 | 2024,BattleFormation,2024,4,0,566,565,1020,4294967295 70 | 4000,IceCrypt-1,4000,8192,3,0,0,1000,4294967295 71 | 4001,IceCrypt-2,4001,8192,3,0,0,1000,4294967295 72 | 6000,BlackJail,6000,10594,0,29,72,6000,4294967295 73 | 6001,BotJail,6000,8266,0,29,72,6001,4294967295 74 | 7001,FreeForAll,7011,0101,0,51,50,7001,4294967295 75 | 7002,CageMatch,1005,0613,0,51,50,7002,4294967295 76 | -------------------------------------------------------------------------------- /server/game/src/systems/commands.rs: -------------------------------------------------------------------------------- 1 | use crate::packets::{MsgTalk, TalkChannel}; 2 | use crate::world::Maps; 3 | use crate::{ActorState, Error}; 4 | use argh::FromArgs; 5 | use tq_network::Actor; 6 | 7 | pub async fn parse_and_execute(state: &crate::State, actor: &Actor, args: &[&str]) -> Result<(), Error> { 8 | let entity = actor.entity(); 9 | let me = entity.as_character().ok_or(Error::CharacterNotFound)?; 10 | let c = match Command::from_args(&["commands"], args) { 11 | Ok(cmd) => cmd, 12 | Err(e) => { 13 | let lines = e.output.lines().map(|e| e.to_owned()).skip_while(|e| e.is_empty()); 14 | for line in lines { 15 | actor 16 | .send(MsgTalk::from_system(me.id(), TalkChannel::System, line)) 17 | .await?; 18 | } 19 | return Ok(()); 20 | }, 21 | }; 22 | match c.commands { 23 | SubCommands::Dc(_) => { 24 | actor.shutdown().await?; 25 | Ok(()) 26 | }, 27 | SubCommands::Teleport(info) => { 28 | let old_map = state.try_map(me.entity().map_id())?; 29 | let map = state.try_map(info.map_id)?; 30 | me.teleport(state, info.map_id, (info.x, info.y)).await?; 31 | map.insert_entity(actor.entity()).await?; 32 | old_map.remove_entity(&actor.entity())?; 33 | if info.all { 34 | // TODO: teleport all 35 | } 36 | Ok(()) 37 | }, 38 | SubCommands::Which(which) => { 39 | if which.map { 40 | let map_id = me.entity().map_id(); 41 | actor 42 | .send(MsgTalk::from_system( 43 | me.id(), 44 | TalkChannel::System, 45 | format!("Current Map: {:?} = {}", Maps::from(map_id), map_id), 46 | )) 47 | .await?; 48 | } 49 | Ok(()) 50 | }, 51 | SubCommands::JumpBack(_) => { 52 | me.kick_back().await?; 53 | Ok(()) 54 | }, 55 | SubCommands::Weather(weather) => { 56 | let map = state.try_map(me.entity().map_id())?; 57 | map.change_weather(weather.kind.into()).await?; 58 | Ok(()) 59 | }, 60 | } 61 | } 62 | 63 | /// In Game Commands 64 | #[derive(Debug, Clone, PartialEq, FromArgs)] 65 | struct Command { 66 | #[argh(subcommand)] 67 | commands: SubCommands, 68 | } 69 | 70 | #[derive(Debug, Clone, PartialEq, FromArgs)] 71 | #[argh(subcommand)] 72 | enum SubCommands { 73 | Dc(DcCmd), 74 | Which(WhichCmd), 75 | Teleport(TeleportCmd), 76 | JumpBack(JumpBackCmd), 77 | Weather(WeatherCmd), 78 | } 79 | 80 | /// Disconnect From Server 81 | #[derive(Debug, Clone, PartialEq, FromArgs)] 82 | #[argh(subcommand, name = "dc")] 83 | struct DcCmd {} 84 | 85 | /// Jump Back to prev location 86 | #[derive(Debug, Clone, PartialEq, FromArgs)] 87 | #[argh(subcommand, name = "jump-back")] 88 | struct JumpBackCmd {} 89 | 90 | /// Ask about things in your environment 91 | #[derive(Debug, Clone, PartialEq, FromArgs)] 92 | #[argh(subcommand, name = "which")] 93 | struct WhichCmd { 94 | /// get your current map (ID, Name) 95 | #[argh(switch)] 96 | map: bool, 97 | } 98 | 99 | /// Teleport to other map at specific location 100 | #[derive(Debug, Clone, PartialEq, FromArgs)] 101 | #[argh(subcommand, name = "tele")] 102 | struct TeleportCmd { 103 | #[argh(positional)] 104 | map_id: u32, 105 | #[argh(positional)] 106 | x: u16, 107 | #[argh(positional)] 108 | y: u16, 109 | /// teleport all characters with you 110 | #[argh(option, default = "false")] 111 | all: bool, 112 | } 113 | 114 | /// Change the current map's weather 115 | #[derive(Debug, Clone, PartialEq, FromArgs)] 116 | #[argh(subcommand, name = "weather")] 117 | struct WeatherCmd { 118 | /// weather type 119 | #[argh(positional)] 120 | kind: u32, 121 | } 122 | -------------------------------------------------------------------------------- /packets/transfer/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(not(feature = "std"), no_std)] 2 | 3 | #[cfg(not(feature = "std"))] 4 | extern crate alloc; 5 | 6 | include!(concat!(env!("OUT_DIR"), "/wasm.rs")); 7 | 8 | use msg_connect_ex::{AccountCredentials, RejectionCode}; 9 | use serde::{Deserialize, Serialize}; 10 | use tq_bindings::{host, Resource}; 11 | use tq_network::{ActorHandle, ErrorPacket, IntoErrorPacket, PacketEncode, PacketID}; 12 | 13 | /// Defines account parameters to be transferred from the account server to the 14 | /// game server. Account information is supplied from the account database, and 15 | /// used on the game server to transfer authentication and authority level. 16 | #[derive(Clone, Default, Debug, Deserialize, Serialize, PacketID)] 17 | #[packet(id = 4001)] 18 | pub struct MsgTransfer { 19 | pub account_id: u32, 20 | pub realm_id: u32, 21 | pub token: u64, 22 | } 23 | 24 | impl MsgTransfer { 25 | pub fn handle(actor: &Resource, realm: &str) -> Result { 26 | let maybe_realm = host::db::realm::by_name(realm)?; 27 | // Check if there is a realm with that name 28 | let realm = match maybe_realm { 29 | Some(realm) => realm, 30 | None => { 31 | return Err(RejectionCode::ServerLocked.packet().error_packet().into()); 32 | }, 33 | }; 34 | // Try to connect to that realm first. 35 | if let Err(e) = host::auth::server_bus::check(&realm) { 36 | tracing::error!( 37 | ip = realm.game_ip_address, 38 | port = realm.game_port, 39 | realm_id = realm.realm_id, 40 | error = ?e, 41 | "Failed to connect to realm" 42 | ); 43 | host::network::actor::send(actor, RejectionCode::ServerDown.packet())?; 44 | host::network::actor::shutdown(actor); 45 | return Err(e.into()); 46 | } 47 | 48 | let res = host::auth::server_bus::transfer(actor, &realm); 49 | match res { 50 | Ok(token) => Ok(AccountCredentials { 51 | token, 52 | server_ip: realm.game_ip_address, 53 | server_port: realm.game_port as u32, 54 | }), 55 | Err(e) => { 56 | tracing::error!( 57 | ip = realm.game_ip_address, 58 | port = realm.game_port, 59 | realm_id = realm.realm_id, 60 | error = ?e, 61 | "Failed to transfer account" 62 | ); 63 | Err(RejectionCode::ServerTimedOut.packet().error_packet().into()) 64 | }, 65 | } 66 | } 67 | } 68 | 69 | /// Possible errors that can occur while processing a packet. 70 | #[derive(Debug, thiserror::Error)] 71 | pub enum Error { 72 | /// Failed to generate a login token. 73 | #[error("Failed to generate login token")] 74 | TokenGenerationFailed, 75 | /// The realm is unavailable. 76 | #[error("Realm unavailable")] 77 | RealmUnavailable, 78 | /// Internal Network error. 79 | #[error(transparent)] 80 | Network(#[from] tq_network::Error), 81 | #[error(transparent)] 82 | Db(#[from] tq_db::Error), 83 | #[error("Error packet: {0:?}")] 84 | /// An error packet to be sent to the client. 85 | Msg(u16, bytes::Bytes), 86 | } 87 | 88 | impl From> for Error { 89 | fn from(v: ErrorPacket) -> Self { 90 | let (id, bytes) = v.0.encode().unwrap(); 91 | Self::Msg(id, bytes) 92 | } 93 | } 94 | 95 | #[tq_network::packet_processor(MsgTransfer)] 96 | pub fn process(msg: MsgTransfer, actor: &Resource) -> Result<(), crate::Error> { 97 | let token = host::game::state::generate_login_token(actor, msg.account_id, msg.realm_id); 98 | let msg = MsgTransfer { 99 | account_id: msg.account_id, 100 | realm_id: msg.realm_id, 101 | token, 102 | }; 103 | host::network::actor::send(actor, msg)?; 104 | host::network::actor::shutdown(actor); 105 | Ok(()) 106 | } 107 | -------------------------------------------------------------------------------- /server/game/src/packets/msg_item.rs: -------------------------------------------------------------------------------- 1 | use super::{MsgTalk, TalkChannel}; 2 | use crate::state::State; 3 | use crate::ActorState; 4 | use async_trait::async_trait; 5 | use num_enum::{FromPrimitive, IntoPrimitive}; 6 | use serde::{Deserialize, Serialize}; 7 | use tq_network::{Actor, PacketID, PacketProcess}; 8 | 9 | /// Enumeration type for defining item actions that may be requested by the 10 | /// user, or given to by the server. Allows for action handling as a packet 11 | /// subtype. Enums should be named by the action they provide to a system in the 12 | /// context of the player item. 13 | #[derive(Default, Debug, FromPrimitive, IntoPrimitive, Clone, Copy)] 14 | #[repr(u32)] 15 | enum ItemActionType { 16 | #[default] 17 | Unknown, 18 | Buy = 1, 19 | Sell = 2, 20 | Drop = 3, 21 | Use = 4, 22 | Equip = 5, 23 | Unequip = 6, 24 | SplitItem = 7, 25 | CombineItem = 8, 26 | QueryMoneySaved = 9, 27 | SaveMoney = 10, 28 | DrawMoney = 11, 29 | DropMoney = 12, 30 | SpendMoney = 13, 31 | Repair = 14, 32 | RepairAll = 15, 33 | Ident = 16, 34 | Durability = 17, 35 | DropEquipement = 18, 36 | Improve = 19, 37 | UpLevel = 20, 38 | BoothQuery = 21, 39 | BoothAdd = 22, 40 | BoothDel = 23, 41 | BoothBuy = 24, 42 | SynchroAmount = 25, 43 | Fireworks = 26, 44 | Ping = 27, 45 | Enchant = 28, 46 | BoothAddCPs = 29, 47 | } 48 | 49 | /// Message containing an item action command. Item actions are usually 50 | /// performed to manage player equipment, inventory, money, or item shop 51 | /// purchases and sales. It is serves a second purpose for measuring client 52 | /// ping. 53 | #[derive(Debug, Serialize, Deserialize, Clone, PacketID)] 54 | #[packet(id = 1009)] 55 | pub struct MsgItem { 56 | character_id: u32, 57 | param0: u32, 58 | action_type: u32, 59 | client_timestamp: u32, 60 | param1: u32, 61 | } 62 | 63 | #[async_trait] 64 | impl PacketProcess for MsgItem { 65 | type ActorState = ActorState; 66 | type Error = crate::Error; 67 | type State = State; 68 | 69 | async fn process(&self, _state: &Self::State, actor: &Actor) -> Result<(), Self::Error> { 70 | let action = self.action_type.into(); 71 | match action { 72 | ItemActionType::Ping => { 73 | // a bit hacky, just testing it out. 74 | // what if we missed with the client timestamp? 75 | // does this yield a negative value? let's find out. 76 | // lets add 30ms from the client timestamp, so when 77 | // the client receives the packet, it can calculate 78 | // the round trip time. 79 | let msg = MsgItem { 80 | character_id: self.character_id, 81 | param0: self.param0, 82 | action_type: self.action_type, 83 | client_timestamp: self.client_timestamp + 30, 84 | param1: self.param1, 85 | }; 86 | // LMFAO, this is so bad. it actually made the ping appear 87 | // negative. I'm not sure if this is a bug in the client 88 | // or if it's a bug in the server. I'm going to remove this 89 | // later, but I'm going to leave it here for now. 90 | actor.send(msg).await?; 91 | }, 92 | _ => { 93 | actor.send(self.clone()).await?; 94 | let p = MsgTalk::from_system( 95 | self.character_id, 96 | TalkChannel::Service, 97 | format!("Missing Item Action Type {:?}", action), 98 | ); 99 | tracing::warn!( 100 | ?action, 101 | param0 = %self.param0, 102 | param1 = %self.param1, 103 | action_id = self.action_type, 104 | "Missing Item Action Type", 105 | ); 106 | actor.send(p).await?; 107 | }, 108 | } 109 | Ok(()) 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /crates/primitives/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Primitives used by the game engine and game server. 2 | #![cfg_attr(not(feature = "std"), no_std)] 3 | 4 | use bytemuck::NoUninit; 5 | use core::fmt; 6 | use num_traits::PrimInt; 7 | 8 | #[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Default)] 9 | pub struct Size { 10 | pub width: I, 11 | pub height: I, 12 | } 13 | 14 | impl Size { 15 | pub const fn new(width: I, height: I) -> Self { 16 | Self { width, height } 17 | } 18 | 19 | pub fn area(&self) -> I { 20 | self.width * self.height 21 | } 22 | } 23 | 24 | impl fmt::Display for Size { 25 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 26 | write!(f, "{}x{}", self.width, self.height) 27 | } 28 | } 29 | 30 | #[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Default)] 31 | pub struct Point { 32 | pub x: I, 33 | pub y: I, 34 | } 35 | 36 | impl Point { 37 | pub fn new(x: I, y: I) -> Self { 38 | Self { x, y } 39 | } 40 | } 41 | 42 | impl fmt::Display for Point { 43 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 44 | write!(f, "({},{})", self.x, self.y) 45 | } 46 | } 47 | 48 | #[derive(Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Default, NoUninit)] 49 | #[repr(C, align(8))] 50 | pub struct Location { 51 | /// Entity X coordinate 52 | pub x: u16, 53 | /// Entity Y coordinate 54 | pub y: u16, 55 | /// Entity direction 56 | pub direction: u8, 57 | 58 | /// Padding to align to 8 bytes 59 | _padding: [u8; 3], 60 | } 61 | 62 | impl fmt::Debug for Location { 63 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 64 | f.debug_struct("Location") 65 | .field("x", &self.x) 66 | .field("y", &self.y) 67 | .field("direction", &self.direction) 68 | .finish() 69 | } 70 | } 71 | 72 | impl Location { 73 | pub fn new(x: u16, y: u16, direction: u8) -> Self { 74 | Self { 75 | x, 76 | y, 77 | direction, 78 | _padding: [0; 3], 79 | } 80 | } 81 | } 82 | 83 | impl From<(u16, u16, u8)> for Location { 84 | fn from((x, y, direction): (u16, u16, u8)) -> Self { 85 | Self::new(x, y, direction) 86 | } 87 | } 88 | 89 | impl From<(u16, u16)> for Location { 90 | fn from((x, y): (u16, u16)) -> Self { 91 | Self::new(x, y, 0) 92 | } 93 | } 94 | 95 | impl From for (u16, u16, u8) { 96 | fn from(location: Location) -> Self { 97 | (location.x, location.y, location.direction) 98 | } 99 | } 100 | 101 | impl From for (u16, u16) { 102 | fn from(location: Location) -> Self { 103 | (location.x, location.y) 104 | } 105 | } 106 | 107 | /// A Gauge is a value that can be incremented and decremented, but never 108 | /// exceeds a maximum value. 109 | /// 110 | /// Gauges are used to represent health, mana, and stamina. 111 | #[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Default, NoUninit)] 112 | #[repr(C, align(4))] 113 | pub struct Gauge { 114 | /// Current value 115 | pub current: u16, 116 | /// Maximum value 117 | pub max: u16, 118 | } 119 | 120 | impl Gauge { 121 | pub fn new(current: u16, max: u16) -> Self { 122 | Self { current, max } 123 | } 124 | 125 | pub fn full(max: u16) -> Self { 126 | Self { current: max, max } 127 | } 128 | 129 | pub fn current(&self) -> u16 { 130 | self.current 131 | } 132 | 133 | pub fn max(&self) -> u16 { 134 | self.max 135 | } 136 | 137 | pub fn make_full(&mut self) { 138 | self.current = self.max 139 | } 140 | 141 | pub fn is_full(&self) -> bool { 142 | self.current == self.max 143 | } 144 | 145 | pub fn is_empty(&self) -> bool { 146 | self.current == 0 147 | } 148 | 149 | pub fn increment(&mut self, amount: u16) { 150 | self.current = (self.current + amount).min(self.max); 151 | } 152 | 153 | pub fn decrement(&mut self, amount: u16) { 154 | self.current = self.current.saturating_sub(amount); 155 | } 156 | 157 | pub fn set(&mut self, value: u16) { 158 | self.current = value.min(self.max); 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /server/auth/src/main.rs: -------------------------------------------------------------------------------- 1 | //! This program encapsulates the account server. 2 | //! The account server is designed to accept login data from the client and 3 | //! to verify that the username and password combination inputted is 4 | //! correct with the database. If the combination is correct, the client 5 | //! will be transferred to the message server of their choice. 6 | 7 | use bytes::Bytes; 8 | use msg_connect::MsgConnect; 9 | use std::env; 10 | use tq_network::{Actor, ActorHandle, PacketDecode, PacketHandler, PacketID, TQCipher}; 11 | #[cfg(feature = "server")] 12 | use tq_server::TQServer; 13 | use wasmtime::{Config, Engine, ExternRef, Linker, Module, Store}; 14 | 15 | use auth::error::Error; 16 | use auth::{Runtime, State}; 17 | 18 | struct AuthServer; 19 | 20 | impl TQServer for AuthServer { 21 | type ActorState = (); 22 | type Cipher = TQCipher; 23 | type PacketHandler = Runtime; 24 | } 25 | 26 | #[tokio::main] 27 | async fn main() -> Result<(), Error> { 28 | dotenvy::dotenv()?; 29 | let log_verbosity = env::var("LOG_VERBOSITY") 30 | .map(|s| s.parse::().unwrap_or(2)) 31 | .unwrap_or(2); 32 | setup_logger(log_verbosity)?; 33 | println!( 34 | r#" 35 | _____ _____ 36 | / __ \ | ___| 37 | | / \/ ___ | |__ _ __ ___ _ _ 38 | | | / _ \ | __|| '_ ` _ \ | | | | 39 | | \__/\| (_) || |___| | | | | || |_| | 40 | \____/ \___/ \____/|_| |_| |_| \__,_| 41 | 42 | 43 | Copyright 2020-2023 Shady Khalifa (@shekohex) 44 | All Rights Reserved. 45 | "# 46 | ); 47 | let mut config = Config::new(); 48 | config.async_support(true).wasm_reference_types(true); 49 | 50 | let engine = Engine::new(&config)?; 51 | let mut linker = Linker::new(&engine); 52 | auth::add_to_linker(&mut linker)?; 53 | tracing::info!("Loading Packet and handlers.."); 54 | 55 | let msg_connect = Module::from_file(&engine, "./target/wasm32-unknown-unknown/wasm/msg_connect.s.wasm")?; 56 | tracing::info!("Initializing State .."); 57 | let state = State::init().await?; 58 | let packets = auth::Packets { msg_connect }; 59 | 60 | let static_runtime = { 61 | let runtime = Runtime { 62 | state, 63 | linker, 64 | engine, 65 | packets, 66 | }; 67 | Box::leak(Box::new(runtime)) as *mut _ 68 | }; 69 | // SAFETY: We are the only owner of this Box, and we are deref 70 | // it. This happens only once, so no one else can access. 71 | let runtime: &'static _ = unsafe { &*static_runtime }; 72 | 73 | tracing::info!("Starting Auth Server"); 74 | tracing::info!("Initializing server..."); 75 | let auth_port = env::var("AUTH_PORT")?; 76 | tracing::info!("Auth Server will be available on {auth_port}"); 77 | AuthServer::run(format!("0.0.0.0:{}", auth_port), runtime).await?; 78 | unsafe { 79 | // SAFETY: We are the only owner of this Box, and we are dropping 80 | // it. This happens at the end of the program, so no one 81 | // else can access. 82 | let _ = Box::from_raw(static_runtime); 83 | }; 84 | tracing::info!("Shutdown."); 85 | Ok(()) 86 | } 87 | 88 | fn setup_logger(verbosity: i32) -> Result<(), Error> { 89 | use tracing::Level; 90 | let log_level = match verbosity { 91 | 0 => Level::ERROR, 92 | 1 => Level::WARN, 93 | 2 => Level::INFO, 94 | 3 => Level::DEBUG, 95 | _ => Level::TRACE, 96 | }; 97 | 98 | let env_filter = tracing_subscriber::EnvFilter::from_default_env() 99 | .add_directive(format!("tq_db={}", log_level).parse().unwrap()) 100 | .add_directive(format!("tq_serde={}", log_level).parse().unwrap()) 101 | .add_directive(format!("tq_crypto={}", log_level).parse().unwrap()) 102 | .add_directive(format!("tq_codec={}", log_level).parse().unwrap()) 103 | .add_directive(format!("tq_network={}", log_level).parse().unwrap()) 104 | .add_directive(format!("tq_server={}", log_level).parse().unwrap()) 105 | .add_directive(format!("auth={}", log_level).parse().unwrap()) 106 | .add_directive(format!("auth_server={}", log_level).parse().unwrap()); 107 | let logger = tracing_subscriber::fmt() 108 | .pretty() 109 | .with_target(true) 110 | .with_max_level(log_level) 111 | .with_env_filter(env_filter); 112 | logger.init(); 113 | Ok(()) 114 | } 115 | -------------------------------------------------------------------------------- /crates/tracing-wasm/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Wasm-specific extensions for `tracing`. 2 | //! This crate provides a `tracing` layer for use in WASM environments. It 3 | //! provides a `tracing` layer for use in WASM environments, and is used by 4 | //! all packets that needs to emit tracing events. 5 | //! 6 | //! # Usage 7 | //! ```rust, no_run 8 | //! use tracing_wasm::MakeWasmWriter; 9 | //! use tracing_subscriber::prelude::*; 10 | //! 11 | //! let fmt_layer = tracing_subscriber::fmt::layer() 12 | //! .without_time() // std::time is not available in host 13 | //! .with_writer(MakeWasmWriter::new()); // write events to the host 14 | //! tracing_subscriber::registry() 15 | //! .with(fmt_layer) 16 | //! .init(); 17 | //! ``` 18 | #![cfg_attr(not(feature = "std"), no_std)] 19 | 20 | #[cfg(not(feature = "std"))] 21 | extern crate alloc; 22 | 23 | #[cfg(not(feature = "std"))] 24 | use alloc::{string::String, vec::Vec}; 25 | 26 | pub use tracing_core::Level; 27 | 28 | #[cfg(feature = "std")] 29 | use tracing_subscriber::fmt::MakeWriter; 30 | 31 | #[link(wasm_import_module = "host")] 32 | #[cfg(target_arch = "wasm32")] 33 | extern "C" { 34 | fn trace_event(level: u8, target: *const u8, target_len: u32, message: *const u8, message_len: u32); 35 | } 36 | 37 | /// A [`MakeWriter`] emitting the written text to the [`host`]. 38 | pub struct MakeWasmWriter { 39 | use_pretty_label: bool, 40 | target: &'static str, 41 | } 42 | 43 | impl Default for MakeWasmWriter { 44 | fn default() -> Self { 45 | Self::new() 46 | } 47 | } 48 | 49 | impl MakeWasmWriter { 50 | /// Create a default console writer, i.e. no level annotation is shown when 51 | /// logging a message. 52 | pub fn new() -> Self { 53 | Self { 54 | use_pretty_label: false, 55 | target: "wasm", 56 | } 57 | } 58 | 59 | /// Change writer with the given target. 60 | pub fn with_target(mut self, target: &'static str) -> Self { 61 | self.target = target; 62 | self 63 | } 64 | 65 | /// Enables an additional label for the log level to be shown. 66 | /// 67 | /// It is recommended that you also use [`Layer::with_level(false)`] if you 68 | /// use this option, to avoid the event level being shown twice. 69 | /// 70 | /// [`Layer::with_level(false)`]: tracing_subscriber::fmt::Layer::with_level 71 | pub fn with_pretty_level(mut self) -> Self { 72 | self.use_pretty_label = true; 73 | self 74 | } 75 | } 76 | 77 | type LogDispatcher = fn(Level, &str, &str); 78 | 79 | /// Dispatches a log message to the host. 80 | #[cfg(target_arch = "wasm32")] 81 | pub fn log(level: Level, target: &str, message: &str) { 82 | let level = match level { 83 | Level::ERROR => 0, 84 | Level::WARN => 1, 85 | Level::INFO => 2, 86 | Level::DEBUG => 3, 87 | Level::TRACE => 4, 88 | }; 89 | let message = message.as_bytes(); 90 | let target = target.as_bytes(); 91 | unsafe { 92 | trace_event( 93 | level, 94 | target.as_ptr(), 95 | target.len() as u32, 96 | message.as_ptr(), 97 | message.len() as u32, 98 | ) 99 | } 100 | } 101 | 102 | /// Does nothing. 103 | 104 | #[cfg(not(target_arch = "wasm32"))] 105 | pub fn log(_level: Level, _target: &str, _message: &str) {} 106 | 107 | /// Concrete [`std::io::Write`] implementation returned by [`MakeWasmWriter`]. 108 | pub struct WasmWriter { 109 | buffer: Vec, 110 | target: String, 111 | level: Level, 112 | log: LogDispatcher, 113 | } 114 | 115 | impl Drop for WasmWriter { 116 | fn drop(&mut self) { 117 | let message = String::from_utf8_lossy(&self.buffer); 118 | (self.log)(self.level, self.target.as_ref(), message.as_ref()) 119 | } 120 | } 121 | 122 | #[cfg(feature = "std")] 123 | impl std::io::Write for WasmWriter { 124 | fn write(&mut self, buf: &[u8]) -> std::io::Result { 125 | self.buffer.write(buf) 126 | } 127 | 128 | fn flush(&mut self) -> std::io::Result<()> { 129 | Ok(()) 130 | } 131 | } 132 | #[cfg(feature = "std")] 133 | impl<'a> MakeWriter<'a> for MakeWasmWriter { 134 | type Writer = WasmWriter; 135 | 136 | fn make_writer(&'a self) -> Self::Writer { 137 | WasmWriter { 138 | buffer: Vec::new(), 139 | level: Level::TRACE, 140 | target: self.target.to_string(), 141 | log, 142 | } 143 | } 144 | 145 | fn make_writer_for(&'a self, meta: &tracing_core::Metadata<'_>) -> Self::Writer { 146 | let level = *meta.level(); 147 | let target = meta.target().to_string(); 148 | WasmWriter { 149 | buffer: Vec::new(), 150 | target, 151 | level, 152 | log, 153 | } 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /crates/network/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! This crate contains the core networking components used by the server and 2 | //! client crates. 3 | 4 | #![cfg_attr(not(feature = "std"), no_std)] 5 | 6 | #[cfg(not(feature = "std"))] 7 | extern crate alloc; 8 | 9 | #[cfg(not(feature = "std"))] 10 | use alloc::boxed::Box; 11 | 12 | use bytes::Bytes; 13 | use serde::de::DeserializeOwned; 14 | use serde::Serialize; 15 | 16 | pub use async_trait::async_trait; 17 | pub use derive_packethandler::PacketHandler; 18 | pub use derive_packetid::PacketID; 19 | pub use derive_packetprocessor::packet_processor; 20 | pub use tq_codec::TQCodec; 21 | pub use tq_crypto::{CQCipher, Cipher, NopCipher, TQCipher}; 22 | 23 | mod error; 24 | pub use error::Error; 25 | 26 | mod actor; 27 | pub use actor::{Actor, ActorHandle, ActorState, Message}; 28 | 29 | /// Assoucitates a packet structure with a packet ID. This is used for 30 | /// serialization and deserialization of packets. The packet ID is used to 31 | /// identify the packet type, and the packet structure is used to serialize and 32 | /// deserialize the packet. 33 | pub trait PacketID { 34 | const PACKET_ID: u16; 35 | } 36 | 37 | #[async_trait] 38 | pub trait PacketProcess { 39 | type Error; 40 | type ActorState: ActorState; 41 | type State: Send + Sync; 42 | /// Process can be invoked by a packet after decode has been called to 43 | /// structure packet fields and properties. For the server 44 | /// implementations, this is called in the packet handler after the 45 | /// message has been dequeued from the server's PacketProcessor 46 | async fn process(&self, state: &Self::State, actor: &Actor) -> Result<(), Self::Error>; 47 | } 48 | 49 | pub trait PacketEncode { 50 | type Error: From + core::fmt::Debug; 51 | /// The Packet that we will encode. 52 | type Packet: Serialize + PacketID; 53 | /// Encodes the packet structure defined by this message struct into a byte 54 | /// packet that can be sent to the client. Invoked automatically by the 55 | /// client's send method. Encodes using byte ordering rules 56 | /// interoperable with the game client. 57 | fn encode(&self) -> Result<(u16, Bytes), Self::Error>; 58 | } 59 | 60 | pub trait PacketDecode { 61 | type Error: From + core::fmt::Debug; 62 | /// The Packet that we will Decode into. 63 | type Packet: DeserializeOwned; 64 | /// Decodes a byte packet into the packet structure defined by this message 65 | /// struct. Should be invoked to structure data from the client for 66 | /// processing. Decoding follows TQ Digital's byte ordering rules for an 67 | /// all-binary protocol. 68 | fn decode(bytes: &Bytes) -> Result; 69 | } 70 | 71 | #[async_trait] 72 | pub trait PacketHandler { 73 | type Error: PacketEncode + Send + Sync; 74 | type ActorState: ActorState; 75 | type State: Send + Sync + 'static; 76 | async fn handle( 77 | packet: (u16, Bytes), 78 | state: &Self::State, 79 | actor: &Actor, 80 | ) -> Result<(), Self::Error>; 81 | } 82 | 83 | impl PacketEncode for T 84 | where 85 | T: Serialize + PacketID, 86 | { 87 | type Error = Error; 88 | type Packet = T; 89 | 90 | fn encode(&self) -> Result<(u16, Bytes), Self::Error> { 91 | let id = Self::PACKET_ID; 92 | let bytes = tq_serde::to_bytes(&self)?; 93 | Ok((id, bytes.freeze())) 94 | } 95 | } 96 | 97 | impl PacketEncode for (u16, Bytes) { 98 | type Error = Error; 99 | type Packet = (); 100 | 101 | fn encode(&self) -> Result<(u16, Bytes), Self::Error> { 102 | Ok(self.clone()) 103 | } 104 | } 105 | 106 | impl<'a> PacketEncode for (u16, &'a [u8]) { 107 | type Error = Error; 108 | type Packet = (); 109 | 110 | fn encode(&self) -> Result<(u16, Bytes), Self::Error> { 111 | let (id, bytes) = self; 112 | Ok((*id, bytes.to_vec().into())) 113 | } 114 | } 115 | 116 | impl PacketDecode for T 117 | where 118 | T: DeserializeOwned, 119 | { 120 | type Error = Error; 121 | type Packet = T; 122 | 123 | fn decode(bytes: &Bytes) -> Result { 124 | Ok(tq_serde::from_bytes(bytes)?) 125 | } 126 | } 127 | 128 | impl PacketID for () { 129 | const PACKET_ID: u16 = 0; 130 | } 131 | 132 | pub struct ErrorPacket(pub T); 133 | 134 | pub trait IntoErrorPacket { 135 | fn error_packet(self) -> ErrorPacket; 136 | } 137 | 138 | impl IntoErrorPacket for T 139 | where 140 | T: PacketEncode, 141 | { 142 | fn error_packet(self) -> ErrorPacket { 143 | ErrorPacket(self) 144 | } 145 | } 146 | 147 | impl From for ErrorPacket 148 | where 149 | T: PacketEncode, 150 | { 151 | fn from(v: T) -> Self { 152 | Self(v) 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /tools/gamemap-decoder/src/main.rs: -------------------------------------------------------------------------------- 1 | use bytes::{Buf, Bytes}; 2 | use std::collections::HashMap; 3 | use std::io::{BufReader, Read}; 4 | use std::path::PathBuf; 5 | use std::{env, fs}; 6 | 7 | #[allow(unused)] 8 | #[derive(Debug, serde::Deserialize, Clone)] 9 | #[serde(rename_all = "PascalCase")] 10 | struct Map { 11 | uid: u32, 12 | name: String, 13 | #[serde(skip)] 14 | path: String, 15 | id: u32, 16 | flags: u32, 17 | weather: u8, 18 | portal_x: u16, 19 | portal_y: u16, 20 | reborn_map: u32, 21 | color: u32, 22 | } 23 | 24 | impl Default for Map { 25 | fn default() -> Self { 26 | Self { 27 | uid: 0, 28 | name: String::new(), 29 | path: String::new(), 30 | id: 0, 31 | flags: 0, 32 | weather: 0, 33 | portal_x: 0, 34 | portal_y: 0, 35 | reborn_map: 0, 36 | color: 4294967295, 37 | } 38 | } 39 | } 40 | 41 | #[derive(Debug, serde::Deserialize, Clone)] 42 | #[serde(rename_all = "PascalCase")] 43 | struct Portal { 44 | id: u32, 45 | from_map_id: u32, 46 | from_x: u16, 47 | from_y: u16, 48 | to_map_id: u32, 49 | to_x: u16, 50 | to_y: u16, 51 | } 52 | 53 | fn main() -> anyhow::Result<()> { 54 | dotenvy::dotenv()?; 55 | let data_path = env::var("DATA_LOCATION")?; 56 | let dat_path = PathBuf::from(&data_path).join("GameMaps").join("GameMap.dat"); 57 | let maps_csv = PathBuf::from(&data_path).join("Maps").join("Maps.csv"); 58 | let portals_csv = PathBuf::from(&data_path).join("Maps").join("Portals.csv"); 59 | let csv_reader = csv::ReaderBuilder::new().has_headers(true).from_path(maps_csv)?; 60 | let mut maps = csv_reader 61 | .into_deserialize::() 62 | .filter_map(Result::ok) 63 | .map(|m| (m.uid, m)) 64 | .collect::>(); 65 | let csv_reader = csv::ReaderBuilder::new().has_headers(true).from_path(portals_csv)?; 66 | let portals = csv_reader 67 | .into_deserialize::() 68 | .filter_map(Result::ok) 69 | .collect::>(); 70 | let f = fs::File::open(dat_path)?; 71 | let mut buffer = Vec::new(); 72 | let mut reader = BufReader::with_capacity(1024, f); 73 | reader.read_to_end(&mut buffer)?; 74 | let mut buffer = Bytes::from(buffer); 75 | let amount = buffer.get_u32_le(); 76 | let mut map_with_path = HashMap::with_capacity(amount as usize); 77 | for _ in 0..amount { 78 | let map_id = buffer.get_u32_le(); 79 | let path_len = buffer.get_u32_le() as usize; 80 | let path_buf = buffer.split_to(path_len); 81 | let mut path = String::from_utf8(path_buf.into())?; 82 | path.replace_range(0..8, ""); 83 | let path = path.replace(".DMap", ".cmap"); 84 | map_with_path.insert(map_id, path); 85 | buffer.advance(4); // puzzle 86 | } 87 | assert!(map_with_path.len() >= amount as usize); 88 | // For maps without a path, we need to repair these 89 | // by using the path of the map with the same id but having a different uid. 90 | for (_, map) in maps.iter_mut() { 91 | if map.path.is_empty() { 92 | if let Some(path) = map_with_path.get(&map.id) { 93 | map.path = path.clone(); 94 | } 95 | } 96 | } 97 | for Map { 98 | uid, 99 | id, 100 | path, 101 | flags, 102 | weather, 103 | portal_x, 104 | portal_y, 105 | reborn_map, 106 | color, 107 | .. 108 | } in maps.values() 109 | { 110 | if path.is_empty() { 111 | println!( 112 | r#"-- INSERT INTO maps VALUES ({uid}, {id}, '{path}', {portal_x}, {portal_y}, {flags}, {weather}, {reborn_map}, {color});"#, 113 | ); 114 | continue; 115 | } 116 | println!( 117 | r#"INSERT INTO maps VALUES ({uid}, {id}, '{path}', {portal_x}, {portal_y}, {flags}, {weather}, {reborn_map}, {color});"#, 118 | ); 119 | } 120 | println!(); 121 | for Portal { 122 | id, 123 | from_map_id, 124 | from_x, 125 | from_y, 126 | to_map_id, 127 | to_x, 128 | to_y, 129 | } in portals 130 | { 131 | match (maps.get(&from_map_id), maps.get(&to_map_id)) { 132 | (Some(from), Some(to)) if !from.path.is_empty() && !to.path.is_empty() => { 133 | println!( 134 | r#"INSERT INTO portals VALUES ({id}, {from_map_id}, {from_x}, {from_y}, {to_map_id}, {to_x}, {to_y});"#, 135 | ); 136 | }, 137 | _ => { 138 | println!( 139 | r#"-- INSERT INTO portals VALUES ({id}, {from_map_id}, {from_x}, {from_y}, {to_map_id}, {to_x}, {to_y});"#, 140 | ); 141 | }, 142 | } 143 | } 144 | 145 | Ok(()) 146 | } 147 | -------------------------------------------------------------------------------- /macros/derive-packetprocessor/src/lib.rs: -------------------------------------------------------------------------------- 1 | use proc_macro::TokenStream; 2 | use quote::quote; 3 | use syn::parse::{Parse, ParseStream}; 4 | use syn::{parse_macro_input, FnArg, Ident, ItemFn, ReturnType}; 5 | 6 | struct Args { 7 | msg: Ident, 8 | } 9 | 10 | impl Parse for Args { 11 | fn parse(input: ParseStream) -> syn::Result { 12 | let msg: Ident = input.parse()?; 13 | let args = Self { msg }; 14 | Ok(args) 15 | } 16 | } 17 | 18 | macro_rules! syn_assert { 19 | ($cond:expr, $msg:expr) => { 20 | if !$cond { 21 | return Err(syn::Error::new_spanned($cond, $msg)); 22 | } 23 | }; 24 | } 25 | 26 | fn derive_packet_processor(args: Args, inner_fn: ItemFn) -> syn::Result { 27 | let msg_ty = args.msg; 28 | // make sure the function has the right signature 29 | // fn process(msg: msg_ty, actor: &Resource) -> Result<(), 30 | // crate::Error> 31 | let fn_sig = &inner_fn.sig; 32 | syn_assert!(fn_sig.constness.is_none(), "const fn not supported"); 33 | syn_assert!(fn_sig.asyncness.is_none(), "async fn not supported"); 34 | syn_assert!(fn_sig.abi.is_none(), "abi fn not supported"); 35 | syn_assert!(fn_sig.unsafety.is_none(), "unsafe fn not supported"); 36 | syn_assert!(fn_sig.generics.params.is_empty(), "generic fn not supported"); 37 | syn_assert!(fn_sig.generics.where_clause.is_none(), "generic fn not supported"); 38 | syn_assert!(fn_sig.inputs.len() == 2, "packet processor must have two arguments"); 39 | syn_assert!( 40 | fn_sig.output != ReturnType::Default, 41 | "packet processor must have a return type" 42 | ); 43 | syn_assert!( 44 | matches!(fn_sig.inputs[0], FnArg::Typed(_)), 45 | "packet processor must have a typed msg argument" 46 | ); 47 | syn_assert!( 48 | matches!(fn_sig.inputs[1], FnArg::Typed(_)), 49 | "packet processor must have a typed actor argument" 50 | ); 51 | match fn_sig.inputs[0] { 52 | FnArg::Receiver(_) => unreachable!(), 53 | FnArg::Typed(ref p) => syn_assert!( 54 | p.ty == syn::parse_quote!(#msg_ty), 55 | "packet processor must have a typed msg argument" 56 | ), 57 | }; 58 | match fn_sig.inputs[1] { 59 | FnArg::Receiver(_) => unreachable!(), 60 | FnArg::Typed(ref p) => { 61 | syn_assert!( 62 | p.ty == syn::parse_quote!(&Resource), 63 | "packet processor must have a typed actor argument" 64 | ); 65 | }, 66 | }; 67 | 68 | let inner_fn_call = inner_fn.sig.ident.clone(); 69 | let msg_ty_name = msg_ty.to_string().to_lowercase(); 70 | // Build the output, possibly using quasi-quotation 71 | let expanded = quote! { 72 | #[export_name = "__alloc"] 73 | #[cfg(target_arch = "wasm32")] 74 | pub extern "C" fn __alloc(size: u32) -> *mut u8 { 75 | #[cfg(not(feature = "std"))] 76 | let v = ::alloc::vec::Vec::with_capacity(size as usize); 77 | #[cfg(feature = "std")] 78 | let v = ::std::vec::Vec::with_capacity(size as usize); 79 | let mut v = core::mem::ManuallyDrop::new(v); 80 | unsafe { 81 | v.set_len(size as usize); 82 | } 83 | v.shrink_to_fit(); 84 | let ptr = v.as_mut_ptr(); 85 | ptr 86 | } 87 | 88 | 89 | #inner_fn 90 | 91 | #[::tq_bindings::externref(crate = "tq_bindings::anyref")] 92 | #[export_name = "process_packet"] 93 | #[cfg(target_arch = "wasm32")] 94 | pub unsafe extern "C" fn _process( 95 | packet_ptr: *mut u8, 96 | packet_len: u32, 97 | actor: &::tq_bindings::Resource, 98 | ) -> i32 { 99 | ::tq_bindings::set_panic_hook_once(#msg_ty_name); 100 | ::tq_bindings::setup_logging(#msg_ty_name); 101 | #[cfg(not(feature = "std"))] 102 | let packet = ::alloc::vec::Vec::from_raw_parts(packet_ptr, packet_len as _, packet_len as _); 103 | #[cfg(feature = "std")] 104 | let packet = ::std::vec::Vec::from_raw_parts(packet_ptr, packet_len as _, packet_len as _); 105 | let bytes = ::bytes::BytesMut::from(packet.as_slice()).freeze(); 106 | let packet = match <#msg_ty as ::tq_network::PacketDecode>::decode(&bytes) { 107 | Ok(packet) => packet, 108 | Err(e) => { 109 | tracing::error!(error = ?e, "While decoding the packet"); 110 | return 0xdec0de; 111 | } 112 | }; 113 | match #inner_fn_call(packet, actor) { 114 | Ok(()) => 0, 115 | Err(e) => { 116 | tracing::error!(error = ?e, "While handling the packet"); 117 | 0x00f 118 | } 119 | } 120 | } 121 | }; 122 | Ok(expanded.into()) 123 | } 124 | 125 | #[proc_macro_attribute] 126 | pub fn packet_processor(args: TokenStream, input: TokenStream) -> TokenStream { 127 | let input = parse_macro_input!(input as ItemFn); 128 | let args = parse_macro_input!(args as Args); 129 | derive_packet_processor(args, input).unwrap_or_else(|err| err.to_compile_error().into()) 130 | } 131 | -------------------------------------------------------------------------------- /server/game/src/packets/msg_talk.rs: -------------------------------------------------------------------------------- 1 | use crate::constants::{ALL_USERS, SYSTEM}; 2 | use crate::state::State; 3 | use crate::systems::commands; 4 | use crate::ActorState; 5 | use async_trait::async_trait; 6 | use num_enum::{FromPrimitive, IntoPrimitive}; 7 | use serde::{Deserialize, Serialize}; 8 | use tq_network::{Actor, PacketID, PacketProcess}; 9 | 10 | /// Enumeration for defining the channel text is printed to. Can also print to 11 | /// separate states of the client such as character registration, and can be 12 | /// used to change the state of the client or deny a login. 13 | #[derive(Copy, Clone, Debug, FromPrimitive, IntoPrimitive)] 14 | #[repr(u16)] 15 | pub enum TalkChannel { 16 | Talk = 2000, 17 | Whisper = 2001, 18 | Action = 2002, 19 | Team = 2003, 20 | Guild = 2004, 21 | Spouse = 2006, 22 | System = 2007, 23 | Yell = 2008, 24 | Friend = 2009, 25 | Center = 2011, 26 | TopLeft = 2012, 27 | Ghost = 2013, 28 | Service = 2014, 29 | Tip = 2015, 30 | World = 2021, 31 | Register = 2100, 32 | Login = 2101, 33 | Shop = 2102, 34 | Vendor = 2104, 35 | Website = 2105, 36 | Right1 = 2108, 37 | Right2 = 2109, 38 | Offline = 2110, 39 | Announce = 2111, 40 | TradeBoard = 2201, 41 | FriendBoard = 2202, 42 | TeamBoard = 2203, 43 | GuildBoard = 2204, 44 | OthersBoard = 2205, 45 | Broadcast = 2500, 46 | Monster = 2600, 47 | #[num_enum(default)] 48 | Unknown, 49 | } 50 | /// Enumeration type for controlling how text is stylized in the client's chat 51 | /// area. By default, text appears and fades overtime. This can be overridden 52 | /// with multiple styles, hard-coded into the client. 53 | #[derive(Copy, Clone, Debug, FromPrimitive, IntoPrimitive)] 54 | #[repr(u16)] 55 | pub enum TalkStyle { 56 | Normal = 0, 57 | Scroll = 1, 58 | Flash = 2, 59 | Blast = 3, 60 | #[num_enum(default)] 61 | Unknown, 62 | } 63 | 64 | /// Message defining a chat message from one player to the other, or from the 65 | /// system to a player. Used for all chat systems in the game, including 66 | /// messages outside of the game world state, such as during character creation 67 | /// or to tell the client to continue logging in after connect. 68 | #[derive(Debug, Default, Deserialize, Serialize, PacketID, Clone)] 69 | #[packet(id = 1004)] 70 | pub struct MsgTalk { 71 | pub color: u32, 72 | pub channel: u16, 73 | pub style: u16, 74 | pub character_id: u32, 75 | pub recipient_mesh: u32, 76 | pub sender_mesh: u32, 77 | pub list_count: u8, 78 | pub sender_name: String, 79 | pub recipient_name: String, 80 | pub suffix: String, 81 | pub message: String, 82 | } 83 | 84 | impl MsgTalk { 85 | pub fn from_system(character_id: u32, channel: TalkChannel, message: impl Into) -> Self { 86 | MsgTalk { 87 | color: 0x00FF_FFFF, 88 | channel: channel.into(), 89 | style: TalkStyle::Normal.into(), 90 | character_id, 91 | recipient_mesh: 0, 92 | sender_mesh: 0, 93 | list_count: 4, 94 | sender_name: SYSTEM.to_string(), 95 | recipient_name: ALL_USERS.to_string(), 96 | suffix: String::new(), 97 | message: message.into(), 98 | } 99 | } 100 | 101 | pub fn login_invalid() -> Self { 102 | Self::from_system(0, TalkChannel::Login, "Login Invalid") 103 | } 104 | 105 | pub fn register_invalid() -> Self { 106 | Self::from_system(0, TalkChannel::Register, String::from("Register Invalid")) 107 | } 108 | 109 | pub fn register_ok() -> Self { 110 | Self::from_system(0, TalkChannel::Register, crate::constants::ANSWER_OK.to_owned()) 111 | } 112 | 113 | pub fn register_name_taken() -> Self { 114 | Self::from_system( 115 | 0, 116 | TalkChannel::Register, 117 | String::from("Character name taken, try another one."), 118 | ) 119 | } 120 | 121 | pub fn login_ok() -> Self { 122 | Self::from_system(0, TalkChannel::Login, crate::constants::ANSWER_OK.to_owned()) 123 | } 124 | 125 | pub fn login_new_role() -> Self { 126 | Self::from_system(0, TalkChannel::Login, crate::constants::NEW_ROLE.to_owned()) 127 | } 128 | } 129 | 130 | #[async_trait] 131 | impl PacketProcess for MsgTalk { 132 | type ActorState = ActorState; 133 | type Error = crate::Error; 134 | type State = State; 135 | 136 | async fn process(&self, state: &Self::State, actor: &Actor) -> Result<(), Self::Error> { 137 | if self.message.starts_with('$') { 138 | // Command Message. 139 | let (_, command) = self.message.split_at(1); 140 | let args: Vec<_> = command.split_whitespace().collect(); 141 | commands::parse_and_execute(state, actor, &args).await?; 142 | } 143 | // For now, we just broadcast the message to all players in our region. 144 | // TODO: Implement this properly. 145 | let map_id = actor.entity().basic().map_id(); 146 | let loc = actor.entity().basic().location(); 147 | let mymap = state.try_map(map_id)?; 148 | let myregion = mymap.region(loc.x, loc.y).ok_or(crate::Error::MapRegionNotFound)?; 149 | myregion.broadcast(self.clone()).await?; 150 | Ok(()) 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /macros/derive-packethandler/src/lib.rs: -------------------------------------------------------------------------------- 1 | use proc_macro::TokenStream; 2 | use quote::quote; 3 | use syn::parse::{Parse, ParseStream}; 4 | use syn::{parse_macro_input, Data, DataEnum, DeriveInput, Expr, Ident, Token}; 5 | 6 | #[derive(Debug, Clone)] 7 | struct Args { 8 | actor_state: Expr, 9 | state: Expr, 10 | } 11 | 12 | impl Parse for Args { 13 | fn parse(input: ParseStream) -> syn::Result { 14 | let ident1: Ident = input 15 | .parse() 16 | .map_err(|e| syn::Error::new(e.span(), "expected `state` or `actor_state` but got nothing"))?; 17 | let _: Token!(=) = input.parse().map_err(|e| syn::Error::new(e.span(), "expected `=`"))?; 18 | let ident1_value: Expr = input 19 | .parse() 20 | .map_err(|e| syn::Error::new(e.span(), "expected `Expr`"))?; 21 | let _: Token!(,) = input.parse().map_err(|e| syn::Error::new(e.span(), "expected `,`"))?; 22 | let ident2: Ident = input 23 | .parse() 24 | .map_err(|e| syn::Error::new(e.span(), "expected `state` or `actor_state` but got nothing"))?; 25 | let _: Token!(=) = input.parse().map_err(|e| syn::Error::new(e.span(), "expected `=`"))?; 26 | let ident2_value: Expr = input 27 | .parse() 28 | .map_err(|e| syn::Error::new(e.span(), "expected `Expr`"))?; 29 | let (state, actor_state) = match (ident1, ident2) { 30 | (ident1, ident2) if ident1 == "state" && ident2 == "actor_state" => (ident1_value, ident2_value), 31 | (ident1, ident2) if ident1 == "actor_state" && ident2 == "state" => (ident2_value, ident1_value), 32 | (v1, v2) => { 33 | return Err(syn::Error::new( 34 | input.span(), 35 | format!("expected `state` or `actor_state` but got {v1} and {v2}",), 36 | )) 37 | }, 38 | }; 39 | 40 | let args = Self { state, actor_state }; 41 | Ok(args) 42 | } 43 | } 44 | fn derive_packet_handler(input: DeriveInput) -> syn::Result { 45 | let body = if let Data::Enum(e) = input.data { 46 | body(e)? 47 | } else { 48 | return Err(syn::Error::new( 49 | input.ident.span(), 50 | "You can only derive `PacketHandler` for enums", 51 | )); 52 | }; 53 | // Used in the quasi-quotation below as `#name`. 54 | let name = input.ident; 55 | let attr = input 56 | .attrs 57 | .iter() 58 | .find(|a| a.path().is_ident("handle")) 59 | .ok_or_else(|| { 60 | syn::Error::new( 61 | name.span(), 62 | "Missing State and ActorState! please add #[handle(state = .., actor_state = ...)] on the enum", 63 | ) 64 | })?; 65 | let args: Args = attr.parse_args()?; 66 | let state = args.state; 67 | let actor_state = args.actor_state; 68 | let generics = input.generics; 69 | let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); 70 | // Build the output, possibly using quasi-quotation 71 | let expanded = quote! { 72 | #[async_trait::async_trait] 73 | impl #impl_generics tq_network::PacketHandler for #name #ty_generics #where_clause { 74 | type Error = crate::Error; 75 | type ActorState = #actor_state; 76 | type State = #state; 77 | #[::tracing::instrument(skip_all, fields(actor = actor.id(), packet_id = packet.0))] 78 | async fn handle( 79 | packet: (u16, bytes::Bytes), 80 | state: &Self::State, 81 | actor: &tq_network::Actor, 82 | ) -> Result<(), Self::Error> { 83 | use tq_network::{PacketID, PacketProcess}; 84 | #body 85 | Ok(()) 86 | } 87 | } 88 | }; 89 | Ok(expanded.into()) 90 | } 91 | 92 | fn body(e: DataEnum) -> syn::Result { 93 | let vars = e.variants.into_iter().filter(|v| v.fields.is_empty()); 94 | let match_stms = vars.into_iter().map(|v| { 95 | let ident = v.ident; 96 | quote! { 97 | #ident::PACKET_ID => { 98 | let maybe_msg = <#ident as tq_network::PacketDecode>::decode(&packet.1); 99 | match maybe_msg { 100 | Ok(msg) => { 101 | tracing::debug!(target: "cq_msg", "{msg:?}"); 102 | msg.process(state, actor).await?; 103 | }, 104 | Err(e) => { 105 | tracing::error!(id = %packet.0, error = ?e, "Failed to decode packet"); 106 | return Ok(()); 107 | } 108 | } 109 | return Ok(()); 110 | }, 111 | } 112 | }); 113 | let tokens = quote! { 114 | match packet.0 { 115 | #(#match_stms)* 116 | _ => { 117 | tracing::warn!(id = %packet.0, "Got Unknown Packet"); 118 | } 119 | } 120 | }; 121 | Ok(tokens) 122 | } 123 | 124 | #[proc_macro_derive(PacketHandler, attributes(handle))] 125 | pub fn derive(input: TokenStream) -> TokenStream { 126 | // Parse the input tokens into a syntax tree 127 | let input = parse_macro_input!(input as DeriveInput); 128 | derive_packet_handler(input).unwrap_or_else(|err| err.to_compile_error().into()) 129 | } 130 | -------------------------------------------------------------------------------- /server/game/src/packets/msg_register.rs: -------------------------------------------------------------------------------- 1 | use super::MsgTalk; 2 | use crate::entities::Character; 3 | use crate::systems::Screen; 4 | use crate::{ActorState, Error, State}; 5 | use num_enum::{IntoPrimitive, TryFromPrimitive}; 6 | use rand::{Rng, SeedableRng}; 7 | use serde::{Deserialize, Serialize}; 8 | use std::convert::TryFrom; 9 | use tq_network::{Actor, IntoErrorPacket, PacketID, PacketProcess}; 10 | use tq_serde::{String16, TQMaskedPassword}; 11 | 12 | #[derive(Debug, Default, Serialize, Deserialize, PacketID)] 13 | #[packet(id = 1001)] 14 | pub struct MsgRegister { 15 | pub username: String16, 16 | pub character_name: String16, 17 | pub password: TQMaskedPassword, 18 | pub mesh: u16, 19 | pub class: u16, 20 | pub token: u32, 21 | } 22 | 23 | impl MsgRegister { 24 | pub fn build_character(&self, account_id: u32, realm_id: u32) -> Result { 25 | Self::build_character_with( 26 | self.character_name.to_string(), 27 | BodyType::try_from(self.mesh).map_err(|_| Error::InvalidBodyType)?, 28 | BaseClass::try_from(self.class).map_err(|_| Error::InvalidClass)?, 29 | account_id, 30 | realm_id, 31 | ) 32 | } 33 | 34 | pub fn build_character_with( 35 | name: String, 36 | mesh: BodyType, 37 | class: BaseClass, 38 | account_id: u32, 39 | realm_id: u32, 40 | ) -> Result { 41 | // Some Math for rand characher. 42 | let mut rng = rand::rngs::StdRng::from_entropy(); 43 | 44 | let avatar = match u16::from(mesh) { 45 | // For Male 46 | m if m < 1005 => rng.gen_range(1..49), 47 | 48 | // For Females 49 | _ => rng.gen_range(201..249), 50 | }; 51 | 52 | let hair_style = rng.gen_range(3..9) * 100 + crate::constants::HAIR_STYLES[rng.gen_range(0..12)]; 53 | let strength = match class { 54 | BaseClass::Taoist => 2, 55 | _ => 4, 56 | }; 57 | let agility = 6; 58 | let vitality = 12; 59 | let spirit = match class { 60 | BaseClass::Taoist => 10, 61 | _ => 0, 62 | }; 63 | 64 | let health_points = (strength * 3) + (agility * 3) + (spirit * 3) + (vitality * 24); 65 | let mana_points = spirit * 5; 66 | 67 | let c = tq_db::character::Character { 68 | account_id: account_id as i32, 69 | realm_id: realm_id as i32, 70 | name, 71 | mesh: u16::from(mesh) as i32, 72 | avatar, 73 | hair_style, 74 | silver: 1000, 75 | cps: 0, 76 | current_class: u16::from(class) as i16, 77 | map_id: 1010, 78 | x: 61, 79 | y: 109, 80 | virtue: 0, 81 | strength, 82 | agility, 83 | vitality, 84 | spirit, 85 | health_points, 86 | mana_points, 87 | ..Default::default() 88 | }; 89 | Ok(c) 90 | } 91 | } 92 | 93 | #[derive(Copy, Clone, Debug, TryFromPrimitive, IntoPrimitive)] 94 | #[repr(u16)] 95 | pub enum BodyType { 96 | AgileMale = 1003, 97 | MuscularMale = 1004, 98 | AgileFemale = 2001, 99 | MuscularFemale = 2002, 100 | } 101 | 102 | #[derive(Copy, Clone, Debug, TryFromPrimitive, IntoPrimitive)] 103 | #[repr(u16)] 104 | pub enum BaseClass { 105 | Trojan = 10, 106 | Warrior = 20, 107 | Archer = 40, 108 | Taoist = 100, 109 | } 110 | 111 | #[async_trait::async_trait] 112 | impl PacketProcess for MsgRegister { 113 | type ActorState = ActorState; 114 | type Error = Error; 115 | type State = State; 116 | 117 | async fn process(&self, state: &Self::State, actor: &Actor) -> Result<(), Self::Error> { 118 | let info = state 119 | .remove_creation_token(self.token) 120 | .map_err(|_| MsgTalk::register_invalid().error_packet())?; 121 | 122 | if tq_db::character::Character::name_taken(state.pool(), &self.character_name).await? { 123 | return Err(MsgTalk::register_name_taken().error_packet().into()); 124 | } 125 | 126 | // Validate Data. 127 | BodyType::try_from(self.mesh).map_err(|_| MsgTalk::register_invalid().error_packet())?; 128 | BaseClass::try_from(self.class).map_err(|_| MsgTalk::register_invalid().error_packet())?; 129 | 130 | let character_id = self 131 | .build_character(info.account_id, info.realm_id)? 132 | .save(state.pool()) 133 | .await?; 134 | let character = tq_db::character::Character::by_id(state.pool(), character_id).await?; 135 | let map_id = character.map_id; 136 | let me = Character::new(actor.handle(), character); 137 | let screen = Screen::new(actor.handle()); 138 | actor.update(me, screen); 139 | state.insert_entity(actor.entity()); 140 | // Set player map. 141 | state 142 | .try_map(map_id as _) 143 | .map_err(|_| MsgTalk::register_invalid().error_packet())? 144 | .insert_entity(actor.entity()) 145 | .await?; 146 | 147 | tracing::info!( 148 | "Account #{} Created Character #{} with Name {}", 149 | info.account_id, 150 | character_id, 151 | self.character_name 152 | ); 153 | actor.send(MsgTalk::register_ok()).await?; 154 | Ok(()) 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /crates/db/src/character.rs: -------------------------------------------------------------------------------- 1 | /// This struct encapsulates the game character for a player. The player 2 | /// controls the character as the protagonist of the Conquer Online storyline. 3 | /// The character is the persona of the player who controls it. The persona can 4 | /// be altered using different avatars, hairstyles, and body types. The player 5 | /// also controls the character's professions and abilities. 6 | #[derive(Debug, Clone, Default)] 7 | #[cfg_attr(feature = "sqlx", derive(sqlx::FromRow))] 8 | pub struct Character { 9 | pub character_id: i32, 10 | pub account_id: i32, 11 | pub realm_id: i32, 12 | pub name: String, 13 | pub mesh: i32, 14 | pub avatar: i16, 15 | pub hair_style: i16, 16 | pub silver: i64, 17 | pub cps: i64, 18 | pub current_class: i16, 19 | pub previous_class: i16, 20 | pub rebirths: i16, 21 | pub level: i16, 22 | pub experience: i64, 23 | pub map_id: i32, 24 | pub x: i16, 25 | pub y: i16, 26 | pub virtue: i16, 27 | pub strength: i16, 28 | pub agility: i16, 29 | pub vitality: i16, 30 | pub spirit: i16, 31 | pub attribute_points: i16, 32 | pub health_points: i16, 33 | pub mana_points: i16, 34 | pub kill_points: i16, 35 | } 36 | 37 | #[derive(Debug)] 38 | #[cfg_attr(feature = "sqlx", derive(sqlx::FromRow))] 39 | pub struct Location { 40 | pub map_id: i32, 41 | pub x: i16, 42 | pub y: i16, 43 | } 44 | 45 | #[cfg(feature = "sqlx")] 46 | impl Character { 47 | pub async fn from_account(pool: &sqlx::SqlitePool, id: u32) -> Result, crate::Error> { 48 | let maybe_character = sqlx::query_as::<_, Self>("SELECT * FROM characters WHERE account_id = ?;") 49 | .bind(id) 50 | .fetch_optional(pool) 51 | .await?; 52 | Ok(maybe_character) 53 | } 54 | 55 | pub async fn name_taken(pool: &sqlx::SqlitePool, name: &str) -> Result { 56 | let result = sqlx::query_as::<_, (i32,)>("SELECT EXISTS (SELECT 1 FROM characters WHERE name = ? LIMIT 1);") 57 | .bind(name) 58 | .fetch_optional(pool) 59 | .await?; 60 | match result { 61 | Some((1,)) => Ok(true), 62 | Some((0,)) => Ok(false), 63 | // This should never happen. 64 | _ => Ok(false), 65 | } 66 | } 67 | 68 | pub async fn by_id(pool: &sqlx::SqlitePool, id: i32) -> Result { 69 | let c = sqlx::query_as::<_, Self>("SELECT * FROM characters WHERE character_id = ?;") 70 | .bind(id) 71 | .fetch_one(pool) 72 | .await?; 73 | Ok(c) 74 | } 75 | 76 | pub async fn save(self, pool: &sqlx::SqlitePool) -> Result { 77 | let (id,) = sqlx::query_as::<_, (i32,)>( 78 | " 79 | INSERT INTO characters 80 | ( 81 | account_id, realm_id, name, mesh, avatar, 82 | hair_style, silver, current_class, 83 | map_id, x, y, virtue, strength, agility, 84 | vitality, spirit, health_points, mana_points 85 | ) 86 | VALUES 87 | ( 88 | ?, ?, ?, ?, ?, ?, 89 | ?, ?, ?, ?, ?, ?, 90 | ?, ?, ?, ?, ?, ? 91 | ) 92 | RETURNING character_id; 93 | ", 94 | ) 95 | .bind(self.account_id) 96 | .bind(self.realm_id) 97 | .bind(self.name) 98 | .bind(self.mesh) 99 | .bind(self.avatar) 100 | .bind(self.hair_style) 101 | .bind(self.silver) 102 | .bind(self.current_class) 103 | .bind(self.map_id) 104 | .bind(self.x) 105 | .bind(self.y) 106 | .bind(self.virtue) 107 | .bind(self.strength) 108 | .bind(self.agility) 109 | .bind(self.vitality) 110 | .bind(self.spirit) 111 | .bind(self.health_points) 112 | .bind(self.mana_points) 113 | .fetch_one(pool) 114 | .await?; 115 | Ok(id) 116 | } 117 | 118 | pub async fn update(self, pool: &sqlx::SqlitePool) -> Result<(), crate::Error> { 119 | sqlx::query( 120 | " 121 | UPDATE characters 122 | SET 123 | name = ?, 124 | mesh = ?, 125 | avatar = ?, 126 | hair_style = ?, 127 | silver = ?, 128 | current_class = ?, 129 | map_id = ?, 130 | x = ?, y = ?, 131 | virtue = ?, 132 | strength = ?, 133 | agility = ?, 134 | vitality = ?, 135 | spirit = ?, 136 | health_points = ?, 137 | mana_points = ? 138 | WHERE character_id = ?; 139 | ", 140 | ) 141 | .bind(self.name) 142 | .bind(self.mesh) 143 | .bind(self.avatar) 144 | .bind(self.hair_style) 145 | .bind(self.silver) 146 | .bind(self.current_class) 147 | .bind(self.map_id) 148 | .bind(self.x) 149 | .bind(self.y) 150 | .bind(self.virtue) 151 | .bind(self.strength) 152 | .bind(self.agility) 153 | .bind(self.vitality) 154 | .bind(self.spirit) 155 | .bind(self.health_points) 156 | .bind(self.mana_points) 157 | .bind(self.character_id) 158 | .execute(pool) 159 | .await?; 160 | Ok(()) 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /crates/network/src/actor.rs: -------------------------------------------------------------------------------- 1 | use crate::{Error, PacketEncode}; 2 | use async_trait::async_trait; 3 | use bytes::Bytes; 4 | use core::hash::Hash; 5 | use core::ops::Deref; 6 | use core::sync::atomic::{AtomicUsize, Ordering}; 7 | use futures::TryFutureExt; 8 | use tokio::sync::mpsc::Sender; 9 | use tracing::instrument; 10 | 11 | #[cfg(not(feature = "std"))] 12 | use alloc::{boxed::Box, sync::Arc}; 13 | #[cfg(feature = "std")] 14 | use std::sync::Arc; 15 | 16 | #[derive(Clone, Debug, PartialEq, Eq)] 17 | pub enum Message { 18 | GenerateKeys(u64), 19 | Packet(u16, Bytes), 20 | Shutdown, 21 | } 22 | 23 | /// This struct is the main actor type for the server. It is a wrapper around 24 | /// connections to client and its state. 25 | #[derive(Debug)] 26 | pub struct Actor { 27 | handle: ActorHandle, 28 | state: S, 29 | } 30 | 31 | /// A Cheap to clone actor handle. This is used to send messages to the actor 32 | /// from other threads. 33 | /// 34 | /// Think of this as a cheap clone of the actor without the state. 35 | #[derive(Clone, Debug)] 36 | pub struct ActorHandle { 37 | id: Arc, 38 | tx: Sender, 39 | } 40 | 41 | impl Hash for Actor { 42 | fn hash(&self, state: &mut H) { 43 | self.handle.id.load(Ordering::Relaxed).hash(state); 44 | } 45 | } 46 | 47 | impl PartialEq for Actor { 48 | fn eq(&self, other: &Self) -> bool { 49 | self.handle.id.load(Ordering::Relaxed) == other.handle.id.load(Ordering::Relaxed) 50 | } 51 | } 52 | 53 | impl Eq for Actor {} 54 | 55 | impl Deref for Actor { 56 | type Target = S; 57 | 58 | fn deref(&self) -> &Self::Target { 59 | &self.state 60 | } 61 | } 62 | 63 | impl From<(u16, Bytes)> for Message { 64 | fn from((id, bytes): (u16, Bytes)) -> Self { 65 | Self::Packet(id, bytes) 66 | } 67 | } 68 | 69 | #[async_trait] 70 | pub trait ActorState: Send + Sync + Sized { 71 | fn init() -> Self; 72 | /// A good chance to dispose the state and clear anything. 73 | #[instrument(skip_all, err)] 74 | async fn dispose(&self, handle: ActorHandle) -> Result<(), Error> { 75 | tracing::debug!(actor_id = %handle.id(), "Disposing Actor State"); 76 | Ok(()) 77 | } 78 | } 79 | 80 | impl ActorState for () { 81 | fn init() -> Self {} 82 | } 83 | 84 | impl Actor { 85 | pub fn new(tx: Sender) -> Self { 86 | Self { 87 | state: S::init(), 88 | handle: ActorHandle { 89 | id: Arc::new(AtomicUsize::new(0)), 90 | tx, 91 | }, 92 | } 93 | } 94 | 95 | /// Returns a cheap clone of the actor handle 96 | pub fn handle(&self) -> ActorHandle { 97 | self.handle.clone() 98 | } 99 | 100 | pub fn id(&self) -> usize { 101 | self.handle.id() 102 | } 103 | 104 | pub fn set_id(&self, id: usize) { 105 | self.handle.set_id(id) 106 | } 107 | 108 | /// Enqueue the packet and send it to the client connected to this actor 109 | #[instrument(skip(self, packet))] 110 | pub async fn send(&self, packet: P) -> Result<(), P::Error> { 111 | self.handle.send(packet).await 112 | } 113 | 114 | /// Enqueue the packets and send it all at once to the client connected to 115 | /// this actor 116 | #[instrument(skip(self, packets))] 117 | pub async fn send_all(&self, packets: I) -> Result<(), P::Error> 118 | where 119 | P: PacketEncode, 120 | I: IntoIterator, 121 | { 122 | self.handle.send_all(packets).await 123 | } 124 | 125 | #[instrument(skip(self))] 126 | pub async fn generate_keys(&self, seed: u64) -> Result<(), Error> { 127 | self.handle.generate_keys(seed).await 128 | } 129 | 130 | #[instrument(skip(self))] 131 | pub async fn shutdown(&self) -> Result<(), Error> { 132 | self.handle.shutdown().await 133 | } 134 | } 135 | 136 | impl ActorHandle { 137 | pub fn id(&self) -> usize { 138 | self.id.load(Ordering::Relaxed) 139 | } 140 | 141 | pub fn set_id(&self, id: usize) { 142 | self.id.store(id, Ordering::Relaxed); 143 | } 144 | 145 | /// Enqueue the packet and send it to the client connected to this actor 146 | #[instrument(skip(self, packet))] 147 | pub async fn send(&self, packet: P) -> Result<(), P::Error> { 148 | let msg = packet.encode()?; 149 | self.tx.send(msg.into()).map_err(Into::into).await?; 150 | Ok(()) 151 | } 152 | 153 | /// Enqueue the packets and send it all at once to the client connected to 154 | /// this actor 155 | #[instrument(skip(self, packets))] 156 | pub async fn send_all(&self, packets: I) -> Result<(), P::Error> 157 | where 158 | P: PacketEncode, 159 | I: IntoIterator, 160 | { 161 | let tasks = packets 162 | .into_iter() 163 | .flat_map(|packet| packet.encode().map(|msg| msg.into())) 164 | .map(|msg| self.tx.send(msg).map_err(crate::Error::from)); 165 | // Wait for all the messages to be sent (in order) 166 | for task in tasks { 167 | task.await?; 168 | } 169 | Ok(()) 170 | } 171 | 172 | #[instrument(skip(self))] 173 | pub async fn generate_keys(&self, seed: u64) -> Result<(), Error> { 174 | let msg = Message::GenerateKeys(seed); 175 | self.tx.send(msg).await?; 176 | Ok(()) 177 | } 178 | 179 | #[instrument(skip(self))] 180 | pub async fn shutdown(&self) -> Result<(), Error> { 181 | self.tx.send(Message::Shutdown).await?; 182 | Ok(()) 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /server/game/src/main.rs: -------------------------------------------------------------------------------- 1 | //! This program encapsulates the game server. 2 | //! The game server is designed to accept authenticated data from the 3 | //! account server, load the player's character data, and control the game 4 | //! world environment. Any game structures involving location and the map 5 | //! are processed on this server. Entity intelligence is processed by this 6 | //! server as well. 7 | 8 | use async_trait::async_trait; 9 | use std::env; 10 | use tq_network::{Actor, ActorState as _, PacketHandler, TQCipher}; 11 | use tq_server::TQServer; 12 | 13 | use game::packets::*; 14 | use game::{ActorState, Error, State}; 15 | 16 | struct GameServer; 17 | 18 | #[async_trait] 19 | impl TQServer for GameServer { 20 | type ActorState = ActorState; 21 | type Cipher = TQCipher; 22 | type PacketHandler = Handler; 23 | 24 | /// Get Called right before ending the connection with that client. 25 | /// good chance to clean up anything related to that actor. 26 | #[tracing::instrument(skip(state, actor))] 27 | async fn on_disconnected( 28 | state: &::State, 29 | actor: Actor, 30 | ) -> Result<(), tq_server::Error> { 31 | if let Ok(entity) = actor.try_entity() { 32 | let me = entity.as_character().ok_or(Error::CharacterNotFound)?; 33 | let mymap_id = me.entity().map_id(); 34 | me.save(state).await?; 35 | me.try_screen()?.remove_from_observers().await?; 36 | ActorState::dispose(&actor, actor.handle()).await?; 37 | state.remove_entity(me.id()); 38 | let mymap = state.try_map(mymap_id)?; 39 | mymap.remove_entity(&entity)?; 40 | } 41 | let _ = actor.shutdown().await; 42 | Ok(()) 43 | } 44 | } 45 | 46 | #[derive(Copy, Clone, PacketHandler)] 47 | #[handle(state = State, actor_state = ActorState)] 48 | pub enum Handler { 49 | MsgConnect, 50 | MsgRegister, 51 | MsgTalk, 52 | MsgAction, 53 | MsgItem, 54 | MsgWalk, 55 | MsgTransfer, 56 | MsgNpc, 57 | MsgTaskDialog, 58 | } 59 | 60 | #[tokio::main] 61 | async fn main() -> Result<(), Error> { 62 | dotenvy::dotenv()?; 63 | let log_verbosity = env::var("LOG_VERBOSITY") 64 | .map(|s| s.parse::().unwrap_or(2)) 65 | .unwrap_or(2); 66 | setup_logger(log_verbosity)?; 67 | println!( 68 | r#" 69 | _____ _____ 70 | / __ \ | ___| 71 | | / \/ ___ | |__ _ __ ___ _ _ 72 | | | / _ \ | __|| '_ ` _ \ | | | | 73 | | \__/\| (_) || |___| | | | | || |_| | 74 | \____/ \___/ \____/|_| |_| |_| \__,_| 75 | 76 | 77 | Copyright 2020 Shady Khalifa (@shekohex) 78 | All Rights Reserved. 79 | "# 80 | ); 81 | tracing::info!("Starting Game Server"); 82 | tracing::info!("Initializing server..."); 83 | 84 | tracing::info!("Initializing State .."); 85 | 86 | let static_state = { 87 | let state = State::init().await?; 88 | Box::leak(Box::new(state)) as *mut State 89 | }; 90 | 91 | // SAFETY: We are the only owner of this Box, and we are deref 92 | // it. This happens only once, so no one else can access. 93 | let state = unsafe { &*static_state }; 94 | let realm = tq_db::realm::Realm::by_name(state.pool(), "CoEmu") 95 | .await? 96 | .ok_or(Error::RealmNotFound)?; 97 | let game_port = realm.game_port; 98 | tracing::info!("Game Server will be available on {}", game_port); 99 | 100 | GameServer::run(format!("0.0.0.0:{}", game_port), state).await?; 101 | unsafe { 102 | // SAFETY: We are the only owner of this Box, and we are dropping 103 | // it. This happens at the end of the program, so no one 104 | // else can access. 105 | let state = Box::from_raw(static_state); 106 | state.clean_up().await?; 107 | // State dropped here. 108 | }; 109 | tracing::info!("Shutdown."); 110 | Ok(()) 111 | } 112 | 113 | fn setup_logger(verbosity: i32) -> Result<(), Error> { 114 | use tracing::Level; 115 | use tracing_subscriber::prelude::*; 116 | 117 | let log_level = match verbosity { 118 | 0 => Level::ERROR, 119 | 1 => Level::WARN, 120 | 2 => Level::INFO, 121 | 3 => Level::DEBUG, 122 | _ => Level::TRACE, 123 | }; 124 | 125 | let logger = tracing_subscriber::fmt::layer().pretty().with_target(true); 126 | let env_filter = tracing_subscriber::EnvFilter::from_default_env() 127 | .add_directive(format!("tq_db={}", log_level).parse().unwrap()) 128 | .add_directive(format!("tq_serde={}", log_level).parse().unwrap()) 129 | .add_directive(format!("tq_crypto={}", log_level).parse().unwrap()) 130 | .add_directive(format!("tq_codec={}", log_level).parse().unwrap()) 131 | .add_directive(format!("tq_network={}", log_level).parse().unwrap()) 132 | .add_directive(format!("tq_server={}", log_level).parse().unwrap()) 133 | .add_directive(format!("game={}", log_level).parse().unwrap()) 134 | .add_directive(format!("game_server={}", log_level).parse().unwrap()); 135 | 136 | #[cfg(feature = "console")] 137 | let env_filter = env_filter 138 | .add_directive("tokio=trace".parse().unwrap()) 139 | .add_directive("runtime=trace".parse().unwrap()); 140 | 141 | #[cfg(feature = "console")] 142 | let console_layer = console_subscriber::ConsoleLayer::builder().with_default_env().spawn(); 143 | 144 | let registry = tracing_subscriber::registry().with(env_filter).with(logger); 145 | 146 | #[cfg(feature = "console")] 147 | let registry = registry.with(console_layer); 148 | 149 | registry.init(); 150 | Ok(()) 151 | } 152 | -------------------------------------------------------------------------------- /crates/wasm-builder/src/prerequisites.rs: -------------------------------------------------------------------------------- 1 | use crate::{write_file_if_changed, CargoCommand, CargoCommandVersioned}; 2 | 3 | use console::style; 4 | use std::fs; 5 | use std::path::Path; 6 | use tempfile::tempdir; 7 | 8 | /// Print an error message. 9 | fn print_error_message(message: &str) -> String { 10 | if super::color_output_enabled() { 11 | style(message).red().bold().to_string() 12 | } else { 13 | message.into() 14 | } 15 | } 16 | 17 | /// Checks that all prerequisites are installed. 18 | /// 19 | /// Returns the versioned cargo command on success. 20 | pub(crate) fn check() -> Result { 21 | let cargo_command = crate::get_cargo_command(); 22 | 23 | if !cargo_command.supports_substrate_wasm_env() { 24 | return Err(print_error_message( 25 | "Cannot compile the WASM runtime: no compatible Rust compiler found!\n\ 26 | Install at least Rust 1.68.0 or a recent nightly version.", 27 | )); 28 | } 29 | 30 | check_wasm_toolchain_installed(cargo_command) 31 | } 32 | 33 | /// Creates a minimal dummy crate at the given path and returns the manifest 34 | /// path. 35 | fn create_minimal_crate(project_dir: &Path) -> std::path::PathBuf { 36 | fs::create_dir_all(project_dir.join("src")).expect("Creating src dir does not fail; qed"); 37 | 38 | let manifest_path = project_dir.join("Cargo.toml"); 39 | write_file_if_changed( 40 | &manifest_path, 41 | r#" 42 | [package] 43 | name = "wasm-test" 44 | version = "1.0.0" 45 | edition = "2021" 46 | 47 | [workspace] 48 | "#, 49 | ); 50 | 51 | write_file_if_changed(project_dir.join("src/main.rs"), "fn main() {}"); 52 | manifest_path 53 | } 54 | 55 | fn check_wasm_toolchain_installed(cargo_command: CargoCommand) -> Result { 56 | let temp = tempdir().expect("Creating temp dir does not fail; qed"); 57 | let manifest_path = create_minimal_crate(temp.path()).display().to_string(); 58 | 59 | let prepare_command = |subcommand| { 60 | let mut cmd = cargo_command.command(); 61 | // Chdir to temp to avoid including project's .cargo/config.toml 62 | // by accident - it can happen in some CI environments. 63 | cmd.current_dir(&temp); 64 | cmd.args([ 65 | subcommand, 66 | "--target=wasm32-unknown-unknown", 67 | "--manifest-path", 68 | &manifest_path, 69 | ]); 70 | 71 | if super::color_output_enabled() { 72 | cmd.arg("--color=always"); 73 | } 74 | 75 | // manually set the `CARGO_TARGET_DIR` to prevent a cargo deadlock 76 | let target_dir = temp.path().join("target").display().to_string(); 77 | cmd.env("CARGO_TARGET_DIR", &target_dir); 78 | 79 | // Make sure the host's flags aren't used here, e.g. if an alternative 80 | // linker is specified in the RUSTFLAGS then the check we do 81 | // here will break unless we clear these. 82 | cmd.env_remove("CARGO_ENCODED_RUSTFLAGS"); 83 | cmd.env_remove("RUSTFLAGS"); 84 | cmd 85 | }; 86 | 87 | let err_msg = print_error_message("Rust WASM toolchain is not properly installed; please install it!"); 88 | let build_result = prepare_command("build").output().map_err(|_| err_msg.clone())?; 89 | if !build_result.status.success() { 90 | return match String::from_utf8(build_result.stderr) { 91 | Ok(ref err) if err.contains("the `wasm32-unknown-unknown` target may not be installed") => { 92 | Err(print_error_message( 93 | "Cannot compile the WASM runtime: the `wasm32-unknown-unknown` target is not installed!\n\ 94 | You can install it with `rustup target add wasm32-unknown-unknown` if you're using `rustup`.", 95 | )) 96 | }, 97 | 98 | // Apparently this can happen when we're running on a non Tier 1 platform. 99 | Ok(ref err) if err.contains("linker `rust-lld` not found") => Err(print_error_message( 100 | "Cannot compile the WASM runtime: `rust-lld` not found!", 101 | )), 102 | 103 | Ok(ref err) => Err(format!( 104 | "{}\n\n{}\n{}\n{}{}\n", 105 | err_msg, 106 | style("Further error information:").yellow().bold(), 107 | style("-".repeat(60)).yellow().bold(), 108 | err, 109 | style("-".repeat(60)).yellow().bold(), 110 | )), 111 | 112 | Err(_) => Err(err_msg), 113 | }; 114 | } 115 | 116 | let mut run_cmd = prepare_command("rustc"); 117 | run_cmd.args(["-q", "--", "--version"]); 118 | 119 | let version = run_cmd 120 | .output() 121 | .ok() 122 | .and_then(|o| String::from_utf8(o.stdout).ok()) 123 | .unwrap_or_else(|| "unknown rustc version".into()); 124 | 125 | if crate::build_std_required() { 126 | let mut sysroot_cmd = prepare_command("rustc"); 127 | sysroot_cmd.args(["-q", "--", "--print", "sysroot"]); 128 | if let Some(sysroot) = sysroot_cmd.output().ok().and_then(|o| String::from_utf8(o.stdout).ok()) { 129 | let src_path = Path::new(sysroot.trim()) 130 | .join("lib") 131 | .join("rustlib") 132 | .join("src") 133 | .join("rust"); 134 | if !src_path.exists() { 135 | return Err(print_error_message( 136 | "Cannot compile the WASM runtime: no standard library sources found!\n\ 137 | You can install them with `rustup component add rust-src` if you're using `rustup`.", 138 | )); 139 | } 140 | } 141 | } 142 | 143 | Ok(CargoCommandVersioned::new(cargo_command, version)) 144 | } 145 | -------------------------------------------------------------------------------- /data/Maps/Portals.csv: -------------------------------------------------------------------------------- 1 | Id,FromMapId,FromX,FromY,ToMapId,ToX,ToY 2 | 0,1000,77,319,1001,312,646 3 | 1,1000,977,668,1002,69,473 4 | 2,1000,163,471,1077,756,873 5 | 3,1001,312,651,1000,85,323 6 | 4,1002,401,387,1004,51,70 7 | 5,1002,382,387,1006,37,31 8 | 6,1002,963,557,1011,11,376 9 | 7,1002,223,196,1015,1010,710 10 | 8,1002,555,964,1020,381,21 11 | 9,1004,51,73,1002,403,394 12 | 10,1005,51,73,1002,455,295 13 | 11,1006,41,31,1002,386,391 14 | 12,1007,25,28,1002,453,335 15 | 13,1008,22,29,1002,419,356 16 | 14,1011,5,376,1002,958,555 17 | 15,1011,379,23,1013,54,81 18 | 16,1011,448,822,1020,919,559 19 | 17,1011,855,482,1076,11,361 20 | 18,1012,531,534,1020,23,383 21 | 19,1013,55,87,1011,379,28 22 | 20,1013,57,21,1014,80,40 23 | 21,1014,77,32,1013,59,28 24 | 22,1014,137,90,1016,50,82 25 | 23,1015,759,510,1015,534,693 26 | 24,1015,537,690,1015,759,513 27 | 25,1015,485,782,1015,570,853 28 | 26,1015,565,851,1015,649,837 29 | 27,1015,644,837,1015,531,593 30 | 28,1015,587,601,1015,584,674 31 | 29,1015,587,679,1015,742,843 32 | 30,1015,740,837,1015,722,926 33 | 31,1015,668,908,1015,564,800 34 | 32,1015,437,658,1015,346,582 35 | 33,1015,340,558,1015,511,533 36 | 34,1015,296,597,1015,195,418 37 | 35,1015,154,414,1015,199,484 38 | 36,1015,205,487,1015,294,561 39 | 37,1015,231,431,1015,300,460 40 | 38,1015,311,463,1015,511,533 41 | 39,1015,442,586,1015,485,675 42 | 40,1015,355,442,1015,285,352 43 | 41,1015,287,358,1015,288,260 44 | 42,1015,281,254,1015,200,224 45 | 43,1015,201,229,1015,150,311 46 | 44,1015,153,306,1015,200,224 47 | 45,1015,299,201,1015,288,260 48 | 46,1015,374,222,1015,411,333 49 | 47,1015,371,165,1015,455,211 50 | 48,1015,461,215,1015,496,280 51 | 49,1015,496,287,1015,611,390 52 | 50,1015,612,397,1015,570,460 53 | 51,1015,1018,710,1002,232,190 54 | 52,1015,475,347,1015,344,206 55 | 53,1015,789,480,1078,184,549 56 | 54,1016,50,88,1014,131,86 57 | 55,1020,926,557,1011,453,819 58 | 56,1020,376,8,1002,555,957 59 | 57,1020,11,377,1012,532,529 60 | 58,1020,610,874,1075,360,10 61 | 59,1023,132,33,4001,260,210 62 | 60,1023,190,55,1024,303,421 63 | 61,1024,307,422,1023,193,58 64 | 62,1025,27,65,1011,933,565 65 | 63,1026,136,98,1020,528,890 66 | 64,1027,136,99,1000,314,9 67 | 65,1028,156,90,1002,53,399 68 | 66,1042,37,58,1002,342,723 69 | 67,1052,182,281,1052,191,228 70 | 68,1052,99,171,1052,191,228 71 | 69,1052,170,99,1052,191,228 72 | 70,1052,272,176,1052,191,228 73 | 71,1052,308,283,1052,191,228 74 | 72,1075,360,5,1020,607,870 75 | 73,1076,2,360,1011,850,485 76 | 74,1077,756,877,1000,159,467 77 | 75,1081,251,166,1002,439,385 78 | 76,1090,170,137,1002,439,385 79 | 77,1201,594,397,1202,8,302 80 | 78,1201,3,201,1213,1072,798 81 | 79,1202,694,398,1205,8,161 82 | 80,1202,3,301,1201,592,398 83 | 81,1204,1356,1199,1213,10,282 84 | 82,1204,3,160,1216,950,478 85 | 83,1205,1355,1198,1077,362,12 86 | 84,1205,3,160,1202,691,398 87 | 85,1207,958,1214,1215,934,565 88 | 86,1207,260,4,1208,9,502 89 | 87,1208,3,500,1207,261,10 90 | 88,1210,4,329,1211,951,558 91 | 89,1211,955,558,1210,8,329 92 | 90,1211,3,400,1212,1017,1294 93 | 91,1212,781,841,1212,705,921 94 | 92,1212,708,921,1212,781,838 95 | 93,1212,579,824,1212,539,752 96 | 94,1212,542,755,1212,582,827 97 | 95,1212,476,688,1212,369,651 98 | 96,1212,372,651,1212,479,691 99 | 97,1212,352,512,1212,404,451 100 | 98,1212,404,454,1212,349,512 101 | 99,1212,392,387,1212,335,282 102 | 100,1212,338,285,1212,395,390 103 | 101,1212,310,171,1212,378,153 104 | 102,1212,375,153,1212,310,174 105 | 103,1212,614,314,1212,623,402 106 | 104,1212,620,399,1212,611,311 107 | 105,1212,674,415,1212,713,377 108 | 106,1212,710,377,1212,674,418 109 | 107,1212,837,458,1212,846,558 110 | 108,1212,846,555,1212,834,455 111 | 109,1212,898,828,1212,872,850 112 | 110,1212,872,847,1212,900,826 113 | 111,1212,861,846,1212,808,823 114 | 112,1212,811,826,1212,864,846 115 | 113,1212,871,856,1212,899,894 116 | 114,1212,896,891,1212,871,853 117 | 115,1212,810,677,1212,690,667 118 | 116,1212,693,667,1212,813,677 119 | 117,1212,942,1015,1212,903,1041 120 | 118,1212,906,1041,1212,942,1012 121 | 119,1212,1025,1038,1212,1087,1124 122 | 120,1212,1083,1120,1212,1022,1035 123 | 121,1212,1049,838,1212,1138,895 124 | 122,1212,1135,892,1212,1046,835 125 | 123,1212,1015,1295,1211,10,401 126 | 124,1213,1075,798,1201,10,202 127 | 125,1213,4,281,1204,1351,1199 128 | 126,1214,640,3,1216,479,950 129 | 127,1214,799,1434,1217,363,13 130 | 128,1215,938,567,1207,958,1211 131 | 129,1215,3,376,1216,481,10 132 | 130,1216,954,479,1204,9,161 133 | 131,1216,479,954,1214,641,9 134 | 132,1216,480,3,1215,9,376 135 | 133,1217,360,5,1214,799,1432 136 | 134,1218,27,65,1210,715,1036 137 | 135,1219,781,841,1219,705,921 138 | 136,1219,708,921,1219,781,838 139 | 137,1219,579,824,1219,539,752 140 | 138,1219,542,755,1219,582,827 141 | 139,1219,476,688,1219,369,651 142 | 140,1219,372,651,1219,479,691 143 | 141,1219,352,512,1219,404,451 144 | 142,1219,404,454,1219,349,512 145 | 143,1219,392,387,1219,335,282 146 | 144,1219,338,285,1219,395,390 147 | 145,1219,310,171,1219,378,153 148 | 146,1219,375,153,1219,310,174 149 | 147,1219,614,314,1219,623,402 150 | 148,1219,620,399,1219,611,311 151 | 149,1219,674,415,1219,713,377 152 | 150,1219,710,377,1219,674,418 153 | 151,1219,837,458,1219,846,558 154 | 152,1219,846,555,1219,834,455 155 | 153,1219,898,828,1219,872,850 156 | 154,1219,872,847,1219,900,826 157 | 155,1219,861,846,1219,808,823 158 | 156,1219,811,826,1219,864,846 159 | 157,1219,871,856,1219,899,894 160 | 158,1219,896,891,1219,871,853 161 | 159,1219,810,677,1219,690,667 162 | 160,1219,693,667,1219,813,677 163 | 161,1219,942,1015,1219,903,1041 164 | 162,1219,906,1041,1219,942,1012 165 | 163,1219,1025,1038,1219,1087,1124 166 | 164,1219,1083,1120,1219,1022,1035 167 | 165,1219,1049,838,1219,1138,895 168 | 166,1219,1135,892,1219,1046,835 169 | 167,1219,1015,1295,1212,378,37 170 | 168,1300,312,651,1001,674,349 171 | 169,1511,51,73,1002,439,385 172 | 170,4000,59,249,1001,674,344 173 | 171,4000,637,372,4001,41,281 174 | 172,4001,39,277,4000,637,375 175 | --------------------------------------------------------------------------------