forked from tsuki/openroller
Initial public source release
Split reusable rendering and format support into vectorail-core and vectorail-gc.
This commit is contained in:
@@ -0,0 +1,537 @@
|
||||
# game471.exe: Notes (Static Analysis)
|
||||
|
||||
Goal: understand file formats and runtime behavior for a clean-room reimplementation.
|
||||
|
||||
This is based on static inspection (strings + disassembly). No patching, no runtime hooking.
|
||||
|
||||
## Quick Map
|
||||
|
||||
- Stage file path templates referenced in code:
|
||||
- `data/stage/%s.dat`
|
||||
- `data/stage/%s_ext.dat`
|
||||
- `data/stage/%s_clip.dat`
|
||||
|
||||
- The game uses a background file reader (a small work queue + thread) that:
|
||||
1. Opens a file path stored in a request object.
|
||||
2. Reads the file into a buffer in 64 KiB chunks.
|
||||
3. Calls a callback function provided when the queue was created.
|
||||
|
||||
## Background File Reader
|
||||
|
||||
### Worker thread proc
|
||||
|
||||
- `0x004ca320` is used as the thread entry function (passed as a function pointer).
|
||||
- It receives a small argument struct, extracts two pointers from it, then calls `0x004ca070`.
|
||||
|
||||
### File read loop
|
||||
|
||||
- `0x004ca070` implements:
|
||||
- open file (`CreateFileA`), then `ReadFile` in a loop
|
||||
- destination buffer is `request->buf` at offset `+0x204`
|
||||
- expected size is `request->size` at offset `+0x208`
|
||||
|
||||
At several points it calls a callback:
|
||||
|
||||
- `queue->callback_fn` stored at offset `queue + 0x120`
|
||||
- `queue->callback_ctx` stored at offset `queue + 0x124` (passed as last argument)
|
||||
|
||||
### Queue initialization
|
||||
|
||||
- `0x004ca5f0` looks like the queue constructor/initializer.
|
||||
- It stores its arguments into `this+0x120` and `this+0x124`.
|
||||
|
||||
So, to find the actual parsing logic for a specific file type, you typically follow the callback function pointer passed into the queue creation.
|
||||
|
||||
## Stage Loader Entry
|
||||
|
||||
There is code that formats the stage file path strings and attempts to open:
|
||||
|
||||
- `data/stage/<name>.dat`
|
||||
- `data/stage/<name>_ext.dat`
|
||||
- `data/stage/<name>_clip.dat`
|
||||
|
||||
This is a good anchor when looking for where the game requests reads for stage containers.
|
||||
|
||||
Static addresses in the 4.71 executable:
|
||||
|
||||
- `0x0063ea70`: formats all three stage paths and requests each resource.
|
||||
- `0x00427730`: resource request wrapper.
|
||||
- `0x004279d0`: creates the 0x4c-byte resource object (Ghidra mislabels it as an MFC `CreateObject`).
|
||||
- `0x0063ecc0`: completion/cleanup callback which unwraps three resources before passing the result onward.
|
||||
|
||||
## Main stage header
|
||||
|
||||
The file is big-endian. The initial words are not all section offsets. Confirmed section slots are:
|
||||
|
||||
| Index | Meaning |
|
||||
| ---: | --- |
|
||||
| 0 | stage config |
|
||||
| 1 | track draw distances |
|
||||
| 2 | track points |
|
||||
| 3 | notes |
|
||||
| 4 | camera |
|
||||
| 5 | particles |
|
||||
| 6 | visualizer |
|
||||
| 7 | unknown section |
|
||||
| 8 | first color table |
|
||||
| 9 | objects |
|
||||
| 10 | scalar/unknown; often `0x30`, not an offset |
|
||||
| 11 | second color table |
|
||||
| 12 | scalar/unknown; can accidentally look like an in-file offset |
|
||||
|
||||
This matters because treating every plausible header word as an offset can split the notes or camera section at a false boundary.
|
||||
|
||||
Older 11-word stage headers instead store the second color-table offset in
|
||||
slot 10. The scalar/offset/scalar arrangement in slots 10..12 belongs to the
|
||||
newer 13-word format used by the 4.71-era charts.
|
||||
|
||||
## Note array: confirmed wire layout
|
||||
|
||||
The note section starts at header word 3 and ends exactly at header word 4. Its layout is:
|
||||
|
||||
```text
|
||||
u32be name_count // observed: 0 in all 2,968 dumped main charts
|
||||
repeat name_count times:
|
||||
string8 name // u8 length followed by bytes
|
||||
u32be count
|
||||
repeat count times:
|
||||
u32be time_ms
|
||||
u8 raw_type
|
||||
u8 type_override
|
||||
s16be params16[9]
|
||||
u8 flag24
|
||||
f32be params25[3]
|
||||
u8 flag37
|
||||
u8 flag38
|
||||
f32be params39[4]
|
||||
u32be params55[3]
|
||||
f32be param67
|
||||
u32be param71
|
||||
f32be params75[5]
|
||||
u32be param95
|
||||
```
|
||||
|
||||
Thus each note record is exactly 99 bytes and the invariant is:
|
||||
|
||||
```text
|
||||
camera_offset - notes_offset == 8 + encoded_names_size + count * 99
|
||||
```
|
||||
|
||||
The invariant holds on tested charts of very different sizes. For example,
|
||||
`ac_10pt8tion_hard.dat` has 669 records from 6162 through 125568 ms.
|
||||
Its raw-type histogram is `01:366, 02:231, 03:1, 04:3, 05:3, 09:63, 0f:2`.
|
||||
|
||||
Observed raw types over the parsed corpus are `00, 01, 02, 03, 04, 05, 06, 09, 0a, 0f`.
|
||||
The payload field meanings remain unnamed until backed by code references or
|
||||
controlled runtime observations.
|
||||
|
||||
The exact read order above comes from the game's stage constructor `0x005ed4c0`
|
||||
and its note-section parser `0x005ea800`. The latter allocates a 0x11c-byte
|
||||
runtime object for every compact 99-byte wire record. It also proves one behavior:
|
||||
if `type_override` is non-zero, the runtime replaces `raw_type` with type 1.
|
||||
|
||||
The post-load pass at `0x005ebaa0` additionally proves that runtime types 11,
|
||||
12, 13 and 14 are compatibility aliases: 11 becomes 10, 12/14 become 9, and
|
||||
13 becomes 4. Other game modes can deliberately simplify types (for example
|
||||
2 to 1, and 4/5/10/15 to 3), so a clean player should preserve both the raw
|
||||
wire type and the effective runtime type.
|
||||
|
||||
Full-dump validation with `tools/scan_gc_notes.py GC/data/stage` covers 2,968
|
||||
main stage files and 1,080,259 notes with zero size mismatches. All of those
|
||||
files currently have zero note-name strings. The aggregate raw-type counts are:
|
||||
|
||||
```text
|
||||
00:6 01:719892 02:163462 03:67753 04:8651
|
||||
05:5115 06:193 09:102699 0a:6360 0f:6128
|
||||
```
|
||||
|
||||
`type_override` is non-zero in 52,506 records, so it cannot be discarded.
|
||||
|
||||
## Confirmed type names
|
||||
|
||||
The executable contains a direct pointer table at `0x0077d480`; its array index
|
||||
is the numeric note type. This yields the complete original enum:
|
||||
|
||||
| Value | Original name |
|
||||
| ---: | --- |
|
||||
| 0 | NONE |
|
||||
| 1 | NORMAL |
|
||||
| 2 | FLICK |
|
||||
| 3 | HOLD |
|
||||
| 4 | SCRATCH |
|
||||
| 5 | BEAT |
|
||||
| 6 | MERRY GO ROUND |
|
||||
| 7 | HIDDEN |
|
||||
| 8 | HIDDEN2 |
|
||||
| 9 | CRITICAL |
|
||||
| 10 | SLIDE HOLD |
|
||||
| 11 | SLIDE COUNTER |
|
||||
| 12 | TURN |
|
||||
| 13 | SPIN |
|
||||
| 14 | FINISH |
|
||||
| 15 | DUAL HOLD |
|
||||
|
||||
The same 16-entry sequence is repeated in several embedded reflection/debug
|
||||
tables, which independently confirms the ordering.
|
||||
|
||||
## Confirmed runtime classifications
|
||||
|
||||
With the exact names assigned, several behavioral groups are also explicit in code:
|
||||
|
||||
- `0x005e9480` classifies types 3, 4, 5, 10 and 15 as duration/path notes.
|
||||
- `0x005ebaa0` generates sampled paths for those types. Type 4 additionally
|
||||
builds two derived point arrays; types 3/10/15 share one path branch.
|
||||
- `0x005e94c0` singles out type 6. During gameplay setup it expands that note
|
||||
into several runtime judge objects using the runtime field originating from
|
||||
wire offset `+71`.
|
||||
- Types 1, 2 and 9 follow the non-duration branches in the preview/gameplay
|
||||
render path at `0x006492f0`.
|
||||
|
||||
The enum names are now exact. The remaining reverse target is the meaning of
|
||||
the compact payload fields and the type-specific hold/slide behavior.
|
||||
|
||||
## Appearance, duration and control-helper effects
|
||||
|
||||
The post-load pass at `0x005ebaa0` converts several compact fields using the
|
||||
BPM active at the note timestamp. With `beat_ms = 60000 / bpm`:
|
||||
|
||||
| Wire field | Runtime meaning |
|
||||
| ---: | --- |
|
||||
| `+6` | signed marker-effect/UV selector; the note-head draw passes `value - 1` to effect 3 |
|
||||
| `+39` | appearance lead in beats; runtime `+0xbc = max(time - value * beat_ms, 0)` |
|
||||
| `+51` | duration in beats for types 3/4/5/10/15; runtime `+0x58` and `+0xac = time + value * beat_ms` |
|
||||
| `+55` | packed authored `RRGGBBAA` colour used by duration-target geometry |
|
||||
| `+71` | number of generated MERRY GO ROUND targets |
|
||||
| `+75` | MERRY GO ROUND spacing in beats |
|
||||
|
||||
`0x005efe40` confirms that MERRY's span is
|
||||
`count * spacing * beat_ms`. Gameplay setup allocates three visual handles per
|
||||
generated target, which explains why treating type 6 as one marker loses most
|
||||
of the authored pattern.
|
||||
|
||||
The four blocks following the BPM table in `StageConfig` are variable-length
|
||||
timing lists, not four fixed records. Each list is `u16be count`, followed by
|
||||
`count` entries of `u32be time_ms, u32be mode, f32be value`.
|
||||
`TuneTimingData::GetTime` selects the last entry at or before the note: mode 1
|
||||
returns `value` as milliseconds, mode 3 returns the spacing to the next note,
|
||||
and the other modes return `value * beat_ms`. The third list supplies the late
|
||||
boundary used as runtime `+0xac` for ordinary targets. FLICK (and raw type
|
||||
`0x10`) receives an additional literal `0.2 * beat_ms`.
|
||||
|
||||
Marker opacity is independent of the fixed judgment-window overrides below.
|
||||
`0x0063ff90` stores `beat_ms` at runtime `+0x20`; `0x0064ab80` divides it by
|
||||
the literal `2.0`, while `0x005ebaa0` sets runtime `+0xc4` to
|
||||
`+0xac + 4 * beat_ms`. With `start = +0xbc`, `finish = +0xc4`, and
|
||||
`fade = beat_ms / 2`, the exact unclamped shape is:
|
||||
|
||||
```text
|
||||
time < start + fade : alpha = (time - start) / fade
|
||||
time <= finish - fade : alpha = 1
|
||||
otherwise : alpha = (finish - time) / fade
|
||||
```
|
||||
|
||||
The renderer clamps this to `[0, 1]`. Duration targets use their authored end
|
||||
as `+0xac`; MERRY GO ROUND first adds its generated-target span. The Linux
|
||||
player now carries the per-note BPM and `+0xc4` equivalent instead of using
|
||||
the former guessed 236 ms marker lifetime.
|
||||
|
||||
The lower-right control helpers are not note-head effects. `0x00661680` reads the 16-entry
|
||||
table at `0x006ea040`, loads the indicated `efcdata.dat` record, and stores it
|
||||
in runtime slot `0xb1 + type`:
|
||||
|
||||
```text
|
||||
type: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
|
||||
effect id: -1 61 66 62 63 64 61 -1 67 65 69 -1 -1 -1 -1 68
|
||||
```
|
||||
|
||||
Thus NONE and HIDDEN have no helper resource. FLICK and SLIDE HOLD also draw
|
||||
runtime slot `0xb9` as a direction overlay in `0x006492f0`. The leading words
|
||||
of effect records 61--69 give animation lengths of 16, 38, 34, 13, 16, 14,
|
||||
14, 38 and 38 update ticks respectively.
|
||||
|
||||
The common-effect interpreter is now mapped far enough to reproduce those
|
||||
records exactly. `0x005f2800` dispatches the child nodes, while
|
||||
`0x005f2030`, `0x005f2250` and `0x005f23f0` implement looping, key lookup and
|
||||
step/linear interpolation. Every child has five relative track offsets. A
|
||||
track begins with `u16be key_count, loop_start, loop_end`, followed by keys of
|
||||
`u16be time, u8 interpolation, f32be values[]`. For a sprite child (type 2),
|
||||
the tracks are:
|
||||
|
||||
| Track | Values |
|
||||
| ---: | --- |
|
||||
| 0 | UV frame plus six unused/secondary values |
|
||||
| 1 | XYZ position |
|
||||
| 2 | alpha, red, green, blue |
|
||||
| 3 | XY scale |
|
||||
| 4 | rotation in degrees |
|
||||
|
||||
Type 0 is a parent rotation node in radians. A sprite whose child flag at
|
||||
`+4` is set inherits that rotation, including the rotated XY offset. Effects
|
||||
61--69 use UV record 13 from `uvdata.dat`, whose image is `img13.bin`: target
|
||||
rings, balls, hand prompts, arrows, action bursts and the type-specific help
|
||||
caption are separate animated layers. `0x006492f0` advances the records with
|
||||
an integer 60 Hz tick, places the selected composite at the literal virtual-
|
||||
screen coordinate `(580, 788, 0)` and applies the literal global XY scale
|
||||
`1.3`. `0x006e9ee4` supplies a 1000 ms look-ahead, so only the current helper
|
||||
is selected; the effect is not cloned onto every world-space note. The Linux
|
||||
player implements this format
|
||||
in `vectorail-gc/src/GcTargetEffect.cpp`; it does not substitute a
|
||||
single guessed icon for the composite any more.
|
||||
|
||||
## Cross-version confirmation of note-head rendering
|
||||
|
||||
The Android ARM64 build preserves C++ symbols which are stripped from the
|
||||
arcade executable. Its `GameScene::DrawGameStageCharacter` establishes the
|
||||
same ordering seen in game471: it disables depth writes, calls `DrawWay`,
|
||||
then calls `DrawMark` without disabling the depth test. The route therefore
|
||||
cannot punch holes through marker sprites drawn afterwards.
|
||||
|
||||
`TuneGameData::BuildTimingDataSub` (`0x00196a74`) fixes the note head's world
|
||||
position before gameplay by calling `GetWayPosition(note_time)` directly and
|
||||
storing the returned XYZ at runtime `+0xe0`. Long-note endpoints likewise use
|
||||
`GetWayPosition(end_time)` at `+0xf8`. There is no conversion through physical
|
||||
track distance and no note-time lead added in this path. `DrawMark` consumes
|
||||
these stored positions. The separate one-frame `-16.6667 ms` lookup in
|
||||
`GameScene::SetCommonParam` is used for the moving player segment, not for note
|
||||
placement.
|
||||
|
||||
The Linux player now performs the same direct timestamp lookup for heads,
|
||||
endpoint heads, generated BEAT/MERRY targets, and approach circles. Its shared
|
||||
physical-distance LUT is still needed for duration-body sampling; it now
|
||||
samples each authored segment independently and includes every exact corner.
|
||||
The previous cumulative `t += 0.02` loop could jump across a segment boundary,
|
||||
measure a shortcut chord through the corner, and progressively place later
|
||||
distance-based geometry ahead of the authored route position.
|
||||
|
||||
`GameScene::DrawMark` creates each head with effect manager group 1,
|
||||
effect 3, and the final selector `wire_mark_effect_id - 1`. Group 1 is bound
|
||||
by `GameScene::LoadSkinData` to the selected skin's `uv.dat` and its
|
||||
texture list. The first signed word at stage-note wire offset `+6` contains
|
||||
the selector source. Examples from `ac_10pt8tion_hard.dat` are:
|
||||
|
||||
| Note | Wire `+6` | Resolved UV record |
|
||||
| --- | ---: | ---: |
|
||||
| NORMAL | 1 | 1 |
|
||||
| HOLD | 9 | 9 |
|
||||
| FLICK | 10 | 10 |
|
||||
| SCRATCH | 11 | 11 |
|
||||
| BEAT | 12 | 12 |
|
||||
| CRITICAL | 32 | 32 |
|
||||
| DUAL HOLD | 37 | 37 |
|
||||
|
||||
Effect 3 has a 15-tick loop. Records 1--12 use 32x32 cells from the common
|
||||
skin atlas, while records 32 and 37 use 64x64 cells from the second common
|
||||
atlas. The effect instance uses the literal XY scale `0.025`; consequently
|
||||
the authored 32px and 64px cells occupy 0.8 and 1.6 route-world units.
|
||||
|
||||
Directional heads have a second world-space layer. `DrawMark` plays effect
|
||||
39 over FLICK with UV base 0 and over SLIDE HOLD with UV base 10. The final
|
||||
root angle is not wire `+29` used directly: wire `+25` is a vector length and
|
||||
`+29/+33` are HPB angles. `BuildTimingDataSub` converts these through
|
||||
`RotateHPB::ToVector_Deg`; `GameScene::buildGameData` projects the route point
|
||||
and the displaced point with the camera evaluated at the note timestamp, then
|
||||
uses `RotateHPB::SetVector` (`atan2(screen_dx, screen_dy_down)`) and stores the
|
||||
resulting screen angle. It is computed before gameplay and remains fixed while
|
||||
the live camera moves. Effect 39 resolves to a 128x32 animated strip whose
|
||||
arrows extend to the sides of the 32x32 head; it uses the same `0.025` scale,
|
||||
colour and marker alpha. This is independent of the lower-right control
|
||||
helper, even though that helper also has a direction overlay.
|
||||
|
||||
The marker animation clock is beat-synchronized, not a free-running 60 Hz
|
||||
counter. Android `GameScene::SetCommonParam` computes
|
||||
`phase = 1 - (((time - bpm_change_time) / beat_ms * 2000) % 2000) / 2000`
|
||||
and `DrawMark` writes `phase * EffectData::GetFrameMax()` to effect field
|
||||
`+8`. Thus effect 3 runs backwards from frame 15 toward frame 0 once per beat
|
||||
and resets at every beat; BPM changes also reset the phase origin. BEAT adds
|
||||
`sample_index * 2` to this base frame.
|
||||
|
||||
The approach ring is geometry, not another atlas layer.
|
||||
`GameScene::drawHitTimingCircle` draws a white 16-segment circle with additive
|
||||
blending, `radius = 0.2 + progress * 0.5`, and alpha `marker_alpha * 128/255`.
|
||||
For ordinary notes `progress = (late_boundary - time) / (2 * (late_boundary - early_boundary))`,
|
||||
capped at 1 and only shown before
|
||||
the target time. The arcade MERRY branch instead evaluates the analogous
|
||||
progress separately at each generated target's shifted timestamp.
|
||||
|
||||
Both that circle and the effect-3 head are camera-facing billboards. The
|
||||
mobile renderer calls `SetTransposeMatrix(world_position, camera_matrix)` for
|
||||
the circle and leaves the effect sprite in billboard mode; neither is laid in
|
||||
the route tangent plane. Duration bodies remain authored 3D route geometry.
|
||||
|
||||
The Linux player now decodes this selector, interprets effect 3, loads
|
||||
`skin/common/common.png`, `skin/common/common2.png`, the selected
|
||||
`skinN/img.dat` list and `skinN/uv.dat`, and renders the selected animated
|
||||
UV record. The former guessed `effect/game/img4.bin` glyph mapping and
|
||||
synthetic pulsing rings are no longer used for decoded GC stages.
|
||||
|
||||
Duration bodies are also type-specific in the executable:
|
||||
|
||||
- HOLD calls `0x00647cf0` and SLIDE HOLD calls `0x00641fd0`; both emit ribbon
|
||||
triangles rather than an OpenGL-style line.
|
||||
- SCRATCH is sampled every `0.15` world units. `0x005ebaa0` derives two
|
||||
opposing paths with radius `0.2` and rotates the offset by 45 degrees per
|
||||
sample; `0x00641d50` emits six vertices per segment for each path.
|
||||
- BEAT is sampled every `0.55` world units and `0x0064ab80` creates a separate
|
||||
effect at every point. There is no continuous BEAT body. Its case-5 loop
|
||||
draws only points whose sampled timestamp has not passed, and writes
|
||||
`base_frame + sample_index * 2` into effect field `+8`. Android's symbolized
|
||||
`EffectData` access confirms this field is the animation frame; the authored
|
||||
UV-list selector remains unchanged.
|
||||
- MERRY GO ROUND computes the `+0xbc…+0xc4` alpha once outside its generated-
|
||||
target loop and multiplies every copied effect by that same parent alpha.
|
||||
Individual offsets from `0x005efe40` change target time and position, not
|
||||
the fade interval.
|
||||
- DUAL HOLD uses `0x00646d70` to build two ribbon bands. Its literal inner and
|
||||
outer offsets are `0.2` and `0.5`.
|
||||
- Android `GameScene::drawLongShotEffect` (matching arcade `0x0064ab80`)
|
||||
draws an effect-3 marker at the authored endpoint as well as the starting
|
||||
marker. The fixed endpoint UV bases are 29 for HOLD, 30 for SCRATCH, 41 for
|
||||
SLIDE HOLD, and 42 for DUAL HOLD. SLIDE HOLD additionally layers effect 39
|
||||
with UV base 10 at the endpoint and rotates its root by the precomputed
|
||||
screen direction. Both endpoint layers inherit the long note's colour, fade alpha,
|
||||
billboard transform, beat-synchronised animation and `0.025` sprite scale.
|
||||
|
||||
## Tap judgment timing
|
||||
|
||||
The system-config loader at `0x00635c90` lays out the timing overrides in four
|
||||
per-difficulty arrays. The stage loader applies them in `0x005ed4c0`, and the
|
||||
post-load pass at `0x005ebaa0` derives these runtime note boundaries:
|
||||
|
||||
| Runtime field | Boundary | Easy override |
|
||||
| ---: | --- | ---: |
|
||||
| `+0x98` | note time minus `MissTimingOverride` | 236 ms |
|
||||
| `+0x9c` | note time minus `UnmuteTimingOverride` | 202 ms |
|
||||
| `+0xa0` | note time plus `LimitTimingOverride` | 202 ms |
|
||||
| `+0xa4` | note time plus `MuteTimingOverride` | 0 ms |
|
||||
|
||||
`0x005d1690` is the tap-grade function. It selects `+0x9c` for an early tap or
|
||||
`+0xa0` for a late tap and computes the absolute distance `W` from the note
|
||||
time to that boundary. It then builds thresholds using the literal table
|
||||
`{100000, 100, 50, 25}` at `0x006e8bc0`, divided by 100. The grade loop checks
|
||||
indices 3 down to 1, which matches the adjacent original enum/string order
|
||||
`MISS, GOOD, COOL, GREAT`.
|
||||
|
||||
For an error magnitude `e` and the selected outer input distance `W`, the
|
||||
unclamped comparisons are therefore:
|
||||
|
||||
```text
|
||||
GREAT: e < 0.25 * W
|
||||
COOL: e < 0.50 * W
|
||||
GOOD: e < 1.00 * W
|
||||
```
|
||||
|
||||
`GreatMinTime` is copied into the gameplay object at `+0x104`; if the computed
|
||||
GREAT threshold is below that value, it is clamped and COOL is redistributed
|
||||
by the same routine to `great + (good - great) / 3`, capped at GOOD. With the
|
||||
shipped Easy `W = 202`, the absolute-error thresholds are 50.5 ms for GREAT,
|
||||
101 ms for COOL, and 202 ms for GOOD. HARD/EXTRA use `W = 134`, producing
|
||||
33.5/67/134 ms. The shipped `GreatMinTime = 32` therefore does not clamp these
|
||||
four standard difficulty windows, but the branch matters for custom configs.
|
||||
|
||||
`MissMarkOverride=1` in the shipped `data/system.cfg` makes the stage loader
|
||||
replace the timing-list results with these four Simple/Normal/Hard/Extra arrays:
|
||||
|
||||
```text
|
||||
MissTimingOverride = (236,202,168,168)
|
||||
UnmuteTimingOverride = (202,168,134,134)
|
||||
LimitTimingOverride = (202,168,134,134)
|
||||
MuteTimingOverride = (0,0,0,0)
|
||||
```
|
||||
|
||||
## SHOT miss gating
|
||||
|
||||
The two stage WAVs remain sample-synchronized, but they are not mixed at a
|
||||
fixed level. `FUN_00640790` reads each player's `IsMute` state after the
|
||||
judgment update. For player zero, `FUN_00611920`/`FUN_00611940` pass `1.0` or
|
||||
`0.0` through `FUN_00611800` to the third volume multiplier of channel 13
|
||||
(SHOT); channel 12 (BGM) is untouched. The write is immediate rather than a
|
||||
timed SoundManager fade.
|
||||
|
||||
An unresolved target closes the SHOT gate at runtime `+0xa4`, which is note
|
||||
time plus `MuteTimingOverride` (zero in the shipped configuration). The note
|
||||
remains judgeable through `+0xa0`; a successful late judgment opens SHOT again
|
||||
immediately. This is why a miss removes the chart-performance layer without
|
||||
stopping or seeking either WAV.
|
||||
|
||||
## Gameplay tap and AD-LIB sound effects
|
||||
|
||||
The hit sounds are a separate three-WAV sound set; they are not baked into
|
||||
either stage stem. `FUN_00613430` resolves the selected entry from
|
||||
`data/boot/se.dat` and loads its three names from record fields `+0x14`,
|
||||
`+0x18`, and `+0x1c`. The default `se0000` ("Ver.3 Set") maps them to:
|
||||
|
||||
| Slot | WAV | `SEList.csv` volume | Playback channels |
|
||||
| ---: | --- | ---: | --- |
|
||||
| 0 | `SE_ARRANGE.wav` | 87 | 6, 7 |
|
||||
| 1 | `TAP_SE1.wav` | 80 | 10, 11 |
|
||||
| 2 | `TAP_SE2.wav` | 77 | 8, 9 |
|
||||
|
||||
Every slot is deliberately double-buffered. `FUN_00611fc0` round-robins slot
|
||||
0 over channels 6/7. `FUN_00611e70` round-robins the two booster sounds over
|
||||
10/11 and 8/9. Consequently consecutive taps overlap instead of cutting off
|
||||
the previous sample. Gameplay update `FUN_00640790` consumes the two booster
|
||||
edge flags at player fields `+0xed/+0xee` and the separate AD-LIB flag at
|
||||
`+0xaa`; the booster edge is what starts TP1/TP2, before the resulting note
|
||||
rank is known. A mistimed press still produces its tap sound. The AD-LIB flag
|
||||
selects slot 0 independently.
|
||||
|
||||
The other `se.dat` entries preserve the same three-slot layout, for example
|
||||
`ALB_06/TP1_06/TP2_06` for the Scratch set. The chosen set id comes from the
|
||||
player customization state (`Afx` state `+0xce0` in the gameplay loader), not
|
||||
from the stage chart.
|
||||
|
||||
The Linux player currently loads the default Ver.3 set at the exact CSV gains
|
||||
and reproduces the original two voices per slot. It decodes serialized marker
|
||||
byte `+5` into the runtime `+8` AD-LIB flag and starts ALB after a successful
|
||||
rank on such a marker. D-pad inputs are treated as booster 1 and face-button
|
||||
inputs as booster 2; this also matches the PSP-style control grouping.
|
||||
|
||||
## Ranked hit effects
|
||||
|
||||
`setRankedEffect` records the rank and gameplay timestamp. The later
|
||||
`DrawGameStageCharacter` pass selects group-1 effect `rank + 0x1c`, so the
|
||||
effect IDs are 29 MISS, 30 GOOD, 31 COOL, and 32 GREAT. Their decoded lifetimes
|
||||
are respectively 20, 12, 12, and 12 frames. The effect advances from the hit
|
||||
timestamp at 60 Hz, uses additive blending, a global XY scale of `0.03`, a
|
||||
camera-facing matrix, and a stored random root rotation from `rand() % 360`.
|
||||
|
||||
## Hit-type dispatch and long-shot ranks
|
||||
|
||||
The arcade dispatcher at `0x005d5fb0` selects the handler from the effective
|
||||
marker type: 1 TAP, 2 FLICK, 3 HOLD, 4 SCRATCH, 5 BEAT, 6 MERRY, 9 CRITICAL,
|
||||
10 SLIDE HOLD, and 15 DUAL HOLD. Android's symbolized
|
||||
`GameScene::checkHitMark` independently confirms the same mapping and names
|
||||
type 9 `checkHitMarkDualTap`.
|
||||
|
||||
Dual tap stores the timestamp of its first independent input, consumes a
|
||||
second independent input, then grades using the first timestamp. DUAL HOLD
|
||||
also waits for two independent inputs, but begins its measured hold duration
|
||||
when the second arrives; releasing either side ends it.
|
||||
|
||||
Arcade `FUN_005d0d80` grades HOLD and DUAL HOLD by the percentage of the
|
||||
authored body actually covered after clamping press/release to its start/end:
|
||||
|
||||
```text
|
||||
GREAT: held > 80%
|
||||
COOL: held > 60%
|
||||
GOOD: held > 40%
|
||||
MISS: otherwise
|
||||
```
|
||||
|
||||
The comparisons are strict. Before computing the percentage it also applies
|
||||
the original short-tail compensation: if 20% of the body is below
|
||||
`66.666664 ms`, that difference is added to covered time. `FUN_005d0eb0`
|
||||
finalizes a still-pressed hold at the authored end; an early release grades it
|
||||
immediately.
|
||||
|
||||
BEAT (`FUN_005d41b0`) uses the same long-shot percentage grader, but records
|
||||
the first tap and continually replaces the covered endpoint with each later
|
||||
tap. If no fresh trigger arrives for `BeatEnableTime` (200 ms in the shipped
|
||||
config), it finalizes at the last trigger. Thus it measures how far through the
|
||||
body the repeated taps continue, rather than merely counting taps.
|
||||
|
||||
SCRATCH (`FUN_005d44f0`) listens to four directional triggers. The first one
|
||||
starts coverage; each later trigger extends it only when its direction differs
|
||||
from the previous accepted direction. It finalizes after `ScratchEnableTime`
|
||||
(250 ms) without a direction change or at the body end, then uses the same
|
||||
40/60/80-percent long-shot grader.
|
||||
Reference in New Issue
Block a user