From 911de537e1b19d913ed01960d56ae945f69016cc Mon Sep 17 00:00:00 2001 From: dank_meme01 <42031238+dankmeme01@users.noreply.github.com> Date: Tue, 28 Nov 2023 15:55:47 +0100 Subject: [PATCH] Potentially breaking changes. --- .gitignore | 4 + README.md | 21 +- mod.json | 19 +- server/central/Cargo.toml | 3 +- server/central/src/config.rs | 10 +- server/central/src/ip_blocker.rs | 58 ++-- server/central/src/logger.rs | 56 ++-- server/central/src/main.rs | 4 +- server/central/src/web/routes/auth.rs | 4 +- server/game/Cargo.toml | 1 - server/game/src/data/bytebufferext.rs | 255 +++++++++++++----- .../src/data/packets/client/connection.rs | 12 +- server/game/src/data/packets/client/game.rs | 24 +- server/game/src/data/packets/mod.rs | 6 +- .../src/data/packets/server/connection.rs | 24 +- server/game/src/data/packets/server/game.rs | 22 +- server/game/src/data/types/audio_frame.rs | 20 +- server/game/src/data/types/cocos.rs | 6 + server/game/src/data/types/fast_string.rs | 172 ++++++++++++ server/game/src/data/types/gd.rs | 26 +- server/game/src/data/types/mod.rs | 2 + server/game/src/logger.rs | 14 +- server/game/src/main.rs | 28 +- server/game/src/managers/player.rs | 10 + server/game/src/server.rs | 101 +++++-- .../src/server_thread/handlers/connection.rs | 19 +- .../game/src/server_thread/handlers/game.rs | 77 ++++-- server/game/src/server_thread/handlers/mod.rs | 30 ++- server/game/src/server_thread/mod.rs | 214 ++++++++++++--- server/readme.md | 6 +- server/shared/src/lib.rs | 4 +- src/audio/audio_frame.cpp | 50 ++-- src/audio/audio_frame.hpp | 26 +- src/audio/audio_manager.cpp | 212 ++++++++++----- src/audio/audio_manager.hpp | 51 +++- src/audio/audio_sample_queue.cpp | 34 +++ src/audio/audio_sample_queue.hpp | 21 ++ src/audio/audio_stream.cpp | 57 ++-- src/audio/audio_stream.hpp | 12 +- src/audio/opus_codec.hpp | 2 +- src/audio/voice_playback_manager.cpp | 18 +- src/audio/voice_playback_manager.hpp | 5 +- src/crypto/box.cpp | 2 +- src/crypto/box.hpp | 2 +- src/data/packets/all.cpp | 1 + src/data/packets/all.hpp | 1 + src/data/packets/client/connection.hpp | 14 +- src/data/packets/client/game.hpp | 28 +- src/data/packets/client/misc.hpp | 35 +++ src/data/packets/packet.hpp | 3 +- src/data/packets/server/connection.hpp | 2 +- src/data/packets/server/game.hpp | 16 +- src/defs/assert.hpp | 14 +- src/defs/platform.hpp | 3 + src/main.cpp | 174 ++++++------ src/managers/server_manager.cpp | 36 +-- src/managers/server_manager.hpp | 3 +- src/managers/settings.cpp | 5 + src/managers/settings.hpp | 31 ++- src/net/game_socket.cpp | 9 +- src/net/network_manager.cpp | 38 ++- src/net/network_manager.hpp | 8 +- src/net/socket.hpp | 2 +- src/net/udp_socket.cpp | 4 +- src/net/udp_socket.hpp | 2 +- src/ui/hooks/play_layer.hpp | 100 ++++++- src/ui/menu/main/globed_menu_layer.cpp | 17 +- src/ui/menu/main/server_list_cell.cpp | 8 +- src/ui/menu/main/signup_popup.cpp | 29 +- src/util/all.hpp | 1 + src/util/debugging.cpp | 12 +- src/util/math.cpp | 43 +++ src/util/math.hpp | 30 +++ src/util/net.hpp | 2 +- src/util/sync.hpp | 77 +----- 75 files changed, 1744 insertions(+), 748 deletions(-) create mode 100644 server/game/src/data/types/fast_string.rs create mode 100644 src/audio/audio_sample_queue.cpp create mode 100644 src/audio/audio_sample_queue.hpp create mode 100644 src/data/packets/client/misc.hpp create mode 100644 src/util/math.cpp create mode 100644 src/util/math.hpp diff --git a/.gitignore b/.gitignore index 293232a7..377c6dd8 100644 --- a/.gitignore +++ b/.gitignore @@ -54,6 +54,10 @@ build.sh build-android.sh +build-android/ + +mod-w.json +mod-a.json server/bench-client server/target diff --git a/README.md b/README.md index 24891e0b..2ba0df78 100644 --- a/README.md +++ b/README.md @@ -7,8 +7,8 @@ This repository contains the complete rewrite of Globed, for Geometry Dash 2.2 a ## Features * Real-time multiplayer -* Voice chat and text messages with full encryption and zero logging -* blazing fast server written in pure Rust 🚀 (feauring only **two** unsafe blocks so far) +* Voice chat and text messages with **full encryption and zero logging** +* blazing fast server written in pure Rust 🚀 (feauring only **four** unsafe blocks so far) ## Installation @@ -30,13 +30,6 @@ Known issues: See the [server readme](./server/readme.md) for more information about the server and how you can host it. -## Building - -```sh -cmake -DCMAKE_BUILD_TYPE=Release -B build -cmake --build build --config Release -``` - ## Credit ca7x3, Firee, Croozington, Coloride, Cvolton, mat, alk, maki, xTymon - thank you for being awesome, whether it's because you helped me, suggested ideas, helped with testing, or if I just found you awesome in general :D @@ -45,9 +38,9 @@ camila314 - thank you for [UIBuilder](https://github.com/camila314/uibuilder) RobTop - thank you for releasing this awesome game :) -## Open source acknowledgments +### Used libraries -* [Geode](https://geode-sdk.org/) - the one thing that made all of this possible :) -* [UIBuilder](https://github.com/camila314/uibuilder) - something you should 100% use when creating GD mods -* [Opus](https://github.com/xiph/opus) - audio codec used for audio compression -* [libsodium](https://github.com/jedisct1/libsodium) - library used for data encryption \ No newline at end of file +* [Geode](https://geode-sdk.org/) +* [UIBuilder](https://github.com/camila314/uibuilder) +* [Opus](https://github.com/xiph/opus) +* [libsodium](https://github.com/jedisct1/libsodium) \ No newline at end of file diff --git a/mod.json b/mod.json index 47a27bee..bac3763d 100644 --- a/mod.json +++ b/mod.json @@ -1,8 +1,15 @@ { - "geode": "1.3.5", - "version": "v1.0.0-alpha.1", - "id": "dankmeme.globed2", - "name": "Globed", - "developer": "dankmeme", - "description": "Feature-rich multiplayer mod for Geometry Dash" + "geode": "1.3.5", + "version": "v1.0.0-alpha.1", + "id": "dankmeme.globed2", + "name": "Globed", + "developer": "dankmeme", + "description": "Feature-rich multiplayer mod for Geometry Dash", + "dependencies": [ + { + "id": "geode.custom-keybinds", + "version": ">=v1.2.2", + "importance": "required" + } + ] } \ No newline at end of file diff --git a/server/central/Cargo.toml b/server/central/Cargo.toml index a1a6fbab..51f06f10 100644 --- a/server/central/Cargo.toml +++ b/server/central/Cargo.toml @@ -9,6 +9,7 @@ edition = "2021" globed-shared = { path = "../shared" } anyhow = "1.0.75" +async-rate-limit = "0.0.3" async-watcher = "0.2.0" base64 = "0.21.5" blake2 = "0.10.6" @@ -25,7 +26,5 @@ sha2 = "0.10.8" time = { version = "0.3.30", features = ["formatting"] } tokio = { version = "1.34.0", features = ["full"] } totp-rs = "5.4.0" -lazy_static = "1.4.0" iprange = "0.6.7" ipnet = "2.9.0" -async-rate-limit = "0.0.3" diff --git a/server/central/src/config.rs b/server/central/src/config.rs index 893ef5b6..fe7e6672 100644 --- a/server/central/src/config.rs +++ b/server/central/src/config.rs @@ -1,5 +1,5 @@ use std::{ - collections::HashMap, + collections::{HashMap, HashSet}, fs::{File, OpenOptions}, path::Path, }; @@ -47,8 +47,8 @@ fn default_userlist_mode() -> UserlistMode { UserlistMode::None } -fn default_userlist() -> Vec { - Vec::new() +fn default_userlist() -> HashSet { + HashSet::new() } fn default_secret_key() -> String { @@ -117,9 +117,9 @@ pub struct ServerConfig { #[serde(default = "default_userlist_mode")] pub userlist_mode: UserlistMode, #[serde(default = "default_userlist")] - pub userlist: Vec, + pub userlist: HashSet, #[serde(default = "default_userlist")] - pub no_chat_list: Vec, + pub no_chat_list: HashSet, // security #[serde(default = "default_use_gd_api")] diff --git a/server/central/src/ip_blocker.rs b/server/central/src/ip_blocker.rs index 7d5c5f93..8fc4f6fb 100644 --- a/server/central/src/ip_blocker.rs +++ b/server/central/src/ip_blocker.rs @@ -1,8 +1,7 @@ -use std::net::IpAddr; +use std::{net::IpAddr, sync::OnceLock}; use ipnet::{Ipv4Net, Ipv6Net}; use iprange::IpRange; -use lazy_static::lazy_static; pub struct IpBlocker { range_v4: IpRange, @@ -10,6 +9,34 @@ pub struct IpBlocker { } impl IpBlocker { + pub fn instance() -> &'static Self { + static INSTANCE: OnceLock = OnceLock::new(); + INSTANCE.get_or_init(|| { + let contents = include_str!("allowed_ranges.txt"); + let mut v4 = Vec::new(); + let mut v6 = Vec::new(); + + for line in contents.lines() { + let line = line.trim().to_lowercase(); + if line.is_empty() || line.starts_with('#') || !line.contains(' ') { + continue; + } + + let (proto, range) = line.split_once(' ').unwrap(); + + if proto == "v4" { + v4.push(range.to_string()); + } else if proto == "v6" { + v6.push(range.to_string()); + } else { + eprintln!("ignoring invalid IP address entry: {line}"); + } + } + + IpBlocker::new(&v4, &v6) + }) + } + pub fn new(v4: &[String], v6: &[String]) -> Self { let range_v4 = v4.iter().map(|s| s.parse().unwrap()).collect(); let range_v6 = v6.iter().map(|s| s.parse().unwrap()).collect(); @@ -24,30 +51,3 @@ impl IpBlocker { } } } - -lazy_static! { - pub static ref IP_BLOCKER: IpBlocker = { - let contents = include_str!("allowed_ranges.txt"); - let mut v4 = Vec::new(); - let mut v6 = Vec::new(); - - for line in contents.lines() { - let line = line.trim().to_lowercase(); - if line.is_empty() || line.starts_with('#') || !line.contains(' ') { - continue; - } - - let (proto, range) = line.split_once(' ').unwrap(); - - if proto == "v4" { - v4.push(range.to_string()); - } else if proto == "v6" { - v6.push(range.to_string()); - } else { - eprintln!("ignoring invalid IP address entry: {line}"); - } - } - - IpBlocker::new(&v4, &v6) - }; -} diff --git a/server/central/src/logger.rs b/server/central/src/logger.rs index ed08e2ac..ca35b051 100644 --- a/server/central/src/logger.rs +++ b/server/central/src/logger.rs @@ -1,13 +1,24 @@ -use std::time::SystemTime; +use std::{sync::OnceLock, time::SystemTime}; use colored::Colorize; use log::Level; use time::{format_description, OffsetDateTime}; -pub struct Logger; +pub struct Logger { + format_desc: Vec>, +} const TIME_FORMAT: &str = "[year]-[month]-[day] [hour]:[minute]:[second].[subsecond digits:3]"; +impl Logger { + pub fn instance() -> &'static Self { + static INSTANCE: OnceLock = OnceLock::new(); + INSTANCE.get_or_init(|| Logger { + format_desc: format_description::parse(TIME_FORMAT).unwrap(), + }) + } +} + impl log::Log for Logger { fn enabled(&self, metadata: &log::Metadata) -> bool { if metadata.target().starts_with("globed_central_server") { @@ -18,27 +29,28 @@ impl log::Log for Logger { } fn log(&self, record: &log::Record) { - if self.enabled(record.metadata()) { - let now: OffsetDateTime = SystemTime::now().into(); - let format_desc = format_description::parse(TIME_FORMAT).unwrap(); - let formatted_time = now.format(&format_desc).unwrap(); - - let (level, args) = match record.level() { - Level::Error => ( - record.level().to_string().bright_red(), - record.args().to_string().bright_red(), - ), - Level::Warn => ( - record.level().to_string().bright_yellow(), - record.args().to_string().bright_yellow(), - ), - Level::Info => (record.level().to_string().cyan(), record.args().to_string().cyan()), - Level::Debug => (record.level().to_string().normal(), record.args().to_string().normal()), - Level::Trace => (record.level().to_string().black(), record.args().to_string().black()), - }; - - println!("[{formatted_time}] [{level}] - {args}"); + if !self.enabled(record.metadata()) { + return; } + + let now: OffsetDateTime = SystemTime::now().into(); + let formatted_time = now.format(&self.format_desc).unwrap(); + + let (level, args) = match record.level() { + Level::Error => ( + record.level().to_string().bright_red(), + record.args().to_string().bright_red(), + ), + Level::Warn => ( + record.level().to_string().bright_yellow(), + record.args().to_string().bright_yellow(), + ), + Level::Info => (record.level().to_string().cyan(), record.args().to_string().cyan()), + Level::Debug => (record.level().to_string().normal(), record.args().to_string().normal()), + Level::Trace => (record.level().to_string().black(), record.args().to_string().black()), + }; + + println!("[{formatted_time}] [{level}] - {args}"); } fn flush(&self) {} diff --git a/server/central/src/main.rs b/server/central/src/main.rs index d7c7358b..5186aca1 100644 --- a/server/central/src/main.rs +++ b/server/central/src/main.rs @@ -21,11 +21,9 @@ pub mod logger; pub mod state; pub mod web; -static LOGGER: Logger = Logger; - #[tokio::main] async fn main() -> Result<(), Box> { - log::set_logger(&LOGGER).unwrap(); + log::set_logger(Logger::instance()).unwrap(); if std::env::var("GLOBED_LESS_LOG").unwrap_or("0".to_string()) == "1" { log::set_max_level(LevelFilter::Warn); diff --git a/server/central/src/web/routes/auth.rs b/server/central/src/web/routes/auth.rs index 81ab344d..1aa382c8 100644 --- a/server/central/src/web/routes/auth.rs +++ b/server/central/src/web/routes/auth.rs @@ -12,7 +12,7 @@ use rand::{distributions::Alphanumeric, Rng}; use roa::{http::StatusCode, preload::PowerBody, query::Query, throw, Context}; use crate::state::{ActiveChallenge, ServerState}; -use crate::{config::UserlistMode, ip_blocker::IP_BLOCKER}; +use crate::{config::UserlistMode, ip_blocker::IpBlocker}; macro_rules! check_user_agent { ($ctx:expr, $ua:ident) => { @@ -35,7 +35,7 @@ macro_rules! get_user_ip { ($state:expr,$context:expr,$out:ident) => { let user_ip: anyhow::Result = if $state.config.cloudflare_protection && !cfg!(debug_assertions) { // verify if the actual peer is cloudflare - if !IP_BLOCKER.is_allowed(&$context.remote_addr.ip()) { + if !IpBlocker::instance().is_allowed(&$context.remote_addr.ip()) { warn!("blocking unknown non-cloudflare address: {}", $context.remote_addr.ip()); throw!(StatusCode::UNAUTHORIZED, "access is denied from this IP address"); } diff --git a/server/game/Cargo.toml b/server/game/Cargo.toml index 4716575b..9dd4c72d 100644 --- a/server/game/Cargo.toml +++ b/server/game/Cargo.toml @@ -14,7 +14,6 @@ array-init = "2.1.0" bytebuffer = "2.2.0" colored = "2.0.4" crypto_box = { version = "0.9.1", features = ["std", "chacha20"] } -lazy_static = "1.4.0" log = { version = "0.4.20" } num_enum = "0.7.1" parking_lot = "0.12.1" diff --git a/server/game/src/data/bytebufferext.rs b/server/game/src/data/bytebufferext.rs index 516a43b6..325ee1f5 100644 --- a/server/game/src/data/bytebufferext.rs +++ b/server/game/src/data/bytebufferext.rs @@ -6,59 +6,114 @@ use anyhow::{anyhow, Result}; use bytebuffer::{ByteBuffer, ByteReader}; pub trait Encodable { + /// write `Self` into the given buffer fn encode(&self, buf: &mut ByteBuffer); + /// write `Self` into the given buffer, except blazingly fast this time fn encode_fast(&self, buf: &mut FastByteBuffer); } pub trait Decodable { + /// read `Self` from the given `ByteBuffer` fn decode(buf: &mut ByteBuffer) -> Result where Self: Sized; + /// read `Self` from the given `ByteReader` fn decode_from_reader(buf: &mut ByteReader) -> Result where Self: Sized; } -// For dynamically sized types, this must be the maximum permitted size in the encoded form. -// If encode() tries to write more bytes than this value, FastByteBuffer will panic. pub trait EncodableWithKnownSize: Encodable { + /// For dynamically sized types, this must be the maximum permitted size in the encoded form. + /// If `FastByteBuffer::write` tries to write more bytes than this value, it will panic. const ENCODED_SIZE: usize; } -pub const MAX_ENCODED_STRING_SIZE: usize = 512 + size_of_types!(u32); // 512 chars - -macro_rules! encode_impl { - ($typ:ty, $buf:ident, $self:ident, $encode:expr) => { - impl crate::data::bytebufferext::Encodable for $typ { +/// maximum characters in a user's name (32). they can only be 15 chars max but we give headroom just in case +pub const MAX_NAME_SIZE: usize = 32; +/// maximum characters in a `ServerNoticePacket` or `ServerDisconnectPacket` (164) +pub const MAX_NOTICE_SIZE: usize = 164; +/// maximum characters in a user message (256) +pub const MAX_MESSAGE_SIZE: usize = 256; + +/// Simple and compact way of implementing `Decodable::decode` and `Decodable::decode_from_reader`. +/// +/// Example usage: +/// ```rust +/// struct Type { +/// some_val: String, +/// } +/// +/// decode_impl!(Type, buf, { +/// Ok(Self { some_val: buf.read()? }) +/// }); +/// ``` +macro_rules! decode_impl { + ($typ:ty, $buf:ident, $decode:expr) => { + impl crate::data::bytebufferext::Decodable for $typ { #[inline] - fn encode(&$self, $buf: &mut bytebuffer::ByteBuffer) { - $encode + fn decode($buf: &mut bytebuffer::ByteBuffer) -> anyhow::Result { + $decode } #[inline] - fn encode_fast(&$self, $buf: &mut crate::data::bytebufferext::FastByteBuffer) { - $encode + fn decode_from_reader($buf: &mut bytebuffer::ByteReader) -> anyhow::Result { + $decode } } }; } -macro_rules! decode_impl { - ($typ:ty, $buf:ident, $decode:expr) => { +/// Implements `Decodable::decode` and `Decodable::decode_from_reader` to panic when called +macro_rules! decode_unimpl { + ($typ:ty) => { impl crate::data::bytebufferext::Decodable for $typ { + fn decode(_: &mut bytebuffer::ByteBuffer) -> anyhow::Result { + panic!( + "Tried to call {}::decode when Decodable was not implemented for this type", + stringify!($typ) + ); + } + + fn decode_from_reader(_: &mut bytebuffer::ByteReader) -> anyhow::Result { + panic!( + "Tried to call {}::decode_from_reader when Decodable was not implemented for this type", + stringify!($typ) + ); + } + } + }; +} + +/// Simple and compact way of implementing `Encodable::encode` and `Encodable::encode_fast`. +/// +/// Example usage: +/// ```rust +/// struct Type { +/// some_val: String, +/// } +/// +/// encode_impl!(Type, buf, self, { +/// buf.write(&self.some_val); +/// }); +/// ``` +macro_rules! encode_impl { + ($typ:ty, $buf:ident, $self:ident, $encode:expr) => { + impl crate::data::bytebufferext::Encodable for $typ { #[inline] - fn decode($buf: &mut bytebuffer::ByteBuffer) -> anyhow::Result { - $decode + fn encode(&$self, $buf: &mut bytebuffer::ByteBuffer) { + $encode } #[inline] - fn decode_from_reader($buf: &mut bytebuffer::ByteReader) -> anyhow::Result { - $decode + fn encode_fast(&$self, $buf: &mut crate::data::bytebufferext::FastByteBuffer) { + $encode } } }; } +/// Implements `Encodable::encode` and `Encodable::encode_fast` to panic when called macro_rules! encode_unimpl { ($typ:ty) => { impl crate::data::bytebufferext::Encodable for $typ { @@ -79,20 +134,16 @@ macro_rules! encode_unimpl { }; } -macro_rules! decode_unimpl { - ($typ:ty) => { - impl crate::data::bytebufferext::Decodable for $typ { - fn decode(_: &mut bytebuffer::ByteBuffer) -> anyhow::Result { - Err(anyhow::anyhow!("decoding unimplemented for {}", stringify!($typ))) - } - - fn decode_from_reader(_: &mut bytebuffer::ByteReader) -> anyhow::Result { - Err(anyhow::anyhow!("decoding unimplemented for {}", stringify!($typ))) - } - } - }; -} - +/// Simple and compact way of implementing `EncodableWithKnownSize`. +/// +/// Example usage: +/// ```rust +/// struct Type { +/// some_val: i32, +/// } +/// +/// size_calc_impl!(Type, 4); +/// ``` macro_rules! size_calc_impl { ($typ:ty, $calc:expr) => { impl crate::data::bytebufferext::EncodableWithKnownSize for $typ { @@ -101,33 +152,69 @@ macro_rules! size_calc_impl { }; } -macro_rules! size_of_types { +/// Simple way of getting total encoded size of given primitives. +/// +/// Example usage: +/// ```rust +/// assert_eq!(16, size_of_primitives!(u64, i32, i16, i8, bool)); +/// ``` +macro_rules! size_of_primitives { ($($t:ty),+ $(,)?) => {{ 0 $(+ std::mem::size_of::<$t>())* }}; } +/// Simple way of getting total (maximum) encoded size of given types that implement `Encodable` and `EncodableWithKnownSize` +/// +/// Example usage: +/// ```rust +/// let size = size_of_types!(u32, Option, bool); +/// ``` +macro_rules! size_of_types { + ($($t:ty),+ $(,)?) => {{ + 0 $(+ <$t>::ENCODED_SIZE)* + }}; +} + pub(crate) use decode_impl; pub(crate) use decode_unimpl; pub(crate) use encode_impl; pub(crate) use encode_unimpl; pub(crate) use size_calc_impl; +pub(crate) use size_of_primitives; pub(crate) use size_of_types; -/* ByteBuffer extensions */ +/* ByteBuffer extensions +* +* With great power comes great responsibility. +* Just because you can use .write(T) and .read() for every single type, +* even primitives (as they impl Encodable/Decodable), doesn't mean you should. +* +* Notable exception is Option, `write` will accept &Option while `write_optional_value` will accept Option<&T> +* so feel free to use whichever method suits you more when dealing with those. +*/ + pub trait ByteBufferExt { fn with_capacity(capacity: usize) -> Self; } pub trait ByteBufferExtWrite { + /// alias to `write_value` + #[inline] + fn write(&mut self, val: &T) { + self.write_value(val); + } + fn write_bool(&mut self, val: bool); - // write a byte vector, prefixed with 4 bytes indicating length + /// write a `&[u8]`, prefixed with 4 bytes indicating length fn write_byte_array(&mut self, vec: &[u8]); fn write_value(&mut self, val: &T); fn write_optional_value(&mut self, val: Option<&T>); + /// write an array `[T; N]`, without prefixing it with the total amount of values fn write_value_array(&mut self, val: &[T; N]); + /// write a `Vec`, prefixed with 4 bytes indicating the amount of values fn write_value_vec(&mut self, val: &[T]); fn write_enum, B: Encodable>(&mut self, val: E); @@ -139,14 +226,24 @@ pub trait ByteBufferExtWrite { } pub trait ByteBufferExtRead { + /// alias to `read_value` + #[inline] + fn read(&mut self) -> Result { + self.read_value() + } + fn read_bool(&mut self) -> Result; - // read a byte vector, prefixed with 4 bytes indicating length + /// read a byte vector, prefixed with 4 bytes indicating length fn read_byte_array(&mut self) -> Result>; + /// read the remaining data into a Vec + fn read_remaining_bytes(&mut self) -> Result>; fn read_value(&mut self) -> Result; fn read_optional_value(&mut self) -> Result>; + /// read an array `[T; N]`, size must be known at compile time fn read_value_array(&mut self) -> Result<[T; N]>; + /// read a `Vec` fn read_value_vec(&mut self) -> Result>; fn read_enum, B: Decodable>(&mut self) -> Result; @@ -157,16 +254,15 @@ pub trait ByteBufferExtRead { fn read_point(&mut self) -> Result; } -/* FastByteBuffer - zero heap allocation buffer for encoding but also limited functionality */ -// will panic on writes if there isn't enough space. - +/// Buffer for encoding that does zero heap allocation but also has limited functionality. +/// It will panic on writes if there isn't enough space. pub struct FastByteBuffer<'a> { pos: usize, data: &'a mut [u8], } impl<'a> FastByteBuffer<'a> { - // Create a new FastByteBuffer given this mutable slice + /// Create a new `FastByteBuffer` given this mutable slice pub fn new(src: &'a mut [u8]) -> Self { Self { pos: 0, data: src } } @@ -252,7 +348,7 @@ impl<'a> FastByteBuffer<'a> { self.data.len() } - // Panics if there is not enough capacity left to write the data. + /// Write the given byte slice. Panics if there is not enough capacity left to write the data. fn internal_write(&mut self, data: &[u8]) { debug_assert!( self.pos + data.len() <= self.capacity(), @@ -356,6 +452,22 @@ macro_rules! impl_extread { Ok(self.read_bytes(length)?) } + #[inline] + fn read_remaining_bytes(&mut self) -> Result> { + let remainder = self.len() - self.get_rpos(); + let mut data = Vec::with_capacity(remainder); + + // safety: we don't allow to read any unitialized memory, as we trust the return value of `Read::read` + unsafe { + let ptr = data.as_mut_ptr(); + let mut slice = std::slice::from_raw_parts_mut(ptr, remainder); + let len = std::io::Read::read(self, &mut slice)?; + data.set_len(len); + } + + Ok(data) + } + #[inline] fn read_value(&mut self) -> Result { T::$decode_fn(self) @@ -435,33 +547,17 @@ impl<'a> ByteBufferExtRead for ByteReader<'a> { macro_rules! impl_primitive { ($typ:ty,$read:ident,$write:ident) => { - impl Encodable for $typ { - fn encode(&self, buf: &mut ByteBuffer) { - buf.$write(*self); - } + encode_impl!($typ, buf, self, { + buf.$write(*self); + }); - fn encode_fast(&self, buf: &mut FastByteBuffer) { - buf.$write(*self); - } - } + decode_impl!($typ, buf, { buf.$read().map_err(|e| e.into()) }); - impl Decodable for $typ { - fn decode(buf: &mut ByteBuffer) -> Result - where - Self: Sized, - { - Ok(buf.$read()?) - } - fn decode_from_reader(buf: &mut ByteReader) -> Result - where - Self: Sized, - { - Ok(buf.$read()?) - } - } + size_calc_impl!($typ, size_of_primitives!(Self)); }; } +impl_primitive!(bool, read_bool, write_bool); impl_primitive!(u8, read_u8, write_u8); impl_primitive!(u16, read_u16, write_u16); impl_primitive!(u32, read_u32, write_u32); @@ -470,29 +566,52 @@ impl_primitive!(i8, read_i8, write_i8); impl_primitive!(i16, read_i16, write_i16); impl_primitive!(i32, read_i32, write_i32); impl_primitive!(i64, read_i64, write_i64); +impl_primitive!(f32, read_f32, write_f32); +impl_primitive!(f64, read_f64, write_f64); -impl Encodable for Vec { +encode_impl!(Vec, buf, self, buf.write_byte_array(self)); +decode_impl!(Vec, buf, buf.read_byte_array()); + +encode_impl!(String, buf, self, buf.write_string(self)); +decode_impl!(String, buf, Ok(buf.read_string()?)); + +encode_impl!(&str, buf, self, buf.write_string(self)); + +impl Encodable for Option +where + T: Encodable, +{ fn encode(&self, buf: &mut ByteBuffer) { - buf.write_byte_array(self); + buf.write_optional_value(self.as_ref()); } fn encode_fast(&self, buf: &mut FastByteBuffer) { - buf.write_byte_array(self); + buf.write_optional_value(self.as_ref()); } } -impl Decodable for Vec { +impl EncodableWithKnownSize for Option +where + T: EncodableWithKnownSize, +{ + const ENCODED_SIZE: usize = size_of_types!(bool, T); +} + +impl Decodable for Option +where + T: Decodable, +{ fn decode(buf: &mut ByteBuffer) -> Result where Self: Sized, { - buf.read_byte_array() + buf.read_optional_value() } fn decode_from_reader(buf: &mut ByteReader) -> Result where Self: Sized, { - buf.read_byte_array() + buf.read_optional_value() } } diff --git a/server/game/src/data/packets/client/connection.rs b/server/game/src/data/packets/client/connection.rs index 15fbc72f..87c5fe79 100644 --- a/server/game/src/data/packets/client/connection.rs +++ b/server/game/src/data/packets/client/connection.rs @@ -21,7 +21,7 @@ encode_unimpl!(CryptoHandshakeStartPacket); decode_impl!(CryptoHandshakeStartPacket, buf, { let protocol = buf.read_u16()?; - let key = buf.read_value()?; + let key = buf.read()?; Ok(Self { protocol, key }) }); @@ -31,17 +31,21 @@ empty_client_packet!(KeepalivePacket, 10002); /* LoginPacket - 10003 */ +pub const MAX_TOKEN_SIZE: usize = 164; + packet!(LoginPacket, 10003, true, { account_id: i32, - token: String, + name: FastString, + token: FastString, }); encode_unimpl!(LoginPacket); decode_impl!(LoginPacket, buf, { let account_id = buf.read_i32()?; - let token = buf.read_string()?; - Ok(Self { account_id, token }) + let name = buf.read()?; + let token = buf.read()?; + Ok(Self { account_id, name, token }) }); /* DisconnectPacket - 10004 */ diff --git a/server/game/src/data/packets/client/game.rs b/server/game/src/data/packets/client/game.rs index 73be04bf..9e9bc426 100644 --- a/server/game/src/data/packets/client/game.rs +++ b/server/game/src/data/packets/client/game.rs @@ -8,13 +8,7 @@ packet!(SyncIconsPacket, 11000, false, { encode_unimpl!(SyncIconsPacket); -decode_impl!( - SyncIconsPacket, - buf, - Ok(Self { - icons: buf.read_value()? - }) -); +decode_impl!(SyncIconsPacket, buf, Ok(Self { icons: buf.read()? })); /* RequestProfilesPacket - 11001 */ @@ -56,7 +50,7 @@ packet!(PlayerDataPacket, 11004, false, { encode_unimpl!(PlayerDataPacket); -decode_impl!(PlayerDataPacket, buf, Ok(Self { data: buf.read_value()? })); +decode_impl!(PlayerDataPacket, buf, Ok(Self { data: buf.read()? })); /* RequestPlayerListPacket - 11005 */ @@ -65,9 +59,19 @@ empty_client_packet!(RequestPlayerListPacket, 11005); /* VoicePacket - 11010 */ packet!(VoicePacket, 11010, true, { - data: EncodedAudioFrame, + data: FastEncodedAudioFrame, }); encode_unimpl!(VoicePacket); -decode_impl!(VoicePacket, buf, Ok(Self { data: buf.read_value()? })); +decode_impl!(VoicePacket, buf, Ok(Self { data: buf.read()? })); + +/* ChatMessagePacket - 11011 */ + +packet!(ChatMessagePacket, 11011, true, { + message: FastString, +}); + +encode_unimpl!(ChatMessagePacket); + +decode_impl!(ChatMessagePacket, buf, Ok(Self { message: buf.read()? })); diff --git a/server/game/src/data/packets/mod.rs b/server/game/src/data/packets/mod.rs index 3239e35e..41867872 100644 --- a/server/game/src/data/packets/mod.rs +++ b/server/game/src/data/packets/mod.rs @@ -91,7 +91,7 @@ pub trait PacketMetadata { } pub struct PacketHeader { - pub packet_id: u16, + pub packet_id: PacketId, pub encrypted: bool, } @@ -104,7 +104,7 @@ impl PacketHeader { } } - pub const SIZE: usize = std::mem::size_of::() + std::mem::size_of::(); + pub const SIZE: usize = size_of_types!(PacketId, bool); } encode_impl!(PacketHeader, buf, self, { @@ -117,3 +117,5 @@ decode_impl!(PacketHeader, buf, { let encrypted = buf.read_bool()?; Ok(Self { packet_id, encrypted }) }); + +size_calc_impl!(PacketHeader, PacketHeader::SIZE); diff --git a/server/game/src/data/packets/server/connection.rs b/server/game/src/data/packets/server/connection.rs index 89e4d040..aebc2044 100644 --- a/server/game/src/data/packets/server/connection.rs +++ b/server/game/src/data/packets/server/connection.rs @@ -23,12 +23,12 @@ packet!(CryptoHandshakeResponsePacket, 20001, false, { }); encode_impl!(CryptoHandshakeResponsePacket, buf, self, { - buf.write_value(&self.key); + buf.write(&self.key); }); decode_unimpl!(CryptoHandshakeResponsePacket); -size_calc_impl!(CryptoHandshakeResponsePacket, CryptoPublicKey::ENCODED_SIZE); +size_calc_impl!(CryptoHandshakeResponsePacket, size_of_types!(CryptoPublicKey)); /* KeepaliveResponsePacket - 20002 */ @@ -47,16 +47,16 @@ size_calc_impl!(KeepaliveResponsePacket, size_of_types!(u32)); /* ServerDisconnectPacket - 20003 */ packet!(ServerDisconnectPacket, 20003, false, { - message: String + message: FastString }); encode_impl!(ServerDisconnectPacket, buf, self, { - buf.write_string(&self.message); + buf.write(&self.message); }); decode_unimpl!(ServerDisconnectPacket); -size_calc_impl!(ServerDisconnectPacket, MAX_ENCODED_STRING_SIZE); +size_calc_impl!(ServerDisconnectPacket, MAX_NOTICE_SIZE); /* LoggedInPacket - 20004 */ @@ -65,28 +65,28 @@ empty_server_packet!(LoggedInPacket, 20004); /* LoginFailedPacket - 20005 */ packet!(LoginFailedPacket, 20005, false, { - message: String + message: FastString }); encode_impl!(LoginFailedPacket, buf, self, { - buf.write_string(&self.message); + buf.write(&self.message); }); decode_unimpl!(LoginFailedPacket); -size_calc_impl!(LoginFailedPacket, MAX_ENCODED_STRING_SIZE); +size_calc_impl!(LoginFailedPacket, MAX_NOTICE_SIZE); /* ServerNoticePacket - 20006 */ // used to communicate a simple message to the user -packet!(ServerNoticePacket, 20006, true, { - message: String +packet!(ServerNoticePacket, 20006, false, { + message: FastString }); encode_impl!(ServerNoticePacket, buf, self, { - buf.write_string(&self.message); + buf.write(&self.message); }); decode_unimpl!(ServerNoticePacket); -size_calc_impl!(ServerNoticePacket, MAX_ENCODED_STRING_SIZE); +size_calc_impl!(ServerNoticePacket, MAX_NOTICE_SIZE); diff --git a/server/game/src/data/packets/server/game.rs b/server/game/src/data/packets/server/game.rs index b79d1db0..ac4643e5 100644 --- a/server/game/src/data/packets/server/game.rs +++ b/server/game/src/data/packets/server/game.rs @@ -14,7 +14,7 @@ decode_unimpl!(PlayerProfilesPacket); /* LevelDataPacket - 21001 * For optimization reasons, LevelDataPacket cannot be dynamically dispatched, and isn't defined here -* It is directly serialized in the packet handler in server_thread.rs. +* It is directly serialized in the packet handler in server_thread/handlers/game.rs. */ empty_server_packet!(LevelDataPacket, 21001); @@ -35,12 +35,28 @@ decode_unimpl!(PlayerListPacket); packet!(VoiceBroadcastPacket, 21010, true, { player_id: i32, - data: EncodedAudioFrame, + data: FastEncodedAudioFrame, }); encode_impl!(VoiceBroadcastPacket, buf, self, { buf.write_i32(self.player_id); - buf.write_value(&self.data); + buf.write(&self.data); }); decode_unimpl!(VoiceBroadcastPacket); + +/* ChatMessageBroadcastPacket - 21011 */ + +packet!(ChatMessageBroadcastPacket, 21011, true, { + player_id: i32, + message: FastString, +}); + +encode_impl!(ChatMessageBroadcastPacket, buf, self, { + buf.write_i32(self.player_id); + buf.write(&self.message); +}); + +decode_unimpl!(ChatMessageBroadcastPacket); + +size_calc_impl!(ChatMessageBroadcastPacket, size_of_types!(i32, FastString)); diff --git a/server/game/src/data/types/audio_frame.rs b/server/game/src/data/types/audio_frame.rs index 6e308b8f..7b57c1b0 100644 --- a/server/game/src/data/types/audio_frame.rs +++ b/server/game/src/data/types/audio_frame.rs @@ -1,12 +1,12 @@ use crate::data::bytebufferext::*; -const VOICE_OPUS_FRAMES_IN_AUDIO_FRAME: usize = 20; +const VOICE_MAX_FRAMES_IN_AUDIO_FRAME: usize = 10; type EncodedOpusData = Vec; #[derive(Clone)] pub struct EncodedAudioFrame { - pub opus_frames: [EncodedOpusData; VOICE_OPUS_FRAMES_IN_AUDIO_FRAME], + pub opus_frames: [Option; VOICE_MAX_FRAMES_IN_AUDIO_FRAME], } encode_impl!(EncodedAudioFrame, buf, self, { @@ -18,3 +18,19 @@ decode_impl!(EncodedAudioFrame, buf, { opus_frames: buf.read_value_array()?, }) }); + +/// `FastEncodedAudioFrame` requires just one heap allocation as opposed to 10. +#[derive(Clone)] +pub struct FastEncodedAudioFrame { + pub data: Vec, +} + +encode_impl!(FastEncodedAudioFrame, buf, self, { + buf.write_bytes(&self.data); +}); + +decode_impl!(FastEncodedAudioFrame, buf, { + Ok(Self { + data: buf.read_remaining_bytes()?, + }) +}); diff --git a/server/game/src/data/types/cocos.rs b/server/game/src/data/types/cocos.rs index cf1f2f4a..5f34d77a 100644 --- a/server/game/src/data/types/cocos.rs +++ b/server/game/src/data/types/cocos.rs @@ -22,6 +22,8 @@ decode_impl!(Color3B, buf, { Ok(Self { r, g, b }) }); +size_calc_impl!(Color3B, size_of_types!(u8, u8, u8)); + impl TryFrom for Color3B { type Error = anyhow::Error; fn try_from(value: String) -> Result { @@ -64,6 +66,8 @@ decode_impl!(Color4B, buf, { Ok(Self { r, g, b, a }) }); +size_calc_impl!(Color4B, size_of_types!(u8, u8, u8, u8)); + impl TryFrom for Color4B { type Error = anyhow::Error; fn try_from(value: String) -> Result { @@ -104,3 +108,5 @@ decode_impl!(Point, buf, { let y = buf.read_f32()?; Ok(Self { x, y }) }); + +size_calc_impl!(Point, size_of_types!(f32, f32)); diff --git a/server/game/src/data/types/fast_string.rs b/server/game/src/data/types/fast_string.rs new file mode 100644 index 00000000..93616a70 --- /dev/null +++ b/server/game/src/data/types/fast_string.rs @@ -0,0 +1,172 @@ +use anyhow::{bail, Result}; +use bytebuffer::{ByteBuffer, ByteReader}; +use std::fmt; + +use crate::data::bytebufferext::*; + +/// `FastString` is a string class more powerful than `&str` but doesn't use heap allocation like `String`. +/// When encoding or decoding into a byte buffer of any kind, the encoded form is identical to a normal `String`, +/// and they can be converted between each other interchangably with `.try_into()` or `.try_from()` +#[derive(Clone)] +pub struct FastString { + buffer: [u8; N], + len: usize, +} + +impl FastString { + pub fn new() -> Self { + Self::from_buffer([0; N], 0) + } + + pub fn from_buffer(buffer: [u8; N], len: usize) -> Self { + Self { buffer, len } + } + + pub fn from_slice(data: &[u8]) -> Self { + assert!( + data.len() <= N, + "Attempting to create a FastString with {} bytes which is more than the capacity ({N})", + data.len() + ); + let mut buffer = [0u8; N]; + buffer[..data.len()].copy_from_slice(data); + Self { buffer, len: data.len() } + } + + // this gives a warning that we should implement `std::str::FromStr::from_str` instead, + // however that returns a `Result` while we need just `Self`, + // as we assume that `data` is already a valid UTF-8 string. + #[allow(clippy::should_implement_trait)] + /// Converts a string slice to a `FastString`. Panics if there isn't enough capacity to store the data. + /// If that is undesired, use `try_into` or `try_from` instead. + pub fn from_str(data: &str) -> Self { + Self::from_slice(data.as_bytes()) + } + + pub fn len(&self) -> usize { + self.len + } + + pub fn is_empty(&self) -> bool { + self.len == 0 + } + + pub const fn capacity() -> usize { + N + } + + #[inline] + pub fn push(&mut self, c: u8) { + if self.len < N { + self.buffer[self.len] = c; + self.len += 1; + } else { + panic!("FastString buffer overflow (writing beyond capacity of {N})"); + } + } + + pub fn extend(&mut self, data: &str) { + for char in data.as_bytes() { + self.push(*char); + } + } + + /// like `extend` but will simply truncate the data instead of panicking if the string doesn't fit + pub fn extend_safe(&mut self, data: &str) { + for char in data.as_bytes() { + if self.len >= N { + break; + } + + self.push(*char); + } + } + + pub fn to_string(&self) -> Result { + Ok(self.to_str()?.to_owned()) + } + + pub fn to_str(&self) -> Result<&str> { + std::str::from_utf8(&self.buffer[..self.len]).map_err(Into::into) + } +} + +impl Encodable for FastString { + fn encode(&self, buf: &mut bytebuffer::ByteBuffer) { + buf.write_u32(self.len as u32); + buf.write_bytes(&self.buffer[..self.len]); + } + + fn encode_fast(&self, buf: &mut FastByteBuffer) { + buf.write_u32(self.len as u32); + buf.write_bytes(&self.buffer[..self.len]); + } +} + +impl EncodableWithKnownSize for FastString { + const ENCODED_SIZE: usize = size_of_types!(u32) + N; +} + +impl Decodable for FastString { + fn decode(buf: &mut ByteBuffer) -> Result + where + Self: Sized, + { + Self::decode_from_reader(&mut ByteReader::from_bytes(buf.as_bytes())) + } + + fn decode_from_reader(buf: &mut ByteReader) -> Result + where + Self: Sized, + { + let len = buf.read_u32()? as usize; + if len > N { + bail!("string is too long ({len} chars) to fit into a FastString with capacity {N}"); + } + + let mut buffer = [0u8; N]; + std::io::Read::read(buf, &mut buffer[..len])?; + + Ok(Self::from_buffer(buffer, len)) + } +} + +impl fmt::Display for FastString { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.to_str().unwrap_or("")) + } +} + +impl TryInto for FastString { + type Error = anyhow::Error; + fn try_into(self) -> anyhow::Result { + self.to_string() + } +} + +impl TryFrom for FastString { + type Error = anyhow::Error; + fn try_from(value: String) -> anyhow::Result { + TryFrom::<&str>::try_from(&value) + } +} + +impl TryFrom<&str> for FastString { + type Error = anyhow::Error; + fn try_from(value: &str) -> anyhow::Result { + if value.len() > N { + bail!( + "Attempting to convert a string slice with size {} into a FastString with capacity {N}", + value.len() + ); + } + + Ok(Self::from_str(value)) + } +} + +impl Default for FastString { + fn default() -> Self { + Self::new() + } +} diff --git a/server/game/src/data/types/gd.rs b/server/game/src/data/types/gd.rs index 1f1589e8..aa44ce03 100644 --- a/server/game/src/data/types/gd.rs +++ b/server/game/src/data/types/gd.rs @@ -1,6 +1,6 @@ use globed_shared::SpecialUser; -use crate::data::bytebufferext::*; +use crate::data::{bytebufferext::*, FastString}; use super::Color3B; @@ -49,6 +49,7 @@ encode_impl!(PlayerIconData, buf, self, { buf.write_i16(self.spider); buf.write_i16(self.swing); buf.write_i16(self.jetpack); + buf.write_i16(self.death_effect); buf.write_i16(self.color1); buf.write_i16(self.color2); @@ -83,6 +84,8 @@ decode_impl!(PlayerIconData, buf, { }) }); +size_calc_impl!(PlayerIconData, size_of_types!(i16) * 12); + impl PlayerIconData { pub fn is_valid(&self) -> bool { // TODO icon ids validation and stuff.. or not? @@ -106,11 +109,13 @@ impl Default for SpecialUserData { } encode_impl!(SpecialUserData, buf, self, { - buf.write_color3(self.name_color); + buf.write(&self.name_color); }); decode_unimpl!(SpecialUserData); +size_calc_impl!(SpecialUserData, size_of_types!(Color3B)); + impl TryFrom for SpecialUserData { type Error = anyhow::Error; fn try_from(value: SpecialUser) -> Result { @@ -125,20 +130,25 @@ impl TryFrom for SpecialUserData { #[derive(Clone, Default)] pub struct PlayerAccountData { pub account_id: i32, - pub name: String, + pub name: FastString, pub icons: PlayerIconData, pub special_user_data: Option, } encode_impl!(PlayerAccountData, buf, self, { buf.write_i32(self.account_id); - buf.write_string(&self.name); - buf.write_value(&self.icons); - buf.write_optional_value(self.special_user_data.as_ref()); + buf.write(&self.name); + buf.write(&self.icons); + buf.write(&self.special_user_data); }); decode_unimpl!(PlayerAccountData); +size_calc_impl!( + PlayerAccountData, + size_of_types!(i32, FastString, PlayerIconData, Option) +); + /* PlayerData */ #[derive(Clone, Default)] @@ -160,7 +170,7 @@ pub struct AssociatedPlayerData { encode_impl!(AssociatedPlayerData, buf, self, { buf.write_i32(self.account_id); - buf.write_value(&self.data); + buf.write(&self.data); }); -size_calc_impl!(AssociatedPlayerData, size_of_types!(i32) + PlayerData::ENCODED_SIZE); +size_calc_impl!(AssociatedPlayerData, size_of_types!(i32, PlayerData)); diff --git a/server/game/src/data/types/mod.rs b/server/game/src/data/types/mod.rs index d6fe52c2..51ad01ac 100644 --- a/server/game/src/data/types/mod.rs +++ b/server/game/src/data/types/mod.rs @@ -1,9 +1,11 @@ pub mod audio_frame; pub mod cocos; pub mod crypto; +pub mod fast_string; pub mod gd; pub use audio_frame::*; pub use cocos::*; pub use crypto::*; +pub use fast_string::*; pub use gd::*; diff --git a/server/game/src/logger.rs b/server/game/src/logger.rs index 3230f38b..57883f0e 100644 --- a/server/game/src/logger.rs +++ b/server/game/src/logger.rs @@ -1,7 +1,6 @@ -use std::time::SystemTime; +use std::{sync::OnceLock, time::SystemTime}; use colored::Colorize; -use lazy_static::lazy_static; use log::Level; use time::{format_description, OffsetDateTime}; @@ -11,10 +10,13 @@ pub struct Logger { const TIME_FORMAT: &str = "[year]-[month]-[day] [hour]:[minute]:[second].[subsecond digits:3]"; -lazy_static! { - pub static ref LOGGER_INSTANCE: Logger = Logger { - format_desc: format_description::parse(TIME_FORMAT).unwrap(), - }; +impl Logger { + pub fn instance() -> &'static Self { + static INSTANCE: OnceLock = OnceLock::new(); + INSTANCE.get_or_init(|| Logger { + format_desc: format_description::parse(TIME_FORMAT).unwrap(), + }) + } } impl log::Log for Logger { diff --git a/server/game/src/main.rs b/server/game/src/main.rs index fe1bb7cb..003ee206 100644 --- a/server/game/src/main.rs +++ b/server/game/src/main.rs @@ -7,11 +7,15 @@ clippy::wildcard_imports )] -use std::{collections::HashMap, error::Error}; +use std::{ + collections::{HashMap, HashSet}, + error::Error, +}; use anyhow::anyhow; use globed_shared::{GameServerBootData, PROTOCOL_VERSION}; use log::{error, info, warn, LevelFilter}; +use logger::Logger; use server::GameServerConfiguration; use state::ServerState; @@ -26,7 +30,7 @@ pub mod state; #[tokio::main] async fn main() -> Result<(), Box> { - log::set_logger(&*logger::LOGGER_INSTANCE).unwrap(); + log::set_logger(Logger::instance()).unwrap(); if std::env::var("GLOBED_GS_LESS_LOG").unwrap_or("0".to_string()) == "1" { log::set_max_level(LevelFilter::Warn); @@ -100,14 +104,13 @@ async fn main() -> Result<(), Box> { let (gsbd, standalone) = if config.central_url == "none" { warn!("Starting in standalone mode, authentication is disabled"); - ( - GameServerBootData { - protocol: PROTOCOL_VERSION, - no_chat: Vec::new(), - special_users: HashMap::new(), - }, - true, - ) + let gsbd = GameServerBootData { + protocol: PROTOCOL_VERSION, + no_chat: HashSet::new(), + special_users: HashMap::new(), + }; + + (gsbd, true) } else { info!("Retrieving config from the central server.."); @@ -135,9 +138,10 @@ async fn main() -> Result<(), Box> { (boot_data, false) }; - let server = Box::leak(Box::new(GameServer::new(host_address, state, gsbd, config, standalone).await)); + let server = GameServer::new(host_address, state, gsbd, config, standalone).await; + let server = Box::leak(Box::new(server)); - server.run().await?; + Box::pin(server.run()).await?; Ok(()) } diff --git a/server/game/src/managers/player.rs b/server/game/src/managers/player.rs index 4cf85549..26c39199 100644 --- a/server/game/src/managers/player.rs +++ b/server/game/src/managers/player.rs @@ -15,37 +15,45 @@ impl PlayerManager { } } + /// get a reference to a player's `AssociatedPlayerData` given an account ID pub fn get_player(&self, account_id: i32) -> Option<&AssociatedPlayerData> { self.players.get(&account_id) } + /// get a `Vec` of references to `AssociatedPlayerData` given a slice of account IDs pub fn get_players(&self, account_ids: &[i32]) -> Vec<&AssociatedPlayerData> { account_ids.iter().filter_map(|&key| self.players.get(&key)).collect() } + /// set player's data, inserting a new entry if doesn't already exist pub fn set_player_data(&mut self, account_id: i32, data: &PlayerData) { let entry = self.players.entry(account_id).or_default(); entry.account_id = account_id; entry.data.clone_from(data); } + /// remove the player from the list of players pub fn remove_player(&mut self, account_id: i32) { self.players.remove(&account_id); } + /// get a reference to a list of account IDs of players on a level given its ID pub fn get_level(&self, level_id: i32) -> Option<&FxHashSet> { self.levels.get(&level_id) } + /// get the amount of players on a level given its ID pub fn get_player_count_on_level(&self, level_id: i32) -> Option { self.levels.get(&level_id).map(FxHashSet::len) } + /// get a `Vec` of references to `AssociatedPlayerData` with all the players on a level given its ID pub fn get_players_on_level(&self, level_id: i32) -> Option> { let ids = self.levels.get(&level_id)?; Some(ids.iter().filter_map(|&key| self.players.get(&key)).collect()) } + /// run a function `f` on each player on a level given its ID, with additional data (wink wink it's always gonna be `&mut FastByteBuffer`) pub fn for_each_player_on_level(&self, level_id: i32, f: F, additional: &mut A) where F: Fn(&AssociatedPlayerData, &mut A), @@ -57,11 +65,13 @@ impl PlayerManager { } } + /// add a player to a level given a level ID and an account ID pub fn add_to_level(&mut self, level_id: i32, account_id: i32) { let players = self.levels.entry(level_id).or_default(); players.insert(account_id); } + /// remove a player from a level given a level ID and an account ID pub fn remove_from_level(&mut self, level_id: i32, account_id: i32) { let should_remove_level = self.levels.get_mut(&level_id).is_some_and(|level| { level.remove(&account_id); diff --git a/server/game/src/server.rs b/server/game/src/server.rs index 3d4dee33..108bca19 100644 --- a/server/game/src/server.rs +++ b/server/game/src/server.rs @@ -6,7 +6,7 @@ use std::{ use parking_lot::Mutex as SyncMutex; -use anyhow::anyhow; +use anyhow::{anyhow, bail}; use crypto_box::{aead::OsRng, SecretKey}; use globed_shared::GameServerBootData; use rustc_hash::FxHashMap; @@ -18,12 +18,12 @@ use log::{debug, error, info, warn}; use tokio::net::UdpSocket; use crate::{ - data::{packets::server::VoiceBroadcastPacket, types::PlayerAccountData}, + data::{packets::server::VoiceBroadcastPacket, types::PlayerAccountData, ChatMessageBroadcastPacket, FastString}, server_thread::{GameServerThread, ServerThreadMessage, SMALL_PACKET_LIMIT}, state::ServerState, }; -const MAX_PACKET_SIZE: usize = 8192; +const MAX_PACKET_SIZE: usize = 16384; pub struct GameServerConfiguration { pub http_client: reqwest::Client, @@ -32,7 +32,6 @@ pub struct GameServerConfiguration { } pub struct GameServer { - pub address: String, pub state: ServerState, pub socket: UdpSocket, pub threads: SyncMutex>>, @@ -50,14 +49,11 @@ impl GameServer { config: GameServerConfiguration, standalone: bool, ) -> Self { - let secret_key = SecretKey::generate(&mut OsRng); - Self { - address: address.clone(), state, socket: UdpSocket::bind(&address).await.unwrap(), threads: SyncMutex::new(FxHashMap::default()), - secret_key, + secret_key: SecretKey::generate(&mut OsRng), central_conf: SyncMutex::new(central_conf), config, standalone, @@ -65,7 +61,7 @@ impl GameServer { } pub async fn run(&'static self) -> anyhow::Result<()> { - info!("Server launched on {}", self.address); + info!("Server launched on {}", self.socket.local_addr()?); if !self.standalone { tokio::spawn(async move { @@ -97,31 +93,73 @@ impl GameServer { /* various calls for other threads */ - pub fn broadcast_voice_packet(&'static self, vpkt: &Arc) -> anyhow::Result<()> { - // TODO dont send it to every single thread in existence - let threads: Vec<_> = self.threads.lock().values().cloned().collect(); - for thread in threads { - thread.push_new_message(ServerThreadMessage::BroadcastVoice(vpkt.clone()))?; + pub fn broadcast_voice_packet(&'static self, vpkt: &Arc, level_id: i32) -> anyhow::Result<()> { + let pm = self.state.player_manager.lock(); + let players = pm.get_level(level_id); + + if let Some(players) = players { + let threads: Vec<_> = self + .threads + .lock() + .values() + .filter(|thread| { + let account_id = thread.account_id.load(Ordering::Relaxed); + account_id != vpkt.player_id && players.contains(&account_id) + }) + .cloned() + .collect(); + + drop(pm); + + for thread in threads { + thread.push_new_message(ServerThreadMessage::BroadcastVoice(vpkt.clone()))?; + } + } + + Ok(()) + } + + pub fn broadcast_chat_packet(&'static self, tpkt: &ChatMessageBroadcastPacket, level_id: i32) -> anyhow::Result<()> { + let pm = self.state.player_manager.lock(); + let players = pm.get_level(level_id); + + if let Some(players) = players { + let threads: Vec<_> = self + .threads + .lock() + .values() + .filter(|thread| { + let account_id = thread.account_id.load(Ordering::Relaxed); + account_id != tpkt.player_id && players.contains(&account_id) + }) + .cloned() + .collect(); + + drop(pm); + + for thread in threads { + thread.push_new_message(ServerThreadMessage::BroadcastText(tpkt.clone()))?; + } } Ok(()) } + // TODO low: look into eliminating heap allocation for these two funcs below + // it's just one vec bro it's not that deep pub fn gather_profiles(&'static self, ids: &[i32]) -> Vec { let threads = self.threads.lock(); - ids.iter() - .filter_map(|id| { - threads - .values() - .find(|thread| thread.account_id.load(Ordering::Relaxed) == *id) - .map(|thread| thread.account_data.lock().clone()) - }) + threads + .values() + .filter(|thread| ids.contains(&thread.account_id.load(Ordering::Relaxed))) + .map(|thread| thread.account_data.lock().clone()) .collect() } pub fn gather_all_profiles(&'static self) -> Vec { let threads = self.threads.lock(); + threads .values() .filter(|thr| thr.authenticated.load(Ordering::Relaxed)) @@ -134,13 +172,17 @@ impl GameServer { } pub fn check_already_logged_in(&'static self, user_id: i32) -> anyhow::Result<()> { - let threads = self.threads.lock(); - let thread = threads.values().find(|thr| thr.account_id.load(Ordering::Relaxed) == user_id); + let thread = self + .threads + .lock() + .values() + .find(|thr| thr.account_id.load(Ordering::Relaxed) == user_id) + .cloned(); if let Some(thread) = thread { - thread.push_new_message(ServerThreadMessage::TerminationNotice( - "Someone logged into the same account from a different place.".to_string(), - ))?; + thread.push_new_message(ServerThreadMessage::TerminationNotice(FastString::from_str( + "Someone logged into the same account from a different place.", + )))?; } Ok(()) @@ -152,7 +194,7 @@ impl GameServer { let (len, peer) = self.socket.recv_from(buf).await?; let peer = match peer { - SocketAddr::V6(_) => return Err(anyhow!("rejecting request from ipv6 host")), + SocketAddr::V6(_) => bail!("rejecting request from ipv6 host"), SocketAddr::V4(x) => x, }; @@ -169,6 +211,9 @@ impl GameServer { // 1. no messages sent by the peer for 60 seconds // 2. the channel was closed (normally impossible for that to happen) // 3. `thread.terminate()` was called on that thread (due to a disconnect from either side) + // additionally, if it panics then the state of the player will be frozen forever, + // they won't be removed from levels or the player count and that person won't be able to connect again. + // so try to avoid panics please.. thread.run().await; log::trace!("removing client: {}", peer); self.post_disconnect_cleanup(&thread, peer); @@ -183,7 +228,7 @@ impl GameServer { let mut smallbuf = [0u8; SMALL_PACKET_LIMIT]; smallbuf[..len].copy_from_slice(&buf[..len]); - ServerThreadMessage::SmallPacket(smallbuf) + ServerThreadMessage::SmallPacket((smallbuf, len as u16)) } else { ServerThreadMessage::Packet(buf[..len].to_vec()) }; diff --git a/server/game/src/server_thread/handlers/connection.rs b/server/game/src/server_thread/handlers/connection.rs index 5548f755..08d45e91 100644 --- a/server/game/src/server_thread/handlers/connection.rs +++ b/server/game/src/server_thread/handlers/connection.rs @@ -26,6 +26,7 @@ impl GameServerThread { format!( "Outdated server! You are running protocol v{p} while the server is still on v{PROTOCOL_VERSION}.", ) + .try_into()? ); } p if p < PROTOCOL_VERSION => { @@ -33,7 +34,7 @@ impl GameServerThread { self, format!( "Outdated client! Please update the mod in order to connect to the server. Client protocol version: v{p}, server: v{PROTOCOL_VERSION}", - ) + ).try_into()? ); } _ => {} @@ -72,7 +73,7 @@ impl GameServerThread { gs_handler!(self, handle_login, LoginPacket, packet, { if self.game_server.standalone { - debug!("Bypassing login for {}", packet.account_id); + debug!("Bypassing login for {} ({})", packet.name, packet.account_id); self.game_server.check_already_logged_in(packet.account_id)?; self.authenticated.store(true, Ordering::Relaxed); self.account_id.store(packet.account_id, Ordering::Relaxed); @@ -80,7 +81,7 @@ impl GameServerThread { { let mut account_data = self.account_data.lock(); account_data.account_id = packet.account_id; - account_data.name = format!("Player{}", packet.account_id); + account_data.name = packet.name; } self.send_packet_fast(&LoggedInPacket {}).await?; return Ok(()); @@ -97,7 +98,7 @@ impl GameServerThread { .post(url) .query(&[ ("account_id", packet.account_id.to_string()), - ("token", packet.token.clone()), + ("token", packet.token.try_into()?), ("pw", self.game_server.config.central_pw.clone()), ]) .send() @@ -108,10 +109,9 @@ impl GameServerThread { if !response.starts_with("status_ok:") { self.terminate(); - self.send_packet(&LoginFailedPacket { - message: format!("authentication failed: {response}"), - }) - .await?; + let mut message = FastString::from_str("authentication failed: "); + message.extend_safe(&response); + self.send_packet_fast(&LoginFailedPacket { message }).await?; return Ok(()); } @@ -129,7 +129,8 @@ impl GameServerThread { { let mut account_data = self.account_data.lock(); account_data.account_id = packet.account_id; - account_data.name = player_name.to_string(); + // we have packet.name but that can be spoofed so we don't trust it in non-standalone mode + account_data.name = FastString::from_str(player_name); let special_user_data = self .game_server diff --git a/server/game/src/server_thread/handlers/game.rs b/server/game/src/server_thread/handlers/game.rs index ad3e2bc7..b08521ae 100644 --- a/server/game/src/server_thread/handlers/game.rs +++ b/server/game/src/server_thread/handlers/game.rs @@ -1,6 +1,6 @@ use std::{ sync::{atomic::Ordering, Arc}, - time::SystemTime, + time::{SystemTime, UNIX_EPOCH}, }; use crate::{ @@ -12,22 +12,27 @@ use log::{debug, warn}; use super::{gs_disconnect, gs_handler, gs_needauth}; use crate::data::*; +/// max voice throughput in kb/s +const MAX_VOICE_THROUGHPUT: usize = 8; +/// max voice packet size in bytes +pub const MAX_VOICE_PACKET_SIZE: usize = 4096; + impl GameServerThread { gs_handler!(self, handle_sync_icons, SyncIconsPacket, packet, { gs_needauth!(self); - let mut account_data = self.account_data.lock(); - account_data.icons.clone_from(&packet.icons); + self.account_data.lock().icons.clone_from(&packet.icons); Ok(()) }); gs_handler!(self, handle_request_profiles, RequestProfilesPacket, packet, { gs_needauth!(self); - self.send_packet(&PlayerProfilesPacket { - profiles: self.game_server.gather_profiles(&packet.ids), - }) - .await + let profiles = self.game_server.gather_profiles(&packet.ids); + let encoded_size = size_of_types!(PlayerAccountData) * profiles.len(); + + self.send_packet_fast_rough(&PlayerProfilesPacket { profiles }, encoded_size) + .await }); gs_handler!(self, handle_level_join, LevelJoinPacket, packet, { @@ -86,7 +91,7 @@ impl GameServerThread { drop(pm); - let calc_size = PacketHeader::SIZE + 4 + (written_players * AssociatedPlayerData::ENCODED_SIZE); + let calc_size = size_of_types!(PacketHeader, u32) + size_of_types!(AssociatedPlayerData) * written_players; alloca::with_alloca(calc_size, move |data| { // safety: 'data' will have garbage data but that is considered safe for all our intents and purposes @@ -140,10 +145,11 @@ impl GameServerThread { gs_handler!(self, handle_request_player_list, RequestPlayerListPacket, _packet, { gs_needauth!(self); - self.send_packet(&PlayerListPacket { - profiles: self.game_server.gather_all_profiles(), - }) - .await + let profiles = self.game_server.gather_all_profiles(); + let encoded_size = size_of_types!(PlayerAccountData) * profiles.len(); + + self.send_packet_fast_rough(&PlayerListPacket { profiles }, encoded_size) + .await }); gs_handler!(self, handle_voice, VoicePacket, packet, { @@ -153,17 +159,27 @@ impl GameServerThread { // check the throughput { - let mut last_voice_packet = self.last_voice_packet.lock(); - let now = SystemTime::now(); - let passed_time = now.duration_since(*last_voice_packet)?.as_millis(); - *last_voice_packet = now; + let now = SystemTime::now().duration_since(UNIX_EPOCH)?.as_millis() as u64; + let last_voice_packet = self.last_voice_packet.swap(now, Ordering::Relaxed); + let mut passed_time = now - last_voice_packet; + + if passed_time == 0 { + passed_time = 1; + } - let total_size = packet.data.opus_frames.iter().map(Vec::len).sum::(); + let total_size = packet.data.data.len(); - let throughput = total_size / passed_time as usize; // in kb/s + // let total_size = packet + // .data + // .opus_frames + // .iter() + // .filter_map(|opt| opt.as_ref()) + // .map(Vec::len) + // .sum::(); - debug!("voice packet throughput: {}kb/s", throughput); - if throughput > 8 { + let throughput = total_size / passed_time as usize; // in kb per second + + if throughput > MAX_VOICE_THROUGHPUT { warn!("rejecting a voice packet, throughput above the limit: {}kb/s", throughput); return Ok(()); } @@ -171,10 +187,27 @@ impl GameServerThread { let vpkt = Arc::new(VoiceBroadcastPacket { player_id: accid, - data: packet.data.clone(), + data: packet.data, }); - self.game_server.broadcast_voice_packet(&vpkt)?; + self.game_server + .broadcast_voice_packet(&vpkt, self.level_id.load(Ordering::Relaxed))?; + + Ok(()) + }); + + gs_handler!(self, handle_chat_message, ChatMessagePacket, packet, { + gs_needauth!(self); + + let accid = self.account_id.load(Ordering::Relaxed); + + let cpkt = ChatMessageBroadcastPacket { + player_id: accid, + message: packet.message, + }; + + self.game_server + .broadcast_chat_packet(&cpkt, self.level_id.load(Ordering::Relaxed))?; Ok(()) }); diff --git a/server/game/src/server_thread/handlers/mod.rs b/server/game/src/server_thread/handlers/mod.rs index 58a6730a..17254158 100644 --- a/server/game/src/server_thread/handlers/mod.rs +++ b/server/game/src/server_thread/handlers/mod.rs @@ -1,49 +1,63 @@ mod connection; mod game; +pub use game::MAX_VOICE_PACKET_SIZE; + +/// packet handler for a specific packet type macro_rules! gs_handler { ($self:ident,$name:ident,$pktty:ty,$pkt:ident,$code:expr) => { pub(crate) async fn $name(&$self, buf: &mut bytebuffer::ByteReader<'_>) -> crate::server_thread::Result<()> { let $pkt = <$pktty>::decode_from_reader(buf)?; #[cfg(debug_assertions)] - log::debug!("Handling packet {}", <$pktty>::NAME); + log::debug!( + "[{} @ {}] Handling packet {}", + $self.account_id.load(Ordering::Relaxed), + $self.peer, + <$pktty>::NAME + ); $code } }; } +/// packet handler except not async macro_rules! gs_handler_sync { ($self:ident,$name:ident,$pktty:ty,$pkt:ident,$code:expr) => { pub(crate) fn $name(&$self, buf: &mut bytebuffer::ByteReader<'_>) -> crate::server_thread::Result<()> { let $pkt = <$pktty>::decode_from_reader(buf)?; #[cfg(debug_assertions)] - log::debug!("Handling packet {}", <$pktty>::NAME); + log::debug!( + "[{} @ {}] Handling packet {}", + $self.account_id.load(Ordering::Relaxed), + $self.peer, + <$pktty>::NAME + ); $code } }; } +/// call disconnect and return from the function macro_rules! gs_disconnect { ($self:ident, $msg:expr) => { - $self.terminate(); - $self - .send_packet_fast(&ServerDisconnectPacket { message: $msg }) - .await?; + $self.disconnect($msg).await?; return Ok(()); }; } #[allow(unused_macros)] +/// send a `ServerNoticePacket` to the client with the given message macro_rules! gs_notice { ($self:expr, $msg:expr) => { - $self.send_packet(&ServerNoticePacket { message: $msg }).await?; + $self.send_packet_fast(&ServerNoticePacket { message: $msg }).await?; }; } +/// if the client isn't authenticated, invoke `gs_disconnect!` and exit the handler macro_rules! gs_needauth { ($self:ident) => { if !$self.authenticated.load(Ordering::Relaxed) { - gs_disconnect!($self, "unauthorized, please try connecting again".to_string()); + gs_disconnect!($self, FastString::from_str("unauthorized, please try connecting again")); } }; } diff --git a/server/game/src/server_thread/mod.rs b/server/game/src/server_thread/mod.rs index 4e225c89..568bd98f 100644 --- a/server/game/src/server_thread/mod.rs +++ b/server/game/src/server_thread/mod.rs @@ -1,17 +1,17 @@ use std::{ net::SocketAddrV4, sync::{ - atomic::{AtomicBool, AtomicI32, Ordering}, + atomic::{AtomicBool, AtomicI32, AtomicU64, Ordering}, Arc, }, - time::{Duration, SystemTime}, + time::Duration, }; use parking_lot::Mutex as SyncMutex; use bytebuffer::{ByteBuffer, ByteReader}; use crypto_box::{ - aead::{Aead, AeadCore, OsRng}, + aead::{Aead, AeadCore, AeadInPlace, OsRng}, ChaChaBox, }; use log::{debug, warn}; @@ -24,15 +24,20 @@ mod handlers; pub use error::{PacketHandlingError, Result}; +use self::handlers::MAX_VOICE_PACKET_SIZE; + // TODO adjust this to PlayerData size in the future plus some headroom -pub const SMALL_PACKET_LIMIT: usize = 128; +pub const SMALL_PACKET_LIMIT: usize = 164; const CHANNEL_BUFFER_SIZE: usize = 8; +const NONCE_SIZE: usize = 24; +const MAC_SIZE: usize = 16; pub enum ServerThreadMessage { Packet(Vec), - SmallPacket([u8; SMALL_PACKET_LIMIT]), + SmallPacket(([u8; SMALL_PACKET_LIMIT], u16)), BroadcastVoice(Arc), - TerminationNotice(String), + BroadcastText(ChatMessageBroadcastPacket), + TerminationNotice(FastString), } pub struct GameServerThread { @@ -49,7 +54,7 @@ pub struct GameServerThread { pub level_id: AtomicI32, pub account_data: SyncMutex, - last_voice_packet: SyncMutex, + last_voice_packet: AtomicU64, } impl GameServerThread { @@ -68,7 +73,7 @@ impl GameServerThread { game_server, awaiting_termination: AtomicBool::new(false), account_data: SyncMutex::new(PlayerAccountData::default()), - last_voice_packet: SyncMutex::new(SystemTime::now()), + last_voice_packet: AtomicU64::new(0), } } @@ -83,41 +88,79 @@ impl GameServerThread { match tokio::time::timeout(Duration::from_secs(60), rx.recv()).await { Ok(Some(message)) => match self.handle_message(message).await { Ok(()) => {} - Err(err) => warn!("[@{}]: {}", self.peer, err.to_string()), + Err(err) => warn!( + "[{} @ {}] err: {}", + self.account_id.load(Ordering::Relaxed), + self.peer, + err.to_string() + ), }, Ok(None) | Err(_) => break, // sender closed | timeout }; } } + /// send a new message to this thread. keep in mind this blocks for a few microseconds. pub fn push_new_message(&self, data: ServerThreadMessage) -> anyhow::Result<()> { self.tx.try_send(data)?; Ok(()) } + /// terminate the thread as soon as possible and cleanup. pub fn terminate(&self) { self.awaiting_termination.store(true, Ordering::Relaxed); } /* private utilities */ + /// disconnect and send a message to the user + async fn disconnect(&self, message: FastString) -> Result<()> { + self.terminate(); + self.send_packet_fast(&ServerDisconnectPacket { message }).await + } + + /// send a packet normally. with zero extra optimizations. like a sane person typically would. + #[allow(dead_code)] async fn send_packet(&self, packet: &P) -> Result<()> { #[cfg(debug_assertions)] - log::debug!("Sending {}", P::NAME); + log::debug!( + "[{} @ {}] Sending packet {} (normal)", + self.account_id.load(Ordering::Relaxed), + self.peer, + P::NAME + ); let serialized = self.serialize_packet(packet)?; self.send_buffer(serialized.as_bytes()).await } - // fast packet sending with best-case zero heap allocation - // packet must implement EncodableWithKnownSize to be fast sendable - // it also must **NOT** be encrypted, for dynamically sized or encrypted packets, use send_packet. - // on average 2-3x faster than send_packet, even worst case should be faster by a bit - // TODO encryption + /// fast packet sending with best-case zero heap allocation. + /// packet must not be encrypted and must implement `EncodableWithKnownSize` to be fast sendable, + /// for dynamically sized packets use `send_packet` or `send_packet_fast_rough`, + /// for encrypted packets use `send_packet_fast_encrypted`. + /// on average, this is 2-3x faster than `send_packet`, even worst case should be faster by a bit. async fn send_packet_fast(&self, packet: &P) -> Result<()> { - assert!(!P::ENCRYPTED, "Attempting to fast encode an encrypted packet ({})", P::NAME); + self.send_packet_fast_rough(packet, P::ENCODED_SIZE).await + } - let to_send: Result>> = alloca::with_alloca(PacketHeader::SIZE + P::ENCODED_SIZE, move |data| { + /// like `send_packet_fast` but without the size being known at compile time. + /// you have to provide a rough estimate of the packet size, but if the packet doesn't fit, the function panics. + async fn send_packet_fast_rough(&self, packet: &P, packet_size: usize) -> Result<()> { + assert!( + !P::ENCRYPTED, + "Attempting to fast encode an encrypted packet ({}), use 'send_packet_fast_encrypted' instead", + P::NAME + ); + + #[cfg(debug_assertions)] + log::debug!( + "[{} @ {}] Sending packet {} (fast)", + self.account_id.load(Ordering::Relaxed), + self.peer, + P::NAME + ); + + let to_send: Result>> = alloca::with_alloca(PacketHeader::SIZE + packet_size, move |data| { // safety: 'data' will have garbage data but that is considered safe for all our intents and purposes // as `FastByteBuffer::as_bytes()` will only return what was already written. let data = unsafe { @@ -132,6 +175,8 @@ impl GameServerThread { buf.write_value(packet); let send_data = buf.as_bytes(); + + // we try a non-blocking send if we can, otherwise fallback to a Vec and an async send match self.send_buffer_immediate(send_data) { Err(PacketHandlingError::SocketWouldBlock) => Ok(Some(send_data.to_vec())), Err(e) => Err(e), @@ -146,6 +191,81 @@ impl GameServerThread { Ok(()) } + /// fast encode & send an encrypted packet. hell yeah! + async fn send_packet_fast_encrypted(&self, packet: &P, packet_raw_size: usize) -> Result<()> { + assert!( + P::ENCRYPTED, + "Attempting to call 'send_packet_fast_encrypted' on a non-encrypted packet ({})", + P::NAME + ); + + #[cfg(debug_assertions)] + log::debug!( + "[{} @ {}] Sending packet {} (fast + encrypted)", + self.account_id.load(Ordering::Relaxed), + self.peer, + P::NAME + ); + + let total_size = PacketHeader::SIZE + NONCE_SIZE + MAC_SIZE + packet_raw_size; + let nonce_start = PacketHeader::SIZE; + let mac_start = nonce_start + NONCE_SIZE; + let raw_data_start = mac_start + MAC_SIZE; + + let to_send: Result>> = alloca::with_alloca(total_size, move |data| { + // safety: i don't even care anymore + let data = unsafe { + let ptr = data.as_mut_ptr().cast::(); + let len = std::mem::size_of_val(data); + std::slice::from_raw_parts_mut(ptr, len) + }; + + // write the header + let mut buf = FastByteBuffer::new(data); + buf.write_packet_header::

