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) <noreply@anthropic.com>
Co-Authored-By: Sam <samuv.eth@gmail.com>
This commit is contained in:
Thorsten42
2026-09-27 20:24:33 +07:00
committed by GitHub
co-authored by Claude Opus 5.5 Sam
parent bd074c3f47
commit 0237dbd19b
6 changed files with 91 additions and 8 deletions
@@ -536,7 +536,7 @@ pub(super) fn attach_entity_visuals(
worn.helm,
worn.tabard_preview,
);
let visible_geosets: Option<Vec<u16>> = 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,
@@ -132,7 +132,7 @@ pub(in crate::entities) fn redress_player_looks(
worn.helm,
worn.tabard_preview,
);
let visible: Option<Vec<u16>> = 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))
});
@@ -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 =
@@ -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<u16>,
}
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<u16> {
) -> 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() {
+3 -1
View File
@@ -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,
+1 -1
View File
@@ -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;