gates: clippy clean again, and the mail repro that never was for commit

`scripts/check.sh` has been failing before it ran a single test, on 62 clippy
findings in code none of the recent rounds touched — 32 in `benilla-world`, 30 in
`benilla-app` — so the in-loop gate could not tell anyone anything. Every one is a
newer clippy noticing older code (`as_chunks` and `is_multiple_of` are both lints
younger than the lines they fire on), and they are fixed, not silenced, except
where the silence is the point:

- **Argued allows, the house idiom.** `clippy::type_complexity` on ten Bevy system
  signatures and two inline caster queries, each with the one-line reason beside
  it, as the other 67 in this workspace have. `clippy::approx_constant` on
  `liquid/waves.rs`'s `TWO_PI`, whose own doc already says why it is spelled
  `6.2831853`: it mirrors a shader line, and `f32::consts::TAU` would stop reading
  against the file it copies.
- **Two runtime assertions became compile-time ones** (`EXT_SEL_EMPTY is index 255`,
  the endless-stream frame count): a claim about constants should fail the build
  that breaks it, not a test run after it.
- **Three real, if harmless, redundancies gone**: `len > MAX_TILT_TAN && len > 0.0`
  (the constant is `tan(4°)`, so the second test never spoke), `u32::from(..) + 1 |
  flags` now parenthesised where `+` binds tighter than `|`, and `palette.get(0)`.
- The rest is mechanical: `chunks_exact(3)` → `as_chunks::<3>()`, `map_or(true, ..)`
  → `is_none_or`, two `Default::default()`-then-assign test fixtures, a doc line
  that began `1.` and so turned its own paragraph into a list item, and
  `local_utc_offset_secs` moved above the test module it had drifted below.

