mirror of
https://github.com/a2x/cs2-dumper.git
synced 2025-09-18 12:50:01 +08:00
Merge dev
branch into main
This commit is contained in:
76
src/analysis/buttons.rs
Normal file
76
src/analysis/buttons.rs
Normal file
@@ -0,0 +1,76 @@
|
||||
use std::env;
|
||||
|
||||
use log::debug;
|
||||
|
||||
use memflow::prelude::v1::*;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use skidscan_macros::signature;
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::source_engine::KeyboardKey;
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct Button {
|
||||
pub name: String,
|
||||
pub value: u32,
|
||||
}
|
||||
|
||||
pub fn buttons(process: &mut IntoProcessInstanceArcBox<'_>) -> Result<Vec<Button>> {
|
||||
let (module_name, sig) = match env::consts::OS {
|
||||
"linux" => (
|
||||
"libclient.so",
|
||||
signature!("48 8B 15 ? ? ? ? 48 89 83 ? ? ? ? 48 85 D2"),
|
||||
),
|
||||
"windows" => (
|
||||
"client.dll",
|
||||
signature!("48 8B 15 ? ? ? ? 48 85 D2 74 ? 0F 1F 40"),
|
||||
),
|
||||
_ => panic!("unsupported os"),
|
||||
};
|
||||
|
||||
let module = process.module_by_name(&module_name)?;
|
||||
let buf = process.read_raw(module.base, module.size as _)?;
|
||||
|
||||
let list_addr = sig
|
||||
.scan(&buf)
|
||||
.and_then(|ptr| process.read_addr64_rip(module.base + ptr).ok())
|
||||
.ok_or_else(|| Error::Other("unable to read button list address"))?;
|
||||
|
||||
read_buttons(process, &module, list_addr)
|
||||
}
|
||||
|
||||
fn read_buttons(
|
||||
process: &mut IntoProcessInstanceArcBox<'_>,
|
||||
module: &ModuleInfo,
|
||||
list_addr: Address,
|
||||
) -> Result<Vec<Button>> {
|
||||
let mut buttons = Vec::new();
|
||||
|
||||
let mut key_ptr = Pointer64::<KeyboardKey>::from(process.read_addr64(list_addr)?);
|
||||
|
||||
while !key_ptr.is_null() {
|
||||
let key = process.read_ptr(key_ptr)?;
|
||||
let name = process.read_char_string(key.name.address())?;
|
||||
|
||||
let value =
|
||||
((key_ptr.address() - module.base) + offset_of!(KeyboardKey.state) as i64) as u32;
|
||||
|
||||
debug!(
|
||||
"found button: {} at {:#X} ({} + {:#X})",
|
||||
name,
|
||||
value as u64 + module.base.to_umem(),
|
||||
module.name,
|
||||
value
|
||||
);
|
||||
|
||||
buttons.push(Button { name, value });
|
||||
|
||||
key_ptr = key.next;
|
||||
}
|
||||
|
||||
buttons.sort_unstable_by(|a, b| a.name.cmp(&b.name));
|
||||
|
||||
Ok(buttons)
|
||||
}
|
80
src/analysis/interfaces.rs
Normal file
80
src/analysis/interfaces.rs
Normal file
@@ -0,0 +1,80 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::env;
|
||||
|
||||
use log::debug;
|
||||
|
||||
use memflow::prelude::v1::*;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use skidscan_macros::signature;
|
||||
|
||||
use crate::error::Result;
|
||||
use crate::source_engine::InterfaceReg;
|
||||
|
||||
pub type InterfaceMap = BTreeMap<String, Vec<Interface>>;
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct Interface {
|
||||
pub name: String,
|
||||
pub value: u32,
|
||||
}
|
||||
|
||||
pub fn interfaces(process: &mut IntoProcessInstanceArcBox<'_>) -> Result<InterfaceMap> {
|
||||
let sig = match env::consts::OS {
|
||||
"linux" => signature!("48 8B 1D ? ? ? ? 48 85 DB 74 ? 49 89 FC"),
|
||||
"windows" => signature!("4C 8B 0D ? ? ? ? 4C 8B D2 4C 8B D9"),
|
||||
_ => panic!("unsupported os"),
|
||||
};
|
||||
|
||||
process
|
||||
.module_list()?
|
||||
.iter()
|
||||
.filter(|module| !module.name.starts_with("nvidia")) // Temporary workaround for upstream bug: https://github.com/memflow/memflow-native/blob/1b063fc573957498b88a13b6120120480bc65ea5/src/linux/mem.rs#L168
|
||||
.filter_map(|module| {
|
||||
let buf = process.read_raw(module.base, module.size as _).ok()?;
|
||||
|
||||
let list_addr = sig
|
||||
.scan(&buf)
|
||||
.and_then(|ptr| process.read_addr64_rip(module.base + ptr).ok())?;
|
||||
|
||||
read_interfaces(process, module, list_addr)
|
||||
.ok()
|
||||
.filter(|ifaces| !ifaces.is_empty())
|
||||
.map(|ifaces| Ok((module.name.to_string(), ifaces)))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn read_interfaces(
|
||||
process: &mut IntoProcessInstanceArcBox<'_>,
|
||||
module: &ModuleInfo,
|
||||
list_addr: Address,
|
||||
) -> Result<Vec<Interface>> {
|
||||
let mut ifaces = Vec::new();
|
||||
|
||||
let mut reg_ptr = Pointer64::<InterfaceReg>::from(process.read_addr64(list_addr)?);
|
||||
|
||||
while !reg_ptr.is_null() {
|
||||
let reg = process.read_ptr(reg_ptr)?;
|
||||
let name = process.read_char_string(reg.name.address())?;
|
||||
|
||||
let value = (reg.create_fn - module.base) as u32;
|
||||
|
||||
debug!(
|
||||
"found interface: {} at {:#X} ({} + {:#X})",
|
||||
name,
|
||||
value as u64 + module.base.to_umem(),
|
||||
module.name,
|
||||
value
|
||||
);
|
||||
|
||||
ifaces.push(Interface { name, value });
|
||||
|
||||
reg_ptr = reg.next;
|
||||
}
|
||||
|
||||
ifaces.sort_unstable_by(|a, b| a.name.cmp(&b.name));
|
||||
|
||||
Ok(ifaces)
|
||||
}
|
9
src/analysis/mod.rs
Normal file
9
src/analysis/mod.rs
Normal file
@@ -0,0 +1,9 @@
|
||||
pub use buttons::*;
|
||||
pub use interfaces::*;
|
||||
pub use offsets::*;
|
||||
pub use schemas::*;
|
||||
|
||||
pub mod buttons;
|
||||
pub mod interfaces;
|
||||
pub mod offsets;
|
||||
pub mod schemas;
|
95
src/analysis/offsets.rs
Normal file
95
src/analysis/offsets.rs
Normal file
@@ -0,0 +1,95 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::mem;
|
||||
use std::str::FromStr;
|
||||
|
||||
use log::{debug, error};
|
||||
|
||||
use memflow::prelude::v1::*;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::config::{Operation, Signature, CONFIG};
|
||||
use crate::error::{Error, Result};
|
||||
|
||||
pub type OffsetMap = BTreeMap<String, Vec<Offset>>;
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct Offset {
|
||||
pub name: String,
|
||||
pub value: u32,
|
||||
}
|
||||
|
||||
pub fn offsets(process: &mut IntoProcessInstanceArcBox<'_>) -> Result<OffsetMap> {
|
||||
let mut map = BTreeMap::new();
|
||||
|
||||
for (module_name, sigs) in CONFIG.signatures.iter().flatten() {
|
||||
let module = process.module_by_name(module_name)?;
|
||||
|
||||
let mut offsets: Vec<_> = sigs
|
||||
.iter()
|
||||
.filter_map(|sig| match read_offset(process, &module, sig) {
|
||||
Ok(offset) => Some(offset),
|
||||
Err(err) => {
|
||||
error!("{}", err);
|
||||
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
if !offsets.is_empty() {
|
||||
offsets.sort_unstable_by(|a, b| a.name.cmp(&b.name));
|
||||
|
||||
map.insert(module_name.to_string(), offsets);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(map)
|
||||
}
|
||||
|
||||
fn read_offset(
|
||||
process: &mut IntoProcessInstanceArcBox<'_>,
|
||||
module: &ModuleInfo,
|
||||
signature: &Signature,
|
||||
) -> Result<Offset> {
|
||||
let buf = process.read_raw(module.base, module.size as _)?;
|
||||
|
||||
let addr = skidscan::Signature::from_str(&signature.pattern)?
|
||||
.scan(&buf)
|
||||
.ok_or_else(|| Error::SignatureNotFound(signature.name.clone()))?;
|
||||
|
||||
let mut result = module.base + addr;
|
||||
|
||||
for op in &signature.operations {
|
||||
result = match op {
|
||||
Operation::Add { value } => result + *value,
|
||||
Operation::Rip { offset, len } => {
|
||||
let offset: i32 = process.read(result + offset.unwrap_or(3))?;
|
||||
|
||||
(result + offset) + len.unwrap_or(7)
|
||||
}
|
||||
Operation::Read => process.read_addr64(result)?,
|
||||
Operation::Slice { start, end } => {
|
||||
let buf = process.read_raw(result + *start, end - start)?;
|
||||
|
||||
let mut bytes = [0; mem::size_of::<usize>()];
|
||||
|
||||
bytes[..buf.len()].copy_from_slice(&buf);
|
||||
|
||||
usize::from_le_bytes(bytes).into()
|
||||
}
|
||||
Operation::Sub { value } => result - *value,
|
||||
};
|
||||
}
|
||||
|
||||
let value = (result - module.base)
|
||||
.try_into()
|
||||
.map_or_else(|_| result.to_umem() as u32, |v| v);
|
||||
|
||||
debug!("found offset: {} at {:#X}", signature.name, value);
|
||||
|
||||
Ok(Offset {
|
||||
name: signature.name.clone(),
|
||||
value,
|
||||
})
|
||||
}
|
337
src/analysis/schemas.rs
Normal file
337
src/analysis/schemas.rs
Normal file
@@ -0,0 +1,337 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::ffi::CStr;
|
||||
use std::ops::Add;
|
||||
use std::{env, mem};
|
||||
|
||||
use log::debug;
|
||||
|
||||
use memflow::prelude::v1::*;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use skidscan_macros::signature;
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::source_engine::*;
|
||||
|
||||
pub type SchemaMap = BTreeMap<String, (Vec<Class>, Vec<Enum>)>;
|
||||
|
||||
#[derive(Debug, Eq, Ord, PartialEq, PartialOrd, Deserialize, Serialize)]
|
||||
pub enum ClassMetadata {
|
||||
Unknown { name: String },
|
||||
NetworkChangeCallback { name: String },
|
||||
NetworkVarNames { name: String, ty: String },
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct Class {
|
||||
pub name: String,
|
||||
pub module_name: String,
|
||||
pub parent: Option<Box<Class>>,
|
||||
pub metadata: Vec<ClassMetadata>,
|
||||
pub fields: Vec<ClassField>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct ClassField {
|
||||
pub name: String,
|
||||
pub ty: String,
|
||||
pub offset: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct Enum {
|
||||
pub name: String,
|
||||
pub ty: String,
|
||||
pub alignment: u8,
|
||||
pub size: u16,
|
||||
pub members: Vec<EnumMember>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct EnumMember {
|
||||
pub name: String,
|
||||
pub value: i64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct TypeScope {
|
||||
pub name: String,
|
||||
pub classes: Vec<Class>,
|
||||
pub enums: Vec<Enum>,
|
||||
}
|
||||
|
||||
pub fn schemas(process: &mut IntoProcessInstanceArcBox<'_>) -> Result<SchemaMap> {
|
||||
let schema_system = read_schema_system(process)?;
|
||||
let type_scopes = read_type_scopes(process, &schema_system)?;
|
||||
|
||||
let map: BTreeMap<_, _> = type_scopes
|
||||
.into_iter()
|
||||
.map(|type_scope| (type_scope.name, (type_scope.classes, type_scope.enums)))
|
||||
.collect();
|
||||
|
||||
Ok(map)
|
||||
}
|
||||
|
||||
fn read_class_binding(
|
||||
process: &mut IntoProcessInstanceArcBox<'_>,
|
||||
binding_ptr: Pointer64<SchemaClassBinding>,
|
||||
) -> Result<Class> {
|
||||
let binding = process.read_ptr(binding_ptr)?;
|
||||
|
||||
let module_name = process
|
||||
.read_char_string(binding.module_name.address())
|
||||
.map(|s| {
|
||||
format!(
|
||||
"{}.{}",
|
||||
s,
|
||||
match env::consts::OS {
|
||||
"linux" => "so",
|
||||
"windows" => ".dll",
|
||||
_ => panic!("unsupported os"),
|
||||
}
|
||||
)
|
||||
})?;
|
||||
|
||||
let name = process.read_char_string(binding.name.address())?;
|
||||
|
||||
let parent = binding.base_classes.non_null().and_then(|ptr| {
|
||||
let base_class = process.read_ptr(ptr).ok()?;
|
||||
|
||||
read_class_binding(process, base_class.prev)
|
||||
.ok()
|
||||
.map(Box::new)
|
||||
});
|
||||
|
||||
let metadata = read_class_binding_metadata(process, &binding)?;
|
||||
let fields = read_class_binding_fields(process, &binding)?;
|
||||
|
||||
debug!(
|
||||
"found class: {} at {:#X} (module name: {}) (parent name: {:?}) (metadata count: {}) (fields count: {})",
|
||||
name,
|
||||
binding_ptr.to_umem(),
|
||||
module_name,
|
||||
parent.as_ref().map(|parent| parent.name.clone()),
|
||||
metadata.len(),
|
||||
fields.len()
|
||||
);
|
||||
|
||||
Ok(Class {
|
||||
name,
|
||||
module_name,
|
||||
parent,
|
||||
metadata,
|
||||
fields,
|
||||
})
|
||||
}
|
||||
|
||||
fn read_class_binding_fields(
|
||||
process: &mut IntoProcessInstanceArcBox<'_>,
|
||||
binding: &SchemaClassBinding,
|
||||
) -> Result<Vec<ClassField>> {
|
||||
(0..binding.fields_count)
|
||||
.map(|i| {
|
||||
let field_ptr: Pointer64<SchemaClassFieldData> = binding
|
||||
.fields
|
||||
.address()
|
||||
.add(i * mem::size_of::<SchemaClassFieldData>() as u16)
|
||||
.into();
|
||||
|
||||
let field = process.read_ptr(field_ptr)?;
|
||||
|
||||
if field.schema_type.is_null() {
|
||||
return Err(Error::Other("field schema type is null"));
|
||||
}
|
||||
|
||||
let name = process.read_char_string(field.name.address())?;
|
||||
let schema_type = process.read_ptr(field.schema_type)?;
|
||||
|
||||
// TODO: Parse this properly.
|
||||
let ty = process.read_char_string(schema_type.name.address())?;
|
||||
|
||||
Ok(ClassField {
|
||||
name,
|
||||
ty,
|
||||
offset: field.offset,
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn read_class_binding_metadata(
|
||||
process: &mut IntoProcessInstanceArcBox<'_>,
|
||||
binding: &SchemaClassBinding,
|
||||
) -> Result<Vec<ClassMetadata>> {
|
||||
if binding.static_metadata.is_null() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
(0..binding.static_metadata_count)
|
||||
.map(|i| {
|
||||
let metadata_ptr: Pointer64<SchemaMetadataEntryData> =
|
||||
binding.static_metadata.offset(i as _).into();
|
||||
|
||||
let metadata = process.read_ptr(metadata_ptr)?;
|
||||
|
||||
if metadata.network_value.is_null() {
|
||||
return Err(Error::Other("class metadata network value is null"));
|
||||
}
|
||||
|
||||
let name = process.read_char_string(metadata.name.address())?;
|
||||
let network_value = process.read_ptr(metadata.network_value)?;
|
||||
|
||||
let metadata = match name.as_str() {
|
||||
"MNetworkChangeCallback" => unsafe {
|
||||
let name =
|
||||
process.read_char_string(network_value.union_data.name_ptr.address())?;
|
||||
|
||||
ClassMetadata::NetworkChangeCallback { name }
|
||||
},
|
||||
"MNetworkVarNames" => unsafe {
|
||||
let var_value = network_value.union_data.var_value;
|
||||
|
||||
let name = process.read_char_string(var_value.name.address())?;
|
||||
let ty = process.read_char_string(var_value.ty.address())?;
|
||||
|
||||
ClassMetadata::NetworkVarNames { name, ty }
|
||||
},
|
||||
_ => ClassMetadata::Unknown { name },
|
||||
};
|
||||
|
||||
Ok(metadata)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn read_enum_binding(
|
||||
process: &mut IntoProcessInstanceArcBox<'_>,
|
||||
binding_ptr: Pointer64<SchemaEnumBinding>,
|
||||
) -> Result<Enum> {
|
||||
let binding = process.read_ptr(binding_ptr)?;
|
||||
let name = process.read_char_string(binding.name.address())?;
|
||||
|
||||
let members = read_enum_binding_members(process, &binding)?;
|
||||
|
||||
debug!(
|
||||
"found enum: {} at {:#X} (type name: {}) (alignment: {}) (members count: {})",
|
||||
name,
|
||||
binding_ptr.to_umem(),
|
||||
binding.type_name(),
|
||||
binding.alignment,
|
||||
binding.size,
|
||||
);
|
||||
|
||||
Ok(Enum {
|
||||
name,
|
||||
ty: binding.type_name().to_string(),
|
||||
alignment: binding.alignment,
|
||||
size: binding.size,
|
||||
members,
|
||||
})
|
||||
}
|
||||
|
||||
fn read_enum_binding_members(
|
||||
process: &mut IntoProcessInstanceArcBox<'_>,
|
||||
binding: &SchemaEnumBinding,
|
||||
) -> Result<Vec<EnumMember>> {
|
||||
(0..binding.size)
|
||||
.map(|i| {
|
||||
let enumerator_info_ptr: Pointer64<SchemaEnumeratorInfoData> = binding
|
||||
.enum_info
|
||||
.address()
|
||||
.add(i * mem::size_of::<SchemaEnumeratorInfoData>() as u16)
|
||||
.into();
|
||||
|
||||
let enumerator_info = process.read_ptr(enumerator_info_ptr)?;
|
||||
let name = process.read_char_string(enumerator_info.name.address())?;
|
||||
|
||||
let value = {
|
||||
let value = unsafe { enumerator_info.union_data.ulong } as i64;
|
||||
|
||||
if value == i64::MAX {
|
||||
-1
|
||||
} else {
|
||||
value
|
||||
}
|
||||
};
|
||||
|
||||
Ok(EnumMember { name, value })
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn read_schema_system(process: &mut IntoProcessInstanceArcBox<'_>) -> Result<SchemaSystem> {
|
||||
let (module_name, sig) = match env::consts::OS {
|
||||
"linux" => (
|
||||
"libschemasystem.so",
|
||||
signature!("48 8D 35 ? ? ? ? 48 8D 3D ? ? ? ? E8 ? ? ? ? 48 8D 15 ? ? ? ? 48 8D 35 ? ? ? ? 48 8D 3D"),
|
||||
),
|
||||
"windows" => (
|
||||
"schemasystem.dll",
|
||||
signature!("48 89 05 ? ? ? ? 4C 8D 45"),
|
||||
),
|
||||
_ => panic!("unsupported os"),
|
||||
};
|
||||
|
||||
let module = process.module_by_name(&module_name)?;
|
||||
let buf = process.read_raw(module.base, module.size as _)?;
|
||||
|
||||
let addr = sig
|
||||
.scan(&buf)
|
||||
.and_then(|ptr| process.read_addr64_rip(module.base + ptr).ok())
|
||||
.ok_or_else(|| Error::Other("unable to read schema system address"))?;
|
||||
|
||||
let schema_system: SchemaSystem = process.read(addr)?;
|
||||
|
||||
if schema_system.num_registrations == 0 {
|
||||
return Err(Error::Other("no schema system registrations found"));
|
||||
}
|
||||
|
||||
Ok(schema_system)
|
||||
}
|
||||
|
||||
fn read_type_scopes(
|
||||
process: &mut IntoProcessInstanceArcBox<'_>,
|
||||
schema_system: &SchemaSystem,
|
||||
) -> Result<Vec<TypeScope>> {
|
||||
let type_scopes = &schema_system.type_scopes;
|
||||
|
||||
(0..type_scopes.size)
|
||||
.map(|i| {
|
||||
let type_scope_ptr = type_scopes.get(process, i as _)?;
|
||||
let type_scope = process.read_ptr(type_scope_ptr)?;
|
||||
|
||||
let name = unsafe { CStr::from_ptr(type_scope.name.as_ptr()) }
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
|
||||
let classes: Vec<_> = type_scope
|
||||
.class_bindings
|
||||
.elements(process)?
|
||||
.iter()
|
||||
.filter_map(|ptr| read_class_binding(process, *ptr).ok())
|
||||
.collect();
|
||||
|
||||
let enums: Vec<_> = type_scope
|
||||
.enum_bindings
|
||||
.elements(process)?
|
||||
.iter()
|
||||
.filter_map(|ptr| read_enum_binding(process, *ptr).ok())
|
||||
.collect();
|
||||
|
||||
debug!(
|
||||
"found type scope: {} at {:#X} (classes count: {}) (enums count: {})",
|
||||
name,
|
||||
type_scope_ptr.to_umem(),
|
||||
classes.len(),
|
||||
enums.len()
|
||||
);
|
||||
|
||||
Ok(TypeScope {
|
||||
name,
|
||||
classes,
|
||||
enums,
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
@@ -1,53 +0,0 @@
|
||||
use std::io::{Result, Write};
|
||||
|
||||
use super::FileBuilder;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct CppFileBuilder;
|
||||
|
||||
impl FileBuilder for CppFileBuilder {
|
||||
fn extension(&mut self) -> &str {
|
||||
"hpp"
|
||||
}
|
||||
|
||||
fn write_top_level(&mut self, output: &mut dyn Write) -> Result<()> {
|
||||
writeln!(output, "#pragma once\n")?;
|
||||
writeln!(output, "#include <cstddef>\n")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_namespace(
|
||||
&mut self,
|
||||
output: &mut dyn Write,
|
||||
name: &str,
|
||||
comment: Option<&str>,
|
||||
) -> Result<()> {
|
||||
let comment = comment.map_or(String::new(), |c| format!(" // {}", c));
|
||||
|
||||
write!(output, "namespace {} {{{}\n", name, comment)
|
||||
}
|
||||
|
||||
fn write_variable(
|
||||
&mut self,
|
||||
output: &mut dyn Write,
|
||||
name: &str,
|
||||
value: usize,
|
||||
comment: Option<&str>,
|
||||
indentation: Option<usize>,
|
||||
) -> Result<()> {
|
||||
let indentation = " ".repeat(indentation.unwrap_or(4));
|
||||
|
||||
let comment = comment.map_or(String::new(), |c| format!(" // {}", c));
|
||||
|
||||
write!(
|
||||
output,
|
||||
"{}constexpr std::ptrdiff_t {} = {:#X};{}\n",
|
||||
indentation, name, value, comment
|
||||
)
|
||||
}
|
||||
|
||||
fn write_closure(&mut self, output: &mut dyn Write, eof: bool) -> Result<()> {
|
||||
write!(output, "{}", if eof { "}" } else { "}\n\n" })
|
||||
}
|
||||
}
|
@@ -1,50 +0,0 @@
|
||||
use std::io::{Result, Write};
|
||||
|
||||
use super::FileBuilder;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct CSharpFileBuilder;
|
||||
|
||||
impl FileBuilder for CSharpFileBuilder {
|
||||
fn extension(&mut self) -> &str {
|
||||
"cs"
|
||||
}
|
||||
|
||||
fn write_top_level(&mut self, _output: &mut dyn Write) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_namespace(
|
||||
&mut self,
|
||||
output: &mut dyn Write,
|
||||
name: &str,
|
||||
comment: Option<&str>,
|
||||
) -> Result<()> {
|
||||
let comment = comment.map_or(String::new(), |c| format!(" // {}", c));
|
||||
|
||||
write!(output, "public static class {} {{{}\n", name, comment)
|
||||
}
|
||||
|
||||
fn write_variable(
|
||||
&mut self,
|
||||
output: &mut dyn Write,
|
||||
name: &str,
|
||||
value: usize,
|
||||
comment: Option<&str>,
|
||||
indentation: Option<usize>,
|
||||
) -> Result<()> {
|
||||
let indentation = " ".repeat(indentation.unwrap_or(4));
|
||||
|
||||
let comment = comment.map_or(String::new(), |c| format!(" // {}", c));
|
||||
|
||||
write!(
|
||||
output,
|
||||
"{}public const nint {} = {:#X};{}\n",
|
||||
indentation, name, value, comment
|
||||
)
|
||||
}
|
||||
|
||||
fn write_closure(&mut self, output: &mut dyn Write, eof: bool) -> Result<()> {
|
||||
write!(output, "{}", if eof { "}" } else { "}\n\n" })
|
||||
}
|
||||
}
|
@@ -1,25 +0,0 @@
|
||||
use std::io::{Result, Write};
|
||||
|
||||
pub trait FileBuilder {
|
||||
fn extension(&mut self) -> &str;
|
||||
|
||||
fn write_top_level(&mut self, output: &mut dyn Write) -> Result<()>;
|
||||
|
||||
fn write_namespace(
|
||||
&mut self,
|
||||
output: &mut dyn Write,
|
||||
name: &str,
|
||||
comment: Option<&str>,
|
||||
) -> Result<()>;
|
||||
|
||||
fn write_variable(
|
||||
&mut self,
|
||||
output: &mut dyn Write,
|
||||
name: &str,
|
||||
value: usize,
|
||||
comment: Option<&str>,
|
||||
indentation: Option<usize>,
|
||||
) -> Result<()>;
|
||||
|
||||
fn write_closure(&mut self, output: &mut dyn Write, eof: bool) -> Result<()>;
|
||||
}
|
@@ -1,79 +0,0 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::io::{Result, Write};
|
||||
|
||||
use serde::Serialize;
|
||||
|
||||
use super::FileBuilder;
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Serialize)]
|
||||
struct JsonOffsetValue {
|
||||
value: usize,
|
||||
comment: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Serialize)]
|
||||
struct JsonModule {
|
||||
data: BTreeMap<String, JsonOffsetValue>,
|
||||
comment: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct JsonFileBuilder {
|
||||
data: BTreeMap<String, JsonModule>,
|
||||
current_namespace: String,
|
||||
}
|
||||
|
||||
impl FileBuilder for JsonFileBuilder {
|
||||
fn extension(&mut self) -> &str {
|
||||
"json"
|
||||
}
|
||||
|
||||
fn write_top_level(&mut self, _output: &mut dyn Write) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_namespace(
|
||||
&mut self,
|
||||
_output: &mut dyn Write,
|
||||
name: &str,
|
||||
comment: Option<&str>,
|
||||
) -> Result<()> {
|
||||
self.current_namespace = name.to_string();
|
||||
self.data.entry(name.to_string()).or_default().comment = comment.map(str::to_string);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_variable(
|
||||
&mut self,
|
||||
_output: &mut dyn Write,
|
||||
name: &str,
|
||||
value: usize,
|
||||
comment: Option<&str>,
|
||||
_indentation: Option<usize>,
|
||||
) -> Result<()> {
|
||||
self.data
|
||||
.entry(self.current_namespace.clone())
|
||||
.or_default()
|
||||
.data
|
||||
.insert(
|
||||
name.to_string(),
|
||||
JsonOffsetValue {
|
||||
value,
|
||||
comment: comment.map(str::to_string),
|
||||
},
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_closure(&mut self, output: &mut dyn Write, eof: bool) -> Result<()> {
|
||||
if eof {
|
||||
write!(output, "{}", serde_json::to_string_pretty(&self.data)?)?;
|
||||
|
||||
self.data.clear();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
@@ -1,75 +0,0 @@
|
||||
pub use cpp_file_builder::CppFileBuilder;
|
||||
pub use csharp_file_builder::CSharpFileBuilder;
|
||||
pub use file_builder::FileBuilder;
|
||||
pub use json_file_builder::JsonFileBuilder;
|
||||
pub use python_file_builder::PythonFileBuilder;
|
||||
pub use rust_file_builder::RustFileBuilder;
|
||||
pub use yaml_file_builder::YamlFileBuilder;
|
||||
|
||||
use std::io::{Result, Write};
|
||||
|
||||
pub mod cpp_file_builder;
|
||||
pub mod csharp_file_builder;
|
||||
pub mod file_builder;
|
||||
pub mod json_file_builder;
|
||||
pub mod python_file_builder;
|
||||
pub mod rust_file_builder;
|
||||
pub mod yaml_file_builder;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum FileBuilderEnum {
|
||||
CppFileBuilder(CppFileBuilder),
|
||||
CSharpFileBuilder(CSharpFileBuilder),
|
||||
JsonFileBuilder(JsonFileBuilder),
|
||||
PythonFileBuilder(PythonFileBuilder),
|
||||
RustFileBuilder(RustFileBuilder),
|
||||
YamlFileBuilder(YamlFileBuilder),
|
||||
}
|
||||
|
||||
impl FileBuilder for FileBuilderEnum {
|
||||
fn extension(&mut self) -> &str {
|
||||
self.as_mut().extension()
|
||||
}
|
||||
|
||||
fn write_top_level(&mut self, output: &mut dyn Write) -> Result<()> {
|
||||
self.as_mut().write_top_level(output)
|
||||
}
|
||||
|
||||
fn write_namespace(
|
||||
&mut self,
|
||||
output: &mut dyn Write,
|
||||
name: &str,
|
||||
comment: Option<&str>,
|
||||
) -> Result<()> {
|
||||
self.as_mut().write_namespace(output, name, comment)
|
||||
}
|
||||
|
||||
fn write_variable(
|
||||
&mut self,
|
||||
output: &mut dyn Write,
|
||||
name: &str,
|
||||
value: usize,
|
||||
comment: Option<&str>,
|
||||
indentation: Option<usize>,
|
||||
) -> Result<()> {
|
||||
self.as_mut()
|
||||
.write_variable(output, name, value, comment, indentation)
|
||||
}
|
||||
|
||||
fn write_closure(&mut self, output: &mut dyn Write, eof: bool) -> Result<()> {
|
||||
self.as_mut().write_closure(output, eof)
|
||||
}
|
||||
}
|
||||
|
||||
impl FileBuilderEnum {
|
||||
fn as_mut(&mut self) -> &mut dyn FileBuilder {
|
||||
match self {
|
||||
FileBuilderEnum::CppFileBuilder(builder) => builder,
|
||||
FileBuilderEnum::CSharpFileBuilder(builder) => builder,
|
||||
FileBuilderEnum::JsonFileBuilder(builder) => builder,
|
||||
FileBuilderEnum::PythonFileBuilder(builder) => builder,
|
||||
FileBuilderEnum::RustFileBuilder(builder) => builder,
|
||||
FileBuilderEnum::YamlFileBuilder(builder) => builder,
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,54 +0,0 @@
|
||||
use std::io::{Result, Write};
|
||||
|
||||
use super::FileBuilder;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct PythonFileBuilder;
|
||||
|
||||
impl FileBuilder for PythonFileBuilder {
|
||||
fn extension(&mut self) -> &str {
|
||||
"py"
|
||||
}
|
||||
|
||||
fn write_top_level(&mut self, _output: &mut dyn Write) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_namespace(
|
||||
&mut self,
|
||||
output: &mut dyn Write,
|
||||
name: &str,
|
||||
comment: Option<&str>,
|
||||
) -> Result<()> {
|
||||
let comment = comment.map_or(String::new(), |c| format!(" # {}", c));
|
||||
|
||||
write!(output, "class {}:{}\n", name, comment)
|
||||
}
|
||||
|
||||
fn write_variable(
|
||||
&mut self,
|
||||
output: &mut dyn Write,
|
||||
name: &str,
|
||||
value: usize,
|
||||
comment: Option<&str>,
|
||||
indentation: Option<usize>,
|
||||
) -> Result<()> {
|
||||
let indentation = " ".repeat(indentation.unwrap_or(4));
|
||||
|
||||
let comment = comment.map_or(String::new(), |c| format!(" # {}", c));
|
||||
|
||||
write!(
|
||||
output,
|
||||
"{}{} = {:#X}{}\n",
|
||||
indentation, name, value, comment
|
||||
)
|
||||
}
|
||||
|
||||
fn write_closure(&mut self, output: &mut dyn Write, eof: bool) -> Result<()> {
|
||||
if !eof {
|
||||
write!(output, "\n")?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
@@ -1,53 +0,0 @@
|
||||
use super::FileBuilder;
|
||||
|
||||
use std::io::{Result, Write};
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct RustFileBuilder;
|
||||
|
||||
impl FileBuilder for RustFileBuilder {
|
||||
fn extension(&mut self) -> &str {
|
||||
"rs"
|
||||
}
|
||||
|
||||
fn write_top_level(&mut self, output: &mut dyn Write) -> Result<()> {
|
||||
write!(
|
||||
output,
|
||||
"#![allow(non_snake_case, non_upper_case_globals)]\n\n"
|
||||
)
|
||||
}
|
||||
|
||||
fn write_namespace(
|
||||
&mut self,
|
||||
output: &mut dyn Write,
|
||||
name: &str,
|
||||
comment: Option<&str>,
|
||||
) -> Result<()> {
|
||||
let comment = comment.map_or(String::new(), |c| format!(" // {}", c));
|
||||
|
||||
write!(output, "pub mod {} {{{}\n", name, comment)
|
||||
}
|
||||
|
||||
fn write_variable(
|
||||
&mut self,
|
||||
output: &mut dyn Write,
|
||||
name: &str,
|
||||
value: usize,
|
||||
comment: Option<&str>,
|
||||
indentation: Option<usize>,
|
||||
) -> Result<()> {
|
||||
let indentation = " ".repeat(indentation.unwrap_or(4));
|
||||
|
||||
let comment = comment.map_or(String::new(), |c| format!(" // {}", c));
|
||||
|
||||
write!(
|
||||
output,
|
||||
"{}pub const {}: usize = {:#X};{}\n",
|
||||
indentation, name, value, comment
|
||||
)
|
||||
}
|
||||
|
||||
fn write_closure(&mut self, output: &mut dyn Write, eof: bool) -> Result<()> {
|
||||
write!(output, "{}", if eof { "}" } else { "}\n\n" })
|
||||
}
|
||||
}
|
@@ -1,46 +0,0 @@
|
||||
use std::io::{Result, Write};
|
||||
|
||||
use super::FileBuilder;
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct YamlFileBuilder;
|
||||
|
||||
impl FileBuilder for YamlFileBuilder {
|
||||
fn extension(&mut self) -> &str {
|
||||
"yaml"
|
||||
}
|
||||
|
||||
fn write_top_level(&mut self, output: &mut dyn Write) -> Result<()> {
|
||||
write!(output, "---\n")
|
||||
}
|
||||
|
||||
fn write_namespace(
|
||||
&mut self,
|
||||
output: &mut dyn Write,
|
||||
name: &str,
|
||||
comment: Option<&str>,
|
||||
) -> Result<()> {
|
||||
let comment = comment.map_or(String::new(), |c| format!(" # {}", c));
|
||||
|
||||
write!(output, "{}:{}\n", name, comment)
|
||||
}
|
||||
|
||||
fn write_variable(
|
||||
&mut self,
|
||||
output: &mut dyn Write,
|
||||
name: &str,
|
||||
value: usize,
|
||||
comment: Option<&str>,
|
||||
indentation: Option<usize>,
|
||||
) -> Result<()> {
|
||||
let indentation = " ".repeat(indentation.unwrap_or(4));
|
||||
|
||||
let comment = comment.map_or(String::new(), |c| format!(" # {}", c));
|
||||
|
||||
write!(output, "{}{}: {}{}\n", indentation, name, value, comment)
|
||||
}
|
||||
|
||||
fn write_closure(&mut self, _output: &mut dyn Write, _eof: bool) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
109
src/config.rs
109
src/config.rs
@@ -1,86 +1,61 @@
|
||||
use std::collections::HashMap;
|
||||
use std::sync::LazyLock;
|
||||
use std::{env, fs};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub static CONFIG: LazyLock<Config> = LazyLock::new(|| {
|
||||
let file_name = match env::consts::OS {
|
||||
"linux" => "config_linux.json",
|
||||
"windows" => "config_win.json",
|
||||
_ => panic!("unsupported os"),
|
||||
};
|
||||
|
||||
let content = fs::read_to_string(file_name).expect("unable to read config file");
|
||||
let config: Config = serde_json::from_str(&content).expect("unable to parse config file");
|
||||
|
||||
config
|
||||
});
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
#[serde(tag = "type", rename_all = "camelCase")]
|
||||
#[serde(rename_all = "snake_case", tag = "type")]
|
||||
pub enum Operation {
|
||||
Add {
|
||||
value: usize,
|
||||
},
|
||||
Deref {
|
||||
times: Option<usize>,
|
||||
size: Option<usize>,
|
||||
},
|
||||
Jmp {
|
||||
offset: Option<usize>,
|
||||
length: Option<usize>,
|
||||
},
|
||||
/// Adds the specified value to the current address.
|
||||
Add { value: usize },
|
||||
|
||||
/// Resolves the absolute address of a RIP-relative address.
|
||||
Rip {
|
||||
offset: Option<usize>,
|
||||
length: Option<usize>,
|
||||
},
|
||||
Slice {
|
||||
start: usize,
|
||||
end: usize,
|
||||
},
|
||||
Sub {
|
||||
value: usize,
|
||||
len: Option<usize>,
|
||||
},
|
||||
|
||||
/// Reads the value at the current address, treating it as a pointer.
|
||||
Read,
|
||||
|
||||
/// Extracts a range of bytes from the current address and interprets them as a value.
|
||||
Slice { start: usize, end: usize },
|
||||
|
||||
/// Subtracts the specified value from the current address.
|
||||
Sub { value: usize },
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct Config {
|
||||
pub signatures: Vec<Signature>,
|
||||
/// Name of the process.
|
||||
pub executable: String,
|
||||
|
||||
/// List of signatures to search for.
|
||||
pub signatures: Vec<HashMap<String, Vec<Signature>>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct Signature {
|
||||
/// Name of the signature.
|
||||
pub name: String,
|
||||
pub module: String,
|
||||
|
||||
/// An IDA-style pattern containing the bytes to search for.
|
||||
pub pattern: String,
|
||||
|
||||
/// List of operations to perform on the matched address.
|
||||
pub operations: Vec<Operation>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct SchemaSystemConfig {
|
||||
pub module_name: &'static str,
|
||||
pub pattern: &'static str,
|
||||
pub type_scope_size_offset: usize,
|
||||
pub type_scope_data_offset: usize,
|
||||
pub declared_classes_offset: usize,
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
pub const SCHEMA_CONF: SchemaSystemConfig = SchemaSystemConfig {
|
||||
module_name: "schemasystem.dll",
|
||||
pattern: "48 8D 0D ? ? ? ? E9 ? ? ? ? CC CC CC CC 48 8D 0D ? ? ? ? E9 ? ? ? ? CC CC CC CC 48 83 EC 28",
|
||||
type_scope_size_offset: 0x190,
|
||||
type_scope_data_offset: 0x198,
|
||||
declared_classes_offset: 0x5B8,
|
||||
};
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
pub const SCHEMA_CONF: SchemaSystemConfig = SchemaSystemConfig {
|
||||
module_name: "libschemasystem.so",
|
||||
pattern: "48 8D 05 ? ? ? ? c3 ? ? ? 00 00 00 00 00 48 8d 05 ? ? ? ? c3 ? ? ? 00 00 00 00 00 48 ? ? ? c3",
|
||||
type_scope_size_offset: 0x1f8,
|
||||
type_scope_data_offset: 0x200,
|
||||
declared_classes_offset: 0x620,
|
||||
};
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
pub const PROC_NAME: &str = "cs2.exe";
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
pub const PROC_NAME: &str = "cs2";
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
pub const OFFSETS_CONF: &str = "config.json";
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
pub const OFFSETS_CONF: &str = "config_linux.json";
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
pub const DEFAULT_OUT_DIR: &str = "generated";
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
pub const DEFAULT_OUT_DIR: &str = "generated_linux";
|
||||
|
@@ -1,110 +0,0 @@
|
||||
use std::ffi::c_char;
|
||||
use std::mem::offset_of;
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use simplelog::{debug, info};
|
||||
|
||||
use super::{generate_files, Entries, Entry};
|
||||
|
||||
use crate::builder::FileBuilderEnum;
|
||||
use crate::os::Process;
|
||||
|
||||
#[derive(Debug)]
|
||||
#[repr(C)]
|
||||
struct InterfaceNode {
|
||||
pub create_fn: *const (),
|
||||
pub name: *const c_char,
|
||||
pub next: *mut InterfaceNode,
|
||||
}
|
||||
|
||||
impl InterfaceNode {
|
||||
fn instance(&self, process: &Process) -> Result<usize> {
|
||||
process
|
||||
.read_memory::<usize>(
|
||||
(self as *const _ as usize + offset_of!(InterfaceNode, create_fn)).into(),
|
||||
)
|
||||
.map(|ptr| ptr.into())
|
||||
}
|
||||
|
||||
fn name(&self, process: &Process) -> Result<String> {
|
||||
let name_ptr = process.read_memory::<usize>(
|
||||
(self as *const _ as usize + offset_of!(InterfaceNode, name)).into(),
|
||||
)?;
|
||||
|
||||
process.read_string(name_ptr.into())
|
||||
}
|
||||
|
||||
fn next(&self, process: &Process) -> Result<*mut InterfaceNode> {
|
||||
process.read_memory::<*mut InterfaceNode>(
|
||||
(self as *const _ as usize + offset_of!(InterfaceNode, next)).into(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn dump_interfaces(
|
||||
process: &Process,
|
||||
builders: &mut Vec<FileBuilderEnum>,
|
||||
file_path: &str,
|
||||
indent: usize,
|
||||
) -> Result<()> {
|
||||
let mut entries = Entries::new();
|
||||
|
||||
for module in process
|
||||
.modules()?
|
||||
.iter()
|
||||
.filter(|m| m.name != "crashhandler64.dll")
|
||||
{
|
||||
if let Some(create_interface_export) = module.export_by_name("CreateInterface") {
|
||||
info!("Dumping interfaces in <blue>{}</>...", module.name);
|
||||
|
||||
let create_interface_address;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
create_interface_address =
|
||||
process.resolve_rip(create_interface_export, None, None)?;
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
let create_interface_fn =
|
||||
process.resolve_jmp(create_interface_export, None, None)?;
|
||||
create_interface_address =
|
||||
process.resolve_rip(create_interface_fn + 0x10, None, None)?;
|
||||
}
|
||||
|
||||
let mut node = process.read_memory::<*mut InterfaceNode>(create_interface_address)?;
|
||||
|
||||
while !node.is_null() {
|
||||
let instance = unsafe { (*node).instance(process) }?;
|
||||
let name = unsafe { (*node).name(process) }?;
|
||||
|
||||
debug!(
|
||||
"Found <bright-yellow>{}</> @ <bright-magenta>{:#X}</> (<blue>{}</> + <bright-blue>{:#X}</>)",
|
||||
name,
|
||||
instance,
|
||||
module.name,
|
||||
instance - module.base()
|
||||
);
|
||||
|
||||
let container = entries.entry(module.name.replace(".", "_")).or_default();
|
||||
|
||||
container.comment = Some(module.name.to_string());
|
||||
|
||||
container.data.push(Entry {
|
||||
name,
|
||||
value: (instance - module.base()).into(),
|
||||
comment: None,
|
||||
indent: Some(indent),
|
||||
});
|
||||
|
||||
node = unsafe { (*node).next(process) }?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
generate_files(builders, &entries, file_path, "interfaces")?;
|
||||
|
||||
Ok(())
|
||||
}
|
@@ -1,108 +0,0 @@
|
||||
pub use interfaces::dump_interfaces;
|
||||
pub use offsets::dump_offsets;
|
||||
pub use schemas::dump_schemas;
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use chrono::Utc;
|
||||
|
||||
use crate::builder::{FileBuilder, FileBuilderEnum};
|
||||
|
||||
pub mod interfaces;
|
||||
pub mod offsets;
|
||||
pub mod schemas;
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub struct Entry {
|
||||
pub name: String,
|
||||
pub value: usize,
|
||||
pub comment: Option<String>,
|
||||
pub indent: Option<usize>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct EntriesContainer {
|
||||
pub data: Vec<Entry>,
|
||||
pub comment: Option<String>,
|
||||
}
|
||||
|
||||
pub type Entries = BTreeMap<String, EntriesContainer>;
|
||||
|
||||
pub fn generate_file(
|
||||
builder: &mut FileBuilderEnum,
|
||||
entries: &Entries,
|
||||
file_path: &str,
|
||||
file_name: &str,
|
||||
) -> Result<()> {
|
||||
if entries.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let file_path = format!("{}/{}.{}", file_path, file_name, builder.extension());
|
||||
|
||||
let mut file = File::create(file_path)?;
|
||||
|
||||
write_banner_to_file(&mut file, builder.extension())?;
|
||||
|
||||
builder.write_top_level(&mut file)?;
|
||||
|
||||
let len = entries.len();
|
||||
|
||||
for (i, pair) in entries.iter().enumerate() {
|
||||
builder.write_namespace(&mut file, pair.0, pair.1.comment.as_deref())?;
|
||||
|
||||
pair.1.data.iter().try_for_each(|entry| {
|
||||
builder.write_variable(
|
||||
&mut file,
|
||||
&entry.name,
|
||||
entry.value,
|
||||
entry.comment.as_deref(),
|
||||
entry.indent,
|
||||
)
|
||||
})?;
|
||||
|
||||
builder.write_closure(&mut file, i == len - 1)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn generate_files(
|
||||
builders: &mut [FileBuilderEnum],
|
||||
entries: &Entries,
|
||||
file_path: &str,
|
||||
file_name: &str,
|
||||
) -> Result<()> {
|
||||
builders
|
||||
.iter_mut()
|
||||
.try_for_each(|builder| generate_file(builder, entries, file_path, file_name))
|
||||
}
|
||||
|
||||
fn write_banner_to_file(file: &mut File, file_extension: &str) -> Result<()> {
|
||||
const REPO_URL: &str = "https://github.com/a2x/cs2-dumper";
|
||||
|
||||
let time_now = Utc::now().to_rfc2822();
|
||||
|
||||
let banner = match file_extension {
|
||||
"json" => None,
|
||||
"py" => Some(format!(
|
||||
"'''\nGenerated using {}\n{}\n'''\n\n",
|
||||
REPO_URL, time_now
|
||||
)),
|
||||
"yaml" => None,
|
||||
_ => Some(format!(
|
||||
"/*\n * Generated using {}\n * {}\n */\n\n",
|
||||
REPO_URL, time_now
|
||||
)),
|
||||
};
|
||||
|
||||
if let Some(banner) = banner {
|
||||
write!(file, "{}", banner)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
@@ -1,420 +0,0 @@
|
||||
use std::fs::File;
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use simplelog::{debug, error, info};
|
||||
|
||||
use super::{generate_files, Entries, Entry};
|
||||
|
||||
use crate::builder::FileBuilderEnum;
|
||||
use crate::config::Operation::*;
|
||||
use crate::config::{self, Config};
|
||||
use crate::os::Process;
|
||||
|
||||
pub fn dump_offsets(
|
||||
process: &Process,
|
||||
builders: &mut Vec<FileBuilderEnum>,
|
||||
file_path: &str,
|
||||
indent: usize,
|
||||
) -> Result<()> {
|
||||
let file = File::open(config::OFFSETS_CONF)?;
|
||||
|
||||
let config: Config = serde_json::from_reader(file)?;
|
||||
|
||||
info!("Dumping offsets...");
|
||||
|
||||
let mut entries = Entries::new();
|
||||
|
||||
for signature in config.signatures {
|
||||
debug!("Searching for <bright-yellow>{}</>...", signature.name);
|
||||
|
||||
let module = process
|
||||
.get_module_by_name(&signature.module)
|
||||
.expect(&format!("Failed to find module {}.", signature.module));
|
||||
|
||||
let mut address = match process.find_pattern(&signature.module, &signature.pattern) {
|
||||
Some(a) => a,
|
||||
None => {
|
||||
error!(
|
||||
"Failed to find pattern for <bright-yellow>{}</>.",
|
||||
signature.name
|
||||
);
|
||||
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
for operation in signature.operations {
|
||||
match operation {
|
||||
Add { value } => address += value,
|
||||
Deref { times, size } => {
|
||||
let times = times.unwrap_or(1);
|
||||
let size = size.unwrap_or(8);
|
||||
|
||||
for _ in 0..times {
|
||||
process.read_memory_raw(address, &mut address as *mut _ as *mut _, size)?;
|
||||
}
|
||||
}
|
||||
Jmp { offset, length } => {
|
||||
address = process.resolve_jmp(address, offset, length)?.into();
|
||||
}
|
||||
Rip { offset, length } => {
|
||||
address = process.resolve_rip(address, offset, length)?.into()
|
||||
}
|
||||
Slice { start, end } => {
|
||||
let mut result: usize = 0;
|
||||
|
||||
process.read_memory_raw(
|
||||
address + start,
|
||||
&mut result as *mut _ as *mut _,
|
||||
end - start,
|
||||
)?;
|
||||
|
||||
address = result.into();
|
||||
}
|
||||
Sub { value } => address -= value,
|
||||
}
|
||||
}
|
||||
|
||||
let (name, value) = if address < module.base() {
|
||||
debug!(
|
||||
"Found <bright-yellow>{}</> @ <bright-blue>{:#X}</>",
|
||||
signature.name, address
|
||||
);
|
||||
|
||||
(signature.name, address)
|
||||
} else {
|
||||
debug!(
|
||||
"Found <bright-yellow>{}</> @ <bright-magenta>{:#X}</> (<blue>{}</> + <bright-blue>{:#X}</>)",
|
||||
signature.name,
|
||||
address,
|
||||
signature.module,
|
||||
address - module.base()
|
||||
);
|
||||
|
||||
(signature.name, address - module.base())
|
||||
};
|
||||
|
||||
if name == "dwBuildNumber" {
|
||||
let build_number = process.read_memory::<u32>(module.base() + value)?;
|
||||
|
||||
debug!("Game build number: <bright-yellow>{}</>", build_number);
|
||||
|
||||
let container = entries.entry("game_info".to_string()).or_default();
|
||||
|
||||
container.comment =
|
||||
Some("Some additional information about the game at dump time".to_string());
|
||||
|
||||
container.data.push(Entry {
|
||||
name: "buildNumber".to_string(),
|
||||
value: build_number as usize,
|
||||
comment: Some("Game build number".to_string()),
|
||||
indent: Some(indent),
|
||||
});
|
||||
}
|
||||
|
||||
let container = entries
|
||||
.entry(signature.module.replace(".", "_"))
|
||||
.or_default();
|
||||
|
||||
container.comment = Some(signature.module);
|
||||
|
||||
container.data.push(Entry {
|
||||
name,
|
||||
value,
|
||||
comment: None,
|
||||
indent: Some(indent),
|
||||
});
|
||||
}
|
||||
|
||||
generate_files(builders, &entries, file_path, "offsets")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
use std::ffi::{c_char, c_void};
|
||||
use std::fs;
|
||||
use std::mem::offset_of;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
use core::arch::x86_64::_bittest;
|
||||
|
||||
use anyhow::anyhow;
|
||||
|
||||
use serde_json::Value;
|
||||
|
||||
fn read_json_value(file_path: &str) -> Result<Value> {
|
||||
let content = fs::read_to_string(file_path)?;
|
||||
|
||||
serde_json::from_str(&content).map_err(Into::into)
|
||||
}
|
||||
|
||||
fn get_class_field(module_name: &str, class_name: &str, class_key: &str) -> Result<u64> {
|
||||
let value = read_json_value(&format!("generated/{}.json", module_name))
|
||||
.expect("unable to read json file");
|
||||
|
||||
value[class_name]["data"][class_key]["value"]
|
||||
.as_u64()
|
||||
.ok_or_else(|| {
|
||||
anyhow!(
|
||||
"unable to find class field {} in class {}",
|
||||
class_key,
|
||||
class_name
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn get_offset_value(module_name: &str, offset_name: &str) -> Result<u64> {
|
||||
let value = read_json_value("generated/offsets.json").expect("unable to read offsets.json");
|
||||
|
||||
value[module_name.replace(".", "_")]["data"][offset_name]["value"]
|
||||
.as_u64()
|
||||
.ok_or_else(|| anyhow!("unable to find offset"))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_number() -> Result<()> {
|
||||
let process = Process::new("cs2.exe")?;
|
||||
|
||||
let engine_base = process
|
||||
.get_module_by_name("engine2.dll")
|
||||
.expect("unable to find engine2.dll")
|
||||
.base();
|
||||
|
||||
let build_number_offset = get_offset_value("engine2.dll", "dwBuildNumber")?;
|
||||
|
||||
let build_number =
|
||||
process.read_memory::<u32>(engine_base + build_number_offset as usize)?;
|
||||
|
||||
println!("build number: {}", build_number);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn key_buttons() -> Result<()> {
|
||||
let process = Process::new("cs2.exe")?;
|
||||
|
||||
let client_base = process
|
||||
.get_module_by_name("client.dll")
|
||||
.expect("unable to find client.dll")
|
||||
.base();
|
||||
|
||||
const KEY_BUTTONS: [&str; 8] = [
|
||||
"dwForceAttack",
|
||||
"dwForceAttack2",
|
||||
"dwForceBackward",
|
||||
"dwForceCrouch",
|
||||
"dwForceForward",
|
||||
"dwForceJump",
|
||||
"dwForceLeft",
|
||||
"dwForceRight",
|
||||
];
|
||||
|
||||
let get_key_state = |value: u32| match value {
|
||||
256 => "key up",
|
||||
65537 => "key down",
|
||||
_ => "unknown",
|
||||
};
|
||||
|
||||
// Sleep for a second, so we're able to test.
|
||||
thread::sleep(Duration::from_secs(1));
|
||||
|
||||
for button in &KEY_BUTTONS {
|
||||
let offset = get_offset_value("client.dll", button).expect("unable to find client.dll");
|
||||
|
||||
let value = process.read_memory::<u32>(client_base + offset as usize)?;
|
||||
|
||||
println!("key button: {} (state: {})", button, get_key_state(value));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn global_vars() -> Result<()> {
|
||||
#[derive(Debug)]
|
||||
#[repr(C)]
|
||||
struct GlobalVarsBase {
|
||||
real_time: f32, // 0x0000
|
||||
frame_count: i32, // 0x0004
|
||||
frame_time: f32, // 0x0008
|
||||
absolute_frame_time: f32, // 0x000C
|
||||
max_clients: i32, // 0x0010
|
||||
pad_0: [u8; 0x14], // 0x0014
|
||||
frame_time_2: f32, // 0x0028
|
||||
current_time: f32, // 0x002C
|
||||
current_time_2: f32, // 0x0030
|
||||
pad_1: [u8; 0xC], // 0x0034
|
||||
tick_count: f32, // 0x0040
|
||||
pad_2: [u8; 0x4], // 0x0044
|
||||
network_channel: *const c_void, // 0x0048
|
||||
pad_3: [u8; 0x130], // 0x0050
|
||||
current_map: *const c_char, // 0x0180
|
||||
current_map_name: *const c_char, // 0x0188
|
||||
}
|
||||
|
||||
impl GlobalVarsBase {
|
||||
fn current_map(&self, process: &Process) -> Result<String> {
|
||||
let name_ptr = process.read_memory::<usize>(
|
||||
(self as *const _ as usize + offset_of!(Self, current_map)).into(),
|
||||
)?;
|
||||
|
||||
process.read_string(name_ptr.into())
|
||||
}
|
||||
|
||||
fn current_map_name(&self, process: &Process) -> Result<String> {
|
||||
let name_ptr = process.read_memory::<usize>(
|
||||
(self as *const _ as usize + offset_of!(Self, current_map_name)).into(),
|
||||
)?;
|
||||
|
||||
process.read_string(name_ptr.into())
|
||||
}
|
||||
}
|
||||
|
||||
let process = Process::new("cs2.exe")?;
|
||||
|
||||
let client_base = process
|
||||
.get_module_by_name("client.dll")
|
||||
.expect("unable to find client.dll")
|
||||
.base();
|
||||
|
||||
let global_vars_offset = get_offset_value("client.dll", "dwGlobalVars")?;
|
||||
|
||||
let global_vars = process
|
||||
.read_memory::<*const GlobalVarsBase>(client_base + global_vars_offset as usize)?;
|
||||
|
||||
let current_map_name = unsafe {
|
||||
(*global_vars)
|
||||
.current_map_name(&process)
|
||||
.unwrap_or_default()
|
||||
};
|
||||
|
||||
println!("current map name: {}", current_map_name);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_key_down() -> Result<()> {
|
||||
let process = Process::new("cs2.exe")?;
|
||||
|
||||
let input_system_base = process
|
||||
.get_module_by_name("inputsystem.dll")
|
||||
.expect("unable to find inputsystem.dll")
|
||||
.base();
|
||||
|
||||
let input_system_offset = get_offset_value("inputsystem.dll", "dwInputSystem")?;
|
||||
|
||||
let input_system = input_system_base + input_system_offset as usize;
|
||||
|
||||
let is_key_down = |key_code: i32| -> bool {
|
||||
let element = process
|
||||
.read_memory::<i32>((input_system + 0x4 * (key_code as usize / 32) + 0x12A0).into())
|
||||
.unwrap_or_default();
|
||||
|
||||
unsafe { _bittest(&element, key_code & 0x1F) != 0 }
|
||||
};
|
||||
|
||||
// Sleep for a second, so we're able to test.
|
||||
thread::sleep(Duration::from_secs(1));
|
||||
|
||||
// See https://www.unknowncheats.me/forum/3855779-post889.html for button codes.
|
||||
println!("insert key down: {}", is_key_down(73));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_player_controller() -> Result<()> {
|
||||
let process = Process::new("cs2.exe")?;
|
||||
|
||||
let client_base = process
|
||||
.get_module_by_name("client.dll")
|
||||
.expect("unable to find client.dll")
|
||||
.base();
|
||||
|
||||
let local_player_controller_offset =
|
||||
get_offset_value("client.dll", "dwLocalPlayerController")?;
|
||||
|
||||
let player_name_offset =
|
||||
get_class_field("client.dll", "CBasePlayerController", "m_iszPlayerName")?;
|
||||
|
||||
let local_player_controller =
|
||||
process.read_memory::<usize>(client_base + local_player_controller_offset as usize)?;
|
||||
|
||||
let player_name =
|
||||
process.read_string((local_player_controller + player_name_offset as usize).into())?;
|
||||
|
||||
println!("local player name: {}", player_name);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_player_pawn() -> Result<()> {
|
||||
#[derive(Debug)]
|
||||
#[repr(C)]
|
||||
struct Vector3D {
|
||||
x: f32,
|
||||
y: f32,
|
||||
z: f32,
|
||||
}
|
||||
|
||||
let process = Process::new("cs2.exe")?;
|
||||
|
||||
let client_base = process
|
||||
.get_module_by_name("client.dll")
|
||||
.expect("unable to find client.dll")
|
||||
.base();
|
||||
|
||||
let local_player_pawn_offset = get_offset_value("client.dll", "dwLocalPlayerPawn")?;
|
||||
|
||||
let game_scene_node_offset =
|
||||
get_class_field("client.dll", "C_BaseEntity", "m_pGameSceneNode")?;
|
||||
|
||||
let absolute_origin_offset =
|
||||
get_class_field("client.dll", "CGameSceneNode", "m_vecAbsOrigin")?;
|
||||
|
||||
let local_player_pawn =
|
||||
process.read_memory::<usize>(client_base + local_player_pawn_offset as usize)?;
|
||||
|
||||
let game_scene_node = process
|
||||
.read_memory::<usize>((local_player_pawn + game_scene_node_offset as usize).into())?;
|
||||
|
||||
let absolute_origin = process
|
||||
.read_memory::<Vector3D>((game_scene_node + absolute_origin_offset as usize).into())?;
|
||||
|
||||
println!("local player origin: {:?}", absolute_origin);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn window_size() -> Result<()> {
|
||||
let process = Process::new("cs2.exe")?;
|
||||
|
||||
let engine_base = process
|
||||
.get_module_by_name("engine2.dll")
|
||||
.expect("unable to find engine2.dll")
|
||||
.base();
|
||||
|
||||
let window_width_offset = get_offset_value("engine2.dll", "dwWindowWidth")?;
|
||||
let window_height_offset = get_offset_value("engine2.dll", "dwWindowHeight")?;
|
||||
|
||||
let window_width =
|
||||
process.read_memory::<u32>(engine_base + window_width_offset as usize)?;
|
||||
|
||||
let window_height =
|
||||
process.read_memory::<u32>(engine_base + window_height_offset as usize)?;
|
||||
|
||||
println!("window size: {}x{}", window_width, window_height);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
@@ -1,65 +0,0 @@
|
||||
use anyhow::Result;
|
||||
|
||||
use simplelog::{debug, info};
|
||||
|
||||
use super::{generate_files, Entries, Entry};
|
||||
|
||||
use crate::builder::FileBuilderEnum;
|
||||
use crate::os::Process;
|
||||
use crate::sdk::SchemaSystem;
|
||||
|
||||
pub fn dump_schemas(
|
||||
process: &Process,
|
||||
builders: &mut Vec<FileBuilderEnum>,
|
||||
file_path: &str,
|
||||
indent: usize,
|
||||
) -> Result<()> {
|
||||
let schema_system = SchemaSystem::new(&process)?;
|
||||
|
||||
for type_scope in schema_system.type_scopes()? {
|
||||
let module_name = type_scope.module_name()?;
|
||||
|
||||
info!("Generating files for <blue>{}</>...", module_name);
|
||||
|
||||
let mut entries = Entries::new();
|
||||
|
||||
for class in type_scope.classes()? {
|
||||
let parent_name = class.parent()?.map(|p| p.name().to_string());
|
||||
|
||||
debug!(
|
||||
"<u><bright-yellow>{}</></> : <u><yellow>{}</></>",
|
||||
class.name(),
|
||||
parent_name.clone().unwrap_or_default()
|
||||
);
|
||||
|
||||
let container = entries.entry(class.name().replace("::", "_")).or_default();
|
||||
|
||||
container.comment = parent_name;
|
||||
|
||||
for field in class.fields()? {
|
||||
let name = field.name()?;
|
||||
let offset = field.offset()?;
|
||||
let type_name = field.r#type()?.name()?;
|
||||
|
||||
debug!(
|
||||
"{}<bright-yellow>{}</> = <bright-blue>{:#X}</> // <b><cyan>{}</></>",
|
||||
" ".repeat(indent),
|
||||
name,
|
||||
offset,
|
||||
type_name
|
||||
);
|
||||
|
||||
container.data.push(Entry {
|
||||
name,
|
||||
value: offset as usize,
|
||||
comment: Some(type_name),
|
||||
indent: Some(indent),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
generate_files(builders, &entries, file_path, &module_name)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
44
src/error.rs
Normal file
44
src/error.rs
Normal file
@@ -0,0 +1,44 @@
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Error {
|
||||
#[error(transparent)]
|
||||
Fmt(#[from] std::fmt::Error),
|
||||
|
||||
#[error("index {idx} is out of bounds for array with length {len}")]
|
||||
IndexOutOfBounds { idx: usize, len: usize },
|
||||
|
||||
#[error(transparent)]
|
||||
Io(#[from] std::io::Error),
|
||||
|
||||
#[error(transparent)]
|
||||
Memflow(#[from] memflow::error::Error),
|
||||
|
||||
#[error(transparent)]
|
||||
Serde(#[from] serde_json::Error),
|
||||
|
||||
#[error("unable to parse signature")]
|
||||
SignatureInvalid,
|
||||
|
||||
#[error("unable to find signature for: {0}")]
|
||||
SignatureNotFound(String),
|
||||
|
||||
#[error("{0}")]
|
||||
Other(&'static str),
|
||||
}
|
||||
|
||||
impl<T> From<memflow::error::PartialError<T>> for Error {
|
||||
#[inline]
|
||||
fn from(err: memflow::error::PartialError<T>) -> Self {
|
||||
Error::Memflow(err.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<skidscan::SignatureParseError> for Error {
|
||||
#[inline]
|
||||
fn from(_err: skidscan::SignatureParseError) -> Self {
|
||||
Error::SignatureInvalid
|
||||
}
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
228
src/main.rs
228
src/main.rs
@@ -1,132 +1,148 @@
|
||||
#![allow(dead_code)]
|
||||
#![feature(lazy_cell)]
|
||||
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::time::Instant;
|
||||
use std::{env, fs};
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
use clap::*;
|
||||
|
||||
use clap::Parser;
|
||||
use log::{info, Level};
|
||||
|
||||
use log::LevelFilter;
|
||||
use memflow::prelude::v1::*;
|
||||
|
||||
use simplelog::{info, ColorChoice, ConfigBuilder, TermLogger, TerminalMode};
|
||||
use simplelog::{ColorChoice, TermLogger};
|
||||
|
||||
use builder::*;
|
||||
use dumper::{dump_interfaces, dump_offsets, dump_schemas};
|
||||
use os::Process;
|
||||
use config::CONFIG;
|
||||
use error::Result;
|
||||
use output::Results;
|
||||
|
||||
mod builder;
|
||||
mod analysis;
|
||||
mod config;
|
||||
mod dumper;
|
||||
mod os;
|
||||
mod sdk;
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
#[command(name = "cs2-dumper")]
|
||||
#[command(author = "a2x")]
|
||||
#[command(version = "1.1.5")]
|
||||
struct Args {
|
||||
/// Whether to dump interfaces.
|
||||
#[arg(short, long)]
|
||||
interfaces: bool,
|
||||
|
||||
/// Whether to dump offsets.
|
||||
#[arg(short, long)]
|
||||
offsets: bool,
|
||||
|
||||
/// Whether to dump schema classes.
|
||||
#[arg(short, long)]
|
||||
schemas: bool,
|
||||
|
||||
/// List of file builders to use.
|
||||
/// Valid values: `.cs`, `.hpp`, `.json`, `.py`, `.rs`, `.yaml`.
|
||||
#[arg(
|
||||
short,
|
||||
long,
|
||||
value_parser = map_file_extension_to_builder,
|
||||
value_delimiter = ',',
|
||||
default_values = [".cs", ".hpp", ".json", ".py", ".rs", ".yaml"],
|
||||
)]
|
||||
builders: Vec<FileBuilderEnum>,
|
||||
|
||||
/// Indentation level for generated files.
|
||||
/// Defaults to 4 spaces.
|
||||
#[arg(long, default_value_t = 4)]
|
||||
indent: usize,
|
||||
|
||||
/// Output directory for generated files.
|
||||
/// Defaults to `generated`.
|
||||
#[arg(long, default_value = config::DEFAULT_OUT_DIR)]
|
||||
output: String,
|
||||
|
||||
/// Enable verbose output.
|
||||
#[arg(short, long)]
|
||||
verbose: bool,
|
||||
}
|
||||
mod error;
|
||||
mod output;
|
||||
mod source_engine;
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let Args {
|
||||
interfaces,
|
||||
offsets,
|
||||
schemas,
|
||||
mut builders,
|
||||
indent,
|
||||
output,
|
||||
verbose,
|
||||
} = Args::parse();
|
||||
let start_time = Instant::now();
|
||||
|
||||
let log_level = if verbose {
|
||||
LevelFilter::Debug
|
||||
} else {
|
||||
LevelFilter::Info
|
||||
};
|
||||
|
||||
let config = ConfigBuilder::new().add_filter_ignore_str("goblin").build();
|
||||
|
||||
TermLogger::init(log_level, config, TerminalMode::Mixed, ColorChoice::Auto)?;
|
||||
|
||||
if !Path::new(config::OFFSETS_CONF).exists() {
|
||||
bail!("Missing {} file", config::OFFSETS_CONF);
|
||||
}
|
||||
let matches = parse_args();
|
||||
let (conn_name, conn_args, indent_size, out_dir) = extract_args(&matches)?;
|
||||
|
||||
// Create the output directory if it doesn't exist.
|
||||
fs::create_dir_all(&output)?;
|
||||
fs::create_dir_all(&out_dir)?;
|
||||
|
||||
let mut process = Process::new(config::PROC_NAME)?;
|
||||
let os = if let Some(conn_name) = conn_name {
|
||||
let inventory = Inventory::scan();
|
||||
|
||||
let now = Instant::now();
|
||||
let os_name = match env::consts::OS {
|
||||
"linux" => "linux",
|
||||
"windows" => "win32",
|
||||
_ => panic!("unsupported os"),
|
||||
};
|
||||
|
||||
let all = !(interfaces || offsets || schemas);
|
||||
inventory
|
||||
.builder()
|
||||
.connector(&conn_name)
|
||||
.args(conn_args)
|
||||
.os(os_name)
|
||||
.build()?
|
||||
} else {
|
||||
// Fallback to the native OS layer if no connector name was provided.
|
||||
memflow_native::create_os(&Default::default(), Default::default())?
|
||||
};
|
||||
|
||||
if schemas || all {
|
||||
dump_schemas(&mut process, &mut builders, &output, indent)?;
|
||||
}
|
||||
let mut process = os.into_process_by_name(&CONFIG.executable)?;
|
||||
|
||||
if interfaces || all {
|
||||
dump_interfaces(&mut process, &mut builders, &output, indent)?;
|
||||
}
|
||||
let buttons = analysis::buttons(&mut process)?;
|
||||
let interfaces = analysis::interfaces(&mut process)?;
|
||||
let offsets = analysis::offsets(&mut process)?;
|
||||
let schemas = analysis::schemas(&mut process)?;
|
||||
|
||||
if offsets || all {
|
||||
dump_offsets(&mut process, &mut builders, &output, indent)?;
|
||||
}
|
||||
let results = Results::new(buttons, interfaces, offsets, schemas);
|
||||
|
||||
info!(
|
||||
"<on-green>Done!</> <green>Time elapsed: <b>{:?}</></>",
|
||||
now.elapsed()
|
||||
);
|
||||
results.dump_all(&out_dir, indent_size)?;
|
||||
|
||||
info!("finished in {:?}", start_time.elapsed());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn map_file_extension_to_builder(extension: &str) -> Result<FileBuilderEnum, &'static str> {
|
||||
match extension {
|
||||
".cs" => Ok(FileBuilderEnum::CSharpFileBuilder(CSharpFileBuilder)),
|
||||
".hpp" => Ok(FileBuilderEnum::CppFileBuilder(CppFileBuilder)),
|
||||
".json" => Ok(FileBuilderEnum::JsonFileBuilder(JsonFileBuilder::default())),
|
||||
".py" => Ok(FileBuilderEnum::PythonFileBuilder(PythonFileBuilder)),
|
||||
".rs" => Ok(FileBuilderEnum::RustFileBuilder(RustFileBuilder)),
|
||||
".yaml" => Ok(FileBuilderEnum::YamlFileBuilder(YamlFileBuilder)),
|
||||
_ => Err("Invalid file extension"),
|
||||
}
|
||||
fn parse_args() -> ArgMatches {
|
||||
Command::new("cs2-dumper")
|
||||
.version(crate_version!())
|
||||
.author(crate_authors!())
|
||||
.arg(
|
||||
Arg::new("verbose")
|
||||
.help("Increase logging verbosity. Can be specified multiple times.")
|
||||
.short('v')
|
||||
.action(ArgAction::Count),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("connector")
|
||||
.help("The name of the memflow connector to use.")
|
||||
.long("connector")
|
||||
.short('c')
|
||||
.required(false),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("connector-args")
|
||||
.help("Additional arguments to supply to the connector.")
|
||||
.long("connector-args")
|
||||
.short('a')
|
||||
.required(false),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("output")
|
||||
.help("The output directory to write the generated files to.")
|
||||
.long("output")
|
||||
.short('o')
|
||||
.default_value("output")
|
||||
.value_parser(value_parser!(PathBuf))
|
||||
.required(false),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("indent-size")
|
||||
.help("The number of spaces to use per indentation level.")
|
||||
.long("indent-size")
|
||||
.short('i')
|
||||
.default_value("4")
|
||||
.value_parser(value_parser!(usize))
|
||||
.required(false),
|
||||
)
|
||||
.get_matches()
|
||||
}
|
||||
|
||||
fn extract_args(matches: &ArgMatches) -> Result<(Option<String>, ConnectorArgs, usize, &PathBuf)> {
|
||||
use std::str::FromStr;
|
||||
|
||||
let log_level = match matches.get_count("verbose") {
|
||||
0 => Level::Error,
|
||||
1 => Level::Warn,
|
||||
2 => Level::Info,
|
||||
3 => Level::Debug,
|
||||
4 => Level::Trace,
|
||||
_ => Level::Trace,
|
||||
};
|
||||
|
||||
TermLogger::init(
|
||||
log_level.to_level_filter(),
|
||||
Default::default(),
|
||||
Default::default(),
|
||||
ColorChoice::Auto,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let conn_name = matches
|
||||
.get_one::<String>("connector")
|
||||
.map(|s| s.to_string());
|
||||
|
||||
let conn_args = matches
|
||||
.get_one::<String>("connector-args")
|
||||
.map(|s| ConnectorArgs::from_str(&s).expect("unable to parse connector arguments"))
|
||||
.unwrap_or_default();
|
||||
|
||||
let indent_size = *matches.get_one::<usize>("indent-size").unwrap();
|
||||
let out_dir = matches.get_one::<PathBuf>("output").unwrap();
|
||||
|
||||
Ok((conn_name, conn_args, indent_size, out_dir))
|
||||
}
|
||||
|
@@ -1,8 +0,0 @@
|
||||
#[cfg(target_os = "linux")]
|
||||
pub use module::ModuleEntry;
|
||||
|
||||
pub use module::Module;
|
||||
pub use process::Process;
|
||||
|
||||
pub mod module;
|
||||
pub mod process;
|
188
src/os/module.rs
188
src/os/module.rs
@@ -1,188 +0,0 @@
|
||||
use anyhow::Result;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
use goblin::pe::{
|
||||
export::Export, import::Import, options::ParseOptions, section_table::SectionTable, PE,
|
||||
};
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
use goblin::elf::{sym, Elf, SectionHeader};
|
||||
#[cfg(target_os = "linux")]
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// Represents the data associated with a specific module on Linux.
|
||||
#[cfg(target_os = "linux")]
|
||||
#[derive(Debug)]
|
||||
pub struct ModuleEntry {
|
||||
pub path: PathBuf,
|
||||
pub start_addr: usize,
|
||||
pub data: Vec<u8>,
|
||||
}
|
||||
|
||||
/// Represents a module loaded in a Windows process.
|
||||
#[cfg(target_os = "windows")]
|
||||
pub struct Module<'a> {
|
||||
/// The name of the module.
|
||||
pub name: &'a str,
|
||||
|
||||
/// A reference to a slice of bytes containing the module data.
|
||||
pub data: &'a [u8],
|
||||
|
||||
/// The PE file format representation of the module.
|
||||
pub pe: PE<'a>,
|
||||
}
|
||||
|
||||
/// Represents a module loaded in a Linux process.
|
||||
#[cfg(target_os = "linux")]
|
||||
pub struct Module<'a> {
|
||||
/// The name of the module.
|
||||
pub name: &'a str,
|
||||
|
||||
/// A reference to a slice of bytes containing the module info.
|
||||
pub module_info: &'a ModuleEntry,
|
||||
|
||||
/// The Elf file format representation of the module.
|
||||
pub elf: Elf<'a>,
|
||||
}
|
||||
|
||||
impl<'a> Module<'a> {
|
||||
#[cfg(target_os = "windows")]
|
||||
pub fn parse(name: &'a str, data: &'a [u8]) -> Result<Self> {
|
||||
let pe = PE::parse_with_opts(
|
||||
data,
|
||||
&ParseOptions {
|
||||
parse_attribute_certificates: false,
|
||||
resolve_rva: false,
|
||||
},
|
||||
)?;
|
||||
|
||||
Ok(Self { name, data, pe })
|
||||
}
|
||||
|
||||
// parse the elf
|
||||
#[cfg(target_os = "linux")]
|
||||
pub fn parse(name: &'a str, module_entry: &'a ModuleEntry) -> Result<Self> {
|
||||
let elf = Elf::parse(&module_entry.data)?;
|
||||
Ok(Self {
|
||||
name,
|
||||
module_info: module_entry,
|
||||
elf,
|
||||
})
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[cfg(target_os = "windows")]
|
||||
pub fn base(&self) -> usize {
|
||||
self.pe.image_base
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[cfg(target_os = "linux")]
|
||||
pub fn base(&self) -> usize {
|
||||
self.module_info.start_addr
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[cfg(target_os = "windows")]
|
||||
pub fn exports(&self) -> &[Export] {
|
||||
&self.pe.exports
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[cfg(target_os = "linux")]
|
||||
pub fn exports(&self) -> Vec<sym::Sym> {
|
||||
let exports: Vec<sym::Sym> = self
|
||||
.elf
|
||||
.dynsyms
|
||||
.iter()
|
||||
.filter(|sym| sym.st_bind() == sym::STB_GLOBAL || sym.st_bind() == sym::STB_WEAK)
|
||||
.collect();
|
||||
exports
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[cfg(target_os = "windows")]
|
||||
pub fn imports(&self) -> &[Import] {
|
||||
&self.pe.imports
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[cfg(target_os = "linux")]
|
||||
pub fn imports(&self) -> Vec<sym::Sym> {
|
||||
let imports: Vec<sym::Sym> = self
|
||||
.elf
|
||||
.dynsyms
|
||||
.iter()
|
||||
.filter(|sym| sym.is_import())
|
||||
.collect();
|
||||
imports
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[cfg(target_os = "windows")]
|
||||
pub fn export_by_name(&self, name: &str) -> Option<usize> {
|
||||
self.pe
|
||||
.exports
|
||||
.iter()
|
||||
.find(|e| e.name.unwrap() == name)
|
||||
.map(|e| self.pe.image_base + e.rva)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[cfg(target_os = "linux")]
|
||||
pub fn export_by_name(&self, name: &str) -> Option<usize> {
|
||||
let base_addr: usize = self.base();
|
||||
self.elf
|
||||
.dynsyms
|
||||
.iter()
|
||||
.find(|sym| {
|
||||
(sym.st_bind() == sym::STB_GLOBAL || sym.st_bind() == sym::STB_WEAK)
|
||||
&& self.elf.dynstrtab.get_at(sym.st_name) == Some(name)
|
||||
})
|
||||
.map(|sym| (base_addr as u64 + sym.st_value) as usize)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[cfg(target_os = "windows")]
|
||||
pub fn import_by_name(&self, name: &str) -> Option<usize> {
|
||||
self.pe
|
||||
.imports
|
||||
.iter()
|
||||
.find(|i| i.name.to_string() == name)
|
||||
.map(|i| self.pe.image_base + i.rva)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[cfg(target_os = "linux")]
|
||||
pub fn get_import_by_name(&self, name: &str) -> Option<usize> {
|
||||
let base_addr: usize = self.base().into();
|
||||
self.elf
|
||||
.dynsyms
|
||||
.iter()
|
||||
.find(|sym| sym.is_import() && self.elf.dynstrtab.get_at(sym.st_name) == Some(name))
|
||||
.map(|sym| (base_addr as u64 + sym.st_value) as usize)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[cfg(target_os = "windows")]
|
||||
pub fn sections(&self) -> &[SectionTable] {
|
||||
&self.pe.sections
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[cfg(target_os = "linux")]
|
||||
pub fn sections(&self) -> &[SectionHeader] {
|
||||
self.elf.section_headers.as_slice()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[cfg(target_os = "windows")]
|
||||
pub fn size(&self) -> u32 {
|
||||
self.pe
|
||||
.header
|
||||
.optional_header
|
||||
.expect("optional header not found")
|
||||
.windows_fields
|
||||
.size_of_image
|
||||
}
|
||||
}
|
@@ -1,389 +0,0 @@
|
||||
use super::Module;
|
||||
#[cfg(target_os = "linux")]
|
||||
use super::ModuleEntry;
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::ffi::c_void;
|
||||
use std::mem;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
use std::ffi::CStr;
|
||||
#[cfg(target_os = "windows")]
|
||||
use std::ptr;
|
||||
#[cfg(target_os = "windows")]
|
||||
use windows::Win32::{
|
||||
Foundation::{CloseHandle, HANDLE},
|
||||
System::Diagnostics::Debug::ReadProcessMemory,
|
||||
System::Diagnostics::ToolHelp::*,
|
||||
System::Threading::{OpenProcess, PROCESS_ALL_ACCESS},
|
||||
};
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
use procfs::process::{self, all_processes};
|
||||
#[cfg(target_os = "linux")]
|
||||
use std::fs::File;
|
||||
#[cfg(target_os = "linux")]
|
||||
use std::io::{Read, Seek, SeekFrom};
|
||||
#[cfg(target_os = "linux")]
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
/// Represents a Windows process.
|
||||
#[cfg(target_os = "windows")]
|
||||
#[derive(Debug)]
|
||||
pub struct Process {
|
||||
/// ID of the process.
|
||||
id: u32,
|
||||
|
||||
/// Handle to the process.
|
||||
handle: HANDLE,
|
||||
|
||||
/// A HashMap containing the name of each module and its corresponding raw data.
|
||||
modules: HashMap<String, Vec<u8>>,
|
||||
}
|
||||
|
||||
/// Represents a Linux process.
|
||||
#[cfg(target_os = "linux")]
|
||||
#[derive(Debug)]
|
||||
pub struct Process {
|
||||
/// PID of the process.
|
||||
pid: u32,
|
||||
|
||||
/// A HashMap containing the name of each module and its corresponding data.
|
||||
modules: HashMap<String, ModuleEntry>,
|
||||
}
|
||||
|
||||
impl Process {
|
||||
#[cfg(target_os = "windows")]
|
||||
pub fn new(name: &str) -> Result<Self> {
|
||||
let id = Self::get_process_id_by_name(name)?;
|
||||
|
||||
let handle = unsafe { OpenProcess(PROCESS_ALL_ACCESS, false, id) }?;
|
||||
|
||||
let mut process = Self {
|
||||
id,
|
||||
handle,
|
||||
modules: HashMap::new(),
|
||||
};
|
||||
process.parse_loaded_modules()?;
|
||||
Ok(process)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
pub fn new(name: &str) -> Result<Self> {
|
||||
let pid = Self::get_process_pid_by_name(name)?;
|
||||
let mut process = Self {
|
||||
pid,
|
||||
modules: HashMap::new(),
|
||||
};
|
||||
process.parse_loaded_modules()?;
|
||||
Ok(process)
|
||||
}
|
||||
#[cfg(target_os = "windows")]
|
||||
pub fn find_pattern(&self, module_name: &str, pattern: &str) -> Option<usize> {
|
||||
let module = self.get_module_by_name(module_name)?;
|
||||
|
||||
let pattern_bytes = Self::pattern_to_bytes(pattern);
|
||||
|
||||
for (i, window) in module.data.windows(pattern_bytes.len()).enumerate() {
|
||||
if window
|
||||
.iter()
|
||||
.zip(&pattern_bytes)
|
||||
.all(|(&x, &y)| x == y as u8 || y == -1)
|
||||
{
|
||||
return Some(module.base() + i);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
pub fn find_pattern(&self, module_name: &str, pattern: &str) -> Option<usize> {
|
||||
let module = self.get_module_by_name(module_name)?;
|
||||
|
||||
let pattern_bytes = Self::pattern_to_bytes(pattern);
|
||||
|
||||
for (i, window) in module
|
||||
.module_info
|
||||
.data
|
||||
.windows(pattern_bytes.len())
|
||||
.enumerate()
|
||||
{
|
||||
if window
|
||||
.iter()
|
||||
.zip(&pattern_bytes)
|
||||
.all(|(&x, &y)| x == y as u8 || y == -1)
|
||||
{
|
||||
return Some(module.base() + i);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
pub fn get_module_by_name<'a>(&'a self, name: &'a str) -> Option<Module<'a>> {
|
||||
self.modules
|
||||
.get(name)
|
||||
.map(|data| Module::parse(name, data).unwrap())
|
||||
}
|
||||
|
||||
pub fn modules(&self) -> Result<Vec<Module>> {
|
||||
let mut modules = Vec::new();
|
||||
|
||||
for (name, data) in &self.modules {
|
||||
modules.push(Module::parse(name, data)?);
|
||||
}
|
||||
|
||||
Ok(modules)
|
||||
}
|
||||
|
||||
pub fn read_memory<T>(&self, address: usize) -> Result<T> {
|
||||
let mut buffer: T = unsafe { mem::zeroed() };
|
||||
|
||||
self.read_memory_raw(
|
||||
address,
|
||||
&mut buffer as *const _ as *mut _,
|
||||
mem::size_of::<T>(),
|
||||
)?;
|
||||
|
||||
Ok(buffer)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
pub fn read_memory_raw(&self, address: usize, buffer: *mut c_void, size: usize) -> Result<()> {
|
||||
unsafe {
|
||||
ReadProcessMemory(
|
||||
self.handle,
|
||||
address as *mut _,
|
||||
buffer,
|
||||
size,
|
||||
Some(ptr::null_mut()),
|
||||
)
|
||||
}
|
||||
.map_err(|e| e.into())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
pub fn read_memory_raw(&self, address: usize, buffer: *mut c_void, size: usize) -> Result<()> {
|
||||
let proc_mem_path = format!("/proc/{}/mem", self.pid);
|
||||
let mut mem_file = File::open(proc_mem_path)?;
|
||||
|
||||
// Go to the start address
|
||||
mem_file.seek(SeekFrom::Start(address as u64))?;
|
||||
|
||||
let buffer_slice = unsafe { std::slice::from_raw_parts_mut(buffer as *mut u8, size) };
|
||||
|
||||
// Try to read the data
|
||||
mem_file.read_exact(buffer_slice)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn read_string(&self, address: usize) -> Result<String> {
|
||||
let mut buffer = Vec::new();
|
||||
|
||||
for i in 0.. {
|
||||
match self.read_memory::<u8>(address + i) {
|
||||
Ok(byte) if byte != 0 => buffer.push(byte),
|
||||
_ => break,
|
||||
}
|
||||
}
|
||||
|
||||
Ok(String::from_utf8(buffer)?)
|
||||
}
|
||||
|
||||
pub fn read_string_length(&self, address: usize, length: usize) -> Result<String> {
|
||||
let mut buffer = vec![0; length];
|
||||
|
||||
self.read_memory_raw(address, buffer.as_mut_ptr() as *mut _, length)?;
|
||||
|
||||
if let Some(end) = buffer.iter().position(|&x| x == 0) {
|
||||
buffer.truncate(end);
|
||||
}
|
||||
|
||||
Ok(String::from_utf8(buffer)?)
|
||||
}
|
||||
|
||||
pub fn resolve_jmp(
|
||||
&self,
|
||||
address: usize,
|
||||
offset: Option<usize>,
|
||||
length: Option<usize>,
|
||||
) -> Result<usize> {
|
||||
// The displacement value can be negative.
|
||||
let displacement = self.read_memory::<i32>(address + offset.unwrap_or(0x1))?;
|
||||
|
||||
let final_address = if displacement.is_negative() {
|
||||
address - displacement.wrapping_abs() as usize
|
||||
} else {
|
||||
address + displacement as usize
|
||||
} + length.unwrap_or(0x5);
|
||||
Ok(final_address)
|
||||
}
|
||||
|
||||
pub fn resolve_rip(
|
||||
&self,
|
||||
address: usize,
|
||||
offset: Option<usize>,
|
||||
length: Option<usize>,
|
||||
) -> Result<usize> {
|
||||
// The displacement value can be negative.
|
||||
let displacement = self.read_memory::<i32>(address + offset.unwrap_or(0x3))?;
|
||||
|
||||
let final_address = if displacement.is_negative() {
|
||||
address - displacement.wrapping_abs() as usize
|
||||
} else {
|
||||
address + displacement as usize
|
||||
} + length.unwrap_or(0x7);
|
||||
Ok(final_address)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn get_process_id_by_name(process_name: &str) -> Result<u32> {
|
||||
let snapshot = unsafe { CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0) }?;
|
||||
|
||||
let mut entry = PROCESSENTRY32 {
|
||||
dwSize: mem::size_of::<PROCESSENTRY32>() as u32,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
unsafe {
|
||||
Process32First(snapshot, &mut entry)?;
|
||||
|
||||
while Process32Next(snapshot, &mut entry).is_ok() {
|
||||
let name = CStr::from_ptr(&entry.szExeFile as *const _ as *const _).to_str()?;
|
||||
|
||||
if name == process_name {
|
||||
return Ok(entry.th32ProcessID);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bail!("Process not found: {}", process_name)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn get_process_pid_by_name(process_name: &str) -> Result<u32> {
|
||||
use std::io::{BufRead, BufReader};
|
||||
|
||||
for process_iter in all_processes()? {
|
||||
let Ok(process) = process_iter else { continue };
|
||||
let comm_path = format!("/proc/{}/comm", process.pid());
|
||||
if let Ok(comm_file) = File::open(Path::new(&comm_path)) {
|
||||
let mut comm = String::new();
|
||||
if BufReader::new(comm_file).read_line(&mut comm).is_ok() {
|
||||
comm.pop();
|
||||
if comm == process_name && process.pid() > 0 {
|
||||
return Ok(process.pid() as u32);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
bail!("Process not found: {}", process_name);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn parse_loaded_modules(&mut self) -> Result<()> {
|
||||
let snapshot = unsafe { CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, self.id) }?;
|
||||
|
||||
let mut entry = MODULEENTRY32 {
|
||||
dwSize: mem::size_of::<MODULEENTRY32>() as u32,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
unsafe {
|
||||
Module32First(snapshot, &mut entry)?;
|
||||
|
||||
while Module32Next(snapshot, &mut entry).is_ok() {
|
||||
let name = CStr::from_ptr(&entry.szModule as *const _ as *const _).to_str()?;
|
||||
|
||||
let mut data = vec![0; entry.modBaseSize as usize];
|
||||
|
||||
if let Ok(_) = self.read_memory_raw(
|
||||
entry.modBaseAddr as _,
|
||||
data.as_mut_ptr() as *mut _,
|
||||
data.len(),
|
||||
) {
|
||||
self.modules.insert(name.to_string(), data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn parse_loaded_modules(&mut self) -> Result<()> {
|
||||
let process = process::Process::new(self.pid as i32)?;
|
||||
|
||||
let mut modules_info: HashMap<String, ((u64, u64), PathBuf)> = HashMap::new();
|
||||
|
||||
for mmap in process.maps()? {
|
||||
let module_path = match mmap.pathname {
|
||||
process::MMapPath::Path(path) => path,
|
||||
_ => continue,
|
||||
};
|
||||
let get_module_name = |path: &PathBuf| -> Option<String> {
|
||||
path.file_name()
|
||||
.and_then(|name| name.to_str())
|
||||
.filter(|name| name.starts_with("lib") && name.ends_with(".so"))
|
||||
.map(|name| name.to_string())
|
||||
};
|
||||
if let Some(module_name) = get_module_name(&module_path) {
|
||||
let module_entry = modules_info
|
||||
.entry(module_name)
|
||||
.or_insert_with(|| (mmap.address, module_path));
|
||||
module_entry.0 = (
|
||||
std::cmp::min(mmap.address.0, module_entry.0 .0),
|
||||
std::cmp::max(mmap.address.1, module_entry.0 .1),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for (module_name, (address_space, path)) in modules_info.into_iter() {
|
||||
let (start, end) = address_space;
|
||||
let read_elf_file = |path: &PathBuf| -> Result<Vec<u8>> {
|
||||
let mut file = File::open(path)?;
|
||||
let mut data = Vec::new();
|
||||
file.read_to_end(&mut data)?;
|
||||
|
||||
Ok(data)
|
||||
};
|
||||
if let Ok(data) = read_elf_file(&path) {
|
||||
self.modules.insert(
|
||||
module_name,
|
||||
ModuleEntry {
|
||||
path: path.clone(),
|
||||
start_addr: start as usize,
|
||||
data: data,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn pattern_to_bytes(pattern: &str) -> Vec<i32> {
|
||||
pattern
|
||||
.split_whitespace()
|
||||
.map(|s| {
|
||||
if s == "?" {
|
||||
-1
|
||||
} else {
|
||||
i32::from_str_radix(s, 16).unwrap_or(0)
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
impl Drop for Process {
|
||||
fn drop(&mut self) {
|
||||
if !self.handle.is_invalid() {
|
||||
unsafe { CloseHandle(self.handle).unwrap() }
|
||||
}
|
||||
}
|
||||
}
|
91
src/output/buttons.rs
Normal file
91
src/output/buttons.rs
Normal 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
78
src/output/formatter.rs
Normal 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
109
src/output/interfaces.rs
Normal 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
178
src/output/mod.rs
Normal 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
109
src/output/offsets.rs
Normal 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
313
src/output/schemas.rs
Normal 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(())
|
||||
}
|
@@ -1,15 +0,0 @@
|
||||
pub use schema_class_field_data::SchemaClassFieldData;
|
||||
pub use schema_class_info::SchemaClassInfo;
|
||||
pub use schema_system::SchemaSystem;
|
||||
pub use schema_system_type_scope::SchemaSystemTypeScope;
|
||||
pub use schema_type::SchemaType;
|
||||
pub use schema_type_declared_class::SchemaTypeDeclaredClass;
|
||||
pub use utl_ts_hash::UtlTsHash;
|
||||
|
||||
pub mod schema_class_field_data;
|
||||
pub mod schema_class_info;
|
||||
pub mod schema_system;
|
||||
pub mod schema_system_type_scope;
|
||||
pub mod schema_type;
|
||||
pub mod schema_type_declared_class;
|
||||
pub mod utl_ts_hash;
|
@@ -1,32 +0,0 @@
|
||||
use anyhow::Result;
|
||||
|
||||
use super::SchemaType;
|
||||
|
||||
use crate::os::Process;
|
||||
|
||||
pub struct SchemaClassFieldData<'a> {
|
||||
process: &'a Process,
|
||||
address: usize,
|
||||
}
|
||||
|
||||
impl<'a> SchemaClassFieldData<'a> {
|
||||
pub fn new(process: &'a Process, address: usize) -> Self {
|
||||
Self { process, address }
|
||||
}
|
||||
|
||||
pub fn name(&self) -> Result<String> {
|
||||
let name_ptr = self.process.read_memory::<usize>(self.address)?;
|
||||
|
||||
self.process.read_string_length(name_ptr.into(), 64)
|
||||
}
|
||||
|
||||
pub fn r#type(&self) -> Result<SchemaType> {
|
||||
let address = self.process.read_memory::<usize>(self.address + 0x8)?;
|
||||
|
||||
Ok(SchemaType::new(self.process, address))
|
||||
}
|
||||
|
||||
pub fn offset(&self) -> Result<u16> {
|
||||
self.process.read_memory::<u16>(self.address + 0x10)
|
||||
}
|
||||
}
|
@@ -1,65 +0,0 @@
|
||||
use anyhow::Result;
|
||||
|
||||
use super::SchemaClassFieldData;
|
||||
|
||||
use crate::os::Process;
|
||||
|
||||
pub struct SchemaClassInfo<'a> {
|
||||
process: &'a Process,
|
||||
address: usize,
|
||||
name: String,
|
||||
}
|
||||
|
||||
impl<'a> SchemaClassInfo<'a> {
|
||||
pub fn new(process: &'a Process, address: usize, name: &str) -> Self {
|
||||
Self {
|
||||
process,
|
||||
address,
|
||||
name: name.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
|
||||
pub fn fields(&self) -> Result<Vec<SchemaClassFieldData>> {
|
||||
let address = self.process.read_memory::<usize>(self.address + 0x28)?;
|
||||
|
||||
if address == 0 {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
let count = self.fields_count()?;
|
||||
|
||||
let fields: Vec<SchemaClassFieldData> = (address..address + count as usize * 0x20)
|
||||
.step_by(0x20)
|
||||
.map(|address| SchemaClassFieldData::new(self.process, address.into()))
|
||||
.collect();
|
||||
|
||||
Ok(fields)
|
||||
}
|
||||
|
||||
pub fn fields_count(&self) -> Result<u16> {
|
||||
self.process.read_memory::<u16>(self.address + 0x1C)
|
||||
}
|
||||
|
||||
pub fn parent(&self) -> Result<Option<SchemaClassInfo>> {
|
||||
let address = self.process.read_memory::<usize>(self.address + 0x38)?;
|
||||
|
||||
if address == 0 {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let parent = self.process.read_memory::<usize>(address + 0x8)?;
|
||||
let name_ptr = self.process.read_memory::<usize>(parent + 0x8)?;
|
||||
let name = self.process.read_string(name_ptr.into())?;
|
||||
|
||||
Ok(Some(SchemaClassInfo::new(
|
||||
self.process,
|
||||
parent.into(),
|
||||
&name,
|
||||
)))
|
||||
}
|
||||
}
|
@@ -1,55 +0,0 @@
|
||||
use std::mem;
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
|
||||
use super::SchemaSystemTypeScope;
|
||||
|
||||
use crate::os::Process;
|
||||
|
||||
use crate::config::SCHEMA_CONF;
|
||||
|
||||
pub struct SchemaSystem<'a> {
|
||||
process: &'a Process,
|
||||
address: usize,
|
||||
}
|
||||
|
||||
impl<'a> SchemaSystem<'a> {
|
||||
pub fn new(process: &'a Process) -> Result<Self> {
|
||||
let mut address = process
|
||||
.find_pattern(SCHEMA_CONF.module_name, SCHEMA_CONF.pattern)
|
||||
.expect("unable to find schema system pattern");
|
||||
|
||||
address = process.resolve_rip(address, None, None)?;
|
||||
|
||||
Ok(Self { process, address })
|
||||
}
|
||||
|
||||
pub fn type_scopes(&self) -> Result<Vec<SchemaSystemTypeScope>> {
|
||||
let size = self
|
||||
.process
|
||||
.read_memory::<u32>(self.address + SCHEMA_CONF.type_scope_size_offset)?;
|
||||
|
||||
if size == 0 {
|
||||
bail!("no type scopes found");
|
||||
}
|
||||
|
||||
let data = self
|
||||
.process
|
||||
.read_memory::<usize>(self.address + SCHEMA_CONF.type_scope_data_offset)?;
|
||||
|
||||
let mut addresses = vec![0; size as usize];
|
||||
|
||||
self.process.read_memory_raw(
|
||||
data.into(),
|
||||
addresses.as_mut_ptr() as *mut _,
|
||||
addresses.len() * mem::size_of::<usize>(),
|
||||
)?;
|
||||
|
||||
let type_scopes: Vec<SchemaSystemTypeScope> = addresses
|
||||
.iter()
|
||||
.map(|&address| SchemaSystemTypeScope::new(self.process, address))
|
||||
.collect();
|
||||
|
||||
Ok(type_scopes)
|
||||
}
|
||||
}
|
@@ -1,47 +0,0 @@
|
||||
use anyhow::Result;
|
||||
|
||||
use super::{SchemaClassInfo, SchemaTypeDeclaredClass, UtlTsHash};
|
||||
|
||||
use crate::os::Process;
|
||||
|
||||
use crate::config::SCHEMA_CONF;
|
||||
|
||||
pub struct SchemaSystemTypeScope<'a> {
|
||||
process: &'a Process,
|
||||
address: usize,
|
||||
}
|
||||
|
||||
impl<'a> SchemaSystemTypeScope<'a> {
|
||||
pub fn new(process: &'a Process, address: usize) -> Self {
|
||||
Self { process, address }
|
||||
}
|
||||
|
||||
pub fn classes(&self) -> Result<Vec<SchemaClassInfo>> {
|
||||
let declared_classes = self
|
||||
.process
|
||||
.read_memory::<UtlTsHash<*mut SchemaTypeDeclaredClass>>(
|
||||
self.address + SCHEMA_CONF.declared_classes_offset,
|
||||
)?;
|
||||
|
||||
let classes: Vec<SchemaClassInfo> = declared_classes
|
||||
.elements(self.process)?
|
||||
.iter()
|
||||
.filter_map(|&class_ptr| {
|
||||
let address = class_ptr as usize;
|
||||
|
||||
let declared_class = SchemaTypeDeclaredClass::new(self.process, address);
|
||||
|
||||
declared_class
|
||||
.name()
|
||||
.ok()
|
||||
.map(|name| SchemaClassInfo::new(self.process, address, &name))
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(classes)
|
||||
}
|
||||
|
||||
pub fn module_name(&self) -> Result<String> {
|
||||
self.process.read_string_length(self.address + 0x8, 256)
|
||||
}
|
||||
}
|
@@ -1,69 +0,0 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use lazy_static::lazy_static;
|
||||
|
||||
use regex::Regex;
|
||||
|
||||
use crate::os::Process;
|
||||
|
||||
const TYPE_MAP: &[(&'static str, &'static str)] = &[
|
||||
("uint8", "uint8_t"),
|
||||
("uint16", "uint16_t"),
|
||||
("uint32", "uint32_t"),
|
||||
("uint64", "uint64_t"),
|
||||
("int8", "int8_t"),
|
||||
("int16", "int16_t"),
|
||||
("int32", "int32_t"),
|
||||
("int64", "int64_t"),
|
||||
("float32", "float"),
|
||||
("float64", "double"),
|
||||
];
|
||||
|
||||
lazy_static! {
|
||||
static ref REGEX_MAP: HashMap<&'static str, Regex> = {
|
||||
let mut map = HashMap::with_capacity(TYPE_MAP.len());
|
||||
|
||||
for (k, _v) in TYPE_MAP.iter() {
|
||||
map.insert(*k, Regex::new(&format!(r"\b{}\b", k)).unwrap());
|
||||
}
|
||||
|
||||
map
|
||||
};
|
||||
}
|
||||
|
||||
pub struct SchemaType<'a> {
|
||||
process: &'a Process,
|
||||
address: usize,
|
||||
}
|
||||
|
||||
impl<'a> SchemaType<'a> {
|
||||
pub fn new(process: &'a Process, address: usize) -> Self {
|
||||
Self { process, address }
|
||||
}
|
||||
|
||||
pub fn name(&self) -> Result<String> {
|
||||
let name_ptr = self.process.read_memory::<usize>(self.address + 0x8)?;
|
||||
|
||||
let name = self
|
||||
.process
|
||||
.read_string(name_ptr.into())?
|
||||
.replace(" ", "")
|
||||
.to_string();
|
||||
|
||||
Ok(Self::convert_type_name(&name))
|
||||
}
|
||||
|
||||
fn convert_type_name(type_name: &str) -> String {
|
||||
let mut result = type_name.to_string();
|
||||
|
||||
for (k, v) in TYPE_MAP.iter() {
|
||||
let re = REGEX_MAP.get(*k).unwrap();
|
||||
|
||||
result = re.replace_all(&result, &v.to_string()).to_string();
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
}
|
@@ -1,20 +0,0 @@
|
||||
use anyhow::Result;
|
||||
|
||||
use crate::os::Process;
|
||||
|
||||
pub struct SchemaTypeDeclaredClass<'a> {
|
||||
process: &'a Process,
|
||||
address: usize,
|
||||
}
|
||||
|
||||
impl<'a> SchemaTypeDeclaredClass<'a> {
|
||||
pub fn new(process: &'a Process, address: usize) -> Self {
|
||||
Self { process, address }
|
||||
}
|
||||
|
||||
pub fn name(&self) -> Result<String> {
|
||||
let name_ptr = self.process.read_memory::<usize>(self.address + 0x8)?;
|
||||
|
||||
self.process.read_string_length(name_ptr, 64)
|
||||
}
|
||||
}
|
@@ -1,159 +0,0 @@
|
||||
use std::mem::offset_of;
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use crate::os::Process;
|
||||
|
||||
#[derive(Debug)]
|
||||
#[repr(C)]
|
||||
struct HashFixedDataInternal<T, K> {
|
||||
ui_key: K, // 0x0010
|
||||
next: *mut HashFixedDataInternal<T, K>, // 0x0010
|
||||
data: T, // 0x0010
|
||||
}
|
||||
|
||||
impl<T, K> HashFixedDataInternal<T, K> {
|
||||
fn next(&self, process: &Process) -> Result<*mut HashFixedDataInternal<T, K>> {
|
||||
process.read_memory::<*mut HashFixedDataInternal<T, K>>(
|
||||
(self as *const _ as usize + offset_of!(HashFixedDataInternal<T, K>, next)) as _,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[repr(C)]
|
||||
struct HashBucketDataInternal<T, K> {
|
||||
data: T, // 0x0000
|
||||
next: *mut HashFixedDataInternal<T, K>, // 0x0008
|
||||
ui_key: K, // 0x0010
|
||||
}
|
||||
|
||||
impl<T, K> HashBucketDataInternal<T, K> {
|
||||
fn next(&self, process: &Process) -> Result<*mut HashFixedDataInternal<T, K>> {
|
||||
process.read_memory::<*mut HashFixedDataInternal<T, K>>(
|
||||
(self as *const _ as usize + offset_of!(HashBucketDataInternal<T, K>, next)) as _,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[repr(C)]
|
||||
pub struct HashAllocatedData<T, K> {
|
||||
pad_0: [u8; 0x18], // 0x0000
|
||||
list: [HashFixedDataInternal<T, K>; 128], // 0x0018
|
||||
}
|
||||
|
||||
impl<T, K> HashAllocatedData<T, K> {
|
||||
fn list(&self, process: &Process) -> Result<[HashFixedDataInternal<T, K>; 128]> {
|
||||
process.read_memory::<[HashFixedDataInternal<T, K>; 128]>(
|
||||
(self as *const _ as usize + offset_of!(HashAllocatedData<T, K>, list)) as _,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[repr(C)]
|
||||
struct HashUnallocatedData<T, K> {
|
||||
next: *mut HashUnallocatedData<T, K>, // 0x0000
|
||||
unknown_1: K, // 0x0008
|
||||
ui_key: K, // 0x0010
|
||||
unknown_2: K, // 0x0018
|
||||
block_list: [HashBucketDataInternal<T, K>; 256], // 0x0020
|
||||
}
|
||||
|
||||
impl<T, K> HashUnallocatedData<T, K> {
|
||||
fn next(&self, process: &Process) -> Result<*mut HashUnallocatedData<T, K>> {
|
||||
process.read_memory::<*mut HashUnallocatedData<T, K>>(
|
||||
(self as *const _ as usize + offset_of!(HashUnallocatedData<T, K>, next)) as _,
|
||||
)
|
||||
}
|
||||
|
||||
fn ui_key(&self, process: &Process) -> Result<K> {
|
||||
process.read_memory::<K>(
|
||||
(self as *const _ as usize + offset_of!(HashUnallocatedData<T, K>, ui_key)) as _,
|
||||
)
|
||||
}
|
||||
|
||||
fn block_list(&self, process: &Process) -> Result<[HashBucketDataInternal<T, K>; 256]> {
|
||||
process.read_memory::<[HashBucketDataInternal<T, K>; 256]>(
|
||||
(self as *const _ as usize + offset_of!(HashUnallocatedData<T, K>, block_list)) as _,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[repr(C)]
|
||||
struct HashBucket<T, K> {
|
||||
pad_0: [u8; 0x10], // 0x0000
|
||||
allocated_data: *const HashAllocatedData<T, K>, // 0x0010
|
||||
unallocated_data: *const HashUnallocatedData<T, K>, // 0x0018
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[repr(C)]
|
||||
struct UtlMemoryPool {
|
||||
block_size: i32, // 0x0000
|
||||
blocks_per_blob: i32, // 0x0004
|
||||
grow_mode: i32, // 0x0008
|
||||
blocks_allocated: i32, // 0x000C
|
||||
block_allocated_size: i32, // 0x0010
|
||||
peak_alloc: i32, // 0x0014
|
||||
}
|
||||
|
||||
impl UtlMemoryPool {
|
||||
#[inline]
|
||||
fn block_size(&self) -> i32 {
|
||||
self.blocks_per_blob
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn count(&self) -> i32 {
|
||||
self.block_allocated_size
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[repr(C)]
|
||||
pub struct UtlTsHash<T, K = u64> {
|
||||
entry_memory: UtlMemoryPool, // 0x0000
|
||||
buckets: HashBucket<T, K>, // 0x0018
|
||||
}
|
||||
|
||||
impl<T, K> UtlTsHash<T, K>
|
||||
where
|
||||
T: Copy,
|
||||
{
|
||||
#[inline]
|
||||
pub fn block_size(&self) -> i32 {
|
||||
self.entry_memory.block_size()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn count(&self) -> i32 {
|
||||
self.entry_memory.count()
|
||||
}
|
||||
|
||||
pub fn elements(&self, process: &Process) -> Result<Vec<T>> {
|
||||
let mut address = self.buckets.unallocated_data;
|
||||
|
||||
let min_size = (self.block_size() as usize).min(self.count() as usize);
|
||||
|
||||
let mut list = Vec::with_capacity(min_size);
|
||||
|
||||
while !address.is_null() {
|
||||
let block_list = unsafe { (*address).block_list(process) }?;
|
||||
|
||||
for i in 0..min_size {
|
||||
list.push(block_list[i].data);
|
||||
|
||||
if list.len() >= self.count() as usize {
|
||||
return Ok(list);
|
||||
}
|
||||
}
|
||||
|
||||
address = unsafe { (*address).next(process) }?;
|
||||
}
|
||||
|
||||
Ok(list)
|
||||
}
|
||||
}
|
13
src/source_engine/client/input.rs
Normal file
13
src/source_engine/client/input.rs
Normal file
@@ -0,0 +1,13 @@
|
||||
use memflow::prelude::v1::*;
|
||||
|
||||
#[repr(C)]
|
||||
pub struct KeyboardKey {
|
||||
pad_0000: [u8; 0x8],
|
||||
pub name: Pointer64<ReprCString>,
|
||||
pad_0010: [u8; 0x20],
|
||||
pub state: u32,
|
||||
pad_0034: [u8; 0x50],
|
||||
pub next: Pointer64<KeyboardKey>,
|
||||
}
|
||||
|
||||
unsafe impl Pod for KeyboardKey {}
|
3
src/source_engine/client/mod.rs
Normal file
3
src/source_engine/client/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
pub use input::KeyboardKey;
|
||||
|
||||
pub mod input;
|
7
src/source_engine/mod.rs
Normal file
7
src/source_engine/mod.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
pub use client::*;
|
||||
pub use schema_system::*;
|
||||
pub use tier1::*;
|
||||
|
||||
pub mod client;
|
||||
pub mod schema_system;
|
||||
pub mod tier1;
|
21
src/source_engine/schema_system/mod.rs
Normal file
21
src/source_engine/schema_system/mod.rs
Normal file
@@ -0,0 +1,21 @@
|
||||
pub use schema_base_class_info_data::*;
|
||||
pub use schema_class_field_data::*;
|
||||
pub use schema_class_info_data::*;
|
||||
pub use schema_enum_info_data::*;
|
||||
pub use schema_enumerator_info_data::*;
|
||||
pub use schema_metadata_entry_data::*;
|
||||
pub use schema_static_field_data::*;
|
||||
pub use schema_system::*;
|
||||
pub use schema_system_type_scope::*;
|
||||
pub use schema_type::*;
|
||||
|
||||
pub mod schema_base_class_info_data;
|
||||
pub mod schema_class_field_data;
|
||||
pub mod schema_class_info_data;
|
||||
pub mod schema_enum_info_data;
|
||||
pub mod schema_enumerator_info_data;
|
||||
pub mod schema_metadata_entry_data;
|
||||
pub mod schema_static_field_data;
|
||||
pub mod schema_system;
|
||||
pub mod schema_system_type_scope;
|
||||
pub mod schema_type;
|
@@ -0,0 +1,11 @@
|
||||
use memflow::prelude::v1::*;
|
||||
|
||||
use super::SchemaClassInfoData;
|
||||
|
||||
#[repr(C)]
|
||||
pub struct SchemaBaseClassInfoData {
|
||||
pub offset: u32,
|
||||
pub prev: Pointer64<SchemaClassInfoData>,
|
||||
}
|
||||
|
||||
unsafe impl Pod for SchemaBaseClassInfoData {}
|
14
src/source_engine/schema_system/schema_class_field_data.rs
Normal file
14
src/source_engine/schema_system/schema_class_field_data.rs
Normal file
@@ -0,0 +1,14 @@
|
||||
use memflow::prelude::v1::*;
|
||||
|
||||
use super::{SchemaMetadataEntryData, SchemaType};
|
||||
|
||||
#[repr(C)]
|
||||
pub struct SchemaClassFieldData {
|
||||
pub name: Pointer64<ReprCString>,
|
||||
pub schema_type: Pointer64<SchemaType>,
|
||||
pub offset: u32,
|
||||
pub metadata_count: u32,
|
||||
pub metadata: Pointer64<SchemaMetadataEntryData>,
|
||||
}
|
||||
|
||||
unsafe impl Pod for SchemaClassFieldData {}
|
33
src/source_engine/schema_system/schema_class_info_data.rs
Normal file
33
src/source_engine/schema_system/schema_class_info_data.rs
Normal file
@@ -0,0 +1,33 @@
|
||||
use memflow::prelude::v1::*;
|
||||
|
||||
use super::{
|
||||
SchemaBaseClassInfoData, SchemaClassFieldData, SchemaMetadataEntryData, SchemaStaticFieldData,
|
||||
SchemaSystemTypeScope, SchemaType,
|
||||
};
|
||||
|
||||
pub type SchemaClassBinding = SchemaClassInfoData;
|
||||
|
||||
#[repr(C)]
|
||||
pub struct SchemaClassInfoData {
|
||||
pub base: Pointer64<SchemaClassInfoData>,
|
||||
pub name: Pointer64<ReprCString>,
|
||||
pub module_name: Pointer64<ReprCString>,
|
||||
pub size: u32,
|
||||
pub fields_count: u16,
|
||||
pub static_fields_count: u16,
|
||||
pub static_metadata_count: u16,
|
||||
pub alignment: u8,
|
||||
pub has_base_class: bool,
|
||||
pub total_class_size: u16,
|
||||
pub derived_class_size: u16,
|
||||
pub fields: Pointer64<SchemaClassFieldData>,
|
||||
pub static_fields: Pointer64<SchemaStaticFieldData>,
|
||||
pub base_classes: Pointer64<SchemaBaseClassInfoData>,
|
||||
pad_0040: [u8; 0x8],
|
||||
pub static_metadata: Pointer64<SchemaMetadataEntryData>,
|
||||
pub type_scope: Pointer64<SchemaSystemTypeScope>,
|
||||
pub schema_type: Pointer64<SchemaType>,
|
||||
pad_0060: [u8; 0x10],
|
||||
}
|
||||
|
||||
unsafe impl Pod for SchemaClassInfoData {}
|
35
src/source_engine/schema_system/schema_enum_info_data.rs
Normal file
35
src/source_engine/schema_system/schema_enum_info_data.rs
Normal file
@@ -0,0 +1,35 @@
|
||||
use memflow::prelude::v1::*;
|
||||
|
||||
use super::{SchemaEnumeratorInfoData, SchemaMetadataEntryData, SchemaSystemTypeScope};
|
||||
|
||||
pub type SchemaEnumBinding = SchemaEnumInfoData;
|
||||
|
||||
#[repr(C)]
|
||||
pub struct SchemaEnumInfoData {
|
||||
pub base: Pointer64<SchemaEnumInfoData>,
|
||||
pub name: Pointer64<ReprCString>,
|
||||
pub module_name: Pointer64<ReprCString>,
|
||||
pub alignment: u8,
|
||||
pad_0019: [u8; 0x3],
|
||||
pub size: u16,
|
||||
pub static_metadata_count: u16,
|
||||
pub enum_info: Pointer64<SchemaEnumeratorInfoData>,
|
||||
pub static_metadata: Pointer64<SchemaMetadataEntryData>,
|
||||
pub type_scope: Pointer64<SchemaSystemTypeScope>,
|
||||
pad_0038: [u8; 0xC],
|
||||
}
|
||||
|
||||
impl SchemaEnumInfoData {
|
||||
#[inline]
|
||||
pub fn type_name(&self) -> &str {
|
||||
match self.alignment {
|
||||
1 => "int8",
|
||||
2 => "int16",
|
||||
4 => "int32",
|
||||
8 => "int64",
|
||||
_ => "unknown",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl Pod for SchemaEnumInfoData {}
|
@@ -0,0 +1,21 @@
|
||||
use memflow::prelude::v1::*;
|
||||
|
||||
use super::SchemaMetadataEntryData;
|
||||
|
||||
#[repr(C)]
|
||||
pub struct SchemaEnumeratorInfoData {
|
||||
pub name: Pointer64<ReprCString>,
|
||||
pub union_data: SchemaEnumeratorInfoDataUnion,
|
||||
pub metadata_count: u32,
|
||||
pub metadata: Pointer64<SchemaMetadataEntryData>,
|
||||
}
|
||||
|
||||
unsafe impl Pod for SchemaEnumeratorInfoData {}
|
||||
|
||||
#[repr(C)]
|
||||
pub union SchemaEnumeratorInfoDataUnion {
|
||||
pub uchar: u8,
|
||||
pub ushort: u16,
|
||||
pub uint: u32,
|
||||
pub ulong: u64,
|
||||
}
|
@@ -0,0 +1,35 @@
|
||||
use std::ffi::c_char;
|
||||
|
||||
use memflow::prelude::v1::*;
|
||||
|
||||
#[repr(C)]
|
||||
pub struct SchemaMetadataEntryData {
|
||||
pub name: Pointer64<ReprCString>,
|
||||
pub network_value: Pointer64<SchemaNetworkValue>,
|
||||
}
|
||||
|
||||
unsafe impl Pod for SchemaMetadataEntryData {}
|
||||
|
||||
#[repr(C)]
|
||||
pub struct SchemaNetworkValue {
|
||||
pub union_data: SchemaNetworkValueUnion,
|
||||
}
|
||||
|
||||
unsafe impl Pod for SchemaNetworkValue {}
|
||||
|
||||
#[repr(C)]
|
||||
pub union SchemaNetworkValueUnion {
|
||||
pub name_ptr: Pointer64<ReprCString>,
|
||||
pub int_value: i32,
|
||||
pub float_value: f32,
|
||||
pub ptr: Pointer64<()>,
|
||||
pub var_value: SchemaVarName,
|
||||
pub name_value: [c_char; 32],
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
#[repr(C)]
|
||||
pub struct SchemaVarName {
|
||||
pub name: Pointer64<ReprCString>,
|
||||
pub ty: Pointer64<ReprCString>,
|
||||
}
|
13
src/source_engine/schema_system/schema_static_field_data.rs
Normal file
13
src/source_engine/schema_system/schema_static_field_data.rs
Normal file
@@ -0,0 +1,13 @@
|
||||
use memflow::prelude::v1::*;
|
||||
|
||||
use super::SchemaType;
|
||||
|
||||
#[repr(C)]
|
||||
pub struct SchemaStaticFieldData {
|
||||
pub name: Pointer64<ReprCString>,
|
||||
pub type_: Pointer64<SchemaType>,
|
||||
pub instance: Address,
|
||||
pad_0018: [u8; 0x10],
|
||||
}
|
||||
|
||||
unsafe impl Pod for SchemaStaticFieldData {}
|
25
src/source_engine/schema_system/schema_system.rs
Normal file
25
src/source_engine/schema_system/schema_system.rs
Normal file
@@ -0,0 +1,25 @@
|
||||
use memflow::prelude::v1::*;
|
||||
|
||||
use super::SchemaSystemTypeScope;
|
||||
|
||||
use crate::source_engine::UtlVector;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[repr(C)]
|
||||
pub struct SchemaSystem {
|
||||
pad_0000: [u8; 0x1F8],
|
||||
pub type_scopes: UtlVector<Pointer64<SchemaSystemTypeScope>>,
|
||||
pad_01a0: [u8; 0x120],
|
||||
pub num_registrations: u32,
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
#[repr(C)]
|
||||
pub struct SchemaSystem {
|
||||
pad_0000: [u8; 0x190],
|
||||
pub type_scopes: UtlVector<Pointer64<SchemaSystemTypeScope>>,
|
||||
pad_01a0: [u8; 0x120],
|
||||
pub num_registrations: u32,
|
||||
}
|
||||
|
||||
unsafe impl Pod for SchemaSystem {}
|
31
src/source_engine/schema_system/schema_system_type_scope.rs
Normal file
31
src/source_engine/schema_system/schema_system_type_scope.rs
Normal file
@@ -0,0 +1,31 @@
|
||||
use std::ffi::c_char;
|
||||
|
||||
use memflow::prelude::v1::*;
|
||||
|
||||
use super::{SchemaClassBinding, SchemaEnumBinding};
|
||||
|
||||
use crate::source_engine::UtlTsHash;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[repr(C)]
|
||||
pub struct SchemaSystemTypeScope {
|
||||
pad_0000: [u8; 0x8],
|
||||
pub name: [c_char; 256],
|
||||
pad_0108: [u8; 0x518],
|
||||
pub class_bindings: UtlTsHash<Pointer64<SchemaClassBinding>>,
|
||||
pad_05f0: [u8; 0x2810],
|
||||
pub enum_bindings: UtlTsHash<Pointer64<SchemaEnumBinding>>,
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
#[repr(C)]
|
||||
pub struct SchemaSystemTypeScope {
|
||||
pad_0000: [u8; 0x8],
|
||||
pub name: [c_char; 256],
|
||||
pad_0108: [u8; 0x4B0],
|
||||
pub class_bindings: UtlTsHash<Pointer64<SchemaClassBinding>>,
|
||||
pad_05f0: [u8; 0x2810],
|
||||
pub enum_bindings: UtlTsHash<Pointer64<SchemaEnumBinding>>,
|
||||
}
|
||||
|
||||
unsafe impl Pod for SchemaSystemTypeScope {}
|
99
src/source_engine/schema_system/schema_type.rs
Normal file
99
src/source_engine/schema_system/schema_type.rs
Normal file
@@ -0,0 +1,99 @@
|
||||
use memflow::prelude::v1::*;
|
||||
|
||||
use super::{SchemaClassInfoData, SchemaEnumBinding, SchemaSystemTypeScope};
|
||||
|
||||
#[derive(Debug, Eq, Ord, PartialEq, PartialOrd)]
|
||||
#[repr(u8)]
|
||||
pub enum SchemaAtomicCategory {
|
||||
Basic = 0,
|
||||
T,
|
||||
CollectionOfT,
|
||||
TF,
|
||||
TT,
|
||||
TTF,
|
||||
I,
|
||||
None,
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, Ord, PartialEq, PartialOrd)]
|
||||
#[repr(u8)]
|
||||
pub enum SchemaTypeCategory {
|
||||
BuiltIn = 0,
|
||||
Ptr,
|
||||
Bitfield,
|
||||
FixedArray,
|
||||
Atomic,
|
||||
DeclaredClass,
|
||||
DeclaredEnum,
|
||||
None,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
#[repr(C)]
|
||||
pub struct SchemaArray {
|
||||
pub array_size: u32,
|
||||
pad_0004: [u8; 0x4],
|
||||
pub element_type: Pointer64<SchemaType>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
#[repr(C)]
|
||||
pub struct SchemaAtomic {
|
||||
pub element_type: Pointer64<SchemaType>,
|
||||
pad_0008: [u8; 0x8],
|
||||
pub template_ty: Pointer64<SchemaType>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
#[repr(C)]
|
||||
pub struct SchemaAtomicI {
|
||||
pad_0000: [u8; 0x10],
|
||||
pub value: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
#[repr(C)]
|
||||
pub struct SchemaAtomicTF {
|
||||
pad_0000: [u8; 0x10],
|
||||
pub template_ty: Pointer64<SchemaType>,
|
||||
pub size: u32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
#[repr(C)]
|
||||
pub struct SchemaAtomicTT {
|
||||
pad_0000: [u8; 0x10],
|
||||
pub templates: [Pointer64<SchemaType>; 2],
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
#[repr(C)]
|
||||
pub struct SchemaAtomicTTF {
|
||||
pad_0000: [u8; 0x10],
|
||||
pub templates: [Pointer64<SchemaType>; 2],
|
||||
pub size: u32,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub struct SchemaType {
|
||||
pad_0000: [u8; 0x8],
|
||||
pub name: Pointer64<ReprCString>,
|
||||
pub type_scope: Pointer64<SchemaSystemTypeScope>,
|
||||
pub type_category: SchemaTypeCategory,
|
||||
pub atomic_category: SchemaAtomicCategory,
|
||||
}
|
||||
|
||||
unsafe impl Pod for SchemaType {}
|
||||
|
||||
#[repr(C)]
|
||||
pub union SchemaTypeUnion {
|
||||
pub schema_type: Pointer64<SchemaType>,
|
||||
pub class_info: Pointer64<SchemaClassInfoData>,
|
||||
pub enum_binding: Pointer64<SchemaEnumBinding>,
|
||||
pub array: SchemaArray,
|
||||
pub atomic: SchemaAtomic,
|
||||
pub atomic_tt: SchemaAtomicTT,
|
||||
pub atomic_tf: SchemaAtomicTF,
|
||||
pub atomic_ttf: SchemaAtomicTTF,
|
||||
pub atomic_i: SchemaAtomicI,
|
||||
}
|
44
src/source_engine/tier1/convar.rs
Normal file
44
src/source_engine/tier1/convar.rs
Normal file
@@ -0,0 +1,44 @@
|
||||
use memflow::prelude::v1::*;
|
||||
|
||||
use super::UtlMemory;
|
||||
|
||||
use crate::error::Result;
|
||||
|
||||
#[repr(C)]
|
||||
pub struct ConCommandBase {
|
||||
pub name: Pointer64<ReprCString>,
|
||||
pub description: Pointer64<ReprCString>,
|
||||
pub flags: u64,
|
||||
pad_0018: [u8; 0x20],
|
||||
}
|
||||
|
||||
unsafe impl Pod for ConCommandBase {}
|
||||
|
||||
#[repr(C)]
|
||||
pub struct CVar {
|
||||
pad_0000: [u8; 0xD8],
|
||||
pub cmds: UtlMemory<ConCommandBase>,
|
||||
}
|
||||
|
||||
impl CVar {
|
||||
pub fn iter(
|
||||
&self,
|
||||
process: &mut IntoProcessInstanceArcBox<'_>,
|
||||
) -> Result<impl Iterator<Item = ConCommandBase>> {
|
||||
let mut cmds = Vec::new();
|
||||
|
||||
for i in 0..self.cmds.alloc_count as usize {
|
||||
let cmd = self.cmds.get(process, i)?;
|
||||
|
||||
if cmd.name.is_null() {
|
||||
continue;
|
||||
}
|
||||
|
||||
cmds.push(cmd);
|
||||
}
|
||||
|
||||
Ok(cmds.into_iter())
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl Pod for CVar {}
|
10
src/source_engine/tier1/interface.rs
Normal file
10
src/source_engine/tier1/interface.rs
Normal file
@@ -0,0 +1,10 @@
|
||||
use memflow::prelude::v1::*;
|
||||
|
||||
#[repr(C)]
|
||||
pub struct InterfaceReg {
|
||||
pub create_fn: Address,
|
||||
pub name: Pointer64<ReprCString>,
|
||||
pub next: Pointer64<InterfaceReg>,
|
||||
}
|
||||
|
||||
unsafe impl Pod for InterfaceReg {}
|
11
src/source_engine/tier1/mod.rs
Normal file
11
src/source_engine/tier1/mod.rs
Normal file
@@ -0,0 +1,11 @@
|
||||
pub use convar::*;
|
||||
pub use interface::*;
|
||||
pub use utl_memory::*;
|
||||
pub use utl_ts_hash::*;
|
||||
pub use utl_vector::*;
|
||||
|
||||
pub mod convar;
|
||||
pub mod interface;
|
||||
pub mod utl_memory;
|
||||
pub mod utl_ts_hash;
|
||||
pub mod utl_vector;
|
30
src/source_engine/tier1/utl_memory.rs
Normal file
30
src/source_engine/tier1/utl_memory.rs
Normal file
@@ -0,0 +1,30 @@
|
||||
use std::mem;
|
||||
|
||||
use memflow::prelude::v1::*;
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
|
||||
#[repr(C)]
|
||||
pub struct UtlMemory<T: Sized + Pod> {
|
||||
pub mem: Pointer64<T>,
|
||||
pub alloc_count: u32,
|
||||
pub grow_size: u32,
|
||||
}
|
||||
|
||||
impl<T: Sized + Pod> UtlMemory<T> {
|
||||
#[inline]
|
||||
pub fn get(&self, process: &mut IntoProcessInstanceArcBox<'_>, idx: usize) -> Result<T> {
|
||||
if idx >= self.alloc_count as usize {
|
||||
return Err(Error::IndexOutOfBounds {
|
||||
idx,
|
||||
len: self.alloc_count as usize,
|
||||
});
|
||||
}
|
||||
|
||||
let ptr = Pointer64::from(self.mem.address() + (idx * mem::size_of::<T>()));
|
||||
|
||||
Ok(process.read_ptr(ptr)?)
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl<T: Sized + Pod> Pod for UtlMemory<T> {}
|
102
src/source_engine/tier1/utl_ts_hash.rs
Normal file
102
src/source_engine/tier1/utl_ts_hash.rs
Normal file
@@ -0,0 +1,102 @@
|
||||
use memflow::prelude::v1::*;
|
||||
|
||||
use crate::error::Result;
|
||||
|
||||
pub trait HashData: Copy + Sized + Pod {}
|
||||
|
||||
impl<T: Copy + Sized + Pod> HashData for T {}
|
||||
|
||||
pub trait HashKey: Copy + Sized + Pod {}
|
||||
|
||||
impl<K: Copy + Sized + Pod> HashKey for K {}
|
||||
|
||||
#[repr(C)]
|
||||
struct HashFixedDataInternal<T: HashData, K: HashKey> {
|
||||
ui_key: K,
|
||||
next: Pointer64<HashFixedDataInternal<T, K>>,
|
||||
data: T,
|
||||
}
|
||||
|
||||
unsafe impl<T: HashData, K: HashKey> Pod for HashFixedDataInternal<T, K> {}
|
||||
|
||||
#[repr(C)]
|
||||
struct HashBucketDataInternal<T: HashData, K: HashKey> {
|
||||
data: T,
|
||||
next: Pointer64<HashFixedDataInternal<T, K>>,
|
||||
ui_key: K,
|
||||
}
|
||||
|
||||
unsafe impl<T: HashData, K: HashKey> Pod for HashBucketDataInternal<T, K> {}
|
||||
|
||||
#[repr(C)]
|
||||
struct HashAllocatedData<T: HashData, K: HashKey> {
|
||||
pad_0000: [u8; 0x18],
|
||||
list: [HashFixedDataInternal<T, K>; 128],
|
||||
}
|
||||
|
||||
unsafe impl<T: HashData, K: HashKey> Pod for HashAllocatedData<T, K> {}
|
||||
|
||||
#[repr(C)]
|
||||
struct HashUnallocatedData<T: HashData, K: HashKey> {
|
||||
next: Pointer64<HashUnallocatedData<T, K>>,
|
||||
unk_1: K,
|
||||
ui_key: K,
|
||||
unk_2: K,
|
||||
block_list: [HashBucketDataInternal<T, K>; 256],
|
||||
}
|
||||
|
||||
unsafe impl<T: HashData, K: HashKey> Pod for HashUnallocatedData<T, K> {}
|
||||
|
||||
#[repr(C)]
|
||||
struct HashBucket<T: HashData, K: HashKey> {
|
||||
pad_0000: [u8; 0x10],
|
||||
allocated_data: Pointer64<HashAllocatedData<T, K>>,
|
||||
unallocated_data: Pointer64<HashUnallocatedData<T, K>>,
|
||||
}
|
||||
|
||||
unsafe impl<T: HashData, K: HashKey> Pod for HashBucket<T, K> {}
|
||||
|
||||
#[repr(C)]
|
||||
struct UtlMemoryPool {
|
||||
block_size: u32,
|
||||
blocks_per_blob: u32,
|
||||
grow_mode: u32,
|
||||
blocks_alloc: u32,
|
||||
block_alloc_size: u32,
|
||||
peak_alloc: u32,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub struct UtlTsHash<T: HashData, K: HashKey = u64> {
|
||||
entry: UtlMemoryPool,
|
||||
buckets: HashBucket<T, K>,
|
||||
}
|
||||
|
||||
impl<T: HashData, K: HashKey> UtlTsHash<T, K> {
|
||||
pub fn elements(&self, process: &mut IntoProcessInstanceArcBox<'_>) -> Result<Vec<T>> {
|
||||
let block_size = self.entry.blocks_per_blob as usize;
|
||||
let num_blocks = self.entry.block_alloc_size as usize;
|
||||
|
||||
let mut element_ptr = self.buckets.unallocated_data;
|
||||
|
||||
let mut list = Vec::with_capacity(num_blocks);
|
||||
|
||||
while !element_ptr.is_null() {
|
||||
let element = process.read_ptr(element_ptr)?;
|
||||
|
||||
for i in 0..num_blocks {
|
||||
if i >= block_size || list.len() >= block_size {
|
||||
break;
|
||||
}
|
||||
|
||||
list.push(element.block_list[i].data);
|
||||
}
|
||||
|
||||
element_ptr = element.next;
|
||||
}
|
||||
|
||||
Ok(list)
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl<T: HashData, K: HashKey> Pod for UtlTsHash<T, K> {}
|
29
src/source_engine/tier1/utl_vector.rs
Normal file
29
src/source_engine/tier1/utl_vector.rs
Normal file
@@ -0,0 +1,29 @@
|
||||
use std::mem;
|
||||
|
||||
use memflow::prelude::v1::*;
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
|
||||
#[repr(C)]
|
||||
pub struct UtlVector<T: Sized + Pod> {
|
||||
pub size: u32,
|
||||
pub mem: Pointer64<T>,
|
||||
}
|
||||
|
||||
impl<T: Sized + Pod> UtlVector<T> {
|
||||
#[inline]
|
||||
pub fn get(&self, process: &mut IntoProcessInstanceArcBox<'_>, idx: usize) -> Result<T> {
|
||||
if idx >= self.size as usize {
|
||||
return Err(Error::IndexOutOfBounds {
|
||||
idx,
|
||||
len: self.size as usize,
|
||||
});
|
||||
}
|
||||
|
||||
let ptr = Pointer64::from(self.mem.address() + (idx * mem::size_of::<T>()));
|
||||
|
||||
Ok(process.read_ptr(ptr)?)
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl<T: Sized + Pod> Pod for UtlVector<T> {}
|
Reference in New Issue
Block a user