Initial parser for stormfate replay

This commit is contained in:
Quentin Rouland 2024-05-25 15:08:14 -04:00
parent 1039ccf224
commit d88e5410a0
11 changed files with 895 additions and 1 deletions

3
.gitignore vendored
View File

@ -14,3 +14,6 @@ Cargo.lock
# MSVC Windows builds of rustc generate these, which store debugging information
*.pdb
# Project specific
replays

13
Cargo.toml Normal file
View File

@ -0,0 +1,13 @@
[package]
name = "sgtool"
version = "0.0.1-dev"
edition = "2021"
authors = [ "Quentin Rouland <quentin@qrouland.com>" ]
[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"

View File

@ -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.

10
gen_proto.sh Executable file
View File

@ -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

0
out Normal file
View File

4
src/lib.rs Normal file
View File

@ -0,0 +1,4 @@
#[macro_use] extern crate log;
pub mod parser;
mod protos;

27
src/main.rs Normal file
View File

@ -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());
}

101
src/parser.rs Normal file
View File

@ -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<ReplayChunk<'a>>,
}
/// 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<Vec<u8>, std::io::Error> + 'a {
move |size| {
let mut buf = vec![0u8; size];
reader.read_exact(&mut buf)?;
Ok(buf)
}
}
impl Header {
///
fn load<T: BufRead>(reader: &mut T) -> Result<Header, std::io::Error> {
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<T: BufRead>(buf_reader: &mut T, buf: &'a mut Vec<u8>) -> Result<Payload<'a>, Box<dyn std::error::Error>> {
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::<ReplayChunk>(buf)?;
data.push(read_message);
}
Ok(Payload { chunks: data })
}
}
impl<'a> Replay<'a> {
fn load<T: BufRead>(buf_reader: &mut T, buf: &'a mut Vec<u8>) -> Result<Replay<'a>, Box<dyn std::error::Error>> {
// 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<u8>) -> Result<Replay<'a>, Box<dyn std::error::Error>> {
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<u8> = 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);
}
}

1
src/protos/mod.rs Normal file
View File

@ -0,0 +1 @@
pub mod stormgate;

104
src/protos/stormgate.proto Normal file
View File

@ -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;
}

619
src/protos/stormgate.rs Normal file
View File

@ -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<i32> 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<stormgate::mod_ReplayChunk::Wrapper<'a>>,
}
impl<'a> MessageRead<'a> for ReplayChunk<'a> {
fn from_reader(r: &mut BytesReader, bytes: &'a [u8]) -> Result<Self> {
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::<stormgate::mod_ReplayChunk::Wrapper>(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<W: WriterBackend>(&self, w: &mut Writer<W>) -> 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<stormgate::mod_ReplayChunk::mod_Wrapper::ReplayContent<'a>>,
}
impl<'a> MessageRead<'a> for Wrapper<'a> {
fn from_reader(r: &mut BytesReader, bytes: &'a [u8]) -> Result<Self> {
let mut msg = Self::default();
while !r.is_eof() {
match r.next_tag(bytes) {
Ok(10) => msg.content = Some(r.read_message::<stormgate::mod_ReplayChunk::mod_Wrapper::ReplayContent>(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<W: WriterBackend>(&self, w: &mut Writer<W>) -> 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<Self> {
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::<stormgate::Map>(bytes)?),
Ok(98) => msg.content_type = stormgate::mod_ReplayChunk::mod_Wrapper::mod_ReplayContent::OneOfcontent_type::player(r.read_message::<stormgate::Player>(bytes)?),
Ok(106) => msg.content_type = stormgate::mod_ReplayChunk::mod_Wrapper::mod_ReplayContent::OneOfcontent_type::change_slot(r.read_message::<stormgate::LobbyChangeSlot>(bytes)?),
Ok(122) => msg.content_type = stormgate::mod_ReplayChunk::mod_Wrapper::mod_ReplayContent::OneOfcontent_type::set_variable(r.read_message::<stormgate::LobbySetVariable>(bytes)?),
Ok(146) => msg.content_type = stormgate::mod_ReplayChunk::mod_Wrapper::mod_ReplayContent::OneOfcontent_type::start_game(r.read_message::<stormgate::StartGame>(bytes)?),
Ok(202) => msg.content_type = stormgate::mod_ReplayChunk::mod_Wrapper::mod_ReplayContent::OneOfcontent_type::player_left_game(r.read_message::<stormgate::PlayerLeftGame>(bytes)?),
Ok(298) => msg.content_type = stormgate::mod_ReplayChunk::mod_Wrapper::mod_ReplayContent::OneOfcontent_type::assign_player_slot(r.read_message::<stormgate::AssignPlayerSlot>(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<W: WriterBackend>(&self, w: &mut Writer<W>) -> 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<Self> {
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<W: WriterBackend>(&self, w: &mut Writer<W>) -> 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<stormgate::UUID>,
pub name: Option<stormgate::mod_Player::PlayerName<'a>>,
}
impl<'a> MessageRead<'a> for Player<'a> {
fn from_reader(r: &mut BytesReader, bytes: &'a [u8]) -> Result<Self> {
let mut msg = Self::default();
while !r.is_eof() {
match r.next_tag(bytes) {
Ok(18) => msg.uuid = Some(r.read_message::<stormgate::UUID>(bytes)?),
Ok(26) => msg.name = Some(r.read_message::<stormgate::mod_Player::PlayerName>(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<W: WriterBackend>(&self, w: &mut Writer<W>) -> 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<Self> {
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<W: WriterBackend>(&self, w: &mut Writer<W>) -> 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<stormgate::mod_LobbyChangeSlot::SlotChoice>,
}
impl<'a> MessageRead<'a> for LobbyChangeSlot {
fn from_reader(r: &mut BytesReader, bytes: &'a [u8]) -> Result<Self> {
let mut msg = Self::default();
while !r.is_eof() {
match r.next_tag(bytes) {
Ok(10) => msg.choice = Some(r.read_message::<stormgate::mod_LobbyChangeSlot::SlotChoice>(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<W: WriterBackend>(&self, w: &mut Writer<W>) -> 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<Self> {
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::<stormgate::mod_LobbyChangeSlot::mod_SlotChoice::SpecificSlot>(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<W: WriterBackend>(&self, w: &mut Writer<W>) -> 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<Self> {
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<W: WriterBackend>(&self, w: &mut Writer<W>) -> 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<Self> {
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<W: WriterBackend>(&self, w: &mut Writer<W>) -> 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<Self> {
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<stormgate::UUID>,
pub reason: stormgate::LeaveReason,
}
impl<'a> MessageRead<'a> for PlayerLeftGame {
fn from_reader(r: &mut BytesReader, bytes: &'a [u8]) -> Result<Self> {
let mut msg = Self::default();
while !r.is_eof() {
match r.next_tag(bytes) {
Ok(10) => msg.player_uuid = Some(r.read_message::<stormgate::UUID>(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<W: WriterBackend>(&self, w: &mut Writer<W>) -> 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<stormgate::UUID>,
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<Self> {
let mut msg = Self::default();
while !r.is_eof() {
match r.next_tag(bytes) {
Ok(10) => msg.uuid = Some(r.read_message::<stormgate::UUID>(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<W: WriterBackend>(&self, w: &mut Writer<W>) -> 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<Self> {
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<W: WriterBackend>(&self, w: &mut Writer<W>) -> 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(())
}
}