Merge dev branch into main

This commit is contained in:
a2x
2024-03-28 22:19:20 +10:00
parent 755093fe06
commit 889ef7dcd8
315 changed files with 552043 additions and 333811 deletions

91
src/output/buttons.rs Normal file
View File

@@ -0,0 +1,91 @@
use std::env;
use std::fmt::Write;
use heck::{AsPascalCase, AsShoutySnakeCase, AsSnakeCase};
use super::{Button, CodeGen, Results};
use crate::error::Result;
impl CodeGen for Vec<Button> {
fn to_cs(&self, results: &Results, indent_size: usize) -> Result<String> {
self.write_content(results, indent_size, |fmt| {
fmt.block("namespace CS2Dumper", |fmt| {
writeln!(fmt, "// Module: {}", get_module_name())?;
fmt.block("public static class Buttons", |fmt| {
for button in self {
writeln!(
fmt,
"public const nint {} = {:#X};",
AsPascalCase(&button.name),
button.value
)?;
}
Ok(())
})
})?;
Ok(())
})
}
fn to_hpp(&self, results: &Results, indent_size: usize) -> Result<String> {
self.write_content(results, indent_size, |fmt| {
writeln!(fmt, "#pragma once\n")?;
writeln!(fmt, "#include <cstddef>\n")?;
fmt.block("namespace cs2_dumper", |fmt| {
writeln!(fmt, "// Module: {}", get_module_name())?;
fmt.block("namespace buttons", |fmt| {
for button in self {
writeln!(
fmt,
"constexpr std::ptrdiff_t {} = {:#X};",
AsSnakeCase(&button.name),
button.value
)?;
}
Ok(())
})
})?;
Ok(())
})
}
fn to_rs(&self, results: &Results, indent_size: usize) -> Result<String> {
self.write_content(results, indent_size, |fmt| {
fmt.block("pub mod cs2_dumper", |fmt| {
writeln!(fmt, "// Module: {}", get_module_name())?;
fmt.block("pub mod buttons", |fmt| {
for button in self {
writeln!(
fmt,
"pub const {}: usize = {:#X};",
AsShoutySnakeCase(&button.name),
button.value
)?;
}
Ok(())
})
})?;
Ok(())
})
}
}
#[inline]
fn get_module_name() -> &'static str {
match env::consts::OS {
"linux" => "libclient.so",
"windows" => "client.dll",
_ => panic!("unsupported os"),
}
}

78
src/output/formatter.rs Normal file
View File

@@ -0,0 +1,78 @@
use std::fmt::{self, Write};
#[derive(Debug)]
pub struct Formatter<'a> {
/// Write destination.
pub out: &'a mut String,
/// Number of spaces per indentation level.
pub indent_size: usize,
/// Current indentation level.
pub indent_level: usize,
}
impl<'a> Formatter<'a> {
pub fn new(out: &'a mut String, indent_size: usize) -> Self {
Self {
out,
indent_size,
indent_level: 0,
}
}
pub fn block<F>(&mut self, heading: &str, f: F) -> fmt::Result
where
F: FnOnce(&mut Self) -> fmt::Result,
{
write!(self, "{} {{\n", heading)?;
self.indent(f)?;
write!(self, "}}\n")?;
Ok(())
}
pub fn indent<F, R>(&mut self, f: F) -> R
where
F: FnOnce(&mut Self) -> R,
{
self.indent_level += 1;
let ret = f(self);
self.indent_level -= 1;
ret
}
#[inline]
#[rustfmt::skip]
fn push_indentation(&mut self) {
if self.indent_level > 0 {
self.out.push_str(&" ".repeat(self.indent_level * self.indent_size));
}
}
}
impl<'a> Write for Formatter<'a> {
fn write_str(&mut self, s: &str) -> fmt::Result {
let mut lines = s.lines().peekable();
while let Some(line) = lines.next() {
// Add indentation before the line if necessary.
if self.out.ends_with('\n') && !line.is_empty() {
self.push_indentation();
}
self.out.push_str(line);
if lines.peek().is_some() || s.ends_with('\n') {
self.out.push('\n');
}
}
Ok(())
}
}

109
src/output/interfaces.rs Normal file
View File

