A building's pool draws only when its room is visited; type from row one (#2937)
A building's pool drew forever once its room had been visited, and a portal-less building with an area id never drew its pool, Northshire Abbey's wall fountain among them. In 1.12.1 the flood's visit `0x685d70` links a visited group into the liquid list (`0x685f6b`, the only link), and the liquid pass `0x684cd0` unlinks each record as it draws (`0x684e1d`). So a pool draws exactly in the frames its group is visited. The latch is gone, and the fountain draws. Across 184,624 camera poses around the Great Forge, benilla's flood keeps the lava wherever the reference's portal graph does. A pool's liquid type read every tile row and the group's override. `0x6ba970` scans only the first row and takes the first wet nibble, and its caller never reads the override. benilla now does the same. `WOW_LIQUID_TRACE=1` prints each pool's per-frame verdict. Fixes #119 Fixes #65 Fixes #2492 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -514,7 +514,6 @@ mod tests {
|
||||
name_set: 0,
|
||||
visible: vec![false, false],
|
||||
interior_fog: vec![false, false],
|
||||
liquid_visited: vec![false, false],
|
||||
flooded: vec![None, None],
|
||||
})
|
||||
.id();
|
||||
|
||||
@@ -295,7 +295,6 @@ pub(crate) fn insert_test_room(world: &mut World, flags: u32) {
|
||||
name_set: 0,
|
||||
visible: vec![true],
|
||||
interior_fog: vec![false],
|
||||
liquid_visited: vec![false],
|
||||
flooded: vec![None],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -196,11 +196,11 @@ pub fn wmo_group_liquid_mesh(group_bytes: &[u8]) -> Option<LiquidMesh> {
|
||||
return None;
|
||||
};
|
||||
let lq = group.liquid?;
|
||||
build_wmo_liquid_mesh(&lq, group.group_liquid)
|
||||
build_wmo_liquid_mesh(&lq)
|
||||
}
|
||||
|
||||
/// The [`LiquidMesh`] for a parsed [`WmoLiquid`] grid and its group's `groupLiquid`.
|
||||
fn build_wmo_liquid_mesh(lq: &WmoLiquid, group_liquid: u32) -> Option<LiquidMesh> {
|
||||
/// The [`LiquidMesh`] for a parsed [`WmoLiquid`] grid.
|
||||
fn build_wmo_liquid_mesh(lq: &WmoLiquid) -> Option<LiquidMesh> {
|
||||
let (xv, yv, xt, yt) = (
|
||||
lq.xverts as usize,
|
||||
lq.yverts as usize,
|
||||
@@ -214,13 +214,13 @@ fn build_wmo_liquid_mesh(lq: &WmoLiquid, group_liquid: u32) -> Option<LiquidMesh
|
||||
return None;
|
||||
}
|
||||
|
||||
// One kind per surface: the `groupLiquid` override, else the first wet nibble (`0x6ba970`).
|
||||
let type_nibble = if group_liquid != NO_GROUP_LIQUID {
|
||||
(group_liquid & 0xf) as u8
|
||||
} else {
|
||||
let first_wet = lq.tile_flags.iter().map(|&f| f & 0xf).find(|&n| n != 0xf)?;
|
||||
first_wet
|
||||
};
|
||||
// One kind per surface, `0x6ba970`'s: the first wet nibble of the first tile row, the only row
|
||||
// it scans (`+0x100`, the x tile count), and never `groupLiquid` (`0x6b62e0` reads no `+0x38`).
|
||||
// A dry first row is type 0xf, which draws nothing (`0x6b6374`).
|
||||
let type_nibble = lq.tile_flags[..xt]
|
||||
.iter()
|
||||
.map(|&f| f & 0xf)
|
||||
.find(|&n| n != 0xf)?;
|
||||
let kind = LiquidKind::from_nibble(type_nibble)?;
|
||||
// Fullbright kinds read no ramp and their vertex bytes are a UV pair: the channel stays 0.
|
||||
let water = !kind.is_fullbright();
|
||||
@@ -617,7 +617,7 @@ mod tests {
|
||||
opacity: vec![0, 51, 102, 153, 204, 255],
|
||||
tile_flags: vec![0x44, 0x0f], // wet (type 4 + fishable), hole
|
||||
};
|
||||
let m = build_wmo_liquid_mesh(&lq, 0xf).expect("a wet mesh");
|
||||
let m = build_wmo_liquid_mesh(&lq).expect("a wet mesh");
|
||||
assert_eq!(m.kind, LiquidKind::Still);
|
||||
assert_eq!(m.positions.len(), 6); // full 3×2 grid emitted
|
||||
assert_eq!(m.indices.len(), 6); // one wet tile → 2 tris → 6 indices
|
||||
@@ -661,8 +661,8 @@ mod tests {
|
||||
opacity: Vec::new(),
|
||||
tile_flags: vec![0x44, 0x44],
|
||||
};
|
||||
let ma = build_wmo_liquid_mesh(&a, 0xf).unwrap();
|
||||
let mb = build_wmo_liquid_mesh(&b, 0xf).unwrap();
|
||||
let ma = build_wmo_liquid_mesh(&a).unwrap();
|
||||
let mb = build_wmo_liquid_mesh(&b).unwrap();
|
||||
// A's vertex (2, 0) and B's (0, 0) are the same point.
|
||||
let (ua, ub) = (ma.uvs[2], mb.uvs[0]);
|
||||
assert!(
|
||||
@@ -674,8 +674,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wmo_liquid_type_override_and_all_holes() {
|
||||
// groupLiquid override = 2 (magma) forces the kind regardless of the wet tile's nibble.
|
||||
fn wmo_magma_and_all_holes() {
|
||||
let lq = WmoLiquid {
|
||||
xverts: 2,
|
||||
yverts: 2,
|
||||
@@ -684,10 +683,10 @@ mod tests {
|
||||
base: [0.0, 0.0, 0.0],
|
||||
material_id: 0,
|
||||
heights: vec![0.0; 4],
|
||||
opacity: Vec::new(),
|
||||
tile_flags: vec![0x40], // nibble 0 (would be Still), but the override wins
|
||||
opacity: vec![200; 4],
|
||||
tile_flags: vec![0x42], // magma
|
||||
};
|
||||
let m = build_wmo_liquid_mesh(&lq, 2).expect("magma mesh");
|
||||
let m = build_wmo_liquid_mesh(&lq).expect("magma mesh");
|
||||
assert_eq!(m.kind, LiquidKind::Magma);
|
||||
assert!(m.depths.iter().all(|&v| v == 0.0)); // fullbright ⇒ depth V unused
|
||||
|
||||
@@ -695,7 +694,42 @@ mod tests {
|
||||
tile_flags: vec![0x0f],
|
||||
..lq
|
||||
};
|
||||
assert!(build_wmo_liquid_mesh(&all_holes, 0xf).is_none());
|
||||
assert!(build_wmo_liquid_mesh(&all_holes).is_none());
|
||||
}
|
||||
|
||||
/// The kind is `0x6ba970`'s alone: the first wet nibble of the first tile row, the `+0x100`
|
||||
/// bytes it scans, never the header's `groupLiquid`, which `0x6b62e0` does not read.
|
||||
#[test]
|
||||
fn wmo_liquid_kind_is_the_first_rows_alone() {
|
||||
// A group file: `groupLiquid` at MOGP 0x34, and an MLIQ of 2×2 tiles at height 0.
|
||||
let group = |group_liquid: u32, tiles: [u8; 4]| {
|
||||
let mut mliq = Vec::new();
|
||||
for n in [3u32, 3, 2, 2] {
|
||||
mliq.extend(n.to_le_bytes());
|
||||
}
|
||||
mliq.extend([0u8; 12 + 2 + 9 * 8]); // base, materialId, 9 vertices
|
||||
mliq.extend(tiles);
|
||||
let mut mogp = vec![0u8; 68];
|
||||
mogp[0x34..0x38].copy_from_slice(&group_liquid.to_le_bytes());
|
||||
mogp.extend(chunk(b"QILM", &mliq));
|
||||
let mut g = Vec::new();
|
||||
g.extend(chunk(b"REVM", &17u32.to_le_bytes()));
|
||||
g.extend(chunk(b"PGOM", &mogp));
|
||||
g
|
||||
};
|
||||
// Row 0: a hole, then water (4); row 1 magma (2); `groupLiquid` says magma.
|
||||
let m = wmo_group_liquid_mesh(&group(2, [0x0f, 0x04, 0x02, 0x02])).expect("a water mesh");
|
||||
assert_eq!(
|
||||
m.kind,
|
||||
LiquidKind::Still,
|
||||
"the first row's tile, not groupLiquid"
|
||||
);
|
||||
assert_eq!(m.sound_nibble, 4);
|
||||
// Row 0 all holes, row 1 magma: the scan finds nothing, type 0xf, which draws nothing.
|
||||
assert!(
|
||||
wmo_group_liquid_mesh(&group(NO_GROUP_LIQUID, [0x0f, 0x0f, 0x02, 0x02])).is_none(),
|
||||
"a later row's tile never types the surface"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -1337,7 +1337,6 @@ mod tests {
|
||||
name_set: 0,
|
||||
visible: vec![true],
|
||||
interior_fog: vec![false],
|
||||
liquid_visited: vec![false],
|
||||
flooded: vec![None],
|
||||
})
|
||||
.id();
|
||||
|
||||
@@ -680,7 +680,6 @@ mod tests {
|
||||
name_set: 0,
|
||||
visible: vec![false, false],
|
||||
interior_fog: vec![false, false],
|
||||
liquid_visited: vec![false, false],
|
||||
flooded: vec![None, None],
|
||||
})
|
||||
.id();
|
||||
|
||||
@@ -976,7 +976,6 @@ mod tests {
|
||||
name_set: 0,
|
||||
visible: vec![true],
|
||||
interior_fog: vec![gate],
|
||||
liquid_visited: vec![false],
|
||||
flooded: vec![None],
|
||||
})
|
||||
.id()
|
||||
|
||||
@@ -911,6 +911,12 @@ pub fn plugin(app: &mut App) {
|
||||
Update,
|
||||
visibility::trace_model_visibility.after(ModelVisSet),
|
||||
);
|
||||
// `WOW_LIQUID_TRACE=1` traces every building pool's verdict per frame; registered only then,
|
||||
// so an untraced run's schedule carries no reader of every `Visibility`.
|
||||
if let Some(trace) = visibility::LiquidTrace::from_env() {
|
||||
app.insert_resource(trace)
|
||||
.add_systems(Update, visibility::trace_wmo_liquid.after(ModelVisSet));
|
||||
}
|
||||
// The material dedup cache and its evictors: by distance, and whole on a cross-map transition.
|
||||
app.init_resource::<ModelMaterials>()
|
||||
.add_systems(Update, (scope_model_materials, evict_model_materials));
|
||||
|
||||
@@ -257,16 +257,15 @@ pub(super) fn apply_model_visibility(
|
||||
},
|
||||
);
|
||||
|
||||
// A building's MLIQ surfaces, gated by the ever-visited latch, not this frame's PVS: a group's
|
||||
// liquid draws from its first visit for the rest of the placement's residency. An index past
|
||||
// the latch fails open.
|
||||
// A building's MLIQ surfaces, on this frame's PVS: the flood's visit callback links a visited
|
||||
// group into the liquid list `0xc7cafc` (`0x685f66`), and the liquid pass unlinks each one it
|
||||
// draws (`0x684e21`), so a pool draws only in a frame its group is visited.
|
||||
for (gv, mut vis, xf, aabb, exterior, tag) in &mut group_only {
|
||||
let portal_ok = !m.portal_cull
|
||||
|| instances.get(gv.instance).ok().is_none_or(|inst| {
|
||||
gv.groups
|
||||
.iter()
|
||||
.any(|&g| inst.liquid_visited.get(g as usize).copied().unwrap_or(true))
|
||||
});
|
||||
|| instances
|
||||
.get(gv.instance)
|
||||
.ok()
|
||||
.is_none_or(|inst| gv.drawn_by(inst));
|
||||
// The same exterior-window term as the submesh walk.
|
||||
let exterior_ok = !exterior || Some(gv.instance) == own_instance || gate.admits(xf, aabb);
|
||||
// A building's water rides the building's toggle: the reference's WMO liquid drain
|
||||
@@ -281,9 +280,8 @@ pub(super) fn apply_model_visibility(
|
||||
if *vis != want {
|
||||
*vis = want;
|
||||
}
|
||||
// The pool's fog follows this frame's flood, not the latch, as the reference's WMO liquid
|
||||
// pass pushes interior fog under the same `[0xca7f00]` as the geometry; a pool drawn by
|
||||
// the latch alone wears scene fog. `liquid.wgsl` ANDs it with the surface's own class.
|
||||
// The reference's WMO liquid pass pushes interior fog under the same `[0xca7f00]` as the
|
||||
// geometry. `liquid.wgsl` ANDs it with the surface's own class.
|
||||
if let Some(mut tag) = tag {
|
||||
let on = instances
|
||||
.get(gv.instance)
|
||||
@@ -395,11 +393,52 @@ pub(super) fn trace_model_visibility(
|
||||
}
|
||||
}
|
||||
|
||||
/// `WOW_LIQUID_TRACE=1` prints, every frame, each building pool's group, that group's PVS bit and
|
||||
/// the pool's verdict: `vis` is this frame's, `view` (Bevy's frustum cull) last frame's.
|
||||
#[derive(Resource, Default)]
|
||||
pub struct LiquidTrace {
|
||||
frame: u64,
|
||||
}
|
||||
|
||||
impl LiquidTrace {
|
||||
/// `None` unless `WOW_LIQUID_TRACE` is set to anything but empty.
|
||||
pub(crate) fn from_env() -> Option<Self> {
|
||||
std::env::var_os("WOW_LIQUID_TRACE")
|
||||
.filter(|v| !v.is_empty())
|
||||
.map(|_| Self::default())
|
||||
}
|
||||
}
|
||||
|
||||
/// Prints [`LiquidTrace`]; runs after the authority.
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub(super) fn trace_wmo_liquid(
|
||||
mut trace: ResMut<LiquidTrace>,
|
||||
instances: Query<&WmoPortalInstance>,
|
||||
pools: Query<(Entity, &WmoGroupVis, &Visibility, Option<&ViewVisibility>), Without<ModelPart>>,
|
||||
) {
|
||||
trace.frame += 1;
|
||||
for (e, gv, vis, view) in &pools {
|
||||
let pvs = instances
|
||||
.get(gv.instance)
|
||||
.ok()
|
||||
.map(|inst| gv.drawn_by(inst));
|
||||
println!(
|
||||
"LIQUID_TRACE frame={} {e} inst={} groups={:?} pvs={} vis={vis:?} view={}",
|
||||
trace.frame,
|
||||
gv.instance,
|
||||
gv.groups,
|
||||
pvs.map_or("-".into(), |p| p.to_string()),
|
||||
view.map_or("-".into(), |v| v.get().to_string()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::billboard::BillboardCard;
|
||||
use benilla_assets::BillboardInfo;
|
||||
use benilla_assets::{WmoGroupNav, WmoModel, WmoPortalInfo, WmoPortalRef};
|
||||
use benilla_formats::{BillboardKind, ModelBlend};
|
||||
|
||||
#[test]
|
||||
@@ -476,8 +515,8 @@ mod tests {
|
||||
.init_resource::<crate::wmo_portal::CameraInteriorClaim>()
|
||||
.init_resource::<super::super::FarSideTwins>()
|
||||
.add_systems(Update, apply_model_visibility);
|
||||
// A portal-less prop's pool: its instance has no latch, so `portal_ok` fails open and the
|
||||
// toggle is the only term in play.
|
||||
// An instance entity with no `WmoPortalInstance`: `portal_ok` fails open and the toggle is
|
||||
// the only term in play.
|
||||
let instance = app.world_mut().spawn(()).id();
|
||||
let pool = app
|
||||
.world_mut()
|
||||
@@ -524,4 +563,237 @@ mod tests {
|
||||
"the doodad toggle is a different bit and must not touch a building's water"
|
||||
);
|
||||
}
|
||||
|
||||
/// The flood and the authority over one placement of `model` at the origin, with a pool in
|
||||
/// `group` keyed as the spawn site keys it, and a world camera to aim with [`aim`].
|
||||
fn pool_app(model: WmoModel, group: u16) -> (App, Entity) {
|
||||
let mut app = App::new();
|
||||
app.init_resource::<DebugState>()
|
||||
.init_resource::<ViewDistance>()
|
||||
.init_resource::<crate::wmo_portal::ExteriorWindows>()
|
||||
.init_resource::<crate::wmo_portal::CameraInteriorClaim>()
|
||||
.init_resource::<crate::wmo_portal::CameraWmoFog>()
|
||||
.init_resource::<crate::wmo_portal::WmoCullProbe>()
|
||||
.init_resource::<crate::terrain_stream::TerrainStreamer>()
|
||||
.init_resource::<super::super::FarSideTwins>()
|
||||
.init_resource::<Assets<benilla_assets::AdtTile>>()
|
||||
.init_resource::<Assets<WmoModel>>()
|
||||
.add_systems(
|
||||
Update,
|
||||
(crate::wmo_portal::compute_wmo_pvs, apply_model_visibility).chain(),
|
||||
);
|
||||
let groups = model.group_nav.len();
|
||||
let handle = app
|
||||
.world_mut()
|
||||
.resource_mut::<Assets<WmoModel>>()
|
||||
.add(model);
|
||||
let instance = app
|
||||
.world_mut()
|
||||
.spawn(WmoPortalInstance {
|
||||
handle,
|
||||
world_from_local: bevy::math::Affine3A::IDENTITY,
|
||||
name_set: 0,
|
||||
visible: vec![true; groups],
|
||||
interior_fog: vec![false; groups],
|
||||
flooded: vec![None; groups],
|
||||
})
|
||||
.id();
|
||||
let pool = app
|
||||
.world_mut()
|
||||
.spawn((
|
||||
WmoGroupVis {
|
||||
instance,
|
||||
groups: std::sync::Arc::from([group].as_slice()),
|
||||
},
|
||||
Visibility::Inherited,
|
||||
GlobalTransform::IDENTITY,
|
||||
))
|
||||
.id();
|
||||
app.world_mut().spawn((
|
||||
WorldCamera,
|
||||
Transform::IDENTITY,
|
||||
GlobalTransform::IDENTITY,
|
||||
Projection::Perspective(PerspectiveProjection {
|
||||
fov: 0.9,
|
||||
aspect_ratio: 16.0 / 9.0,
|
||||
near: 0.1,
|
||||
far: 1000.0,
|
||||
..default()
|
||||
}),
|
||||
));
|
||||
(app, pool)
|
||||
}
|
||||
|
||||
/// Puts the world camera at WoW model-space `eye`, looking at `at`, and runs a frame.
|
||||
fn aim(app: &mut App, eye: [f32; 3], at: [f32; 3]) {
|
||||
use benilla_assets::coords::wow_to_bevy;
|
||||
let pose =
|
||||
Transform::from_translation(wow_to_bevy(eye)).looking_at(wow_to_bevy(at), Vec3::Y);
|
||||
let mut q = app
|
||||
.world_mut()
|
||||
.query_filtered::<(&mut Transform, &mut GlobalTransform), With<WorldCamera>>();
|
||||
let (mut t, mut g) = q.single_mut(app.world_mut()).expect("one world camera");
|
||||
*t = pose;
|
||||
*g = GlobalTransform::from(pose);
|
||||
app.update();
|
||||
}
|
||||
|
||||
fn drawn(app: &App, pool: Entity) -> bool {
|
||||
*app.world().entity(pool).get::<Visibility>().unwrap() == Visibility::Inherited
|
||||
}
|
||||
|
||||
/// Two interior rooms joined by a doorway in the WoW `x = 0` plane (y in [-0.5, 0.5], z in
|
||||
/// [0, 2]): `g0`, floored at `z = 0`, holds the camera, and `g1` beyond holds the pool.
|
||||
fn two_rooms() -> WmoModel {
|
||||
let room = |min: [f32; 3], max: [f32; 3], ref_start: u16| WmoGroupNav {
|
||||
flags: 0x2000,
|
||||
bbox_min: min,
|
||||
bbox_max: max,
|
||||
ref_start,
|
||||
ref_count: 1,
|
||||
area_table_id: 0,
|
||||
fog_indices: [0; 4],
|
||||
group_liquid: benilla_formats::NO_GROUP_LIQUID,
|
||||
};
|
||||
WmoModel {
|
||||
portal_vertices: vec![
|
||||
[0.0, -0.5, 0.0],
|
||||
[0.0, 0.5, 0.0],
|
||||
[0.0, 0.5, 2.0],
|
||||
[0.0, -0.5, 2.0],
|
||||
],
|
||||
portal_infos: vec![WmoPortalInfo {
|
||||
start_vertex: 0,
|
||||
count: 4,
|
||||
plane: [1.0, 0.0, 0.0, 0.0],
|
||||
}],
|
||||
// `side = -1` puts an eye at x < 0 on g0's entering half-space.
|
||||
portal_refs: vec![
|
||||
WmoPortalRef {
|
||||
portal: 0,
|
||||
group: 1,
|
||||
side: -1,
|
||||
},
|
||||
WmoPortalRef {
|
||||
portal: 0,
|
||||
group: 0,
|
||||
side: 1,
|
||||
},
|
||||
],
|
||||
group_nav: vec![
|
||||
room([-10.0, -10.0, 0.0], [0.0, 10.0, 3.0], 0),
|
||||
room([0.0, -10.0, 0.0], [10.0, 10.0, 3.0], 1),
|
||||
],
|
||||
group_collision_tris: vec![
|
||||
vec![
|
||||
[[-10.0, -10.0, 0.0], [0.0, -10.0, 0.0], [0.0, 10.0, 0.0]],
|
||||
[[-10.0, -10.0, 0.0], [0.0, 10.0, 0.0], [-10.0, 10.0, 0.0]],
|
||||
],
|
||||
Vec::new(),
|
||||
],
|
||||
..WmoModel::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// A building's pool draws off this frame's flood: the reference links a visited group into
|
||||
/// the liquid list `0xc7cafc` (`0x685f66`) and unlinks it as it draws (`0x684e21`), so a
|
||||
/// room the flood stops reaching loses its pool that frame, and gets it back with the room.
|
||||
#[test]
|
||||
fn a_pool_follows_this_frames_pvs() {
|
||||
let (mut app, pool) = pool_app(two_rooms(), 1);
|
||||
let eye = [-3.0, 0.0, 1.0];
|
||||
let room_visible = |app: &mut App| {
|
||||
let mut q = app.world_mut().query::<&WmoPortalInstance>();
|
||||
q.single(app.world()).expect("one instance").visible[1]
|
||||
};
|
||||
|
||||
aim(&mut app, eye, [10.0, 0.0, 1.0]);
|
||||
assert!(
|
||||
room_visible(&mut app),
|
||||
"the flood crosses the doorway ahead"
|
||||
);
|
||||
assert!(drawn(&app, pool), "the far room's pool draws with its room");
|
||||
|
||||
aim(&mut app, eye, [-10.0, 0.0, 1.0]);
|
||||
assert!(!room_visible(&mut app), "the doorway is behind the camera");
|
||||
assert!(
|
||||
!drawn(&app, pool),
|
||||
"a pool whose room the flood no longer visits does not draw, though it drew last frame"
|
||||
);
|
||||
|
||||
aim(&mut app, eye, [10.0, 0.0, 1.0]);
|
||||
assert!(drawn(&app, pool), "the room is back, and its pool with it");
|
||||
}
|
||||
|
||||
/// A building without a portal graph keeps every group visible, so its pool draws though an
|
||||
/// area id gives it an instance.
|
||||
#[test]
|
||||
fn a_portal_less_buildings_pool_draws() {
|
||||
let model = WmoModel {
|
||||
wmo_id: 63,
|
||||
group_nav: vec![WmoGroupNav {
|
||||
flags: 0x1000,
|
||||
bbox_min: [-5.0; 3],
|
||||
bbox_max: [5.0; 3],
|
||||
ref_start: 0,
|
||||
ref_count: 0,
|
||||
area_table_id: 0,
|
||||
fog_indices: [0; 4],
|
||||
group_liquid: benilla_formats::NO_GROUP_LIQUID,
|
||||
}],
|
||||
..WmoModel::default()
|
||||
};
|
||||
let (mut app, pool) = pool_app(model, 0);
|
||||
for at in [[10.0, 0.0, 0.0], [-10.0, 0.0, 0.0]] {
|
||||
aim(&mut app, [0.0, 0.0, 20.0], at);
|
||||
assert!(drawn(&app, pool), "the fountain's pool draws from any view");
|
||||
}
|
||||
}
|
||||
|
||||
/// Northshire Abbey's wall fountain, `abbeygate01.wmo`: no portal graph and WMO id 63, so its
|
||||
/// placement takes an instance (the spawn site's `has_portals || wmo_id != 0`), and group 0
|
||||
/// holds still water, which draws.
|
||||
#[test]
|
||||
fn the_abbey_wall_fountain_draws_its_pool() {
|
||||
let data = benilla_formats::wow_data_or_skip!();
|
||||
let chain = benilla_formats::open_chain(&data).expect("open the MPQ chain");
|
||||
let path = r"World\wmo\Azeroth\Collidable Doodads\Elwynn\AbbeyGate\abbeygate01.wmo";
|
||||
let root_bytes = chain.read(path).expect("abbeygate01.wmo ships");
|
||||
let root = benilla_formats::parse_wmo_root(&root_bytes).expect("parse the root");
|
||||
let group = chain
|
||||
.read(&path.replace(".wmo", "_000.wmo"))
|
||||
.expect("its one group ships");
|
||||
let portals = root.portals();
|
||||
assert!(
|
||||
portals.refs.is_empty() && portals.infos.is_empty(),
|
||||
"the fountain has no portal graph"
|
||||
);
|
||||
let wmo_id = benilla_formats::wmo_root_id(&root_bytes);
|
||||
assert_eq!(wmo_id, 63, "an area id: the placement spawns an instance");
|
||||
assert_eq!(root.group_count(), 1);
|
||||
let water = benilla_formats::wmo_group_liquid_mesh(&group).expect("group 0 holds a pool");
|
||||
assert_eq!(water.kind, benilla_formats::LiquidKind::Still);
|
||||
let header = benilla_formats::wmo_group_header(&group).expect("group header");
|
||||
let info = &root.group_infos()[0];
|
||||
let model = WmoModel {
|
||||
wmo_id,
|
||||
group_nav: vec![WmoGroupNav {
|
||||
flags: header.flags,
|
||||
bbox_min: info.bbox_min,
|
||||
bbox_max: info.bbox_max,
|
||||
ref_start: header.portal_ref_start,
|
||||
ref_count: header.portal_ref_count,
|
||||
area_table_id: header.area_table_id,
|
||||
fog_indices: header.fog_indices,
|
||||
group_liquid: header.group_liquid,
|
||||
}],
|
||||
group_liquids: vec![Some(water)],
|
||||
..WmoModel::default()
|
||||
};
|
||||
let (mut app, pool) = pool_app(model, 0);
|
||||
for at in [[10.0, 0.0, 0.0], [-10.0, 0.0, 0.0]] {
|
||||
aim(&mut app, [0.0, -18.0, 6.0], at);
|
||||
assert!(drawn(&app, pool), "the basin holds its water from any view");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -918,7 +918,6 @@ pub(crate) mod tests {
|
||||
handle: Handle::default(),
|
||||
world_from_local: bevy::math::Affine3A::IDENTITY,
|
||||
name_set: 0,
|
||||
liquid_visited: vec![false; 3],
|
||||
flooded: vec![None; 3],
|
||||
visible: vec![true, false, false],
|
||||
interior_fog: vec![false; 3],
|
||||
|
||||
@@ -320,7 +320,6 @@ pub(super) fn spawn_loaded_placements(
|
||||
visible: vec![true; m.group_nav.len()],
|
||||
// Fogged only once a flood puts the group on the chain.
|
||||
interior_fog: vec![false; m.group_nav.len()],
|
||||
liquid_visited: vec![false; m.group_nav.len()],
|
||||
// MOGP `groupLiquid`: a room wholly submerged, with no liquid grid.
|
||||
flooded: m
|
||||
.group_nav
|
||||
|
||||
@@ -978,7 +978,6 @@ mod tests {
|
||||
name_set: 0,
|
||||
visible: vec![true],
|
||||
interior_fog: vec![false],
|
||||
liquid_visited: vec![false],
|
||||
flooded: vec![None],
|
||||
})
|
||||
.id()
|
||||
|
||||
@@ -180,10 +180,6 @@ pub struct WmoPortalInstance {
|
||||
/// draws the group and its doodads with the building's MFOG triple, `false` with the scene fog.
|
||||
/// Seeded all-false, which a portal-less prop keeps.
|
||||
pub interior_fog: Vec<bool>,
|
||||
/// Per-group ever-visited latch: once the flood visits a group, its MLIQ surface draws for the
|
||||
/// rest of the placement's residency, whatever this frame's PVS. Latched by
|
||||
/// [`compute_wmo_pvs`]; starts all-false.
|
||||
pub liquid_visited: Vec<bool>,
|
||||
/// Per-group whole-group submersion from MOGP `groupLiquid`: `Some(kind)` means anywhere in the
|
||||
/// group is under liquid, at every z. Baked at spawn; empty on a placement spawned before its
|
||||
/// model resolved.
|
||||
@@ -273,7 +269,7 @@ const MAX_FLOOR_DROP: f32 = 1760.0;
|
||||
|
||||
/// Recompute each resident WMO's per-group visible set from the camera; a portal-less prop stays
|
||||
/// all-visible.
|
||||
fn compute_wmo_pvs(
|
||||
pub(crate) fn compute_wmo_pvs(
|
||||
wmos: Res<Assets<WmoModel>>,
|
||||
cam: Query<(&GlobalTransform, &Projection), With<WorldCamera>>,
|
||||
mut instances: Query<(Entity, &mut WmoPortalInstance)>,
|
||||
@@ -426,23 +422,6 @@ fn compute_wmo_pvs(
|
||||
}
|
||||
}
|
||||
}
|
||||
// The `liquid_visited` latch, through the change gate: only a slot that flips marks the
|
||||
// instance changed.
|
||||
let held = inst.bypass_change_detection();
|
||||
let mut latched = false;
|
||||
if held.liquid_visited.len() != groups {
|
||||
held.liquid_visited = vec![false; groups];
|
||||
latched = true;
|
||||
}
|
||||
for g in 0..groups {
|
||||
if held.visible.get(g).copied().unwrap_or(false) && !held.liquid_visited[g] {
|
||||
held.liquid_visited[g] = true;
|
||||
latched = true;
|
||||
}
|
||||
}
|
||||
if latched {
|
||||
inst.set_changed();
|
||||
}
|
||||
}
|
||||
if camera_fog.0 != fog_target {
|
||||
camera_fog.0 = fog_target;
|
||||
@@ -1017,7 +996,6 @@ mod tests {
|
||||
name_set: 0,
|
||||
visible: vec![false, true, false],
|
||||
interior_fog: vec![false; 3],
|
||||
liquid_visited: vec![false; 3],
|
||||
flooded: vec![None; 3],
|
||||
};
|
||||
let key = |groups: &[u16]| WmoGroupVis {
|
||||
@@ -1047,7 +1025,6 @@ mod tests {
|
||||
name_set: 0,
|
||||
visible: vec![true; 3],
|
||||
interior_fog: vec![false, true, false],
|
||||
liquid_visited: vec![false; 3],
|
||||
flooded: vec![None; 3],
|
||||
};
|
||||
let key = |groups: &[u16]| WmoGroupVis {
|
||||
|
||||
+1
-1
@@ -935,7 +935,7 @@ Each entry is the first sentence of a `Deviation:` comment, linking to where it
|
||||
- A read still loading after `INNER_WAIT_CAP` is given up on with a warning, once per request, and its tile then streams on unwaited like the outer ring, where the reference waits for ever, because that way a hung read costs one capped stall, not one every frame. ([`src/terrain_stream.rs:870`](../crates/benilla-world/src/terrain_stream.rs#L870))
|
||||
- Benilla gates on terrain alone, which is more permissive; the settle hold covers a building or doodad arriving after a teleport. And an authored tile whose ADT failed to load counts as resident, where the reference would have loaded it, because a permanent wall over a bad file is worse for a player than walking on without its ground. ([`src/terrain_stream/residency.rs:9`](../crates/benilla-world/src/terrain_stream/residency.rs#L9))
|
||||
- Tagged per submesh, where the reference tests the whole object, because a placement has no root entity to carry the bound. The anim-host root is skipped: with no geometry it has no `Aabb`, and would only pad the counts. ([`src/terrain_stream/spawn/mod.rs:229`](../crates/benilla-world/src/terrain_stream/spawn/mod.rs#L229))
|
||||
- The box, where the reference tests its circumsphere (`0x683700` → `0x682ef0` → `0x686b80` on the `[rec+0x5c]` centre and `[rec+0x68]` radius × scale), because Bevy culls by box and the box is tighter. ([`src/terrain_stream/spawn/mod.rs:889`](../crates/benilla-world/src/terrain_stream/spawn/mod.rs#L889))
|
||||
- The box, where the reference tests its circumsphere (`0x683700` → `0x682ef0` → `0x686b80` on the `[rec+0x5c]` centre and `[rec+0x68]` radius × scale), because Bevy culls by box and the box is tighter. ([`src/terrain_stream/spawn/mod.rs:888`](../crates/benilla-world/src/terrain_stream/spawn/mod.rs#L888))
|
||||
- Whole tiles, because benilla loads, spawns and releases ADT tiles, not chunks: a tile is wanted when any of its chunks lies in the outer window, and waited for when any lies in the inner one, so each edge is a tile line where the reference's is a chunk line. ([`src/terrain_stream/window.rs:7`](../crates/benilla-world/src/terrain_stream/window.rs#L7))
|
||||
- A one-chunk keep band (`KEEP_BAND_CHUNKS`), because a tile reload re-decodes and re-spawns where the reference's is a cheap read: a tile releases only past `outer + 1`. ([`src/terrain_stream/window.rs:10`](../crates/benilla-world/src/terrain_stream/window.rs#L10))
|
||||
|
||||
|
||||
@@ -289,6 +289,7 @@
|
||||
- `WOW_LIGHT_DUMP` — benilla-world/src/lighting/global_light.rs, benilla-world/src/lighting/resolve.rs
|
||||
- `WOW_LIGHT_GRID` — benilla-world/src/wmo_portal/audit/light_probe.rs
|
||||
- `WOW_LIQUID_DC` — benilla-assets/src/world_assets/images.rs
|
||||
- `WOW_LIQUID_TRACE` — benilla-world/src/model_render/visibility.rs
|
||||
- `WOW_LIVE_` — benilla-world/src/bgwin.rs
|
||||
- `WOW_LIVE_FPS` — benilla-app/src/capture/probes/live_fps.rs, benilla-app/src/dev.rs, benilla-world/src/bgwin.rs
|
||||
- `WOW_LIVE_FPS_AT` — benilla-app/src/capture/probes/live_fps.rs
|
||||
|
||||
Reference in New Issue
Block a user