mirror of
https://github.com/SK8-ENGINE/skate-3-rust-engine.git
synced 2026-10-26 19:47:44 +00:00
Water rendering: extract the ocean animation table from default.xex
Water and ocean shaders need assets/private/ocean-pca.json, which setup never created, so 52 water/ocean materials rendered static (or black). - skate_data::xex: XEX2 unpacker (retail AES file key + CBC payload, LZX or basic decompression), no new dependencies - skate_data::ocean_pca: locates cPCAWaterAnimationData's table in any build by its code addressing (disc build: 0x82FC3978; TU3: 0x830118D8) - skate3rust --extract-ocean-pca <default.xex> <out.json> - Setup environment group runs it (optional content); refreshes that group In-play check of the rendered water still pending. See docs/hails-additions/09-water.md.
This commit is contained in:
13 files changed
+1086
-4
No files matched your search
@@ -0,0 +1,201 @@
|
||||
# Water (investigation in progress)
|
||||
|
||||
Branch: `gameplay/water`. Status: **water bail confirmed in play; water animation extraction added, awaiting in-play check.**
|
||||
|
||||
## Problem
|
||||
|
||||
Water does nothing in play: the skater rides over it or falls through it as if it
|
||||
were ordinary ground or empty space. The retail game has water-specific behaviour
|
||||
(the `IsInWater` motion-graph condition, the water ragdoll profile, and the
|
||||
"special surface" path in the wipeout state).
|
||||
|
||||
## Evidence so far
|
||||
|
||||
### 1. Water is a retail collision surface type, and it is in the converted maps
|
||||
|
||||
Retail collision units carry a 16-bit surface ID. The surface **type** is
|
||||
`(surface >> 7) & 31`; the engine already treats type 12 as water:
|
||||
|
||||
- `crates/skate-core/src/physics/board_ground.rs` — the board sets collision flag
|
||||
bit 25 (`Body872`) and records the contact height (`Body864`,
|
||||
`surface_twelve_height`) when any part touches type 12.
|
||||
- `crates/skate-game/src/physics/wipeout_states/prediction.rs` — the wipeout
|
||||
trajectory query tests `surface & 0xF80 == 0x600` (type 12).
|
||||
|
||||
New tool: `crates/skate-data/examples/water_surfaces.rs` lists the surface types
|
||||
in each map's embedded RWCM collision and details the type-12 triangles.
|
||||
|
||||
```
|
||||
cargo run --locked --release -p skate-data --example water_surfaces -- data/installations/<id>/maps/*.skate
|
||||
```
|
||||
|
||||
Results (installation `c82bd63f…`):
|
||||
|
||||
| Map | Water triangles | Surface ID | Heights | Notes |
|
||||
|---|---|---|---|---|
|
||||
| DownTown | 355 (330 flat, all one-sided) | 1591 only | 0.4 m … 48.3 m, many levels | fountains/pools spread over 26 tiles |
|
||||
| University | 322 (all flat, all one-sided) | 1591 only | 296 at 217.9 m, rest 67.9–71.0 m | reservoir plus smaller pools |
|
||||
| Industrial, all parks | 0 | — | — | — |
|
||||
|
||||
So water collision exists only in DownTown and University. Every collision
|
||||
stream is already converted (cities use their `cSim_*_high` tiles; parks use
|
||||
`cSim_Global`), and the volume filter from change 5 cannot drop water, because
|
||||
water triangles carry a surface ID.
|
||||
|
||||
Render materials agree: `water.alpha` / `water.flowingalpha` appear only in
|
||||
DownTown and `water.default` / `water.flowing` only in University. Industrial's
|
||||
harbour/sea is only the backdrop `ocean.reflection` mesh
|
||||
(`assets/private/native-backdrops/Industrial.skate`), with no water collision
|
||||
under it; the harbour bed is ordinary collision at about y = -3 (see change 4).
|
||||
|
||||
### 2. The skater never learns it is in water (the missing link)
|
||||
|
||||
The skater-side water signal comes from the collision output:
|
||||
|
||||
- `CollisionOutputFields.flag_3481` → `Processed.flags_2488` bit 30
|
||||
(`0x4000_0000`), and
|
||||
- `CollisionOutputFields.scalar_28` → `Processed.collision_scalar_2924`
|
||||
(water height)
|
||||
|
||||
(`crates/skate-core/src/player/input_phase/publication.rs:253-254`). The wipeout
|
||||
state reads both (`wipeout_states/lifecycle.rs:81-82`, `update.rs:24-26`) to
|
||||
enter the "special surface" path (ragdoll profile 10, `below_surface`, the
|
||||
`IsInWater` motion-graph condition).
|
||||
|
||||
**Nothing in the codebase ever writes `flag_3481` or `scalar_28`.** They stay
|
||||
at their defaults (0), so the water path can never start, even on DownTown and
|
||||
University where the data exists.
|
||||
|
||||
The board already computes the matching values (`collision_flags` bit 25 and
|
||||
`surface_twelve_height`), and the board manager (`offboard/board_manager/runtime.rs`
|
||||
`surface()`) and the player state (`player_state/publication.rs`) already read bit 25.
|
||||
The likely retail behaviour is that the physics output fill copies the board's
|
||||
`Body872` bit 25 → `Collision+3481` and `Body864` → `Collision+28`. **This
|
||||
is a hypothesis; the retail write site has not been found yet.**
|
||||
|
||||
### 3. The skater's own contacts also classify water
|
||||
|
||||
`SkeletonCollision` (82BD4A30, `crates/skate-core/src/physics/skeleton_body/collision_update.rs`)
|
||||
sets `flags.material_12` and `material_12_height` when any body part touches
|
||||
type 12. Its flags sit at native 4079/4080/4081 (materials 10/11/12); the
|
||||
collision output's 3479/3480/3481 use the same layout 600 bytes lower, and
|
||||
3479/3480 are already the material-10/11 results. This makes the skater
|
||||
body the most likely retail source of Collision+3481/+28.
|
||||
|
||||
### 4. Industrial's sea
|
||||
|
||||
The user remembers that falling into Industrial's sea respawned the player in
|
||||
retail, and suggests the sea may simply be out of reach in retail. Industrial's
|
||||
harbour floor uses ordinary surfaces (concrete and so on) and its sea has no
|
||||
collision of any kind on the disc (the proxy archives are visual only), so no
|
||||
data-driven water detection is possible there. Not handled by this change.
|
||||
|
||||
## Change
|
||||
|
||||
`PlayerInputRuntime::publish_water` (`crates/skate-game/src/physics/player_input/mod.rs`),
|
||||
called from `frame.rs` right after `publish_board` (after the packet reset and
|
||||
the skeleton contact feedback for the frame):
|
||||
|
||||
- skater body touching type 12 → `flag_3481 = 1`, `scalar_28 = material_12_height`;
|
||||
- otherwise board touching type 12 (`collision_flags` bit 25) → `flag_3481 = 1`,
|
||||
`scalar_28 = surface_twelve_height`;
|
||||
- otherwise both stay at their reset values (0).
|
||||
|
||||
Project choice: retail's writer is unconfirmed; the skater body wins over the
|
||||
board because the wipeout water path simulates the body. Water triangles stay
|
||||
solid one-sided collision, as before.
|
||||
|
||||
### Water bail (added after the first in-play test)
|
||||
|
||||
First in-play test (University fountain basin): the skater landed on the water
|
||||
and rode on it; no bail. Expected: the published flag only feeds the wipeout
|
||||
state, and nothing starts a bail on water. I searched every recovered bail check
|
||||
(`skate-core/src/player/wipeout/`, the state selector, all motion-graph conditions
|
||||
in the stock state XML) and found no water/type-12 trigger. The retail
|
||||
trigger is in code that hasn't been recovered, and no decrypted executable is
|
||||
available.
|
||||
|
||||
`physics/wipeout.rs` `check_after_physics` now requests a bail (request slot 33,
|
||||
`WATER_BAIL_REASON`, otherwise unused and not a runout reason) when the board
|
||||
(`collision_flags` bit 25) or the skater body (`flags.material_12`) touches
|
||||
water, unless the skater is already in WipeoutGround, Teleporting or Sleeping.
|
||||
It then takes the ordinary path: state flag 65 → `PhysicsWantsWipeOut` →
|
||||
WipeoutGround, where `Collision+3481` starts the water ragdoll and the
|
||||
respawn timer. Logged as `WATER_BAIL tick=… state=…`. **Project choice**, not
|
||||
recovered retail behaviour.
|
||||
|
||||
Not changed: water stays solid. The water buoyancy code
|
||||
(`wipeout_state::body::special_surface`, which pushes parts up when below
|
||||
height + 0.1) suggests that in retail the body sinks slightly into water, so
|
||||
water may not be solid for the body in retail. Left alone until play shows
|
||||
whether that matters.
|
||||
|
||||
### Water rendering: extract the ocean animation table from default.xex
|
||||
|
||||
Water (family 33) and ocean (family 31) shaders need `assets/private/ocean-pca.json`,
|
||||
which nothing in setup created (see open question 5), so 52 water/ocean materials
|
||||
fell back to static shading (black for `ocean.default`, which has no diffuse).
|
||||
|
||||
- `crates/skate-data/src/xex/`: XEX2 unpacker (retail AES-128 file-key and CBC
|
||||
payload decryption, "normal" LZX or "basic" decompression) producing the
|
||||
mapped base image. No new dependencies; AES is checked against FIPS-197 and
|
||||
NIST SP 800-38A vectors. The owned disc `default.xex` (normal encryption,
|
||||
LZX) unpacks to an 18,022,400-byte image at 0x82000000 in about 0.2 s
|
||||
(sha256 `ce1e3ae5…`, not the TU3 image `ocean_pca.py` expects).
|
||||
- `crates/skate-data/src/ocean_pca.rs`: locates cPCAWaterAnimationData's table
|
||||
the way the code addresses it (`lis` + D-form pairs forming two addresses
|
||||
0x168 apart within 32 instructions, data plausibility check), so it works for
|
||||
any build. The disc build has exactly one match: means at 0x82FC3978, weights
|
||||
at 0x82FC3AE0 (TU3: 0x830118D8 / 0x83011A40); frame 0 mean is
|
||||
(127.55, 251.83, 127.54), an "up" normal. JSON layout matches `ocean_pca.py`.
|
||||
- `skate3rust --extract-ocean-pca <default.xex> <out.json>` (`main.rs`, runs
|
||||
before any game initialization; prints `OCEAN_PCA_READY`).
|
||||
- Setup: `ocean_pca.convert` runs it in the `environment` group
|
||||
(`asset_exports.environment`, new `game_exe` argument; `versions.py` adds
|
||||
`ocean_pca.py` to the group). Failure is optional content, like other
|
||||
environment parts. Changing the environment recipe refreshes that group once.
|
||||
|
||||
## Verification
|
||||
|
||||
- Unit test `physics::player_input::tests::water_contact_prefers_the_skater_body_height`.
|
||||
- `cargo test --locked -p skate-game --release --bin skate3rust -- water_contact`: passes.
|
||||
- Release build staged into `bin\`; `--test-world --check-assets` → `SKATE_ASSETS_READY`.
|
||||
- In play, first build: rode on top of the water, no bail (led to the water bail above).
|
||||
- In play, second build: bails on University fountain basin (F2), University reservoir (F3) and DownTown fountain (F4). Log shows `WATER_BAIL ... state=KnownAir` on each drop.
|
||||
- Asset-backed wipeout tests (`--ignored wipeout`): 4 pass; `marker_reply_restores_on_foot`
|
||||
fails identically with these changes stashed (pre-existing).
|
||||
|
||||
## Open questions
|
||||
|
||||
1. Where does retail write `Collision+3481` / `+28`? The wiring above is the
|
||||
best match to the evidence; it is not confirmed against retail.
|
||||
2. Water triangles are solid one-sided surfaces in our collision world. Does the
|
||||
retail board/body pass through type 12, or rest on it? (In play: does the
|
||||
skater stand on the University reservoir?)
|
||||
3. Industrial's sea: retail respawned the player (user's memory), possibly
|
||||
out of reach in retail. No collision data exists for it; see section 4.
|
||||
5. Water rendering (user: static texture on the fountains, black on the
|
||||
University reservoir). Cause found: the log says "52 of 8546 world
|
||||
materials use an unsupported shader family and render as family 1". Water
|
||||
(family 33) and ocean (family 31) shaders require `assets/private/ocean-pca.json`
|
||||
(`MaterialTuning::supported`, `read_pca`). Nothing in setup (here or on
|
||||
upstream `60efdef`) creates that file; `tools/asset_pipeline/ocean_pca.py`
|
||||
needs a decrypted, decompressed TU3 executable image (hard-coded sha256 and
|
||||
table addresses 0x830118D8 / 0x83011A40). The disc `default.xex` is XEX2 with
|
||||
normal encryption and LZX compression, and setup never unpacks it. Fallback
|
||||
family 1 shows the static diffuse; `ocean.default` has no diffuse, hence black.
|
||||
This affects upstream users equally.
|
||||
4. What is surface type 13 (DownTown 88, University 17,790 triangles)? It may be
|
||||
related (shallow water or a splash surface) or something unrelated, such as grass.
|
||||
|
||||
## Files
|
||||
|
||||
- `crates/skate-data/examples/water_surfaces.rs` (new, diagnostic only; also prints water/ocean render mesh heights).
|
||||
- `crates/skate-game/src/physics/player_input/mod.rs` (`publish_water`, unit test).
|
||||
- `crates/skate-game/src/physics/frame.rs` (call site).
|
||||
- `crates/skate-game/src/physics/wipeout.rs` (water bail request).
|
||||
- `mods/water-test-teleport/` (dev-only test mod, F2/F3/F4 teleports; not committed, not for upstream).
|
||||
- `crates/skate-data/src/xex/{mod,aes,lzx}.rs`, `crates/skate-data/src/ocean_pca.rs`, `crates/skate-data/src/lib.rs`.
|
||||
- `crates/skate-data/examples/xex_unpack.rs` (diagnostic: unpack + locate table).
|
||||
- `crates/skate-game/src/main.rs` (`--extract-ocean-pca`).
|
||||
- `tools/asset_pipeline/ocean_pca.py` (`convert`), `asset_exports.py`, `install.py`, `versions.py`.
|
||||
@@ -14,6 +14,7 @@ Each document stands alone. File paths are relative to the repository root.
|
||||
| 3 | [SDL3 gamepad input](03-sdl3-gamepad-input.md) | Game (Rust), build scripts, vendored `sdl3-sys` | Done, verified in play with an Xbox Elite Series 2 |
|
||||
| 4 | [Authored map spawns](04-map-spawns.md) | Asset pipeline (Python) | Done, all 10 maps confirmed in play |
|
||||
| 5 | [Invisible collision volumes](05-collision-volumes.md) | Game data loader (Rust) | Done, all four affected maps confirmed in play |
|
||||
| 9 | [Water](09-water.md) | Game physics (Rust), diagnostics | Water flag wired up (DownTown, University); awaiting in-play test |
|
||||
|
||||
## Environment used for verification
|
||||
|
||||
|
||||
Reference in new issue
Block a user