mirror of
https://github.com/a2x/cs2-dumper.git
synced 2026-03-14 13:17:24 +08:00
feat(output): add Zig code generation backend
- add "zig" as supported file type in output dispatcher - implement Zig writers for buttons/interfaces/offsets/schemas - add Zig identifier escaping for keywords and invalid names - handle negative enum values safely in Zig schema output - include zig in default --file-types and update README
This commit is contained in:
@@ -36,7 +36,7 @@ Linux or as an administrator on Windows.
|
||||
|
||||
- `-c, --connector <connector>`: The name of the memflow connector to use.
|
||||
- `-a, --connector-args <connector-args>`: Additional arguments to pass to the memflow connector.
|
||||
- `-f, --file-types <file-types>`: The types of files to generate. Default: `cs`, `hpp`, `json`, `rs`.
|
||||
- `-f, --file-types <file-types>`: The types of files to generate. Default: `cs`, `hpp`, `json`, `rs`, `zig`.
|
||||
- `-i, --indent-size <indent-size>`: The number of spaces to use per indentation level. Default: `4`.
|
||||
- `-o, --output <output>`: The output directory to write the generated files to. Default: `output`.
|
||||
- `-p, --process-name <process-name>`: The name of the game process. Default: `cs2.exe`.
|
||||
|
||||
@@ -36,7 +36,12 @@ struct Args {
|
||||
connector_args: Option<String>,
|
||||
|
||||
/// 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<String>,
|
||||
|
||||
/// The number of spaces to use per indentation level.
|
||||
|
||||
@@ -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(())
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(())
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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(())
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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::<Vec<_>>()
|
||||
.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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user