Files
benilla-octo/crates/benilla-world/src/surface.rs
T
SamandClaude Fable 5 8eea16157c benilla-world: the engine becomes its own crate
- the world engine splits from the game into a benilla-world crate — 112 files, and the wall is ratcheted to zero names of the game inside the engine
- the engine's public doorway is counted and gated at 32 items, and the render materials, shaders and asset cache move down to benilla-assets
- the game's files leave the engine's asset root — game:// is its own space — and a spike binary boots the engine with no game attached
- weather rides along: snowfall emission is denominated in the reference's frame cap, and a long teleport retires the stranded precipitation field instead of leaving it behind

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-11 18:13:44 +07:00

111 lines
5.6 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! **What you are standing on** — the `TerrainType` id under a unit's feet, for every consumer of
//! it (decision 1161).
//!
//! The reference keeps this as one cached dword per unit, `CGUnit+0xc60`: an environment-node
//! down-ray (`0x6a8a20`) resolves it on movement, and four readers consume that single value — the
//! `$FSD` footstep sound (`0x62341d`), the footprint decal (`0x5fc06e`), the footstep spray
//! (`0x5fc20f`) and `0x623749`. They cannot disagree about the ground, and neither can ours: this
//! module is the one place the question is answered.
//!
//! **Two legs, arbitrated by nearest hit.** The client races a terrain probe (`0x69c320`) against a
//! WMO probe (`0x6a8840`) over the building's COLLISION faces; whichever surface is nearer under
//! the foot supplies the type. That arbitration is already ours — it is the same faces-only,
//! terrain-raced down-ray that publishes [`UnitWmoRoom`], so a room claim *is* "the building won
//! this column".
//!
//! - **WMO leg.** The claim names a group; [`surface_terrain_sample`] re-casts the column over that
//! group's RENDER faces and reads the hit face's `MOPY.material_id → MOMT+0x20` — a
//! `TerrainType.dbc` id carried by the surface itself, with no `GroundEffectTexture` hop.
//! - **ADT leg.** No claim: the MCNK dominant layer → `GroundEffectTexture` → `TerrainType`, the
//! chain that was already here.
//!
//! `None` is the client's `−1` sentinel and means **silent, and no print** — not "try the other
//! leg". A building that won the column but whose group has no render face under the probe is
//! silent; falling back to the ground beneath the floor is exactly the bug this module exists to
//! kill (B236's sequel: snow crunching inside the Kharanos inn, and snow prints on its floorboards,
//! because the ADT leg answered indoors).
//!
//! **Known gap — exterior groups.** Both the claim and the render-face set are built for INTERIOR
//! groups only, so a WMO surface authored on an *exterior* group (bridges and docks, `Wood`) still
//! falls to the ADT leg. Wrong in the same direction as before this module, never newly wrong; the
//! fix is widening the claim, not special-casing here.
use bevy::prelude::*;
use benilla_assets::coords::bevy_to_wow;
use benilla_assets::{AdtTile, WmoModel};
use benilla_formats::FootstepCatalog;
use crate::terrain_stream::{
area_id_under, ground_effect_under, terrain_height_under, TerrainStreamer,
};
use crate::wmo_portal::{
indoors_at, surface_terrain_sample, UnitWmoRoom, WmoPortalInstance, POSITION_PROBE_LIFT,
};
/// The world state the surface question needs, as one `SystemParam` — the placements it rays, the
/// terrain it races them against. Threaded as a query rather than read into a resource for the
/// same reason `liquid::RoomPlacements` is: the answer must not outlive the placement.
#[derive(bevy::ecs::system::SystemParam)]
pub(crate) struct SurfaceUnderfoot<'w, 's> {
wmos: Res<'w, Assets<WmoModel>>,
instances: Query<'w, 's, &'static WmoPortalInstance>,
streamer: Res<'w, TerrainStreamer>,
adt_tiles: Res<'w, Assets<AdtTile>>,
}
impl SurfaceUnderfoot<'_, '_> {
/// The `TerrainType` id under `pos` (Bevy space) for a unit holding `room`, or `None` for the
/// client's `−1` — silent, no print. See the module docs for why `None` never retries.
pub(crate) fn terrain_type(
&self,
cat: &FootstepCatalog,
room: Option<&UnitWmoRoom>,
pos: Vec3,
) -> Option<u32> {
match room.and_then(UnitWmoRoom::room) {
Some(room) => {
let inst = self.instances.get(room.instance).ok()?;
let model = self.wmos.get(&inst.handle)?;
// The SAME origin lift the claim's own ray used: feet rest on the floor plane, and
// a coplanar `z <= probe.z` test must not lose the floor to a rounding hair. Using
// a different origin here could resolve a different face than the one that won.
let probe_world = pos + Vec3::Y * POSITION_PROBE_LIFT;
let local = inst
.world_from_local
.inverse()
.transform_point3(probe_world);
surface_terrain_sample(model, usize::from(room.group), bevy_to_wow(local))
}
None => ground_effect_under(&self.streamer, &self.adt_tiles, pos)
.and_then(|e| cat.terrain_of(e)),
}
}
/// The MCNK `areaId` under `pos` (Bevy space) — the *outdoor* leg of "where is this", with no
/// WMO-interior claim. The player's own area is [`crate::terrain_stream::CurrentArea`], which
/// includes that claim; this is what every other body gets.
pub fn area_id_under(&self, pos: Vec3) -> Option<u32> {
area_id_under(&self.streamer, &self.adt_tiles, pos)
}
/// The terrain height under `pos` (Bevy space), from the MCNK heightfield — `None` off the
/// streamed set. **Not** the physics trimesh: the two are allowed to differ, which is why
/// tracing a body through the world is a separate question (1164 item 11).
pub fn terrain_height_under(&self, pos: Vec3) -> Option<f32> {
terrain_height_under(&self.streamer, &self.adt_tiles, pos)
}
/// Is `feet_world` inside a building? The faces-only down-ray, raced against the terrain —
/// standing on the grass above a mine's tunnels is not standing in the mine.
pub fn indoors_at(&self, feet_world: Vec3) -> bool {
indoors_at(
&self.wmos,
self.instances,
&self.streamer,
&self.adt_tiles,
feet_world,
)
}
}