diff --git a/crates/eldenring/src/cs/net_man.rs b/crates/eldenring/src/cs/net_man.rs index e8921d19..4fc4a5f3 100644 --- a/crates/eldenring/src/cs/net_man.rs +++ b/crates/eldenring/src/cs/net_man.rs @@ -1,15 +1,13 @@ use std::ptr::NonNull; -use windows::core::PCWSTR; - use crate::{ BasicVector, Vector, dltx::DLString, - fd4::{FD4StepBaseInterface, FD4Time}, + fd4::{FD4StepTemplateBase, FD4Time}, position::BlockPosition, stl::DoublyLinkedList, }; -use shared::OwnedPtr; +use shared::{OwnedPtr, StepperStates}; use super::{BlockId, CSEzTask, CSEzUpdateTask}; @@ -112,6 +110,7 @@ pub struct QuickmatchManager { unk18: u32, /// List of speffects applied to the players during battle. /// Source of names: debug strings + /// /// ```text /// 1110 Team A Summon/Respawn チームA用召喚・リスポン時 /// 1111 Team B Summon/Respawn チームB用召喚・リスポン時 @@ -129,48 +128,38 @@ pub struct QuickmatchManager { // TODO: more fields up to 0xd8 } -#[repr(u32)] -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +#[repr(i32)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, StepperStates)] pub enum CSQuickMatchingCtrlState { - None = 0x0, - SearchRegister = 0x1, - SearchRegisterWait = 0x2, - // Waiting for lobby to gain enough people to start. - GuestInviteWait = 0x3, - GuestWaitSession = 0x4, - GuestReadyWait = 0x5, - // Moving to arena map. - GuestMoveMap = 0x6, - // People are loaded into the map and match is running or has errored. - GuestInGame = 0x7, - HostWaitSession = 0x8, - // Hosting and allowing other people to join the room before starting. - HostInvite = 0x9, - HostReadyWait = 0xa, - HostReadyWaitBlockList = 0xb, - // Moving to arena map. - HostMoveMap = 0xc, - // People are loaded into the map and match is running or has errored. - HostInGame = 0xd, - // Match has ended either by completion or error. - Unregister = 0xe, + /// Stepper is not running. + Inactive = -1, + /// No quickmatch is active. + None = 0, + /// Looking up existing rooms that match the quickmatch settings. + SearchRegister = 1, + /// Waiting for a response for the SearchRegister request. + SearchRegisterWait = 2, + GuestInviteWait = 3, + GuestWaitSession = 4, + GuestReadyWait = 5, + GuestMoveMap = 6, + /// People are loaded into the map and match is running. + GuestInGame = 7, + HostWaitSession = 8, + HostInvite = 9, + HostReadyWait = 10, + HostReadyWaitBlockList = 11, + HostMoveMap = 12, + /// People are loaded into the map and match is running. + HostInGame = 13, + /// Match has ended either by completion or error. + Unregister = 14, } /// Source of name: RTTI #[repr(C)] pub struct CSQuickMatchingCtrl { - pub base: FD4StepBaseInterface<15, Self>, - unk18: [u8; 0x28], - pub current_state: CSQuickMatchingCtrlState, - pub requested_state: CSQuickMatchingCtrlState, - unk48: [u8; 0x50], - /// FD4Step state string. - state_string: PCWSTR, - unka0: bool, - unka1: bool, - unka2: bool, - unka3: bool, - unka4: u32, + pub stepper: FD4StepTemplateBase, pub context: NonNull, menu_job: usize, unkb8: FD4Time, diff --git a/crates/eldenring/src/fd4/step.rs b/crates/eldenring/src/fd4/step.rs index dfd41f62..0f4dd895 100644 --- a/crates/eldenring/src/fd4/step.rs +++ b/crates/eldenring/src/fd4/step.rs @@ -1,65 +1,54 @@ use std::ptr::NonNull; +use shared::StepperStates; use windows::core::PCWSTR; -use crate::{Tree, dlkr::DLAllocatorBase, dltx::DLString}; - -use super::FD4TaskBase; +use crate::{Tree, dlkr::DLAllocatorRef, dltx::DLString, fd4::FD4Time}; /// Source of name: RTTI #[repr(C)] -pub struct FD4StepTemplateBase { - pub task: FD4TaskBase, - pub stepper_fns: NonNull<[StepperFn; N]>, - unk18: FD4StepTemplateBase0x18, - /// Index into the stepper_fns array. - pub current_state: u32, - /// Target step for next cycle. - pub request_state: u32, - unk50: bool, - unk51: [u8; 7], - allocator: NonNull, - unk60: usize, - unk68: i8, - unk69: bool, - _pad6a: [u8; 6], - unk70: DLString, - state_description: PCWSTR, - unka8: bool, - unka9: [u8; 3], -} - -impl AsRef for FD4StepTemplateBase { - fn as_ref(&self) -> &FD4TaskBase { - &self.task - } +pub struct FD4StepTemplateBase { + vftable: *const (), + pub stepper_fns: NonNull>>, + pub attach: FD4ComponentAttachSystem_Step, + /// Current state executing this frame. + pub current_state: States, + /// Target step for next frames execution. + pub requested_state: States, + unk48: u8, + + // Seemingly all debug stuff after this point. + pub allocator: DLAllocatorRef, + unk58: usize, + unk60: i8, + unk61: bool, + unk68: DLString, + /// State label seemingly used for debug tooling. + /// Examples: "NotExecuting", "State Finished.(No StepMethod is Executing.)" + pub debug_state_label: PCWSTR, + unka0: bool, + unka4: i32, } /// Single state for the stepper to be executing from. #[repr(C)] pub struct StepperFn { - pub executor: fn(&mut T, usize), - name: PCWSTR, -} - -impl StepperFn { - pub fn name(&self) -> String { - unsafe { self.name.to_string().unwrap() } - } + pub executor: extern "C" fn(&mut T, &FD4Time), + pub name: PCWSTR, } +/// Source of name: RTTI #[repr(C)] -pub struct FD4StepTemplateBase0x18 { - unk0: NonNull<()>, +pub struct FD4ComponentAttachSystem { + vftable: *const (), unk8: Tree<()>, - unk20: NonNull, - unk28: NonNull, + pub allocator: DLAllocatorRef, } /// Source of name: RTTI +#[allow(non_camel_case_types)] #[repr(C)] -pub struct FD4StepBaseInterface { - vftable: usize, - pub stepper_fns: NonNull<[StepperFn; N]>, - unk10: NonNull<()>, +pub struct FD4ComponentAttachSystem_Step { + pub base: FD4ComponentAttachSystem, + pub allocator: DLAllocatorRef, } diff --git a/crates/shared/macros/src/lib.rs b/crates/shared/macros/src/lib.rs index fc5827e9..63e2e3d1 100644 --- a/crates/shared/macros/src/lib.rs +++ b/crates/shared/macros/src/lib.rs @@ -5,6 +5,7 @@ use syn::*; mod multi_param; mod for_all_subclasses; +mod stepper; mod subclass; mod superclass; mod utils; @@ -303,3 +304,50 @@ pub fn for_all_subclasses(_args: TokenStream, input: TokenStream) -> TokenStream Err(err) => err.into_compile_error().into(), } } + +/// A derive macro that implements the StepperStates trait on a given enum. +/// +/// - The enum must be exhaustive (represent all states and no more). +/// - The enum must have a -1 state for inactive steppers. +/// - The enum must have no gaps in the discriminants. +/// +/// # Safety +/// +/// The implementer must ensure that the enum is exhaustive as unknown discriminants can be used to +/// trigger undefined behavior. +/// The implementer must ensure that the enum does not have more states than the game defines. +/// Failing to do so will allow for out-of-bound access to the stepper array. +/// The implementer must ensure that the enum discriminants have no gaps. Failing to do so will +/// allow out of bounds access to the stepper array as well as cause unknown discriminants. +#[proc_macro_derive(StepperStates)] +pub fn derive_stepper_states(input: TokenStream) -> TokenStream { + fn error(ident: &Ident, message: &str) -> TokenStream { + syn::Error::new_spanned(ident, message) + .to_compile_error() + .into() + } + + let input = parse_macro_input!(input as DeriveInput); + let input_struct_ident = &input.ident; + + let Data::Enum(e) = &input.data else { + return error(&input.ident, "StepperStates can only be derived on enums"); + }; + + if let Err(e) = stepper::validate_stepper_enum_storage(&input) { + return e.to_compile_error().into(); + }; + + if let Err(e) = stepper::validate_stepper_enum_variants(e) { + return e.to_compile_error().into(); + }; + + let count = e.variants.len(); + let expanded = quote! { + unsafe impl ::fromsoftware_shared::StepperStates for #input_struct_ident { + type StepperFnArray = [TStepperFn; #count]; + } + }; + + TokenStream::from(expanded) +} diff --git a/crates/shared/macros/src/stepper.rs b/crates/shared/macros/src/stepper.rs new file mode 100644 index 00000000..82538a54 --- /dev/null +++ b/crates/shared/macros/src/stepper.rs @@ -0,0 +1,133 @@ +use std::collections::BTreeSet; + +use syn::{DataEnum, DeriveInput, Expr, ExprLit, ExprUnary, Fields, Lit, Meta, UnOp}; + +pub fn validate_stepper_enum_storage(i: &DeriveInput) -> syn::Result<()> { + let Some(repr_attr) = i.attrs.iter().find(|a| a.path().is_ident("repr")) else { + return Err(syn::Error::new_spanned( + &i.ident, + "Enum must apply a #[repr(i32)], there is currently no repr specified at all", + )); + }; + + let Meta::List(repr_args) = &repr_attr.meta else { + return Err(syn::Error::new_spanned( + &i.ident, + "Enum must apply a #[repr(i32)], the repr attribute currently has no arguments", + )); + }; + + if !repr_args + .tokens + .to_string() + .split(',') + .map(|s| s.trim()) + .any(|s| s == "i32") + { + return Err(syn::Error::new_spanned( + &i.ident, + "Enum must apply a #[repr(i32)]", + )); + } + + Ok(()) +} + +pub fn validate_stepper_enum_variants(e: &DataEnum) -> syn::Result<()> { + let mut values = BTreeSet::::new(); + + for v in &e.variants { + if !matches!(v.fields, Fields::Unit) { + return Err(syn::Error::new_spanned( + &v.ident, + "All variants must be unit", + )); + } + + let Some((_, expr)) = &v.discriminant else { + return Err(syn::Error::new_spanned( + &v.ident, + "All variants must have explicit discriminants (e.g. `GuestInviteWait = 3`)", + )); + }; + + let val = read_i32_lit(expr)?; + if val < 0 && val != -1 { + return Err(syn::Error::new_spanned( + &v.ident, + "Disciminant cannot be a negative unless it's the Inactive state", + )); + } + + if !values.insert(val) { + return Err(syn::Error::new_spanned( + &v.ident, + format!("Duplicate discriminant value {val}"), + )); + } + } + + if !values.contains(&-1) { + return Err(syn::Error::new_spanned( + &e.variants[0].ident, + "Missing Inactive variant with discriminant -1", + )); + } + + let non_sentinel: Vec = values.iter().copied().filter(|&x| x != -1).collect(); + if non_sentinel.is_empty() { + return Err(syn::Error::new_spanned( + &e.variants[0].ident, + "Stepper states must have more states than just the Inactive variant (-1)", + )); + } + + let min = *non_sentinel.first().unwrap(); + let max = *non_sentinel.last().unwrap(); + + let expected_len = (max - min + 1) as usize; + if expected_len != non_sentinel.len() { + let set: BTreeSet = non_sentinel.iter().copied().collect(); + let missing: Vec = (min..=max).filter(|x| !set.contains(x)).collect(); + + return Err(syn::Error::new_spanned( + &e.variants[0].ident, + format!("Discriminants contain gaps; missing values: {missing:?}"), + )); + } + + Ok(()) +} + +fn read_i32_lit(expr: &Expr) -> syn::Result { + match expr { + Expr::Lit(ExprLit { + lit: Lit::Int(i), .. + }) => i + .base10_parse::() + .map_err(|_| syn::Error::new_spanned(expr, "Discriminant out of i32 range")), + Expr::Unary(ExprUnary { + op: UnOp::Neg(_), + expr: inner, + .. + }) => match inner.as_ref() { + Expr::Lit(ExprLit { + lit: Lit::Int(i), .. + }) => { + let v = i + .base10_parse::() + .map_err(|_| syn::Error::new_spanned(inner, "Discriminant out of i32 range"))?; + v.checked_neg() + .ok_or_else(|| syn::Error::new_spanned(expr, "Discriminant out of i32 range")) + } + _ => Err(syn::Error::new_spanned( + expr, + "Use an integer literal like -1 or 3", + )), + }, + _ => Err(syn::Error::new_spanned( + expr, + "Use an integer literal like -1 or 3", + )), + } +} diff --git a/crates/shared/src/lib.rs b/crates/shared/src/lib.rs index c9108122..83a214f0 100644 --- a/crates/shared/src/lib.rs +++ b/crates/shared/src/lib.rs @@ -7,6 +7,7 @@ pub mod program; pub mod rtti; mod r#static; pub mod steam; +pub mod stepper; mod subclass; pub mod task; pub mod util; @@ -19,6 +20,7 @@ pub use program::*; pub use rtti::*; pub use r#static::*; pub use steam::*; +pub use stepper::*; pub use subclass::*; pub use task::*; pub use util::*; diff --git a/crates/shared/src/stepper.rs b/crates/shared/src/stepper.rs new file mode 100644 index 00000000..9ed4c73e --- /dev/null +++ b/crates/shared/src/stepper.rs @@ -0,0 +1,19 @@ +/// State indices for steppers. +/// +/// # Safety +/// +/// The implementer must ensure that the trait is implemented exclusively on enums with a +/// #[repr(i32)]. +/// The implementer must ensure that the enum contains only unit variants. +/// The implementer must ensure that the enum has a -1 discriminant to represent the inactive +/// state. +/// The implementer must ensure that the enum is exhaustive as unknown discriminants can be used to +/// trigger undefined behavior. +/// The implementer must ensure that the enum does not have more states than the game defines. +/// Failing to do so will allow for out-of-bound access to the stepper array. +/// The implementer must ensure that the enum discriminants have no gaps. Failing to do so will +/// allow out of bounds access to the stepper array as well as cause unknown discriminants. +pub unsafe trait StepperStates: Copy + std::fmt::Debug + 'static { + // GAT since we can't use the count itself on FD4StepTemplateBase. + type StepperFnArray: AsRef<[StepperFn]>; +} diff --git a/tools/debug-eldenring/src/display/net_man.rs b/tools/debug-eldenring/src/display/net_man.rs index b109d754..6b23a77b 100644 --- a/tools/debug-eldenring/src/display/net_man.rs +++ b/tools/debug-eldenring/src/display/net_man.rs @@ -105,6 +105,6 @@ impl DebugDisplay for CSBattleRoyalContext { impl DebugDisplay for CSQuickMatchingCtrl { fn render_debug(&self, ui: &Ui) { - ui.display_copiable("Match state", format!("{:?}", self.current_state)); + ui.display_copiable("Match state", format!("{:?}", self.stepper.current_state)); } }