@@ -0,0 +1,109 @@
use std::fmt::Write;
use heck::{AsPascalCase, AsShoutySnakeCase, AsSnakeCase};
use super::{format_module_name, CodeGen, InterfaceMap, Results};
use crate::error::Result;
impl CodeGen for InterfaceMap {
fn to_cs(&self, results: &Results, indent_size: usize) -> Result<String> {
self.write_content(results, indent_size, |fmt| {
fmt.block("namespace CS2Dumper.Interfaces", |fmt| {
for (module_name, ifaces) in self {
writeln!(fmt, "// Module: {}", module_name)?;
fmt.block(
&format!(
"public static class {}",
AsPascalCase(format_module_name(module_name))
),
|fmt| {
for iface in ifaces {
writeln!(
fmt,
"public const nint {} = {:#X};",
AsPascalCase(&iface.name),
iface.value
)?;
}
Ok(())
},
)?;
}
Ok(())
})?;
Ok(())
})
}
fn to_hpp(&self, results: &Results, indent_size: usize) -> Result<String> {
self.write_content(results, indent_size, |fmt| {
writeln!(fmt, "#pragma once\n")?;
writeln!(fmt, "#include <cstddef>\n")?;
fmt.block("namespace cs2_dumper", |fmt| {
fmt.block("namespace interfaces", |fmt| {
for (module_name, ifaces) in self {
writeln!(fmt, "// Module: {}", module_name)?;
fmt.block(
&format!("namespace {}", AsSnakeCase(format_module_name(module_name))),
|fmt| {
for iface in ifaces {
writeln!(
fmt,
"constexpr std::ptrdiff_t {} = {:#X};",
AsSnakeCase(&iface.name),
iface.value
)?;
}
Ok(())
},
)?;
}
Ok(())
})
})?;
Ok(())
})
}
fn to_rs(&self, results: &Results, indent_size: usize) -> Result<String> {
self.write_content(results, indent_size, |fmt| {
fmt.block("pub mod cs2_dumper", |fmt| {
fmt.block("pub mod interfaces", |fmt| {
for (module_name, ifaces) in self {
writeln!(fmt, "// Module: {}", module_name)?;
fmt.block(
&format!("pub mod {}", AsSnakeCase(format_module_name(module_name))),
|fmt| {
for iface in ifaces {
writeln!(
fmt,
"pub const {}: usize = {:#X};",
AsShoutySnakeCase(&iface.name),
iface.value
)?;
}
Ok(())
},
)?;
}
Ok(())
})
})?;
Ok(())
})
}
}

178
src/output/mod.rs Normal file
View File