`tests/repro_mail.rs` is deleted. Its first line says "TEMPORARY repro harness —
not for commit"; it reads a loose mirror of FrameXML out of `/tmp/opencode/
vanilla-ui`, which no longer exists, so it has been failing on a missing directory
rather than telling anyone about mail.

What is left of the gate's red is one test: `world_api_wall` counts 174 crossings
of the `benilla-world` boundary against a ceiling of 107 — 90 of them already
filed with a close plan, 84 unsorted. That is not cleanup; the wall's own message
says the remedies are to close the crossing or to publish it as engine API with the
record that says so, and 84 of those is a design pass on what the engine's public
surface is.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
This commit is contained in:
2026-10-18 15:18:10 -06:00
co-authored by Claude Opus 5
parent 3e616e5cb3
commit 56fb28fa2b
33 changed files with 202 additions and 305 deletions
@@ -51,6 +51,7 @@ struct CharacterLane {
}
#[allow(clippy::too_many_arguments)]
#[allow(clippy::type_complexity)] // one Bevy system's full input set
fn collect_character_shadows(
video: Res<VideoConfig>,
time: Res<Time>,
+2 -2
View File
@@ -3124,8 +3124,8 @@ mod tests {
apply_lighting_preset(&mut cvars, name);
// Somewhere else on the row's own scale: the flags flip, the numbers move by one.
let moved = match v.trim().parse::<f32>() {
Ok(x) if x == 0.0 => "1".to_string(),
Ok(x) if x == 1.0 => "0".to_string(),
Ok(0.0) => "1".to_string(),
Ok(1.0) => "0".to_string(),
Ok(x) => (x + 1.0).to_string(),
Err(_) => format!("{v}x"),
};
@@ -453,6 +453,7 @@ impl CarriedLightMotion {
/// carried light is a child of a moving parent, so its `GlobalTransform` is only this frame's once
/// `Propagate` has run. `update_torch_shadows` reads the verdict in `Last`, i.e. later in the same
/// frame — never one stale.
#[allow(clippy::type_complexity)] // one Bevy system's full input set
pub(crate) fn track_carried_light_motion(
mut commands: Commands,
time: Res<Time>,
+1 -1
View File
@@ -468,7 +468,7 @@ fn spawn_screen(
LoginAction::ToggleSave,
CheckMark::Save,
form.save,
&strings.text("SAVE_ACCOUNT_NAME", "Remember Account Name"),
strings.text("SAVE_ACCOUNT_NAME", "Remember Account Name"),
);
// Remember Password, one row below. Not a reference control at all — 1.12 stores an
// account name and never a secret — so the label is literal rather than a `GlueStrings`
+3 -2
View File
@@ -3860,8 +3860,9 @@ mod stage_latch_tests {
}
/// The second half of the handover, and the reason [`super::sync_body_booth`] clears the
/// latch: a pane's bake lives only as long as its room, and every pane numbers its bakes from
/// 1. A window that closes and reopens brings the *same* pane back at revision 1, so a latch
/// latch: a pane's bake lives only as long as its room, and every pane numbers its bakes
/// from one. A window that closes and reopens brings the *same* pane back at revision 1, so a
/// latch
/// still holding that pane's revision 1 from its previous life reads as "already current"
/// while the booth actually holds whatever the mirror left in it. Cleared, it is fresh again.
#[test]
@@ -82,6 +82,7 @@ impl Plugin for SunShaftsPlugin {
}
}
#[allow(clippy::type_complexity)] // one Bevy system's full input set
fn update_views(
mut commands: Commands,
video: Res<VideoConfig>,
+4 -2
View File
@@ -456,6 +456,7 @@ fn apply_shadow_quality(
}
#[allow(clippy::too_many_arguments)]
#[allow(clippy::type_complexity)] // one Bevy system's full input set
fn manage_rig(
state: Res<State<ClientState>>,
video: Res<VideoConfig>,
@@ -695,7 +696,7 @@ pub(crate) fn append_triangles(
base: u32,
indices: &mut Vec<u32>,
) {
for triangle in source.chunks_exact(3) {
for triangle in source.as_chunks::<3>().0 {
if triangle.iter().all(|index| *index < vertex_count) {
indices.extend(triangle.iter().map(|index| base + *index));
}
@@ -753,6 +754,7 @@ pub(crate) fn empty_cutout_mesh() -> Mesh {
/// `(admitted, occluder-rejected)` for the trace. Range-gates before any vertex work. The `parts`
/// query is written inline (not aliased) so a lane can pass its own identically-typed query.
#[allow(clippy::too_many_arguments)]
#[allow(clippy::type_complexity)] // the caster query a lane passes in, spelled out
pub(crate) fn collect_entity_geometry(
parts: &Query<
(
@@ -868,7 +870,7 @@ fn append_skinned(
let (Some(joints), Some(weights)) = (pick.0.joints.get(index), pick.0.weights.get(index))
else {
let fallback = palette
.get(0)
.first()
.map(|matrix| matrix.transform_point3(wow_to_bevy(*position)))
.unwrap_or(Vec3::ZERO);
positions.push(fallback.to_array());
+1 -1
View File
@@ -434,7 +434,7 @@ mod tests {
fn the_endless_frame_count_is_a_day_at_the_streams_own_rate() {
assert_eq!(ENDLESS_SECS * 44_100, 3_810_240_000);
assert_eq!(ENDLESS_SECS * 48_000, 4_147_200_000);
assert!(
const _: () = assert!(
ENDLESS_SECS * 48_000 < usize::MAX / 1024,
"nowhere near the arithmetic's edge"
);
+85 -83
View File
@@ -647,6 +647,7 @@ fn entity_gather_due(rate: u32, elapsed: f64, mask: u32, last_mask: u32) -> bool
/// Not tidiness — [`update_torch_shadows`] sat at 15 of Bevy's 16 system params before this change
/// added the player anchor and the interior knobs, and three separate members would not compile.
#[derive(bevy::ecs::system::SystemParam)]
#[allow(clippy::type_complexity)] // the caster query, written inline by the lane's own contract
struct EntityCasters<'w, 's> {
/// The SAME query the world lane passes to `collect_entity_geometry` (written inline by that
/// contract so a lane can pass its own).
@@ -760,6 +761,7 @@ fn cube_view_projs(fixture: Vec3) -> [Mat4; CUBE_FACES] {
}
#[allow(clippy::too_many_arguments)]
#[allow(clippy::type_complexity)] // one Bevy system's full input set
fn update_torch_shadows(
video: Res<VideoConfig>,
state: Res<State<ClientState>>,
@@ -1226,15 +1228,17 @@ fn update_torch_shadows(
}
_ => 0,
};
let census = (slot_count > 0)
.then(|| PartCensus {
let census = if slot_count > 0 {
PartCensus {
gx: gx
.as_ref()
.map_or(0, |gx| gx.torch_residency_generation())
.wrapping_add(terrain_gen),
parts: static_part_census(&ents),
})
.unwrap_or_default();
}
} else {
PartCensus::default()
};
// A slot that has NEVER been fingerprinted jumps the queue: with one scan a frame a fresh
// promotion would otherwise wait a whole sweep for its first map, and a slot with no map
// publishes none (`map_publishable`), so that wait is a shadow that is simply missing. There
@@ -1542,87 +1546,85 @@ fn update_torch_shadows(
// `WOW_TORCH_TRACE=1` — once a second, what the lane published: fixture distances + whether a
// caster mesh exists. The app-side half of the debug (the shader group-3 sample is the GPU half).
if std::env::var_os("WOW_TORCH_TRACE").is_some() {
if now - *last_trace >= 1.0 {
*last_trace = now;
// MONKEY (outdoor torch shadows): the header carries the lane budget too — "6 of the
// 12 slots may be exterior, 3 currently are, the sun is at 0.00" is the whole state of
// the split in one line, and it is the first thing to read when an outdoor shadow is
// missing (no EXT slots at sun 1.00 is correct; no EXT slots at sun 0.00 is a bug).
if std::env::var_os("WOW_TORCH_TRACE").is_some() && now - *last_trace >= 1.0 {
*last_trace = now;
// MONKEY (outdoor torch shadows): the header carries the lane budget too — "6 of the
// 12 slots may be exterior, 3 currently are, the sun is at 0.00" is the whole state of
// the split in one line, and it is the first thing to read when an outdoor shadow is
// missing (no EXT slots at sun 1.00 is correct; no EXT slots at sun 0.00 is a bug).
info!(
"torch-trace: {}/{} slots of {} (ext {}/{}, sun {:.2}, {} candidates, caster {}, {} entity casters)",
promoted.len(),
want,
MAX_TORCH_CASTERS,
promoted.iter().filter(|(.., ext)| *ext).count(),
ext_cap,
sun_w,
cands.len(),
if lane.slots.iter().any(|s| s.mesh.is_some()) {
"built"
} else {
"none"
},
lane.trace_parts,
);
// MONKEY (torch lane perf): the FIXED per-frame cost, in the two counters that bound
// it. `gather parts N (rate R Hz, reach X yd)` is the CPU-skinned moving-caster set and
// the cadence + union radius it was taken at; `fingerprint scans S` is how many full
// resident-scene walks the census gate actually let through in the last second (a
// still room should read 0, a streaming one a handful). Both are per SECOND, so they
// are directly comparable to the frame rate beside them: `gathers` equal to the fps is
// `interiorShadowEntityRate 0`, and `scans` equal to the fps is the old behaviour with
// the gate defeated.
info!(
"torch-perf: gather parts {} (rate {} Hz, {} gathers/s, reach {:.0} yd), fingerprint scans {}/s",
lane.trace_parts,
video.interior_shadow_entity_rate,
lane.trace_gathers,
lane.trace_reach,
lane.trace_scans,
);
lane.trace_gathers = 0;
lane.trace_scans = 0;
// MONKEY (torch caster selection): per SLOT, the three numbers the selection is made
// of — which fixture, what it scores at the player, and how far its cross-fade has
// ramped. A shadow that pops is a `w` that jumped; a shadow that should be there and
// is not is a fixture missing from this list (then read `cands`, above).
// MONKEY (torch caster reach): scientific scores expose the small distant tail;
// distance is still from the player, and eligibility now extends to 4R + 3 yd.
for (i, e, p, sc, w, ext) in &promoted {
// MONKEY (torch owner exclusion): `owner` + `own body` are the fix's own readout —
// "who this fixture belongs to" and "how many retained items / entity parts its
// LAST rebuild dropped as that owner's body". A fixture standing in its own black
// square with `own body 0/0` is a missing owner tag; one with a plausible count and
// a shadow still under it is a caster arriving by some third route.
let slot = lane.slots.iter().find(|s| s.cache_slot == *i);
let (gx_excl, part_excl) = slot.map_or((0, 0), |s| s.trace_excluded);
// MONKEY (moving fixture): `moving` is this feature's readout — `live` is a slot
// being re-rendered from scratch every frame, `dropped` one that is moving but past
// TORCH_MOVING_MAX and fast-fading out, `settled` the ordinary cached path. An imp
// whose shadow lags is a slot reading `settled` while it walks (then look at
// `still_since` / the owner tag); a room whose static shadows freeze is two slots
// stuck on `live`.
info!(
"torch-trace: {}/{} slots of {} (ext {}/{}, sun {:.2}, {} candidates, caster {}, {} entity casters)",
promoted.len(),
want,
MAX_TORCH_CASTERS,
promoted.iter().filter(|(.., ext)| *ext).count(),
ext_cap,
sun_w,
cands.len(),
if lane.slots.iter().any(|s| s.mesh.is_some()) {
"built"
} else {
"none"
" slot {i}: lane {} {e} at [{:.1},{:.1},{:.1}] d(player) {:.1} score {:.4e} w {:.2} owner {:?} moving {} own body {gx_excl} gx + {part_excl} parts (camera <= shadowDistance AND player <= 4R+3) static {} dynamic {} requested_live_rank {:?}",
if *ext { "EXT" } else { "INT" },
p.x,
p.y,
p.z,
p.distance(anchor),
sc,
w,
slot.and_then(|s| s.owner),
match slot {
Some(s) if s.moving_live => "live",
Some(s) if s.fast_fade => "dropped",
_ => "settled",
},
lane.trace_parts,
if static_rebuilt & (1 << i) != 0 { "rebuilt (GPU pending)" } else { "cached/requested" },
if dynamic & (1 << i) != 0 { "yes" } else { "no" },
(dynamic & (1 << i) != 0).then(|| TorchShadowViews::live_rank(dynamic, *i)),
);
// MONKEY (torch lane perf): the FIXED per-frame cost, in the two counters that bound
// it. `gather parts N (rate R Hz, reach X yd)` is the CPU-skinned moving-caster set and
// the cadence + union radius it was taken at; `fingerprint scans S` is how many full
// resident-scene walks the census gate actually let through in the last second (a
// still room should read 0, a streaming one a handful). Both are per SECOND, so they
// are directly comparable to the frame rate beside them: `gathers` equal to the fps is
// `interiorShadowEntityRate 0`, and `scans` equal to the fps is the old behaviour with
// the gate defeated.
info!(
"torch-perf: gather parts {} (rate {} Hz, {} gathers/s, reach {:.0} yd), fingerprint scans {}/s",
lane.trace_parts,
video.interior_shadow_entity_rate,
lane.trace_gathers,
lane.trace_reach,
lane.trace_scans,
);
lane.trace_gathers = 0;
lane.trace_scans = 0;
// MONKEY (torch caster selection): per SLOT, the three numbers the selection is made
// of — which fixture, what it scores at the player, and how far its cross-fade has
// ramped. A shadow that pops is a `w` that jumped; a shadow that should be there and
// is not is a fixture missing from this list (then read `cands`, above).
// MONKEY (torch caster reach): scientific scores expose the small distant tail;
// distance is still from the player, and eligibility now extends to 4R + 3 yd.
for (i, e, p, sc, w, ext) in &promoted {
// MONKEY (torch owner exclusion): `owner` + `own body` are the fix's own readout —
// "who this fixture belongs to" and "how many retained items / entity parts its
// LAST rebuild dropped as that owner's body". A fixture standing in its own black
// square with `own body 0/0` is a missing owner tag; one with a plausible count and
// a shadow still under it is a caster arriving by some third route.
let slot = lane.slots.iter().find(|s| s.cache_slot == *i);
let (gx_excl, part_excl) = slot.map_or((0, 0), |s| s.trace_excluded);
// MONKEY (moving fixture): `moving` is this feature's readout — `live` is a slot
// being re-rendered from scratch every frame, `dropped` one that is moving but past
// TORCH_MOVING_MAX and fast-fading out, `settled` the ordinary cached path. An imp
// whose shadow lags is a slot reading `settled` while it walks (then look at
// `still_since` / the owner tag); a room whose static shadows freeze is two slots
// stuck on `live`.
info!(
" slot {i}: lane {} {e} at [{:.1},{:.1},{:.1}] d(player) {:.1} score {:.4e} w {:.2} owner {:?} moving {} own body {gx_excl} gx + {part_excl} parts (camera <= shadowDistance AND player <= 4R+3) static {} dynamic {} requested_live_rank {:?}",
if *ext { "EXT" } else { "INT" },
p.x,
p.y,
p.z,
p.distance(anchor),
sc,
w,
slot.and_then(|s| s.owner),
match slot {
Some(s) if s.moving_live => "live",
Some(s) if s.fast_fade => "dropped",
_ => "settled",
},
if static_rebuilt & (1 << i) != 0 { "rebuilt (GPU pending)" } else { "cached/requested" },
if dynamic & (1 << i) != 0 { "yes" } else { "no" },
(dynamic & (1 << i) != 0).then(|| TorchShadowViews::live_rank(dynamic, *i)),
);
}
}
}
}
@@ -1714,7 +1716,7 @@ fn collect_torch_entities(
if added == 0 {
continue;
}
for tri in pick.0.indices.chunks_exact(3) {
for tri in pick.0.indices.as_chunks::<3>().0 {
if tri.iter().all(|i| *i < added) {
indices.extend(tri.iter().map(|i| base + *i));
}
-1
View File
@@ -200,7 +200,6 @@ fn an_emote_argument_targets_the_named_player_and_not_the_selection() {
let sel = Selection {
target: Some(Entity::from_raw_u32(11).unwrap()),
guid: Some(0x5e1),
..Default::default()
};
let find = |n: &str| (n == "Bob").then_some((bob, 0xb0b));
assert_eq!(emote_target(Some("Bob"), &sel, find, Some(me)), 0xb0b);
+5 -3
View File
@@ -198,9 +198,11 @@ mod tests {
#[test]
fn a_vm_built_after_boot_still_fills_pfquests_file_scope_class_bitmask() {
let mut app = App::new();
app.insert_resource(ChrClassTable(ChrClasses::from_name_rows(
[(1u32, "Warrior".to_owned(), "WARRIOR".to_owned())].into_iter(),
)));
app.insert_resource(ChrClassTable(ChrClasses::from_name_rows([(
1u32,
"Warrior".to_owned(),
"WARRIOR".to_owned(),
)])));
// No VM yet — exactly the ordering a world-entry VM meets: the resource is long since
// loaded, and `PostStartup` has already come and gone.
app.add_systems(PostStartup, feed_class_info);
+1 -1
View File
@@ -109,7 +109,7 @@ fn inspect_slot_view(
.player_visible_item_enchant(slot0, u8::try_from(j).unwrap_or(0))
.unwrap_or(0) as i32
}),
random_properties_id: u32::from(roll),
random_properties_id: roll,
..Default::default()
},
..Default::default()
+2 -3
View File
@@ -968,9 +968,8 @@ mod tests {
};
use benilla_ui::script::EQUIPMENT_BAG;
/// Both ends of every rung of `GetKeyRingSize` (`ContainerFrame.lua:773`).
/// The wire's 16 positions (vmangos `KEYRING_SLOT_END` 97), not the level-gated count.
/// Both ends of every rung of `GetKeyRingSize` (`ContainerFrame.lua:773`), on the wire's 16
/// positions (vmangos `KEYRING_SLOT_END` 97) rather than the level-gated count.
#[test]
fn wire_pos_maps_the_keyring_onto_the_player_grid() {
assert_eq!(wire_pos(KEYRING_CONTAINER, 1), Some((255, 81)));
@@ -120,7 +120,7 @@ pub(crate) fn trace_visibility_churn(
// Worst first: the driver is usually the top one, and its children follow it.
// Worst first. The tie-break is the resolved name, so the line is stable run to run; it
// resolves only after the truncation, which is the whole point of carrying handles.
loud.sort_by(|a, b| b.1.cmp(&a.1));
loud.sort_by_key(|row| std::cmp::Reverse(row.1));
loud.truncate(12);
let mut named: Vec<(String, u32)> = loud
.into_iter()
+24 -24
View File
@@ -856,6 +856,30 @@ pub(crate) fn shutdown_on_exit(
/// player's own create (`0x5deb60 call 0x4908c0`). This fires `PLAYER_LOGIN` at the end of the
/// load on both, the `/reload` shape; `PLAYER_ENTERING_WORLD` fires from [`crate::ui_unit`] once
/// the self descriptor lands.
/// The player's UTC offset in seconds, for `date()` and `C_DateAndTime.GetServerTimeLocal`.
///
/// `std` has no local-time support, so this is `localtime_r` on the current instant: the offset
/// *as it stands now*, DST included, rather than the zone's standard offset. Read once at VM setup
/// — a session that crosses a DST boundary keeps the offset it booted with, which is one hour
/// wrong for the rest of that session and is the same thing every client that caches it does.
///
/// 0 on failure, which is UTC — the behaviour this tree had everywhere before, so a broken or
/// missing zone database leaves the clock exactly where it used to be rather than off by a
/// garbage amount.
fn local_utc_offset_secs() -> i32 {
// SAFETY: `localtime_r` writes a caller-owned `tm` and takes a pointer to a live `time_t`;
// both live for the call, and the `_r` form keeps no global state.
unsafe {
let now: libc::time_t = libc::time(std::ptr::null_mut());
let mut tm: libc::tm = std::mem::zeroed();
if libc::localtime_r(&now, &mut tm).is_null() {
return 0;
}
// `tm_gmtoff` is seconds EAST of UTC, which is the sign `date()` wants to add.
i32::try_from(tm.tm_gmtoff).unwrap_or(0)
}
}
#[cfg(test)]
pub(crate) fn finish_ui_load(script: &mut UiScript) {
finish_ui_load_with(script, |_| {}, |_| {});
@@ -1099,27 +1123,3 @@ mod tests {
);
}
}
/// The player's UTC offset in seconds, for `date()` and `C_DateAndTime.GetServerTimeLocal`.
///
/// `std` has no local-time support, so this is `localtime_r` on the current instant: the offset
/// *as it stands now*, DST included, rather than the zone's standard offset. Read once at VM setup
/// — a session that crosses a DST boundary keeps the offset it booted with, which is one hour
/// wrong for the rest of that session and is the same thing every client that caches it does.
///
/// 0 on failure, which is UTC — the behaviour this tree had everywhere before, so a broken or
/// missing zone database leaves the clock exactly where it used to be rather than off by a
/// garbage amount.
fn local_utc_offset_secs() -> i32 {
// SAFETY: `localtime_r` writes a caller-owned `tm` and takes a pointer to a live `time_t`;
// both live for the call, and the `_r` form keeps no global state.
unsafe {
let now: libc::time_t = libc::time(std::ptr::null_mut());
let mut tm: libc::tm = std::mem::zeroed();
if libc::localtime_r(&now, &mut tm).is_null() {
return 0;
}
// `tm_gmtoff` is seconds EAST of UTC, which is the sign `date()` wants to add.
i32::try_from(tm.tm_gmtoff).unwrap_or(0)
}
}
+1
View File
@@ -232,6 +232,7 @@ fn select_fog_lamps(
selected
}
#[allow(clippy::type_complexity)] // one Bevy system's full input set
fn update_fog(
mut commands: Commands,
video: Res<VideoConfig>,
+1 -1
View File
@@ -761,6 +761,6 @@ mod tests {
assert_eq!(VolLightOverride(None).tier(&video), 2);
assert_eq!(VolLightOverride(Some(1)).tier(&video), 1);
assert_eq!(STEPS[0], 0.0);
assert!(STEPS[2] > STEPS[1]);
const _: () = assert!(STEPS[2] > STEPS[1]);
}
}
+1
View File
@@ -435,6 +435,7 @@ fn occlusion_probe(eye: Vec3, unit_origin: Vec3) -> Option<(Dir3, f32)> {
(reach > 0.0).then_some((dir, reach))
}
#[allow(clippy::type_complexity)] // one Bevy system's full input set
fn drive_vplates(
mode: Res<VPlateMode>,
mut plates: ResMut<VPlates>,
+9 -6
View File
@@ -156,6 +156,7 @@ impl WorldLane {
}
#[allow(clippy::too_many_arguments)]
#[allow(clippy::type_complexity)] // one Bevy system's full input set
fn update_world_shadows(
video: Res<VideoConfig>,
time: Res<Time>,
@@ -271,9 +272,9 @@ fn update_world_shadows(
lane.static_mesh = Some(mesh);
lane.static_rebuilt_at = None;
}
let due = lane.static_rebuilt_at.map_or(true, |at| {
at.distance(frame.light_position) > STATIC_REBUILD_STEP
});
let due = lane
.static_rebuilt_at
.is_none_or(|at| at.distance(frame.light_position) > STATIC_REBUILD_STEP);
if !due {
return;
}
@@ -400,9 +401,11 @@ mod streamed_refresh_tests {
#[test]
fn shadow_cache_refresh_runs_without_volumetric_fog_and_is_quiet_when_unchanged() {
let mut app = App::new();
let mut video = VideoConfig::default();
video.volumetric_fog = 0;
video.world_shadows = false;
let video = VideoConfig {
volumetric_fog: 0,
world_shadows: false,
..Default::default()
};
app.insert_resource(video)
.init_resource::<StaticGx>()
.init_resource::<WorldLane>()
-137
View File
@@ -1,137 +0,0 @@
//! TEMPORARY repro harness — not for commit. Loads the stock MailFrame chain from loose mirror
//! files under /tmp/opencode/vanilla-ui and drives the open-letter path, printing Lua errors.
use benilla_ui::script::{MailInboxRow, MailState, UiScript};
const UI: &str = "/tmp/opencode/vanilla-ui";
const FILES: &[&str] = &[
"Interface\\FrameXML\\Fonts.xml",
r"Interface\FrameXML\MoneyFrame.lua",
r"Interface\FrameXML\MoneyFrame.xml",
r"Interface\FrameXML\MoneyInputFrame.lua",
r"Interface\FrameXML\MoneyInputFrame.xml",
"Interface\\FrameXML\\GlobalStrings.lua",
r"Interface\FrameXML\UIParent.xml",
"ScrollTemplates.xml",
r"Interface\FrameXML\UIPanelTemplates.lua",
r"Interface\FrameXML\UIPanelTemplates.xml",
"Interface\\FrameXML\\BasicControls.xml",
"Interface\\FrameXML\\LocaleProperties.lua",
"Interface\\FrameXML\\StaticPopup.xml",
"Interface\\FrameXML\\GameTooltip.xml",
r"Interface\FrameXML\ItemButtonTemplate.xml",
"Interface\\FrameXML\\UIDropDownMenu.xml",
"Interface\\FrameXML\\CharacterFrameTemplates.xml",
"Interface\\FrameXML\\FriendsFrame.xml",
"Interface\\FrameXML\\MailFrame.xml",
];
fn load_ui(script: &UiScript) {
let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("assets/ui");
let read = |req: &str| -> Option<Vec<u8>> {
let p = std::path::Path::new(UI).join(req.replace('\\', "/"));
if let Ok(b) = std::fs::read(&p) {
return Some(b);
}
std::fs::read(dir.join(req)).ok()
};
for entry in FILES {
let bytes = read(entry).unwrap_or_else(|| panic!("{entry}: not found"));
if entry.to_ascii_lowercase().ends_with(".lua") {
script
.run_chunk_named(&bytes, &format!("@{entry}"))
.unwrap_or_else(|e| panic!("{entry}: {e}"));
continue;
}
let doc = benilla_ui::framexml::parse(&benilla_ui::source::decode(&bytes))
.unwrap_or_else(|e| panic!("{entry}: {e}"));
let report = benilla_ui::loader::load_in(script, &doc, &entry.replace('\\', "/"), &read);
assert!(
report.errors.is_empty(),
"{entry}: loader errors: {:#?}",
report.errors
);
let leaf = entry.rsplit(['\\', '/']).next().unwrap_or("");
if leaf.eq_ignore_ascii_case("UIParent.xml") && entry.contains('\\') {
script
.run(
"FCF_OnUpdate = FCF_OnUpdate or function() end \
FCF_DockUpdate = FCF_DockUpdate or function() end \
UnitPopup_OnUpdate = UnitPopup_OnUpdate or function() end \
BattlefieldFrame_OnUpdate = BattlefieldFrame_OnUpdate or function() end \
MultiActionBar_Update = MultiActionBar_Update or function() end \
RaidOptionsFrame_UpdatePartyFrames = RaidOptionsFrame_UpdatePartyFrames or function() end \
LocalizeFrames = LocalizeFrames or function() end \
updateContainerFrameAnchors = updateContainerFrameAnchors or function() end \
UpdateNameplates = UpdateNameplates or function() end \
CloseAllBags = CloseAllBags or function() end \
OpenBackpack = OpenBackpack or function() end \
CloseBackpack = CloseBackpack or function() end \
NUM_CONTAINER_FRAMES = NUM_CONTAINER_FRAMES or 0 \
CONTAINER_OFFSET_X = CONTAINER_OFFSET_X or 0 \
CONTAINER_OFFSET_Y = CONTAINER_OFFSET_Y or 70 \
BATTLEFIELD_TAB_OFFSET_Y = BATTLEFIELD_TAB_OFFSET_Y or 210 \
PETACTIONBAR_YPOS = PETACTIONBAR_YPOS or 98 \
PETACTIONBAR_XPOS = PETACTIONBAR_XPOS or 36",
)
.unwrap();
}
}
}
fn row(sender: Option<&str>, subject: &str, money: u32, cod: u32) -> MailInboxRow {
MailInboxRow {
package_icon: None,
stationery_icon: Some("Interface\\Icons\\INV_Misc_Note_01".to_string()),
sender: sender.map(str::to_string),
subject: subject.to_string(),
money,
cod,
days_left: 29.0,
item_count: 0,
was_read: false,
was_returned: false,
text_created: false,
can_reply: sender.is_some(),
is_gm: false,
body: Some("body".into()),
stationery_texture: "STATIONERYTEST".into(),
is_invoice: false,
invoice: None,
has_body: true,
item_id: 0,
item_name: None,
item_texture: None,
item_quality: None,
can_delete: false,
item_random_property_id: 0,
}
}
#[test]
fn repro() {
// A stale openMailID: open row 1, then the inbox re-lists smaller (row purged), keeping the
// letter open; MAIL_INBOX_UPDATE re-runs InboxFrame_Update + OpenMail_Update on the stale id.
let mut s = UiScript::new().unwrap();
load_ui(&s);
s.set_mail(Some(MailState {
inbox: vec![
row(None, "Welcome", 0, 0),
row(Some("Jaina"), "second", 0, 0),
],
}));
s.fire_event("MAIL_SHOW", vec![]);
s.fire_event("MAIL_INBOX_UPDATE", vec![]);
let _ = s.take_errors();
s.run("MailItem1Button:Click()").unwrap();
let errs = s.take_errors();
eprintln!("errors after click: {errs:#?}");
// The letter stays open while the list empties out (rows purged) — row 1 is now out of range.
s.set_mail(Some(MailState { inbox: vec![] }));
s.fire_event("MAIL_INBOX_UPDATE", vec![]);
let errs = s.take_errors();
eprintln!("errors after empty re-list: {errs:#?}");
assert!(true);
}
@@ -189,7 +189,6 @@ mod tests {
assert_eq!(b.point_count(), MAX_LIVE_POINT_LIGHTS);
assert_eq!(b.points.len(), 2 * MAX_LIVE_POINT_LIGHTS);
assert_eq!(MAX_LIVE_POINT_LIGHTS, MAX_POINT_LIGHTS - 1);
assert!(MAX_LIVE_POINT_LIGHTS <= 255, "EXT_SEL_EMPTY is index 255");
}
/// The glue scene's fog toggle flips only the enable and leaves the race's fog rows standing.
+14 -11
View File
@@ -509,9 +509,9 @@ pub fn daylight_target(light: &WowLighting) -> ([f32; 3], f32, f32) {
let ndotl = l.y.max(0.0);
let mut target = [0.0f32; 3];
let mut peak = 0.0f32;
for i in 0..3 {
target[i] = (light.ambient[i] + light.diffuse[i] * ndotl).clamp(0.0, 1.0);
peak = peak.max(target[i]);
for (i, channel) in target.iter_mut().enumerate() {
*channel = (light.ambient[i] + light.diffuse[i] * ndotl).clamp(0.0, 1.0);
peak = peak.max(*channel);
}
let hue = if peak > 1e-4 {
[target[0] / peak, target[1] / peak, target[2] / peak]
@@ -885,9 +885,9 @@ fn plan_clusters(points: &[[f32; 3]]) -> Vec<([f32; 3], [f32; 3])> {
for (p, ci) in points.iter().zip(of_point.iter()) {
let root = find(&mut parent, *ci);
let e = boxes.entry(root).or_insert((*p, *p));
for a in 0..3 {
e.0[a] = e.0[a].min(p[a]);
e.1[a] = e.1[a].max(p[a]);
for ((lo, hi), v) in e.0.iter_mut().zip(e.1.iter_mut()).zip(p) {
*lo = lo.min(*v);
*hi = hi.max(*v);
}
}
let mut out: Vec<([f32; 3], [f32; 3])> = boxes.into_values().collect();
@@ -938,6 +938,7 @@ const WINDOW_SPLIT_MAX: usize = 6;
/// stitch -- three vertices where two walls meet a floor), must stand no more than
/// [`SEAM_MAX_Z_EXTENT`] tall (or it is the vertical seam of a whole party wall), and must fall in
/// the same 1..24 yd size band the aperture seed uses.
#[allow(clippy::type_complexity)] // per cluster: its group, centroid, inward normal and extent
fn boundary_clusters(
groups: &[WmoGroupInfo],
batches: &[(u16, bool, &[[f32; 3]])],
@@ -955,8 +956,8 @@ fn boundary_clusters(
slot.push(positions);
let acc = &mut sum[usize::from(gi)];
for p in positions {
for a in 0..3 {
acc.0[a] += f64::from(p[a]);
for (axis, v) in acc.0.iter_mut().zip(p) {
*axis += f64::from(*v);
}
acc.1 += 1;
}
@@ -1135,9 +1136,9 @@ fn cluster_boxes(points: &[[f32; 3]]) -> Vec<([f32; 3], [f32; 3])> {
for (p, ci) in points.iter().zip(of_point.iter()) {
let root = find(&mut parent, *ci);
let e = boxes.entry(root).or_insert((*p, *p));
for a in 0..3 {
e.0[a] = e.0[a].min(p[a]);
e.1[a] = e.1[a].max(p[a]);
for ((lo, hi), v) in e.0.iter_mut().zip(e.1.iter_mut()).zip(p) {
*lo = lo.min(*v);
*hi = hi.max(*v);
}
}
let mut out: Vec<([f32; 3], [f32; 3])> = boxes.into_values().collect();
@@ -1204,6 +1205,7 @@ pub fn daylight_modes() -> (bool, bool, bool) {
/// contribute nothing. Removing the component takes it out of the packer's query outright and costs
/// two archetype moves per game day. Everything else about the entity (its claims, its lane, its
/// reach) is untouched, so dawn re-inserts and the claim set is exactly the one built at spawn.
#[allow(clippy::type_complexity)] // one Bevy system's full input set
pub fn update_daylight_fixtures(
mut commands: Commands,
light: Res<WowLighting>,
@@ -1959,6 +1961,7 @@ fn bleed_intensity(lit: [f32; 3], r_eff: f32, k_fill: f32) -> ([f32; 3], f32) {
/// remains is structural and harmless: `update_daylight_fixtures` INSERTS and REMOVES its `PointLight`
/// through `Commands`, so on the single frame a daylight fixture switches on at dawn or off at dusk
/// the bleed reading it is one frame stale. Its steady-state value is written in place and is current.
#[allow(clippy::type_complexity)] // one Bevy system's full input set
pub fn update_bleed_fixtures(
mut commands: Commands,
knobs: Res<DynamicInteriors>,
@@ -9,6 +9,7 @@
use super::*;
use benilla_formats::{open_chain, parse_wmo_portals, parse_wmo_root, wmo_group_header};
#[allow(clippy::type_complexity)] // the batch row is the tuple the loader hands back
struct Loaded {
groups: Vec<WmoGroupInfo>,
names: Vec<String>,
@@ -41,7 +42,9 @@ fn group_names(bytes: &[u8]) -> Vec<String> {
let (Some(mogn), Some(mogi)) = (mogn, mogi) else {
return Vec::new();
};
mogi.chunks_exact(32)
mogi.as_chunks::<32>()
.0
.iter()
.map(|r| {
let off = i32::from_le_bytes(r[28..32].try_into().unwrap());
usize::try_from(off)
@@ -281,9 +281,11 @@ mod tests {
#[test]
fn modern_marks_the_scene_end_and_fades_the_sun_at_night() {
let mut l = WowLighting::default();
l.fog_end = 444.0;
l.celestial_dir = Vec3::new(0.0, 0.8, 0.6);
let mut l = WowLighting {
fog_end: 444.0,
celestial_dir: Vec3::new(0.0, 0.8, 0.6),
..Default::default()
};
let mut f = MonkeyFrame::default();
fill_frame(&mut f, FogModel::Modern, &l, &derived_band(&l));
assert_eq!(f.fog_scene_end, 444.0);
@@ -145,6 +145,10 @@ pub(super) const MAX_POINT_LIGHTS: usize = 256;
/// blob or the room-claim table changes.
pub(super) const MAX_LIVE_POINT_LIGHTS: usize = MAX_POINT_LIGHTS - 1;
/// The selection byte spends index 255 on `EXT_SEL_EMPTY`, so no live light may land there. At
/// compile time, where a test could only say it after the build that broke it.
const _: () = assert!(MAX_LIVE_POINT_LIGHTS <= 255, "EXT_SEL_EMPTY is index 255");
/// MONKEY (dynamic interiors): a fixture this close to the camera (yd) is packed regardless of the
/// portal flood — see the room term in [`build_light_data`]. Sized to a WHOLE BUILDING PLUS its
/// approach, not just the room: at 40 yd, walking around the outside of the inn kept crossing the
@@ -194,6 +198,7 @@ impl LightRooms {
/// hall, guest rooms and basement claim NOTHING.
/// * **NSabbey** — 42 fixtures over 14 groups, but `Main Hall`, `LftWng`, `RtWng`, `Library`,
/// `Library2`, `Library Wing` and `Stairs2` are named by no MOLR either.
///
/// Gating purely on MOLR would therefore black out most of both buildings — the abbey's look is
/// the one the owner signed off, so that is a regression, not a fix. MOLR is authored for the
/// reference's own purpose (register GL lights while drawing a VISIBLE group's doodads and units),
@@ -970,6 +975,7 @@ fn unpack_shadow_lane(w: f32) -> (f32, f32) {
/// reach it from below, and cannot leave it from above.
/// * `u32(max(w - 1, 0) + 0.5)` — the debug decode (`static_gx` x3, `wow_model`, `terrain`):
/// `debug + f + 0.5` truncates to `debug` for every `f` in `[0, 0.5)`.
///
/// **0.49** keeps a clear margin under that 0.5 cliff while spending the whole of the rest of the
/// range on the value. `1 + debug` is an exact integer in f32, so the shader's `fract(w)` returns
/// `daylight * 0.49` bit-for-bit up to the f32 ulp at `w <= 5.49` (~5e-7 — four orders of magnitude
@@ -1275,7 +1281,7 @@ impl RoomClaim {
Some(g) => {
// MONKEY (soft portal claims): the entry weight rides the spare high bits.
let entry = self.fades[i].entry.clamp(0.0, 1.0);
u32::from(*g & !LIT_ROOM_EXT_DENY) + 1
(u32::from(*g & !LIT_ROOM_EXT_DENY) + 1)
| if *g & LIT_ROOM_EXT_DENY == 0 {
CLAIM_EXT_OK
} else {
+2 -2
View File
@@ -152,10 +152,10 @@ fn stitch_tag(
tx: f32,
ty: f32,
) -> f32 {
let (a, b, t) = if (fy == 0 || fy + 1 == fine_rows) && fx % SUBDIVISIONS != 0 {
let (a, b, t) = if (fy == 0 || fy + 1 == fine_rows) && !fx.is_multiple_of(SUBDIVISIONS) {
let y = if fy == 0 { 0 } else { rows - 1 };
(y * cols + sx, y * cols + sx + 1, tx)
} else if (fx == 0 || fx + 1 == fine_cols) && fy % SUBDIVISIONS != 0 {
} else if (fx == 0 || fx + 1 == fine_cols) && !fy.is_multiple_of(SUBDIVISIONS) {
let x = if fx == 0 { 0 } else { cols - 1 };
(sy * cols + x, (sy + 1) * cols + x, ty)
} else {
+8 -4
View File
@@ -43,6 +43,7 @@ const GRAVITY_YD: f32 = 10.72;
/// `6.2831853` exactly as the shader spells it (`enhanced_water.wgsl`). Identical to `f32::TAU` once
/// rounded, spelled as the literal so the mirror reads against the line it copies.
#[allow(clippy::approx_constant)] // the shader's spelling is the point, see above
const TWO_PI: f32 = 6.2831853;
/// The ADT **ocean**'s wave energy — `liquid/surface.rs` packs `water.mode.y = 1.0` for
@@ -215,10 +216,13 @@ mod tests {
"enhanced_water.wgsl WATER_WAVES[0..2]"
);
assert_eq!(GRAVITY_YD, 10.72, "enhanced_water.wgsl gravity");
assert_eq!(
TWO_PI, 6.2831853,
"enhanced_water.wgsl k = 6.2831853/wavelength"
);
#[allow(clippy::approx_constant)] // likewise the mirror's own literal
{
assert_eq!(
TWO_PI, 6.2831853,
"enhanced_water.wgsl k = 6.2831853/wavelength"
);
}
assert_eq!(WATER_WIND_DIR, 0.35, "enhanced_water.wgsl WATER_WIND_DIR");
assert_eq!(
GERSTNER_CHOP, 2.4,
+1 -1
View File
@@ -420,7 +420,7 @@ fn prepare_static_gx(
// (a bare static_gx harness has no lighting) — absent reads as the shipped Gaussian.
shadow_filter: Option<Res<crate::lighting::ShadowFilterGaussian>>,
) {
let shadow_gaussian = shadow_filter.map_or(true, |f| f.0);
let shadow_gaussian = shadow_filter.is_none_or(|f| f.0);
let _t = super::gx_perf_guard(3);
let Some((view, msaa, _, _)) = views.iter().next() else {
return;
+2 -2
View File
@@ -77,7 +77,7 @@ impl StaticGx {
.map(|p| t.transform_point(wow_to_bevy(*p)).to_array()),
);
let added = positions.len() as u32 - base;
for tri in item.geometry.indices.chunks_exact(3) {
for tri in item.geometry.indices.as_chunks::<3>().0 {
if tri.iter().all(|i| *i < added) {
indices.extend(tri.iter().map(|i| base + *i));
}
@@ -144,7 +144,7 @@ impl StaticGx {
);
bucket.uvs.extend_from_slice(&geometry.uvs);
let added = bucket.positions.len() as u32 - base;
for tri in geometry.indices.chunks_exact(3) {
for tri in geometry.indices.as_chunks::<3>().0 {
if tri.iter().all(|i| *i < added) {
bucket.indices.extend(tri.iter().map(|i| base + *i));
}
@@ -1122,7 +1122,7 @@ impl super::StaticGx {
.to_array()
}));
let added = positions.len() as u32 - base;
for tri in item.geometry.indices.chunks_exact(3) {
for tri in item.geometry.indices.as_chunks::<3>().0 {
if tri.iter().all(|i| *i < added) {
indices.extend(tri.iter().map(|i| base + *i));
}
+1 -1
View File
@@ -113,7 +113,7 @@ fn ride(state: &BobState) -> (f32, Quat) {
// Clamp the SLOPE, not the resulting quaternion: the wave normal is `(-∂x, 1, -∂z)`
// (`enhanced_water.wgsl`), so a gradient of length `tan(θ)` is a lean of exactly θ.
let len = g.length();
let g = if len > MAX_TILT_TAN && len > 0.0 {
let g = if len > MAX_TILT_TAN {
g * (MAX_TILT_TAN / len)
} else {
g
+9 -7
View File
@@ -353,13 +353,15 @@ fn drive_unit(
// hanging in deep water is the state the reference's driver renders almost invisible, and it
// is exactly the state the owner's murloc will be in. Classic touches nothing here — not the
// timer, not the RNG stream.
if look.enhanced && look.swimming && !matches!(state, WadeState::Translating { .. }) {
if now >= foam_state.tread_ready {
foam_state.tread_ready = now + TREAD_INTERVAL;
if let Some(p) = tread_params(look, scale, &mut foam_state.rng) {
let heading = rand01(&mut foam_state.rng) * std::f32::consts::TAU;
emit_record(alloc, index, water, [wow[0], wow[1]], &p, heading, now);
}
if look.enhanced
&& look.swimming
&& !matches!(state, WadeState::Translating { .. })
&& now >= foam_state.tread_ready
{
foam_state.tread_ready = now + TREAD_INTERVAL;
if let Some(p) = tread_params(look, scale, &mut foam_state.rng) {
let heading = rand01(&mut foam_state.rng) * std::f32::consts::TAU;
emit_record(alloc, index, water, [wow[0], wow[1]], &p, heading, now);
}
}
+4 -2
View File
@@ -175,8 +175,10 @@ mod tests {
#[test]
fn the_ripple_clock_wraps() {
let mut w = Wetness::default();
w.ripple_time_s = RIPPLE_WRAP_S - 0.05;
let mut w = Wetness {
ripple_time_s: RIPPLE_WRAP_S - 0.05,
..Default::default()
};
w.step(0.0, 0.1);
assert!(w.ripple_time_s < 0.1);
}