diff --git a/.gitignore b/.gitignore index aae89fc..a638136 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ config.json *.bin *.db +config.yaml diff --git a/Cargo.lock b/Cargo.lock index a2de563..e05e42a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -217,6 +217,12 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + [[package]] name = "fallible-iterator" version = "0.3.0" @@ -419,6 +425,16 @@ dependencies = [ "want", ] +[[package]] +name = "indexmap" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3954d50fe15b02142bf25d3b8bdadb634ec3948f103d04ffe3031bc8fe9d7058" +dependencies = [ + "equivalent", + "hashbrown", +] + [[package]] name = "itoa" version = "1.0.15" @@ -438,6 +454,7 @@ dependencies = [ "rusqlite", "serde", "serde_json", + "serde_yaml_ng", "tokio", "tracing", "tracing-subscriber", @@ -742,6 +759,19 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_yaml_ng" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4db627b98b36d4203a7b458cf3573730f2bb591b28871d916dfa9efabfd41f" +dependencies = [ + "indexmap", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + [[package]] name = "sha1" version = "0.10.6" @@ -924,6 +954,12 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + [[package]] name = "vcpkg" version = "0.2.15" diff --git a/Cargo.toml b/Cargo.toml index 9337b30..16c4055 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,6 +44,7 @@ axum = { version = "0.6", features = ["json", "tokio", "http1", "form", "query", # Serialization used in almost every server serde = { version = "1.0", features = ["derive"], default-features = false } serde_json = { version = "1.0", default-features = false } +serde_yaml_ng = "0.10" # Async runtime tokio = { version = "1.37", features = ["macros", "rt", "rt-multi-thread", "io-util"], default-features = false } diff --git a/USAGE.md b/USAGE.md index 79d35f4..487ae94 100644 --- a/USAGE.md +++ b/USAGE.md @@ -15,21 +15,13 @@ Kawari is designed to be easy to run, with the goal of being accessible to anyon ## Setup -Build Kawari with `cargo build`. Afterwards, create a `config.json` in the current directory that looks like this: +Build Kawari with `cargo build`. Afterwards, create a `config.yaml` in the current directory. Currently the minimal config you need to run most services looks like this: -```json -{ - "worlds_open": true, - "login_open": true, - "supported_platforms": ["win32"], - "boot_patches_location": "/path/to/boot/patches", - "game_location": "/path/to/game" -} +```yaml +game_location: pathtogamedir ``` -All values in the config are optional, but some may be required for certain services to work correctly. - -Finally, run Kawari with the helper script: +More configuration options can be found in `config.rs`, such as changing the ports services run on. Finally, run Kawari with the helper script: ```shell $ ./run.sh diff --git a/src/bin/kawari-admin.rs b/src/bin/kawari-admin.rs index cb54233..f045f0f 100644 --- a/src/bin/kawari-admin.rs +++ b/src/bin/kawari-admin.rs @@ -1,5 +1,3 @@ -use std::net::SocketAddr; - use axum::response::{Html, Redirect}; use axum::routing::post; use axum::{Router, extract::Form, routing::get}; @@ -20,7 +18,7 @@ async fn root() -> Html { let environment = setup_default_environment(); let template = environment.get_template("admin.html").unwrap(); - Html(template.render(context! { worlds_open => config.worlds_open, login_open => config.login_open, boot_patch_location => config.boot_patches_location }).unwrap()) + Html(template.render(context! { worlds_open => config.frontier.worlds_open, login_open => config.frontier.login_open, boot_patch_location => config.boot_patches_location }).unwrap()) } #[derive(Deserialize, Debug)] @@ -37,22 +35,22 @@ async fn apply(Form(input): Form) -> Redirect { let mut config = get_config(); if let Some(gate_open) = input.worlds_open { - config.worlds_open = gate_open == "on"; + config.frontier.worlds_open = gate_open == "on"; } else { - config.worlds_open = false; + config.frontier.worlds_open = false; } if let Some(gate_open) = input.login_open { - config.login_open = gate_open == "on"; + config.frontier.login_open = gate_open == "on"; } else { - config.login_open = false; + config.frontier.login_open = false; } if let Some(boot_patch_location) = input.boot_patch_location { config.boot_patches_location = boot_patch_location; } - serde_json::to_writer(&std::fs::File::create("config.json").unwrap(), &config) + serde_yaml_ng::to_writer(&std::fs::File::create("config.yaml").unwrap(), &config) .expect("TODO: panic message"); Redirect::to("/") @@ -66,8 +64,10 @@ async fn main() { .route("/", get(root)) .route("/apply", post(apply)); - let addr = SocketAddr::from(([127, 0, 0, 1], 5800)); - tracing::info!("Admin server started on {}", addr); + let config = get_config(); + + let addr = config.admin.get_socketaddr(); + tracing::info!("Admin server started on {addr}"); axum::Server::bind(&addr) .serve(app.into_make_service()) .await diff --git a/src/bin/kawari-frontier.rs b/src/bin/kawari-frontier.rs index d41d5e2..a6a9406 100644 --- a/src/bin/kawari-frontier.rs +++ b/src/bin/kawari-frontier.rs @@ -1,5 +1,3 @@ -use std::net::SocketAddr; - use axum::{Json, Router, routing::get}; use kawari::config::get_config; use serde::{Deserialize, Serialize}; @@ -14,7 +12,7 @@ async fn get_login_status() -> Json { let config = get_config(); Json(GateStatus { - status: config.login_open.into(), + status: config.frontier.login_open.into(), }) } @@ -23,7 +21,7 @@ async fn get_world_status() -> Json { let config = get_config(); Json(GateStatus { - status: config.worlds_open.into(), + status: config.frontier.worlds_open.into(), }) } @@ -76,8 +74,10 @@ async fn main() { .route("/worldStatus/login_status.json", get(get_login_status)) .route("/news/headline.json", get(get_headline)); - let addr = SocketAddr::from(([127, 0, 0, 1], 5857)); - tracing::info!("Frontier server started on {}", addr); + let config = get_config(); + + let addr = config.frontier.get_socketaddr(); + tracing::info!("Frontier server started on {addr}"); axum::Server::bind(&addr) .serve(app.into_make_service()) .await diff --git a/src/bin/kawari-lobby.rs b/src/bin/kawari-lobby.rs index 1f3552e..581daad 100644 --- a/src/bin/kawari-lobby.rs +++ b/src/bin/kawari-lobby.rs @@ -1,6 +1,7 @@ use kawari::common::custom_ipc::CustomIpcData; use kawari::common::custom_ipc::CustomIpcSegment; use kawari::common::custom_ipc::CustomIpcType; +use kawari::config::get_config; use kawari::lobby::LobbyConnection; use kawari::lobby::ipc::{ CharacterDetails, ClientLobbyIpcData, LobbyCharacterActionKind, ServerLobbyIpcData, @@ -18,9 +19,13 @@ use tokio::net::TcpListener; async fn main() { tracing_subscriber::fmt::init(); - let listener = TcpListener::bind("127.0.0.1:7000").await.unwrap(); + let config = get_config(); - tracing::info!("Lobby server started on 127.0.0.1:7000"); + let addr = config.lobby.get_socketaddr(); + + let listener = TcpListener::bind(addr).await.unwrap(); + + tracing::info!("Lobby server started on {addr}"); loop { let (socket, _) = listener.accept().await.unwrap(); diff --git a/src/bin/kawari-login.rs b/src/bin/kawari-login.rs index 6690095..d0bc182 100644 --- a/src/bin/kawari-login.rs +++ b/src/bin/kawari-login.rs @@ -1,10 +1,10 @@ -use std::net::SocketAddr; use std::sync::Arc; use axum::extract::{Query, State}; use axum::response::{Html, Redirect}; use axum::routing::post; use axum::{Form, Router, routing::get}; +use kawari::config::get_config; use kawari::login::{LoginDatabase, LoginError}; use serde::Deserialize; @@ -128,8 +128,10 @@ async fn main() { .route("/private/check_session", get(check_session)) .with_state(state); - let addr = SocketAddr::from(([127, 0, 0, 1], 6700)); - tracing::info!("Login server started on {}", addr); + let config = get_config(); + + let addr = config.login.get_socketaddr(); + tracing::info!("Login server started on {addr}"); axum::Server::bind(&addr) .serve(app.into_make_service()) .await diff --git a/src/bin/kawari-patch.rs b/src/bin/kawari-patch.rs index d75cdb5..d5e0df1 100644 --- a/src/bin/kawari-patch.rs +++ b/src/bin/kawari-patch.rs @@ -1,6 +1,5 @@ use std::cmp::Ordering; use std::fs::read_dir; -use std::net::SocketAddr; use axum::extract::Path; use axum::http::{HeaderMap, StatusCode}; @@ -132,8 +131,10 @@ async fn main() { get(verify_boot), ); // NOTE: for future programmers, this is a wildcard because axum hates the /version/?time=blah format. - let addr = SocketAddr::from(([127, 0, 0, 1], 6900)); - tracing::info!("Patch server started on {}", addr); + let config = get_config(); + + let addr = config.patch.get_socketaddr(); + tracing::info!("Patch server started on {addr}"); axum::Server::bind(&addr) .serve(app.into_make_service()) .await diff --git a/src/bin/kawari-web.rs b/src/bin/kawari-web.rs index 43339aa..67aba3e 100644 --- a/src/bin/kawari-web.rs +++ b/src/bin/kawari-web.rs @@ -1,5 +1,3 @@ -use std::net::SocketAddr; - use axum::response::Html; use axum::{Router, routing::get}; use kawari::config::get_config; @@ -37,7 +35,7 @@ async fn world_status() -> Html { let template = environment.get_template("worldstatus.html").unwrap(); Html( template - .render(context! { login_open => config.login_open, worlds_open => config.worlds_open }) + .render(context! { login_open => config.frontier.login_open, worlds_open => config.frontier.worlds_open }) .unwrap(), ) } @@ -52,8 +50,10 @@ async fn main() { .route("/register", get(register)) .route("/worldstatus", get(world_status)); - let addr = SocketAddr::from(([127, 0, 0, 1], 5801)); - tracing::info!("Web server started on {}", addr); + let config = get_config(); + + let addr = config.web.get_socketaddr(); + tracing::info!("Web server started on {addr}"); axum::Server::bind(&addr) .serve(app.into_make_service()) .await diff --git a/src/bin/kawari-world.rs b/src/bin/kawari-world.rs index 1c77492..598fd01 100644 --- a/src/bin/kawari-world.rs +++ b/src/bin/kawari-world.rs @@ -29,9 +29,13 @@ use tokio::net::TcpListener; async fn main() { tracing_subscriber::fmt::init(); - let listener = TcpListener::bind("127.0.0.1:7100").await.unwrap(); + let config = get_config(); - tracing::info!("World server started on 127.0.0.1:7100"); + let addr = config.world.get_socketaddr(); + + let listener = TcpListener::bind(addr).await.unwrap(); + + tracing::info!("World server started on {addr}"); let database = Arc::new(WorldDatabase::new()); diff --git a/src/config.rs b/src/config.rs index 4993201..d4264bb 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,13 +1,200 @@ +use std::{ + net::{IpAddr, SocketAddr}, + str::FromStr, +}; + use serde::{Deserialize, Serialize}; +/// Configuration for the admin server. +#[derive(Serialize, Deserialize)] +pub struct AdminConfig { + pub port: u16, + pub listen_address: String, +} + +impl Default for AdminConfig { + fn default() -> Self { + Self { + port: 5800, + listen_address: "127.0.0.1".to_string(), + } + } +} + +impl AdminConfig { + /// Returns the configured IP address & port as a `SocketAddr`. + pub fn get_socketaddr(&self) -> SocketAddr { + SocketAddr::from(( + IpAddr::from_str(&self.listen_address).expect("Invalid IP address format in config!"), + self.port, + )) + } +} + +/// Configuration for the frontier server. +#[derive(Serialize, Deserialize)] +pub struct FrontierConfig { + pub port: u16, + pub listen_address: String, + pub worlds_open: bool, + pub login_open: bool, +} + +impl Default for FrontierConfig { + fn default() -> Self { + Self { + port: 5857, + listen_address: "127.0.0.1".to_string(), + worlds_open: true, + login_open: true, + } + } +} + +impl FrontierConfig { + /// Returns the configured IP address & port as a `SocketAddr`. + pub fn get_socketaddr(&self) -> SocketAddr { + SocketAddr::from(( + IpAddr::from_str(&self.listen_address).expect("Invalid IP address format in config!"), + self.port, + )) + } +} + +/// Configuration for the lobby server. +#[derive(Serialize, Deserialize)] +pub struct LobbyConfig { + pub port: u16, + pub listen_address: String, +} + +impl Default for LobbyConfig { + fn default() -> Self { + Self { + port: 7000, + listen_address: "127.0.0.1".to_string(), + } + } +} + +impl LobbyConfig { + /// Returns the configured IP address & port as a `SocketAddr`. + pub fn get_socketaddr(&self) -> SocketAddr { + SocketAddr::from(( + IpAddr::from_str(&self.listen_address).expect("Invalid IP address format in config!"), + self.port, + )) + } +} + +/// Configuration for the login server. +#[derive(Serialize, Deserialize)] +pub struct LoginConfig { + pub port: u16, + pub listen_address: String, +} + +impl Default for LoginConfig { + fn default() -> Self { + Self { + port: 6700, + listen_address: "127.0.0.1".to_string(), + } + } +} + +impl LoginConfig { + /// Returns the configured IP address & port as a `SocketAddr`. + pub fn get_socketaddr(&self) -> SocketAddr { + SocketAddr::from(( + IpAddr::from_str(&self.listen_address).expect("Invalid IP address format in config!"), + self.port, + )) + } +} + +/// Configuration for the patch server. +#[derive(Serialize, Deserialize)] +pub struct PatchConfig { + pub port: u16, + pub listen_address: String, +} + +impl Default for PatchConfig { + fn default() -> Self { + Self { + port: 6900, + listen_address: "127.0.0.1".to_string(), + } + } +} + +impl PatchConfig { + /// Returns the configured IP address & port as a `SocketAddr`. + pub fn get_socketaddr(&self) -> SocketAddr { + SocketAddr::from(( + IpAddr::from_str(&self.listen_address).expect("Invalid IP address format in config!"), + self.port, + )) + } +} + +/// Configuration for the web server. +#[derive(Serialize, Deserialize)] +pub struct WebConfig { + pub port: u16, + pub listen_address: String, +} + +impl Default for WebConfig { + fn default() -> Self { + Self { + port: 5801, + listen_address: "127.0.0.1".to_string(), + } + } +} + +impl WebConfig { + /// Returns the configured IP address & port as a `SocketAddr`. + pub fn get_socketaddr(&self) -> SocketAddr { + SocketAddr::from(( + IpAddr::from_str(&self.listen_address).expect("Invalid IP address format in config!"), + self.port, + )) + } +} + +/// Configuration for the world server. +#[derive(Serialize, Deserialize)] +pub struct WorldConfig { + pub port: u16, + pub listen_address: String, +} + +impl Default for WorldConfig { + fn default() -> Self { + Self { + port: 7100, + listen_address: "127.0.0.1".to_string(), + } + } +} + +impl WorldConfig { + /// Returns the configured IP address & port as a `SocketAddr`. + pub fn get_socketaddr(&self) -> SocketAddr { + SocketAddr::from(( + IpAddr::from_str(&self.listen_address).expect("Invalid IP address format in config!"), + self.port, + )) + } +} + +/// Global and all-encompassing config. +/// Settings that affect all servers belong here. #[derive(Serialize, Deserialize)] pub struct Config { - #[serde(default)] - pub worlds_open: bool, - - #[serde(default)] - pub login_open: bool, - #[serde(default = "default_supported_platforms")] pub supported_platforms: Vec, @@ -16,16 +203,42 @@ pub struct Config { #[serde(default)] pub game_location: String, + + #[serde(default)] + pub admin: AdminConfig, + + #[serde(default)] + pub frontier: FrontierConfig, + + #[serde(default)] + pub lobby: LobbyConfig, + + #[serde(default)] + pub login: LoginConfig, + + #[serde(default)] + pub patch: PatchConfig, + + #[serde(default)] + pub web: WebConfig, + + #[serde(default)] + pub world: WorldConfig, } impl Default for Config { fn default() -> Self { Self { - worlds_open: false, - login_open: false, boot_patches_location: String::new(), supported_platforms: default_supported_platforms(), game_location: String::new(), + admin: AdminConfig::default(), + frontier: FrontierConfig::default(), + lobby: LobbyConfig::default(), + login: LoginConfig::default(), + patch: PatchConfig::default(), + web: WebConfig::default(), + world: WorldConfig::default(), } } } @@ -41,8 +254,8 @@ fn default_supported_platforms() -> Vec { } pub fn get_config() -> Config { - if let Ok(data) = std::fs::read_to_string("config.json") { - serde_json::from_str(&data).expect("Failed to parse") + if let Ok(data) = std::fs::read_to_string("config.yaml") { + serde_yaml_ng::from_str(&data).expect("Failed to parse") } else { Config::default() } diff --git a/src/lobby/connection.rs b/src/lobby/connection.rs index 439e07c..911ec11 100644 --- a/src/lobby/connection.rs +++ b/src/lobby/connection.rs @@ -9,6 +9,7 @@ use crate::{ custom_ipc::{CustomIpcData, CustomIpcSegment, CustomIpcType}, timestamp_secs, }, + config::get_config, oodle::OodleNetwork, packet::{ CompressionType, ConnectionType, PacketSegment, PacketState, SegmentType, @@ -278,17 +279,15 @@ impl LobbyConnection { /// Send the host information for the world server to the client. pub async fn send_enter_world(&mut self, sequence: u64, content_id: u64, actor_id: u32) { - let Some(session_id) = &self.session_id else { - panic!("Missing session id!"); - }; + let config = get_config(); let enter_world = ServerLobbyIpcData::LobbyEnterWorld { sequence, actor_id, content_id, token: String::new(), - port: 7100, - host: "127.0.0.1".to_string(), + port: config.world.port, + host: config.world.listen_address, }; let ipc = ServerLobbyIpcSegment { @@ -339,7 +338,11 @@ impl LobbyConnection { /// Sends a custom IPC packet to the world server, meant for private server-to-server communication. /// Returns the first custom IPC segment returned. pub async fn send_custom_world_packet(segment: CustomIpcSegment) -> Option { - let mut stream = TcpStream::connect("127.0.0.1:7100").await.unwrap(); + let config = get_config(); + + let addr = config.world.get_socketaddr(); + + let mut stream = TcpStream::connect(addr).await.unwrap(); let mut packet_state = PacketState { client_key: None,