From 1cde53adbde8e512a4669c0215d99a7bd73d0fd5 Mon Sep 17 00:00:00 2001 From: axd1x8a Date: Sat, 25 Jul 2026 05:44:16 +0300 Subject: [PATCH 1/3] Allow STL to use same address allocators --- crates/shared/stl/src/string.rs | 21 +++++++++++++-------- crates/shared/stl/src/vector.rs | 13 +++++++++---- crates/shared/stl/src/vector_bool.rs | 7 +++++-- 3 files changed, 27 insertions(+), 14 deletions(-) diff --git a/crates/shared/stl/src/string.rs b/crates/shared/stl/src/string.rs index 4b5ff8b1..80d51aaf 100644 --- a/crates/shared/stl/src/string.rs +++ b/crates/shared/stl/src/string.rs @@ -227,16 +227,21 @@ where } fn reallocate(&mut self, new_cap: usize) { + let old_ptr = self.as_ptr(); let new_ptr = self.allocator.allocate_n::(new_cap + 1).cast::(); - // Copy existing data + NUL terminator in one shot - unsafe { - std::ptr::copy_nonoverlapping(self.as_ptr(), new_ptr.as_ptr(), self.size + 1); - } - if !self.is_sso() { + // Some allocators (e.g. single-slot fixed-buffer allocators) can + // hand back the same address on every call; skip the copy/free + // then, since the data is already where it needs to be. + if !std::ptr::eq(old_ptr, new_ptr.as_ptr()) { unsafe { - self.allocator - .deallocate_raw(self.buffer.pointer.as_ptr() as _) - }; + std::ptr::copy_nonoverlapping(old_ptr, new_ptr.as_ptr(), self.size + 1); + } + if !self.is_sso() { + unsafe { + self.allocator + .deallocate_raw(self.buffer.pointer.as_ptr() as _) + }; + } } self.buffer.pointer = new_ptr; self.capacity = new_cap; diff --git a/crates/shared/stl/src/vector.rs b/crates/shared/stl/src/vector.rs index 7959c2ac..1756c64f 100644 --- a/crates/shared/stl/src/vector.rs +++ b/crates/shared/stl/src/vector.rs @@ -149,12 +149,17 @@ impl Vector { let old_cap = self.capacity(); let new_cap = (old_cap + old_cap / 2).max(old_cap + 1).max(4); - let new_ptr = self.allocator.allocate_n::(new_cap).as_ptr() as _; + let new_ptr: *mut T = self.allocator.allocate_n::(new_cap).as_ptr() as _; unsafe { - std::ptr::copy_nonoverlapping(self.first, new_ptr, old_len); - if old_cap > 0 { - self.allocator.deallocate_raw(self.first as _); + // Some allocators (e.g. single-slot fixed-buffer allocators) can + // hand back the same address on every call; skip the copy/free + // then, since the data is already where it needs to be. + if !std::ptr::eq(self.first, new_ptr) { + std::ptr::copy_nonoverlapping(self.first, new_ptr, old_len); + if old_cap > 0 { + self.allocator.deallocate_raw(self.first as _); + } } } diff --git a/crates/shared/stl/src/vector_bool.rs b/crates/shared/stl/src/vector_bool.rs index 9c2bc6a9..7b61f03a 100644 --- a/crates/shared/stl/src/vector_bool.rs +++ b/crates/shared/stl/src/vector_bool.rs @@ -188,9 +188,12 @@ impl VectorBool { let new_bits = (self.end + self.end / 2).max(VBITS).next_multiple_of(VBITS); let new_words = bits_to_words(new_bits); - let new_ptr = self.allocator.allocate_n::(new_words).as_ptr() as _; + let new_ptr: *mut VBase = self.allocator.allocate_n::(new_words).as_ptr() as _; unsafe { - if old_words > 0 { + // Some allocators (e.g. single-slot fixed-buffer allocators) can + // hand back the same address on every call; skip the copy/free + // then, since the data is already where it needs to be. + if old_words > 0 && !std::ptr::eq(self.first, new_ptr) { std::ptr::copy_nonoverlapping(self.first, new_ptr, old_words); self.allocator.deallocate_raw(self.first as _); } From ad2330afd2747ea81967274652dd6cd89966bd43 Mon Sep 17 00:00:00 2001 From: axd1x8a Date: Sat, 25 Jul 2026 05:52:30 +0300 Subject: [PATCH 2/3] Implement `DLFixedString` --- crates/eldenring/src/dlkr/allocator.rs | 3 + .../src/dlkr/allocator/fixed_std_allocator.rs | 138 +++++++++++++ crates/eldenring/src/dltx.rs | 190 +++++++++++++++++- 3 files changed, 330 insertions(+), 1 deletion(-) create mode 100644 crates/eldenring/src/dlkr/allocator/fixed_std_allocator.rs diff --git a/crates/eldenring/src/dlkr/allocator.rs b/crates/eldenring/src/dlkr/allocator.rs index 6a3fbe35..3a8e362c 100644 --- a/crates/eldenring/src/dlkr/allocator.rs +++ b/crates/eldenring/src/dlkr/allocator.rs @@ -1,3 +1,6 @@ +mod fixed_std_allocator; + +pub use fixed_std_allocator::*; use pelite::pe64::Pe; use shared::Program; use vtable_rs::VPtr; diff --git a/crates/eldenring/src/dlkr/allocator/fixed_std_allocator.rs b/crates/eldenring/src/dlkr/allocator/fixed_std_allocator.rs new file mode 100644 index 00000000..bbd1619f --- /dev/null +++ b/crates/eldenring/src/dlkr/allocator/fixed_std_allocator.rs @@ -0,0 +1,138 @@ +use bitfield::bitfield; +use std::cell::Cell; +use std::ffi::c_void; +use thiserror::Error; + +#[repr(C)] +struct DLFixedStdAllocatorFlags(Cell); + +impl DLFixedStdAllocatorFlags { + fn new() -> Self { + Self(Cell::new(unsafe { std::mem::zeroed() })) + } + + /// # Safety + /// + /// The returned reference is valid for the lifetime of `&self` and + /// aliases the last byte of the underlying `Cell`. + fn flags_cell(&self) -> &Cell { + let last_byte = self.0.as_ptr().wrapping_add(1).wrapping_byte_sub(1) + as *const Cell; + unsafe { &*last_byte } + } + + fn get(&self) -> AllocatorStateFlags { + self.flags_cell().get() + } + + fn set(&self, flags: AllocatorStateFlags) { + self.flags_cell().set(flags); + } +} + +#[repr(C)] +pub struct DLFixedStdAllocator { + buffer: Cell<[T; N]>, + flags: DLFixedStdAllocatorFlags, +} + +bitfield! { + #[repr(C)] + #[derive(Clone, Copy)] + pub struct AllocatorStateFlags(u8); + impl Debug; + /// Bit 0: "copied allocator" restriction + pub copied_allocator, set_copied_allocator: 0; + /// Bit 1: reentrancy guard, held only for the duration of a single + /// `allocate()` call + pub is_allocating, set_allocating: 1; +} + +impl Default for DLFixedStdAllocator { + fn default() -> Self { + Self { + buffer: Cell::new([unsafe { std::mem::zeroed() }; N]), + flags: DLFixedStdAllocatorFlags::new(), + } + } +} + +#[derive(Error, Debug)] +pub enum DLFixedStdAllocatorError { + #[error("Tried to allocate too large memory block from copied DLFixedStdAllocator.")] + CopiedAllocatorTooLarge, + #[error("Expected buffer size too large.")] + BufferSizeTooLarge, + #[error("Reentrant call to allocate() on the same DLFixedStdAllocator.")] + MemoryAlreadyAllocated, +} + +impl DLFixedStdAllocator { + /// Allocate from the fixed buffer + /// Returns an Error if: + /// - size > N (buffer overflow) + /// - copied_allocator flag set and size != 1 + /// - buffer already occupied (reentrant call) + pub fn allocate(&self, size: usize) -> Result<*mut T, DLFixedStdAllocatorError> { + if size > N { + return Err(DLFixedStdAllocatorError::BufferSizeTooLarge); + } + + let mut flags = self.flags.get(); + + if flags.copied_allocator() && size != 1 { + return Err(DLFixedStdAllocatorError::CopiedAllocatorTooLarge); + } + + if flags.is_allocating() { + return Err(DLFixedStdAllocatorError::MemoryAlreadyAllocated); + } + + flags.set_allocating(true); + self.flags.set(flags); + + // Return aligned pointer to buffer + let buffer_ptr = self.buffer.as_ptr().cast::(); + let alignment_offset = (-(buffer_ptr as isize) & 1) as usize; + let ptr = unsafe { buffer_ptr.byte_add(alignment_offset * std::mem::size_of::()) }; + + flags.set_allocating(false); + self.flags.set(flags); + + Ok(ptr) + } + + pub fn deallocate(&self) {} +} + +impl Clone for DLFixedStdAllocator { + fn clone(&self) -> Self { + let mut flags = self.flags.get(); + flags.set_copied_allocator(true); + let cloned_flags = DLFixedStdAllocatorFlags::new(); + cloned_flags.set(flags); + Self { + buffer: Cell::new(self.buffer.get()), + flags: cloned_flags, + } + } +} + +impl fromsoftware_shared_stl::StlAllocator + for DLFixedStdAllocator +{ + unsafe fn allocate_raw(&self, size: usize, align: usize) -> *mut c_void { + debug_assert!(align <= std::mem::align_of::()); + + let count = size.div_ceil(std::mem::size_of::()).max(1); + + match self.allocate(count) { + Ok(ptr) => ptr as *mut c_void, + Err(err) => panic!("DLFixedStdAllocator failed to allocate: {err}"), + } + } + + unsafe fn deallocate_raw(&self, _ptr: *mut c_void) { + self.deallocate(); + } +} diff --git a/crates/eldenring/src/dltx.rs b/crates/eldenring/src/dltx.rs index 8440ad6c..7b5d7ef7 100644 --- a/crates/eldenring/src/dltx.rs +++ b/crates/eldenring/src/dltx.rs @@ -3,12 +3,13 @@ use std::fmt; use std::hash::{Hash, Hasher}; use std::ops::{Deref, DerefMut}; use std::ptr::NonNull; +use std::str::FromStr; use encoding_rs::DecoderResult; use fromsoftware_shared_stl::{BasicString, CodeUnit}; use thiserror::Error; -use crate::dlkr::DLAllocator; +use crate::dlkr::{DLAllocator, DLFixedStdAllocator}; #[repr(u8)] #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)] @@ -709,3 +710,190 @@ impl fmt::Debug for DLInplaceStr { self.base.fmt(f) } } + +#[repr(C)] +/// Fixed-size string with embedded fixed buffer allocator. +/// String itself is stored in either the sso buffer or fixed allocator's buffer. +pub struct DLFixedString { + pub base: BasicString>, + pub encoding: DLCharacterSet, +} + +impl DLFixedString { + fn decode_storage(&self) -> Result, DLStringError> { + T::decode(self.base.as_bytes()) + } + + pub fn new() -> Self { + Self { + base: BasicString::new_in(DLFixedStdAllocator::::default()), + encoding: T::ENCODING, + } + } + + /// Replaces the entire content by encoding a UTF-8 string. + pub fn assign_str(&mut self, s: impl AsRef) -> Result<(), DLStringError> { + let units = T::encode(s.as_ref())?; + self.base.assign(&units); + Ok(()) + } + + /// Decodes the stored bytes to an owned UTF-8 `String` + pub fn to_string(&self) -> Result { + self.decode_storage().map(Cow::into_owned) + } + + /// Transcodes from a `DLFixedString` of a different kind. + /// If the encodings match the bytes are copied directly without going + /// through UTF-8 + pub fn transcode_from( + other: &DLFixedString, + ) -> Result { + if T::ENCODING == U::ENCODING { + // Safety: T::Unit and U::Unit are guaranteed to be the same here + let units: &[::Unit] = + unsafe { std::mem::transmute(other.base.as_code_units()) }; + Ok(Self { + base: BasicString::from_units_in( + units, + DLFixedStdAllocator::::default(), + ), + encoding: T::ENCODING, + }) + } else { + Self::from_str(&other.decode_storage()?) + } + } +} + +impl FromStr for DLFixedString { + type Err = DLStringError; + fn from_str(s: &str) -> Result { + let units = T::encode(s)?; + Ok(Self { + base: BasicString::from_units_in(&units, DLFixedStdAllocator::::default()), + encoding: T::ENCODING, + }) + } +} +impl TryFrom<&str> for DLFixedString { + type Error = DLStringError; + fn try_from(s: &str) -> Result { + Self::from_str(s) + } +} + +impl Default for DLFixedString { + fn default() -> Self { + Self::new() + } +} + +impl Deref for DLFixedString { + type Target = BasicString>; + + fn deref(&self) -> &Self::Target { + &self.base + } +} + +impl DerefMut for DLFixedString { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.base + } +} + +impl fmt::Display for DLFixedString { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self.to_string() { + Ok(s) => f.write_str(&s), + Err(_) => Err(fmt::Error), + } + } +} + +impl fmt::Debug for DLFixedString { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self.to_string() { + Ok(s) => write!(f, "DLFixedString({:?}, {:?})", T::ENCODING, s), + Err(_) => write!(f, "DLFixedString({:?}, )", T::ENCODING), + } + } +} + +/// `DLFixedString == DLFixedString`: byte comparison when same +/// encoding, UTF-8 round-trip when different. +impl + PartialEq> for DLFixedString +{ + fn eq(&self, other: &DLFixedString) -> bool { + if T::ENCODING == U::ENCODING { + self.base.as_bytes() == other.base.as_bytes() + } else { + match (self.to_string(), other.to_string()) { + (Ok(a), Ok(b)) => a == b, + _ => false, + } + } + } +} + +impl Eq for DLFixedString {} + +/// `DLFixedString == &str`, `DLFixedString == String`, `DLFixedString == Cow`, etc. +/// +/// For UTF-8/16/32 this is allocation-free. +/// For legacy encodings (Shift-JIS, EUC-JP, ISO-8859-1) it uses a +/// stack-allocated 64-byte decode buffer. +impl> PartialEq for DLFixedString { + fn eq(&self, other: &S) -> bool { + bytes_eq_str(self.base.as_bytes(), T::ENCODING, other.as_ref()) + } +} + +impl Ord for DLFixedString +where + T::Unit: Ord, +{ + #[inline] + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.base.cmp(&other.base) + } +} + +impl + PartialOrd> for DLFixedString +{ + #[inline] + fn partial_cmp(&self, other: &DLFixedString) -> Option { + if T::ENCODING == U::ENCODING { + self.base.as_bytes().partial_cmp(other.base.as_bytes()) + } else { + match (self.to_string(), other.to_string()) { + (Ok(a), Ok(b)) => a.partial_cmp(&b), + _ => None, + } + } + } +} + +impl> PartialOrd for DLFixedString { + #[inline] + fn partial_cmp(&self, other: &S) -> Option { + if T::ENCODING == DLCharacterSet::UTF8 { + self.base.as_bytes().partial_cmp(other.as_ref().as_bytes()) + } else { + self.to_string() + .ok() + .as_deref() + .partial_cmp(&Some(other.as_ref())) + } + } +} + +impl Hash for DLFixedString { + fn hash(&self, state: &mut H) { + T::ENCODING.hash(state); + self.base.as_code_units().hash(state); + } +} From b81560213c978fd30df7d610ab3653786805b4cf Mon Sep 17 00:00:00 2001 From: axd1x8a Date: Sat, 25 Jul 2026 05:53:01 +0300 Subject: [PATCH 3/3] Model more of MSB-related structures --- crates/eldenring/src/cs/chr_ins.rs | 28 +- crates/eldenring/src/cs/field_area.rs | 3 +- crates/eldenring/src/cs/net_man.rs | 28 +- crates/eldenring/src/cs/player_game_data.rs | 41 +- crates/eldenring/src/cs/world_geom_man.rs | 381 +++++++++++++----- tools/debug-eldenring/src/display/chr.rs | 4 + tools/debug-eldenring/src/display/geometry.rs | 92 ++++- tools/debug-eldenring/src/display/net_man.rs | 9 +- 8 files changed, 459 insertions(+), 127 deletions(-) diff --git a/crates/eldenring/src/cs/chr_ins.rs b/crates/eldenring/src/cs/chr_ins.rs index 221865b7..d7bd9d70 100755 --- a/crates/eldenring/src/cs/chr_ins.rs +++ b/crates/eldenring/src/cs/chr_ins.rs @@ -11,7 +11,10 @@ use crate::cs::session_manager::SessionManagerPlayerEntryBase; use crate::cs::sp_effect::{NpcSpEffectEquipCtrl, SpecialEffect}; use crate::cs::task::{CSEzRabbitNoUpdateTask, CSEzVoidTask}; use crate::cs::world_chr_man::{ChrSetEntry, WorldChrMan}; -use crate::cs::{BlockId, CSPlayerMenuCtrl, EquipmentDurabilityStatus, OptionalItemId}; +use crate::cs::{ + BlockId, CSEzRabbitTask, CSEzUpdateTask, CSPlayerMenuCtrl, EquipmentDurabilityStatus, + OptionalItemId, +}; use crate::dltx::DLString; use crate::fd4::FD4Time; use crate::param::{ATK_PARAM_ST, NPC_PARAM_ST}; @@ -255,11 +258,14 @@ pub struct ChrIns { /// sfx. pub mimicry_establishment_param_id: i32, unk228: u32, - unk22c: u32, + /// Light set ID affecting this character in GParams. + /// Eg. 1100 for main player or 100 for most enemies. + pub gparam_light_set_id: i32, // Possibly contains some id related to current gparam and attached to chr geometry unk230: u32, - // Same as above - unk234: u32, + /// Fog ID affecting this character in GParams. + /// Eg. 1100 for main player or 100 for most enemies. + pub gparam_fog_id: i32, /// Transparency multiplier for the character /// Controlled by TAE Event 193 SetOpacityKeyframe pub opacity_keyframes_multiplier: f32, @@ -817,6 +823,10 @@ pub struct CSModelIns { pub model_item: OwnedPtr, pub model_disp_entity: usize, pub location_entity: usize, + unk28: usize, + draw_param: [u8; 0xd0], + pub update_task: CSEzUpdateTask, + unk130: [u8; 0x20], } #[repr(C)] @@ -1047,17 +1057,21 @@ pub enum ChrType { Local = 0, WhitePhantom = 1, Duelist = 2, - Ghost = 3, + /// Name Source: %s_Wander string + WanderGhost = 3, Ghost1 = 4, Npc = 5, Unk6 = 6, Unk7 = 7, GrayPhantom = 8, Unk9 = 9, - BloodstainGhost = 10, - BonfireGhost = 11, + /// Name Source: %s_Replay string + ReplayGhost = 10, + /// Name Source: %s_Display string + DisplayGhost = 11, Unk12 = 12, Arena = 13, + /// Name Source: %s_Message string MessageGhost = 14, BloodyFinger = 15, Recusant = 16, diff --git a/crates/eldenring/src/cs/field_area.rs b/crates/eldenring/src/cs/field_area.rs index 81debc7f..ae0e9d9e 100644 --- a/crates/eldenring/src/cs/field_area.rs +++ b/crates/eldenring/src/cs/field_area.rs @@ -181,7 +181,8 @@ pub struct WorldBlockInfo { pub world_area_info_index: i32, unk3c: u32, unk40: bool, - unk41: [u8; 0x7], + /// Whether this block is a skybox block (index 99). + pub is_skybox_block: bool, msb_res_cap: NonNull<()>, unk50: u64, unk58: u64, diff --git a/crates/eldenring/src/cs/net_man.rs b/crates/eldenring/src/cs/net_man.rs index b28d17f5..8a93a961 100644 --- a/crates/eldenring/src/cs/net_man.rs +++ b/crates/eldenring/src/cs/net_man.rs @@ -2,6 +2,7 @@ use std::ptr::NonNull; use crate::{ DLVector, + cs::{DisplayGhostData, PasswordData}, cs::{MultiplayRole, MultiplayType, SummonParamType}, dltx::DLString, fd4::{FD4StepBase, FD4StepBaseInterface, FD4Time}, @@ -62,7 +63,7 @@ pub struct CSNetBloodMessageDb { pub entries: DLList>, unk20: usize, /// Seemingly contains message data for messages created by local user - pub created_data: DLList, + pub created_data: DLList>, // Contains ??? unk40: DLList, unk58: usize, @@ -75,6 +76,31 @@ pub struct CSNetBloodMessageDb { unk160: usize, } +#[repr(C)] +pub struct CSNetBloodMessageCreatedData { + pub player_id: u32, + unk4: [u8; 8], + pub message_id: u64, + pub block_id: BlockId, + unk1c: u32, + pub position: BlockPosition, + pub template1: u16, + pub gesture_param: u16, + pub part1: u16, + pub infix: u16, + pub template2: u16, + pub part2: u16, + unk3c: u16, + pub display_ghost: DisplayGhostData, + pub character_name: [u16; 20], + pub positive_rating: u16, + pub negative_rating: u16, + unkfc: u16, + unkfe: [u8; 2], + pub group_passwords: [PasswordData; 5], + pub net_blood_message_db_item: OwnedPtr, +} + #[repr(C)] pub struct CSNetBloodMessageDbItem { vftable: usize, diff --git a/crates/eldenring/src/cs/player_game_data.rs b/crates/eldenring/src/cs/player_game_data.rs index 6950ecf0..06312e87 100644 --- a/crates/eldenring/src/cs/player_game_data.rs +++ b/crates/eldenring/src/cs/player_game_data.rs @@ -100,18 +100,9 @@ pub struct PlayerGameData { unk108: u8, pub reached_max_rune_memory: u8, unk10a: [u8; 0xE], - pub password: [u16; 0x8], - unk128: u16, - group_password_1: [u16; 0x8], - unk13a: u16, - group_password_2: [u16; 0x8], - unk14c: u16, - group_password_3: [u16; 0x8], - unk15e: u16, - group_password_4: [u16; 0x8], - unk170: u16, - group_password_5: [u16; 0x8], - unk182: [u8; 0x36], + pub password: PasswordData, + pub group_passwords: [PasswordData; 5], + unk184: [u8; 0x34], pub sp_effects: [PlayerGameDataSpEffect; 0xD], /// Level after any buffs and corrections pub effective_vigor: u32, @@ -244,6 +235,32 @@ pub enum PlayerDataInvasionItemType { RecusantFinger = 2, } +#[repr(C)] +pub struct PasswordData { + /// Raw wchar_t data with null terminator + pub raw: [u16; 9], +} + +impl PasswordData { + /// Checks if the password is empty + pub fn is_empty(&self) -> bool { + self.raw[0] == 0 + } +} + +impl TryInto for &PasswordData { + type Error = std::string::FromUtf16Error; + + fn try_into(self) -> Result { + let len = self + .raw + .iter() + .position(|&c| c == 0) + .unwrap_or(self.raw.len()); + String::from_utf16(&self.raw[..len]) + } +} + #[repr(C)] pub struct PlayerDataAttackRating { pub left_armament_primary: i32, diff --git a/crates/eldenring/src/cs/world_geom_man.rs b/crates/eldenring/src/cs/world_geom_man.rs index e3e4d2ce..58b990b8 100644 --- a/crates/eldenring/src/cs/world_geom_man.rs +++ b/crates/eldenring/src/cs/world_geom_man.rs @@ -1,9 +1,13 @@ use std::{fmt::Formatter, mem::transmute, ptr::NonNull}; use pelite::pe64::Pe; +use shared::{F32ModelMatrix, F32Vector3, F32Vector4}; +use vtable_rs::VPtr; use windows::core::PCWSTR; use super::{BlockId, FieldInsHandle, WorldInfoOwner}; +use crate::cs::{CSModelIns, FieldInsBaseVmt}; +use crate::dltx::{DLFixedString, DLUTF16StringKind}; use crate::position::BlockPosition; use crate::{DLMap, DLVector, param::ASSET_GEOMETORY_PARAM_ST, rva}; use shared::{OwnedPtr, Subclass, Superclass, program::Program}; @@ -58,9 +62,13 @@ pub struct CSWorldGeomManBlockData { /// Seems to be the next field ins index that will be assiged. pub next_geom_ins_field_ins_index: u32, /// Seems to indicate if the geometry_ins vector has reached some hardcoded capacity? - unk334: bool, + pub reached_geom_ins_vector_capacity: bool, _pad335: [u8; 3], - unk338: [u8; 0x50], + pub geom_event_entity_id_map: DLMap, + unk350: usize, + unk358: usize, + unk360: usize, + ladder_geometry: DLVector<()>, pub sos_sign_geometry: DLVector>>, pub disable_on_singleplay_geometry: DLVector>>, unk3c8: [u8; 0x2E0], @@ -106,27 +114,10 @@ impl CSWorldGeomManBlockData { }; let mut request = GeometrySpawnRequest { - asset_string: [0u16; 0x20], - unk40: 0, - unk44: 0, - asset_string_ptr: 0, - unk50: 0, - unk54: 0, - unk58: 0, - unk5c: 0, - unk60: 0, - unk64: 0, - unk68: 0, - unk6c: 0, - pos_x: 0.0, - pos_y: 0.0, - pos_z: 0.0, - rot_x: 0.0, - rot_y: 0.0, - rot_z: 0.0, - scale_x: 0.0, - scale_y: 0.0, - scale_z: 0.0, + asset_string: Default::default(), + block_pos: F32Vector3(0.0, 0.0, 0.0), + rotation: F32Vector3(0.0, 0.0, 0.0), + scale: F32Vector3(1.0, 1.0, 1.0), unk94: [0u8; 0x6C], }; @@ -134,16 +125,10 @@ impl CSWorldGeomManBlockData { request.set_asset(asset); let BlockPosition { x, y, z, yaw: _ } = parameters.position; - request.pos_x = x; - request.pos_y = y; - request.pos_z = z; + request.block_pos = F32Vector3(x, y, z); - request.rot_x = parameters.rot_x; - request.rot_y = parameters.rot_y; - request.rot_z = parameters.rot_z; - request.scale_x = parameters.scale_x; - request.scale_y = parameters.scale_y; - request.scale_z = parameters.scale_z; + request.rotation = F32Vector3(parameters.rot_x, parameters.rot_y, parameters.rot_z); + request.scale = F32Vector3(parameters.scale_x, parameters.scale_y, parameters.scale_z); spawn_geometry(self, &request) } @@ -154,13 +139,57 @@ impl CSWorldGeomManBlockData { /// /// Source of name: RTTI pub struct CSWorldGeomIns { - vfptr: usize, + vftable: VPtr, pub field_ins_handle: FieldInsHandle, /// Points to the map data hosting this GeomIns. pub block_data: NonNull, /// Points to the world placement data for this geometry instance. pub info: CSWorldGeomInfo, - unk1a8: [u8; 0x288], + pub res_proxy: CSWorldGeomResProxy, + unk1e0: usize, + unk1e8: [u8; 0x28], + geombnd_res_cap: usize, + pub model_matrix: F32ModelMatrix, + unk260: [u8; 0x20], + pub render_data: CSGeomInsRenderData, + unk340: [u8; 0xf0], +} + +#[repr(C)] +pub struct CSGeomInsRenderData { + pub model_tint: F32Vector4, + pub use_alpha_blend: bool, + unk20: F32Vector4, + unk30: F32Vector4, + unk40: F32Vector4, + unk50: F32Vector4, + pub transparency: f32, + unk70: F32Vector4, + unk80: F32Vector4, + unk90: u32, + unk94: u32, + unk98: u8, + unk99: u8, + unka0: F32Vector4, + unkb0: f32, + unkb8: f32, +} + +#[repr(C)] +pub struct CSGeomModelIns { + pub base: CSModelIns, + unk150: [u8; 0x20], +} + +#[repr(C)] +pub struct CSWorldGeomResProxy { + unk0: usize, + unk8: usize, + unk10: usize, + pub owner: NonNull, + unk20: usize, + pub model_ins: OwnedPtr, + unk30: i32, } #[repr(C)] @@ -173,14 +202,13 @@ pub struct CSWorldGeomInfo { /// Points to the param row this geometry instance uses. pub asset_geometry_param: NonNull, unk10: u32, - unk14: u32, pub msb_parts_geom: CSMsbPartsGeom, unk68: u32, unk6c: u32, unk70: u32, unk74: u32, - unk78: CSWorldGeomInfoUnk, - unke0: CSWorldGeomInfoUnk, + unk78: CSWorldGeomInfoRenderInfo, + unke0: CSWorldGeomInfoRenderInfo, unk148: u16, unk14a: u8, unk14b: u8, @@ -202,9 +230,9 @@ pub struct CSWorldGeomInfo { unk170: f32, pub sound_obj_enable_dist: f32, unk178: u8, - unk179: u8, + /// Whether this geometry is part of the skybox. + pub is_on_skybox: bool, unk17a: u8, - unk17c: u8, /// Source of name: Params being copied over pub has_tex_lv01_border_dist: bool, /// Source of name: Params being copied over @@ -212,28 +240,22 @@ pub struct CSWorldGeomInfo { /// Source of name: Params being copied over pub is_trace_camera_xz: bool, /// Source of name: Params being copied over + pub is_sky_dome_draw_phase: bool, + /// Source of name: Params being copied over pub forward_draw_envmap_blend_type: bool, unk180: u16, unk182: u16, /// Hides the object whenever the player is alone, used for fogwalls and such. - pub disable_on_singleplay: u8, + pub disable_on_singleplay: bool, unk185: u8, unk186: u16, unk188: usize, } #[repr(C)] -pub struct CSWorldGeomInfoUnk { - unk0: u32, - unk4: u32, - unk8: u32, - unkc: u32, - unk10: u32, - unk14: u32, - unk18: u32, - unk1c: u32, - unk20: usize, - unk28: [u8; 0x38], +pub struct CSWorldGeomInfoRenderInfo { + pub render_group_mask: [u8; 0x20], + unk20: [u8; 0x40], unk60: usize, } @@ -250,11 +272,20 @@ pub struct CSMsbPartsGeom { /// Seems to describe how to draw the MSB part. pub struct CSMsbParts { vfptr: usize, - pub draw_flags: u32, - _padc: u32, + msb_res_cap: usize, unk10: usize, - pub msb_part: OwnedPtr, - unk20: [u8; 0x30], + /// Owned by MsbResCap + pub msb_part: NonNull, + unk20: usize, + pub msb_geom_info: OwnedPtr, + /// Temporary storage for the MsbPart during some processing. + /// Should be used instead of [Self::msb_part] if set. + msb_part_temp_storage: Option>, + /// Temporary storage for the MsbGeomModelInfo during some processing. + /// Should be used instead of [Self::msb_geom_info] if set. + pub msb_geom_info_temp_storage: Option>, + pub map_studio_layer_mask: i32, + unk44: F32Vector3, } #[repr(C)] @@ -264,61 +295,207 @@ pub struct CSMsbPartsEne { pub cs_msb_parts: CSMsbParts, } +#[repr(C)] +pub struct MsbGeomModelInfo { + /// Actual name of AEG or map piece. + pub model_name: PCWSTR, + unk8: u32, + unkc: i32, + /// Path to SIB file used by this model. + pub sib_path: PCWSTR, + unk18: [u8; 0x10], +} + #[repr(C)] pub struct MsbPart { + /// Name of the part as defined in the MSB. + /// + /// IMPORTANT: This is NOT the model name, see [MsbGeomModelInfo::model_name] for that. pub name: PCWSTR, - // TODO: rest + pub instance_id: i32, + pub part_type: MsbPartType, + /// Same as [crate::cs::FieldInsSelector::index] on Enemy parts. + /// Used to create FieldInsHandles. + pub field_ins_index: i32, + unk14: i32, + /// Path to SIB file, set on msb load + pub model_placeholder_path: PCWSTR, + pub position: F32Vector3, + pub rotation: F32Vector3, + pub scale: F32Vector3, + unk44: i32, + pub map_studio_layer: i32, + pub display_data: OwnedPtr, + /// Only set for [MsbPartType::Asset], [MsbPartType::ConnectCollision] and [MsbPartType::Collision] parts. + pub display_group_data: Option>, + pub msb_part_entity: OwnedPtr, + /// Type-specific data + unk68: usize, + /// Set for all types except [MsbPartType::Player] and [MsbPartType::ConnectCollision]. + pub gparam_config: Option>, + /// Only set for [MsbPartType::Collision] parts. + pub scene_gparam: Option>, + /// Only set for [MsbPartType::MapPiece] and [MsbPartType::Asset]. + pub grass_config: Option>, + unk90: OwnedPtr, + /// Only set for [MsbPartType::MapPiece] and [MsbPartType::Asset]. + unk98: Option>, + pub tile_load_config: OwnedPtr, + /// Only set for [MsbPartType::MapPiece], [MsbPartType::Asset], [MsbPartType::ConnectCollision] and [MsbPartType::Collision]. + unka0: Option>, +} + +#[repr(u32)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum MsbPartType { + MapPiece = 0, + Enemy = 2, + Player = 4, + Collision = 5, + DummyAsset = 9, + DummyEnemy = 10, + ConnectCollision = 11, + Asset = 13, +} + +#[repr(C)] +pub struct MsbPartDisplayData { + pub display_groups: [u32; 8], + pub draw_groups: [u32; 8], + /// Source of name: PrimDispMask_%s in CSRemoModelPrimDispMaskAct ctor + pub prim_disp_masks: [u32; 32], + unkc0: u8, + unkc1: u8, + unkc2: u8, + unkc3: u8, + unkc4: u16, + unkc6: u16, + reserved: [u32; 48], +} + +#[repr(C)] +pub struct MsbPartDisplayGroupData { + pub condition: i32, + pub display_groups: [u32; 8], + unk24: u16, + unk26: i16, + reserved: [u32; 8], +} + +#[repr(C)] +pub struct MsbPartEntity { + pub entity_id: i32, + pub is_use_parts_draw_param_id: bool, + unk5: u8, + unk6: u8, + pub lantern_id: u8, + pub parts_draw_param_id: i16, + pub point_light_shadow_source: i8, + unkb: i8, + pub shadow_source: bool, + pub static_shadow_source: bool, + pub cascade3_shadow_source: bool, + unk10: u8, + unk11: u8, + pub is_shadow_destination: bool, + pub is_shadow_only: bool, + pub draw_by_reflect_camera: bool, + pub draw_only_reflect_camera: bool, + pub use_depth_bias: bool, + pub disable_point_light_effect: u8, + unk18: u8, + pub entity_group_ids: [i32; 8], + unk3c: u16, + unk3e: u8, + pub disable_rtao: bool, +} + +#[repr(C)] +pub struct MsbGeomGparamConfig { + pub light_set_id: i32, + pub fog_id: i32, + pub light_scattering_id: i32, + pub environment_map_id: i32, + reserved: [u32; 4], +} + +#[repr(C)] +pub struct MsbSceneGparamConfig { + /// Supposedly unused + unk0: [u32; 4], + pub transition_time: f32, + unk14: u32, + pub gparam_sub_id: i32, + unk1c: i8, + unk1d: i8, + unk20: i8, + unk21: i8, + unk24: [u32; 11], +} + +#[repr(C)] +pub struct MsbPartsGrassConfig { + pub grass_type_params: [u32; 5], + unk18: i16, + unk1a: i16, +} + +#[repr(C)] +pub struct MsbPartsUnk8 { + unk0: [u8; 0x20], +} + +#[repr(C)] +pub struct MsbPartsUnk9 { + unk0: [u8; 0x20], +} + +#[repr(C)] +pub struct MsbPartTileLoadConfig { + /// Block ID this part is associated with. + /// Some MSBs can host parts for completely different maps, so this field is used to track that. + pub target_block_id: BlockId, + /// Offset in characters where to start reading [MsbPart::name] for some of the operations (eg search by name). + pub part_name_string_start_offset: u8, + unk8: u32, + unkc: u32, + unk10: u32, + pub culling_height_behavior: u32, + unk18: u32, + unk1c: u32, +} + +#[repr(C)] +pub struct MsbPartUnk11 { + unk0: [u8; 0x20], } #[repr(C)] /// Used by the game to seperate geometry spawning code (like MSB parser) from the actual GeomIns /// construction details. pub struct GeometrySpawnRequest { - /// Contains the asset string, ex. "AEG020_370" - pub asset_string: [u16; 0x20], - pub unk40: u32, - pub unk44: u32, - /// Contains a pointer to the asset string - pub asset_string_ptr: u64, - pub unk50: u32, - pub unk54: u32, - pub unk58: u32, - pub unk5c: u32, - pub unk60: u32, - pub unk64: u32, - pub unk68: u32, - pub unk6c: u32, - pub pos_x: f32, - pub pos_y: f32, - pub pos_z: f32, - pub rot_x: f32, - pub rot_y: f32, - pub rot_z: f32, - pub scale_x: f32, - pub scale_y: f32, - pub scale_z: f32, + pub asset_string: DLFixedString, + pub block_pos: F32Vector3, + pub rotation: F32Vector3, + pub scale: F32Vector3, pub unk94: [u8; 0x6C], } impl GeometrySpawnRequest { pub fn asset(&self) -> String { - let mut result = String::new(); - for val in self.asset_string.iter() { - let c: u8 = (*val & 0xFF) as u8; - if c == 0 { - break; - } else { - result.push(c as char); - } - } - result + self.asset_string.to_string().unwrap_or_default() } - // TODO: guard against strings that are too long + /// Sets the asset name. + /// + /// # Panics + /// + /// Panics if `asset` (in UTF-16 code units, plus the null terminator) + /// does not fit in the fixed 32-unit buffer. pub fn set_asset(&mut self, asset: &str) { - for (i, char) in asset.as_bytes().iter().enumerate() { - self.asset_string[i] = *char as u16; - } + self.asset_string + .assign_str(asset) + .expect("asset name failed to encode as UTF-16") } } @@ -326,15 +503,15 @@ impl std::fmt::Debug for GeometrySpawnRequest { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { f.debug_struct("GeometrySpawnRequest") .field("asset", &self.asset()) - .field("positionX", &self.pos_x) - .field("positionY", &self.pos_y) - .field("positionZ", &self.pos_z) - .field("rotationX", &self.rot_x) - .field("rotationY", &self.rot_y) - .field("rotationZ", &self.rot_z) - .field("scaleX", &self.scale_x) - .field("scaleY", &self.scale_y) - .field("scaleZ", &self.scale_z) + .field("positionX", &self.block_pos.0) + .field("positionY", &self.block_pos.1) + .field("positionZ", &self.block_pos.2) + .field("rotationX", &self.rotation.0) + .field("rotationY", &self.rotation.1) + .field("rotationZ", &self.rotation.2) + .field("scaleX", &self.scale.0) + .field("scaleY", &self.scale.1) + .field("scaleZ", &self.scale.2) .finish() } } diff --git a/tools/debug-eldenring/src/display/chr.rs b/tools/debug-eldenring/src/display/chr.rs index 84d52945..37c2e6c1 100644 --- a/tools/debug-eldenring/src/display/chr.rs +++ b/tools/debug-eldenring/src/display/chr.rs @@ -661,6 +661,7 @@ impl StatefulDebugDisplay for ChrIns { fn chr_ins_common_debug(chr_ins: &mut ChrIns, ui: &Ui, state: &mut ChrInsState) { ui.display("Team", chr_ins.team_type); ui.debug("Chr Type", chr_ins.chr_type); + ui.display("Character ID", chr_ins.character_id); ui.display("Field Ins Handle", chr_ins.field_ins_handle); ui.display("P2P Entity Handle", &chr_ins.p2p_entity_handle); @@ -673,6 +674,9 @@ fn chr_ins_common_debug(chr_ins: &mut ChrIns, ui: &Ui, state: &mut ChrInsState) ui.nested("Initial Position", chr_ins.initial_position); ui.nested("Initial Orientation", chr_ins.initial_orientation_euler); + ui.display("Light Set ID", chr_ins.gparam_light_set_id); + ui.display("Fog ID", chr_ins.gparam_fog_id); + ui.display("Last hit by", chr_ins.last_hit_by); ui.debug("TAE use item", chr_ins.tae_queued_use_item); diff --git a/tools/debug-eldenring/src/display/geometry.rs b/tools/debug-eldenring/src/display/geometry.rs index 3a3be43d..82a19db3 100644 --- a/tools/debug-eldenring/src/display/geometry.rs +++ b/tools/debug-eldenring/src/display/geometry.rs @@ -1,7 +1,10 @@ +use eldenring::cs::{ + CSMsbParts, CSMsbPartsGeom, CSWorldGeomInfo, CSWorldGeomIns, CSWorldGeomMan, + CSWorldGeomManBlockData, MsbPart, +}; use hudhook::imgui::Ui; use debug::UiExt; -use eldenring::cs::{CSWorldGeomIns, CSWorldGeomMan, CSWorldGeomManBlockData}; use super::{DebugDisplay, DisplayUiExt}; @@ -39,6 +42,7 @@ impl DebugDisplay for CSWorldGeomManBlockData { .msb_parts_geom .msb_parts .msb_part + .as_ref() .name .to_string() } @@ -67,6 +71,7 @@ impl DebugDisplay for CSWorldGeomManBlockData { .msb_parts_geom .msb_parts .msb_part + .as_ref() .name .to_string() } @@ -88,5 +93,90 @@ impl DebugDisplay for CSWorldGeomManBlockData { } impl DebugDisplay for CSWorldGeomIns { + fn render_debug(&self, ui: &Ui) { + ui.text(format!("Field Ins Handle: {}", self.field_ins_handle)); + ui.header("World geom info", || { + self.info.render_debug(ui); + }); + } +} + +impl DebugDisplay for CSWorldGeomInfo { + fn render_debug(&self, ui: &Ui) { + ui.header("CSMsbPartsGeom", || { + self.msb_parts_geom.render_debug(ui); + }); + ui.text(format!("Far clip distance: {}", self.far_clip_distance)); + ui.text(format!( + "Distant view model border distance: {}", + self.distant_view_model_border_dist + )); + ui.text(format!( + "Distant view model play distance: {}", + self.distant_view_model_play_dist + )); + ui.text(format!( + "Limited activate border distance for grid: {}", + self.limted_activate_border_dist_for_grid + )); + ui.text(format!( + "Limited activate play distance for grid: {}", + self.limted_activate_play_dist_for_grid + )); + ui.text(format!( + "Z sort offset for no far clip draw: {}", + self.z_sort_offset_for_no_far_clip_draw + )); + ui.text(format!( + "Sound object enable distance: {}", + self.sound_obj_enable_dist + )); + ui.text(format!( + "Has texture lv01 border distance: {}", + self.has_tex_lv01_border_dist + )); + ui.text(format!("Is no far clip draw: {}", self.is_no_far_clip_draw)); + ui.text(format!("Is trace camera xz: {}", self.is_trace_camera_xz)); + ui.text(format!( + "Forward draw envmap blend type: {}", + self.forward_draw_envmap_blend_type + )); + ui.text(format!( + "Disable on singleplay: {}", + self.disable_on_singleplay + )); + } +} + +impl DebugDisplay for CSMsbPartsGeom { + fn render_debug(&self, ui: &Ui) { + self.msb_parts.render_debug(ui); + } +} + +impl DebugDisplay for CSMsbParts { fn render_debug(&self, _ui: &Ui) {} } + +impl DebugDisplay for MsbPart { + fn render_debug(&self, ui: &Ui) { + unsafe { + let name = self + .name + .to_string() + .unwrap_or_else(|_| "".to_string()); + ui.text(format!("Name: {}", name)); + } + ui.text(format!("Instance ID: {}", self.instance_id)); + ui.text(format!("Map studio layer: {}", self.map_studio_layer)); + ui.header("Position", || { + self.position.render_debug(ui); + }); + ui.header("Rotation", || { + self.rotation.render_debug(ui); + }); + ui.header("Scale", || { + self.scale.render_debug(ui); + }); + } +} diff --git a/tools/debug-eldenring/src/display/net_man.rs b/tools/debug-eldenring/src/display/net_man.rs index 48915478..e65f421a 100644 --- a/tools/debug-eldenring/src/display/net_man.rs +++ b/tools/debug-eldenring/src/display/net_man.rs @@ -91,9 +91,12 @@ impl DebugDisplay for CSNetBloodMessageDb { }); ui.header("Created message data", || { - self.created_data - .iter() - .for_each(|f| ui.text(format!("{f} {f:x}"))); + render_message_table( + self.created_data + .iter() + .map(|msg| msg.net_blood_message_db_item.as_ref()), + ui, + ); }); ui.header("Discovered messages", || {