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::type_complexity)] // one Bevy system's full input set
|
||||
fn collect_character_shadows(
|
||||
video: Res<VideoConfig>,
|
||||
time: Res<Time>,
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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"
|
||||
);
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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>()
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user