From d88e5410a0e045780929f71dac4cab93ca07f515 Mon Sep 17 00:00:00 2001 From: Quentin Rouland Date: Sat, 25 May 2024 15:08:14 -0400 Subject: [PATCH] Initial parser for stormfate replay --- .gitignore | 3 + Cargo.toml | 13 + README.md | 14 +- gen_proto.sh | 10 + out | 0 src/lib.rs | 4 + src/main.rs | 27 ++ src/parser.rs | 101 ++++++ src/protos/mod.rs | 1 + src/protos/stormgate.proto | 104 +++++++ src/protos/stormgate.rs | 619 +++++++++++++++++++++++++++++++++++++ 11 files changed, 895 insertions(+), 1 deletion(-) create mode 100644 Cargo.toml create mode 100755 gen_proto.sh create mode 100644 out create mode 100644 src/lib.rs create mode 100644 src/main.rs create mode 100644 src/parser.rs create mode 100644 src/protos/mod.rs create mode 100644 src/protos/stormgate.proto create mode 100644 src/protos/stormgate.rs diff --git a/.gitignore b/.gitignore index 3ca43ae..88a08a8 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,6 @@ Cargo.lock # MSVC Windows builds of rustc generate these, which store debugging information *.pdb +# Project specific +replays + diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..f5cd12d --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "sgtool" +version = "0.0.1-dev" +edition = "2021" +authors = [ "Quentin Rouland " ] + +[dependencies] +clap = { version = "4.5", features = ["derive"] } +byteorder = "1.5" +flate2 = "1.0" +quick-protobuf = { version = "0.8", features = ["std"] } +log = "0.4.21" +pretty_env_logger = "0.5.0" diff --git a/README.md b/README.md index c62789a..d614094 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,14 @@ -# SGTools +[![License: AGPL v3](https://img.shields.io/badge/License-AGPL_v3-blue.svg)](https://www.gnu.org/licenses/agpl-3.0) +# SGTool + +SGTool is a library and tool for analyze of the replay of the Stormgate game. + + +# Acknowledgement + +[shroudstone](https://github.com/acarapetis/shroudstone) : A project with similar goal wrote in python. We thank them for part of the reverse engineering notabally the protobuf schema also used this project. + +# Disclamer + +SGTool is community build which is not affiliate in any way to the official Stormgate Game in anyway. \ No newline at end of file diff --git a/gen_proto.sh b/gen_proto.sh new file mode 100755 index 0000000..89c54a8 --- /dev/null +++ b/gen_proto.sh @@ -0,0 +1,10 @@ +#!/bin/sh + +if ! command -v pb-rs &> /dev/null +then + echo "Failed to find pb-rs command" + echo "To install it run : cargo install pb-rs" + exit 1 +fi + +pb-rs src/protos/stormgate.proto \ No newline at end of file diff --git a/out b/out new file mode 100644 index 0000000..e69de29 diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..0af67b8 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,4 @@ +#[macro_use] extern crate log; + +pub mod parser; +mod protos; \ No newline at end of file diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..a81edf3 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,27 @@ +extern crate pretty_env_logger; +#[macro_use] extern crate log; + +use std::path::PathBuf; +use clap::Parser; +use crate::parser::Replay; + + +mod protos; +mod parser; + +/// Simple replay Stormgate replay parser +#[derive(Parser, Debug)] +#[command(version, about, long_about = None)] +struct Args { + /// Path of replay to parse + #[arg(short, long)] + input: PathBuf, +} + +fn main() { + pretty_env_logger::init(); + let args = Args::parse(); + info!("Input : {}", args.input.to_string_lossy()); + let mut bytes = Vec::new(); + debug!("{:#?}", Replay::load_file(&args.input, &mut bytes).unwrap()); +} \ No newline at end of file diff --git a/src/parser.rs b/src/parser.rs new file mode 100644 index 0000000..5dd1e0a --- /dev/null +++ b/src/parser.rs @@ -0,0 +1,101 @@ +use std::{fs::OpenOptions, io::{BufRead, BufReader, Read}, path::Path}; +use byteorder::{ByteOrder, LittleEndian}; +use flate2::bufread::GzDecoder; +use quick_protobuf::BytesReader; + +use crate::protos::stormgate::ReplayChunk; + +/// Stormgate Replay Header. +/// It consists of 16 bytes at the top of the replay +#[derive(Debug, Clone)] +pub struct Header { + h1: u32, // 12 first bytes repsentation are unknown at the moment + build_number: u32, // 4 next is he build number +} + +/// Stormgate Replay Payload. +/// After the the 16 bytes header, the actual payload of the replay of Protobuf messages that are gzipped +/// Each Protobuf messages represents events that appended in the game from the differetns clients +#[derive(Debug, Clone)] +pub struct Payload<'a> { + chunks: Vec>, +} + +/// Stormgate replay +#[derive(Debug, Clone)] +pub struct Replay<'a> { + header: Header, // header of the replay + payload: Payload<'a>, // the content of a replay is set of messages +} + +fn load_part<'a, R: Read>(reader: &'a mut R) -> impl FnMut(usize) -> Result, std::io::Error> + 'a { + move |size| { + let mut buf = vec![0u8; size]; + reader.read_exact(&mut buf)?; + Ok(buf) + } +} + +impl Header { + /// + fn load(reader: &mut T) -> Result { + let mut load = load_part(reader); + let h1 = LittleEndian::read_u32(&load(12)?); + let build_number = LittleEndian::read_u32(&load(4)?); + Ok(Header { h1, build_number }) + } +} + +impl<'a> Payload<'a> { + fn load(buf_reader: &mut T, buf: &'a mut Vec) -> Result, Box> { + let mut d = GzDecoder::new(buf_reader); + d.read_to_end(buf)?; + let mut reader = BytesReader::from_bytes(&buf); + + let mut data = Vec::new(); + + while !reader.is_eof() { + let read_message = reader.read_message::(buf)?; + data.push(read_message); + } + + Ok(Payload { chunks: data }) + } +} + + +impl<'a> Replay<'a> { + fn load(buf_reader: &mut T, buf: &'a mut Vec) -> Result, Box> { + // Get Header + let header = Header::load(buf_reader)?; + + // Get Payload + let data = Payload::load(buf_reader, buf)?; + + Ok(Replay { header, payload: data }) + } + + pub fn load_file(path: &Path, buf: &'a mut Vec) -> Result, Box> { + let file = OpenOptions::new().read(true).open(path).unwrap(); + let mut buf_reader = BufReader::new(file); + Replay::load(&mut buf_reader, buf) + } +} + + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn load_file() { + // Replay used is form Alpha phase of stormgate, threfore is not provided in the repo to avoid any problem with NDA at this time + // TODO : when the game officially can out fix provide sample replay for testing directly in the repo + let replay_path = Path::new("replays/CL55366-2024.05.12-01.53.SGReplay"); + let mut buffer : Vec = vec![]; + let r = Replay::load_file(replay_path, &mut buffer).unwrap(); + assert_eq!(r.header.build_number, 55366); + assert_eq!(r.payload.chunks.len(), 1373); + } +} + diff --git a/src/protos/mod.rs b/src/protos/mod.rs new file mode 100644 index 0000000..1805157 --- /dev/null +++ b/src/protos/mod.rs @@ -0,0 +1 @@ +pub mod stormgate; \ No newline at end of file diff --git a/src/protos/stormgate.proto b/src/protos/stormgate.proto new file mode 100644 index 0000000..131f2b7 --- /dev/null +++ b/src/protos/stormgate.proto @@ -0,0 +1,104 @@ +syntax = "proto3"; + +package stormgate; + +// protobuf schema for parsing lobby info from Stormgate replays +// Thanks to CascadeFury for most of this. + +// Each chunk seems to be of the form 3: {1: {actual content}} +// Since I don't know the meaning of those outer structs yet, I'm just putting them as inline messages: +message ReplayChunk { + int32 timestamp = 1; // Time since game start in units of 1/1024 seconds + int32 client_id = 2; + message Wrapper { + message ReplayContent { + oneof content_type { + Map map = 3; + Player player = 12; + LobbyChangeSlot change_slot = 13; + LobbySetVariable set_variable = 15; + StartGame start_game = 18; + PlayerLeftGame player_left_game = 25; + AssignPlayerSlot assign_player_slot = 37; + } + } + ReplayContent content = 1; + } + Wrapper inner = 3; +} + +// 3 - Map +message Map { + string folder = 1; + string name = 2; + int32 seed = 3; +} + +message Player { + UUID uuid = 2; + message PlayerName { + string nickname = 1; + string discriminator = 2; + } + PlayerName name = 3; +} + +// 13 - Sent when player changes slot (leave/enter), note that SlotChoice +// contains either a request for a specific slot, or what I assume is a "next +// slot available", slot 255 is observer +message LobbyChangeSlot { + message SlotChoice { + message SpecificSlot { + int32 slot = 1; + } + oneof choice_type { + SpecificSlot specific_slot = 2; + } + } + + SlotChoice choice = 1; +} + +// 15 - Sent when a player slot has a variable changed +// Var 374945738: Type, 0 = Closed, 1 = Human, 2 = AI +// Var 2952722564: Faction, 0 = Vanguard, 1 = Infernals +// Var 655515685: AIType, 0 = Peaceful, 1 = Junior, 2 = Senior +message LobbySetVariable { + int32 slot = 3; + uint32 variable_id = 4; + uint32 value = 5; +} + +// 18 - "Start Game" Sent by players after second ReadyUp, that probably indicates player finished loading into the map +message StartGame { +} + + +enum LeaveReason { + Unknown = 0; + Surrender = 1; // Player surrenders + Leave = 2; // Player leaves game normally (game ended earlier, if this is the first PlayerLeftGame message, the outcome should be considered unknown) +} + +// 25 - When a player exits the game/disconnects +message PlayerLeftGame { + UUID player_uuid = 1; + LeaveReason reason = 2; +} + +// 37 - AssignPlayer (done by server as pl=64) +// Appears only in ladder games? +message AssignPlayerSlot { + UUID uuid = 1; + int64 slot = 2; + string nickname = 3; +} + +// UUIDs are encoded as 2 varints. +// To recover the original UUID, encode these as signed 64-bit big-endian +// integers and concatenate the resulting bitstrings; or in python: +// uuid.UUID(bytes=struct.pack(">qq", part1, part2)) +message UUID { + int64 part1 = 1; + int64 part2 = 2; +} diff --git a/src/protos/stormgate.rs b/src/protos/stormgate.rs new file mode 100644 index 0000000..3ffe0ce --- /dev/null +++ b/src/protos/stormgate.rs @@ -0,0 +1,619 @@ +// Automatically generated rust module for 'stormgate.proto' file + +#![allow(non_snake_case)] +#![allow(non_upper_case_globals)] +#![allow(non_camel_case_types)] +#![allow(unused_imports)] +#![allow(unknown_lints)] +#![allow(clippy::all)] +#![cfg_attr(rustfmt, rustfmt_skip)] + + +use std::borrow::Cow; +use quick_protobuf::{MessageInfo, MessageRead, MessageWrite, BytesReader, Writer, WriterBackend, Result}; +use quick_protobuf::sizeofs::*; +use super::*; + +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub enum LeaveReason { + Unknown = 0, + Surrender = 1, + Leave = 2, +} + +impl Default for LeaveReason { + fn default() -> Self { + LeaveReason::Unknown + } +} + +impl From for LeaveReason { + fn from(i: i32) -> Self { + match i { + 0 => LeaveReason::Unknown, + 1 => LeaveReason::Surrender, + 2 => LeaveReason::Leave, + _ => Self::default(), + } + } +} + +impl<'a> From<&'a str> for LeaveReason { + fn from(s: &'a str) -> Self { + match s { + "Unknown" => LeaveReason::Unknown, + "Surrender" => LeaveReason::Surrender, + "Leave" => LeaveReason::Leave, + _ => Self::default(), + } + } +} + +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Debug, Default, PartialEq, Clone)] +pub struct ReplayChunk<'a> { + pub timestamp: i32, + pub client_id: i32, + pub inner: Option>, +} + +impl<'a> MessageRead<'a> for ReplayChunk<'a> { + fn from_reader(r: &mut BytesReader, bytes: &'a [u8]) -> Result { + let mut msg = Self::default(); + while !r.is_eof() { + match r.next_tag(bytes) { + Ok(8) => msg.timestamp = r.read_int32(bytes)?, + Ok(16) => msg.client_id = r.read_int32(bytes)?, + Ok(26) => msg.inner = Some(r.read_message::(bytes)?), + Ok(t) => { r.read_unknown(bytes, t)?; } + Err(e) => return Err(e), + } + } + Ok(msg) + } +} + +impl<'a> MessageWrite for ReplayChunk<'a> { + fn get_size(&self) -> usize { + 0 + + if self.timestamp == 0i32 { 0 } else { 1 + sizeof_varint(*(&self.timestamp) as u64) } + + if self.client_id == 0i32 { 0 } else { 1 + sizeof_varint(*(&self.client_id) as u64) } + + self.inner.as_ref().map_or(0, |m| 1 + sizeof_len((m).get_size())) + } + + fn write_message(&self, w: &mut Writer) -> Result<()> { + if self.timestamp != 0i32 { w.write_with_tag(8, |w| w.write_int32(*&self.timestamp))?; } + if self.client_id != 0i32 { w.write_with_tag(16, |w| w.write_int32(*&self.client_id))?; } + if let Some(ref s) = self.inner { w.write_with_tag(26, |w| w.write_message(s))?; } + Ok(()) + } +} + +pub mod mod_ReplayChunk { + +use super::*; + +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Debug, Default, PartialEq, Clone)] +pub struct Wrapper<'a> { + pub content: Option>, +} + +impl<'a> MessageRead<'a> for Wrapper<'a> { + fn from_reader(r: &mut BytesReader, bytes: &'a [u8]) -> Result { + let mut msg = Self::default(); + while !r.is_eof() { + match r.next_tag(bytes) { + Ok(10) => msg.content = Some(r.read_message::(bytes)?), + Ok(t) => { r.read_unknown(bytes, t)?; } + Err(e) => return Err(e), + } + } + Ok(msg) + } +} + +impl<'a> MessageWrite for Wrapper<'a> { + fn get_size(&self) -> usize { + 0 + + self.content.as_ref().map_or(0, |m| 1 + sizeof_len((m).get_size())) + } + + fn write_message(&self, w: &mut Writer) -> Result<()> { + if let Some(ref s) = self.content { w.write_with_tag(10, |w| w.write_message(s))?; } + Ok(()) + } +} + +pub mod mod_Wrapper { + +use super::*; + +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Debug, Default, PartialEq, Clone)] +pub struct ReplayContent<'a> { + pub content_type: stormgate::mod_ReplayChunk::mod_Wrapper::mod_ReplayContent::OneOfcontent_type<'a>, +} + +impl<'a> MessageRead<'a> for ReplayContent<'a> { + fn from_reader(r: &mut BytesReader, bytes: &'a [u8]) -> Result { + let mut msg = Self::default(); + while !r.is_eof() { + match r.next_tag(bytes) { + Ok(26) => msg.content_type = stormgate::mod_ReplayChunk::mod_Wrapper::mod_ReplayContent::OneOfcontent_type::map(r.read_message::(bytes)?), + Ok(98) => msg.content_type = stormgate::mod_ReplayChunk::mod_Wrapper::mod_ReplayContent::OneOfcontent_type::player(r.read_message::(bytes)?), + Ok(106) => msg.content_type = stormgate::mod_ReplayChunk::mod_Wrapper::mod_ReplayContent::OneOfcontent_type::change_slot(r.read_message::(bytes)?), + Ok(122) => msg.content_type = stormgate::mod_ReplayChunk::mod_Wrapper::mod_ReplayContent::OneOfcontent_type::set_variable(r.read_message::(bytes)?), + Ok(146) => msg.content_type = stormgate::mod_ReplayChunk::mod_Wrapper::mod_ReplayContent::OneOfcontent_type::start_game(r.read_message::(bytes)?), + Ok(202) => msg.content_type = stormgate::mod_ReplayChunk::mod_Wrapper::mod_ReplayContent::OneOfcontent_type::player_left_game(r.read_message::(bytes)?), + Ok(298) => msg.content_type = stormgate::mod_ReplayChunk::mod_Wrapper::mod_ReplayContent::OneOfcontent_type::assign_player_slot(r.read_message::(bytes)?), + Ok(t) => { r.read_unknown(bytes, t)?; } + Err(e) => return Err(e), + } + } + Ok(msg) + } +} + +impl<'a> MessageWrite for ReplayContent<'a> { + fn get_size(&self) -> usize { + 0 + + match self.content_type { + stormgate::mod_ReplayChunk::mod_Wrapper::mod_ReplayContent::OneOfcontent_type::map(ref m) => 1 + sizeof_len((m).get_size()), + stormgate::mod_ReplayChunk::mod_Wrapper::mod_ReplayContent::OneOfcontent_type::player(ref m) => 1 + sizeof_len((m).get_size()), + stormgate::mod_ReplayChunk::mod_Wrapper::mod_ReplayContent::OneOfcontent_type::change_slot(ref m) => 1 + sizeof_len((m).get_size()), + stormgate::mod_ReplayChunk::mod_Wrapper::mod_ReplayContent::OneOfcontent_type::set_variable(ref m) => 1 + sizeof_len((m).get_size()), + stormgate::mod_ReplayChunk::mod_Wrapper::mod_ReplayContent::OneOfcontent_type::start_game(ref m) => 2 + sizeof_len((m).get_size()), + stormgate::mod_ReplayChunk::mod_Wrapper::mod_ReplayContent::OneOfcontent_type::player_left_game(ref m) => 2 + sizeof_len((m).get_size()), + stormgate::mod_ReplayChunk::mod_Wrapper::mod_ReplayContent::OneOfcontent_type::assign_player_slot(ref m) => 2 + sizeof_len((m).get_size()), + stormgate::mod_ReplayChunk::mod_Wrapper::mod_ReplayContent::OneOfcontent_type::None => 0, + } } + + fn write_message(&self, w: &mut Writer) -> Result<()> { + match self.content_type { stormgate::mod_ReplayChunk::mod_Wrapper::mod_ReplayContent::OneOfcontent_type::map(ref m) => { w.write_with_tag(26, |w| w.write_message(m))? }, + stormgate::mod_ReplayChunk::mod_Wrapper::mod_ReplayContent::OneOfcontent_type::player(ref m) => { w.write_with_tag(98, |w| w.write_message(m))? }, + stormgate::mod_ReplayChunk::mod_Wrapper::mod_ReplayContent::OneOfcontent_type::change_slot(ref m) => { w.write_with_tag(106, |w| w.write_message(m))? }, + stormgate::mod_ReplayChunk::mod_Wrapper::mod_ReplayContent::OneOfcontent_type::set_variable(ref m) => { w.write_with_tag(122, |w| w.write_message(m))? }, + stormgate::mod_ReplayChunk::mod_Wrapper::mod_ReplayContent::OneOfcontent_type::start_game(ref m) => { w.write_with_tag(146, |w| w.write_message(m))? }, + stormgate::mod_ReplayChunk::mod_Wrapper::mod_ReplayContent::OneOfcontent_type::player_left_game(ref m) => { w.write_with_tag(202, |w| w.write_message(m))? }, + stormgate::mod_ReplayChunk::mod_Wrapper::mod_ReplayContent::OneOfcontent_type::assign_player_slot(ref m) => { w.write_with_tag(298, |w| w.write_message(m))? }, + stormgate::mod_ReplayChunk::mod_Wrapper::mod_ReplayContent::OneOfcontent_type::None => {}, + } Ok(()) + } +} + +pub mod mod_ReplayContent { + +use super::*; + +#[derive(Debug, PartialEq, Clone)] +pub enum OneOfcontent_type<'a> { + map(stormgate::Map<'a>), + player(stormgate::Player<'a>), + change_slot(stormgate::LobbyChangeSlot), + set_variable(stormgate::LobbySetVariable), + start_game(stormgate::StartGame), + player_left_game(stormgate::PlayerLeftGame), + assign_player_slot(stormgate::AssignPlayerSlot<'a>), + None, +} + +impl<'a> Default for OneOfcontent_type<'a> { + fn default() -> Self { + OneOfcontent_type::None + } +} + +} + +} + +} + +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Debug, Default, PartialEq, Clone)] +pub struct Map<'a> { + pub folder: Cow<'a, str>, + pub name: Cow<'a, str>, + pub seed: i32, +} + +impl<'a> MessageRead<'a> for Map<'a> { + fn from_reader(r: &mut BytesReader, bytes: &'a [u8]) -> Result { + let mut msg = Self::default(); + while !r.is_eof() { + match r.next_tag(bytes) { + Ok(10) => msg.folder = r.read_string(bytes).map(Cow::Borrowed)?, + Ok(18) => msg.name = r.read_string(bytes).map(Cow::Borrowed)?, + Ok(24) => msg.seed = r.read_int32(bytes)?, + Ok(t) => { r.read_unknown(bytes, t)?; } + Err(e) => return Err(e), + } + } + Ok(msg) + } +} + +impl<'a> MessageWrite for Map<'a> { + fn get_size(&self) -> usize { + 0 + + if self.folder == "" { 0 } else { 1 + sizeof_len((&self.folder).len()) } + + if self.name == "" { 0 } else { 1 + sizeof_len((&self.name).len()) } + + if self.seed == 0i32 { 0 } else { 1 + sizeof_varint(*(&self.seed) as u64) } + } + + fn write_message(&self, w: &mut Writer) -> Result<()> { + if self.folder != "" { w.write_with_tag(10, |w| w.write_string(&**&self.folder))?; } + if self.name != "" { w.write_with_tag(18, |w| w.write_string(&**&self.name))?; } + if self.seed != 0i32 { w.write_with_tag(24, |w| w.write_int32(*&self.seed))?; } + Ok(()) + } +} + +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Debug, Default, PartialEq, Clone)] +pub struct Player<'a> { + pub uuid: Option, + pub name: Option>, +} + +impl<'a> MessageRead<'a> for Player<'a> { + fn from_reader(r: &mut BytesReader, bytes: &'a [u8]) -> Result { + let mut msg = Self::default(); + while !r.is_eof() { + match r.next_tag(bytes) { + Ok(18) => msg.uuid = Some(r.read_message::(bytes)?), + Ok(26) => msg.name = Some(r.read_message::(bytes)?), + Ok(t) => { r.read_unknown(bytes, t)?; } + Err(e) => return Err(e), + } + } + Ok(msg) + } +} + +impl<'a> MessageWrite for Player<'a> { + fn get_size(&self) -> usize { + 0 + + self.uuid.as_ref().map_or(0, |m| 1 + sizeof_len((m).get_size())) + + self.name.as_ref().map_or(0, |m| 1 + sizeof_len((m).get_size())) + } + + fn write_message(&self, w: &mut Writer) -> Result<()> { + if let Some(ref s) = self.uuid { w.write_with_tag(18, |w| w.write_message(s))?; } + if let Some(ref s) = self.name { w.write_with_tag(26, |w| w.write_message(s))?; } + Ok(()) + } +} + +pub mod mod_Player { + +use std::borrow::Cow; +use super::*; + +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Debug, Default, PartialEq, Clone)] +pub struct PlayerName<'a> { + pub nickname: Cow<'a, str>, + pub discriminator: Cow<'a, str>, +} + +impl<'a> MessageRead<'a> for PlayerName<'a> { + fn from_reader(r: &mut BytesReader, bytes: &'a [u8]) -> Result { + let mut msg = Self::default(); + while !r.is_eof() { + match r.next_tag(bytes) { + Ok(10) => msg.nickname = r.read_string(bytes).map(Cow::Borrowed)?, + Ok(18) => msg.discriminator = r.read_string(bytes).map(Cow::Borrowed)?, + Ok(t) => { r.read_unknown(bytes, t)?; } + Err(e) => return Err(e), + } + } + Ok(msg) + } +} + +impl<'a> MessageWrite for PlayerName<'a> { + fn get_size(&self) -> usize { + 0 + + if self.nickname == "" { 0 } else { 1 + sizeof_len((&self.nickname).len()) } + + if self.discriminator == "" { 0 } else { 1 + sizeof_len((&self.discriminator).len()) } + } + + fn write_message(&self, w: &mut Writer) -> Result<()> { + if self.nickname != "" { w.write_with_tag(10, |w| w.write_string(&**&self.nickname))?; } + if self.discriminator != "" { w.write_with_tag(18, |w| w.write_string(&**&self.discriminator))?; } + Ok(()) + } +} + +} + +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Debug, Default, PartialEq, Clone)] +pub struct LobbyChangeSlot { + pub choice: Option, +} + +impl<'a> MessageRead<'a> for LobbyChangeSlot { + fn from_reader(r: &mut BytesReader, bytes: &'a [u8]) -> Result { + let mut msg = Self::default(); + while !r.is_eof() { + match r.next_tag(bytes) { + Ok(10) => msg.choice = Some(r.read_message::(bytes)?), + Ok(t) => { r.read_unknown(bytes, t)?; } + Err(e) => return Err(e), + } + } + Ok(msg) + } +} + +impl MessageWrite for LobbyChangeSlot { + fn get_size(&self) -> usize { + 0 + + self.choice.as_ref().map_or(0, |m| 1 + sizeof_len((m).get_size())) + } + + fn write_message(&self, w: &mut Writer) -> Result<()> { + if let Some(ref s) = self.choice { w.write_with_tag(10, |w| w.write_message(s))?; } + Ok(()) + } +} + +pub mod mod_LobbyChangeSlot { + +use super::*; + +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Debug, Default, PartialEq, Clone)] +pub struct SlotChoice { + pub choice_type: stormgate::mod_LobbyChangeSlot::mod_SlotChoice::OneOfchoice_type, +} + +impl<'a> MessageRead<'a> for SlotChoice { + fn from_reader(r: &mut BytesReader, bytes: &'a [u8]) -> Result { + let mut msg = Self::default(); + while !r.is_eof() { + match r.next_tag(bytes) { + Ok(18) => msg.choice_type = stormgate::mod_LobbyChangeSlot::mod_SlotChoice::OneOfchoice_type::specific_slot(r.read_message::(bytes)?), + Ok(t) => { r.read_unknown(bytes, t)?; } + Err(e) => return Err(e), + } + } + Ok(msg) + } +} + +impl MessageWrite for SlotChoice { + fn get_size(&self) -> usize { + 0 + + match self.choice_type { + stormgate::mod_LobbyChangeSlot::mod_SlotChoice::OneOfchoice_type::specific_slot(ref m) => 1 + sizeof_len((m).get_size()), + stormgate::mod_LobbyChangeSlot::mod_SlotChoice::OneOfchoice_type::None => 0, + } } + + fn write_message(&self, w: &mut Writer) -> Result<()> { + match self.choice_type { stormgate::mod_LobbyChangeSlot::mod_SlotChoice::OneOfchoice_type::specific_slot(ref m) => { w.write_with_tag(18, |w| w.write_message(m))? }, + stormgate::mod_LobbyChangeSlot::mod_SlotChoice::OneOfchoice_type::None => {}, + } Ok(()) + } +} + +pub mod mod_SlotChoice { + +use super::*; + +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Debug, Default, PartialEq, Clone)] +pub struct SpecificSlot { + pub slot: i32, +} + +impl<'a> MessageRead<'a> for SpecificSlot { + fn from_reader(r: &mut BytesReader, bytes: &'a [u8]) -> Result { + let mut msg = Self::default(); + while !r.is_eof() { + match r.next_tag(bytes) { + Ok(8) => msg.slot = r.read_int32(bytes)?, + Ok(t) => { r.read_unknown(bytes, t)?; } + Err(e) => return Err(e), + } + } + Ok(msg) + } +} + +impl MessageWrite for SpecificSlot { + fn get_size(&self) -> usize { + 0 + + if self.slot == 0i32 { 0 } else { 1 + sizeof_varint(*(&self.slot) as u64) } + } + + fn write_message(&self, w: &mut Writer) -> Result<()> { + if self.slot != 0i32 { w.write_with_tag(8, |w| w.write_int32(*&self.slot))?; } + Ok(()) + } +} + +#[derive(Debug, PartialEq, Clone)] +pub enum OneOfchoice_type { + specific_slot(stormgate::mod_LobbyChangeSlot::mod_SlotChoice::SpecificSlot), + None, +} + +impl Default for OneOfchoice_type { + fn default() -> Self { + OneOfchoice_type::None + } +} + +} + +} + +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Debug, Default, PartialEq, Clone)] +pub struct LobbySetVariable { + pub slot: i32, + pub variable_id: u32, + pub value: u32, +} + +impl<'a> MessageRead<'a> for LobbySetVariable { + fn from_reader(r: &mut BytesReader, bytes: &'a [u8]) -> Result { + let mut msg = Self::default(); + while !r.is_eof() { + match r.next_tag(bytes) { + Ok(24) => msg.slot = r.read_int32(bytes)?, + Ok(32) => msg.variable_id = r.read_uint32(bytes)?, + Ok(40) => msg.value = r.read_uint32(bytes)?, + Ok(t) => { r.read_unknown(bytes, t)?; } + Err(e) => return Err(e), + } + } + Ok(msg) + } +} + +impl MessageWrite for LobbySetVariable { + fn get_size(&self) -> usize { + 0 + + if self.slot == 0i32 { 0 } else { 1 + sizeof_varint(*(&self.slot) as u64) } + + if self.variable_id == 0u32 { 0 } else { 1 + sizeof_varint(*(&self.variable_id) as u64) } + + if self.value == 0u32 { 0 } else { 1 + sizeof_varint(*(&self.value) as u64) } + } + + fn write_message(&self, w: &mut Writer) -> Result<()> { + if self.slot != 0i32 { w.write_with_tag(24, |w| w.write_int32(*&self.slot))?; } + if self.variable_id != 0u32 { w.write_with_tag(32, |w| w.write_uint32(*&self.variable_id))?; } + if self.value != 0u32 { w.write_with_tag(40, |w| w.write_uint32(*&self.value))?; } + Ok(()) + } +} + +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Debug, Default, PartialEq, Clone)] +pub struct StartGame { } + +impl<'a> MessageRead<'a> for StartGame { + fn from_reader(r: &mut BytesReader, _: &[u8]) -> Result { + r.read_to_end(); + Ok(Self::default()) + } +} + +impl MessageWrite for StartGame { } + +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Debug, Default, PartialEq, Clone)] +pub struct PlayerLeftGame { + pub player_uuid: Option, + pub reason: stormgate::LeaveReason, +} + +impl<'a> MessageRead<'a> for PlayerLeftGame { + fn from_reader(r: &mut BytesReader, bytes: &'a [u8]) -> Result { + let mut msg = Self::default(); + while !r.is_eof() { + match r.next_tag(bytes) { + Ok(10) => msg.player_uuid = Some(r.read_message::(bytes)?), + Ok(16) => msg.reason = r.read_enum(bytes)?, + Ok(t) => { r.read_unknown(bytes, t)?; } + Err(e) => return Err(e), + } + } + Ok(msg) + } +} + +impl MessageWrite for PlayerLeftGame { + fn get_size(&self) -> usize { + 0 + + self.player_uuid.as_ref().map_or(0, |m| 1 + sizeof_len((m).get_size())) + + if self.reason == stormgate::LeaveReason::Unknown { 0 } else { 1 + sizeof_varint(*(&self.reason) as u64) } + } + + fn write_message(&self, w: &mut Writer) -> Result<()> { + if let Some(ref s) = self.player_uuid { w.write_with_tag(10, |w| w.write_message(s))?; } + if self.reason != stormgate::LeaveReason::Unknown { w.write_with_tag(16, |w| w.write_enum(*&self.reason as i32))?; } + Ok(()) + } +} + +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Debug, Default, PartialEq, Clone)] +pub struct AssignPlayerSlot<'a> { + pub uuid: Option, + pub slot: i64, + pub nickname: Cow<'a, str>, +} + +impl<'a> MessageRead<'a> for AssignPlayerSlot<'a> { + fn from_reader(r: &mut BytesReader, bytes: &'a [u8]) -> Result { + let mut msg = Self::default(); + while !r.is_eof() { + match r.next_tag(bytes) { + Ok(10) => msg.uuid = Some(r.read_message::(bytes)?), + Ok(16) => msg.slot = r.read_int64(bytes)?, + Ok(26) => msg.nickname = r.read_string(bytes).map(Cow::Borrowed)?, + Ok(t) => { r.read_unknown(bytes, t)?; } + Err(e) => return Err(e), + } + } + Ok(msg) + } +} + +impl<'a> MessageWrite for AssignPlayerSlot<'a> { + fn get_size(&self) -> usize { + 0 + + self.uuid.as_ref().map_or(0, |m| 1 + sizeof_len((m).get_size())) + + if self.slot == 0i64 { 0 } else { 1 + sizeof_varint(*(&self.slot) as u64) } + + if self.nickname == "" { 0 } else { 1 + sizeof_len((&self.nickname).len()) } + } + + fn write_message(&self, w: &mut Writer) -> Result<()> { + if let Some(ref s) = self.uuid { w.write_with_tag(10, |w| w.write_message(s))?; } + if self.slot != 0i64 { w.write_with_tag(16, |w| w.write_int64(*&self.slot))?; } + if self.nickname != "" { w.write_with_tag(26, |w| w.write_string(&**&self.nickname))?; } + Ok(()) + } +} + +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Debug, Default, PartialEq, Clone)] +pub struct UUID { + pub part1: i64, + pub part2: i64, +} + +impl<'a> MessageRead<'a> for UUID { + fn from_reader(r: &mut BytesReader, bytes: &'a [u8]) -> Result { + let mut msg = Self::default(); + while !r.is_eof() { + match r.next_tag(bytes) { + Ok(8) => msg.part1 = r.read_int64(bytes)?, + Ok(16) => msg.part2 = r.read_int64(bytes)?, + Ok(t) => { r.read_unknown(bytes, t)?; } + Err(e) => return Err(e), + } + } + Ok(msg) + } +} + +impl MessageWrite for UUID { + fn get_size(&self) -> usize { + 0 + + if self.part1 == 0i64 { 0 } else { 1 + sizeof_varint(*(&self.part1) as u64) } + + if self.part2 == 0i64 { 0 } else { 1 + sizeof_varint(*(&self.part2) as u64) } + } + + fn write_message(&self, w: &mut Writer) -> Result<()> { + if self.part1 != 0i64 { w.write_with_tag(8, |w| w.write_int64(*&self.part1))?; } + if self.part2 != 0i64 { w.write_with_tag(16, |w| w.write_int64(*&self.part2))?; } + Ok(()) + } +} +