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:
@@ -51,6 +51,7 @@ struct CharacterLane {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
#[allow(clippy::type_complexity)] // one Bevy system's full input set
|
||||||
fn collect_character_shadows(
|
fn collect_character_shadows(
|
||||||
video: Res<VideoConfig>,
|
video: Res<VideoConfig>,
|
||||||
time: Res<Time>,
|
time: Res<Time>,
|
||||||
|
|||||||
@@ -3124,8 +3124,8 @@ mod tests {
|
|||||||
apply_lighting_preset(&mut cvars, name);
|
apply_lighting_preset(&mut cvars, name);
|
||||||
// Somewhere else on the row's own scale: the flags flip, the numbers move by one.
|
// Somewhere else on the row's own scale: the flags flip, the numbers move by one.
|
||||||
let moved = match v.trim().parse::<f32>() {
|
let moved = match v.trim().parse::<f32>() {
|
||||||
Ok(x) if x == 0.0 => "1".to_string(),
|
Ok(0.0) => "1".to_string(),
|
||||||
Ok(x) if x == 1.0 => "0".to_string(),
|
Ok(1.0) => "0".to_string(),
|
||||||
Ok(x) => (x + 1.0).to_string(),
|
Ok(x) => (x + 1.0).to_string(),
|
||||||
Err(_) => format!("{v}x"),
|
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
|
/// 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
|
/// `Propagate` has run. `update_torch_shadows` reads the verdict in `Last`, i.e. later in the same
|
||||||
/// frame — never one stale.
|
/// frame — never one stale.
|
||||||
|
#[allow(clippy::type_complexity)] // one Bevy system's full input set
|
||||||
pub(crate) fn track_carried_light_motion(
|
pub(crate) fn track_carried_light_motion(
|
||||||
mut commands: Commands,
|
mut commands: Commands,
|
||||||
time: Res<Time>,
|
time: Res<Time>,
|
||||||
|
|||||||
@@ -468,7 +468,7 @@ fn spawn_screen(
|
|||||||
LoginAction::ToggleSave,
|
LoginAction::ToggleSave,
|
||||||
CheckMark::Save,
|
CheckMark::Save,
|
||||||
form.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
|
// 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`
|
// account name and never a secret — so the label is literal rather than a `GlueStrings`
|
||||||
|
|||||||
@@ -3860,8 +3860,9 @@ mod stage_latch_tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// The second half of the handover, and the reason [`super::sync_body_booth`] clears the
|
/// 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
|
/// latch: a pane's bake lives only as long as its room, and every pane numbers its bakes
|
||||||
/// 1. A window that closes and reopens brings the *same* pane back at revision 1, so a latch
|
/// 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"
|
/// 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.
|
/// while the booth actually holds whatever the mirror left in it. Cleared, it is fresh again.
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ impl Plugin for SunShaftsPlugin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::type_complexity)] // one Bevy system's full input set
|
||||||
fn update_views(
|
fn update_views(
|
||||||
mut commands: Commands,
|
mut commands: Commands,
|
||||||
video: Res<VideoConfig>,
|
video: Res<VideoConfig>,
|
||||||
|
|||||||
@@ -456,6 +456,7 @@ fn apply_shadow_quality(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
#[allow(clippy::type_complexity)] // one Bevy system's full input set
|
||||||
fn manage_rig(
|
fn manage_rig(
|
||||||
state: Res<State<ClientState>>,
|
state: Res<State<ClientState>>,
|
||||||
video: Res<VideoConfig>,
|
video: Res<VideoConfig>,
|
||||||
@@ -695,7 +696,7 @@ pub(crate) fn append_triangles(
|
|||||||
base: u32,
|
base: u32,
|
||||||
indices: &mut Vec<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) {
|
if triangle.iter().all(|index| *index < vertex_count) {
|
||||||
indices.extend(triangle.iter().map(|index| base + *index));
|
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`
|
/// `(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.
|
/// query is written inline (not aliased) so a lane can pass its own identically-typed query.
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
#[allow(clippy::type_complexity)] // the caster query a lane passes in, spelled out
|
||||||
pub(crate) fn collect_entity_geometry(
|
pub(crate) fn collect_entity_geometry(
|
||||||
parts: &Query<
|
parts: &Query<
|
||||||
(
|
(
|
||||||
@@ -868,7 +870,7 @@ fn append_skinned(
|
|||||||
let (Some(joints), Some(weights)) = (pick.0.joints.get(index), pick.0.weights.get(index))
|
let (Some(joints), Some(weights)) = (pick.0.joints.get(index), pick.0.weights.get(index))
|
||||||
else {
|
else {
|
||||||
let fallback = palette
|
let fallback = palette
|
||||||
.get(0)
|
.first()
|
||||||
.map(|matrix| matrix.transform_point3(wow_to_bevy(*position)))
|
.map(|matrix| matrix.transform_point3(wow_to_bevy(*position)))
|
||||||
.unwrap_or(Vec3::ZERO);
|
.unwrap_or(Vec3::ZERO);
|
||||||
positions.push(fallback.to_array());
|
positions.push(fallback.to_array());
|
||||||
|
|||||||
@@ -434,7 +434,7 @@ mod tests {
|
|||||||
fn the_endless_frame_count_is_a_day_at_the_streams_own_rate() {
|
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 * 44_100, 3_810_240_000);
|
||||||
assert_eq!(ENDLESS_SECS * 48_000, 4_147_200_000);
|
assert_eq!(ENDLESS_SECS * 48_000, 4_147_200_000);
|
||||||
assert!(
|
const _: () = assert!(
|
||||||
ENDLESS_SECS * 48_000 < usize::MAX / 1024,
|
ENDLESS_SECS * 48_000 < usize::MAX / 1024,
|
||||||
"nowhere near the arithmetic's edge"
|
"nowhere near the arithmetic's edge"
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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
|
/// 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.
|
/// added the player anchor and the interior knobs, and three separate members would not compile.
|
||||||
#[derive(bevy::ecs::system::SystemParam)]
|
#[derive(bevy::ecs::system::SystemParam)]
|
||||||
|
#[allow(clippy::type_complexity)] // the caster query, written inline by the lane's own contract
|
||||||
struct EntityCasters<'w, 's> {
|
struct EntityCasters<'w, 's> {
|
||||||
/// The SAME query the world lane passes to `collect_entity_geometry` (written inline by that
|
/// The SAME query the world lane passes to `collect_entity_geometry` (written inline by that
|
||||||
/// contract so a lane can pass its own).
|
/// 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::too_many_arguments)]
|
||||||
|
#[allow(clippy::type_complexity)] // one Bevy system's full input set
|
||||||
fn update_torch_shadows(
|
fn update_torch_shadows(
|
||||||
video: Res<VideoConfig>,
|
video: Res<VideoConfig>,
|
||||||
state: Res<State<ClientState>>,
|
state: Res<State<ClientState>>,
|
||||||
@@ -1226,15 +1228,17 @@ fn update_torch_shadows(
|
|||||||
}
|
}
|
||||||
_ => 0,
|
_ => 0,
|
||||||
};
|
};
|
||||||
let census = (slot_count > 0)
|
let census = if slot_count > 0 {
|
||||||
.then(|| PartCensus {
|
PartCensus {
|
||||||
gx: gx
|
gx: gx
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map_or(0, |gx| gx.torch_residency_generation())
|
.map_or(0, |gx| gx.torch_residency_generation())
|
||||||
.wrapping_add(terrain_gen),
|
.wrapping_add(terrain_gen),
|
||||||
parts: static_part_census(&ents),
|
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
|
// 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
|
// 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
|
// 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
|
// `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).
|
// 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 std::env::var_os("WOW_TORCH_TRACE").is_some() && now - *last_trace >= 1.0 {
|
||||||
if now - *last_trace >= 1.0 {
|
*last_trace = now;
|
||||||
*last_trace = now;
|
// MONKEY (outdoor torch shadows): the header carries the lane budget too — "6 of the
|
||||||
// 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
|
||||||
// 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
|
||||||
// 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).
|
||||||
// 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!(
|
info!(
|
||||||
"torch-trace: {}/{} slots of {} (ext {}/{}, sun {:.2}, {} candidates, caster {}, {} entity casters)",
|
" 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 {:?}",
|
||||||
promoted.len(),
|
if *ext { "EXT" } else { "INT" },
|
||||||
want,
|
p.x,
|
||||||
MAX_TORCH_CASTERS,
|
p.y,
|
||||||
promoted.iter().filter(|(.., ext)| *ext).count(),
|
p.z,
|
||||||
ext_cap,
|
p.distance(anchor),
|
||||||
sun_w,
|
sc,
|
||||||
cands.len(),
|
w,
|
||||||
if lane.slots.iter().any(|s| s.mesh.is_some()) {
|
slot.and_then(|s| s.owner),
|
||||||
"built"
|
match slot {
|
||||||
} else {
|
Some(s) if s.moving_live => "live",
|
||||||
"none"
|
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 {
|
if added == 0 {
|
||||||
continue;
|
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) {
|
if tri.iter().all(|i| *i < added) {
|
||||||
indices.extend(tri.iter().map(|i| base + *i));
|
indices.extend(tri.iter().map(|i| base + *i));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -200,7 +200,6 @@ fn an_emote_argument_targets_the_named_player_and_not_the_selection() {
|
|||||||
let sel = Selection {
|
let sel = Selection {
|
||||||
target: Some(Entity::from_raw_u32(11).unwrap()),
|
target: Some(Entity::from_raw_u32(11).unwrap()),
|
||||||
guid: Some(0x5e1),
|
guid: Some(0x5e1),
|
||||||
..Default::default()
|
|
||||||
};
|
};
|
||||||
let find = |n: &str| (n == "Bob").then_some((bob, 0xb0b));
|
let find = |n: &str| (n == "Bob").then_some((bob, 0xb0b));
|
||||||
assert_eq!(emote_target(Some("Bob"), &sel, find, Some(me)), 0xb0b);
|
assert_eq!(emote_target(Some("Bob"), &sel, find, Some(me)), 0xb0b);
|
||||||
|
|||||||
@@ -198,9 +198,11 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn a_vm_built_after_boot_still_fills_pfquests_file_scope_class_bitmask() {
|
fn a_vm_built_after_boot_still_fills_pfquests_file_scope_class_bitmask() {
|
||||||
let mut app = App::new();
|
let mut app = App::new();
|
||||||
app.insert_resource(ChrClassTable(ChrClasses::from_name_rows(
|
app.insert_resource(ChrClassTable(ChrClasses::from_name_rows([(
|
||||||
[(1u32, "Warrior".to_owned(), "WARRIOR".to_owned())].into_iter(),
|
1u32,
|
||||||
)));
|
"Warrior".to_owned(),
|
||||||
|
"WARRIOR".to_owned(),
|
||||||
|
)])));
|
||||||
// No VM yet — exactly the ordering a world-entry VM meets: the resource is long since
|
// No VM yet — exactly the ordering a world-entry VM meets: the resource is long since
|
||||||
// loaded, and `PostStartup` has already come and gone.
|
// loaded, and `PostStartup` has already come and gone.
|
||||||
app.add_systems(PostStartup, feed_class_info);
|
app.add_systems(PostStartup, feed_class_info);
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ fn inspect_slot_view(
|
|||||||
.player_visible_item_enchant(slot0, u8::try_from(j).unwrap_or(0))
|
.player_visible_item_enchant(slot0, u8::try_from(j).unwrap_or(0))
|
||||||
.unwrap_or(0) as i32
|
.unwrap_or(0) as i32
|
||||||
}),
|
}),
|
||||||
random_properties_id: u32::from(roll),
|
random_properties_id: roll,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
..Default::default()
|
..Default::default()
|
||||||
|
|||||||
@@ -968,9 +968,8 @@ mod tests {
|
|||||||
};
|
};
|
||||||
use benilla_ui::script::EQUIPMENT_BAG;
|
use benilla_ui::script::EQUIPMENT_BAG;
|
||||||
|
|
||||||
/// Both ends of every rung of `GetKeyRingSize` (`ContainerFrame.lua:773`).
|
/// 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.
|
||||||
/// The wire's 16 positions (vmangos `KEYRING_SLOT_END` 97), not the level-gated count.
|
|
||||||
#[test]
|
#[test]
|
||||||
fn wire_pos_maps_the_keyring_onto_the_player_grid() {
|
fn wire_pos_maps_the_keyring_onto_the_player_grid() {
|
||||||
assert_eq!(wire_pos(KEYRING_CONTAINER, 1), Some((255, 81)));
|
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 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
|
// 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.
|
// 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);
|
loud.truncate(12);
|
||||||
let mut named: Vec<(String, u32)> = loud
|
let mut named: Vec<(String, u32)> = loud
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
|||||||
@@ -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
|
/// 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
|
/// load on both, the `/reload` shape; `PLAYER_ENTERING_WORLD` fires from [`crate::ui_unit`] once
|
||||||
/// the self descriptor lands.
|
/// 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)]
|
#[cfg(test)]
|
||||||
pub(crate) fn finish_ui_load(script: &mut UiScript) {
|
pub(crate) fn finish_ui_load(script: &mut UiScript) {
|
||||||
finish_ui_load_with(script, |_| {}, |_| {});
|
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -232,6 +232,7 @@ fn select_fog_lamps(
|
|||||||
selected
|
selected
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::type_complexity)] // one Bevy system's full input set
|
||||||
fn update_fog(
|
fn update_fog(
|
||||||
mut commands: Commands,
|
mut commands: Commands,
|
||||||
video: Res<VideoConfig>,
|
video: Res<VideoConfig>,
|
||||||
|
|||||||
@@ -761,6 +761,6 @@ mod tests {
|
|||||||
assert_eq!(VolLightOverride(None).tier(&video), 2);
|
assert_eq!(VolLightOverride(None).tier(&video), 2);
|
||||||
assert_eq!(VolLightOverride(Some(1)).tier(&video), 1);
|
assert_eq!(VolLightOverride(Some(1)).tier(&video), 1);
|
||||||
assert_eq!(STEPS[0], 0.0);
|
assert_eq!(STEPS[0], 0.0);
|
||||||
assert!(STEPS[2] > STEPS[1]);
|
const _: () = assert!(STEPS[2] > STEPS[1]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -435,6 +435,7 @@ fn occlusion_probe(eye: Vec3, unit_origin: Vec3) -> Option<(Dir3, f32)> {
|
|||||||
(reach > 0.0).then_some((dir, reach))
|
(reach > 0.0).then_some((dir, reach))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::type_complexity)] // one Bevy system's full input set
|
||||||
fn drive_vplates(
|
fn drive_vplates(
|
||||||
mode: Res<VPlateMode>,
|
mode: Res<VPlateMode>,
|
||||||
mut plates: ResMut<VPlates>,
|
mut plates: ResMut<VPlates>,
|
||||||
|
|||||||
@@ -156,6 +156,7 @@ impl WorldLane {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
#[allow(clippy::type_complexity)] // one Bevy system's full input set
|
||||||
fn update_world_shadows(
|
fn update_world_shadows(
|
||||||
video: Res<VideoConfig>,
|
video: Res<VideoConfig>,
|
||||||
time: Res<Time>,
|
time: Res<Time>,
|
||||||
@@ -271,9 +272,9 @@ fn update_world_shadows(
|
|||||||
lane.static_mesh = Some(mesh);
|
lane.static_mesh = Some(mesh);
|
||||||
lane.static_rebuilt_at = None;
|
lane.static_rebuilt_at = None;
|
||||||
}
|
}
|
||||||
let due = lane.static_rebuilt_at.map_or(true, |at| {
|
let due = lane
|
||||||
at.distance(frame.light_position) > STATIC_REBUILD_STEP
|
.static_rebuilt_at
|
||||||
});
|
.is_none_or(|at| at.distance(frame.light_position) > STATIC_REBUILD_STEP);
|
||||||
if !due {
|
if !due {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -400,9 +401,11 @@ mod streamed_refresh_tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn shadow_cache_refresh_runs_without_volumetric_fog_and_is_quiet_when_unchanged() {
|
fn shadow_cache_refresh_runs_without_volumetric_fog_and_is_quiet_when_unchanged() {
|
||||||
let mut app = App::new();
|
let mut app = App::new();
|
||||||
let mut video = VideoConfig::default();
|
let video = VideoConfig {
|
||||||
video.volumetric_fog = 0;
|
volumetric_fog: 0,
|
||||||
video.world_shadows = false;
|
world_shadows: false,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
app.insert_resource(video)
|
app.insert_resource(video)
|
||||||
.init_resource::<StaticGx>()
|
.init_resource::<StaticGx>()
|
||||||
.init_resource::<WorldLane>()
|
.init_resource::<WorldLane>()
|
||||||
|
|||||||
@@ -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.point_count(), MAX_LIVE_POINT_LIGHTS);
|
||||||
assert_eq!(b.points.len(), 2 * 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_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.
|
/// The glue scene's fog toggle flips only the enable and leaves the race's fog rows standing.
|
||||||
|
|||||||
@@ -509,9 +509,9 @@ pub fn daylight_target(light: &WowLighting) -> ([f32; 3], f32, f32) {
|
|||||||
let ndotl = l.y.max(0.0);
|
let ndotl = l.y.max(0.0);
|
||||||
let mut target = [0.0f32; 3];
|
let mut target = [0.0f32; 3];
|
||||||
let mut peak = 0.0f32;
|
let mut peak = 0.0f32;
|
||||||
for i in 0..3 {
|
for (i, channel) in target.iter_mut().enumerate() {
|
||||||
target[i] = (light.ambient[i] + light.diffuse[i] * ndotl).clamp(0.0, 1.0);
|
*channel = (light.ambient[i] + light.diffuse[i] * ndotl).clamp(0.0, 1.0);
|
||||||
peak = peak.max(target[i]);
|
peak = peak.max(*channel);
|
||||||
}
|
}
|
||||||
let hue = if peak > 1e-4 {
|
let hue = if peak > 1e-4 {
|
||||||
[target[0] / peak, target[1] / peak, target[2] / peak]
|
[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()) {
|
for (p, ci) in points.iter().zip(of_point.iter()) {
|
||||||
let root = find(&mut parent, *ci);
|
let root = find(&mut parent, *ci);
|
||||||
let e = boxes.entry(root).or_insert((*p, *p));
|
let e = boxes.entry(root).or_insert((*p, *p));
|
||||||
for a in 0..3 {
|
for ((lo, hi), v) in e.0.iter_mut().zip(e.1.iter_mut()).zip(p) {
|
||||||
e.0[a] = e.0[a].min(p[a]);
|
*lo = lo.min(*v);
|
||||||
e.1[a] = e.1[a].max(p[a]);
|
*hi = hi.max(*v);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let mut out: Vec<([f32; 3], [f32; 3])> = boxes.into_values().collect();
|
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
|
/// 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
|
/// [`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.
|
/// 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(
|
fn boundary_clusters(
|
||||||
groups: &[WmoGroupInfo],
|
groups: &[WmoGroupInfo],
|
||||||
batches: &[(u16, bool, &[[f32; 3]])],
|
batches: &[(u16, bool, &[[f32; 3]])],
|
||||||
@@ -955,8 +956,8 @@ fn boundary_clusters(
|
|||||||
slot.push(positions);
|
slot.push(positions);
|
||||||
let acc = &mut sum[usize::from(gi)];
|
let acc = &mut sum[usize::from(gi)];
|
||||||
for p in positions {
|
for p in positions {
|
||||||
for a in 0..3 {
|
for (axis, v) in acc.0.iter_mut().zip(p) {
|
||||||
acc.0[a] += f64::from(p[a]);
|
*axis += f64::from(*v);
|
||||||
}
|
}
|
||||||
acc.1 += 1;
|
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()) {
|
for (p, ci) in points.iter().zip(of_point.iter()) {
|
||||||
let root = find(&mut parent, *ci);
|
let root = find(&mut parent, *ci);
|
||||||
let e = boxes.entry(root).or_insert((*p, *p));
|
let e = boxes.entry(root).or_insert((*p, *p));
|
||||||
for a in 0..3 {
|
for ((lo, hi), v) in e.0.iter_mut().zip(e.1.iter_mut()).zip(p) {
|
||||||
e.0[a] = e.0[a].min(p[a]);
|
*lo = lo.min(*v);
|
||||||
e.1[a] = e.1[a].max(p[a]);
|
*hi = hi.max(*v);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let mut out: Vec<([f32; 3], [f32; 3])> = boxes.into_values().collect();
|
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
|
/// 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
|
/// 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.
|
/// 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(
|
pub fn update_daylight_fixtures(
|
||||||
mut commands: Commands,
|
mut commands: Commands,
|
||||||
light: Res<WowLighting>,
|
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`
|
/// 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
|
/// 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.
|
/// 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(
|
pub fn update_bleed_fixtures(
|
||||||
mut commands: Commands,
|
mut commands: Commands,
|
||||||
knobs: Res<DynamicInteriors>,
|
knobs: Res<DynamicInteriors>,
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
use super::*;
|
use super::*;
|
||||||
use benilla_formats::{open_chain, parse_wmo_portals, parse_wmo_root, wmo_group_header};
|
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 {
|
struct Loaded {
|
||||||
groups: Vec<WmoGroupInfo>,
|
groups: Vec<WmoGroupInfo>,
|
||||||
names: Vec<String>,
|
names: Vec<String>,
|
||||||
@@ -41,7 +42,9 @@ fn group_names(bytes: &[u8]) -> Vec<String> {
|
|||||||
let (Some(mogn), Some(mogi)) = (mogn, mogi) else {
|
let (Some(mogn), Some(mogi)) = (mogn, mogi) else {
|
||||||
return Vec::new();
|
return Vec::new();
|
||||||
};
|
};
|
||||||
mogi.chunks_exact(32)
|
mogi.as_chunks::<32>()
|
||||||
|
.0
|
||||||
|
.iter()
|
||||||
.map(|r| {
|
.map(|r| {
|
||||||
let off = i32::from_le_bytes(r[28..32].try_into().unwrap());
|
let off = i32::from_le_bytes(r[28..32].try_into().unwrap());
|
||||||
usize::try_from(off)
|
usize::try_from(off)
|
||||||
|
|||||||
@@ -281,9 +281,11 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn modern_marks_the_scene_end_and_fades_the_sun_at_night() {
|
fn modern_marks_the_scene_end_and_fades_the_sun_at_night() {
|
||||||
let mut l = WowLighting::default();
|
let mut l = WowLighting {
|
||||||
l.fog_end = 444.0;
|
fog_end: 444.0,
|
||||||
l.celestial_dir = Vec3::new(0.0, 0.8, 0.6);
|
celestial_dir: Vec3::new(0.0, 0.8, 0.6),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
let mut f = MonkeyFrame::default();
|
let mut f = MonkeyFrame::default();
|
||||||
fill_frame(&mut f, FogModel::Modern, &l, &derived_band(&l));
|
fill_frame(&mut f, FogModel::Modern, &l, &derived_band(&l));
|
||||||
assert_eq!(f.fog_scene_end, 444.0);
|
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.
|
/// blob or the room-claim table changes.
|
||||||
pub(super) const MAX_LIVE_POINT_LIGHTS: usize = MAX_POINT_LIGHTS - 1;
|
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
|
/// 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
|
/// 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
|
/// 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.
|
/// hall, guest rooms and basement claim NOTHING.
|
||||||
/// * **NSabbey** — 42 fixtures over 14 groups, but `Main Hall`, `LftWng`, `RtWng`, `Library`,
|
/// * **NSabbey** — 42 fixtures over 14 groups, but `Main Hall`, `LftWng`, `RtWng`, `Library`,
|
||||||
/// `Library2`, `Library Wing` and `Stairs2` are named by no MOLR either.
|
/// `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
|
/// 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
|
/// 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),
|
/// 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.
|
/// 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`):
|
/// * `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)`.
|
/// `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
|
/// **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
|
/// 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
|
/// `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) => {
|
Some(g) => {
|
||||||
// MONKEY (soft portal claims): the entry weight rides the spare high bits.
|
// MONKEY (soft portal claims): the entry weight rides the spare high bits.
|
||||||
let entry = self.fades[i].entry.clamp(0.0, 1.0);
|
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 {
|
| if *g & LIT_ROOM_EXT_DENY == 0 {
|
||||||
CLAIM_EXT_OK
|
CLAIM_EXT_OK
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -152,10 +152,10 @@ fn stitch_tag(
|
|||||||
tx: f32,
|
tx: f32,
|
||||||
ty: f32,
|
ty: f32,
|
||||||
) -> 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 };
|
let y = if fy == 0 { 0 } else { rows - 1 };
|
||||||
(y * cols + sx, y * cols + sx + 1, tx)
|
(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 };
|
let x = if fx == 0 { 0 } else { cols - 1 };
|
||||||
(sy * cols + x, (sy + 1) * cols + x, ty)
|
(sy * cols + x, (sy + 1) * cols + x, ty)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -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
|
/// `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.
|
/// 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;
|
const TWO_PI: f32 = 6.2831853;
|
||||||
|
|
||||||
/// The ADT **ocean**'s wave energy — `liquid/surface.rs` packs `water.mode.y = 1.0` for
|
/// 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]"
|
"enhanced_water.wgsl WATER_WAVES[0..2]"
|
||||||
);
|
);
|
||||||
assert_eq!(GRAVITY_YD, 10.72, "enhanced_water.wgsl gravity");
|
assert_eq!(GRAVITY_YD, 10.72, "enhanced_water.wgsl gravity");
|
||||||
assert_eq!(
|
#[allow(clippy::approx_constant)] // likewise the mirror's own literal
|
||||||
TWO_PI, 6.2831853,
|
{
|
||||||
"enhanced_water.wgsl k = 6.2831853/wavelength"
|
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!(WATER_WIND_DIR, 0.35, "enhanced_water.wgsl WATER_WIND_DIR");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
GERSTNER_CHOP, 2.4,
|
GERSTNER_CHOP, 2.4,
|
||||||
|
|||||||
@@ -420,7 +420,7 @@ fn prepare_static_gx(
|
|||||||
// (a bare static_gx harness has no lighting) — absent reads as the shipped Gaussian.
|
// (a bare static_gx harness has no lighting) — absent reads as the shipped Gaussian.
|
||||||
shadow_filter: Option<Res<crate::lighting::ShadowFilterGaussian>>,
|
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 _t = super::gx_perf_guard(3);
|
||||||
let Some((view, msaa, _, _)) = views.iter().next() else {
|
let Some((view, msaa, _, _)) = views.iter().next() else {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ impl StaticGx {
|
|||||||
.map(|p| t.transform_point(wow_to_bevy(*p)).to_array()),
|
.map(|p| t.transform_point(wow_to_bevy(*p)).to_array()),
|
||||||
);
|
);
|
||||||
let added = positions.len() as u32 - base;
|
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) {
|
if tri.iter().all(|i| *i < added) {
|
||||||
indices.extend(tri.iter().map(|i| base + *i));
|
indices.extend(tri.iter().map(|i| base + *i));
|
||||||
}
|
}
|
||||||
@@ -144,7 +144,7 @@ impl StaticGx {
|
|||||||
);
|
);
|
||||||
bucket.uvs.extend_from_slice(&geometry.uvs);
|
bucket.uvs.extend_from_slice(&geometry.uvs);
|
||||||
let added = bucket.positions.len() as u32 - base;
|
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) {
|
if tri.iter().all(|i| *i < added) {
|
||||||
bucket.indices.extend(tri.iter().map(|i| base + *i));
|
bucket.indices.extend(tri.iter().map(|i| base + *i));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1122,7 +1122,7 @@ impl super::StaticGx {
|
|||||||
.to_array()
|
.to_array()
|
||||||
}));
|
}));
|
||||||
let added = positions.len() as u32 - base;
|
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) {
|
if tri.iter().all(|i| *i < added) {
|
||||||
indices.extend(tri.iter().map(|i| base + *i));
|
indices.extend(tri.iter().map(|i| base + *i));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ fn ride(state: &BobState) -> (f32, Quat) {
|
|||||||
// Clamp the SLOPE, not the resulting quaternion: the wave normal is `(-∂x, 1, -∂z)`
|
// 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 θ.
|
// (`enhanced_water.wgsl`), so a gradient of length `tan(θ)` is a lean of exactly θ.
|
||||||
let len = g.length();
|
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)
|
g * (MAX_TILT_TAN / len)
|
||||||
} else {
|
} else {
|
||||||
g
|
g
|
||||||
|
|||||||
@@ -353,13 +353,15 @@ fn drive_unit(
|
|||||||
// hanging in deep water is the state the reference's driver renders almost invisible, and it
|
// 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
|
// is exactly the state the owner's murloc will be in. Classic touches nothing here — not the
|
||||||
// timer, not the RNG stream.
|
// timer, not the RNG stream.
|
||||||
if look.enhanced && look.swimming && !matches!(state, WadeState::Translating { .. }) {
|
if look.enhanced
|
||||||
if now >= foam_state.tread_ready {
|
&& look.swimming
|
||||||
foam_state.tread_ready = now + TREAD_INTERVAL;
|
&& !matches!(state, WadeState::Translating { .. })
|
||||||
if let Some(p) = tread_params(look, scale, &mut foam_state.rng) {
|
&& now >= foam_state.tread_ready
|
||||||
let heading = rand01(&mut foam_state.rng) * std::f32::consts::TAU;
|
{
|
||||||
emit_record(alloc, index, water, [wow[0], wow[1]], &p, heading, now);
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -175,8 +175,10 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn the_ripple_clock_wraps() {
|
fn the_ripple_clock_wraps() {
|
||||||
let mut w = Wetness::default();
|
let mut w = Wetness {
|
||||||
w.ripple_time_s = RIPPLE_WRAP_S - 0.05;
|
ripple_time_s: RIPPLE_WRAP_S - 0.05,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
w.step(0.0, 0.1);
|
w.step(0.0, 0.1);
|
||||||
assert!(w.ripple_time_s < 0.1);
|
assert!(w.ripple_time_s < 0.1);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user