Similar to wardrive, I'm inclined to use the solutions available already.
My rough build process is as such:
On new client version, I automate a git commit with the new version into my analysis / system bindings generation pipeline.
I analyze using https://crates.io/crates/iced-x86 statically, then similar to reclass or what not I generate explicit C structs and function definitions.
My C structs and function bindings are their own libraries generated per client version: "prisma_era", "prisma_classic", "prisma_retail", "prisma_era_ptr", etc...
All of this can be done with github actions and use ubuntu runners
Then I have a separate repo which uses this as a dependency and has the general common "prisma_rs" bindings.
when you link against the prisma_rs for a script, you can choose what sys structs you need
structs generation snippet
Code:
let mut namespace = Namespace::anonymous();
namespace.add_constant(Constant::public(
"ANALYZER_GIT_VERSION",
FieldType::Ref(StructTag::of_lit("str").into(), None),
format!("\"{}\"", git_version::git_version!()),
));
namespace.add_constant(Constant::public(
"ANALYZER_VERSION",
FieldType::Ref(StructTag::of_lit("str").into(), None),
format!("\"{}\"", env!("CARGO_PKG_VERSION")),
));
namespace.add_constant(Constant::public(
"CLIENT_VERSION",
FieldType::Ref(StructTag::of_lit("str").into(), None),
format!("\"{}\"", binary_version),
));
let _active_player_compile = namespace.compile_struct(
StructBuilder::new("ActivePlayer")
.extends(StructTag::of_lit("Unit"))
.field(StructField::public(
"inventory_count",
PrimitiveType::U32,
player_inventory_offset,
))
.field(StructField::public(
"inventory_items",
FieldType::Ptr(StructTag::of_lit("Guid").into(), 1),
player_inventory_offset + 0x08,
))
.field(StructField::public(
"skill_lines",
FieldType::Array(PrimitiveType::U16.into(), 256),
skill_lines,
))
.field(StructField::public(
"skill_levels",
FieldType::Array(PrimitiveType::U16.into(), 256),
skill_levels,
)),
)?;
sys bindings example output
Code:
#![allow(non_snake_case)]
pub const ANALYZER_GIT_VERSION: &str = "b99e823-modified";
pub const ANALYZER_VERSION: &str = "0.1.0";
pub const CLIENT_VERSION: &str = "3.4.2.50664";
#[repr(C)]
pub struct ActivePlayer {
pub vmt: *const *const usize,
_padding_to_0x10: [u8; 0x8],
pub r#type: u8,
_padding_to_0x18: [u8; 0x7],
pub guid: Guid,
_padding_to_0xD8: [u8; 0xB0],
pub entry_id: u32,
_padding_to_0x148: [u8; 0x6C],
pub position: Vector3,
_padding_to_0x158: [u8; 0x4],
pub rotation: f32,
_padding_to_0x708: [u8; 0x5AC],
pub casting_spell_id: u32,
_padding_to_0x810: [u8; 0x104],
pub channeling_spell_id: u32,
_padding_to_0x868: [u8; 0x54],
pub auras_len: u32,
_padding_to_0x8F8: [u8; 0x8C],
pub aura_table: [Aura; 0x100],
_padding_to_0xD730: [u8; 0x1E38],
pub summoner_guid: Guid,
_padding_to_0xE674: [u8; 0xF34],
pub skill_lines: [u16; 0x100],
_padding_to_0xEA74: [u8; 0x200],
pub skill_levels: [u16; 0x100],
_padding_to_0x12988: [u8; 0x3D14],
pub inventory_count: u32,
_padding_to_0x12990: [u8; 0x4],
pub inventory_items: *const Guid,
}
my safe prisma bindings then come out as such, where the structs alignment is abstracted away mostly.
So this persists for era,classic,retail etc
Code:
/// Represents the local player of the client. There should only be one of these at a time.
pub struct ActivePlayer<'client>(&'client sys::ActivePlayer);
pub trait ActivePlayerAccess<'owner> {
/// Total active inventory items
fn inventory_count(&self) -> u32;
/// List of item guids
fn inventory_guids(&self) -> &[Guid];
/// List of skill lines
fn skill_lines(&self) -> &[u16];
/// List of skill levels
fn skill_levels(&self) -> &[u16];
}
impl<'o> ActivePlayer<'o> {
pub fn can_cast(&self) -> bool {
self.casting_spell_id().is_none() && self.channeling_spell_id().is_none()
}
pub fn professions<'i, 'owner: 'i>(
&'owner self,
) -> impl Iterator<Item = (Profession, u16)> + 'i {
self.skill_lines()
.iter()
.zip(self.skill_levels())
.flat_map(|(id, level)| {
Profession::try_from_raw(*id).map(|profession| (profession, *level))
})
}
pub fn secondary_skills<'i, 'owner: 'i>(
&'owner self,
) -> impl Iterator<Item = (SecondarySkill, u16)> + 'i {
self.skill_lines()
.iter()
.zip(self.skill_levels())
.flat_map(|(id, level)| {
SecondarySkill::try_from_raw(*id).map(|profession| (profession, *level))
})
}
}