(); + + // first encode the packet + let mut buf = FastByteBuffer::new(&mut data[raw_data_start..raw_data_start + packet_raw_size]); + buf.write_value(packet); + + // if the written size isn't equal to `packet_raw_size`, we use buffer length instead + let raw_data_end = raw_data_start + buf.len(); + + // encrypt in place + let cbox = self.crypto_box.lock(); + if cbox.is_none() { + return Err(PacketHandlingError::WrongCryptoBoxState); + } + + let cbox = cbox.as_ref().unwrap(); + let nonce = ChaChaBox::generate_nonce(&mut OsRng); + let tag = cbox + .encrypt_in_place_detached(&nonce, b"", &mut data[raw_data_start..raw_data_end]) + .map_err(|e| PacketHandlingError::EncryptionError(e.to_string()))?; + + // prepend the nonce + data[nonce_start..mac_start].copy_from_slice(&nonce); + + // prepend the mac tag + data[mac_start..raw_data_start].copy_from_slice(&tag); + + // we try a non-blocking send if we can, otherwise fallback to a Vec and an async send + let send_data = &data[..raw_data_end]; + match self.send_buffer_immediate(send_data) { + Err(PacketHandlingError::SocketWouldBlock) => Ok(Some(send_data.to_vec())), + Err(e) => Err(e), + Ok(()) => Ok(None), + } + }); + + if let Some(to_send) = to_send? { + self.send_buffer(&to_send).await?; + } + + Ok(()) + } + + /// sends a buffer to our peer via the socket async fn send_buffer(&self, buffer: &[u8]) -> Result<()> { self.game_server .socket @@ -155,7 +275,7 @@ impl GameServerThread { .map_err(PacketHandlingError::SocketSendFailed) } - // attempt to send a buffer immediately to the socket, but if it requires blocking then nuh uh + /// attempt to send a buffer immediately to the socket, but if it requires blocking then returns an error fn send_buffer_immediate(&self, buffer: &[u8]) -> Result<()> { self.game_server .socket @@ -170,6 +290,8 @@ impl GameServerThread { }) } + /// serialize (and potentially encrypt) a packet, returning a `ByteBuffer` with the output data + #[allow(dead_code)] fn serialize_packet(&self, packet: &P) -> Result { let mut buf = ByteBuffer::new(); buf.write_packet_header::

