From 0237dbd19b8bf21d78eae30643b733b3d58a593b Mon Sep 17 00:00:00 2001 From: Thorsten42 Date: Sun, 27 Sep 2026 15:24:33 +0200 Subject: [PATCH] Geosets above 1700 stay visible, as 1.12.1 switches off only 0 to 1700: an asset pack's Night Elf eye glow on 1702 shows (#438) - fix(characters): preserve unmanaged geosets above 1700 - The geoset selection's comments state the 1.12 fact Co-Authored-By: Claude Opus 5.5 (1M context) Co-Authored-By: Sam --- crates/benilla-app/src/entities/attach/mod.rs | 2 +- .../src/entities/attach/redress.rs | 2 +- .../src/bin/benilla-extract/charatlas.rs | 2 +- .../benilla-formats/src/characters/geosets.rs | 87 ++++++++++++++++++- crates/benilla-formats/src/characters/mod.rs | 4 +- crates/benilla-formats/src/lib.rs | 2 +- 6 files changed, 91 insertions(+), 8 deletions(-) diff --git a/crates/benilla-app/src/entities/attach/mod.rs b/crates/benilla-app/src/entities/attach/mod.rs index 08da268d..27dc03f6 100644 --- a/crates/benilla-app/src/entities/attach/mod.rs +++ b/crates/benilla-app/src/entities/attach/mod.rs @@ -536,7 +536,7 @@ pub(super) fn attach_entity_visuals( worn.helm, worn.tabard_preview, ); - let visible_geosets: Option> = look.as_ref().and_then(|l| { + let visible_geosets = look.as_ref().and_then(|l| { let cg = characters.as_deref()?; Some(cg.0.visible_geosets( l.race, diff --git a/crates/benilla-app/src/entities/attach/redress.rs b/crates/benilla-app/src/entities/attach/redress.rs index 683d359e..a1007441 100644 --- a/crates/benilla-app/src/entities/attach/redress.rs +++ b/crates/benilla-app/src/entities/attach/redress.rs @@ -132,7 +132,7 @@ pub(in crate::entities) fn redress_player_looks( worn.helm, worn.tabard_preview, ); - let visible: Option> = look.as_ref().and_then(|l| { + let visible = look.as_ref().and_then(|l| { let cg = characters.as_deref()?; Some(cg.0.visible_geosets(l.race, l.sex, l.hair_style, l.facial_hair, &eg)) }); diff --git a/crates/benilla-formats/src/bin/benilla-extract/charatlas.rs b/crates/benilla-formats/src/bin/benilla-extract/charatlas.rs index 8ff0b1e2..d13584ef 100644 --- a/crates/benilla-formats/src/bin/benilla-extract/charatlas.rs +++ b/crates/benilla-formats/src/bin/benilla-extract/charatlas.rs @@ -218,7 +218,7 @@ pub fn charatlas(chain: &mut Chain, look: &Look, out: Option<&std::path::Path>) // The shirt-cuff geoset's gate is the ArmLower tile's occupancy, read off the same plan. eq.forearm_dressed = forearm_dressed(&equipment); let ids = geosets.visible_geosets(look.race, look.sex, look.hair_style, look.facial_hair, &eq); - println!("\nvisible geosets: {ids:?}"); + println!("\nvisible geosets: {ids:?} + all IDs above 1700"); if let Some(path) = out { let img = diff --git a/crates/benilla-formats/src/characters/geosets.rs b/crates/benilla-formats/src/characters/geosets.rs index 7a35f523..e34804fb 100644 --- a/crates/benilla-formats/src/characters/geosets.rs +++ b/crates/benilla-formats/src/characters/geosets.rs @@ -17,6 +17,21 @@ const REGION_BASES: [u16; 16] = [ 1, 101, 201, 301, 401, 501, 601, 702, 801, 901, 1001, 1101, 1201, 1301, 1401, 1501, ]; +/// The geosets a character shows: the selector's picks, plus every ID above 1700. `0x477520` first +/// disables only 0..=1700 (`0x6a4`, through `0x7110d0`) and then enables its picks, and a model's +/// submeshes start visible (`0x70ebd0` fills their flags with 1), so a higher ID stays shown. +#[derive(Debug)] +pub struct VisibleGeosets { + selected: Vec, +} + +impl VisibleGeosets { + /// Whether a model submesh survives the character compositor's geoset selection. + pub fn contains(&self, geoset_id: &u16) -> bool { + *geoset_id > 1700 || self.selected.contains(geoset_id) + } +} + /// The customization → geoset tables, resolving an appearance to the set of visible geosets. pub struct CharacterGeosets { /// (race, sex, hairStyle) → hair `GeosetID` (group 0). @@ -47,7 +62,8 @@ pub struct EquipGeosets { impl CharacterGeosets { /// The geosets (`skinSectionId`s) this appearance draws, sorted and deduplicated: the naked - /// set, then the eight equipment branches B1–B8 of `0x477520`. + /// set, then the eight equipment branches B1–B8 of `0x477520`. Use + /// [`VisibleGeosets::contains`] to retain unmanaged IDs above 1700. pub fn visible_geosets( &self, race: u8, @@ -55,7 +71,7 @@ impl CharacterGeosets { hair_style: u8, facial_hair: u8, equip: &EquipGeosets, - ) -> Vec { + ) -> VisibleGeosets { // The naked set: the region bases and geoset 0, the body. let mut set = REGION_BASES.to_vec(); set.push(0); @@ -158,7 +174,7 @@ impl CharacterGeosets { // The client sets a flag per submesh (`0x7110d0`), so a repeated enable is a no-op there. set.sort_unstable(); set.dedup(); - set + VisibleGeosets { selected: set } } /// Load the customization DBCs from the patch chain. A repeated `(race, sex, variation)` key @@ -287,6 +303,71 @@ pub(crate) fn char_facial_hair_schema() -> Schema { mod tests { use super::*; + #[test] + fn geoset_visibility_preserves_unmanaged_ids() { + let cg = CharacterGeosets { + hair: HashMap::new(), + facial: HashMap::new(), + helmet_vis: HashMap::new(), + }; + let mut boots = EquipGeosets::default(); + boots.bodyslots[4] = Some([2, 0, 0]); + for equip in [EquipGeosets::default(), boots] { + let visible = cg.visible_geosets(4, 1, 0, 0, &equip); + assert!( + visible.contains(&0), + "stock eye glow stays on the body geoset" + ); + for id in [2, 302, 1600, 1699, 1700] { + assert!(!visible.contains(&id), "unselected managed geoset {id}"); + } + for id in [1701, 1702, 2001, u16::MAX] { + assert!(visible.contains(&id), "unmanaged geoset {id} stays visible"); + } + } + let visible = cg.visible_geosets(4, 1, 0, 0, &boots); + assert!(!visible.contains(&501), "boots still hide the bare ankle"); + assert!( + visible.contains(&503), + "the equipped boot variant is visible" + ); + } + + /// Both Night Elf sexes carry two eye-glow cards (on geoset 0 in the stock data; an asset pack + /// may put them above 1700), and both survive the selection, bare and robed. + #[test] + fn geoset_visibility_keeps_night_elf_eye_glow() { + let data = crate::wow_data_or_skip!(); + let mut chain = crate::open_chain(&data).expect("open chain"); + let cg = CharacterGeosets::load(&mut chain).expect("load customization tables"); + for (sex, name) in [(0, "Male"), (1, "Female")] { + let path = format!("Character\\NightElf\\{name}\\NightElf{name}.m2"); + let parts = crate::load_m2_mesh(&mut chain, &path).expect("Night Elf model"); + let eyes: Vec<_> = parts + .iter() + .filter(|p| { + p.texture + .as_ref() + .is_some_and(|t| t.to_ascii_lowercase().ends_with("eyeglow.blp")) + }) + .collect(); + assert_eq!(eyes.len(), 2, "{name} has two eye-glow cards"); + assert!(eyes.iter().all(|p| p.billboard.is_some() && p.additive)); + let mut robe = EquipGeosets::default(); + robe.bodyslots[1] = Some([0, 0, 1]); + for equip in [EquipGeosets::default(), robe] { + let visible = cg.visible_geosets(4, sex, 0, 0, &equip); + assert_eq!( + eyes.iter() + .filter(|p| visible.contains(&p.geoset_id)) + .count(), + 2, + "{name}: both eye-glow cards must survive the character filter" + ); + } + } + } + /// Gloves, boots, a robe and a cloak replace their groups; a robe hides the tabard flap. #[test] fn equipment_geoset_branches() { diff --git a/crates/benilla-formats/src/characters/mod.rs b/crates/benilla-formats/src/characters/mod.rs index 4f5ee29c..35fb1e9f 100644 --- a/crates/benilla-formats/src/characters/mod.rs +++ b/crates/benilla-formats/src/characters/mod.rs @@ -5,13 +5,15 @@ //! //! DBC layouts follow the client's record readers: CharSections `0x575540`, the geoset tables //! `0x5753b0`/`0x575a80`. +//! The compositor disables geosets 0..=1700; higher IDs keep their model visibility through +//! [`VisibleGeosets::contains`]. mod customization; mod geosets; mod sections; pub use customization::{CharCreateCatalog, DialRanges, StartOutfitItem}; -pub use geosets::{CharacterGeosets, EquipGeosets}; +pub use geosets::{CharacterGeosets, EquipGeosets, VisibleGeosets}; pub use sections::{ equip_blits, equip_column, equip_region_candidates, equip_tex_dir, equip_tile, forearm_dressed, BlitSource, CharSections, EmblemLayer, EquipBlit, GuildEmblem, diff --git a/crates/benilla-formats/src/lib.rs b/crates/benilla-formats/src/lib.rs index 2821ac76..4975d51d 100644 --- a/crates/benilla-formats/src/lib.rs +++ b/crates/benilla-formats/src/lib.rs @@ -23,7 +23,7 @@ mod characters; pub use characters::{ equip_blits, equip_column, equip_region_candidates, equip_tex_dir, equip_tile, forearm_dressed, BlitSource, CharCreateCatalog, CharSections, CharacterGeosets, DialRanges, EmblemLayer, - EquipBlit, EquipGeosets, GuildEmblem, StartOutfitItem, + EquipBlit, EquipGeosets, GuildEmblem, StartOutfitItem, VisibleGeosets, }; mod camera_shakes; mod cinematics;