@@ -0,0 +1,178 @@
use std::fmt::Write;
use std::path::Path;
use std::{env, fs};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use formatter::Formatter;
use crate::analysis::*;
use crate::error::Result;
mod buttons;
mod formatter;
mod interfaces;
mod offsets;
mod schemas;
#[derive(Serialize)]
#[serde(rename_all = "snake_case")]
enum Item<'a> {
Buttons(&'a Vec<Button>),
Interfaces(&'a InterfaceMap),
Offsets(&'a OffsetMap),
Schemas(&'a SchemaMap),
}
impl<'a> Item<'a> {
fn generate(&self, results: &Results, indent_size: usize, file_type: &str) -> Result<String> {
match file_type {
"cs" => self.to_cs(results, indent_size),
"hpp" => self.to_hpp(results, indent_size),
"rs" => self.to_rs(results, indent_size),
"json" => serde_json::to_string_pretty(self).map_err(Into::into),
_ => unreachable!(),
}
}
}
trait CodeGen {
fn to_cs(&self, results: &Results, indent_size: usize) -> Result<String>;
fn to_hpp(&self, results: &Results, indent_size: usize) -> Result<String>;
fn to_rs(&self, results: &Results, indent_size: usize) -> Result<String>;
fn write_content<F>(&self, results: &Results, indent_size: usize, callback: F) -> Result<String>
where
F: FnOnce(&mut Formatter<'_>) -> Result<()>,
{
let mut buf = String::new();
let mut fmt = Formatter::new(&mut buf, indent_size);
results.write_banner(&mut fmt)?;
callback(&mut fmt)?;
Ok(buf)
}
}
impl<'a> CodeGen for Item<'a> {
fn to_cs(&self, results: &Results, indent_size: usize) -> Result<String> {
match self {
Item::Buttons(buttons) => buttons.to_cs(results, indent_size),
Item::Interfaces(interfaces) => interfaces.to_cs(results, indent_size),
Item::Offsets(offsets) => offsets.to_cs(results, indent_size),
Item::Schemas(schemas) => schemas.to_cs(results, indent_size),
}
}
fn to_hpp(&self, results: &Results, indent_size: usize) -> Result<String> {
match self {
Item::Buttons(buttons) => buttons.to_hpp(results, indent_size),
Item::Interfaces(interfaces) => interfaces.to_hpp(results, indent_size),
Item::Offsets(offsets) => offsets.to_hpp(results, indent_size),
Item::Schemas(schemas) => schemas.to_hpp(results, indent_size),
}
}
fn to_rs(&self, results: &Results, indent_size: usize) -> Result<String> {
match self {
Item::Buttons(buttons) => buttons.to_rs(results, indent_size),
Item::Interfaces(interfaces) => interfaces.to_rs(results, indent_size),
Item::Offsets(offsets) => offsets.to_rs(results, indent_size),
Item::Schemas(schemas) => schemas.to_rs(results, indent_size),
}
}
}
#[derive(Deserialize, Serialize)]
pub struct Results {
/// Timestamp of the dump.
pub timestamp: DateTime<Utc>,
/// List of buttons to dump.
pub buttons: Vec<Button>,
/// Map of interfaces to dump.
pub interfaces: InterfaceMap,
/// Map of offsets to dump.
pub offsets: OffsetMap,
/// Map of schema classes and enums to dump.
pub schemas: SchemaMap,
}
impl Results {
pub fn new(
buttons: Vec<Button>,
interfaces: InterfaceMap,
offsets: OffsetMap,
schemas: SchemaMap,
) -> Self {
Self {
timestamp: Utc::now(),
buttons,
interfaces,
offsets,
schemas,
}
}
pub fn dump_all<P: AsRef<Path>>(&self, out_dir: P, indent_size: usize) -> Result<()> {
let items = [
("buttons", Item::Buttons(&self.buttons)),
("interfaces", Item::Interfaces(&self.interfaces)),
("offsets", Item::Offsets(&self.offsets)),
("schemas", Item::Schemas(&self.schemas)),
];
// TODO: Make this user-configurable.
let file_types = ["cs", "hpp", "json", "rs"];
for (file_name, item) in &items {
for &file_type in &file_types {
let content = item.generate(self, indent_size, file_type)?;
self.dump_file(out_dir.as_ref(), file_name, file_type, &content)?;
}
}
Ok(())
}
fn dump_file<P: AsRef<Path>>(
&self,
out_dir: P,
file_name: &str,
file_type: &str,
content: &str,
) -> Result<()> {
let file_path = out_dir
.as_ref()
.join(format!("{}.{}", file_name, file_type));
fs::write(file_path, content)?;
Ok(())
}
fn write_banner(&self, fmt: &mut Formatter<'_>) -> Result<()> {
writeln!(fmt, "// Generated using https://github.com/a2x/cs2-dumper")?;
writeln!(fmt, "// {}\n", self.timestamp)?;
Ok(())
}
}
pub fn format_module_name(module_name: &String) -> String {
let extension = match env::consts::OS {
"linux" => ".so",
"windows" => ".dll",
_ => panic!("unsupported os"),
};
module_name.strip_suffix(extension).unwrap().to_string()
}

109
src/output/offsets.rs Normal file
View File

@@ -0,0 +1,109 @@
use std::fmt::Write;
use heck::{AsPascalCase, AsShoutySnakeCase, AsSnakeCase};
use super::{format_module_name, CodeGen, OffsetMap, Results};
use crate::error::Result;
impl CodeGen for OffsetMap {
fn to_cs(&self, results: &Results, indent_size: usize) -> Result<String> {
self.write_content(results, indent_size, |fmt| {
fmt.block("namespace CS2Dumper.Offsets", |fmt| {
for (module_name, offsets) in self {
writeln!(fmt, "// Module: {}", module_name)?;
fmt.block(
&format!(
"public static class {}",
AsPascalCase(format_module_name(module_name))
),
|fmt| {
for offset in offsets {
writeln!(
fmt,
"public const nint {} = {:#X};",
AsPascalCase(&offset.name),
offset.value
)?;
}
Ok(())
},
)?;
}
Ok(())
})?;
Ok(())
})
}
fn to_hpp(&self, results: &Results, indent_size: usize) -> Result<String> {
self.write_content(results, indent_size, |fmt| {
writeln!(fmt, "#pragma once\n")?;
writeln!(fmt, "#include <cstddef>\n")?;
fmt.block("namespace cs2_dumper", |fmt| {
fmt.block("namespace offsets", |fmt| {
for (module_name, offsets) in self {
writeln!(fmt, "// Module: {}", module_name)?;
fmt.block(
&format!("namespace {}", AsSnakeCase(format_module_name(module_name))),
|fmt| {
for offset in offsets {
writeln!(
fmt,
"constexpr std::ptrdiff_t {} = {:#X};",
AsSnakeCase(&offset.name),
offset.value
)?;
}
Ok(())
},
)?;
}
Ok(())
})
})?;
Ok(())
})
}
fn to_rs(&self, results: &Results, indent_size: usize) -> Result<String> {
self.write_content(results, indent_size, |fmt| {
fmt.block("pub mod cs2_dumper", |fmt| {
fmt.block("pub mod offsets", |fmt| {
for (module_name, offsets) in self {
writeln!(fmt, "// Module: {}", module_name)?;
fmt.block(
&format!("pub mod {}", AsSnakeCase(format_module_name(module_name))),
|fmt| {
for offset in offsets {
writeln!(
fmt,
"pub const {}: usize = {:#X};",
AsShoutySnakeCase(&offset.name),
offset.value
)?;
}
Ok(())
},
)?;
}
Ok(())
})
})?;
Ok(())
})
}
}

313
src/output/schemas.rs Normal file
View File

@@ -0,0 +1,313 @@
use std::fmt::{self, Write};
use heck::{AsPascalCase, AsShoutySnakeCase, AsSnakeCase};
use super::{format_module_name, CodeGen, Formatter, Results, SchemaMap};
use crate::analysis::ClassMetadata;
use crate::error::Result;
impl CodeGen for SchemaMap {
fn to_cs(&self, results: &Results, indent_size: usize) -> Result<String> {
self.write_content(results, indent_size, |fmt| {
fmt.block("namespace CS2Dumper.Schemas", |fmt| {
for (module_name, (classes, enums)) in self {
writeln!(fmt, "// Module: {}", module_name)?;
writeln!(fmt, "// Classes count: {}", classes.len())?;
writeln!(fmt, "// Enums count: {}", enums.len())?;
fmt.block(
&format!(
"public static class {}",
AsPascalCase(format_module_name(module_name))
),
|fmt| {
for enum_ in enums {
let ty = match enum_.ty.as_str() {
"int8" => "sbyte",
"int16" => "short",
"int32" => "int",
"int64" => "long",
_ => continue,
};
writeln!(fmt, "// Alignment: {}", enum_.alignment)?;
writeln!(fmt, "// Members count: {}", enum_.size)?;
fmt.block(
&format!("public enum {} : {}", AsPascalCase(&enum_.name), ty),
|fmt| {
let members = enum_
.members
.iter()
.map(|member| {
format!(
"{} = {}",
AsPascalCase(&member.name),
member.value
)
})
.collect::<Vec<_>>()
.join(",\n");
writeln!(fmt, "{}", members)
},
)?;
}
for class in classes {
let parent_name = class
.parent
.as_ref()
.map(|parent| format!("{}", AsPascalCase(&parent.name)))
.unwrap_or_else(|| "None".to_string());
writeln!(fmt, "// Parent: {}", parent_name)?;
writeln!(fmt, "// Fields count: {}", class.fields.len())?;
write_metadata(fmt, &class.metadata)?;
fmt.block(
&format!("public static class {}", AsPascalCase(&class.name)),
|fmt| {
for field in &class.fields {
writeln!(
fmt,
"public const nint {} = {:#X}; // {}",
AsPascalCase(&field.name),
field.offset,
field.ty
)?;
}
Ok(())
},
)?;
}
Ok(())
},
)?;
}
Ok(())
})?;
Ok(())
})
}
fn to_hpp(&self, results: &Results, indent_size: usize) -> Result<String> {
self.write_content(results, indent_size, |fmt| {
writeln!(fmt, "#pragma once\n")?;
writeln!(fmt, "#include <cstddef>\n")?;
fmt.block("namespace cs2_dumper", |fmt| {
fmt.block("namespace schemas", |fmt| {
for (module_name, (classes, enums)) in self {
writeln!(fmt, "// Module: {}", module_name)?;
writeln!(fmt, "// Classes count: {}", classes.len())?;
writeln!(fmt, "// Enums count: {}", enums.len())?;
fmt.block(
&format!("namespace {}", AsSnakeCase(format_module_name(module_name))),
|fmt| {
for enum_ in enums {
let ty = match enum_.ty.as_str() {
"int8" => "int8_t",
"int16" => "int16_t",
"int32" => "int32_t",
"int64" => "int64_t",
_ => continue,
};
writeln!(fmt, "// Alignment: {}", enum_.alignment)?;
writeln!(fmt, "// Members count: {}", enum_.size)?;
fmt.block(
&format!(
"enum class {} : {}",
AsSnakeCase(&enum_.name),
ty
),
|fmt| {
let members = enum_
.members
.iter()
.map(|member| {
format!(
"{} = {}",
AsSnakeCase(&member.name),
member.value
)
})
.collect::<Vec<_>>()
.join(",\n");
writeln!(fmt, "{}", members)
},
)?;
}
for class in classes {
let parent_name = class
.parent
.as_ref()
.map(|parent| format!("{}", AsSnakeCase(&parent.name)))
.unwrap_or_else(|| "None".to_string());
writeln!(fmt, "// Parent: {}", parent_name)?;
writeln!(fmt, "// Fields count: {}", class.fields.len())?;
write_metadata(fmt, &class.metadata)?;
fmt.block(
&format!("namespace {}", AsSnakeCase(&class.name)),
|fmt| {
for field in &class.fields {
writeln!(
fmt,
"constexpr std::ptrdiff_t {} = {:#X}; // {}",
AsSnakeCase(&field.name),
field.offset,
field.ty
)?;
}
Ok(())
},
)?;
}
Ok(())
},
)?;
}
Ok(())
})
})?;
Ok(())
})
}
fn to_rs(&self, results: &Results, indent_size: usize) -> Result<String> {
self.write_content(results, indent_size, |fmt| {
fmt.block("pub mod cs2_dumper", |fmt| {
fmt.block("pub mod schemas", |fmt| {
for (module_name, (classes, enums)) in self {
writeln!(fmt, "// Module: {}", module_name)?;
writeln!(fmt, "// Classes count: {}", classes.len())?;
writeln!(fmt, "// Enums count: {}", enums.len())?;
fmt.block(
&format!("pub mod {}", AsSnakeCase(format_module_name(module_name))),
|fmt| {
for enum_ in enums {
let ty = match enum_.ty.as_str() {
"int8" => "i8",
"int16" => "i16",
"int32" => "i32",
"int64" => "i64",
_ => continue,
};
writeln!(fmt, "// Alignment: {}", enum_.alignment)?;
writeln!(fmt, "// Members count: {}", enum_.size)?;
fmt.block(
&format!(
"#[repr({})]\npub enum {}",
ty,
AsPascalCase(&enum_.name),
),
|fmt| {
// TODO: Handle the case where multiple members share
// the same value.
let members = enum_
.members
.iter()
.map(|member| {
format!(
"{} = {}",
AsPascalCase(&member.name),
member.value
)
})
.collect::<Vec<_>>()
.join(",\n");
writeln!(fmt, "{}", members)
},
)?;
}
for class in classes {
let parent_name = class
.parent
.as_ref()
.map(|parent| format!("{}", AsSnakeCase(&parent.name)))
.unwrap_or_else(|| "None".to_string());
writeln!(fmt, "// Parent: {}", parent_name)?;
writeln!(fmt, "// Fields count: {}", class.fields.len())?;
write_metadata(fmt, &class.metadata)?;
fmt.block(
&format!("pub mod {}", AsSnakeCase(&class.name)),
|fmt| {
for field in &class.fields {
writeln!(
fmt,
"pub const {}: usize = {:#X}; // {}",
AsShoutySnakeCase(&field.name),
field.offset,
field.ty
)?;
}
Ok(())
},
)?;
}
Ok(())
},
)?;
}
Ok(())
})
})?;
Ok(())
})
}
}
fn write_metadata(fmt: &mut Formatter<'_>, metadata: &[ClassMetadata]) -> fmt::Result {
if metadata.is_empty() {
return Ok(());
}
writeln!(fmt, "//")?;
writeln!(fmt, "// Metadata:")?;
for metadata in metadata {
match metadata {
ClassMetadata::NetworkChangeCallback { name } => {
writeln!(fmt, "// NetworkChangeCallback: {}", name)?;
}
ClassMetadata::NetworkVarNames { name, ty } => {
writeln!(fmt, "// NetworkVarNames: {} ({})", name, ty)?;
}
ClassMetadata::Unknown { name } => {
writeln!(fmt, "// {}", name)?;
}
}
}
Ok(())
}