diff --git a/README.md b/README.md index 47c72fa4..3aefea99 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ Linux or as an administrator on Windows. - `-c, --connector `: The name of the memflow connector to use. - `-a, --connector-args `: Additional arguments to pass to the memflow connector. -- `-f, --file-types `: The types of files to generate. Default: `cs`, `hpp`, `json`, `rs`. +- `-f, --file-types `: The types of files to generate. Default: `cs`, `hpp`, `json`, `rs`, `zig`. - `-i, --indent-size `: The number of spaces to use per indentation level. Default: `4`. - `-o, --output `: The output directory to write the generated files to. Default: `output`. - `-p, --process-name `: The name of the game process. Default: `cs2.exe`. diff --git a/src/main.rs b/src/main.rs index 6e210c6b..7107ec1b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -36,7 +36,12 @@ struct Args { connector_args: Option, /// The types of files to generate. - #[arg(short, long, value_delimiter = ',', default_values = ["cs", "hpp", "json", "rs"])] + #[arg( + short, + long, + value_delimiter = ',', + default_values = ["cs", "hpp", "json", "rs", "zig"] + )] file_types: Vec, /// The number of spaces to use per indentation level. diff --git a/src/output/buttons.rs b/src/output/buttons.rs index 149e2270..52eeb7cc 100644 --- a/src/output/buttons.rs +++ b/src/output/buttons.rs @@ -1,7 +1,7 @@ use std::collections::BTreeMap; use std::fmt::{self, Write}; -use super::{ButtonMap, CodeWriter, Formatter}; +use super::{ButtonMap, CodeWriter, Formatter, zig_ident}; impl CodeWriter for ButtonMap { fn write_cs(&self, fmt: &mut Formatter<'_>) -> fmt::Result { @@ -67,4 +67,18 @@ impl CodeWriter for ButtonMap { }) }) } + + fn write_zig(&self, fmt: &mut Formatter<'_>) -> fmt::Result { + fmt.block("pub const cs2_dumper = struct", true, |fmt| { + writeln!(fmt, "// Module: client.dll")?; + + fmt.block("pub const buttons = struct", true, |fmt| { + for (name, value) in self { + writeln!(fmt, "pub const {}: usize = {:#X};", zig_ident(name), value)?; + } + + Ok(()) + }) + }) + } } diff --git a/src/output/interfaces.rs b/src/output/interfaces.rs index 58b7b4e9..f7c59b6e 100644 --- a/src/output/interfaces.rs +++ b/src/output/interfaces.rs @@ -3,7 +3,7 @@ use std::fmt::{self, Write}; use heck::{AsPascalCase, AsSnakeCase}; -use super::{CodeWriter, Formatter, InterfaceMap, slugify}; +use super::{CodeWriter, Formatter, InterfaceMap, slugify, zig_ident}; impl CodeWriter for InterfaceMap { fn write_cs(&self, fmt: &mut Formatter<'_>) -> fmt::Result { @@ -103,4 +103,35 @@ impl CodeWriter for InterfaceMap { }) }) } + + fn write_zig(&self, fmt: &mut Formatter<'_>) -> fmt::Result { + fmt.block("pub const cs2_dumper = struct", true, |fmt| { + fmt.block("pub const interfaces = struct", true, |fmt| { + for (module_name, ifaces) in self { + writeln!(fmt, "// Module: {}", module_name)?; + + let module_name = zig_ident(&AsSnakeCase(slugify(module_name)).to_string()); + + fmt.block( + &format!("pub const {} = struct", module_name), + true, + |fmt| { + for (name, value) in ifaces { + writeln!( + fmt, + "pub const {}: usize = {:#X};", + zig_ident(name), + value + )?; + } + + Ok(()) + }, + )?; + } + + Ok(()) + }) + }) + } } diff --git a/src/output/mod.rs b/src/output/mod.rs index 54196b44..95fb653a 100644 --- a/src/output/mod.rs +++ b/src/output/mod.rs @@ -34,6 +34,7 @@ impl<'a> Item<'a> { "hpp" => self.write_hpp(fmt), "json" => self.write_json(fmt), "rs" => self.write_rs(fmt), + "zig" => self.write_zig(fmt), _ => unimplemented!(), } } @@ -44,6 +45,7 @@ trait CodeWriter { fn write_hpp(&self, fmt: &mut Formatter<'_>) -> fmt::Result; fn write_json(&self, fmt: &mut Formatter<'_>) -> fmt::Result; fn write_rs(&self, fmt: &mut Formatter<'_>) -> fmt::Result; + fn write_zig(&self, fmt: &mut Formatter<'_>) -> fmt::Result; } impl<'a> CodeWriter for Item<'a> { @@ -82,6 +84,15 @@ impl<'a> CodeWriter for Item<'a> { Item::Schemas(schemas) => schemas.write_rs(fmt), } } + + fn write_zig(&self, fmt: &mut Formatter<'_>) -> fmt::Result { + match self { + Item::Buttons(buttons) => buttons.write_zig(fmt), + Item::Interfaces(ifaces) => ifaces.write_zig(fmt), + Item::Offsets(offsets) => offsets.write_zig(fmt), + Item::Schemas(schemas) => schemas.write_zig(fmt), + } + } } pub struct Output<'a> { @@ -193,3 +204,85 @@ impl<'a> Output<'a> { fn slugify(input: &str) -> String { input.replace(|c: char| !c.is_alphanumeric(), "_") } + +#[inline] +fn zig_ident(input: &str) -> String { + if is_zig_identifier(input) && !is_zig_keyword(input) { + input.to_string() + } else { + let escaped = input.replace('\\', "\\\\").replace('"', "\\\""); + + format!("@\"{}\"", escaped) + } +} + +#[inline] +fn is_zig_identifier(input: &str) -> bool { + let mut chars = input.chars(); + + match chars.next() { + Some(c) if c == '_' || c.is_ascii_alphabetic() => {} + _ => return false, + } + + chars.all(|c| c == '_' || c.is_ascii_alphanumeric()) +} + +#[inline] +fn is_zig_keyword(input: &str) -> bool { + matches!( + input, + "addrspace" + | "align" + | "allowzero" + | "and" + | "anyframe" + | "anytype" + | "asm" + | "async" + | "await" + | "break" + | "callconv" + | "catch" + | "comptime" + | "const" + | "continue" + | "defer" + | "else" + | "enum" + | "errdefer" + | "error" + | "export" + | "extern" + | "false" + | "fn" + | "for" + | "if" + | "inline" + | "linksection" + | "noalias" + | "noinline" + | "nosuspend" + | "null" + | "opaque" + | "or" + | "orelse" + | "packed" + | "pub" + | "resume" + | "return" + | "struct" + | "suspend" + | "switch" + | "test" + | "threadlocal" + | "true" + | "try" + | "union" + | "unreachable" + | "usingnamespace" + | "var" + | "volatile" + | "while" + ) +} diff --git a/src/output/offsets.rs b/src/output/offsets.rs index 82447b86..77d1cbc6 100644 --- a/src/output/offsets.rs +++ b/src/output/offsets.rs @@ -2,7 +2,7 @@ use std::fmt::{self, Write}; use heck::{AsPascalCase, AsSnakeCase}; -use super::{CodeWriter, Formatter, OffsetMap, slugify}; +use super::{CodeWriter, Formatter, OffsetMap, slugify, zig_ident}; impl CodeWriter for OffsetMap { fn write_cs(&self, fmt: &mut Formatter<'_>) -> fmt::Result { @@ -84,4 +84,35 @@ impl CodeWriter for OffsetMap { }) }) } + + fn write_zig(&self, fmt: &mut Formatter<'_>) -> fmt::Result { + fmt.block("pub const cs2_dumper = struct", true, |fmt| { + fmt.block("pub const offsets = struct", true, |fmt| { + for (module_name, offsets) in self { + writeln!(fmt, "// Module: {}", module_name)?; + + let module_name = zig_ident(&AsSnakeCase(slugify(module_name)).to_string()); + + fmt.block( + &format!("pub const {} = struct", module_name), + true, + |fmt| { + for (name, value) in offsets { + writeln!( + fmt, + "pub const {}: usize = {:#X};", + zig_ident(name), + value + )?; + } + + Ok(()) + }, + )?; + } + + Ok(()) + }) + }) + } } diff --git a/src/output/schemas.rs b/src/output/schemas.rs index 0194abbe..83e564e3 100644 --- a/src/output/schemas.rs +++ b/src/output/schemas.rs @@ -5,7 +5,7 @@ use heck::{AsPascalCase, AsSnakeCase}; use serde_json::json; -use super::{CodeWriter, Formatter, SchemaMap, slugify}; +use super::{CodeWriter, Formatter, SchemaMap, slugify, zig_ident}; use crate::analysis::ClassMetadata; @@ -390,6 +390,111 @@ impl CodeWriter for SchemaMap { }) }) } + + fn write_zig(&self, fmt: &mut Formatter<'_>) -> fmt::Result { + fmt.block("pub const cs2_dumper = struct", true, |fmt| { + fmt.block("pub const schemas = struct", true, |fmt| { + for (module_name, (classes, enums)) in self { + writeln!(fmt, "// Module: {}", module_name)?; + writeln!(fmt, "// Class count: {}", classes.len())?; + writeln!(fmt, "// Enum count: {}", enums.len())?; + + let module_name = zig_ident(&AsSnakeCase(slugify(module_name)).to_string()); + + fmt.block( + &format!("pub const {} = struct", module_name), + true, + |fmt| { + for enum_ in enums { + let type_name = match enum_.alignment { + 1 => "u8", + 2 => "u16", + 4 => "u32", + 8 => "u64", + _ => continue, + }; + + writeln!(fmt, "// Alignment: {}", enum_.alignment)?; + writeln!(fmt, "// Member count: {}", enum_.size)?; + + let enum_name = zig_ident(&slugify(&enum_.name)); + + fmt.block( + &format!("pub const {} = enum({})", enum_name, type_name), + true, + |fmt| { + let mut used_values = HashSet::new(); + + let members = enum_ + .members + .iter() + .filter_map(|member| { + // Skip duplicate values. + if !used_values.insert(member.value) { + return None; + } + + let formatted_value = format_zig_enum_member_value( + member.value, + type_name, + ); + + Some(format!( + "{} = {}", + zig_ident(&member.name), + formatted_value + )) + }) + .collect::>() + .join(",\n"); + + writeln!(fmt, "{}", members) + }, + )?; + } + + for class in classes { + let parent_name = class + .parent_name + .as_deref() + .map(slugify) + .unwrap_or("None".to_string()); + + writeln!(fmt, "// Parent: {}", parent_name)?; + writeln!(fmt, "// Field count: {}", class.fields.len())?; + + write_metadata(fmt, &class.metadata)?; + + let class_name = zig_ident(&slugify(&class.name)); + + fmt.block( + &format!("pub const {} = struct", class_name), + true, + |fmt| { + for field in &class.fields { + writeln!( + fmt, + "pub const {}: usize = {:#X}; // {}", + zig_ident(&field.name), + field.offset, + field.type_name + )?; + } + + Ok(()) + }, + )?; + } + + Ok(()) + }, + )?; + } + + Ok(()) + }) + }) + } } fn write_metadata(fmt: &mut Formatter<'_>, metadata: &[ClassMetadata]) -> fmt::Result { @@ -416,3 +521,19 @@ fn write_metadata(fmt: &mut Formatter<'_>, metadata: &[ClassMetadata]) -> fmt::R Ok(()) } + +fn format_zig_enum_member_value(value: i64, type_name: &str) -> String { + if value >= 0 { + return format!("{:#X}", value); + } + + let wrapped_value = match type_name { + "u8" => value as u8 as u64, + "u16" => value as u16 as u64, + "u32" => value as u32 as u64, + "u64" => value as u64, + _ => 0, + }; + + format!("{:#X}", wrapped_value) +}