(); @@ -181,7 +303,7 @@ impl GameServerThread { let cbox = self.crypto_box.lock(); - // should never happen + // should be impossible to happen #[cfg(debug_assertions)] if !cbox.is_some() { return Err(PacketHandlingError::WrongCryptoBoxState); @@ -206,30 +328,29 @@ impl GameServerThread { Ok(buf) } + /// handle a message sent from the `GameServer` async fn handle_message(&self, message: ServerThreadMessage) -> Result<()> { match message { - ServerThreadMessage::Packet(data) => self.handle_packet(&data).await?, - ServerThreadMessage::SmallPacket(data) => self.handle_packet(&data).await?, - ServerThreadMessage::BroadcastVoice(voice_packet) => self.send_packet(&*voice_packet).await?, - ServerThreadMessage::TerminationNotice(message) => { - self.terminate(); - self.send_packet(&ServerDisconnectPacket { message }).await?; + ServerThreadMessage::Packet(mut data) => self.handle_packet(&mut data).await?, + ServerThreadMessage::SmallPacket((mut data, len)) => self.handle_packet(&mut data[..len as usize]).await?, + ServerThreadMessage::BroadcastText(text_packet) => self.send_packet_fast(&text_packet).await?, + ServerThreadMessage::BroadcastVoice(voice_packet) => { + let calc_size = voice_packet.data.data.len() + size_of_types!(u32); + self.send_packet_fast_encrypted(&*voice_packet, calc_size).await?; } + ServerThreadMessage::TerminationNotice(message) => self.disconnect(message).await?, } Ok(()) } - /* packet handlers */ - - async fn handle_packet(&self, message: &[u8]) -> Result<()> { - #[cfg(debug_assertions)] + /// handle an incoming packet + async fn handle_packet(&self, message: &mut [u8]) -> Result<()> { if message.len() < PacketHeader::SIZE { return Err(PacketHandlingError::MalformedMessage); } let mut data = ByteReader::from_bytes(message); - let header = data.read_packet_header()?; // minor optimization @@ -237,13 +358,21 @@ impl GameServerThread { return self.handle_player_data(&mut data).await; } - // also for optimization, reject the voice packet immediately if the player is blocked from vc + // also for optimization, reject the voice packet immediately on certain conditions if header.packet_id == VoicePacket::PACKET_ID { let accid = self.account_id.load(Ordering::Relaxed); if self.game_server.chat_blocked(accid) { debug!("blocking voice packet from {accid}"); return Ok(()); } + + if message.len() > MAX_VOICE_PACKET_SIZE { + debug!( + "blocking voice packet from {accid} because it's too big ({} bytes)", + message.len() + ); + return Ok(()); + } } // reject cleartext credentials @@ -251,9 +380,8 @@ impl GameServerThread { return Err(PacketHandlingError::MalformedLoginAttempt); } - let cleartext_vec: Vec; if header.encrypted { - if message.len() < 24 + PacketHeader::SIZE { + if message.len() < PacketHeader::SIZE + NONCE_SIZE + MAC_SIZE { return Err(PacketHandlingError::MalformedCiphertext); } @@ -264,13 +392,22 @@ impl GameServerThread { let cbox = cbox.as_ref().unwrap(); - let nonce = &message[PacketHeader::SIZE..PacketHeader::SIZE + 24]; - let ciphertext = &message[PacketHeader::SIZE + 24..]; + let nonce_start = PacketHeader::SIZE; + let mac_start = nonce_start + NONCE_SIZE; + let ciphertext_start = mac_start + MAC_SIZE; - cleartext_vec = cbox - .decrypt(nonce.into(), ciphertext) + let mut nonce = [0u8; NONCE_SIZE]; + nonce[..NONCE_SIZE].clone_from_slice(&message[nonce_start..mac_start]); + let nonce = nonce.into(); + + let mut mac = [0u8; MAC_SIZE]; + mac[..MAC_SIZE].clone_from_slice(&message[mac_start..ciphertext_start]); + let mac = mac.into(); + + cbox.decrypt_in_place_detached(&nonce, b"", &mut message[ciphertext_start..], &mac) .map_err(|e| PacketHandlingError::DecryptionError(e.to_string()))?; - data = ByteReader::from_bytes(&cleartext_vec); + + data = ByteReader::from_bytes(&message[ciphertext_start..]); } match header.packet_id { @@ -290,6 +427,7 @@ impl GameServerThread { RequestPlayerListPacket::PACKET_ID => self.handle_request_player_list(&mut data).await, VoicePacket::PACKET_ID => self.handle_voice(&mut data).await, + ChatMessagePacket::PACKET_ID => self.handle_chat_message(&mut data).await, x => Err(PacketHandlingError::NoHandler(x)), } } diff --git a/server/readme.md b/server/readme.md index 68deaa0e..d30454d3 100644 --- a/server/readme.md +++ b/server/readme.md @@ -10,7 +10,7 @@ The central server uses a single JSON file for configuration. By default, the fi ### Central server configuration -The central server allows for configuration hot reloading, so you can modify the file and see updates in real time without restarting the server. +The central server allows configuration hot reloading, so you can modify the file and see updates in real time without restarting the server. Note that the server is written with security in mind, so many of those options may not be exactly interesting for you. If you are hosting a small server for your friends then the defaults should be good enough, however if you are hosting a big public server, it is recommended that you adjust the settings accordingly. @@ -22,7 +22,7 @@ Note that the server is written with security in mind, so many of those options | `special_users` | `{}` | ✅** | Each entry has the account ID as the key and an object with properties `name` and `color` as the value. The `color` property is used for changing the color of the name for this user | | `game_servers` | `[]` | ✅ | Each object has 4 keys: `id` (must be a unique string), `name`, `address` (in format `ip:port`), `region` | | `userlist_mode` | `"none"` | ✅ | Can be `blacklist`, `whitelist`, `none`. See `userlist` property for more information | -| `userlist` | `[]` | ✅ | If `userlist_mode` is set to `blacklist`, block account IDs in this list. If set to `whitelist`, only the users in the list will be allowed access | +| `userlist` | `[]` | ✅ | If `userlist_mode` is set to `blacklist`, block account IDs in this list. If set to `whitelist`, only the users in the list will be allowed to connect | | `no_chat_list` | `[]` | ✅** | List of account IDs of users who are able to connect and play, but have cannot send text/voice messages | | `use_gd_api`* | `false` | ✅ | Use robtop's API to verify account ownership. Note that you must set `challenge_level` accordingly if you enable this setting | | `gd_api`* | `(...)` | ✅ | Link to robtop's API that will be used if `use_gd_api` is enabled. This setting is useful for GDPS owners | @@ -51,7 +51,7 @@ To bridge the servers together, you must use the password from the `game_server_ # on linux remember to replace `set` with `export` and all that set GLOBED_GS_ADDRESS=0.0.0.0:41001 -set GLOBED_GS_CENTRAL_URL=http://127.0.0.1:41000 +set GLOBED_GS_CENTRAL_URL=http://127.0.0.1:41000 set GLOBED_GS_CENTRAL_PASSWORD=password ./globed-game-server.exe ``` diff --git a/server/shared/src/lib.rs b/server/shared/src/lib.rs index 822dc6a3..ff232aa0 100644 --- a/server/shared/src/lib.rs +++ b/server/shared/src/lib.rs @@ -1,4 +1,4 @@ -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use serde::{Deserialize, Serialize}; @@ -18,6 +18,6 @@ pub struct SpecialUser { #[derive(Serialize, Deserialize)] pub struct GameServerBootData { pub protocol: u16, - pub no_chat: Vec, + pub no_chat: HashSet, pub special_users: HashMap, } diff --git a/src/audio/audio_frame.cpp b/src/audio/audio_frame.cpp index 09972f99..41aafd9e 100644 --- a/src/audio/audio_frame.cpp +++ b/src/audio/audio_frame.cpp @@ -4,44 +4,60 @@ using namespace util::data; -EncodedAudioFrame::EncodedAudioFrame() { - // TODO remove tracking - geode::log::debug("constructing audio frame"); -} +EncodedAudioFrame::EncodedAudioFrame() {} EncodedAudioFrame::~EncodedAudioFrame() { - // TODO remove tracking - geode::log::debug("destructing audio frame ({} frames)", frameCount); - for (size_t i = 0; i < frameCount; i++) { - OpusCodec::freeData(frames[i]); - } + this->clear(); } void EncodedAudioFrame::pushOpusFrame(EncodedOpusData&& frame) { - if (frameCount >= VOICE_OPUS_FRAMES_IN_AUDIO_FRAME) { + if (frames.size() >= VOICE_MAX_FRAMES_IN_AUDIO_FRAME) { geode::log::warn("tried to push an extra frame into EncodedAudioFrame, 20 is the max"); return; } - frames[frameCount++] = std::move(frame); + frames.push_back(std::move(frame)); } -// riveting -const std::array& EncodedAudioFrame::getFrames() const { +void EncodedAudioFrame::clear() { + for (const auto& frame : frames) { + OpusCodec::freeData(frame); + } + + frames.clear(); +} + +size_t EncodedAudioFrame::size() const { + return frames.size(); +} + +const std::vector& EncodedAudioFrame::getFrames() const { return frames; } void EncodedAudioFrame::encode(ByteBuffer& buf) const { GLOBED_REQUIRE( - frameCount == VOICE_OPUS_FRAMES_IN_AUDIO_FRAME, - fmt::format("tried to encode an EncodedAudioFrame with {} frames when {} is needed", frameCount, VOICE_OPUS_FRAMES_IN_AUDIO_FRAME) + frames.size() <= VOICE_MAX_FRAMES_IN_AUDIO_FRAME, + fmt::format("tried to encode an EncodedAudioFrame with {} frames when at most {} is permitted", frames.size(), VOICE_MAX_FRAMES_IN_AUDIO_FRAME) ) - buf.writeValueArray(frames); + // first encode all opus frames + for (auto& frame : frames) { + buf.writeOptionalValue(frame); + } + + // if we have written less than 10, write nullopts + + for (size_t i = frames.size(); i < VOICE_MAX_FRAMES_IN_AUDIO_FRAME; i++) { + buf.writeOptionalValue(std::nullopt); + } } void EncodedAudioFrame::decode(ByteBuffer& buf) { - frames = buf.readValueArray(); + for (size_t i = 0; i < VOICE_MAX_FRAMES_IN_AUDIO_FRAME; i++) { + auto frame = buf.readOptionalValue(); + if (frame) frames.push_back(frame.value()); + } } #endif // GLOBED_VOICE_SUPPORT \ No newline at end of file diff --git a/src/audio/audio_frame.hpp b/src/audio/audio_frame.hpp index 64553434..e9983036 100644 --- a/src/audio/audio_frame.hpp +++ b/src/audio/audio_frame.hpp @@ -4,21 +4,14 @@ #if GLOBED_VOICE_SUPPORT -constexpr size_t VOICE_TARGET_SAMPLERATE = 24000; -constexpr size_t VOICE_TARGET_FRAMESIZE = 1440; // for opus, 60ms per single opus frame -constexpr float VOICE_CHUNK_RECORD_TIME = 1.2f; // the audio buffer that will be sent in a single packet in seconds - #include "opus_codec.hpp" #include -// Represents an audio frame (usually around a second long) that contains multiple opus frames +// Represents an audio frame that contains multiple encoded opus frames class EncodedAudioFrame { public: - static constexpr size_t VOICE_OPUS_FRAMES_IN_AUDIO_FRAME = - static_cast( - VOICE_CHUNK_RECORD_TIME * - static_cast(VOICE_TARGET_SAMPLERATE) / static_cast(VOICE_TARGET_FRAMESIZE) - ); + static constexpr size_t VOICE_MAX_FRAMES_IN_AUDIO_FRAME = 10; + EncodedAudioFrame(); ~EncodedAudioFrame(); @@ -30,11 +23,17 @@ class EncodedAudioFrame { EncodedAudioFrame(EncodedAudioFrame&& other) noexcept = default; EncodedAudioFrame& operator=(EncodedAudioFrame&&) noexcept = default; - // adds this audio frame to the list, returns 'true' if we are above the threshold of frames + // adds this audio frame to the list void pushOpusFrame(EncodedOpusData&& frame); + void clear(); + size_t size() const; + constexpr static size_t capacity() { + return VOICE_MAX_FRAMES_IN_AUDIO_FRAME; + } + // extract all frames - const std::array& getFrames() const; + const std::vector& getFrames() const; // implement Serializable @@ -42,8 +41,7 @@ class EncodedAudioFrame { void decode(ByteBuffer& buf); protected: - std::array frames; - size_t frameCount = 0; + mutable std::vector frames; }; diff --git a/src/audio/audio_manager.cpp b/src/audio/audio_manager.cpp index 4fc543d3..378fb175 100644 --- a/src/audio/audio_manager.cpp +++ b/src/audio/audio_manager.cpp @@ -68,11 +68,11 @@ AudioRecordingDevice GlobedAudioManager::getRecordingDevice(int deviceId) { deviceId, name, 256, &device.guid, &device.sampleRate, - &device.speakerMode, + &device.speakerMode, &device.speakerModeChannels, &device.driverState ), "System::getRecordDriverInfo"); - + device.id = deviceId; device.name = std::string(name); @@ -86,7 +86,7 @@ AudioPlaybackDevice GlobedAudioManager::getPlaybackDevice(int deviceId) { deviceId, name, 256, &device.guid, &device.sampleRate, - &device.speakerMode, + &device.speakerMode, &device.speakerModeChannels ), "System::getDriverInfo"); @@ -96,86 +96,137 @@ AudioPlaybackDevice GlobedAudioManager::getPlaybackDevice(int deviceId) { return device; } +bool GlobedAudioManager::isRecordingDeviceSet() { + return recordDevice.id != -1; +} + +void GlobedAudioManager::validateDevices() { + if (recordDevice.id != -1) { + try { + recordDevice = this->getRecordingDevice(recordDevice.id); + } catch (const std::exception& e) { + geode::log::info("Invalidating recording device {}: {}", recordDevice.id, e.what()); + } + } + + if (playbackDevice.id != -1) { + try { + playbackDevice = this->getPlaybackDevice(playbackDevice.id); + } catch (const std::exception& e) { + geode::log::info("Invalidating playback device {}: {}", playbackDevice.id, e.what()); + } + } +} + void GlobedAudioManager::startRecording(std::function callback) { GLOBED_REQUIRE(this->recordDevice.id >= 0, "no recording device is set") - GLOBED_REQUIRE(!isRecording(), "attempting to record when already recording"); + GLOBED_REQUIRE(!this->isRecording() && !recordActive, "attempting to record when already recording"); FMOD_CREATESOUNDEXINFO exinfo = {}; + // TODO figure it out in 2.2. the size is erroneously calculated as 144 on android. +#ifdef GLOBED_ANDROID + exinfo.cbsize = 140; +#else exinfo.cbsize = sizeof(FMOD_CREATESOUNDEXINFO); +#endif + exinfo.numchannels = 1; exinfo.format = FMOD_SOUND_FORMAT_PCMFLOAT; exinfo.defaultfrequency = VOICE_TARGET_SAMPLERATE; - exinfo.length = sizeof(float) * exinfo.numchannels * (int)((float)VOICE_TARGET_SAMPLERATE * VOICE_CHUNK_RECORD_TIME); + exinfo.length = sizeof(float) * exinfo.defaultfrequency * exinfo.numchannels; recordChunkSize = exinfo.length; FMOD_ERR_CHECK( - this->getSystem()->createSound(nullptr, FMOD_2D | FMOD_OPENUSER | FMOD_CREATESAMPLE, &exinfo, &recordSound), + this->getSystem()->createSound(nullptr, FMOD_2D | FMOD_OPENUSER | FMOD_LOOP_NORMAL, &exinfo, &recordSound), "System::createSound" ); - std::lock_guard lock(recordMutex); - - recordContinueStream(); + FMOD_ERR_CHECK( + this->getSystem()->recordStart(recordDevice.id, recordSound, true), + "System::recordStart" + ); - recordActive = true; + recordQueuedStop = false; + recordQueuedHalt = false; + recordLastPosition = 0; recordCallback = callback; + recordActive = true; } -void GlobedAudioManager::stopRecording() { - recordMutex.lock(); - - recordActive = false; - recordCallback = [](const auto& _){}; - - recordMutex.unlock(); +void GlobedAudioManager::internalStopRecording() { + FMOD_ERR_CHECK( + this->getSystem()->recordStop(recordDevice.id), + "System::recordStop" + ); - // wait for audio thread to finish tasks - while (!audioThreadSleeping) { - std::this_thread::sleep_for(std::chrono::milliseconds(5)); + // if halting instead of stopping, don't call the callback + if (recordQueuedHalt) { + recordFrame.clear(); + recordQueuedHalt = false; + } else { + // call the callback if there's any audio leftover + this->recordInvokeCallback(); } - std::lock_guard lock(recordMutex); + // cleanup + recordCallback = [](const auto& _){}; + recordLastPosition = 0; + recordChunkSize = 0; + recordQueue.clear(); if (recordSound) { recordSound->release(); recordSound = nullptr; } + + recordActive = false; } -void GlobedAudioManager::queueStopRecording() { +void GlobedAudioManager::stopRecording() { recordQueuedStop = true; } -void GlobedAudioManager::recordContinueStream() { - FMOD_ERR_CHECK( - this->getSystem()->recordStart(recordDevice.id, recordSound, false), - "System::recordStart" - ); +void GlobedAudioManager::haltRecording() { + recordQueuedStop = true; + recordQueuedHalt = true; } bool GlobedAudioManager::isRecording() { - GLOBED_REQUIRE(this->recordDevice.id >= 0, "no recording device is set") + if (this->recordDevice.id == -1) { + return false; + } + bool recording; + FMOD_ERR_CHECK( this->getSystem()->isRecording(this->recordDevice.id, &recording), "System::isRecording" ); + return recording; } -void GlobedAudioManager::playSound(FMOD::Sound* sound) { +FMOD::Channel* GlobedAudioManager::playSound(FMOD::Sound* sound) { FMOD::Channel* ch = nullptr; FMOD_ERR_CHECK( this->getSystem()->playSound(sound, nullptr, false, &ch), - "System::playSound" + "System::playSound" ); + + return ch; } FMOD::Sound* GlobedAudioManager::createSound(const float* pcm, size_t samples, int sampleRate) { FMOD_CREATESOUNDEXINFO exinfo = {}; + // TODO figure it out in 2.2. the size is erroneously calculated as 144 on android. +#ifdef GLOBED_ANDROID + exinfo.cbsize = 140; +#else exinfo.cbsize = sizeof(FMOD_CREATESOUNDEXINFO); +#endif + exinfo.numchannels = 1; exinfo.format = FMOD_SOUND_FORMAT_PCMFLOAT; exinfo.defaultfrequency = sampleRate; @@ -221,6 +272,10 @@ AudioPlaybackDevice GlobedAudioManager::getPlaybackDevice() { } void GlobedAudioManager::setActiveRecordingDevice(int deviceId) { + if (recordDevice.id != -1) { + GLOBED_REQUIRE(!this->isRecording(), "attempting to change the recording device while recording") + } + recordDevice = this->getRecordingDevice(deviceId); } @@ -228,72 +283,95 @@ void GlobedAudioManager::setActivePlaybackDevice(int deviceId) { playbackDevice = this->getPlaybackDevice(deviceId); } +void GlobedAudioManager::recordInvokeCallback() { + if (recordFrame.size() == 0) return; + + try { + recordCallback(recordFrame); + } catch (const std::exception& e) { + ErrorQueues::get().error(std::string("Exception in audio callback: ") + e.what()); + } + + recordFrame.clear(); +} void GlobedAudioManager::audioThreadFunc() { while (!_terminating) { + // if we are not recording right now, sleep if (!recordActive) { audioThreadSleeping = true; - std::this_thread::sleep_for(std::chrono::milliseconds(100)); + std::this_thread::sleep_for(std::chrono::milliseconds(25)); continue; } + // if someone queued us to stop recording, back to sleeping if (recordQueuedStop) { recordQueuedStop = false; audioThreadSleeping = true; - stopRecording(); + this->internalStopRecording(); continue; } audioThreadSleeping = false; - if (!this->isRecording()) { - // chunk is available, process it - std::lock_guard lock(recordMutex); + float* pcmData; + unsigned int pcmLen; - EncodedAudioFrame frame; + unsigned int pos; + FMOD_ERR_CHECK( + this->getSystem()->getRecordPosition(recordDevice.id, &pos), + "System::getRecordPosition" + ); - float* pcmData; - unsigned int pcmLen; + // if we are at the same position, do nothing + if (pos == recordLastPosition) { + this->getSystem()->update(); + std::this_thread::sleep_for(std::chrono::milliseconds(5)); + continue; + } - FMOD_ERR_CHECK( - recordSound->lock(0, recordChunkSize, (void**)&pcmData, nullptr, &pcmLen, nullptr), - "Sound::lock" - ); + FMOD_ERR_CHECK( + recordSound->lock(0, recordChunkSize, (void**)&pcmData, nullptr, &pcmLen, nullptr), + "Sound::lock" + ); + + if (pos > recordLastPosition) { + recordQueue.writeData(pcmData + recordLastPosition, pos - recordLastPosition); + } else if (pos < recordLastPosition) { // we have reached the end of the buffer + // write the data left at the end + recordQueue.writeData(pcmData + recordLastPosition, pcmLen / sizeof(float) - recordLastPosition); + // write the data from beginning to current pos + recordQueue.writeData(pcmData, pos); + } - float* tmpBuf = new float[pcmLen / sizeof(float)]; - std::memcpy(tmpBuf, pcmData, pcmLen); + recordLastPosition = pos; - FMOD_ERR_CHECK( - recordSound->unlock(pcmData, nullptr, pcmLen, 0), - "Sound::unlock" - ); + FMOD_ERR_CHECK( + recordSound->unlock(pcmData, nullptr, pcmLen, 0), + "Sound::unlock" + ); - recordContinueStream(); + if (recordQueue.size() >= VOICE_TARGET_FRAMESIZE) { + float pcmbuf[VOICE_TARGET_FRAMESIZE]; + recordQueue.copyTo(pcmbuf, VOICE_TARGET_FRAMESIZE); try { - size_t totalOpusFrames = static_cast(VOICE_TARGET_SAMPLERATE) / VOICE_TARGET_FRAMESIZE * VOICE_CHUNK_RECORD_TIME; - - for (size_t i = 0; i < totalOpusFrames; i++) { - const float* dataStart = tmpBuf + i * VOICE_TARGET_FRAMESIZE; - auto encodedFrame = opus.encode(dataStart); - frame.pushOpusFrame(std::move(encodedFrame)); - } + recordFrame.pushOpusFrame(opus.encode(pcmbuf)); } catch (const std::exception& e) { - delete[] tmpBuf; - ErrorQueues::get().warn(std::string("Exception in audio thread: ") + e.what()); + ErrorQueues::get().error(std::string("Exception in audio thread: ") + e.what()); continue; } + } - delete[] tmpBuf; - - try { - recordCallback(frame); - } catch (const std::exception& e) { - ErrorQueues::get().warn(std::string("Exception in audio callback: ") + e.what()); - } - } else { - std::this_thread::sleep_for(std::chrono::milliseconds(3)); + if (recordFrame.size() >= recordFrame.capacity()) { + this->recordInvokeCallback(); } + + this->getSystem()->update(); + + // TODO maybe do something with this i dunno + std::this_thread::sleep_for(std::chrono::milliseconds(5)); + // std::this_thread::yield(); } } diff --git a/src/audio/audio_manager.hpp b/src/audio/audio_manager.hpp index a1e6f64c..a922a072 100644 --- a/src/audio/audio_manager.hpp +++ b/src/audio/audio_manager.hpp @@ -9,8 +9,10 @@ #include #include "opus_codec.hpp" #include "audio_frame.hpp" +#include "audio_sample_queue.hpp" using util::sync::WrappingMutex; +using util::sync::AtomicBool; struct AudioRecordingDevice { int id = -1; @@ -31,6 +33,11 @@ struct AudioPlaybackDevice { int speakerModeChannels; }; +constexpr size_t VOICE_TARGET_SAMPLERATE = 24000; +constexpr float VOICE_CHUNK_RECORD_TIME = 0.06f; // the audio buffer that is recorded at once (60ms) +constexpr size_t VOICE_TARGET_FRAMESIZE = VOICE_TARGET_SAMPLERATE * VOICE_CHUNK_RECORD_TIME; // opus framesize + +// This class is not thread safe. At all. class GlobedAudioManager { public: GLOBED_SINGLETON(GlobedAudioManager); @@ -50,19 +57,29 @@ class GlobedAudioManager { // get the current active playback device AudioPlaybackDevice getPlaybackDevice(); - // start recording the voice and call the callback + // get if the recording device is set + bool isRecordingDeviceSet(); + + // if the current selected recording/playback is invalid (i.e. disconnected), + // it will be reset. if no device is selected or a valid device is selected, nothing happens. + void validateDevices(); + + // start recording the voice and call the callback once a full frame is ready. + // if `stopRecording()` is called at any point, the callback will be called with the remaining data. + // in that case it may have less than the full 10 frames. void startRecording(std::function callback); + // tell the audio thread to stop recording void stopRecording(); - // like stopRecording but is safe to call from the callback in startRecording - void queueStopRecording(); + // tell the audio thread to stop recording, don't call the callback with leftover data + void haltRecording(); bool isRecording(); - // play a sound - void playSound(FMOD::Sound* sound); + // play a sound and return the channel associated with it + [[nodiscard]] FMOD::Channel* playSound(FMOD::Sound* sound); // create a sound from raw PCM data [[nodiscard]] FMOD::Sound* createSound(const float* pcm, size_t samples, int sampleRate = VOICE_TARGET_SAMPLERATE); - + void setActiveRecordingDevice(int deviceId); void setActivePlaybackDevice(int deviceId); @@ -75,28 +92,34 @@ class GlobedAudioManager { FMOD::System* getSystem(); private: - /* recording*/ + /* devices */ AudioRecordingDevice recordDevice; + AudioPlaybackDevice playbackDevice; // unused + + /* recording */ + AtomicBool recordActive = false; + AtomicBool recordQueuedStop = false; + AtomicBool recordQueuedHalt = false; FMOD::Sound* recordSound = nullptr; - util::sync::AtomicBool recordActive = false; size_t recordChunkSize = 0; std::function recordCallback; - std::mutex recordMutex; - util::sync::AtomicBool recordQueuedStop = false; + AudioSampleQueue recordQueue; + unsigned int recordLastPosition = 0; + EncodedAudioFrame recordFrame; void recordContinueStream(); - - AudioPlaybackDevice playbackDevice; + void recordInvokeCallback(); + void internalStopRecording(); /* opus */ OpusCodec opus; /* misc */ - util::sync::AtomicBool _terminating; + AtomicBool _terminating = false; FMOD::System* cachedSystem = nullptr; void audioThreadFunc(); - util::sync::AtomicBool audioThreadSleeping = true; + AtomicBool audioThreadSleeping = true; std::thread audioThreadHandle; }; diff --git a/src/audio/audio_sample_queue.cpp b/src/audio/audio_sample_queue.cpp new file mode 100644 index 00000000..d05eab42 --- /dev/null +++ b/src/audio/audio_sample_queue.cpp @@ -0,0 +1,34 @@ +#include "audio_sample_queue.hpp" + +void AudioSampleQueue::writeData(DecodedOpusData data) { + this->writeData(data.ptr, data.length); +} + +void AudioSampleQueue::writeData(float* pcm, size_t length) { + buf.insert(buf.end(), pcm, pcm + length); +} + +size_t AudioSampleQueue::copyTo(float* dest, size_t samples) { + size_t total; + + if (buf.size() < samples) { + total = buf.size(); + + std::copy(buf.data(), buf.data() + buf.size(), dest); + buf.clear(); + return total; + } + + std::copy(buf.data(), buf.data() + samples, dest); + buf.erase(buf.begin(), buf.begin() + samples); + + return samples; +} + +size_t AudioSampleQueue::size() const { + return buf.size(); +} + +void AudioSampleQueue::clear() { + buf.clear(); +} \ No newline at end of file diff --git a/src/audio/audio_sample_queue.hpp b/src/audio/audio_sample_queue.hpp new file mode 100644 index 00000000..098f2bb7 --- /dev/null +++ b/src/audio/audio_sample_queue.hpp @@ -0,0 +1,21 @@ +#pragma once +#include + +#if GLOBED_VOICE_SUPPORT + +#include "opus_codec.hpp" + +class AudioSampleQueue { +public: + void writeData(DecodedOpusData data); + void writeData(float* pcm, size_t length); + // contrary to the name, this will erase the samples from this queue after copying them to `dest` + size_t copyTo(float* dest, size_t samples); + size_t size() const; + void clear(); + +private: + std::vector buf; +}; + +#endif // GLOBED_VOICE_SUPPORT \ No newline at end of file diff --git a/src/audio/audio_stream.cpp b/src/audio/audio_stream.cpp index 0f6ab6c4..fe4bbe4c 100644 --- a/src/audio/audio_stream.cpp +++ b/src/audio/audio_stream.cpp @@ -8,44 +8,21 @@ #include #include -/* AudioSampleQueue */ - -void AudioSampleQueue::writeData(DecodedOpusData data) { - buf.insert(buf.end(), data.ptr, data.ptr + data.length); -} - -size_t AudioSampleQueue::copyTo(float* dest, size_t samples) { - size_t total; - - if (buf.size() < samples) { - total = buf.size(); - - std::copy(buf.data(), buf.data() + buf.size(), dest); - buf.clear(); - return total; - } - - std::copy(buf.data(), buf.data() + samples, dest); - buf.erase(buf.begin(), buf.begin() + samples); - - return samples; -} - -size_t AudioSampleQueue::size() { - return buf.size(); -} - -/* AudioStream */ - AudioStream::AudioStream() { FMOD_CREATESOUNDEXINFO exinfo = {}; + // TODO figure it out in 2.2. the size is erroneously calculated as 144 on android. +#ifdef GLOBED_ANDROID + exinfo.cbsize = 140; +#else exinfo.cbsize = sizeof(FMOD_CREATESOUNDEXINFO); +#endif + exinfo.numchannels = 1; exinfo.format = FMOD_SOUND_FORMAT_PCMFLOAT; exinfo.defaultfrequency = VOICE_TARGET_SAMPLERATE; exinfo.userdata = this; - exinfo.length = sizeof(float) * exinfo.numchannels * exinfo.defaultfrequency * (VOICE_CHUNK_RECORD_TIME / 2); + exinfo.length = sizeof(float) * exinfo.numchannels * exinfo.defaultfrequency * (VOICE_CHUNK_RECORD_TIME * EncodedAudioFrame::VOICE_MAX_FRAMES_IN_AUDIO_FRAME); // geode::log::debug("{}: creating stream, length: {}", util::time::nowPretty(), exinfo.length); exinfo.pcmreadcallback = [](FMOD_SOUND* sound_, void* data, unsigned int len) -> FMOD_RESULT { @@ -66,7 +43,7 @@ AudioStream::AudioStream() { size_t copied = stream->queue.copyTo((float*)data, neededSamples); if (copied != neededSamples) { - geode::log::debug("{}: could not match samples, needed: {}, got: {}", util::time::nowPretty(), neededSamples, copied); + // geode::log::debug("{}: could not match samples, needed: {}, got: {}", util::time::nowPretty(), neededSamples, copied); // fill the rest with the void to not repeat stuff for (size_t i = copied; i < neededSamples; i++) { ((float*)data)[i] = 0.0f; @@ -77,9 +54,9 @@ AudioStream::AudioStream() { return FMOD_OK; }; - + auto& vm = GlobedAudioManager::get(); - + FMOD_RESULT res; auto system = vm.getSystem(); res = system->createStream(nullptr, FMOD_OPENUSER | FMOD_2D | FMOD_LOOP_NORMAL, &exinfo, &sound); @@ -88,13 +65,19 @@ AudioStream::AudioStream() { } AudioStream::~AudioStream() { - geode::log::debug("releasing sound"); - // TODO fix this wtf - // if (sound) sound->release(); + if (channel) { + channel->stop(); + channel = nullptr; + } + + if (sound) { + sound->release(); + sound = nullptr; + } } void AudioStream::start() { - GlobedAudioManager::get().playSound(sound); + this->channel = GlobedAudioManager::get().playSound(sound); } void AudioStream::writeData(const EncodedAudioFrame& frame) { diff --git a/src/audio/audio_stream.hpp b/src/audio/audio_stream.hpp index 6ca65bf8..21f21588 100644 --- a/src/audio/audio_stream.hpp +++ b/src/audio/audio_stream.hpp @@ -4,16 +4,7 @@ #if GLOBED_VOICE_SUPPORT #include "audio_frame.hpp" - -class AudioSampleQueue { -public: - void writeData(DecodedOpusData data); - size_t copyTo(float* dest, size_t samples); - size_t size(); - -private: - std::vector buf; -}; +#include "audio_sample_queue.hpp" class AudioStream { public: @@ -35,6 +26,7 @@ class AudioStream { private: FMOD::Sound* sound = nullptr; + FMOD::Channel* channel = nullptr; AudioSampleQueue queue; }; diff --git a/src/audio/opus_codec.hpp b/src/audio/opus_codec.hpp index 1270c1d4..9c828a89 100644 --- a/src/audio/opus_codec.hpp +++ b/src/audio/opus_codec.hpp @@ -7,7 +7,7 @@ #include #include -const size_t VOICE_MAX_BYTES_IN_FRAME = 1500; // TODO ??? adjust later +const size_t VOICE_MAX_BYTES_IN_FRAME = 1000; // on avg 200 bytes but y'know, just in case class EncodedOpusData { public: diff --git a/src/audio/voice_playback_manager.cpp b/src/audio/voice_playback_manager.cpp index ae2c8a3b..0c1fcc83 100644 --- a/src/audio/voice_playback_manager.cpp +++ b/src/audio/voice_playback_manager.cpp @@ -13,13 +13,25 @@ VoicePlaybackManager::~VoicePlaybackManager() {} void VoicePlaybackManager::playFrameStreamed(int playerId, const EncodedAudioFrame& frame) { // if the stream doesn't exist yet, create it if (!streams.contains(playerId)) { - auto stream = std::make_unique(); - stream->start(); - streams.insert(std::make_pair(playerId, std::move(stream))); + this->prepareStream(playerId); } auto& stream = streams.at(playerId); stream->writeData(frame); } +void VoicePlaybackManager::stopAllStreams() { + streams.clear(); +} + +void VoicePlaybackManager::prepareStream(int playerId) { + auto stream = std::make_unique(); + stream->start(); + streams.insert(std::make_pair(playerId, std::move(stream))); +} + +void VoicePlaybackManager::removeStream(int playerId) { + streams.erase(playerId); +} + #endif // GLOBED_VOICE_SUPPORT \ No newline at end of file diff --git a/src/audio/voice_playback_manager.hpp b/src/audio/voice_playback_manager.hpp index 7003ddbe..75f33448 100644 --- a/src/audio/voice_playback_manager.hpp +++ b/src/audio/voice_playback_manager.hpp @@ -10,6 +10,7 @@ /* * VoicePlaybackManager is responsible for playing voices of multiple people * at the same time efficiently and without memory leaks. +* Not thread safe. */ class VoicePlaybackManager { public: @@ -18,8 +19,10 @@ class VoicePlaybackManager { ~VoicePlaybackManager(); void playFrameStreamed(int playerId, const EncodedAudioFrame& frame); + void stopAllStreams(); - void releaseStaleSounds(); + void prepareStream(int playerId); + void removeStream(int playerId); private: std::unordered_map> streams; diff --git a/src/crypto/box.cpp b/src/crypto/box.cpp index cce80183..903c0511 100644 --- a/src/crypto/box.cpp +++ b/src/crypto/box.cpp @@ -25,7 +25,7 @@ CryptoBox::CryptoBox(byte* key) { CRYPTO_ERR_CHECK(func_box_keypair(publicKey, secretKey), "func_box_keypair failed"); if (key != nullptr) { - setPeerKey(key); + this->setPeerKey(key); } } diff --git a/src/crypto/box.hpp b/src/crypto/box.hpp index 137b72f8..df5341fe 100644 --- a/src/crypto/box.hpp +++ b/src/crypto/box.hpp @@ -39,7 +39,7 @@ class CryptoBox : public BaseCryptoBox { CryptoBox(const CryptoBox&) = delete; CryptoBox& operator=(const CryptoBox&) = delete; ~CryptoBox(); - + // Get our public key. The returned pointer lives as long as this `CryptoBox` object does. util::data::byte* getPublicKey() noexcept; diff --git a/src/data/packets/all.cpp b/src/data/packets/all.cpp index ffd4aa93..bd2827e6 100644 --- a/src/data/packets/all.cpp +++ b/src/data/packets/all.cpp @@ -21,6 +21,7 @@ std::shared_ptr matchPacket(packetid_t packetId) { #if GLOBED_VOICE_SUPPORT PACKET(VoiceBroadcastPacket); #endif + PACKET(ChatMessageBroadcastPacket); default: return std::shared_ptr(nullptr); diff --git a/src/data/packets/all.hpp b/src/data/packets/all.hpp index 1b80f3ec..e0312504 100644 --- a/src/data/packets/all.hpp +++ b/src/data/packets/all.hpp @@ -19,6 +19,7 @@ #include "client/connection.hpp" #include "client/game.hpp" +#include "client/misc.hpp" #include "server/connection.hpp" #include "server/game.hpp" diff --git a/src/data/packets/client/connection.hpp b/src/data/packets/client/connection.hpp index 6da5e21a..3b5c1033 100644 --- a/src/data/packets/client/connection.hpp +++ b/src/data/packets/client/connection.hpp @@ -24,7 +24,7 @@ class CryptoHandshakeStartPacket : public Packet { buf.writeU16(protocol); buf.writeValue(key); } - + GLOBED_PACKET_DECODE_UNIMPL CryptoHandshakeStartPacket(uint16_t _protocol, CryptoPublicKey _key) : protocol(_protocol), key(_key) {} @@ -39,7 +39,7 @@ class CryptoHandshakeStartPacket : public Packet { class KeepalivePacket : public Packet { GLOBED_PACKET(10002, false) - + GLOBED_PACKET_ENCODE {} GLOBED_PACKET_DECODE_UNIMPL @@ -54,18 +54,20 @@ class LoginPacket : public Packet { GLOBED_PACKET_ENCODE { buf.writeI32(accountId); + buf.writeString(name); buf.writeString(token); } GLOBED_PACKET_DECODE_UNIMPL - - LoginPacket(int32_t _accid, const std::string& _token) : accountId(_accid), token(_token) {} - static std::shared_ptr create(int32_t accid, const std::string& token) { - return std::make_shared(accid, token); + LoginPacket(int32_t accid, const std::string& name, const std::string& token) : accountId(accid), name(name), token(token) {} + + static std::shared_ptr create(int32_t accid, const std::string& name, const std::string& token) { + return std::make_shared(accid, name, token); } int32_t accountId; + std::string name; std::string token; }; diff --git a/src/data/packets/client/game.hpp b/src/data/packets/client/game.hpp index 92dd8592..7fc75d74 100644 --- a/src/data/packets/client/game.hpp +++ b/src/data/packets/client/game.hpp @@ -79,7 +79,7 @@ class PlayerDataPacket : public Packet { GLOBED_PACKET_ENCODE { buf.writeValue(data); } - + GLOBED_PACKET_DECODE_UNIMPL PlayerDataPacket(const PlayerData& data) : data(data) {} @@ -116,13 +116,31 @@ class VoicePacket : public Packet { GLOBED_PACKET_DECODE_UNIMPL - VoicePacket(std::unique_ptr _frame) : frame(std::move(_frame)) {} + VoicePacket(std::shared_ptr _frame) : frame(std::move(_frame)) {} - static std::shared_ptr create(std::unique_ptr frame) { + static std::shared_ptr create(std::shared_ptr frame) { return std::make_shared(std::move(frame)); } - std::unique_ptr frame; + std::shared_ptr frame; }; -#endif // GLOBED_VOICE_SUPPORT \ No newline at end of file +#endif // GLOBED_VOICE_SUPPORT + +class ChatMessagePacket : public Packet { + GLOBED_PACKET(11011, true) + + GLOBED_PACKET_ENCODE { + buf.writeString(message); + } + + GLOBED_PACKET_DECODE_UNIMPL + + ChatMessagePacket(const std::string& message) : message(message) {} + + static std::shared_ptr create(const std::string& message) { + return std::make_shared(message); + } + + std::string message; +}; \ No newline at end of file diff --git a/src/data/packets/client/misc.hpp b/src/data/packets/client/misc.hpp new file mode 100644 index 00000000..257e412a --- /dev/null +++ b/src/data/packets/client/misc.hpp @@ -0,0 +1,35 @@ +#pragma once +#include +#include +#include + +/* +* RawPacket is a special packet. It is not an actual specific packet and has no consistent representation. +* Example usage and explanation can be found in `ui/hooks/play_layer.hpp` in the audio callback function. +*/ +class RawPacket : public Packet { +public: + RawPacket(packetid_t id, bool encrypted, ByteBuffer buffer) : id(id), encrypted(encrypted), buffer(std::move(buffer)) {} + + packetid_t getPacketId() const override { + return id; + } + + bool getEncrypted() const override { + return encrypted; + } + + GLOBED_PACKET_ENCODE { + buf.writeBytes(buffer.getDataRef()); + } + + GLOBED_PACKET_DECODE_UNIMPL + + static std::shared_ptr create(packetid_t id, bool encrypted, ByteBuffer buffer) { + return std::make_shared(id, encrypted, std::move(buffer)); + } + + packetid_t id; + bool encrypted; + mutable ByteBuffer buffer; +}; \ No newline at end of file diff --git a/src/data/packets/packet.hpp b/src/data/packets/packet.hpp index edf03af4..1aaa021c 100644 --- a/src/data/packets/packet.hpp +++ b/src/data/packets/packet.hpp @@ -6,8 +6,9 @@ using packetid_t = uint16_t; #define GLOBED_PACKET(id,enc) \ public: \ static constexpr packetid_t PACKET_ID = id; \ + static constexpr bool ENCRYPTED = enc; \ packetid_t getPacketId() const override { return this->PACKET_ID; } \ - bool getEncrypted() const override { return enc; } + bool getEncrypted() const override { return this->ENCRYPTED; } #define GLOBED_PACKET_ENCODE_UNIMPL \ GLOBED_PACKET_ENCODE { \ diff --git a/src/data/packets/server/connection.hpp b/src/data/packets/server/connection.hpp index 53b841be..51e91d22 100644 --- a/src/data/packets/server/connection.hpp +++ b/src/data/packets/server/connection.hpp @@ -58,7 +58,7 @@ class LoginFailedPacket : public Packet { }; class ServerNoticePacket : public Packet { - GLOBED_PACKET(20006, true) + GLOBED_PACKET(20006, false) GLOBED_PACKET_ENCODE_UNIMPL GLOBED_PACKET_DECODE { message = buf.readString(); } diff --git a/src/data/packets/server/game.hpp b/src/data/packets/server/game.hpp index 8edbbbef..33d05f60 100644 --- a/src/data/packets/server/game.hpp +++ b/src/data/packets/server/game.hpp @@ -47,7 +47,7 @@ class VoiceBroadcastPacket : public Packet { GLOBED_PACKET(21010, true) GLOBED_PACKET_ENCODE_UNIMPL - + #if GLOBED_VOICE_SUPPORT GLOBED_PACKET_DECODE { sender = buf.readI32(); @@ -60,3 +60,17 @@ class VoiceBroadcastPacket : public Packet { GLOBED_PACKET_DECODE {} #endif // GLOBED_VOICE_SUPPORT }; + +class ChatMessageBroadcastPacket : public Packet { + GLOBED_PACKET(21011, true) + + GLOBED_PACKET_ENCODE_UNIMPL + + GLOBED_PACKET_DECODE { + sender = buf.readI32(); + message = buf.readString(); + } + + int sender; + std::string message; +}; \ No newline at end of file diff --git a/src/defs/assert.hpp b/src/defs/assert.hpp index 59074047..572bce9e 100644 --- a/src/defs/assert.hpp +++ b/src/defs/assert.hpp @@ -32,14 +32,14 @@ #if GLOBED_CAN_USE_SOURCE_LOCATION && !defined(GLOBED_ROOT_NO_GEODE) # define GLOBED_REQUIRE(condition,message) \ if (!(condition)) [[unlikely]] { \ - auto ev_msg = message; \ + auto ev_msg = (message); \ auto loc = GLOBED_SOURCE; \ - geode::log::error("Condition failed at {}: {}", fmt::format("{}:{} ({})", loc.file_name(), loc.line(), loc.function_name()), ev_msg); \ - throw std::runtime_error(std::string("Globed condition failed: ") + ev_msg); \ + geode::log::warn("Condition failed at {}: {}", fmt::format("{}:{} ({})", loc.file_name(), loc.line(), loc.function_name()), ev_msg); \ + throw std::runtime_error(std::string(ev_msg)); \ } # define GLOBED_HARD_ASSERT(condition,message) \ if (!(condition)) [[unlikely]] { \ - auto ev_msg = message; \ + auto ev_msg = (message); \ auto loc = GLOBED_SOURCE; \ geode::log::error("Condition failed at {}: {}", fmt::format("{}:{} ({})", loc.file_name(), loc.line(), loc.function_name()), ev_msg); \ GLOBED_SUICIDE; \ @@ -47,13 +47,13 @@ #else # define GLOBED_REQUIRE(condition,message) \ if (!(condition)) [[unlikely]] { \ - auto ev_msg = message; \ + auto ev_msg = (message); \ GLOBED_REQUIRE_LOG(std::string("Condition failed: ") + ev_msg); \ - throw std::runtime_error(std::string("Globed condition failed: ") + ev_msg); \ + throw std::runtime_error(std::string(ev_msg)); \ } # define GLOBED_HARD_ASSERT(condition,message) \ if (!(condition)) [[unlikely]] { \ - auto ev_msg = message; \ + auto ev_msg = (message); \ GLOBED_REQUIRE_LOG(std::string("Condition failed: ") + ev_msg); \ GLOBED_SUICIDE; \ } diff --git a/src/defs/platform.hpp b/src/defs/platform.hpp index 8efc42a9..2df8b6e8 100644 --- a/src/defs/platform.hpp +++ b/src/defs/platform.hpp @@ -64,12 +64,15 @@ #ifdef GLOBED_WIN32 # define GLOBED_HAS_FMOD GLOBED_FMOD_WINDOWS # define GLOBED_HAS_DRPC GLOBED_DRPC_WINDOWS +# define GLOBED_HAS_KEYBINDS 1 #elif defined(GLOBED_MAC) # define GLOBED_HAS_FMOD GLOBED_FMOD_MAC # define GLOBED_HAS_DRPC GLOBED_DRPC_MAC +# define GLOBED_HAS_KEYBINDS 1 #elif defined(GLOBED_ANDROID) # define GLOBED_HAS_FMOD GLOBED_FMOD_ANDROID # define GLOBED_HAS_DRPC GLOBED_DRPC_ANDROID +# define GLOBED_HAS_KEYBINDS 0 #elif defined(GLOBED_TESTING) #else # error "what" diff --git a/src/main.cpp b/src/main.cpp index 5578271c..bfc81a3f 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,5 +1,10 @@ +#include #include -#include + +#if GLOBED_HAS_KEYBINDS +#include +#endif + #include #include @@ -18,35 +23,42 @@ #include #include +#include + using namespace geode::prelude; +void setupSodiumMisuse(); +void setupErrorCheckNode(); +void setupCustomKeybinds(); +void printDebugInfo(); + +// vvv this is for testing fmod +// void createStreamDetour(FMOD::System* self, const char* unused1, FMOD_MODE mode, FMOD_CREATESOUNDEXINFO* exinfo, FMOD::Sound** sound) { +// log::debug("creating stream with mode {}", mode); +// log::debug("exinfo cbs: {}", exinfo->cbsize); +// log::debug("exinfo len: {}", exinfo->length); +// log::debug("exinfo channels: {}", exinfo->numchannels); +// log::debug("exinfo freq: {}", exinfo->defaultfrequency); +// log::debug("exinfo fmt: {}", exinfo->format); +// log::debug("exinfo stype: {}", exinfo->suggestedsoundtype); +// self->createStream(unused1, mode, exinfo, sound); +// } + $on_mod(Loaded) { - // if there is a logic error in the crypto code, sodium_misuse() gets called - sodium_set_misuse_handler([](){ - log::error("sodium_misuse called. we are officially screwed."); - util::debugging::suicide(); - }); + setupSodiumMisuse(); + setupErrorCheckNode(); + setupCustomKeybinds(); - auto ecn = ErrorCheckNode::create(); - ecn->setID("error-check-node"_spr); - SceneManager::get()->keepAcrossScenes(ecn); - #if defined(GLOBED_DEBUG) && GLOBED_DEBUG - geode::log::warn("=== Globed v{} has been loaded in debug mode ===", Mod::get()->getVersion().toString()); - geode::log::info("Platform: {}", GLOBED_PLATFORM_STRING); - geode::log::info("FMOD support: {}", GLOBED_HAS_FMOD == 0 ? "false" : "true"); - geode::log::info("Voice support: {}", GLOBED_VOICE_SUPPORT == 0 ? "false" : "true"); - geode::log::info("Discord RPC support: {}", GLOBED_HAS_DRPC == 0 ? "false" : "true"); - geode::log::info("Little endian: {}", GLOBED_LITTLE_ENDIAN ? "true" : "false"); - geode::log::info("Libsodium version: {}", SODIUM_VERSION_STRING); - #if GLOBED_VOICE_SUPPORT - geode::log::info("Opus version: {}", opus_get_version_string()); - #endif + printDebugInfo(); #endif + + /// vvv this is for testing fmod + // (void) Mod::get()->addHook(reinterpret_cast( + // geode::addresser::getNonVirtual(&FMOD::System::createStream) + // ), &createStreamDetour, "FMOD::System::createStream", tulip::hook::TulipConvention::Default); } -void testFmod1(); -void testFmod2(); class $modify(MyMenuLayer, MenuLayer) { void onMoreGames(CCObject*) { @@ -54,82 +66,62 @@ class $modify(MyMenuLayer, MenuLayer) { util::debugging::PacketLogger::get().getSummary().print(); } - // nm.addListener([](auto* packet) { - // log::debug("got ping packet with id {}, pc: {}", packet->id, packet->playerCount); - // }); - - // nm.addListener(PingResponsePacket::PACKET_ID, [](std::shared_ptr packet) { - // auto pkt = static_cast(packet.get()); - // log::debug("got ping packet with id {}, pc: {}", pkt->id, pkt->playerCount); - // }); - - // nm.connect("127.0.0.1", 41001); - // nm.send(PingPacket::create(69696969)); - - // DiscordManager::get().update(); - // auto& vm = GlobedAudioManager::get(); - // vm.setActiveRecordingDevice(2); - // log::debug("Listening to: {}", vm.getRecordingDevice().name); - - // testFmod1(); - // testFmod2(); - - // util::debugging::PacketLogger::get().getSummary().print(); - + try { + throw std::runtime_error("oopsie"); + } catch (std::exception e) { + log::debug("caught it! {}", e.what()); + } } }; -#if GLOBED_VOICE_SUPPORT -// following listens to a frame and immediately plays it -void testFmod1() { - auto& vm = GlobedAudioManager::get(); - vm.startRecording([&vm](const EncodedAudioFrame& frame){ - util::debugging::Benchmarker bench; +// if there is a logic error in the crypto code, this lambda will be called +void setupSodiumMisuse() { + sodium_set_misuse_handler([](){ + log::error("sodium_misuse called. we are officially screwed."); + util::debugging::suicide(); + }); +} - ByteBuffer bb; - bb.writeValue(frame); +// error check node runs on every scene and shows popups/notifications if an error has occured in another thread +void setupErrorCheckNode() { + auto ecn = ErrorCheckNode::create(); + ecn->setID("error-check-node"_spr); + SceneManager::get()->keepAcrossScenes(ecn); +} - bb.setPosition(0); +void setupCustomKeybinds() { +#if GLOBED_HAS_KEYBINDS + using namespace keybinds; - auto decodedFrame = bb.readValue(); - VoicePlaybackManager::get().playFrameStreamed(0, decodedFrame); + BindManager::get()->registerBindable({ + "voice-activate"_spr, + "Voice", + "Records audio from your microphone and sends it off to other users on the level.", + { Keybind::create(KEY_V, Modifier::None) }, + Category::PLAY, }); -} - -// following joins 5 different frames into one sound and plays it -void testFmod2() { - auto& vm = GlobedAudioManager::get(); - - ByteBuffer* bb = new ByteBuffer; - int* total = new int{0}; - - vm.startRecording([&vm, bb, total](const EncodedAudioFrame& frame){ - *total = *total + 1; - - bb->writeValue(frame); - log::debug("Frame {} recorded", static_cast(*total)); - - if (*total > 5) { // end after 5 frames - vm.queueStopRecording(); - bb->setPosition(0); - std::vector out; - for (size_t i = 0; i < *total; i++) { - log::debug("Reading frame {}", i); - auto encFrame = bb->readValue(); - const auto& opusFrames = encFrame.getFrames(); - - for (const auto& opusFrame : opusFrames) { - auto rawFrame = vm.decodeSound(opusFrame); - out.insert(out.end(), rawFrame.ptr, rawFrame.ptr + rawFrame.length); - } - } - - log::debug("total pcm samples: {}", out.size()); - - auto sound = vm.createSound(out.data(), out.size()); - vm.playSound(sound); - } + BindManager::get()->registerBindable({ + "voice-deafen"_spr, + "Deafen", + "Mutes voices of other players when toggled.", + { Keybind::create(KEY_B, Modifier::None) }, + Category::PLAY, }); + +#endif // GLOBED_HAS_KEYBINDS } -#endif // GLOBED_VOICE_SUPPORT + +// just debug printing +void printDebugInfo() { + geode::log::warn("=== Globed v{} has been loaded in debug mode ===", Mod::get()->getVersion().toString()); + geode::log::info("Platform: {}", GLOBED_PLATFORM_STRING); + geode::log::info("FMOD support: {}", GLOBED_HAS_FMOD == 0 ? "false" : "true"); + geode::log::info("Voice support: {}", GLOBED_VOICE_SUPPORT == 0 ? "false" : "true"); + geode::log::info("Discord RPC support: {}", GLOBED_HAS_DRPC == 0 ? "false" : "true"); + geode::log::info("Little endian: {}", GLOBED_LITTLE_ENDIAN ? "true" : "false"); + geode::log::info("Libsodium version: {} (CryptoBox algorithm: {})", SODIUM_VERSION_STRING, CryptoBox::ALGORITHM); + #if GLOBED_VOICE_SUPPORT + geode::log::info("Opus version: {}", opus_get_version_string()); + #endif +} \ No newline at end of file diff --git a/src/managers/server_manager.cpp b/src/managers/server_manager.cpp index 3caa8d10..130a40e2 100644 --- a/src/managers/server_manager.cpp +++ b/src/managers/server_manager.cpp @@ -10,31 +10,31 @@ GlobedServerManager::GlobedServerManager() { if (storedActive.empty()) { // storedActive = "https://globed.dankmeme.dev"; // TODO prod ^^^ - storedActive = "http://127.0.0.1:41000"; + storedActive = "http://192.168.0.100:41000"; } - _data.write()->central = storedActive; + _data.lock()->central = storedActive; } void GlobedServerManager::setCentral(std::string address) { if (!address.empty() && address.ends_with('/')) { address.pop_back(); } - + geode::Mod::get()->setSavedValue("active-central-server", address); - auto data = _data.write(); + auto data = _data.lock(); data->central = address; data->servers.clear(); } std::string GlobedServerManager::getCentral() { - return _data.read()->central; + return _data.lock()->central; } void GlobedServerManager::addGameServer(const std::string& serverId, const std::string& name, const std::string& address, const std::string& region) { auto addr = util::net::splitAddress(address); - _data.write()->servers[serverId] = GameServerInfo { + _data.lock()->servers[serverId] = GameServerInfo { .name = name, .region = region, .address = {.ip = addr.first, .port = addr.second}, @@ -44,28 +44,28 @@ void GlobedServerManager::addGameServer(const std::string& serverId, const std:: } void GlobedServerManager::setActiveGameServer(const std::string& serverId) { - auto data = _data.write()->game = serverId; + _data.lock()->game = serverId; } std::string GlobedServerManager::getActiveGameServer() { - return _data.read()->game; + return _data.lock()->game; } void GlobedServerManager::clearGameServers() { - auto data = _data.write(); + auto data = _data.lock(); data->servers.clear(); data->activePingId = 0; data->game = ""; } size_t GlobedServerManager::gameServerCount() { - return _data.read()->servers.size(); + return _data.lock()->servers.size(); } uint32_t GlobedServerManager::pingStart(const std::string& serverId) { uint32_t pingId = util::rng::Random::get().generate(); - auto data = _data.write(); + auto data = _data.lock(); auto& gsi = data->servers.at(serverId); if (gsi.pendingPings.size() > 50) { @@ -80,18 +80,18 @@ uint32_t GlobedServerManager::pingStart(const std::string& serverId) { } void GlobedServerManager::pingStartActive() { - std::string gameServer = _data.read()->game; + std::string gameServer = _data.lock()->game; if (!gameServer.empty()) { auto pingId = this->pingStart(gameServer); - _data.write()->activePingId = pingId; + _data.lock()->activePingId = pingId; } } void GlobedServerManager::pingFinish(uint32_t pingId, uint32_t playerCount) { auto now = util::time::nowMillis(); - auto data = _data.write(); + auto data = _data.lock(); for (auto* server : util::collections::mapValuesBorrowed(data->servers)) { if (server->pendingPings.contains(pingId)) { auto start = server->pendingPings.at(pingId); @@ -108,13 +108,13 @@ void GlobedServerManager::pingFinish(uint32_t pingId, uint32_t playerCount) { } void GlobedServerManager::pingFinishActive(uint32_t playerCount) { - uint32_t pingId = _data.read()->activePingId; + uint32_t pingId = _data.lock()->activePingId; this->pingFinish(pingId, playerCount); } GameServerView GlobedServerManager::getGameServer(const std::string& serverId) { - auto data = _data.read(); + auto data = _data.lock(); auto& gsi = data->servers.at(serverId); return GameServerView { .id = serverId, @@ -127,7 +127,7 @@ GameServerView GlobedServerManager::getGameServer(const std::string& serverId) { } std::vector GlobedServerManager::getPingHistory(const std::string& serverId) { - auto data = _data.read(); + auto data = _data.lock(); auto& gsi = data->servers.at(serverId); return gsi.pingHistory.extract(); } @@ -135,7 +135,7 @@ std::vector GlobedServerManager::getPingHistory(const std: std::unordered_map GlobedServerManager::extractGameServers() { std::unordered_map out; - auto data = _data.read(); + auto data = _data.lock(); for (const auto& [serverId, gsi] : data->servers) { out[serverId] = GameServerView { .id = serverId, diff --git a/src/managers/server_manager.hpp b/src/managers/server_manager.hpp index eb99b3a8..f0fde1cc 100644 --- a/src/managers/server_manager.hpp +++ b/src/managers/server_manager.hpp @@ -5,7 +5,6 @@ #include #include -#include struct GameServerAddress { std::string ip; @@ -78,5 +77,5 @@ class GlobedServerManager { uint32_t activePingId; }; - util::sync::WrappingRwLock _data; + util::sync::WrappingMutex _data; }; \ No newline at end of file diff --git a/src/managers/settings.cpp b/src/managers/settings.cpp index 2df88f76..ad6987c1 100644 --- a/src/managers/settings.cpp +++ b/src/managers/settings.cpp @@ -5,6 +5,11 @@ GlobedSettings::GlobedSettings() { this->refreshCache(); } +void GlobedSettings::reset(const std::string& key) { + this->setFlag("_gset_-" + key, false); + this->refreshCache(); +} + CachedSettings GlobedSettings::getCached() { return *_cache.lock(); } diff --git a/src/managers/settings.hpp b/src/managers/settings.hpp index 0d29e7be..db2c8726 100644 --- a/src/managers/settings.hpp +++ b/src/managers/settings.hpp @@ -6,16 +6,30 @@ struct CachedSettings { bool test; }; +#define MAKE_DEFAULT(_key, value) if (key == (_key)) return (value); + // Besides `getCached()`, this class is not thread safe (reason: getSavedValue/setSavedValue) class GlobedSettings { GLOBED_SINGLETON(GlobedSettings); GlobedSettings(); + /* + * ok i know this is a mess but here's a rundown of how the settings work: + * say we have a setting that is an int called 'hello'. + * when retreiving, we first attempt to read `gflag-_gset_-hello` via getFlag() + * if the flag is false, we return a default value (defaults reside in `get`). + * otherwise, we return `gsetting-hello`. + * + * upon saving, we explicitly set the flag described earlier to true and save the value. + * i hope this isn't confusing! + */ + // directly set and save the setting as json template inline void set(const std::string& key, const T& elem, bool refresh = true) { + this->setFlag("_gset_-" + key); geode::Mod::get()->setSavedValue("gsetting-" + key, elem); - + if (refresh) { this->refreshCache(); } @@ -24,9 +38,18 @@ class GlobedSettings { // directly get the setting as json template inline T get(const std::string& key) { - return geode::Mod::get()->getSavedValue("gsetting-" + key); + if (this->getFlag("_gset_-" + key)) { + return geode::Mod::get()->getSavedValue("gsetting-" + key); + } else { + MAKE_DEFAULT("test", true) + + return T{}; + } } + // reset a setting to its default value + void reset(const std::string& key); + // get cached settings for performance CachedSettings getCached(); @@ -37,4 +60,6 @@ class GlobedSettings { private: util::sync::WrappingMutex _cache; -}; \ No newline at end of file +}; + +#undef MAKE_DEFAULT \ No newline at end of file diff --git a/src/net/game_socket.cpp b/src/net/game_socket.cpp index 0a84bed6..44b960f7 100644 --- a/src/net/game_socket.cpp +++ b/src/net/game_socket.cpp @@ -50,7 +50,12 @@ std::shared_ptr GameSocket::recvPacket() { buf.resize(messageLength + PacketHeader::SIZE); } - packet->decode(buf); + try { + packet->decode(buf); + } catch (const std::exception& e) { + geode::log::warn("Decoding packet ID {} failed: {}", header.id, e.what()); + throw; + } return packet; } @@ -65,7 +70,7 @@ void GameSocket::sendPacket(std::shared_ptr packet) { buf.writeValue(header); packet->encode(buf); - + size_t packetSize = buf.size() - PacketHeader::SIZE; bytevector& dataref = buf.getDataRef(); diff --git a/src/net/network_manager.cpp b/src/net/network_manager.cpp index b5215ef2..9096a7af 100644 --- a/src/net/network_manager.cpp +++ b/src/net/network_manager.cpp @@ -1,7 +1,6 @@ #include "network_manager.hpp" #include -#include #include #include #include @@ -21,12 +20,13 @@ NetworkManager::NetworkManager() { // add builtin listeners addBuiltinListener([this](auto packet) { - this->gameSocket.box->setPeerKey(packet->data.key.data()); + gameSocket.box->setPeerKey(packet->data.key.data()); _established = true; // and lets try to login! auto& am = GlobedAccountManager::get(); auto authtoken = *am.authToken.lock(); - this->send(LoginPacket::create(am.gdData.lock()->accountId, authtoken)); + auto gddata = am.gdData.lock(); + this->send(LoginPacket::create(gddata->accountId, gddata->accountName, authtoken)); }); addBuiltinListener([this](auto packet) { @@ -62,13 +62,13 @@ NetworkManager::NetworkManager() { NetworkManager::~NetworkManager() { // clear listeners - removeAllListeners(); + this->removeAllListeners(); builtinListeners.lock()->clear(); // wait for threads _running = false; - if (connected()) { + if (this->connected()) { log::debug("disconnecting from the server.."); this->disconnect(); } @@ -86,14 +86,14 @@ NetworkManager::~NetworkManager() { } void NetworkManager::connect(const std::string& addr, unsigned short port) { - if (connected()) { + if (this->connected()) { this->disconnect(false); } lastReceivedPacket = chrono::system_clock::now(); GLOBED_REQUIRE(!GlobedAccountManager::get().authToken.lock()->empty(), "attempting to connect with no authtoken set in account manager") - + GLOBED_REQUIRE(gameSocket.connect(addr, port), "failed to connect to the server") gameSocket.createBox(); @@ -101,8 +101,18 @@ void NetworkManager::connect(const std::string& addr, unsigned short port) { this->send(packet); } +void NetworkManager::connectWithView(const GameServerView& gsview) { + try { + this->connect(gsview.address.ip, gsview.address.port); + GlobedServerManager::get().setActiveGameServer(gsview.id); + } catch (const std::exception& e) { + this->disconnect(true); + ErrorQueues::get().error(std::string("Connection failed: ") + e.what()); + } +} + void NetworkManager::disconnect(bool quiet) { - if (!connected()) { + if (!this->connected()) { return; } @@ -121,7 +131,7 @@ void NetworkManager::disconnect(bool quiet) { } void NetworkManager::send(std::shared_ptr packet) { - GLOBED_REQUIRE(connected(), "tried to send a packet while disconnected") + GLOBED_REQUIRE(this->connected(), "tried to send a packet while disconnected") packetQueue.push(packet); } @@ -261,13 +271,13 @@ void NetworkManager::maybeSendKeepalive() { // Disconnects from the server if there has been no response for a while void NetworkManager::maybeDisconnectIfDead() { auto now = chrono::system_clock::now(); - if (connected() && (now - lastReceivedPacket) > DISCONNECT_AFTER) { + if (this->connected() && (now - lastReceivedPacket) > DISCONNECT_AFTER) { ErrorQueues::get().error("The server you were connected to is not responding to any requests. You have been disconnected."); this->disconnect(); } } -PollBothResult NetworkManager::pollBothSockets(long msDelay) { +PollBothResult NetworkManager::pollBothSockets(int msDelay) { PollBothResult out; GLOBED_SOCKET_POLLFD fds[2]; @@ -278,7 +288,7 @@ PollBothResult NetworkManager::pollBothSockets(long msDelay) { fds[1].fd = pingSocket.socket_; fds[1].events = POLLIN; - int result = GLOBED_SOCKET_POLL(fds, 2, (int)msDelay); + int result = GLOBED_SOCKET_POLL(fds, 2, msDelay); if (result == -1) { util::net::throwLastError(); @@ -304,9 +314,9 @@ bool NetworkManager::connected() { } bool NetworkManager::established() { - return connected() && _established; + return this->connected() && _established; } bool NetworkManager::authenticated() { - return established() && _loggedin; + return this->established() && _loggedin; } \ No newline at end of file diff --git a/src/net/network_manager.hpp b/src/net/network_manager.hpp index 15bac995..40a43ae7 100644 --- a/src/net/network_manager.hpp +++ b/src/net/network_manager.hpp @@ -1,6 +1,8 @@ #pragma once #include "game_socket.hpp" #include + +#include #include #include @@ -22,7 +24,7 @@ struct PollBothResult { bool hasPing; }; -// This class is fully thread safe..? +// This class is fully thread safe..? hell do i know.. class NetworkManager { public: using PacketCallback = std::function)>; @@ -38,6 +40,8 @@ class NetworkManager { // Connect to a server void connect(const std::string& addr, unsigned short port); + // Safer version of `connect`, sets the active game server in `GlobedServerManager` doesn't throw an exception on error + void connectWithView(const GameServerView& gsview); // Disconnect from a server. Does nothing if not connected void disconnect(bool quiet = false); @@ -115,7 +119,7 @@ class NetworkManager { void maybeSendKeepalive(); void maybeDisconnectIfDead(); - PollBothResult pollBothSockets(long msDelay); + PollBothResult pollBothSockets(int msDelay); // Builtin listeners have priority above the others. WrappingMutex> builtinListeners; diff --git a/src/net/socket.hpp b/src/net/socket.hpp index fc2c1301..be5b89f4 100644 --- a/src/net/socket.hpp +++ b/src/net/socket.hpp @@ -13,5 +13,5 @@ class Socket { void receiveExact(char* buffer, int bufferSize); virtual bool close(); virtual ~Socket(); - virtual bool poll(long msDelay) = 0; + virtual bool poll(int msDelay) = 0; }; \ No newline at end of file diff --git a/src/net/udp_socket.cpp b/src/net/udp_socket.cpp index d63144e1..9b72e87f 100644 --- a/src/net/udp_socket.cpp +++ b/src/net/udp_socket.cpp @@ -51,13 +51,13 @@ bool UdpSocket::close() { #endif } -bool UdpSocket::poll(long msDelay) { +bool UdpSocket::poll(int msDelay) { GLOBED_SOCKET_POLLFD fds[1]; fds[0].fd = socket_; fds[0].events = POLLIN; - int result = GLOBED_SOCKET_POLL(fds, 1, (int)msDelay); + int result = GLOBED_SOCKET_POLL(fds, 1, msDelay); if (result == -1) { util::net::throwLastError(); diff --git a/src/net/udp_socket.hpp b/src/net/udp_socket.hpp index f162534f..9b4e3585 100644 --- a/src/net/udp_socket.hpp +++ b/src/net/udp_socket.hpp @@ -15,7 +15,7 @@ class UdpSocket : public Socket { int receive(char* buffer, int bufferSize) override; bool close() override; virtual void disconnect(); - bool poll(long msDelay) override; + bool poll(int msDelay) override; util::sync::AtomicBool connected = false; protected: diff --git a/src/ui/hooks/play_layer.hpp b/src/ui/hooks/play_layer.hpp index 721ed8a3..fc059176 100644 --- a/src/ui/hooks/play_layer.hpp +++ b/src/ui/hooks/play_layer.hpp @@ -2,10 +2,17 @@ #include #include +#include + +#if GLOBED_HAS_KEYBINDS +#include +#endif // GLOBED_CUSTOM_KEYBINDS + #include