26 Commits

Author SHA1 Message Date
6c9380ff13 re(flight): a per-frame sampler, and nav oracles that a menu bar cannot break
WIP toward the residual flight-speed-law question (does a 1 s burst reach the
steady angular rate, or is there a per-axis multiplier?). The write-up already
concluded that host-side polling cannot answer it and named a Canary-side hook
as the tool required; that hook now exists (--frame_probe_log, committed as
auto/re-frame-probe in xenia-canary-native) and this is the harness for it.

- `rebuild_canary.sh` -- the surgical rebuild the box can actually do, kept in
  the repo this time instead of in /tmp: compile only the changed objects, `ar`
  them into their archive, and re-run the link command lifted out of the
  generated ninja. A full `ninja` is impossible here (several TUs need dev
  headers the image lacks) and the build cache cannot be re-configured. 31 s.
- `frame_burst.py` -- points the probe at the player craft's transform block
  (pos-112, the three 16-byte-strided rows plus the position) and drives full
  stick holds, recording each hold's start and end in the same clock the probe
  stamps its lines with.
- `frame_session.sh` -- the whole run as ONE blocking foreground call, per the
  session-lifetime rule; REUSE=1 drives a Canary that is already up.
- `nav_to_flight.sh` -- fly_stage.sh's navigation, split out so a live emulator
  can be re-used. A boot to the title costs minutes under lavapipe and a run
  that only failed to NAVIGATE should not pay for it twice.

The navigation change is the one worth reading. Every screen oracle here tested
named pixels ("648,221 is white"), which is only valid while the game image sits
at a known place on the root window -- and it does not: xenia's GTK window has a
menu bar, so on this display the image is ~25 px lower and every constant reads
the wrong row. Nothing errors. One run sat 300 s in front of a plainly visible
MAIN MENU reporting "no main menu"; the next missed the title screen entirely
and let the attract movie loop for ten minutes.

So `screen_id.py` identifies screens by WHOLE-IMAGE statistics instead -- the
fraction of green UI-text pixels, the fraction of near-white pixels, and the
per-channel means -- which no vertical shift, scale or letterbox can move. It is
calibrated against known-good captures and classifies all of them correctly:
title, three different menu screens, in-flight, and four movie frames as
"other". `bin/screenshot` additionally crops the menu bar off saved evidence
shots, deriving the offset from the window's own height rather than a constant.

Not yet a finding: the run has not reached flight, so no rate has been measured.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-14 06:03:44 +00:00
4f6fcf36dc re(flight): the ramp test is inconclusive, and names the tool the residual needs
To separate "a 1 s burst never reaches the steady rate" from "a per-axis
multiplier", measure inside ONE hold: successive 0.25 s windows of a single 3 s
press, holding speed, attitude and starting conditions constant by construction.

    rep0 rate   16   59  325  209  130  322  238  110  310  151  183
    rep1 rate   52  181  231  246  236  169  368  195  181  255  182

Not usable. At 0.25 s the windows do not contain enough guest updates to average,
so rate and speed both swing 3x window to window -- the same aliasing that once
manufactured a rate-vs-speed curve, reappearing at finer resolution. The first
window is lowest in BOTH repeats, which is what a ramp would look like, but the
sequence never plateaus, so the signal cannot be separated from the sampling. No
claim either way.

Widening the window does not rescue it: 0.5 s averages well enough, but a hold
long enough to contain several 0.5 s windows bleeds speed -- and speed is the
variable under test. The two effects are entangled at this observation rate.

So the residual needs a different INSTRUMENT, not another script. Live-RAM polling
samples an unsynchronised snapshot; the question wants the craft's angular
velocity as the guest computes it, once per frame. That is a Canary-side hook --
the same shape as the existing F10 ship-capture patch -- and the rebuild toolchain
already makes it cheap. Recorded as the recommendation rather than attempted as a
seventh variation of the same measurement.
2026-08-13 23:53:23 +00:00
8b03a52aa6 re(flight): both axes at settled speeds -- shape confirmed, absolute scale is not
Burst design repeated with the HUD clock bracketed (01:01.86 -> 02:10.61, ~x1.26)
and roll measured in the same flight.

    pitch   87.0 @ 85    82.8 @ 308    47.0 @ 1077   min:max 1.85
    roll   125.0 @ 76   119.0 @ 291    82.6 @ 1023   min:max 1.51

SHAPE CONFIRMED on both axes: the rate interpolates between _Min (at minimum
speed) and _Max (at maximum speed), matching to 1.6% for pitch (vs PitchMinus
75/40 = 1.88) and 5.6% for roll (vs 200/125 = 1.60). Both ratios are
clock-independent so they stand regardless of the conversion, and pitch's
re-confirms ly+ = pitch-MINUS (reproducing 1.82 from the previous run).

ABSOLUTE SCALE DOES NOT MATCH, and not in the same direction:

    pitch  measured/predicted = 1.15, 1.21, 1.07   consistently OVER
    roll                      = 0.62, 0.64, 0.60   consistently UNDER, very flat

A clock error cannot explain this -- it would move both axes together and these
go opposite -- so the discrepancy is per-axis. Candidates, neither measured: a 1 s
burst not completing the angular-acceleration ramp (would under-read; fits roll's
flat 0.62, not pitch's excess), or an unidentified per-axis multiplier. Left as an
open question with the separating test named: 1 s versus 3 s bursts at the same
throttle, where the ramp hypothesis predicts the longer burst reads higher.

Also noted: the roll run correctly REFUSED on a WEAK row pin after the pitch
bursts left the craft tumbled -- and roll is immune to that labelling, which the
guard's own message says, so ALLOW_WEAK_PIN=1 is its documented exception. A guard
that states its own exception is worth having.
2026-08-13 23:42:28 +00:00
92d9683f3a re(flight): short bursts confirm the rate-vs-speed SHAPE, without needing the clock
The design the sweep could not provide: settle the throttle, measure the settled
speed, pitch for ONE second so speed barely moves inside the burst. Three
throttles, two repeats, row pin CONFIDENT, fresh flight.

    LT min      burst speed ~105    rate 113.6, 109.5 deg/wall-s
    cruise                  ~383         100.2,  88.4
    RT max                 ~1483          52.2,  70.5

Rate falls monotonically with speed -- 111.5 -> 94.3 -> 61.4 -- at three KNOWN,
SETTLED speeds instead of smeared across a bleeding one.

The decisive comparison needs no clock. Absolute rates depend on the run's clock
ratio, but the min:max RATIO cancels it:

    measured min:max            = 1.82
    AV_PitchMinus_Min/Max 75/40 = 1.88   ->  3.0% apart
    AV_PitchPlus_Min/Max 150/70 = 2.14   -> 15.1% apart

Two conclusions, neither resting on a clock measurement:
 - _Min/_Max really do mean "at minimum / at maximum speed", with the rate
   interpolating between them: shape confirmed to 3%.
 - ly+ drives pitch-MINUS, not plus. The craft has asymmetric pitch authority
   (75/40 down vs 150/70 up) and the ratio picks the pair cleanly.

Absolute magnitudes remain open: this run did not bracket the HUD clock, so
deg/GAME-second cannot be computed from it, and picking a ratio that makes the
numbers fit would be circular. The probe now screenshots the clock at both ends.

Also: fly_stage.sh now waits for the TAKE-OFF load too. Guarding only the stage
load left a run pressing A into a black screen and then reporting "player entity
not found" from a game that never reached flight.
2026-08-13 23:29:23 +00:00
a3f14710a4 re(flight): clean pitch sweep -- magnitudes agree, the interpolation law does not
Fresh flight, row pinning CONFIDENT (margin 0.413), one sweep and nothing before
it. axis_probe now REFUSES to measure on a WEAK pin (ALLOW_WEAK_PIN=1 overrides)
since it is a precondition, not a warning: roll is immune to the up/right
labelling but pitch and yaw are not.

Clock x1.26. Binned by speed, both in game units, against the linear
interpolation of AV_PitchPlus_Min 150 (at MinimumVelocity 100) to _Max 70 (at
MaximumVelocity 1200):

    speed ~435   measured 100.8   predicted 125.6
    speed ~572            113.8             115.7
    speed ~709            126.3             105.7
    speed ~846             83.1              95.7
    speed ~983             72.7              85.8

Supported: the magnitudes (73-126 measured vs 86-126 predicted) and a falling
high-speed end. NOT supported: the interpolation law. Scatter is +-25%, the two
fastest bins hold 1 and 2 windows (the first moments before the speed bled), and
the slowest bin misses in the wrong direction.

The flaw is structural, not statistical: a sweep DRIVEN by the speed bleeding
cannot dwell at either extreme, which is exactly where the law is most testable.

What would settle it: hold a settled throttle and pitch for ~1 SECOND, so speed
barely moves inside the burst and each burst gives one honest (speed, rate) point;
repeat at LT / neutral / RT for three clean points at known speeds. Recorded as
the next design rather than attempted as a fifth variation of the same sweep.
2026-08-13 23:12:50 +00:00
0a84c1358e re(flight): polling faster than the guest updates manufactures a clean curve
Fitting rate against instantaneous speed produced a tidy "rate rises with speed"
relationship, with speeds up to 4795 when the craft's maximum is 1200. It is
entirely an artefact: 20 Hz polling is faster than the guest updates these fields,
so a per-read delta is either exactly zero (no update yet) or a whole frame's
worth divided by a fraction of a frame. 111 of 352 reads were zero on BOTH
channels -- position and attitude update on the same frame, so the two are
perfectly correlated, and dividing each by the short wall dt produced the
correlation out of nothing.

Fix: aggregate over windows spanning many frames (0.5 s). A sum of |delta| over
such a window is right however the updates fall inside it.

This does NOT affect the swept-total probes (roll_axis.py, rate_probe.py) -- they
already summed over the whole dwell, immune for the same reason. Only per-sample
instantaneous rates were ever wrong, so no earlier number moves.

The windowed re-run is NOT yet claimed as a result. It gives plausible magnitudes
but still shows rate rising with speed, against the definition's PitchPlus_Min 150
> _Max 70, and it has two disqualifiers: it ran on an instance where the craft was
already tumbling from the previous sweep, so pinning reported "WEAK -- craft may
not be level", and the sweep started mid-range rather than at maximum. A clean
answer needs a fresh flight with pinning CONFIDENT and nothing before it. Since
what is in doubt is precisely what _Min/_Max mean, a measurement through a
doubtful instrument cannot settle it.

Both datasets kept, the bad one labelled, because the aliased curve is a good
example of what a manufactured correlation looks like.
2026-08-13 23:00:59 +00:00
6c7025851e re(flight): the rate probe measures a MOVING speed -- pitching bleeds it hard
Measured pitch with the rows properly pinned, against this craft's own disc caps
(AV_PitchPlus_Min 150, AV_PitchPlus_Max 70):

    min speed (LT)  1391.0 deg / 8.00 s, clock x1.326 -> 131.1 deg/game-s  vs 150
    max speed (RT)   989.0 deg / 8.05 s, clock x1.318 ->  93.2 deg/game-s  vs 70

A rate 33% ABOVE a cap is not a finding, it is a broken instrument. The HUD speed
is in the same bracketing screenshots that give the clock, so read it:

    slow phase   102 ->  18
    fast phase  1193 -> 589

The speed is NOT constant during the dwell -- pitching halves it in 8 seconds.
The cap is speed-dependent, so as the craft slowed its cap rose, and an 8-second
average necessarily lands between the max-speed cap and a mid-speed one. The 133%
is entirely the instrument.

This also weakens the roll result committed earlier: same method, so 120.9 vs
AV_Roll_Max 125 is CONSISTENT but is not a tight test -- the true cap could be
lower and still produce that average. Said plainly in the doc rather than left
standing as a clean confirmation. Min-speed figures are less affected; there is
little speed left to lose.

Proper fix, not yet done: dwell ~1-2 s so speed barely moves, or sample HUD speed
continuously and fit rate against INSTANTANEOUS speed -- which yields the whole
rate-vs-speed curve instead of two points.

Separately this is a flight-model finding: TURNING COSTS SPEED, steeply, with the
throttle still at maximum. A reimplementation treating the throttle as a speed the
craft simply holds will be wrong during manoeuvres.
2026-08-13 22:48:29 +00:00
dc46339b63 re(flight): rows pinned by world-Y, and nothing yaws -- the yaw gap is answered
The probe now pins which non-forward row is up and which is right, instead of
taking the D3D convention on faith:

    row world-Y means: [0.469, 0.883, -0.000]   forward = row 2
      -> up = row 1, right = row 0   CONFIDENT

That is the OPPOSITE of the assumption the previous run used, so that run's yaw
and pitch columns were swapped -- under the correct labels its ly+ reading of
154.1 deg/wall-s is PITCH, which is what a left-stick Y should do.

Measured with the unknown inputs FIRST, each passing a liveness check:

    rx  0.0  0.0  0.0      ry  0.0  0.0  0.0
    LB  0.0  0.0  0.0      RB  0.0  0.0  0.0
    lx  roll 209.8         ly  pitch 154.1

These zeros are trustworthy where the previous run's were not: the craft was
verified alive between inputs, and the probe aborted the moment it stopped moving
rather than reporting the clean zeros a destroyed craft produces (it did abort,
after lx+, which is why lx/ly are carried from the earlier run rather than
re-measured).

So NO PAD INPUT YAWS THE CRAFT. AV_Yaw_* (45/25) exists in the definitions but
nothing on the right stick or the shoulders drives it, which upgrades the old
"yaw: no input found" from a failure to find one into a measurement that the
remaining candidates do nothing. The natural reading is that yaw is a consequence
of banking rather than a commanded axis.

Not covered, and not claimed: the d-pad (tactical map) and the face buttons
(fire/weapon select). Neither is a plausible flight axis; neither was measured.
2026-08-13 22:35:36 +00:00
dfa769420d re(flight): axis probe pins the rows and checks liveness; fly_stage waits, not sleeps
Both fixes the previous run's caveats asked for, plus one the run itself forced.

axis_probe.py now:
 - PINS which non-forward row is up and which is right, by comparing world-Y
   across the rows in level flight, and says CONFIDENT or WEAK. entities2
   measures row 2 = forward against velocity, but the other two were labelled by
   the D3D convention, and yaw/pitch SWAP if that is wrong -- so the previous
   run's last two columns were named on an assumption.
 - checks the craft is ALIVE between inputs, and ABORTS with a message instead of
   reporting the clean zeros a destroyed craft produces. The first run ended on
   GAME OVER and only said so afterwards.
 - measures the UNKNOWN inputs (rx, ry, LB, RB) first while the craft is healthy,
   keeping the established lx/ly as controls at the end.

fly_stage.sh now WAITS for the stage load instead of sleeping a fixed guess. The
fixed sleeps worked until they didn't: one load ran long, the script pressed START
into a black screen, and every later step went to nothing while the screenshots
recorded a plausible-looking sequence. It now polls for a non-black frame and
aborts with a pointer to the log if the load hangs (PhysicalHeap::Release
failures) rather than continuing blind.

The probe itself did not run this iteration -- the stage load hung -- so there is
no new axis data, and none is claimed.
2026-08-13 22:22:19 +00:00
5f3c618b51 re(flight): axis probe -- lx is roll, ly drives one clean axis, rest not trustworthy
New probe (axis_probe.py) decomposes every held input into all THREE rotation
components at once, instead of measuring one axis at a time through a non-forward
matrix row -- the flaw that once made roll and pitch produce identical numbers.
For previous rows (f,u,w): roll = atan2(u.w_old, u.u_old), and forward's rotation
toward each of the other two rows gives the remaining pair.

Stage 02, file pad, full deflection on exactly one channel at a time:

    lx+   roll 209.8   b 0.9    c 10.1     deg/wall-s
    ly+   roll   0.0   b 154.1  c  0.0
    rx+   roll   0.0   b 0.0    c  0.0
    ry+   roll 161.1   b 87.0   c 37.1
    LB/RB all zero

What this supports: lx = ROLL, cleanly (~0 on both other channels), agreeing with
the independent roll measurement. ly drives ONE axis, cleanly.

What it does NOT support, and I am not claiming:
 - WHICH axis ly drives. entities2 measures row 2 = forward against velocity, but
   rows 0 and 1 are labelled up/right by the D3D convention rather than by
   evidence, and yaw/pitch SWAP if that is wrong. Roll is immune (rotation of
   either non-forward row in their shared plane is roll either way).
 - anything about rx/ry/LB/RB. The run ended on GAME OVER: full-deflection spin in
   a live combat mission gets the craft destroyed, and the only symptom is "0
   player candidates" AFTERWARDS, so late rows may be post-death. rx+ reading all
   zeros and ry+ reading mixed are exactly what a dying craft would produce.

So "yaw: no input found" is NOT resolved. Both gaps are now written into the
probe's header with what would fix them: a liveness check between inputs, and
pinning up-vs-right against world Y.
2026-08-13 22:10:23 +00:00
850b04c606 re(flight): roll DOES depend on speed -- the withdrawal is reversed
Measured about the FORWARD axis (roll_axis.py), which is the fix the withdrawal
specified: express the new up-vector in the old (up, right) basis and take
atan2(u.w_old, u.u_old), so the component along forward -- what pitch produces --
is dropped by construction. The old probe watched a non-forward matrix row, which
sees any rotation that moves it, and that is why two different stick axes produced
the same numbers.

Stage 02, file pad, single-axis holds (lx=32767 with every other channel exactly
0, trigger held in the same write since the pad state is written whole), 5 s
settles, 8 s dwells, each phase bracketed by HUD-clock screenshots:

  min speed (LT)  1 856.9 deg / 8.01 s wall, clock x1.247 -> 185.9 deg/game-s
                  vs AV_Roll_Min 200
  max speed (RT)  1 181.2 deg / 8.02 s wall, clock x1.218 -> 120.9 deg/game-s
                  vs AV_Roll_Max 125

So roll behaves exactly like pitch: the rate cap falls as speed rises and
_Min/_Max mean "at minimum / at maximum speed". The withdrawn claim that roll
shows NO speed dependence is reversed, and the axis rule is now confirmed on a
second axis instead of contradicted by it.

Both land just under their caps (93% and 97%), the right side for a rate limit.
The shortfall is NOT explained and no claim is made about it: candidates are the
craft not being exactly at min/max speed after 5 s, and the 20 Hz swept-angle sum
undercounting.

The tell that the old result was broken was two conditions agreeing too well. The
tell that this one is sound is that they disagree in the direction the definitions
predict, on two independently bracketed phases.
2026-08-13 21:57:10 +00:00
3d3d6726fa re(flight): START skips the briefing, and the player-entity lock is stage-specific
The scripted route now reaches FLIGHT unattended: boot -> main menu -> poke the
cleared-stage mask -> EXTRAS -> MISSION SELECT -> stage -> briefing -> READY ROOM
-> TAKE OFF -> flight, verified by a full HUD (TIME 00:52.54, speed 350, shields,
REMAINING OB 018) on stage 01.

Two things that cost a run each:

START skips the briefing; A does not. A pages through the brief, and ten A taps
still left the run sitting on a briefing screen -- twice, on two different
stages. One START press lands on the READY ROOM. fly_stage.sh now presses START.

entities2.py's `self` locked on `"Player" in name`, and that suffix is
STAGE-SPECIFIC: stage 02 fields UN_f002_TCAF_DeltaSaber_W_Player, but stage 01
fields UN_f001_TCAF_DeltaSaber_T with no suffix, so the filter found nothing
while the game was visibly flying and reporting 64 typed live entities. Falls
back to the craft class and prefers the instance that is actually moving (a
mission holds more than one). Recorded rather than worked around, because the
same assumption is embedded in several probes.
2026-08-13 21:45:54 +00:00
0b2f004ad0 re(flight): port the roll probe to the file pad, and drive fly_stage.sh to flight
roll_axis.py was written last session to re-measure roll ABOUT THE FORWARD AXIS --
the fix for the withdrawn result, whose defect was the MEASUREMENT (a non-forward
matrix row sees any rotation that moves it, and pitch moves it as much as roll)
and not the input device. It was never run, and it still drove vgamepad, which is
retired for leaking to the host.

Ported to the file pad. The important difference is not the device but the shape:
the file pad is a SNAPSHOT, not independent channels -- each write replaces the
whole state -- so holding a trigger while deflecting a stick has to be one write.
pad_state(**kw) does that. The values are also exact rather than whatever a
virtual stick quantises to, which is what makes a single-axis hold (lx=32767 with
everything else exactly 0) trustworthy for an axis-separation question.

fly_stage.sh now drives the rest of the route it previously stopped short of:
briefing (A: Continue) -> READY ROOM -> up to TAKE OFF -> flight, and notes why
it must get there at all (a snapshot at the briefing yields zero unit-definition
objects; they are instantiated at stage load proper).
2026-08-13 21:37:44 +00:00
faa7b3d611 re(challenge): the part id is never persisted -- differential search says stack only
No literal 26 exists anywhere, so the GamePart id is computed. That does not stop
it being found: the id is KNOWN at each screen from the GamePart table (EXTRAS = 5,
MISSION SELECT = 7), so snapshot both and intersect. New tool diff_words.py does
the classic differential search over the sparse guest image, and find_partslot.sh
drives the two screens and runs it.

Result: 171 MB scanned, exactly 4 addresses read 5 then 7 -- 0x708FFBEC,
0x708FFCBC, 0x708FFDAC, 0x708FFE20 -- and all four are guest STACK (the same run's
log puts thread stacks at 0x709...). So the requested part id exists only as a
stack argument in flight; there is no persistent field, which is consistent with
finding no literal store, and means there is nothing stable to poke.

That closes the last memory-and-menu route to the challenge missions. Reaching
them needs either the genuine in-game unlock (an in-mission attainment, per
AVSCRIPT_COMMAND_ATTAINMENT_CHALLENGE_MISSION_CARGO_SCORE) or an emulator-side
hook that forces the transition -- a code change, not a poke.

diff_words.py is worth keeping well beyond this question: it locates any field
whose address is unknown but whose value is known at two moments.
2026-08-13 21:31:47 +00:00
a79a1da183 re(challenge): withdraw the kind-field constructor attribution -- snapshot refutes it
Section 4 claimed the mission-kind field at obj+144 "is initialised to 0 in the
constructor sub_821783D8, alongside +148 = 0, +132 = 2, +136/+140 = -1". That
linked two code regions on nothing more than both touching +144/+148, and an
in-flight snapshot refutes it:

sub_821783D8 initialises the STATIC at 0x828F3EC0 -- its first act is
InitializeCriticalSection(obj, 256) -- and in the snapshot that object holds
0x000B1C8B at +144 and a float at +592. Not a kind, not flags. So the object that
owns the kind field is UNIDENTIFIED. Scanning the snapshot for it (kind in
{0,3,5,6} at +144, stage 10 at +148, pointers at +0/+52/+604) returns only
matches inside the executable's own static data; the value 10 at +148 is far too
common to discriminate.

The switch itself stands -- it is direct disassembly at two independent sites
(0x82184df0, 0x82185ed0) plus two grouping tests. Only the attribution was wrong,
and the wrong half is the half I inferred rather than read.

Confirmed on screen instead: launching a story stage from MISSION SELECT reaches
a READY ROOM carrying an "EXTRA" watermark -- the EXTRA config section, kind 3,
visible in the UI. Independent evidence the field means what section 4 says even
though its owning object is not pinned.
2026-08-13 21:21:49 +00:00
4cbce6bd21 re(units): S10 closes the story campaign for real -- 69 units, 7 204 values
roster_target had flagged a contradiction: S10, a STORY stage, still fielded an
unharvested unit, which the "story campaign complete at 68 units" claim did not
account for. S10 was simply never flown. Flying it settles it:
UN_e005_ADAN_ElanTypeQ_Margras is in the snapshot, 144 rows, and the merge takes
the file 68 -> 69 units, 9 393 -> 9 537 rows, 7 115 -> 7 204 defaulted-on-disc
values, with 154 disc cross-checks agreeing and 0 disagreeing. roster_target now
reports S10: 0 missing, and every remaining gap is a CHALLENGE stage
(S24/S25/S27/S28/S29).

A much simpler way to fly a story stage, replacing the save-editing route
(tools/re-capture/fly_stage.sh): poke ONE word -- 0x828F40C0 = 0x0001FFFE marks
stages 1-16 cleared -- and MISSION SELECT will launch any of them. Nothing is
written to disc, so there is no save to back up and restore, and TRAP 1 from the
old recipe (launch_mission.sh silently loading the last-used slot) cannot happen.

Route, learned by screenshotting each step: MISSION SELECT -> pick stage -> A ->
mission briefing (A: Continue) -> READY ROOM -> TAKE OFF -> flight. The READY
ROOM carries an "EXTRA" watermark, which is the static analysis's mission-KIND
= 3 ("EXTRA" config section, docs/re/challenge-mission-gate.md section 4) visible
on screen -- an independent confirmation of that field's meaning.

Also recorded: a snapshot taken at the BRIEFING yields 0 runtime objects. Unit
definitions are instantiated at stage load proper, so the snapshot has to wait
for flight; the briefing screen is too early.
2026-08-13 21:14:21 +00:00
aac017dd0d re(challenge): poking unlocks the whole story campaign; MISSION SELECT is story-only
Two results from the running game, one positive and one a clean negative.

POSITIVE: with word A = 0x0001FFFE (stages 1-16) every entry Stage01..Stage16 is
selectable, where the control run had only Stage01 and the rest greyed. Stage16
reads "Lonely Blue Planet - NO RECORD". So any story stage can be launched from
the menu by poking one word, with no save editing at all -- a simpler lever than
the GHAD stage-field patch used until now.

NEGATIVE: with word B = 0x3F (challenge stages 24-29 marked cleared) the list
still saturates at Stage16 -- the cursor stops there and further presses do
nothing. That matches the disc: the debriefing config declares exactly
px_deb_stage01..16, so the list is capped by data, not by the mask. The
challenge missions are NOT reachable through MISSION SELECT, and word B does not
feed it.

Also mapped, without finding the caller: the GP_DIALOG registry (tables.pak #41)
gives DLG_GO_CHALLENGE_MISSION_MENU = 41 and DLG_CHALLENGE_MISSION_AVAILABLE = 42
(0-based, in config order). No raw immediate 41/39/37 appears anywhere in the
GamePart code region, so dialogs are raised through a computed index and the
entry point to GamePart 26 is still unknown. New probe: examples/screen_configs.rs
dumps any tables.pak screen config by substring.
2026-08-13 20:54:57 +00:00
3ef2c438ae re(challenge): MISSION SELECT renders the cleared-stage mask, and a control run
With word A = 2 the screen lists Stage01 SELECTABLE, carrying a High Score and a
Best Time, and Stage02-Stage08 GREYED OUT. One cleared stage, one selectable
entry, at the bit index that names it -- the mask's meaning is now visible on
screen rather than inferred from disassembly.

Two runs, identical navigation, fresh boot each:
  control  word A 0x00000002  -> MISSION SELECT opens normally
  poked    word A 0xFFFFFFFF  -> MmAllocatePhysicalMemoryEx fails on 128 MB,
                                 guest throws, Xenia shows "Disc Read Error"
So last commit's heap failure was caused by the poke, and by a careless one:
0xFFFFFFFF claims stages that do not exist (0, 17, 24-31 in word A). Poking only
real story ids (0x0001FFFE = stages 1-16) does not blow the heap. That the list
screen changes behaviour with the mask is itself evidence word A feeds it.

Getting a trustworthy control took three tries, and every failure produced a
plausible wrong answer rather than an error:
  1. the title-glyph oracle fired during the ATTRACT MOVIE, so A was pressed at
     nothing and the run "reported 0 failures" for a screen it never reached;
  2. Xvfb keeps the previous instance's framebuffer until the new one draws, so
     a screenshot seconds after launch showed the OLD run -- "MAIN MENU reached
     after 1s", against a process that no longer existed;
  3. a single-pixel "is NEW GAME white?" test matched a white LOADING FLASH.
Fixes, all in challenge_probe.sh: blank the root and refuse screen oracles for
the first 40 s; identify a screen by a PATTERN of sampled points (white text AND
the dark panel behind it), not one pixel; require two consecutive samples. The
new oracle was validated offline against all six saved screenshots and accepts
exactly the one real main menu.

General lesson worth the words: a navigation oracle that can only fail by
returning the wrong screen will happily produce a whole run of confident,
meaningless results.
2026-08-13 20:44:27 +00:00
0bc790de52 docs(index): record the file-input pad and the live-write tool 2026-08-13 20:29:18 +00:00
023bb71cfd re(challenge): the cleared-stage mask is CONFIRMED on the running game
Booted the title and read the two gate words live:

    0x828F40C0 = 0x00000002     word A
    0x828F4814 = 0x00000000     word B

Word A = 2 = bit 1. The profile's save is Stage 02 "At Standby" -- stage 01
cleared -- so the mask is exactly one bit, at the index of the one cleared
stage, 1-BASED. Reproduced across two cold boots. That confirms against a known
progress state, on the real game:

  - the singleton is the static object at 0x828F4070, as derived statically;
  - word A is a cleared-stage bitmask (not achievements, not a stage number);
  - bit index = stage id, 1-based, so TimeAttack's REQUIREMENT 16 means "clear
    stage 16" -- the last story mission;
  - word B is the challenge half and is 0 on a story-only profile.

New tools: gpoke.py (live guest-memory WRITE, companion to gmem.py, prints
before/after for every word), pad.py (drives the new --hid=file pad; replaces
vgamepad, which leaked to the host through /dev/uinput), challenge_probe.sh
(one blocking session: boot, wait for title, drive in, poke, screenshot).

Poking both words did NOT surface a challenge entry in EXTRAS -- and that menu
was built 26 s after the poke, so it is not staleness. Entering MISSION SELECT
then failed, but the log names the real cause and it is not the gate:
MmAllocatePhysicalMemoryEx could not satisfy a 128 MB request (parent free
30633/131072 pages), the guest threw a C++ exception, and Xenia surfaced its
generic "Disc Read Error". It is preceded by "BaseHeap::Release failed because
address is not a region start" -- a failed release leaking the range. Recorded
as an emulator heap problem, with the control run (same navigation, no poke)
named as the next step.
2026-08-13 20:28:19 +00:00
f490fecefb re(challenge): both gate words are one ~1880-byte record, and it is not the save
Two results, one of which kills an operational hope I had been carrying.

THE RECORD IS MUCH BIGGER THAN I MEASURED. Reading 0x82175110 to the end: after
the two words, the 8-byte pair, the 184-byte memcpy and the 8 words at +200, it
copies 816 bytes at +232, 816 more at +1048, a sub-object at +1864 and a final
word at +1876. So the record spans +0..~+1880 = singleton +80..+1960, which means
word A is record +0 AND WORD B IS RECORD +1876. That retires last commit's "word
B has no known writer": there is no separate store because the whole record is
copied out, modified and assigned back as a unit (0x8216FF70 -> compare
0x822C3708, assign 0x82170650, then a worker 0x821700A8 -- note 168 decimal, not
hex, which I misread first time -- retrying a commit 0x822C33B8 up to five times).

IT IS NOT THE SAVEGAME, by two independent checks:
 - size: every real save on disk is a 276-byte container deflating to 545 bytes;
   the record is ~1880. It does not fit.
 - files: after many sessions the content tree holds only game0N/savedata and
   game0N/__thumbnail.png per slot plus three Headers/*.header. No second data
   file exists anywhere under the title id.
Also negative: the savegame object is *(*(this+4)) + 304, and the singleton's
holder address 0x828F48B0 is referenced NOWHERE outside the accessor, so this+4
is a different holder -- the save block is not a window into this record.

=> Hand-editing a save cannot unlock the challenge missions. The earlier
savegame-editing win does not extend here.

WHAT WOULD WORK. The singleton is a static object at 0x828F4070 (0x8216F650:
addis 0x828F + addi 16496), so the gate words sit at fixed guest addresses with
no scanning: word A = 0x828F40C0, word B = 0x828F4814. Canary maps guest RAM into
/dev/shm and the project already reads it live, so writing 0xFFFF / 0x3F there
while the title sits on a menu should open all six challenge missions without
playing the campaign -- the route to the last 42 EX units. Untested; needs a run.
2026-08-13 20:06:11 +00:00
20299c05a1 re(challenge): the gate is CLEARED STAGES -- my achievement reading was wrong
Third revision of this claim, and this one has the writer.

Word A (singleton +80) has exactly ONE writer in the image. Scanning all 22
callers of the +80 struct copier (0x82175110) for one that stores to the copy's
word 0 gives a single hit, 0x821C1820, inside GamePart_StageClear:

    if (this+1004 & 0x20000) skip           ; already recorded
    copy local = singleton->progress        ; src = obj+80
    local.word0 |= 1 << (this+84)
    if changed: singleton->set(local)       ; assign + async persist

and this+84 is the STAGE NUMBER, by two independent uses: 0x821C1760 indexes a
20-byte per-stage record array with it (this + (x+7)*20, three words plus an
8-byte timestamp -- the savegame's 16x20 SHAB shape), and 0x821C1EEC/0x821C1924
pass it as the index into the config key "STAGE" (0x820A2540), the debriefing's
px_deb_stage01..16 sprite list.

So a challenge mission's REQUIREMENT n means "stage n has been cleared", and the
<24 / >=24 split is the disc's own stage numbering: story 1-16 and tutorial 18-23
in word A, challenge 24-29 as word B bits 0-5. TimeAttack needs stage 16 -- the
last story mission -- and the other five chain off challenge stages 25-29. That
is what the raw numbers suggested in the first place.

REFUTED: "the bit space is the game's 24 achievements". ACHIEVEMENTS_REQUIREMENTS
having 24 entries and the gate splitting at 24 is a COINCIDENCE -- 24 is also the
first challenge stage's id. Nothing copies the Debriefing's masks (+208/+736/+740)
into +80, which is the check that should have preceded the claim.

The achievement work itself stands and is kept, retitled and rescoped in
structures/achievements.md: the XACH table (.pe 0x8FBCBC, 36-byte records,
1000G self-check), the 24 names/descriptions, the on-disc ACHIEVEMENTS_REQUIREMENTS
list, and the XACHIEVEMENT_DETAILS/XAM read path. It just does not gate the
challenge missions.

Still open: word B (+1956) has no known writer -- GamePart_StageClear's
unconditional 1 << x would land a challenge stage on word A bits 24-29, not word
B, so clearing a challenge mission must be recorded by another path (presumably
the one that also stores its Time/Points record).
2026-08-13 19:54:39 +00:00
90e0e66c7b re(achievements): earned state comes from XAM, and my bit numbering was wrong
Two findings, one of them a correction to the previous commit.

FOUND: GamePart_Debriefing enumerates XACHIEVEMENT_DETAILS from XAM
(0x8218F888). The records are 36 bytes -- confirmed by the arithmetic, not by
eye: the count is a byte count divided by 36 through the 0x38E38E39 multiply-high
magic plus srawi 3. Field +0 is used as a shift amount, field +32 is tested for
0x00020000, and the buffer sits behind a handle that is waited on (0x824AA330
with -1) then closed (0x824AA3E0). That is the XDK struct exactly, with
XACHIEVEMENT_DETAILS_ACHIEVED == 0x20000, via the
XamUserCreateAchievementEnumerator / XEnumerate pattern.

So the title does NOT persist earned achievements itself -- it asks the console.
The lever for achievement-gated content is the emulator's PROFILE data, not the
savegame. Independently, the singleton the challenge gate reads is not the object
holding the save block: re-scanning all 202 accessor call sites WITH function-
boundary stops touches only +80/+1956/+1960/+1964 and none of the known save
offsets (+304/+316/+320/+336/+380/+440). The earlier scan that seemed to find
them had register tracking bleeding into the next function -- a false positive I
am recording rather than quietly dropping.

CORRECTED: the previous commit claimed "bit n <-> achievement n+1" and treated
the Debriefing award pass and the challenge gate as the same bit space, both as
confirmed. Neither holds up:

- the only place the image is observed turning an achievement into a bit does
  1 << dwId with 1-based ids, so bit = the id and bit 0 is unused;
- the "list index is the bit index" step assumed 0x82448338's out-parameter is
  the loop ordinal. It is the child entry's first word out of a 12-byte array,
  and whether that is an ordinal, an id or a name hash is not pinned;
- the Debriefing's masks are its own fields (+208/+736/+740). Nothing observed
  copies them into the singleton's +80, and no writer of that bitmask has been
  found at all.

The requirement-type/id agreement still stands, but it confirms the LIST ORDER,
not the bit numbering. Consequence: if the join holds, TimeAttack's REQUIREMENT
16 is achievement 16 (Night Ravens Patch), not 17 (Solar System Defense Award).
Both read plausibly as a first-challenge gate, which is exactly why it needs
evidence and not the better story.

Also noted: +1960 is a third bitfield on the same singleton, and a what-changed
pass at 0x8219F3A4 diffs it and reports each newly set bit as bit + 64 -- so
there is a wider flag-id space whose bases are not yet worked out.
2026-08-13 19:43:35 +00:00
33ae20896e re(challenge): the gate's bit space is the game's 24 ACHIEVEMENTS
Static only. Last commit left "REQUIREMENT is a bit index into a progress
bitfield" with the space unidentified. It is the achievement space, and both
halves are now readable off the disc and the executable.

- GamePart_Debriefing (0x8218CF38-0x82191B18) awards them: sub_8218F9A8 walks
  the on-disc ACHIEVEMENTS_REQUIREMENTS list (tables.pak #16, schema 744c0519),
  and for entry index n tests bit n, evaluates the entry when clear, and sets
  the bit when satisfied. The list is literally ACHIEVEMENT01..ACHIEVEMENT24 --
  24 entries, which is exactly where the challenge gate splits word A from
  word B.

- The XEX carries the definitions: XACH at .pe 0x8FBCBC, 36-byte records
  {id, name_id, unlocked_desc_id, locked_desc_id, image_id u32, gamerscore u16,
  pad, flags u32, 16 zero bytes}, strings from one XSTR per language (English is
  table #5). tools/xach_dump.py parses it. SELF-CHECK: the 24 gamerscores sum to
  exactly 1000, the retail total -- a wrong stride does not land on a round 1000.

- The two sources agree on ORDER independently: the requirement types
  ShootDownAircrafts 1000/10000, ShootDownShips 100, ShootDownWeight MegaTons,
  GetAllWeapons and GetAllAchievements line up with ids 19-24 exactly as XACH
  names them. So bit n <-> achievement n+1 is evidence, not inference. (Those
  last two are requirement TYPES, not debug cheats, despite how they read.)

- Corollary: TimeAttack's REQUIREMENT 16 -- the one value that sits in direct
  value-before-key adjacency, so it survives IDXD dedup -- is bit 16 =
  achievement 17, "Solar System Defense Award", i.e. finish the story campaign.
  The other five values (25-29) are >= 24 and so index word B, a second flag
  space, plausibly a challenge-clear chain. Still 🟡.

REFUTED, from the last commit: the stores to +1956 in 0x822AF278 / sub_822C8748
are NOT this singleton. That object comes from 0x822CEB30, checks a +2652 flag
and stores string POINTERS at +1956/+2024 -- and a pointer ANDed with 1<<n is
meaningless as a gate. So nothing in the image writes this singleton's +1956
field-wise, and where the mask persists (save vs Xbox profile) is open. XEX
imports are by ordinal, so absent XamUser* strings are not evidence either way.
2026-08-13 19:32:43 +00:00
8ecd70f1bc re(challenge): the unlock is a bit test, and the mission table is on disc
Static only. Bounding each GamePart's code block by its factory creator thunk
(id -> creator recovered for 22 of 24 registrations at 0x8280C000-0x8280F800)
puts GamePart_ChallengeMission at 0x82187E60-0x8218CF10. Resolving every string
that block references gives the screen's config schema, and the record itself is
on disc -- tables.pak schema 54a10697, one copy per language, English entry #64.

Six missions: TimeAttack (record Time), ScoreAttack (record Points) and
Extra01..Extra04, each with MISSION_ID / REQUIREMENT / REQUIREMENT_DESC /
THUMBNAIL / STAGE_DESC / NEW_STAGE and a NORMAL_BUTTON / GRAY_BUTTON pair -- so
the screen always lists all six and greys out what is not earned.

THE GATE (0x82189970-0x821899D8), read off the code:

  REQUIREMENT absent      -> available
  REQUIREMENT == "Always" -> available
  else n = atoi(REQUIREMENT)
      n == 0              -> locked
      n <  24             -> test bit n      of the word at singleton+80
      n >= 24             -> test bit (n-24) of the word at singleton+1956

The singleton is 0x821707C0 (lazy, global 0x828F48BC). So availability is one
bit in a progress bitfield and REQUIREMENT is a bit INDEX -- not a stage number,
score or difficulty.

Values per mission are 🟡: the pool's numeric tokens are 16/25/26/27/29 and
24/28 already appear earlier as font metrics, so they would be deduped -- which
fits 24..29 but IDXD dedup makes positional pairing unsound here, so it is
recorded as a hypothesis, not a table.

Negative: the requirement TEXT is not in GP_CHALLENGE.pak (TextIndex over it =
0 entries; its only prose is embedded font copyright). Its PATH is a per-language
branch the loader does not currently reproduce.

Next: three stores to +1956 sit in 0x822C7DD0 / 0x822C8748, the same region as
the save serializer 0x822C00E8 -- if the bits are save-backed, a hand-written
save unlocks all six challenge missions and the last 42 units become one run.
2026-08-13 19:14:48 +00:00
10a96844ba re(challenge): the EX missions are a MODE, not a stage number
Static-only (no emulator, no pad input). Three findings, each with its own
evidence:

- The disc holds exactly 29 StageResource records in three families --
  S01-S16 story, S18-S23 tutorial (all bg=Original), S24-S29 challenge, plus
  Test. That is 16 + 6 + 6 + 1, matching weapon.tbl's stage01..16 /
  tutorial01..06 / challenge01..06 key set exactly. S17 does not exist.
  GP_CHALLENGE.pak has 0 IDXD objects -- it is the menu screen; challenge
  missions reuse GP_MAIN_GAME_E.pak's records.

- The GamePart id table is at 0x820A1630 (29 ids). Indices are confirmed by
  the image's own RegisterToFactory<N, class silph::GamePart_*> text, not by
  position: GP_CHALLENGE = 26, GP_TUTORIAL = 25, GP_BUNK = 10.

- The stage loader selects its config section from a mission-KIND field at
  object+144: 3 -> EXTRA, 5|6 -> CHALLENGE, else FILE (two independent sites,
  0x82184df0 and 0x82185ed0; two more classify {3,5,6} as one group). The
  constructor sets it to 0 and every write inside the class only clears it,
  and no immediate 3/5/6 store to it exists image-wide -- so the kind is
  supplied by the launching GamePart, never derived from the stage number.

That last point is a mechanism (unproven) for why patching the save's stage
field to 27 kills the load: the record is a challenge stage but the kind stays
FILE. Names an untried, zero-cost discriminator -- try stage 18-23.

Also flagged, not resolved: roster_target says S10 (a STORY stage) still
fields an unharvested unit, which contradicts the "story campaign complete"
claim by one unit.
2026-08-13 18:59:52 +00:00
50 changed files with 5147 additions and 3 deletions

View File

@@ -0,0 +1,55 @@
//! RE probe: the `ACHIEVEMENTS_REQUIREMENTS` config list.
//!
//! `GamePart_Debriefing` (`0x8218CF38`..`0x82191B18`) walks this list after a
//! mission (`sub_8218F9A8`): for each entry it takes the entry's **index** `n`,
//! tests bit `n` of an awarded-mask, and if the bit is clear it evaluates the
//! entry (`0x8218FAB0`) and sets the bit when satisfied. `GamePart_ChallengeMission`
//! then gates each challenge mission on a bit of the same space via its own
//! `REQUIREMENT` key. So this list *is* the achievement/bit numbering.
//!
//! Run: cargo run --release -p sylpheed-formats --example achievements_map -- <disc-root>
use sylpheed_formats::{idxd::IdxdObject, pak::PakArchive};
fn find(h: &[u8], n: &[u8]) -> bool {
h.windows(n.len()).any(|w| w == n)
}
fn main() {
let disc = std::env::args().nth(1).unwrap_or_else(|| {
std::env::var("SYLPHEED_DISC").expect("pass disc root or set SYLPHEED_DISC")
});
let dat = format!("{disc}/dat");
let mut paks: Vec<_> = std::fs::read_dir(&dat)
.expect("dat dir")
.filter_map(|e| e.ok())
.map(|e| e.path())
.filter(|p| p.extension().is_some_and(|x| x == "pak"))
.collect();
paks.sort();
for p in &paks {
let Ok(arc) = PakArchive::open(p) else { continue };
for (i, e) in arc.entries().iter().enumerate() {
let Ok(b) = arc.read(e) else { continue };
if !find(&b, b"ACHIEVEMENTS_REQUIREMENTS") {
continue;
}
let name = p.file_name().unwrap().to_string_lossy().to_string();
match IdxdObject::parse(&b) {
Ok(o) => {
let t = o.tokens();
println!(
"\n===== {name} entry #{i} schema {:08x} {} tokens =====",
o.schema_hash,
t.len()
);
for (j, tok) in t.iter().enumerate() {
println!(" {j:3} {tok}");
}
}
Err(err) => println!("\n===== {name} entry #{i}: not IDXD ({err}) ====="),
}
}
}
}

View File

@@ -0,0 +1,81 @@
//! RE probe: what does the disc say about CHALLENGE / EXTRA missions?
//!
//! The title's stage-config reader (`0x82184b98`..`0x82184e94`) selects one of three
//! config sections by a mode field at `obj+144`:
//! mode == 3 -> "EXTRA"
//! mode == 5 or 6 -> "CHALLENGE"
//! otherwise -> "FILE"
//! so challenge missions are a *mode*, not a separate stage numbering. This probe
//! asks the disc which stage records exist and what GP_CHALLENGE.pak carries.
//!
//! Run: cargo run --release -p sylpheed-formats --example challenge_map -- <disc-root>
use sylpheed_formats::{idxd::IdxdObject, pak::PakArchive};
fn main() {
let disc = std::env::args().nth(1).unwrap_or_else(|| {
std::env::var("SYLPHEED_DISC").expect("pass disc root or set SYLPHEED_DISC")
});
println!("=== 1. StageResource records in GP_MAIN_GAME_E.pak ===");
let arc = PakArchive::open(format!("{disc}/dat/GP_MAIN_GAME_E.pak")).unwrap();
let mut rows = vec![];
for e in arc.entries() {
let Ok(b) = arc.read(e) else { continue };
let Ok(o) = IdxdObject::parse(&b) else { continue };
if o.schema_hash != 0x3c9ae32e {
continue;
}
let t = o.tokens();
let stage = t
.iter()
.find_map(|s| s.strip_prefix("EnumUnit_").map(|x| x.trim_end_matches(".tbl").to_string()))
.unwrap_or("?".into());
let bg = o.get_raw("BackGroundID").unwrap_or("?").to_string();
rows.push((stage, bg, t.len()));
}
rows.sort();
println!(" {} stage records", rows.len());
for (s, bg, n) in &rows {
println!(" {s:8} bg={bg:16} tokens={n}");
}
println!("\n=== 2. GP_CHALLENGE.pak contents ===");
match PakArchive::open(format!("{disc}/dat/GP_CHALLENGE.pak")) {
Ok(ch) => {
let mut by_schema: std::collections::BTreeMap<u32, usize> = Default::default();
let mut all_tokens: Vec<(u32, Vec<String>)> = vec![];
for e in ch.entries() {
let Ok(b) = ch.read(e) else { continue };
let Ok(o) = IdxdObject::parse(&b) else { continue };
*by_schema.entry(o.schema_hash).or_default() += 1;
all_tokens.push((o.schema_hash, o.tokens().iter().map(|s| s.to_string()).collect()));
}
println!(" {} entries, {} IDXD objects", ch.entries().len(), all_tokens.len());
for (h, n) in &by_schema {
println!(" schema {h:08x} x{n}");
}
for (h, t) in all_tokens.iter().take(12) {
println!(" -- {h:08x}: {:?}", &t[..t.len().min(60)]);
}
}
Err(e) => println!(" open failed: {e}"),
}
println!("\n=== 3. tokens mentioning Challenge / EX across the main pak ===");
let mut hits: std::collections::BTreeSet<String> = Default::default();
for e in arc.entries() {
let Ok(b) = arc.read(e) else { continue };
let Ok(o) = IdxdObject::parse(&b) else { continue };
for t in o.tokens() {
let l = t.to_ascii_lowercase();
if l.contains("challenge") || t.ends_with("_EX") || t.contains("_EX4") || t.contains("_EX5") {
hits.insert(format!("{:08x} {t}", o.schema_hash));
}
}
}
for h in hits.iter().take(120) {
println!(" {h}");
}
println!(" ({} distinct)", hits.len());
}

View File

@@ -0,0 +1,78 @@
//! RE probe: the GamePart_ChallengeMission screen config.
//!
//! The class's code range (`0x82187E60`..`0x8218CF10`, bounded by the factory
//! creator thunks either side) references these config keys:
//! MISSIONS, MISSION_ID, NEW_STAGE, REQUIREMENT, REQUIREMENT_DESC, "Always",
//! GRAY_BUTTON, NORMAL_BUTTON, THUMBNAIL, STAGE_DESC, TEXT_STAGE,
//! RECORD_TYPE, "Time", TEXT_RECORD, BASE_INFO
//! i.e. the challenge list is a *config record* with a per-mission REQUIREMENT.
//! It lives in `tables.pak` (one copy per language), not in GP_CHALLENGE.pak.
//!
//! Run: cargo run --release -p sylpheed-formats --example challenge_screen -- <disc-root>
use sylpheed_formats::{idxd::IdxdObject, pak::PakArchive};
const KEYS: &[&str] = &[
"MISSIONS",
"MISSION_ID",
"REQUIREMENT",
"REQUIREMENT_DESC",
"NEW_STAGE",
"GRAY_BUTTON",
"NORMAL_BUTTON",
"RECORD_TYPE",
"THUMBNAIL",
"STAGE_DESC",
];
fn find(h: &[u8], n: &[u8]) -> bool {
h.windows(n.len()).any(|w| w == n)
}
fn main() {
let disc = std::env::args().nth(1).unwrap_or_else(|| {
std::env::var("SYLPHEED_DISC").expect("pass disc root or set SYLPHEED_DISC")
});
let dat = format!("{disc}/dat");
let mut paks: Vec<_> = std::fs::read_dir(&dat)
.expect("dat dir")
.filter_map(|e| e.ok())
.map(|e| e.path())
.filter(|p| p.extension().is_some_and(|x| x == "pak"))
.collect();
paks.sort();
for p in &paks {
let Ok(arc) = PakArchive::open(p) else { continue };
for (i, e) in arc.entries().iter().enumerate() {
let Ok(b) = arc.read(e) else { continue };
if KEYS.iter().filter(|k| find(&b, k.as_bytes())).count() < KEYS.len() {
continue;
}
let name = p.file_name().unwrap().to_string_lossy().to_string();
match IdxdObject::parse(&b) {
Ok(o) => {
let toks = o.tokens();
// language tag: the PATH value, e.g. "dat\GP_CHALLENGE.pak+eng\"
let lang = toks
.iter()
.find(|t| t.contains("GP_CHALLENGE.pak+"))
.map(|t| t.to_string())
.unwrap_or_default();
println!(
"\n===== {name} entry #{i} schema {:08x} {} tokens [{lang}] =====",
o.schema_hash,
toks.len()
);
// Print the pool in order; the record is key/value interleaved and
// IDXD dedupes repeats, so pairing is read by eye, not asserted.
for (j, t) in toks.iter().enumerate() {
println!(" {j:3} {t}");
}
}
Err(e) => println!("\n===== {name} entry #{i}: IDXD parse failed: {e} ====="),
}
}
}
}

View File

@@ -0,0 +1,18 @@
//! RE probe: dump tables.pak screen-config records that mention a given token.
//! Usage: screen_configs <disc-root> <substring>
use sylpheed_formats::{idxd::IdxdObject, pak::PakArchive};
fn main(){
let a:Vec<String>=std::env::args().collect();
let needle=a.get(2).cloned().unwrap_or_else(||"CHALLENGE".into());
let arc=PakArchive::open(format!("{}/dat/tables.pak",a[1])).unwrap();
for (i,e) in arc.entries().iter().enumerate(){
let Ok(b)=arc.read(e) else{continue};
let Ok(o)=IdxdObject::parse(&b) else{continue};
let t=o.tokens();
if !t.iter().any(|s|s.to_lowercase().contains(&needle.to_lowercase())){continue}
let path=t.iter().find(|s|s.contains(".pak+")).cloned().unwrap_or_default();
if !path.is_empty() && !path.contains("+eng"){continue}
println!("\n=== entry #{i} schema {:08x} [{path}] {} tokens ===",o.schema_hash,t.len());
for (j,tok) in t.iter().enumerate(){println!(" {j:3} {tok}");}
}
}

View File

@@ -31,6 +31,8 @@ Promote to a prose `structures/…md` file when a format needs behavioural notes
| Technique | Conf. | Spec | Notes |
|-----------|-------|------|-------|
| Scripted input without a virtual controller | ✅ | [`tools/re-capture/pad.py`](../../tools/re-capture/pad.py) + Canary `--hid=file` | The old `vgamepad` path created its device through `/dev/uinput`, which is **not namespaced** — a pad made inside the container registers with the HOST's input stack, so every scripted press leaked to the desktop. Canary now carries a header-only driver that reads pad state from a **text file** (`--hid=file --pad_file=…`): no kernel device, nothing leaves the container, and analogue values are exact. ⚠️ The trap: 360 menus poll **`XamInputGetKeystrokeEx`**, not `GetState` — with `GetKeystroke` stubbed the pad looks completely dead on a title screen while its own log shows the press arriving. Implemented edge-triggered, no auto-repeat (scripted input wants one event per press). Proven end to end: boot → title → main menu → EXTRAS from the file alone |
| Live guest-memory write | ✅ | [`tools/re-capture/gpoke.py`](../../tools/re-capture/gpoke.py) | Write companion to `gmem.py`, same guest-VA → `/dev/shm` map; prints before/after per word. Used to confirm the challenge gate's cleared-stage mask on the running game |
| Live guest-memory read | ✅ | [`tools/re-capture/gmem.py`](../../tools/re-capture/gmem.py) | Canary backs the guest address space with `/dev/shm/xenia_memory_*`; guest VAs map in through Xenia's fixed table. Full-RAM search ~0.2 s (sparse, `SEEK_DATA`). No debugger, no emulator patch, game keeps running |
| IDXD object layout solver | ✅ | [`tools/re-capture/weapon_runtime.py`](../../tools/re-capture/weapon_runtime.py) | Scan RAM for a class's vtable → enumerate its objects → brute-force `(field, offset, encoding)` against the disc records. Accepts a binding only on **zero** contradictions. Generalizes to any IDXD-backed definition |
| Live entity state, anchored on the definition | ✅ | [`tools/re-capture/own_state.py`](../../tools/re-capture/own_state.py) · [autopilot](autopilot-memory-driven.md) | An undamaged craft holds its definition's own numbers, so a *solved definition field* locates the matching live field without a value scan: definition `HP` (1500) → **hull at `position+0x154`**, confirmed by a trace across a death (30/60/90 per hit, negative at 0). Reusable for any live counter whose maximum the definition carries |
@@ -41,8 +43,7 @@ Promote to a prose `structures/…md` file when a format needs behavioural notes
## Functions / code paths
_None documented yet — populated during the dynamic-RE phase._
| Function | Conf. | Reimpl. | Summary |
|----------|-------|---------|---------|
| — | — | — | — |
| Achievement table + XAM read path | ✅/🟡 | [achievements](structures/achievements.md) | The XEX's `XACH` resource (`.pe` `0x8FBCBC`, 36-byte records) defines **24 achievements summing to 1000G** — the retail total, which self-checks the stride and field offsets. `GamePart_Debriefing` (`0x8218CF38``0x82191B18`) does two things: it walks the on-disc `ACHIEVEMENTS_REQUIREMENTS` list (`tables.pak` #16, entries `ACHIEVEMENT01…24`, in achievement-id order — its `ShootDownAircrafts`/`ShootDownShips`/`ShootDownWeight`/`GetAllWeapons`/`GetAllAchievements` types line up with ids 1924 exactly as `XACH` names them), evaluating each and setting a bit; **and it enumerates `XACHIEVEMENT_DETAILS` from XAM** — 36-byte records confirmed by the `0x38E38E39`+`srawi 3` divide-by-36, `dwId` at `+0`, `dwFlags & 0x00020000` (`…_ACHIEVED`) at `+32`, behind a waited-then-closed async handle. **So earned state comes from the console profile, not the 545-byte save** — and the masks it builds are `1 << dwId`, i.e. **bit = the 1-based id**. **The challenge missions do NOT gate on this** — an earlier claim here, refuted by finding `+80`'s sole writer: it is `GamePart_StageClear` setting `1 << stage`, so that word is a *cleared-stage* mask and the shared "24" was a coincidence. `GetAllAchievements`/`GetAllWeapons` are requirement **types**, not debug cheats |
| Challenge-mission gate + stage-config switch | ✅ | [challenge-mission-gate](challenge-mission-gate.md) | **`GamePart_ChallengeMission` gates each of the six challenge missions on a CLEARED-STAGE bit**: `REQUIREMENT` absent or `"Always"` → available, else `n = atoi(v)` and it tests bit `n` of singleton `+80` (`n < 24`) or bit `n-24` of `+1956` (`n ≥ 24`) — which is the disc's own stage numbering (story 116 and tutorial 1823 below 24, challenge 2429 above). Word A's **sole writer** is `0x821C1820` in `GamePart_StageClear`, doing `1 << (this+84)` where `this+84` is the stage number (it also indexes a 20-byte per-stage record array and the debriefing `STAGE` sprite list). So `TimeAttack` needs **stage 16** — the last story mission — and the rest chain off challenge stages 2529. The six-mission table is on disc in `tables.pak` (schema `54a10697`). Separately, the stage loader picks its config section from a **mission-kind field at `object+144`**: `3``EXTRA`, `5`/`6``CHALLENGE`, anything else → `FILE`; two further sites treat `{3,5,6}` as one class. Constructed as `0` (`sub_821783D8`) and only ever *cleared* inside the class, so the kind comes from the launching GamePart, **not** from the stage number — which is a mechanism (🟡, unproven) for why patching the save's stage field to a challenge stage kills the load. Same note carries the **GamePart id table** (`0x820A1630`, 29 ids, `GP_CHALLENGE` = 26, cross-checked against the image's own `RegisterToFactory<26, …>` text) and the disc's **three stage families**`S01``S16` story, `S18``S23` tutorial, `S24``S29` challenge, plus `Test`, matching `weapon.tbl`'s 16 + 6 + 6 key set exactly. `GP_CHALLENGE.pak` holds **0 IDXD objects** — it is the menu screen; challenge missions reuse `GP_MAIN_GAME_E.pak`'s stage records |

View File

@@ -0,0 +1,76 @@
XACH @0x8FBCBC 24 achievements (string table #5 of 7)
id 1 | bit 0 | 20G | Space Combat Award
unlocked: Received after your first space battle in the Glasner Training Area.
locked : Awarded for participating in fighter combat in outer space.
id 2 | bit 1 | 20G | Schlos Base Defense Award
unlocked: Received for stopping the missile attack on Schlos Base.
locked : Awarded for stopping enemy attacks on Schlos Base.
id 3 | bit 2 | 20G | Aegis of the People Medal
unlocked: Received for escorting all 7 refugee ships to safety.
locked : Awarded for escorting the 7 refugee ships to safety.
id 4 | bit 3 | 20G | TCAF Luna Medal
unlocked: Received for bravely helping the fleet escape during the invasion of the Matisse System.
locked : Awarded for bravery beyond the call of duty to escort allied vessels to safety.
id 5 | bit 4 | 40G | TCAF Mars Medal
unlocked: Received for bravery beyond the call of duty during the escape from the Matisse System.
locked : Awarded for bravery beyond the call of duty under fierce enemy attacks.
id 6 | bit 5 | 50G | Soldier's Charm Amulet
unlocked: Given to you by Raymond as a token of his trust.
locked : Given to you by Raymond as a token of his trust.
id 7 | bit 6 | 20G | White Griffons Patch
unlocked: Received by the commander and pilots of the White Griffon Squadron when it is formed.
locked : Awarded to the commander and pilots of the White Griffon Squadron when it is formed.
id 8 | bit 7 | 30G | TCAF Jupiter Medal
unlocked: Received for bravery during the enemy's attack on the Alberti System.
locked : Awarded for bravery beyond the call of duty during withdrawal of the allied fleet.
id 9 | bit 8 | 40G | Furious Pursuit Badge
unlocked: Received for continuing attacks on the enemy and shooting down a large number of ships.
locked : Awarded for diligently shooting down large numbers of enemy ships.
id 10 | bit 9 | 30G | Solo Aerospace Combat Award
unlocked: Received for descending into Acheron's atmosphere and engaging in combat alone.
locked : Awarded for descending into the atmosphere and engaging in combat alone.
id 11 | bit 10 | 30G | Operation Nebula Blaze Award
unlocked: Received for completing the extremely hazardous Operation Nebula Blaze.
locked : Awarded for fulfilling duty and fighting bravely in this difficult operation.
id 12 | bit 11 | 40G | Guilty Roses Patch
unlocked: Received for repelling the Guilty Roses Squadron.
locked : Awarded for repelling the enemy Guilty Roses Squadron.
id 13 | bit 12 | 30G | Super Battleship Slayer Patch
unlocked: Received for shooting down the second S battleship.
locked : Awarded for shooting down the second S battleship supporting the enemy fleet.
id 14 | bit 13 | 40G | TCAF Terra Medal
unlocked: Received for shielding the fleet and seeing that all ships safely fled the Ingres System.
locked : Awarded for shielding the fleet to allow its safe escape.
id 15 | bit 14 | 40G | Hellfires Patch
unlocked: Received for repelling the enemy Hellfire Squadron.
locked : Awarded for repelling the enemy Hellfire Squadron.
id 16 | bit 15 | 50G | Night Ravens Patch
unlocked: Received for challenging and eradicating the Night Raven Squadron.
locked : Awarded for challenging and eradicating the enemy Night Raven Squadron.
id 17 | bit 16 | 40G | Solar System Defense Award
unlocked: Received for great achievements during the campaign to defend the Solar System.
locked : Awarded for great achievement during the campaign to defend the Solar System.
id 18 | bit 17 | 40G | Special Operations Medal
unlocked: Received for heroically destroying the Prometheus Driver.
locked : Awarded for heroism in destroying the enemy's main weapon.
id 19 | bit 18 | 30G | 1,000 Units Destroyed Medal
unlocked: Received for shooting down 1,000 enemy fighters and attackers in combat.
locked : Awarded for shooting down 1,000 enemy fighters and attackers in space combat.
id 20 | bit 19 | 70G | 10,000 Units Destroyed Medal
unlocked: Received for shooting down 10,000 enemy fighters and attackers in combat.
locked : Awarded for shooting down 10,000 enemy fighters and attackers in space combat.
id 21 | bit 20 | 50G | Ship Hunter Award
unlocked: Received for shooting down 100 enemy warships in combat.
locked : Awarded for shooting down 100 warships in space combat.
id 22 | bit 21 | 70G | Gigaton Club Patch
unlocked: Received for shooting down enemy vessels with a total weight of one gigaton.
locked : Awarded for downing several enemy vessels with a combined weight of one gigaton.
id 23 | bit 22 | 80G | Weapon Lord Patch
unlocked: Received after you collect all usable equipment for the Delta Saber.
locked : Awarded for collecting all usable equipment for the Delta Saber.
id 24 | bit 23 | 100G | TCAF Pilot's Commendation
unlocked: Commemmorates you as one of the greatest pilots in history.
locked : Awarded to the greatest pilots in the TCAF.
total gamerscore = 1000 [OK: retail total]

View File

@@ -0,0 +1,5 @@
input,roll_deg_s,yaw_deg_s,pitch_deg_s
rx+,0.0,0.0,0.0
ry+,0.0,0.0,0.0
LB,0.0,0.0,0.0
RB,0.0,0.0,0.0
1 input roll_deg_s yaw_deg_s pitch_deg_s
2 rx+ 0.0 0.0 0.0
3 ry+ 0.0 0.0 0.0
4 LB 0.0 0.0 0.0
5 RB 0.0 0.0 0.0

View File

@@ -0,0 +1,7 @@
input,roll_deg_s,yaw_deg_s,pitch_deg_s
lx+,209.8,0.87,10.07
ly+,0.0,154.12,0.0
rx+,0.0,0.0,0.0
ry+,161.07,86.98,37.06
LB,0.0,0.0,0.0
RB,0.0,0.0,0.0
1 input roll_deg_s yaw_deg_s pitch_deg_s
2 lx+ 209.8 0.87 10.07
3 ly+ 0.0 154.12 0.0
4 rx+ 0.0 0.0 0.0
5 ry+ 161.07 86.98 37.06
6 LB 0.0 0.0 0.0
7 RB 0.0 0.0 0.0

Binary file not shown.

After

Width:  |  Height:  |  Size: 921 KiB

View File

@@ -0,0 +1,96 @@
=== 1. StageResource records in GP_MAIN_GAME_E.pak ===
29 stage records
S01 bg=Lebendorf tokens=52
S02 bg=Lebendorf tokens=51
S03 bg=Planet_Lebendorf tokens=51
S04 bg=Lebendorf_far tokens=54
S05 bg=Lebendorf_far tokens=54
S06 bg=Hargenteen tokens=51
S07 bg=Hargenteen tokens=53
S08 bg=Barch tokens=59
S09 bg=Acheron tokens=53
S10 bg=Acheron tokens=53
S11 bg=Anastasis tokens=53
S12 bg=Hargenteen tokens=53
S13 bg=Hargenteen tokens=56
S14 bg=Earth tokens=59
S15 bg=Earth tokens=53
S16 bg=PD tokens=53
S18 bg=Original tokens=43
S19 bg=Original tokens=43
S20 bg=Original tokens=43
S21 bg=Original tokens=43
S22 bg=Original tokens=50
S23 bg=Original tokens=43
S24 bg=Anastasis tokens=53
S25 bg=Hargenteen tokens=53
S26 bg=Hargenteen tokens=53
S27 bg=Planet_Lebendorf tokens=53
S28 bg=Lebendorf tokens=54
S29 bg=Earth tokens=53
Test bg=Earth tokens=43
=== 2. GP_CHALLENGE.pak contents ===
151 entries, 0 IDXD objects
=== 3. tokens mentioning Challenge / EX across the main pak ===
033b5b7e pgmsg_challenge1.t32
033b5b7e pgmsg_challenge2.t32
033b5b7e pgmsg_challenge3.t32
033b5b7e pgmsg_challenge4.t32
033b5b7e pgmsg_challenge5.t32
033b5b7e pgmsg_challenge6.t32
35b8dc67 UN_e001_ADAN_Elan_EX4
35b8dc67 UN_e007_ADAN_Turret_EX4
35b8dc67 UN_e008_ADAN_TurretPlus_EX4
35b8dc67 UN_e010_ADAN_Attacker_S_EX4
35b8dc67 UN_e011_ADAN_Attacker_B_EX4
35b8dc67 UN_e107_ADAN_AAFrigate_EX4
35b8dc67 UN_f001_TCAF_DeltaSaber_T_EX5
35b8dc67 UN_f001_TCAF_DeltaSaber_T_EX5_el
35b8dc67 UN_f003_TCAF_ArrowHead_EX4
35b8dc67 UN_f003_TCAF_ArrowHead_EX5
35b8dc67 UN_f104_TCAF_Battleship_EX5
35b8dc67 UN_f105_TCAF_Cruiser_EX5
3c5b0549 UN_e107_ADAN_AAFrigate_EX4
3c5b0549 UN_f104_TCAF_Battleship_EX5
3c5b0549 UN_f105_TCAF_Cruiser_EX5
3c5b0549 UnitName_UN_e107_ADAN_AAFrigate_EX4
3c5b0549 UnitName_UN_f104_TCAF_Battleship_EX5
3c5b0549 UnitName_UN_f105_TCAF_Cruiser_EX5
3c5b0549 Weapon_TCAF_Ship_AAGun_EX5
3c9ae32e EnumWeapon_EX5.tbl
43faa517 UN_e001_ADAN_Elan_EX4
43faa517 UN_e007_ADAN_Turret_EX4
43faa517 UN_e008_ADAN_TurretPlus_EX4
43faa517 UN_e010_ADAN_Attacker_S_EX4
43faa517 UN_e011_ADAN_Attacker_B_EX4
43faa517 UN_f001_TCAF_DeltaSaber_T_EX5
43faa517 UN_f001_TCAF_DeltaSaber_T_EX5_el
43faa517 UN_f003_TCAF_ArrowHead_EX4
43faa517 UN_f003_TCAF_ArrowHead_EX5
43faa517 UnitName_UN_e001_ADAN_Elan_EX4
43faa517 UnitName_UN_e007_ADAN_Turret_EX4
43faa517 UnitName_UN_e008_ADAN_TurretPlus_EX4
43faa517 UnitName_UN_e010_ADAN_Attacker_S_EX4
43faa517 UnitName_UN_e011_ADAN_Attacker_B_EX4
43faa517 UnitName_UN_f001_TCAF_DeltaSaber_T_EX5
43faa517 UnitName_UN_f001_TCAF_DeltaSaber_T_EX5_el
43faa517 UnitName_UN_f003_TCAF_ArrowHead_EX4
43faa517 UnitName_UN_f003_TCAF_ArrowHead_EX5
659aff47 UN_e001_ADAN_Elan_EX4
659aff47 UN_e007_ADAN_Turret_EX4
659aff47 UN_e008_ADAN_TurretPlus_EX4
659aff47 UN_e010_ADAN_Attacker_S_EX4
659aff47 UN_e011_ADAN_Attacker_B_EX4
659aff47 UN_e107_ADAN_AAFrigate_EX4
659aff47 UN_f003_TCAF_ArrowHead_EX4
6ab4825a WeaponCannonName_TCAF_Ship_AAGun_EX5
6ab4825a Weapon_TCAF_Ship_AAGun_EX5
6b9b000d UN_f001_TCAF_DeltaSaber_T_EX5
6b9b000d UN_f001_TCAF_DeltaSaber_T_EX5_el
6b9b000d UN_f003_TCAF_ArrowHead_EX5
6b9b000d UN_f104_TCAF_Battleship_EX5
6b9b000d UN_f105_TCAF_Cruiser_EX5
ffbc19c2 Weapon_TCAF_Ship_AAGun_EX5
(59 distinct)

View File

@@ -0,0 +1,750 @@
===== tables.pak entry #60 schema 54a10697 123 tokens [dat\GP_CHALLENGE.pak+fra\] =====
0 xBASE_INFO
1 0x061031
2 VERSION
3 dat\GP_CHALLENGE.pak+fra\
4 PATH
5 strings.tbl
6 STRINGS
7 py_menu_scr.prt
8 MENU
9 CHIPS
10 py_menu_n_btn1.prt
11 Button_TimeAttack
12 py_menu_n_btn2.prt
13 Button_ScoreAttack
14 py_menu_n_btn3.prt
15 Button_Extra01
16 py_menu_n_btn4.prt
17 Button_Extra02
18 py_menu_n_btn5.prt
19 Button_Extra03
20 py_menu_n_btn6.prt
21 Button_Extra04
22 py_menu_b_btn1.prt
23 Button_TimeAttack_Gray
24 py_menu_b_btn2.prt
25 Button_ScoreAttack_Gray
26 py_menu_b_btn3.prt
27 Button_Extra01_Gray
28 py_menu_b_btn4.prt
29 Button_Extra02_Gray
30 py_menu_b_btn5.prt
31 Button_Extra03_Gray
32 py_menu_b_btn6.prt
33 Button_Extra04_Gray
34 py_menu_challenge1.t32
35 Thumbnail_TimeAttack
36 py_menu_challenge2.t32
37 Thumbnail_ScoreAttack
38 py_menu_challenge3.t32
39 Thumbnail_Extra01
40 py_menu_challenge4.t32
41 Thumbnail_Extra02
42 py_menu_challenge5.t32
43 Thumbnail_Extra03
44 py_menu_challenge6.t32
45 Thumbnail_Extra04
46 py_menu_str_time.t32
47 Record_Time
48 py_menu_str_point.t32
49 Record_Point
50 py_menu_str_p.t32
51 Unit_Point
52 py_menu_new.prt
53 New_Stage
54 FONTS
55 Font_Desc
56 Font_Record
57 CONTM___.TTF
58 FONT
59 24
60 HEIGHT
61 28
62 Desc_Stage
63 580,400
64 LOCATE
65 8
66 LINE_SPACE
67 Desc_Time
68 1048,608,R
69 0
70 INDEX
71 Desc_Points
72 1032,608,R
73 UNIT
74 New_Stage01
75 New_Stage02
76 0,60
77 INDEX_ADJUST
78 New_Stage03
79 0,120
80 New_Stage04
81 0,180
82 New_Stage05
83 0,240
84 New_Stage06
85 0,300
86 MISSIONS
87 TimeAttack
88 ScoreAttack
89 Extra01
90 Extra02
91 Extra03
92 Extra04
93 MISSION_ID
94 Time
95 RECORD_TYPE
96 16
97 REQUIREMENT
98 NORMAL_BUTTON
99 GRAY_BUTTON
100 THUMBNAIL
101 TimeAttackRequirement
102 REQUIREMENT_DESC
103 TimeAttackStageDesc
104 STAGE_DESC
105 TEXT_STAGE
106 TEXT_RECORD
107 NEW_STAGE
108 25
109 Points
110 ScoreAttackRequirement
111 ScoreAttackStageDesc
112 26
113 Extra01Requirement
114 ExtraStage01Desc
115 27
116 Extra02Requirement
117 ExtraStage02Desc
118 Extra03Requirement
119 ExtraStage03Desc
120 29
121 Extra04Requirement
122 ExtraStage04Desc
===== tables.pak entry #64 schema 54a10697 123 tokens [dat\GP_CHALLENGE.pak+eng\] =====
0 xBASE_INFO
1 0x061031
2 VERSION
3 dat\GP_CHALLENGE.pak+eng\
4 PATH
5 strings.tbl
6 STRINGS
7 py_menu_scr.prt
8 MENU
9 CHIPS
10 py_menu_n_btn1.prt
11 Button_TimeAttack
12 py_menu_n_btn2.prt
13 Button_ScoreAttack
14 py_menu_n_btn3.prt
15 Button_Extra01
16 py_menu_n_btn4.prt
17 Button_Extra02
18 py_menu_n_btn5.prt
19 Button_Extra03
20 py_menu_n_btn6.prt
21 Button_Extra04
22 py_menu_b_btn1.prt
23 Button_TimeAttack_Gray
24 py_menu_b_btn2.prt
25 Button_ScoreAttack_Gray
26 py_menu_b_btn3.prt
27 Button_Extra01_Gray
28 py_menu_b_btn4.prt
29 Button_Extra02_Gray
30 py_menu_b_btn5.prt
31 Button_Extra03_Gray
32 py_menu_b_btn6.prt
33 Button_Extra04_Gray
34 py_menu_challenge1.t32
35 Thumbnail_TimeAttack
36 py_menu_challenge2.t32
37 Thumbnail_ScoreAttack
38 py_menu_challenge3.t32
39 Thumbnail_Extra01
40 py_menu_challenge4.t32
41 Thumbnail_Extra02
42 py_menu_challenge5.t32
43 Thumbnail_Extra03
44 py_menu_challenge6.t32
45 Thumbnail_Extra04
46 py_menu_str_time.t32
47 Record_Time
48 py_menu_str_point.t32
49 Record_Point
50 py_menu_str_p.t32
51 Unit_Point
52 py_menu_new.prt
53 New_Stage
54 FONTS
55 Font_Desc
56 Font_Record
57 CONTM___.TTF
58 FONT
59 24
60 HEIGHT
61 28
62 Desc_Stage
63 580,400
64 LOCATE
65 8
66 LINE_SPACE
67 Desc_Time
68 1048,608,R
69 0
70 INDEX
71 Desc_Points
72 1032,608,R
73 UNIT
74 New_Stage01
75 New_Stage02
76 0,60
77 INDEX_ADJUST
78 New_Stage03
79 0,120
80 New_Stage04
81 0,180
82 New_Stage05
83 0,240
84 New_Stage06
85 0,300
86 MISSIONS
87 TimeAttack
88 ScoreAttack
89 Extra01
90 Extra02
91 Extra03
92 Extra04
93 MISSION_ID
94 Time
95 RECORD_TYPE
96 16
97 REQUIREMENT
98 NORMAL_BUTTON
99 GRAY_BUTTON
100 THUMBNAIL
101 TimeAttackRequirement
102 REQUIREMENT_DESC
103 TimeAttackStageDesc
104 STAGE_DESC
105 TEXT_STAGE
106 TEXT_RECORD
107 NEW_STAGE
108 25
109 Points
110 ScoreAttackRequirement
111 ScoreAttackStageDesc
112 26
113 Extra01Requirement
114 ExtraStage01Desc
115 27
116 Extra02Requirement
117 ExtraStage02Desc
118 Extra03Requirement
119 ExtraStage03Desc
120 29
121 Extra04Requirement
122 ExtraStage04Desc
===== tables.pak entry #67 schema 54a10697 123 tokens [dat\GP_CHALLENGE.pak+deu\] =====
0 xBASE_INFO
1 0x061031
2 VERSION
3 dat\GP_CHALLENGE.pak+deu\
4 PATH
5 strings.tbl
6 STRINGS
7 py_menu_scr.prt
8 MENU
9 CHIPS
10 py_menu_n_btn1.prt
11 Button_TimeAttack
12 py_menu_n_btn2.prt
13 Button_ScoreAttack
14 py_menu_n_btn3.prt
15 Button_Extra01
16 py_menu_n_btn4.prt
17 Button_Extra02
18 py_menu_n_btn5.prt
19 Button_Extra03
20 py_menu_n_btn6.prt
21 Button_Extra04
22 py_menu_b_btn1.prt
23 Button_TimeAttack_Gray
24 py_menu_b_btn2.prt
25 Button_ScoreAttack_Gray
26 py_menu_b_btn3.prt
27 Button_Extra01_Gray
28 py_menu_b_btn4.prt
29 Button_Extra02_Gray
30 py_menu_b_btn5.prt
31 Button_Extra03_Gray
32 py_menu_b_btn6.prt
33 Button_Extra04_Gray
34 py_menu_challenge1.t32
35 Thumbnail_TimeAttack
36 py_menu_challenge2.t32
37 Thumbnail_ScoreAttack
38 py_menu_challenge3.t32
39 Thumbnail_Extra01
40 py_menu_challenge4.t32
41 Thumbnail_Extra02
42 py_menu_challenge5.t32
43 Thumbnail_Extra03
44 py_menu_challenge6.t32
45 Thumbnail_Extra04
46 py_menu_str_time.t32
47 Record_Time
48 py_menu_str_point.t32
49 Record_Point
50 py_menu_str_p.t32
51 Unit_Point
52 py_menu_new.prt
53 New_Stage
54 FONTS
55 Font_Desc
56 Font_Record
57 CONTM___.TTF
58 FONT
59 24
60 HEIGHT
61 28
62 Desc_Stage
63 580,400
64 LOCATE
65 8
66 LINE_SPACE
67 Desc_Time
68 1048,608,R
69 0
70 INDEX
71 Desc_Points
72 1032,608,R
73 UNIT
74 New_Stage01
75 New_Stage02
76 0,60
77 INDEX_ADJUST
78 New_Stage03
79 0,120
80 New_Stage04
81 0,180
82 New_Stage05
83 0,240
84 New_Stage06
85 0,300
86 MISSIONS
87 TimeAttack
88 ScoreAttack
89 Extra01
90 Extra02
91 Extra03
92 Extra04
93 MISSION_ID
94 Time
95 RECORD_TYPE
96 16
97 REQUIREMENT
98 NORMAL_BUTTON
99 GRAY_BUTTON
100 THUMBNAIL
101 TimeAttackRequirement
102 REQUIREMENT_DESC
103 TimeAttackStageDesc
104 STAGE_DESC
105 TEXT_STAGE
106 TEXT_RECORD
107 NEW_STAGE
108 25
109 Points
110 ScoreAttackRequirement
111 ScoreAttackStageDesc
112 26
113 Extra01Requirement
114 ExtraStage01Desc
115 27
116 Extra02Requirement
117 ExtraStage02Desc
118 Extra03Requirement
119 ExtraStage03Desc
120 29
121 Extra04Requirement
122 ExtraStage04Desc
===== tables.pak entry #68 schema 54a10697 123 tokens [dat\GP_CHALLENGE.pak+ita\] =====
0 xBASE_INFO
1 0x061031
2 VERSION
3 dat\GP_CHALLENGE.pak+ita\
4 PATH
5 strings.tbl
6 STRINGS
7 py_menu_scr.prt
8 MENU
9 CHIPS
10 py_menu_n_btn1.prt
11 Button_TimeAttack
12 py_menu_n_btn2.prt
13 Button_ScoreAttack
14 py_menu_n_btn3.prt
15 Button_Extra01
16 py_menu_n_btn4.prt
17 Button_Extra02
18 py_menu_n_btn5.prt
19 Button_Extra03
20 py_menu_n_btn6.prt
21 Button_Extra04
22 py_menu_b_btn1.prt
23 Button_TimeAttack_Gray
24 py_menu_b_btn2.prt
25 Button_ScoreAttack_Gray
26 py_menu_b_btn3.prt
27 Button_Extra01_Gray
28 py_menu_b_btn4.prt
29 Button_Extra02_Gray
30 py_menu_b_btn5.prt
31 Button_Extra03_Gray
32 py_menu_b_btn6.prt
33 Button_Extra04_Gray
34 py_menu_challenge1.t32
35 Thumbnail_TimeAttack
36 py_menu_challenge2.t32
37 Thumbnail_ScoreAttack
38 py_menu_challenge3.t32
39 Thumbnail_Extra01
40 py_menu_challenge4.t32
41 Thumbnail_Extra02
42 py_menu_challenge5.t32
43 Thumbnail_Extra03
44 py_menu_challenge6.t32
45 Thumbnail_Extra04
46 py_menu_str_time.t32
47 Record_Time
48 py_menu_str_point.t32
49 Record_Point
50 py_menu_str_p.t32
51 Unit_Point
52 py_menu_new.prt
53 New_Stage
54 FONTS
55 Font_Desc
56 Font_Record
57 CONTM___.TTF
58 FONT
59 24
60 HEIGHT
61 28
62 Desc_Stage
63 580,400
64 LOCATE
65 8
66 LINE_SPACE
67 Desc_Time
68 1048,608,R
69 0
70 INDEX
71 Desc_Points
72 1032,608,R
73 UNIT
74 New_Stage01
75 New_Stage02
76 0,60
77 INDEX_ADJUST
78 New_Stage03
79 0,120
80 New_Stage04
81 0,180
82 New_Stage05
83 0,240
84 New_Stage06
85 0,300
86 MISSIONS
87 TimeAttack
88 ScoreAttack
89 Extra01
90 Extra02
91 Extra03
92 Extra04
93 MISSION_ID
94 Time
95 RECORD_TYPE
96 16
97 REQUIREMENT
98 NORMAL_BUTTON
99 GRAY_BUTTON
100 THUMBNAIL
101 TimeAttackRequirement
102 REQUIREMENT_DESC
103 TimeAttackStageDesc
104 STAGE_DESC
105 TEXT_STAGE
106 TEXT_RECORD
107 NEW_STAGE
108 25
109 Points
110 ScoreAttackRequirement
111 ScoreAttackStageDesc
112 26
113 Extra01Requirement
114 ExtraStage01Desc
115 27
116 Extra02Requirement
117 ExtraStage02Desc
118 Extra03Requirement
119 ExtraStage03Desc
120 29
121 Extra04Requirement
122 ExtraStage04Desc
===== tables.pak entry #74 schema 54a10697 123 tokens [dat\GP_CHALLENGE.pak+jpn\] =====
0 BASE_INFO
1 0x061031
2 VERSION
3 dat\GP_CHALLENGE.pak+jpn\
4 PATH
5 strings.tbl
6 STRINGS
7 py_menu_scr.prt
8 MENU
9 CHIPS
10 py_menu_n_btn1.prt
11 Button_TimeAttack
12 py_menu_n_btn2.prt
13 Button_ScoreAttack
14 py_menu_n_btn3.prt
15 Button_Extra01
16 py_menu_n_btn4.prt
17 Button_Extra02
18 py_menu_n_btn5.prt
19 Button_Extra03
20 py_menu_n_btn6.prt
21 Button_Extra04
22 py_menu_b_btn1.prt
23 Button_TimeAttack_Gray
24 py_menu_b_btn2.prt
25 Button_ScoreAttack_Gray
26 py_menu_b_btn3.prt
27 Button_Extra01_Gray
28 py_menu_b_btn4.prt
29 Button_Extra02_Gray
30 py_menu_b_btn5.prt
31 Button_Extra03_Gray
32 py_menu_b_btn6.prt
33 Button_Extra04_Gray
34 py_menu_challenge1.t32
35 Thumbnail_TimeAttack
36 py_menu_challenge2.t32
37 Thumbnail_ScoreAttack
38 py_menu_challenge3.t32
39 Thumbnail_Extra01
40 py_menu_challenge4.t32
41 Thumbnail_Extra02
42 py_menu_challenge5.t32
43 Thumbnail_Extra03
44 py_menu_challenge6.t32
45 Thumbnail_Extra04
46 py_menu_str_time.t32
47 Record_Time
48 py_menu_str_point.t32
49 Record_Point
50 py_menu_str_p.t32
51 Unit_Point
52 py_menu_new.prt
53 New_Stage
54 FONTS
55 Font_Desc
56 Font_Record
57 DFSOGE5.TTC
58 FONT
59 24
60 HEIGHT
61 CONTM___.TTF
62 28
63 Desc_Stage
64 580,400
65 LOCATE
66 8
67 LINE_SPACE
68 Desc_Time
69 1048,608,R
70 INDEX
71 Desc_Points
72 1032,608,R
73 UNIT
74 New_Stage01
75 New_Stage02
76 0,60
77 INDEX_ADJUST
78 New_Stage03
79 0,120
80 New_Stage04
81 0,180
82 New_Stage05
83 0,240
84 New_Stage06
85 0,300
86 MISSIONS
87 TimeAttack
88 ScoreAttack
89 Extra01
90 Extra02
91 Extra03
92 Extra04
93 MISSION_ID
94 Time
95 RECORD_TYPE
96 16
97 REQUIREMENT
98 NORMAL_BUTTON
99 GRAY_BUTTON
100 THUMBNAIL
101 TimeAttackRequirement
102 REQUIREMENT_DESC
103 TimeAttackStageDesc
104 STAGE_DESC
105 TEXT_STAGE
106 TEXT_RECORD
107 NEW_STAGE
108 25
109 Points
110 ScoreAttackRequirement
111 ScoreAttackStageDesc
112 26
113 Extra01Requirement
114 ExtraStage01Desc
115 27
116 Extra02Requirement
117 ExtraStage02Desc
118 Extra03Requirement
119 ExtraStage03Desc
120 29
121 Extra04Requirement
122 ExtraStage04Desc
===== tables.pak entry #75 schema 54a10697 123 tokens [dat\GP_CHALLENGE.pak+esp\] =====
0 xBASE_INFO
1 0x061031
2 VERSION
3 dat\GP_CHALLENGE.pak+esp\
4 PATH
5 strings.tbl
6 STRINGS
7 py_menu_scr.prt
8 MENU
9 CHIPS
10 py_menu_n_btn1.prt
11 Button_TimeAttack
12 py_menu_n_btn2.prt
13 Button_ScoreAttack
14 py_menu_n_btn3.prt
15 Button_Extra01
16 py_menu_n_btn4.prt
17 Button_Extra02
18 py_menu_n_btn5.prt
19 Button_Extra03
20 py_menu_n_btn6.prt
21 Button_Extra04
22 py_menu_b_btn1.prt
23 Button_TimeAttack_Gray
24 py_menu_b_btn2.prt
25 Button_ScoreAttack_Gray
26 py_menu_b_btn3.prt
27 Button_Extra01_Gray
28 py_menu_b_btn4.prt
29 Button_Extra02_Gray
30 py_menu_b_btn5.prt
31 Button_Extra03_Gray
32 py_menu_b_btn6.prt
33 Button_Extra04_Gray
34 py_menu_challenge1.t32
35 Thumbnail_TimeAttack
36 py_menu_challenge2.t32
37 Thumbnail_ScoreAttack
38 py_menu_challenge3.t32
39 Thumbnail_Extra01
40 py_menu_challenge4.t32
41 Thumbnail_Extra02
42 py_menu_challenge5.t32
43 Thumbnail_Extra03
44 py_menu_challenge6.t32
45 Thumbnail_Extra04
46 py_menu_str_time.t32
47 Record_Time
48 py_menu_str_point.t32
49 Record_Point
50 py_menu_str_p.t32
51 Unit_Point
52 py_menu_new.prt
53 New_Stage
54 FONTS
55 Font_Desc
56 Font_Record
57 CONTM___.TTF
58 FONT
59 24
60 HEIGHT
61 28
62 Desc_Stage
63 580,400
64 LOCATE
65 8
66 LINE_SPACE
67 Desc_Time
68 1048,608,R
69 0
70 INDEX
71 Desc_Points
72 1032,608,R
73 UNIT
74 New_Stage01
75 New_Stage02
76 0,60
77 INDEX_ADJUST
78 New_Stage03
79 0,120
80 New_Stage04
81 0,180
82 New_Stage05
83 0,240
84 New_Stage06
85 0,300
86 MISSIONS
87 TimeAttack
88 ScoreAttack
89 Extra01
90 Extra02
91 Extra03
92 Extra04
93 MISSION_ID
94 Time
95 RECORD_TYPE
96 16
97 REQUIREMENT
98 NORMAL_BUTTON
99 GRAY_BUTTON
100 THUMBNAIL
101 TimeAttackRequirement
102 REQUIREMENT_DESC
103 TimeAttackStageDesc
104 STAGE_DESC
105 TEXT_STAGE
106 TEXT_RECORD
107 NEW_STAGE
108 25
109 Points
110 ScoreAttackRequirement
111 ScoreAttackStageDesc
112 26
113 Extra01Requirement
114 ExtraStage01Desc
115 27
116 Extra02Requirement
117 ExtraStage02Desc
118 Extra03Requirement
119 ExtraStage03Desc
120 29
121 Extra04Requirement
122 ExtraStage04Desc

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 938 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

View File

@@ -0,0 +1,7 @@
throttle,rep,settled_speed,burst_speed,rate_deg_per_wall_s
min,0,126.5,98.1,100.93
min,1,130.0,115.5,118.43
cruise,0,414.3,395.5,101.48
cruise,1,462.7,381.0,107.22
max,0,1463.5,1330.5,56.83
max,1,1496.4,1382.8,61.68
1 throttle rep settled_speed burst_speed rate_deg_per_wall_s
2 min 0 126.5 98.1 100.93
3 min 1 130.0 115.5 118.43
4 cruise 0 414.3 395.5 101.48
5 cruise 1 462.7 381.0 107.22
6 max 0 1463.5 1330.5 56.83
7 max 1 1496.4 1382.8 61.68

View File

@@ -0,0 +1,7 @@
throttle,rep,settled_speed,burst_speed,rate_deg_per_wall_s
min,0,121.3,102.6,113.64
min,1,152.0,108.8,109.46
cruise,0,428.5,374.7,100.19
cruise,1,361.6,391.4,88.39
max,0,1564.4,1352.0,52.22
max,1,1249.3,1614.6,70.52
1 throttle rep settled_speed burst_speed rate_deg_per_wall_s
2 min 0 121.3 102.6 113.64
3 min 1 152.0 108.8 109.46
4 cruise 0 428.5 374.7 100.19
5 cruise 1 361.6 391.4 88.39
6 max 0 1564.4 1352.0 52.22
7 max 1 1249.3 1614.6 70.52

View File

@@ -0,0 +1,40 @@
t,speed_units_per_wall_s,rate_deg_per_wall_s
0.52,1498.8,37.47
1.03,1276.1,91.64
1.53,1045.5,90.49
2.05,1133.7,118.83
2.55,885.4,129.88
3.06,777.7,127.51
3.57,788.1,174.38
4.07,546.7,157.56
4.57,536.2,124.41
5.09,946.8,161.62
5.59,821.9,138.76
6.09,636.7,131.35
6.61,529.3,133.56
7.13,674.7,162.01
7.63,669.2,125.78
8.17,746.4,127.35
8.67,821.1,154.31
9.17,569.7,135.57
9.67,645.2,167.18
10.19,720.0,150.99
10.69,698.6,123.63
11.21,717.0,125.53
11.72,826.9,165.47
12.23,471.3,114.2
12.77,594.0,146.56
13.29,858.3,174.73
13.79,734.8,130.83
14.31,502.8,91.53
14.81,822.9,168.67
15.31,696.2,166.37
15.83,462.2,109.81
16.33,791.0,162.87
16.85,854.4,155.88
17.37,536.8,98.26
17.89,873.1,182.24
18.41,630.0,155.61
18.93,584.1,129.64
19.43,699.6,132.45
19.97,805.1,142.68
1 t speed_units_per_wall_s rate_deg_per_wall_s
2 0.52 1498.8 37.47
3 1.03 1276.1 91.64
4 1.53 1045.5 90.49
5 2.05 1133.7 118.83
6 2.55 885.4 129.88
7 3.06 777.7 127.51
8 3.57 788.1 174.38
9 4.07 546.7 157.56
10 4.57 536.2 124.41
11 5.09 946.8 161.62
12 5.59 821.9 138.76
13 6.09 636.7 131.35
14 6.61 529.3 133.56
15 7.13 674.7 162.01
16 7.63 669.2 125.78
17 8.17 746.4 127.35
18 8.67 821.1 154.31
19 9.17 569.7 135.57
20 9.67 645.2 167.18
21 10.19 720.0 150.99
22 10.69 698.6 123.63
23 11.21 717.0 125.53
24 11.72 826.9 165.47
25 12.23 471.3 114.2
26 12.77 594.0 146.56
27 13.29 858.3 174.73
28 13.79 734.8 130.83
29 14.31 502.8 91.53
30 14.81 822.9 168.67
31 15.31 696.2 166.37
32 15.83 462.2 109.81
33 16.33 791.0 162.87
34 16.85 854.4 155.88
35 17.37 536.8 98.26
36 17.89 873.1 182.24
37 18.41 630.0 155.61
38 18.93 584.1 129.64
39 19.43 699.6 132.45
40 19.97 805.1 142.68

View File

@@ -0,0 +1,315 @@
phase,t,dpitch_deg
slow,0.051,0.0953
slow,0.101,1.6885
slow,0.152,0.8717
slow,0.202,1.6238
slow,0.252,-0.0
slow,0.302,-0.0
slow,0.352,8.8412
slow,0.403,5.679
slow,0.453,5.3839
slow,0.503,12.9544
slow,0.553,7.5713
slow,0.603,4.0593
slow,0.653,0.0
slow,0.703,0.0
slow,0.753,23.7081
slow,0.804,14.501
slow,0.854,9.6502
slow,0.905,9.64
slow,0.955,2.4036
slow,1.006,-0.0
slow,1.056,-0.0
slow,1.106,24.1631
slow,1.156,9.7201
slow,1.206,14.5817
slow,1.26,7.2861
slow,1.31,-0.0
slow,1.36,-0.0
slow,1.41,21.8962
slow,1.461,12.1865
slow,1.511,14.6395
slow,1.561,4.8866
slow,1.611,4.8766
slow,1.661,0.0
slow,1.712,0.0
slow,1.762,24.4862
slow,1.812,12.272
slow,1.863,14.7764
slow,1.913,14.8237
slow,1.964,9.9081
slow,2.014,-0.0
slow,2.064,-0.0
slow,2.116,22.3434
slow,2.166,4.9684
slow,2.217,14.8905
slow,2.267,9.9066
slow,2.317,2.4656
slow,2.367,-0.0
slow,2.417,19.7888
slow,2.467,7.374
slow,2.517,9.8264
slow,2.569,14.7106
slow,2.619,12.2195
slow,2.671,0.0
slow,2.721,0.0
slow,2.771,0.0
slow,2.821,24.4044
slow,2.871,12.1781
slow,2.951,14.5889
slow,3.001,14.5786
slow,3.052,0.0
slow,3.102,0.0
slow,3.153,19.432
slow,3.203,14.5741
slow,3.253,4.8605
slow,3.303,19.4596
slow,3.355,2.4281
slow,3.405,0.0
slow,3.455,0.0
slow,3.506,31.6949
slow,3.556,9.7829
slow,3.606,14.7094
slow,3.656,9.8314
slow,3.706,7.3808
slow,3.756,-0.0
slow,3.807,-0.0
slow,3.857,19.7355
slow,3.907,14.8675
slow,3.958,4.9655
slow,4.008,17.3918
slow,4.06,2.4777
slow,4.11,-0.0
slow,4.16,-0.0
slow,4.211,32.264
slow,4.261,9.866
slow,4.311,19.6734
slow,4.361,7.3673
slow,4.412,7.3404
slow,4.462,0.0
slow,4.513,0.0
slow,4.563,19.5894
slow,4.613,12.1841
slow,4.663,4.8719
slow,4.713,17.0264
slow,4.765,2.4241
slow,4.815,0.0
slow,4.866,0.0
slow,4.918,26.7496
slow,4.968,17.0367
slow,5.019,14.6052
slow,5.069,7.3143
slow,5.119,2.4296
slow,5.17,0.0
slow,5.222,17.0719
slow,5.272,12.2148
slow,5.351,14.6765
slow,5.401,14.7096
slow,5.454,2.4482
slow,5.504,0.0
slow,5.555,0.0
slow,5.605,27.0453
slow,5.655,12.3214
slow,5.705,14.8308
slow,5.756,4.9518
slow,5.806,19.8347
slow,5.856,2.4736
slow,5.906,-0.0
slow,5.957,-0.0
slow,6.008,24.7941
slow,6.058,9.8906
slow,6.108,14.805
slow,6.159,4.9105
slow,6.209,-0.0
slow,6.26,-0.0
slow,6.31,24.6254
slow,6.36,7.3425
slow,6.41,14.6791
slow,6.461,9.7716
slow,6.511,7.3144
slow,6.561,0.0
slow,6.612,0.0
slow,6.662,21.9754
slow,6.713,12.1647
slow,6.763,9.7355
slow,6.813,19.4718
slow,6.864,2.4274
slow,6.914,0.0
slow,6.964,0.0
slow,7.014,29.2378
slow,7.064,9.7533
slow,7.114,14.6513
slow,7.165,4.8902
slow,7.215,2.4395
slow,7.266,0.0
slow,7.316,0.0
slow,7.368,26.9512
slow,7.418,14.7524
slow,7.468,9.8579
slow,7.518,12.3414
slow,7.569,2.4662
slow,7.621,-0.0
slow,7.671,-0.0
slow,7.722,19.8018
slow,7.772,9.92
slow,7.851,14.8814
slow,7.901,12.3787
slow,7.951,2.4669
slow,8.001,0.0
fast,0.05,0.0
fast,0.104,0.0
fast,0.154,0.0
fast,0.204,0.2374
fast,0.254,0.206
fast,0.306,1.8902
fast,0.356,1.5885
fast,0.406,1.4595
fast,0.456,0.0
fast,0.506,0.0
fast,0.556,5.7624
fast,0.606,2.347
fast,0.657,3.5323
fast,0.707,6.1276
fast,0.757,1.1012
fast,0.807,-0.0
fast,0.857,-0.0
fast,0.909,12.418
fast,0.959,5.1346
fast,1.009,7.8218
fast,1.059,6.5806
fast,1.109,1.3424
fast,1.16,0.0
fast,1.21,10.761
fast,1.263,4.2102
fast,1.313,8.2445
fast,1.363,6.8621
fast,1.413,6.9892
fast,1.463,-0.0
fast,1.542,-0.0
fast,1.592,11.0635
fast,1.642,5.9249
fast,1.692,8.7078
fast,1.742,2.8665
fast,1.793,10.1297
fast,1.843,0.0
fast,1.893,0.0
fast,1.943,0.0
fast,1.993,20.6893
fast,2.043,3.2503
fast,2.094,11.2506
fast,2.144,6.3385
fast,2.194,7.8452
fast,2.244,1.556
fast,2.295,-0.0
fast,2.345,-0.0
fast,2.395,18.7454
fast,2.445,10.3395
fast,2.495,10.608
fast,2.545,7.0808
fast,2.596,7.0815
fast,2.646,0.0
fast,2.696,0.0
fast,2.746,0.0
fast,2.796,17.7627
fast,2.847,10.6635
fast,2.897,7.1571
fast,2.947,7.1983
fast,3.002,5.4229
fast,3.052,0.0
fast,3.102,0.0
fast,3.152,0.0
fast,3.202,25.5105
fast,3.253,7.3926
fast,3.303,11.2085
fast,3.353,3.7679
fast,3.404,3.7764
fast,3.455,-0.0
fast,3.505,-0.0
fast,3.556,19.0406
fast,3.642,11.5779
fast,3.692,11.712
fast,3.742,3.9222
fast,3.792,0.0
fast,3.842,0.0
fast,3.892,17.7661
fast,3.942,1.9679
fast,3.993,17.8611
fast,4.043,3.9876
fast,4.093,15.9815
fast,4.146,0.0
fast,4.196,0.0
fast,4.246,0.0
fast,4.296,21.9362
fast,4.346,7.908
fast,4.396,11.8184
fast,4.447,7.8388
fast,4.497,1.9466
fast,4.547,0.0
fast,4.597,0.0
fast,4.647,0.0
fast,4.697,29.0258
fast,4.747,11.3566
fast,4.797,11.275
fast,4.847,3.7393
fast,4.898,7.4494
fast,4.948,-0.0
fast,4.998,-0.0
fast,5.048,-0.0
fast,5.098,24.0113
fast,5.148,10.888
fast,5.202,10.8585
fast,5.252,7.225
fast,5.302,5.4068
fast,5.352,0.0
fast,5.403,0.0
fast,5.453,0.0
fast,5.504,26.97
fast,5.554,10.7617
fast,5.604,10.8137
fast,5.654,3.6172
fast,5.709,5.4336
fast,5.759,-0.0
fast,5.809,-0.0
fast,5.862,20.0109
fast,5.942,10.985
fast,5.992,11.0845
fast,6.042,5.5755
fast,6.092,0.0
fast,6.142,0.0
fast,6.192,18.6832
fast,6.245,1.866
fast,6.295,15.0963
fast,6.345,3.8107
fast,6.395,9.5834
fast,6.446,1.9237
fast,6.496,0.0
fast,6.546,0.0
fast,6.598,21.2979
fast,6.651,7.8108
fast,6.701,13.7645
fast,6.751,3.9499
fast,6.802,5.9406
fast,6.852,0.0
fast,6.902,15.8604
fast,6.952,5.9146
fast,7.002,11.8545
fast,7.053,7.9034
fast,7.103,9.8434
fast,7.153,1.9608
fast,7.203,-0.0
fast,7.253,-0.0
fast,7.303,19.6693
fast,7.357,13.4968
fast,7.407,11.5239
fast,7.457,7.6491
fast,7.507,11.4147
fast,7.559,1.8882
fast,7.642,-0.0
fast,7.692,15.147
fast,7.742,5.584
fast,7.792,11.1374
fast,7.842,3.6992
fast,7.892,9.2126
fast,7.946,1.8328
fast,7.996,-0.0
fast,8.046,-0.0
1 phase t dpitch_deg
2 slow 0.051 0.0953
3 slow 0.101 1.6885
4 slow 0.152 0.8717
5 slow 0.202 1.6238
6 slow 0.252 -0.0
7 slow 0.302 -0.0
8 slow 0.352 8.8412
9 slow 0.403 5.679
10 slow 0.453 5.3839
11 slow 0.503 12.9544
12 slow 0.553 7.5713
13 slow 0.603 4.0593
14 slow 0.653 0.0
15 slow 0.703 0.0
16 slow 0.753 23.7081
17 slow 0.804 14.501
18 slow 0.854 9.6502
19 slow 0.905 9.64
20 slow 0.955 2.4036
21 slow 1.006 -0.0
22 slow 1.056 -0.0
23 slow 1.106 24.1631
24 slow 1.156 9.7201
25 slow 1.206 14.5817
26 slow 1.26 7.2861
27 slow 1.31 -0.0
28 slow 1.36 -0.0
29 slow 1.41 21.8962
30 slow 1.461 12.1865
31 slow 1.511 14.6395
32 slow 1.561 4.8866
33 slow 1.611 4.8766
34 slow 1.661 0.0
35 slow 1.712 0.0
36 slow 1.762 24.4862
37 slow 1.812 12.272
38 slow 1.863 14.7764
39 slow 1.913 14.8237
40 slow 1.964 9.9081
41 slow 2.014 -0.0
42 slow 2.064 -0.0
43 slow 2.116 22.3434
44 slow 2.166 4.9684
45 slow 2.217 14.8905
46 slow 2.267 9.9066
47 slow 2.317 2.4656
48 slow 2.367 -0.0
49 slow 2.417 19.7888
50 slow 2.467 7.374
51 slow 2.517 9.8264
52 slow 2.569 14.7106
53 slow 2.619 12.2195
54 slow 2.671 0.0
55 slow 2.721 0.0
56 slow 2.771 0.0
57 slow 2.821 24.4044
58 slow 2.871 12.1781
59 slow 2.951 14.5889
60 slow 3.001 14.5786
61 slow 3.052 0.0
62 slow 3.102 0.0
63 slow 3.153 19.432
64 slow 3.203 14.5741
65 slow 3.253 4.8605
66 slow 3.303 19.4596
67 slow 3.355 2.4281
68 slow 3.405 0.0
69 slow 3.455 0.0
70 slow 3.506 31.6949
71 slow 3.556 9.7829
72 slow 3.606 14.7094
73 slow 3.656 9.8314
74 slow 3.706 7.3808
75 slow 3.756 -0.0
76 slow 3.807 -0.0
77 slow 3.857 19.7355
78 slow 3.907 14.8675
79 slow 3.958 4.9655
80 slow 4.008 17.3918
81 slow 4.06 2.4777
82 slow 4.11 -0.0
83 slow 4.16 -0.0
84 slow 4.211 32.264
85 slow 4.261 9.866
86 slow 4.311 19.6734
87 slow 4.361 7.3673
88 slow 4.412 7.3404
89 slow 4.462 0.0
90 slow 4.513 0.0
91 slow 4.563 19.5894
92 slow 4.613 12.1841
93 slow 4.663 4.8719
94 slow 4.713 17.0264
95 slow 4.765 2.4241
96 slow 4.815 0.0
97 slow 4.866 0.0
98 slow 4.918 26.7496
99 slow 4.968 17.0367
100 slow 5.019 14.6052
101 slow 5.069 7.3143
102 slow 5.119 2.4296
103 slow 5.17 0.0
104 slow 5.222 17.0719
105 slow 5.272 12.2148
106 slow 5.351 14.6765
107 slow 5.401 14.7096
108 slow 5.454 2.4482
109 slow 5.504 0.0
110 slow 5.555 0.0
111 slow 5.605 27.0453
112 slow 5.655 12.3214
113 slow 5.705 14.8308
114 slow 5.756 4.9518
115 slow 5.806 19.8347
116 slow 5.856 2.4736
117 slow 5.906 -0.0
118 slow 5.957 -0.0
119 slow 6.008 24.7941
120 slow 6.058 9.8906
121 slow 6.108 14.805
122 slow 6.159 4.9105
123 slow 6.209 -0.0
124 slow 6.26 -0.0
125 slow 6.31 24.6254
126 slow 6.36 7.3425
127 slow 6.41 14.6791
128 slow 6.461 9.7716
129 slow 6.511 7.3144
130 slow 6.561 0.0
131 slow 6.612 0.0
132 slow 6.662 21.9754
133 slow 6.713 12.1647
134 slow 6.763 9.7355
135 slow 6.813 19.4718
136 slow 6.864 2.4274
137 slow 6.914 0.0
138 slow 6.964 0.0
139 slow 7.014 29.2378
140 slow 7.064 9.7533
141 slow 7.114 14.6513
142 slow 7.165 4.8902
143 slow 7.215 2.4395
144 slow 7.266 0.0
145 slow 7.316 0.0
146 slow 7.368 26.9512
147 slow 7.418 14.7524
148 slow 7.468 9.8579
149 slow 7.518 12.3414
150 slow 7.569 2.4662
151 slow 7.621 -0.0
152 slow 7.671 -0.0
153 slow 7.722 19.8018
154 slow 7.772 9.92
155 slow 7.851 14.8814
156 slow 7.901 12.3787
157 slow 7.951 2.4669
158 slow 8.001 0.0
159 fast 0.05 0.0
160 fast 0.104 0.0
161 fast 0.154 0.0
162 fast 0.204 0.2374
163 fast 0.254 0.206
164 fast 0.306 1.8902
165 fast 0.356 1.5885
166 fast 0.406 1.4595
167 fast 0.456 0.0
168 fast 0.506 0.0
169 fast 0.556 5.7624
170 fast 0.606 2.347
171 fast 0.657 3.5323
172 fast 0.707 6.1276
173 fast 0.757 1.1012
174 fast 0.807 -0.0
175 fast 0.857 -0.0
176 fast 0.909 12.418
177 fast 0.959 5.1346
178 fast 1.009 7.8218
179 fast 1.059 6.5806
180 fast 1.109 1.3424
181 fast 1.16 0.0
182 fast 1.21 10.761
183 fast 1.263 4.2102
184 fast 1.313 8.2445
185 fast 1.363 6.8621
186 fast 1.413 6.9892
187 fast 1.463 -0.0
188 fast 1.542 -0.0
189 fast 1.592 11.0635
190 fast 1.642 5.9249
191 fast 1.692 8.7078
192 fast 1.742 2.8665
193 fast 1.793 10.1297
194 fast 1.843 0.0
195 fast 1.893 0.0
196 fast 1.943 0.0
197 fast 1.993 20.6893
198 fast 2.043 3.2503
199 fast 2.094 11.2506
200 fast 2.144 6.3385
201 fast 2.194 7.8452
202 fast 2.244 1.556
203 fast 2.295 -0.0
204 fast 2.345 -0.0
205 fast 2.395 18.7454
206 fast 2.445 10.3395
207 fast 2.495 10.608
208 fast 2.545 7.0808
209 fast 2.596 7.0815
210 fast 2.646 0.0
211 fast 2.696 0.0
212 fast 2.746 0.0
213 fast 2.796 17.7627
214 fast 2.847 10.6635
215 fast 2.897 7.1571
216 fast 2.947 7.1983
217 fast 3.002 5.4229
218 fast 3.052 0.0
219 fast 3.102 0.0
220 fast 3.152 0.0
221 fast 3.202 25.5105
222 fast 3.253 7.3926
223 fast 3.303 11.2085
224 fast 3.353 3.7679
225 fast 3.404 3.7764
226 fast 3.455 -0.0
227 fast 3.505 -0.0
228 fast 3.556 19.0406
229 fast 3.642 11.5779
230 fast 3.692 11.712
231 fast 3.742 3.9222
232 fast 3.792 0.0
233 fast 3.842 0.0
234 fast 3.892 17.7661
235 fast 3.942 1.9679
236 fast 3.993 17.8611
237 fast 4.043 3.9876
238 fast 4.093 15.9815
239 fast 4.146 0.0
240 fast 4.196 0.0
241 fast 4.246 0.0
242 fast 4.296 21.9362
243 fast 4.346 7.908
244 fast 4.396 11.8184
245 fast 4.447 7.8388
246 fast 4.497 1.9466
247 fast 4.547 0.0
248 fast 4.597 0.0
249 fast 4.647 0.0
250 fast 4.697 29.0258
251 fast 4.747 11.3566
252 fast 4.797 11.275
253 fast 4.847 3.7393
254 fast 4.898 7.4494
255 fast 4.948 -0.0
256 fast 4.998 -0.0
257 fast 5.048 -0.0
258 fast 5.098 24.0113
259 fast 5.148 10.888
260 fast 5.202 10.8585
261 fast 5.252 7.225
262 fast 5.302 5.4068
263 fast 5.352 0.0
264 fast 5.403 0.0
265 fast 5.453 0.0
266 fast 5.504 26.97
267 fast 5.554 10.7617
268 fast 5.604 10.8137
269 fast 5.654 3.6172
270 fast 5.709 5.4336
271 fast 5.759 -0.0
272 fast 5.809 -0.0
273 fast 5.862 20.0109
274 fast 5.942 10.985
275 fast 5.992 11.0845
276 fast 6.042 5.5755
277 fast 6.092 0.0
278 fast 6.142 0.0
279 fast 6.192 18.6832
280 fast 6.245 1.866
281 fast 6.295 15.0963
282 fast 6.345 3.8107
283 fast 6.395 9.5834
284 fast 6.446 1.9237
285 fast 6.496 0.0
286 fast 6.546 0.0
287 fast 6.598 21.2979
288 fast 6.651 7.8108
289 fast 6.701 13.7645
290 fast 6.751 3.9499
291 fast 6.802 5.9406
292 fast 6.852 0.0
293 fast 6.902 15.8604
294 fast 6.952 5.9146
295 fast 7.002 11.8545
296 fast 7.053 7.9034
297 fast 7.103 9.8434
298 fast 7.153 1.9608
299 fast 7.203 -0.0
300 fast 7.253 -0.0
301 fast 7.303 19.6693
302 fast 7.357 13.4968
303 fast 7.407 11.5239
304 fast 7.457 7.6491
305 fast 7.507 11.4147
306 fast 7.559 1.8882
307 fast 7.642 -0.0
308 fast 7.692 15.147
309 fast 7.742 5.584
310 fast 7.792 11.1374
311 fast 7.842 3.6992
312 fast 7.892 9.2126
313 fast 7.946 1.8328
314 fast 7.996 -0.0
315 fast 8.046 -0.0

View File

@@ -0,0 +1,353 @@
t,speed_units_per_wall_s,rate_deg_per_wall_s
0.05,0.0,0.0
0.101,4682.4,0.0
0.154,1865.9,8.21
0.205,1915.4,22.53
0.273,1722.2,32.72
0.323,2309.7,64.27
0.374,0.0,0.0
0.426,0.0,0.0
0.476,0.0,0.0
0.526,3781.4,166.09
0.576,1133.2,56.22
0.626,2228.8,125.61
0.676,735.8,46.29
0.726,2161.2,150.9
0.777,0.0,0.0
0.827,0.0,0.0
0.877,0.0,0.0
0.927,4795.2,363.58
0.978,1400.3,105.58
1.028,2080.9,159.73
1.078,688.0,53.58
1.129,1018.2,79.65
1.179,0.0,0.0
1.229,0.0,0.0
1.279,3169.4,272.63
1.332,1891.3,159.33
1.382,656.8,55.51
1.432,1958.3,167.87
1.485,311.4,26.7
1.535,0.0,0.0
1.585,0.0,0.0
1.637,2189.9,217.26
1.687,1470.3,152.38
1.737,1832.8,178.53
1.788,611.6,57.44
1.838,311.7,29.03
1.889,0.0,0.0
1.939,0.0,0.0
1.989,2568.6,290.03
2.039,1303.4,156.28
2.09,1571.3,188.96
2.173,987.0,114.66
2.223,1389.3,157.19
2.274,0.0,0.0
2.324,0.0,0.0
2.374,0.0,0.0
2.425,3014.5,437.25
2.476,851.8,137.78
2.526,1720.0,282.99
2.576,855.8,142.04
2.626,1272.7,213.71
2.676,420.4,71.25
2.726,0.0,0.0
2.776,0.0,0.0
2.827,2337.7,393.95
2.879,605.4,103.05
2.929,1229.4,216.06
2.979,798.9,145.37
3.029,1356.7,257.13
3.081,183.1,35.66
3.131,0.0,0.0
3.181,0.0,0.0
3.231,1858.0,372.22
3.281,541.0,112.82
3.332,1372.1,301.88
3.382,335.4,76.89
3.432,658.3,154.84
3.482,162.0,38.8
3.536,0.0,0.0
3.586,0.0,0.0
3.636,1591.8,390.72
3.686,311.3,78.69
3.736,1215.5,316.75
3.786,602.4,159.01
3.839,429.8,113.96
3.889,0.0,0.0
3.939,0.0,0.0
3.989,0.0,0.0
4.04,1835.6,469.81
4.09,311.3,78.92
4.14,1251.2,315.27
4.19,317.9,78.47
4.24,320.5,78.42
4.29,0.0,0.0
4.341,0.0,0.0
4.391,1352.5,313.43
4.441,850.5,192.03
4.492,1033.9,227.09
4.542,1073.0,227.87
4.592,551.1,113.43
4.642,0.0,0.0
4.692,0.0,0.0
4.743,1735.5,337.79
4.793,779.7,147.59
4.843,1186.4,220.17
4.893,808.4,146.17
4.943,1018.0,181.41
4.993,204.4,36.08
5.043,0.0,0.0
5.093,0.0,0.0
5.144,0.0,0.0
5.194,3438.2,575.96
5.244,853.2,142.26
5.294,1685.8,285.28
5.344,1250.1,215.19
5.394,825.0,144.01
5.445,202.2,35.56
5.495,0.0,0.0
5.545,0.0,0.0
5.596,2042.5,361.76
5.674,762.2,138.77
5.725,1171.7,219.95
5.775,384.3,73.55
5.826,0.0,0.0
5.877,0.0,0.0
5.927,0.0,0.0
5.977,1528.0,296.2
6.028,1281.9,256.26
6.078,365.6,75.03
6.128,1073.3,226.52
6.178,176.0,37.88
6.231,0.0,0.0
6.281,0.0,0.0
6.332,1720.1,372.76
6.382,518.3,114.37
6.432,1009.5,230.82
6.484,480.0,112.96
6.534,807.9,194.56
6.584,159.3,38.98
6.675,0.0,0.0
6.725,1449.1,352.93
6.775,477.0,117.26
6.826,927.2,232.38
6.877,306.8,77.6
6.927,928.5,236.59
6.977,154.5,39.37
7.027,0.0,0.0
7.077,0.0,0.0
7.131,1353.2,333.31
7.181,963.9,234.69
7.231,1304.5,311.86
7.281,332.6,77.62
7.331,506.1,116.36
7.381,0.0,0.0
7.432,0.0,0.0
7.482,0.0,0.0
7.534,2615.1,556.47
7.584,1101.8,225.72
7.634,1108.3,222.17
7.686,929.5,182.14
7.736,576.3,111.11
7.786,0.0,0.0
7.836,0.0,0.0
7.886,0.0,0.0
7.936,2840.4,515.81
7.987,807.7,144.56
8.037,1214.6,217.75
8.087,809.7,145.24
8.137,606.3,108.89
8.187,0.0,0.0
8.237,0.0,0.0
8.288,0.0,0.0
8.339,2628.6,463.46
8.389,811.4,144.9
8.439,1198.5,218.35
8.489,392.4,72.53
8.539,784.5,146.63
8.59,0.0,0.0
8.64,0.0,0.0
8.69,0.0,0.0
8.741,2111.3,400.11
8.791,946.6,185.46
8.841,1110.6,224.19
8.891,364.4,75.15
8.941,720.3,150.86
8.991,0.0,0.0
9.042,0.0,0.0
9.092,1792.7,378.77
9.142,529.6,113.67
9.193,1022.7,226.47
9.273,624.6,143.36
9.325,644.6,151.82
9.375,0.0,0.0
9.425,0.0,0.0
9.476,1289.1,307.07
9.526,1122.5,273.41
9.577,311.7,76.8
9.627,789.7,196.14
9.677,156.4,38.99
9.727,0.0,0.0
9.778,0.0,0.0
9.828,2131.2,510.02
9.878,653.9,155.64
9.928,983.1,233.48
9.978,330.0,77.72
10.028,496.6,116.32
10.079,0.0,0.0
10.129,0.0,0.0
10.179,0.0,0.0
10.23,2470.2,540.62
10.282,672.7,144.51
10.333,1067.2,227.85
10.383,1076.8,227.17
10.433,724.0,150.8
10.483,0.0,0.0
10.533,0.0,0.0
10.583,0.0,0.0
10.633,2272.0,451.3
10.683,759.7,148.67
10.734,762.5,148.52
10.784,756.3,146.57
10.834,770.1,148.16
10.885,0.0,0.0
10.935,0.0,0.0
10.985,0.0,0.0
11.035,2168.6,406.08
11.085,984.6,183.61
11.136,1178.3,220.07
11.186,590.5,110.24
11.236,393.0,73.44
11.286,0.0,0.0
11.336,0.0,0.0
11.386,2007.6,367.45
11.436,597.4,109.35
11.486,1180.4,219.81
11.537,1160.8,220.86
11.587,575.3,111.02
11.639,368.0,71.56
11.689,0.0,0.0
11.739,0.0,0.0
11.789,2275.4,445.88
11.879,415.2,83.5
11.929,1448.9,300.69
11.979,356.5,75.48
12.031,0.0,0.0
12.081,0.0,0.0
12.131,1612.8,341.73
12.184,504.1,108.0
12.234,1042.0,228.85
12.284,683.1,153.38
12.334,674.3,153.83
12.386,0.0,0.0
12.473,0.0,0.0
12.524,1701.7,386.01
12.574,672.1,154.19
12.625,974.9,227.26
12.675,330.4,77.54
12.725,986.1,232.99
12.775,163.8,38.73
12.825,0.0,0.0
12.878,0.0,0.0
12.928,0.0,0.0
12.978,2405.7,542.85
13.029,1022.4,230.5
13.079,681.6,153.81
13.129,1354.2,305.37
13.179,343.9,76.77
13.229,692.5,153.38
13.28,0.0,0.0
13.33,0.0,0.0
13.38,0.0,0.0
13.43,2362.5,496.01
13.483,693.6,143.63
13.533,1466.7,300.97
13.583,555.0,112.79
13.633,184.3,37.4
13.683,0.0,0.0
13.735,0.0,0.0
13.785,1917.4,375.17
13.838,542.8,105.29
13.888,1144.2,222.78
13.938,1513.8,297.05
13.988,568.4,111.69
14.038,378.0,74.42
14.088,0.0,0.0
14.139,0.0,0.0
14.189,1933.6,372.51
14.239,955.5,184.91
14.289,756.9,148.61
14.374,662.1,132.25
14.424,554.7,112.16
14.474,0.0,0.0
14.524,0.0,0.0
14.577,1603.7,323.84
14.627,1276.0,263.01
14.677,542.3,113.23
14.727,714.5,150.87
14.777,354.7,75.59
14.83,0.0,0.0
14.88,0.0,0.0
14.93,1795.9,379.83
14.98,355.3,75.74
15.031,1386.0,301.97
15.083,330.9,73.04
15.133,688.3,153.24
15.183,170.9,38.24
15.234,0.0,0.0
15.284,0.0,0.0
15.334,0.0,0.0
15.384,2304.1,499.06
15.435,1027.8,225.69
15.485,1024.9,230.47
15.536,1000.9,229.21
15.586,664.6,153.64
15.636,665.2,154.54
15.687,0.0,0.0
15.737,0.0,0.0
15.788,0.0,0.0
15.84,2131.8,478.6
15.89,856.1,192.06
15.941,1025.4,230.39
15.991,342.4,76.74
16.043,663.2,148.34
16.093,0.0,0.0
16.143,0.0,0.0
16.193,0.0,0.0
16.246,2555.4,545.64
16.296,895.5,189.56
16.373,694.2,146.49
16.424,1443.5,302.27
16.476,0.0,0.0
16.526,0.0,0.0
16.577,0.0,0.0
16.627,2074.2,414.87
16.677,751.3,149.12
16.727,1123.6,223.97
16.777,374.5,74.66
16.827,1122.8,223.93
16.88,0.0,0.0
16.93,0.0,0.0
16.98,0.0,0.0
17.03,2296.0,447.62
17.08,379.9,74.23
17.13,1503.9,297.32
17.18,563.4,111.98
17.233,538.0,107.34
17.283,0.0,0.0
17.333,0.0,0.0
17.383,1890.3,373.65
17.433,1119.5,223.93
17.485,361.3,72.88
17.535,1651.2,337.72
17.585,365.1,75.24
17.635,547.2,113.12
17.686,0.0,0.0
17.736,0.0,0.0
17.786,0.0,0.0
17.839,2490.6,503.11
17.892,1033.4,213.33
17.942,1073.2,226.83
17.995,670.1,143.94
18.045,1223.2,266.8
1 t speed_units_per_wall_s rate_deg_per_wall_s
2 0.05 0.0 0.0
3 0.101 4682.4 0.0
4 0.154 1865.9 8.21
5 0.205 1915.4 22.53
6 0.273 1722.2 32.72
7 0.323 2309.7 64.27
8 0.374 0.0 0.0
9 0.426 0.0 0.0
10 0.476 0.0 0.0
11 0.526 3781.4 166.09
12 0.576 1133.2 56.22
13 0.626 2228.8 125.61
14 0.676 735.8 46.29
15 0.726 2161.2 150.9
16 0.777 0.0 0.0
17 0.827 0.0 0.0
18 0.877 0.0 0.0
19 0.927 4795.2 363.58
20 0.978 1400.3 105.58
21 1.028 2080.9 159.73
22 1.078 688.0 53.58
23 1.129 1018.2 79.65
24 1.179 0.0 0.0
25 1.229 0.0 0.0
26 1.279 3169.4 272.63
27 1.332 1891.3 159.33
28 1.382 656.8 55.51
29 1.432 1958.3 167.87
30 1.485 311.4 26.7
31 1.535 0.0 0.0
32 1.585 0.0 0.0
33 1.637 2189.9 217.26
34 1.687 1470.3 152.38
35 1.737 1832.8 178.53
36 1.788 611.6 57.44
37 1.838 311.7 29.03
38 1.889 0.0 0.0
39 1.939 0.0 0.0
40 1.989 2568.6 290.03
41 2.039 1303.4 156.28
42 2.09 1571.3 188.96
43 2.173 987.0 114.66
44 2.223 1389.3 157.19
45 2.274 0.0 0.0
46 2.324 0.0 0.0
47 2.374 0.0 0.0
48 2.425 3014.5 437.25
49 2.476 851.8 137.78
50 2.526 1720.0 282.99
51 2.576 855.8 142.04
52 2.626 1272.7 213.71
53 2.676 420.4 71.25
54 2.726 0.0 0.0
55 2.776 0.0 0.0
56 2.827 2337.7 393.95
57 2.879 605.4 103.05
58 2.929 1229.4 216.06
59 2.979 798.9 145.37
60 3.029 1356.7 257.13
61 3.081 183.1 35.66
62 3.131 0.0 0.0
63 3.181 0.0 0.0
64 3.231 1858.0 372.22
65 3.281 541.0 112.82
66 3.332 1372.1 301.88
67 3.382 335.4 76.89
68 3.432 658.3 154.84
69 3.482 162.0 38.8
70 3.536 0.0 0.0
71 3.586 0.0 0.0
72 3.636 1591.8 390.72
73 3.686 311.3 78.69
74 3.736 1215.5 316.75
75 3.786 602.4 159.01
76 3.839 429.8 113.96
77 3.889 0.0 0.0
78 3.939 0.0 0.0
79 3.989 0.0 0.0
80 4.04 1835.6 469.81
81 4.09 311.3 78.92
82 4.14 1251.2 315.27
83 4.19 317.9 78.47
84 4.24 320.5 78.42
85 4.29 0.0 0.0
86 4.341 0.0 0.0
87 4.391 1352.5 313.43
88 4.441 850.5 192.03
89 4.492 1033.9 227.09
90 4.542 1073.0 227.87
91 4.592 551.1 113.43
92 4.642 0.0 0.0
93 4.692 0.0 0.0
94 4.743 1735.5 337.79
95 4.793 779.7 147.59
96 4.843 1186.4 220.17
97 4.893 808.4 146.17
98 4.943 1018.0 181.41
99 4.993 204.4 36.08
100 5.043 0.0 0.0
101 5.093 0.0 0.0
102 5.144 0.0 0.0
103 5.194 3438.2 575.96
104 5.244 853.2 142.26
105 5.294 1685.8 285.28
106 5.344 1250.1 215.19
107 5.394 825.0 144.01
108 5.445 202.2 35.56
109 5.495 0.0 0.0
110 5.545 0.0 0.0
111 5.596 2042.5 361.76
112 5.674 762.2 138.77
113 5.725 1171.7 219.95
114 5.775 384.3 73.55
115 5.826 0.0 0.0
116 5.877 0.0 0.0
117 5.927 0.0 0.0
118 5.977 1528.0 296.2
119 6.028 1281.9 256.26
120 6.078 365.6 75.03
121 6.128 1073.3 226.52
122 6.178 176.0 37.88
123 6.231 0.0 0.0
124 6.281 0.0 0.0
125 6.332 1720.1 372.76
126 6.382 518.3 114.37
127 6.432 1009.5 230.82
128 6.484 480.0 112.96
129 6.534 807.9 194.56
130 6.584 159.3 38.98
131 6.675 0.0 0.0
132 6.725 1449.1 352.93
133 6.775 477.0 117.26
134 6.826 927.2 232.38
135 6.877 306.8 77.6
136 6.927 928.5 236.59
137 6.977 154.5 39.37
138 7.027 0.0 0.0
139 7.077 0.0 0.0
140 7.131 1353.2 333.31
141 7.181 963.9 234.69
142 7.231 1304.5 311.86
143 7.281 332.6 77.62
144 7.331 506.1 116.36
145 7.381 0.0 0.0
146 7.432 0.0 0.0
147 7.482 0.0 0.0
148 7.534 2615.1 556.47
149 7.584 1101.8 225.72
150 7.634 1108.3 222.17
151 7.686 929.5 182.14
152 7.736 576.3 111.11
153 7.786 0.0 0.0
154 7.836 0.0 0.0
155 7.886 0.0 0.0
156 7.936 2840.4 515.81
157 7.987 807.7 144.56
158 8.037 1214.6 217.75
159 8.087 809.7 145.24
160 8.137 606.3 108.89
161 8.187 0.0 0.0
162 8.237 0.0 0.0
163 8.288 0.0 0.0
164 8.339 2628.6 463.46
165 8.389 811.4 144.9
166 8.439 1198.5 218.35
167 8.489 392.4 72.53
168 8.539 784.5 146.63
169 8.59 0.0 0.0
170 8.64 0.0 0.0
171 8.69 0.0 0.0
172 8.741 2111.3 400.11
173 8.791 946.6 185.46
174 8.841 1110.6 224.19
175 8.891 364.4 75.15
176 8.941 720.3 150.86
177 8.991 0.0 0.0
178 9.042 0.0 0.0
179 9.092 1792.7 378.77
180 9.142 529.6 113.67
181 9.193 1022.7 226.47
182 9.273 624.6 143.36
183 9.325 644.6 151.82
184 9.375 0.0 0.0
185 9.425 0.0 0.0
186 9.476 1289.1 307.07
187 9.526 1122.5 273.41
188 9.577 311.7 76.8
189 9.627 789.7 196.14
190 9.677 156.4 38.99
191 9.727 0.0 0.0
192 9.778 0.0 0.0
193 9.828 2131.2 510.02
194 9.878 653.9 155.64
195 9.928 983.1 233.48
196 9.978 330.0 77.72
197 10.028 496.6 116.32
198 10.079 0.0 0.0
199 10.129 0.0 0.0
200 10.179 0.0 0.0
201 10.23 2470.2 540.62
202 10.282 672.7 144.51
203 10.333 1067.2 227.85
204 10.383 1076.8 227.17
205 10.433 724.0 150.8
206 10.483 0.0 0.0
207 10.533 0.0 0.0
208 10.583 0.0 0.0
209 10.633 2272.0 451.3
210 10.683 759.7 148.67
211 10.734 762.5 148.52
212 10.784 756.3 146.57
213 10.834 770.1 148.16
214 10.885 0.0 0.0
215 10.935 0.0 0.0
216 10.985 0.0 0.0
217 11.035 2168.6 406.08
218 11.085 984.6 183.61
219 11.136 1178.3 220.07
220 11.186 590.5 110.24
221 11.236 393.0 73.44
222 11.286 0.0 0.0
223 11.336 0.0 0.0
224 11.386 2007.6 367.45
225 11.436 597.4 109.35
226 11.486 1180.4 219.81
227 11.537 1160.8 220.86
228 11.587 575.3 111.02
229 11.639 368.0 71.56
230 11.689 0.0 0.0
231 11.739 0.0 0.0
232 11.789 2275.4 445.88
233 11.879 415.2 83.5
234 11.929 1448.9 300.69
235 11.979 356.5 75.48
236 12.031 0.0 0.0
237 12.081 0.0 0.0
238 12.131 1612.8 341.73
239 12.184 504.1 108.0
240 12.234 1042.0 228.85
241 12.284 683.1 153.38
242 12.334 674.3 153.83
243 12.386 0.0 0.0
244 12.473 0.0 0.0
245 12.524 1701.7 386.01
246 12.574 672.1 154.19
247 12.625 974.9 227.26
248 12.675 330.4 77.54
249 12.725 986.1 232.99
250 12.775 163.8 38.73
251 12.825 0.0 0.0
252 12.878 0.0 0.0
253 12.928 0.0 0.0
254 12.978 2405.7 542.85
255 13.029 1022.4 230.5
256 13.079 681.6 153.81
257 13.129 1354.2 305.37
258 13.179 343.9 76.77
259 13.229 692.5 153.38
260 13.28 0.0 0.0
261 13.33 0.0 0.0
262 13.38 0.0 0.0
263 13.43 2362.5 496.01
264 13.483 693.6 143.63
265 13.533 1466.7 300.97
266 13.583 555.0 112.79
267 13.633 184.3 37.4
268 13.683 0.0 0.0
269 13.735 0.0 0.0
270 13.785 1917.4 375.17
271 13.838 542.8 105.29
272 13.888 1144.2 222.78
273 13.938 1513.8 297.05
274 13.988 568.4 111.69
275 14.038 378.0 74.42
276 14.088 0.0 0.0
277 14.139 0.0 0.0
278 14.189 1933.6 372.51
279 14.239 955.5 184.91
280 14.289 756.9 148.61
281 14.374 662.1 132.25
282 14.424 554.7 112.16
283 14.474 0.0 0.0
284 14.524 0.0 0.0
285 14.577 1603.7 323.84
286 14.627 1276.0 263.01
287 14.677 542.3 113.23
288 14.727 714.5 150.87
289 14.777 354.7 75.59
290 14.83 0.0 0.0
291 14.88 0.0 0.0
292 14.93 1795.9 379.83
293 14.98 355.3 75.74
294 15.031 1386.0 301.97
295 15.083 330.9 73.04
296 15.133 688.3 153.24
297 15.183 170.9 38.24
298 15.234 0.0 0.0
299 15.284 0.0 0.0
300 15.334 0.0 0.0
301 15.384 2304.1 499.06
302 15.435 1027.8 225.69
303 15.485 1024.9 230.47
304 15.536 1000.9 229.21
305 15.586 664.6 153.64
306 15.636 665.2 154.54
307 15.687 0.0 0.0
308 15.737 0.0 0.0
309 15.788 0.0 0.0
310 15.84 2131.8 478.6
311 15.89 856.1 192.06
312 15.941 1025.4 230.39
313 15.991 342.4 76.74
314 16.043 663.2 148.34
315 16.093 0.0 0.0
316 16.143 0.0 0.0
317 16.193 0.0 0.0
318 16.246 2555.4 545.64
319 16.296 895.5 189.56
320 16.373 694.2 146.49
321 16.424 1443.5 302.27
322 16.476 0.0 0.0
323 16.526 0.0 0.0
324 16.577 0.0 0.0
325 16.627 2074.2 414.87
326 16.677 751.3 149.12
327 16.727 1123.6 223.97
328 16.777 374.5 74.66
329 16.827 1122.8 223.93
330 16.88 0.0 0.0
331 16.93 0.0 0.0
332 16.98 0.0 0.0
333 17.03 2296.0 447.62
334 17.08 379.9 74.23
335 17.13 1503.9 297.32
336 17.18 563.4 111.98
337 17.233 538.0 107.34
338 17.283 0.0 0.0
339 17.333 0.0 0.0
340 17.383 1890.3 373.65
341 17.433 1119.5 223.93
342 17.485 361.3 72.88
343 17.535 1651.2 337.72
344 17.585 365.1 75.24
345 17.635 547.2 113.12
346 17.686 0.0 0.0
347 17.736 0.0 0.0
348 17.786 0.0 0.0
349 17.839 2490.6 503.11
350 17.892 1033.4 213.33
351 17.942 1073.2 226.83
352 17.995 670.1 143.94
353 18.045 1223.2 266.8

View File

@@ -0,0 +1,40 @@
t,speed_units_per_wall_s,rate_deg_per_wall_s
0.52,766.4,15.56
1.06,875.3,81.57
1.56,319.7,79.47
2.08,589.5,124.0
2.6,833.9,118.23
3.1,846.4,144.01
3.62,682.9,147.83
4.12,442.3,114.32
4.62,782.3,183.76
5.13,869.9,160.12
5.66,691.9,117.79
6.18,770.1,146.5
6.68,659.7,149.99
7.19,546.7,135.85
7.7,760.5,163.7
8.2,645.7,121.72
8.72,884.3,159.2
9.26,546.3,107.3
9.78,633.0,139.66
10.28,716.1,169.56
10.8,607.5,129.02
11.32,866.0,156.23
11.82,677.8,119.63
12.32,537.0,103.57
12.82,821.2,187.87
13.34,545.6,133.44
13.86,608.8,128.22
14.38,778.1,141.08
14.88,543.6,93.59
15.38,935.1,176.36
15.9,708.2,159.77
16.4,439.7,105.71
16.9,847.5,185.58
17.42,584.5,112.34
17.94,874.4,158.62
18.44,733.0,149.31
18.96,527.8,128.4
19.46,642.2,156.88
19.98,737.8,149.45
1 t speed_units_per_wall_s rate_deg_per_wall_s
2 0.52 766.4 15.56
3 1.06 875.3 81.57
4 1.56 319.7 79.47
5 2.08 589.5 124.0
6 2.6 833.9 118.23
7 3.1 846.4 144.01
8 3.62 682.9 147.83
9 4.12 442.3 114.32
10 4.62 782.3 183.76
11 5.13 869.9 160.12
12 5.66 691.9 117.79
13 6.18 770.1 146.5
14 6.68 659.7 149.99
15 7.19 546.7 135.85
16 7.7 760.5 163.7
17 8.2 645.7 121.72
18 8.72 884.3 159.2
19 9.26 546.3 107.3
20 9.78 633.0 139.66
21 10.28 716.1 169.56
22 10.8 607.5 129.02
23 11.32 866.0 156.23
24 11.82 677.8 119.63
25 12.32 537.0 103.57
26 12.82 821.2 187.87
27 13.34 545.6 133.44
28 13.86 608.8 128.22
29 14.38 778.1 141.08
30 14.88 543.6 93.59
31 15.38 935.1 176.36
32 15.9 708.2 159.77
33 16.4 439.7 105.71
34 16.9 847.5 185.58
35 17.42 584.5 112.34
36 17.94 874.4 158.62
37 18.44 733.0 149.31
38 18.96 527.8 128.4
39 19.46 642.2 156.88
40 19.98 737.8 149.45

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

View File

@@ -0,0 +1,313 @@
phase,t,droll_deg
slow,0.05,0.0
slow,0.101,-0.0909
slow,0.155,-1.4615
slow,0.206,-7.3152
slow,0.256,-2.7442
slow,0.307,-8.4132
slow,0.357,0.0
slow,0.407,0.0
slow,0.457,0.0
slow,0.507,-38.7821
slow,0.557,-13.0283
slow,0.609,-19.4952
slow,0.659,-12.9643
slow,0.711,-3.2265
slow,0.764,-0.0
slow,0.814,-0.0
slow,0.865,-38.9513
slow,0.915,-13.0541
slow,0.965,-13.0281
slow,1.015,-25.9821
slow,1.066,-12.9527
slow,1.117,0.0
slow,1.167,0.0
slow,1.217,-25.9507
slow,1.268,-9.7894
slow,1.355,-26.0652
slow,1.405,-19.4799
slow,1.456,-9.7073
slow,1.506,0.0
slow,1.557,0.0
slow,1.607,-29.1521
slow,1.661,-9.7987
slow,1.711,-19.5758
slow,1.761,-6.5117
slow,1.811,-16.2414
slow,1.861,-3.2347
slow,1.911,0.0
slow,1.961,0.0
slow,2.012,-29.2256
slow,2.062,-13.0531
slow,2.112,-19.532
slow,2.162,-12.9885
slow,2.212,-9.7139
slow,2.263,-0.0
slow,2.313,-0.0
slow,2.363,-0.0
slow,2.413,-42.2278
slow,2.464,-19.6123
slow,2.514,-13.0415
slow,2.564,-13.0156
slow,2.614,-3.2392
slow,2.664,0.0
slow,2.714,0.0
slow,2.766,-35.7889
slow,2.816,-13.0315
slow,2.867,-6.5058
slow,2.917,-25.9631
slow,2.967,-3.2272
slow,3.017,0.0
slow,3.067,0.0
slow,3.117,-25.9064
slow,3.168,-9.7872
slow,3.218,-22.8201
slow,3.268,-9.762
slow,3.318,-19.4592
slow,3.368,-3.2268
slow,3.456,0.0
slow,3.511,-29.1619
slow,3.561,-6.5294
slow,3.612,-19.5509
slow,3.662,-6.5035
slow,3.713,-22.7155
slow,3.763,-0.0
slow,3.813,-0.0
slow,3.863,-0.0
slow,3.913,-35.7343
slow,3.963,-13.0477
slow,4.013,-22.7862
slow,4.064,-6.4918
slow,4.116,-9.7153
slow,4.167,-0.0
slow,4.217,-0.0
slow,4.267,-32.469
slow,4.317,-19.576
slow,4.368,-6.5118
slow,4.418,-19.4983
slow,4.469,-3.2333
slow,4.519,0.0
slow,4.57,0.0
slow,4.62,-29.208
slow,4.671,-9.8048
slow,4.721,-13.0645
slow,4.773,-6.5222
slow,4.823,-26.0281
slow,4.873,-3.2352
slow,4.923,0.0
slow,4.974,0.0
slow,5.024,-29.2291
slow,5.074,-13.0522
slow,5.125,-13.0262
slow,5.175,-16.259
slow,5.225,-12.9694
slow,5.275,-0.0
slow,5.325,-0.0
slow,5.376,-0.0
slow,5.426,-38.9864
slow,5.476,-9.8137
slow,5.527,-19.5613
slow,5.577,-13.0078
slow,5.632,-6.4941
slow,5.682,-0.0
slow,5.755,-0.0
slow,5.805,-39.0388
slow,5.855,-13.0591
slow,5.905,-26.0433
slow,5.956,-6.4938
slow,6.006,-12.9688
slow,6.056,0.0
slow,6.106,0.0
slow,6.156,-29.1717
slow,6.206,-16.3415
slow,6.257,-13.0519
slow,6.309,-19.5302
slow,6.359,-12.9873
slow,6.412,-6.4839
slow,6.463,0.0
slow,6.513,0.0
slow,6.563,-32.4511
slow,6.613,-22.8937
slow,6.665,-13.0382
slow,6.716,-22.7698
slow,6.766,-12.9684
slow,6.817,-6.4745
slow,6.867,-0.0
slow,6.918,-0.0
slow,6.968,-32.4432
slow,7.018,-16.2949
slow,7.069,-13.0149
slow,7.119,-22.7295
slow,7.17,-16.1918
slow,7.221,-3.2315
slow,7.271,0.0
slow,7.321,0.0
slow,7.371,-38.9877
slow,7.421,-9.8138
slow,7.474,-22.8806
slow,7.555,-19.5384
slow,7.605,-16.2268
slow,7.655,0.0
slow,7.705,0.0
slow,7.756,0.0
slow,7.806,-39.0224
slow,7.856,-13.064
slow,7.907,-19.5481
slow,7.957,-6.5026
slow,8.007,-9.7314
fast,0.05,-0.0596
fast,0.1,-0.3593
fast,0.15,-2.5234
fast,0.202,-4.9514
fast,0.252,-3.1468
fast,0.302,-0.0
fast,0.352,-0.0
fast,0.403,-0.0
fast,0.469,-20.1363
fast,0.519,-8.0449
fast,0.569,-8.6891
fast,0.619,-10.948
fast,0.67,-2.1745
fast,0.72,-0.0
fast,0.77,-0.0
fast,0.821,-23.8257
fast,0.872,-4.363
fast,0.922,-15.4009
fast,0.972,-4.4215
fast,1.023,-17.711
fast,1.073,0.0
fast,1.123,0.0
fast,1.173,0.0
fast,1.223,-26.3637
fast,1.275,-4.4318
fast,1.325,-15.5068
fast,1.375,-4.4229
fast,1.426,-11.048
fast,1.476,0.0
fast,1.527,0.0
fast,1.577,0.0
fast,1.627,-24.1538
fast,1.677,-8.8455
fast,1.727,-15.5045
fast,1.778,-8.8441
fast,1.83,-8.8553
fast,1.88,-0.0
fast,1.93,-0.0
fast,1.98,-0.0
fast,2.034,-28.7329
fast,2.084,-11.0611
fast,2.134,-13.2881
fast,2.184,-8.8429
fast,2.236,-2.2041
fast,2.286,-0.0
fast,2.336,-0.0
fast,2.387,-21.9271
fast,2.437,-6.6459
fast,2.488,-11.0875
fast,2.569,-13.2631
fast,2.619,-8.8432
fast,2.669,-0.0
fast,2.719,-0.0
fast,2.769,-17.7007
fast,2.82,-11.1022
fast,2.87,-4.4224
fast,2.92,-13.265
fast,2.97,-4.3887
fast,3.02,0.0
fast,3.07,0.0
fast,3.12,0.0
fast,3.176,-19.5844
fast,3.226,-17.5675
fast,3.277,-4.4215
fast,3.327,-19.9265
fast,3.377,-2.2046
fast,3.427,0.0
fast,3.478,0.0
fast,3.528,-19.7653
fast,3.578,-4.4075
fast,3.628,-15.5094
fast,3.678,-8.8442
fast,3.729,-11.0476
fast,3.78,0.0
fast,3.83,0.0
fast,3.88,0.0
fast,3.93,-26.3791
fast,3.981,-8.8468
fast,4.031,-13.2966
fast,4.081,-4.4213
fast,4.133,-4.3888
fast,4.183,0.0
fast,4.233,0.0
fast,4.283,-19.5663
fast,4.333,-13.2564
fast,4.383,-4.4215
fast,4.433,-17.6873
fast,4.488,-2.2041
fast,4.569,-0.0
fast,4.619,-0.0
fast,4.67,-17.544
fast,4.72,-8.8326
fast,4.77,-4.4305
fast,4.82,-17.6862
fast,4.872,-2.2041
fast,4.922,-0.0
fast,4.972,-0.0
fast,5.023,-19.7606
fast,5.073,-0.0
fast,5.124,-17.6727
fast,5.174,-4.4215
fast,5.224,-11.0475
fast,5.274,0.0
fast,5.325,0.0
fast,5.375,0.0
fast,5.425,-19.7606
fast,5.475,-8.8292
fast,5.529,-19.9262
fast,5.579,-4.4214
fast,5.629,-4.4216
fast,5.68,0.0
fast,5.73,0.0
fast,5.78,-19.937
fast,5.87,-15.529
fast,5.92,-13.2634
fast,5.971,-6.6258
fast,6.021,0.0
fast,6.071,0.0
fast,6.121,0.0
fast,6.174,-19.7607
fast,6.224,-17.6728
fast,6.274,-4.4215
fast,6.324,-13.2651
fast,6.375,-4.3887
fast,6.425,0.0
fast,6.475,0.0
fast,6.526,-23.9369
fast,6.576,-4.383
fast,6.626,-13.2366
fast,6.676,-8.8431
fast,6.727,-8.8552
fast,6.777,-0.0
fast,6.827,-0.0
fast,6.877,-0.0
fast,6.93,-22.0832
fast,6.98,-8.8599
fast,7.03,-19.929
fast,7.08,-4.4214
fast,7.132,-8.8553
fast,7.182,-0.0
fast,7.232,-0.0
fast,7.285,-19.8506
fast,7.336,-8.8927
fast,7.386,-4.424
fast,7.436,-17.687
fast,7.486,-8.7844
fast,7.537,-0.0
fast,7.589,-0.0
fast,7.639,-15.4299
fast,7.689,-0.0
fast,7.739,-13.3118
fast,7.79,-8.8378
fast,7.84,-15.4965
fast,7.89,0.0
fast,7.969,0.0
fast,8.019,0.0
1 phase t droll_deg
2 slow 0.05 0.0
3 slow 0.101 -0.0909
4 slow 0.155 -1.4615
5 slow 0.206 -7.3152
6 slow 0.256 -2.7442
7 slow 0.307 -8.4132
8 slow 0.357 0.0
9 slow 0.407 0.0
10 slow 0.457 0.0
11 slow 0.507 -38.7821
12 slow 0.557 -13.0283
13 slow 0.609 -19.4952
14 slow 0.659 -12.9643
15 slow 0.711 -3.2265
16 slow 0.764 -0.0
17 slow 0.814 -0.0
18 slow 0.865 -38.9513
19 slow 0.915 -13.0541
20 slow 0.965 -13.0281
21 slow 1.015 -25.9821
22 slow 1.066 -12.9527
23 slow 1.117 0.0
24 slow 1.167 0.0
25 slow 1.217 -25.9507
26 slow 1.268 -9.7894
27 slow 1.355 -26.0652
28 slow 1.405 -19.4799
29 slow 1.456 -9.7073
30 slow 1.506 0.0
31 slow 1.557 0.0
32 slow 1.607 -29.1521
33 slow 1.661 -9.7987
34 slow 1.711 -19.5758
35 slow 1.761 -6.5117
36 slow 1.811 -16.2414
37 slow 1.861 -3.2347
38 slow 1.911 0.0
39 slow 1.961 0.0
40 slow 2.012 -29.2256
41 slow 2.062 -13.0531
42 slow 2.112 -19.532
43 slow 2.162 -12.9885
44 slow 2.212 -9.7139
45 slow 2.263 -0.0
46 slow 2.313 -0.0
47 slow 2.363 -0.0
48 slow 2.413 -42.2278
49 slow 2.464 -19.6123
50 slow 2.514 -13.0415
51 slow 2.564 -13.0156
52 slow 2.614 -3.2392
53 slow 2.664 0.0
54 slow 2.714 0.0
55 slow 2.766 -35.7889
56 slow 2.816 -13.0315
57 slow 2.867 -6.5058
58 slow 2.917 -25.9631
59 slow 2.967 -3.2272
60 slow 3.017 0.0
61 slow 3.067 0.0
62 slow 3.117 -25.9064
63 slow 3.168 -9.7872
64 slow 3.218 -22.8201
65 slow 3.268 -9.762
66 slow 3.318 -19.4592
67 slow 3.368 -3.2268
68 slow 3.456 0.0
69 slow 3.511 -29.1619
70 slow 3.561 -6.5294
71 slow 3.612 -19.5509
72 slow 3.662 -6.5035
73 slow 3.713 -22.7155
74 slow 3.763 -0.0
75 slow 3.813 -0.0
76 slow 3.863 -0.0
77 slow 3.913 -35.7343
78 slow 3.963 -13.0477
79 slow 4.013 -22.7862
80 slow 4.064 -6.4918
81 slow 4.116 -9.7153
82 slow 4.167 -0.0
83 slow 4.217 -0.0
84 slow 4.267 -32.469
85 slow 4.317 -19.576
86 slow 4.368 -6.5118
87 slow 4.418 -19.4983
88 slow 4.469 -3.2333
89 slow 4.519 0.0
90 slow 4.57 0.0
91 slow 4.62 -29.208
92 slow 4.671 -9.8048
93 slow 4.721 -13.0645
94 slow 4.773 -6.5222
95 slow 4.823 -26.0281
96 slow 4.873 -3.2352
97 slow 4.923 0.0
98 slow 4.974 0.0
99 slow 5.024 -29.2291
100 slow 5.074 -13.0522
101 slow 5.125 -13.0262
102 slow 5.175 -16.259
103 slow 5.225 -12.9694
104 slow 5.275 -0.0
105 slow 5.325 -0.0
106 slow 5.376 -0.0
107 slow 5.426 -38.9864
108 slow 5.476 -9.8137
109 slow 5.527 -19.5613
110 slow 5.577 -13.0078
111 slow 5.632 -6.4941
112 slow 5.682 -0.0
113 slow 5.755 -0.0
114 slow 5.805 -39.0388
115 slow 5.855 -13.0591
116 slow 5.905 -26.0433
117 slow 5.956 -6.4938
118 slow 6.006 -12.9688
119 slow 6.056 0.0
120 slow 6.106 0.0
121 slow 6.156 -29.1717
122 slow 6.206 -16.3415
123 slow 6.257 -13.0519
124 slow 6.309 -19.5302
125 slow 6.359 -12.9873
126 slow 6.412 -6.4839
127 slow 6.463 0.0
128 slow 6.513 0.0
129 slow 6.563 -32.4511
130 slow 6.613 -22.8937
131 slow 6.665 -13.0382
132 slow 6.716 -22.7698
133 slow 6.766 -12.9684
134 slow 6.817 -6.4745
135 slow 6.867 -0.0
136 slow 6.918 -0.0
137 slow 6.968 -32.4432
138 slow 7.018 -16.2949
139 slow 7.069 -13.0149
140 slow 7.119 -22.7295
141 slow 7.17 -16.1918
142 slow 7.221 -3.2315
143 slow 7.271 0.0
144 slow 7.321 0.0
145 slow 7.371 -38.9877
146 slow 7.421 -9.8138
147 slow 7.474 -22.8806
148 slow 7.555 -19.5384
149 slow 7.605 -16.2268
150 slow 7.655 0.0
151 slow 7.705 0.0
152 slow 7.756 0.0
153 slow 7.806 -39.0224
154 slow 7.856 -13.064
155 slow 7.907 -19.5481
156 slow 7.957 -6.5026
157 slow 8.007 -9.7314
158 fast 0.05 -0.0596
159 fast 0.1 -0.3593
160 fast 0.15 -2.5234
161 fast 0.202 -4.9514
162 fast 0.252 -3.1468
163 fast 0.302 -0.0
164 fast 0.352 -0.0
165 fast 0.403 -0.0
166 fast 0.469 -20.1363
167 fast 0.519 -8.0449
168 fast 0.569 -8.6891
169 fast 0.619 -10.948
170 fast 0.67 -2.1745
171 fast 0.72 -0.0
172 fast 0.77 -0.0
173 fast 0.821 -23.8257
174 fast 0.872 -4.363
175 fast 0.922 -15.4009
176 fast 0.972 -4.4215
177 fast 1.023 -17.711
178 fast 1.073 0.0
179 fast 1.123 0.0
180 fast 1.173 0.0
181 fast 1.223 -26.3637
182 fast 1.275 -4.4318
183 fast 1.325 -15.5068
184 fast 1.375 -4.4229
185 fast 1.426 -11.048
186 fast 1.476 0.0
187 fast 1.527 0.0
188 fast 1.577 0.0
189 fast 1.627 -24.1538
190 fast 1.677 -8.8455
191 fast 1.727 -15.5045
192 fast 1.778 -8.8441
193 fast 1.83 -8.8553
194 fast 1.88 -0.0
195 fast 1.93 -0.0
196 fast 1.98 -0.0
197 fast 2.034 -28.7329
198 fast 2.084 -11.0611
199 fast 2.134 -13.2881
200 fast 2.184 -8.8429
201 fast 2.236 -2.2041
202 fast 2.286 -0.0
203 fast 2.336 -0.0
204 fast 2.387 -21.9271
205 fast 2.437 -6.6459
206 fast 2.488 -11.0875
207 fast 2.569 -13.2631
208 fast 2.619 -8.8432
209 fast 2.669 -0.0
210 fast 2.719 -0.0
211 fast 2.769 -17.7007
212 fast 2.82 -11.1022
213 fast 2.87 -4.4224
214 fast 2.92 -13.265
215 fast 2.97 -4.3887
216 fast 3.02 0.0
217 fast 3.07 0.0
218 fast 3.12 0.0
219 fast 3.176 -19.5844
220 fast 3.226 -17.5675
221 fast 3.277 -4.4215
222 fast 3.327 -19.9265
223 fast 3.377 -2.2046
224 fast 3.427 0.0
225 fast 3.478 0.0
226 fast 3.528 -19.7653
227 fast 3.578 -4.4075
228 fast 3.628 -15.5094
229 fast 3.678 -8.8442
230 fast 3.729 -11.0476
231 fast 3.78 0.0
232 fast 3.83 0.0
233 fast 3.88 0.0
234 fast 3.93 -26.3791
235 fast 3.981 -8.8468
236 fast 4.031 -13.2966
237 fast 4.081 -4.4213
238 fast 4.133 -4.3888
239 fast 4.183 0.0
240 fast 4.233 0.0
241 fast 4.283 -19.5663
242 fast 4.333 -13.2564
243 fast 4.383 -4.4215
244 fast 4.433 -17.6873
245 fast 4.488 -2.2041
246 fast 4.569 -0.0
247 fast 4.619 -0.0
248 fast 4.67 -17.544
249 fast 4.72 -8.8326
250 fast 4.77 -4.4305
251 fast 4.82 -17.6862
252 fast 4.872 -2.2041
253 fast 4.922 -0.0
254 fast 4.972 -0.0
255 fast 5.023 -19.7606
256 fast 5.073 -0.0
257 fast 5.124 -17.6727
258 fast 5.174 -4.4215
259 fast 5.224 -11.0475
260 fast 5.274 0.0
261 fast 5.325 0.0
262 fast 5.375 0.0
263 fast 5.425 -19.7606
264 fast 5.475 -8.8292
265 fast 5.529 -19.9262
266 fast 5.579 -4.4214
267 fast 5.629 -4.4216
268 fast 5.68 0.0
269 fast 5.73 0.0
270 fast 5.78 -19.937
271 fast 5.87 -15.529
272 fast 5.92 -13.2634
273 fast 5.971 -6.6258
274 fast 6.021 0.0
275 fast 6.071 0.0
276 fast 6.121 0.0
277 fast 6.174 -19.7607
278 fast 6.224 -17.6728
279 fast 6.274 -4.4215
280 fast 6.324 -13.2651
281 fast 6.375 -4.3887
282 fast 6.425 0.0
283 fast 6.475 0.0
284 fast 6.526 -23.9369
285 fast 6.576 -4.383
286 fast 6.626 -13.2366
287 fast 6.676 -8.8431
288 fast 6.727 -8.8552
289 fast 6.777 -0.0
290 fast 6.827 -0.0
291 fast 6.877 -0.0
292 fast 6.93 -22.0832
293 fast 6.98 -8.8599
294 fast 7.03 -19.929
295 fast 7.08 -4.4214
296 fast 7.132 -8.8553
297 fast 7.182 -0.0
298 fast 7.232 -0.0
299 fast 7.285 -19.8506
300 fast 7.336 -8.8927
301 fast 7.386 -4.424
302 fast 7.436 -17.687
303 fast 7.486 -8.7844
304 fast 7.537 -0.0
305 fast 7.589 -0.0
306 fast 7.639 -15.4299
307 fast 7.689 -0.0
308 fast 7.739 -13.3118
309 fast 7.79 -8.8378
310 fast 7.84 -15.4965
311 fast 7.89 0.0
312 fast 7.969 0.0
313 fast 8.019 0.0

View File

@@ -0,0 +1,7 @@
throttle,rep,settled_speed,burst_speed,rate_deg_per_wall_s
min,0,103.5,99.3,165.34
min,1,123.8,91.9,149.72
cruise,0,434.7,352.0,149.9
cruise,1,423.1,382.5,150.07
max,0,1230.6,1258.2,102.76
max,1,1624.8,1319.1,105.38
1 throttle rep settled_speed burst_speed rate_deg_per_wall_s
2 min 0 103.5 99.3 165.34
3 min 1 123.8 91.9 149.72
4 cruise 0 434.7 352.0 149.9
5 cruise 1 423.1 382.5 150.07
6 max 0 1230.6 1258.2 102.76
7 max 1 1624.8 1319.1 105.38

View File

@@ -0,0 +1,23 @@
rep,t,speed,rate_deg_per_wall_s
0,0.27,390.1,15.5
0,0.54,233.9,58.9
0,0.81,573.5,325.0
0,1.07,341.0,209.1
0,1.34,222.8,130.3
0,1.61,525.5,321.8
0,1.87,386.6,237.6
0,2.14,190.9,110.4
0,2.41,511.3,310.2
0,2.66,246.7,151.1
0,2.94,312.3,183.0
1,0.27,446.6,51.6
1,0.54,372.5,181.1
1,0.8,377.7,231.0
1,1.07,417.7,246.3
1,1.32,385.0,236.1
1,1.59,232.9,169.4
1,1.85,590.3,368.1
1,2.11,307.0,194.7
1,2.38,311.9,180.7
1,2.64,418.6,255.2
1,2.9,306.5,181.9
1 rep t speed rate_deg_per_wall_s
2 0 0.27 390.1 15.5
3 0 0.54 233.9 58.9
4 0 0.81 573.5 325.0
5 0 1.07 341.0 209.1
6 0 1.34 222.8 130.3
7 0 1.61 525.5 321.8
8 0 1.87 386.6 237.6
9 0 2.14 190.9 110.4
10 0 2.41 511.3 310.2
11 0 2.66 246.7 151.1
12 0 2.94 312.3 183.0
13 1 0.27 446.6 51.6
14 1 0.54 372.5 181.1
15 1 0.8 377.7 231.0
16 1 1.07 417.7 246.3
17 1 1.32 385.0 236.1
18 1 1.59 232.9 169.4
19 1 1.85 590.3 368.1
20 1 2.11 307.0 194.7
21 1 2.38 311.9 180.7
22 1 2.64 418.6 255.2
23 1 2.9 306.5 181.9

View File

@@ -3023,6 +3023,150 @@ UN_e004_ADAN_ElanPlus_NF,TurnAway_Time_Minimum,0x1b0,f32,0.5,defaulted-on-disc,l
UN_e004_ADAN_ElanPlus_NF,Turn_AngularVelocity,0x1ac,f32,1.0471975803375244,disc,verified
UN_e004_ADAN_ElanPlus_NF,UsingChaffRatio,0x234,f32,0.0,defaulted-on-disc,layout-derived
UN_e004_ADAN_ElanPlus_NF,YawDragFactor,0x104,f32,3.0,disc,verified
UN_e005_ADAN_ElanTypeQ_Margras,AA_AxisMode_Max,0x134,f32,0.0,defaulted-on-disc,layout-derived
UN_e005_ADAN_ElanTypeQ_Margras,AA_AxisMode_Min,0x138,f32,0.0,defaulted-on-disc,layout-derived
UN_e005_ADAN_ElanTypeQ_Margras,AA_PitchMinus_Max,0x0c8,f32,0.1745329350233078,disc,verified
UN_e005_ADAN_ElanTypeQ_Margras,AA_PitchMinus_Min,0x0cc,f32,0.6981317400932312,defaulted-on-disc,layout-derived
UN_e005_ADAN_ElanTypeQ_Margras,AA_PitchPlus_Max,0x0b8,f32,0.5235987901687622,disc,verified
UN_e005_ADAN_ElanTypeQ_Margras,AA_PitchPlus_Min,0x0bc,f32,1.7453293800354004,defaulted-on-disc,layout-derived
UN_e005_ADAN_ElanTypeQ_Margras,AA_Roll_Max,0x0e8,f32,1.0471975803375244,defaulted-on-disc,layout-derived
UN_e005_ADAN_ElanTypeQ_Margras,AA_Roll_Min,0x0ec,f32,2.6179940700531006,disc,verified
UN_e005_ADAN_ElanTypeQ_Margras,AA_Yaw_Max,0x0d8,f32,0.8726646900177002,disc,verified
UN_e005_ADAN_ElanTypeQ_Margras,AA_Yaw_Min,0x0dc,f32,4.188790321350098,disc,verified
UN_e005_ADAN_ElanTypeQ_Margras,AB_AA_PitchMinus,0x158,f32,0.0,defaulted-on-disc,layout-derived
UN_e005_ADAN_ElanTypeQ_Margras,AB_AA_PitchPlus,0x150,f32,0.0,defaulted-on-disc,layout-derived
UN_e005_ADAN_ElanTypeQ_Margras,AB_AA_Roll,0x168,f32,0.0,defaulted-on-disc,layout-derived
UN_e005_ADAN_ElanTypeQ_Margras,AB_AA_Yaw,0x160,f32,0.0,defaulted-on-disc,layout-derived
UN_e005_ADAN_ElanTypeQ_Margras,AB_AV_PitchMinus,0x154,f32,0.0,defaulted-on-disc,layout-derived
UN_e005_ADAN_ElanTypeQ_Margras,AB_AV_PitchPlus,0x14c,f32,0.0,defaulted-on-disc,layout-derived
UN_e005_ADAN_ElanTypeQ_Margras,AB_AV_Roll,0x164,f32,0.0,defaulted-on-disc,layout-derived
UN_e005_ADAN_ElanTypeQ_Margras,AB_AV_Yaw,0x15c,f32,0.0,defaulted-on-disc,layout-derived
UN_e005_ADAN_ElanTypeQ_Margras,AB_ConsumeShield,0x148,f32,0.0,defaulted-on-disc,layout-derived
UN_e005_ADAN_ElanTypeQ_Margras,AB_ConsumeShield_Begin,0x144,f32,0.0,defaulted-on-disc,layout-derived
UN_e005_ADAN_ElanTypeQ_Margras,AV_AxisMode_Max,0x12c,f32,0.0,defaulted-on-disc,layout-derived
UN_e005_ADAN_ElanTypeQ_Margras,AV_AxisMode_Min,0x130,f32,0.0,defaulted-on-disc,layout-derived
UN_e005_ADAN_ElanTypeQ_Margras,AV_PitchMinus_Max,0x0c0,f32,0.6981317400932312,disc,verified
UN_e005_ADAN_ElanTypeQ_Margras,AV_PitchMinus_Min,0x0c4,f32,2.094395160675049,disc,verified
UN_e005_ADAN_ElanTypeQ_Margras,AV_PitchPlus_Max,0x0b0,f32,1.3962634801864624,disc,verified
UN_e005_ADAN_ElanTypeQ_Margras,AV_PitchPlus_Min,0x0b4,f32,3.490658760070801,disc,verified
UN_e005_ADAN_ElanTypeQ_Margras,AV_Roll_Max,0x0e0,f32,2.094395160675049,defaulted-on-disc,layout-derived
UN_e005_ADAN_ElanTypeQ_Margras,AV_Roll_Min,0x0e4,f32,2.792526960372925,disc,verified
UN_e005_ADAN_ElanTypeQ_Margras,AV_Yaw_Max,0x0d0,f32,0.4363323450088501,disc,verified
UN_e005_ADAN_ElanTypeQ_Margras,AV_Yaw_Min,0x0d4,f32,1.0471975803375244,disc,verified
UN_e005_ADAN_ElanTypeQ_Margras,AccPitchFactor,0x124,f32,30.0,defaulted-on-disc,layout-derived
UN_e005_ADAN_ElanTypeQ_Margras,Acceleration,0x0a8,f32,600.0,disc,verified
UN_e005_ADAN_ElanTypeQ_Margras,ArterBurner_Acc,0x11c,f32,2.0,defaulted-on-disc,layout-derived
UN_e005_ADAN_ElanTypeQ_Margras,ArterBurner_Vc,0x114,f32,2.0,disc,verified
UN_e005_ADAN_ElanTypeQ_Margras,AttackCraftPoint,0x2b8,f32,0.800000011920929,disc,verified
UN_e005_ADAN_ElanTypeQ_Margras,AttackVesselPoint,0x2b4,f32,0.0010000000474974513,disc,verified
UN_e005_ADAN_ElanTypeQ_Margras,BarrelRoll,0x178,word,1,defaulted-on-disc,layout-derived
UN_e005_ADAN_ElanTypeQ_Margras,BarrelRoll_CountMaximum,0x180,f32,5.0,defaulted-on-disc,layout-derived
UN_e005_ADAN_ElanTypeQ_Margras,BarrelRoll_CountMinimum,0x17c,f32,0.5,defaulted-on-disc,layout-derived
UN_e005_ADAN_ElanTypeQ_Margras,BarrelRoll_Radius,0x188,f32,250.0,disc,verified
UN_e005_ADAN_ElanTypeQ_Margras,BarrelRoll_Time,0x184,f32,1.2000000476837158,disc,verified
UN_e005_ADAN_ElanTypeQ_Margras,BoostAway,0x1b8,word,1,defaulted-on-disc,layout-derived
UN_e005_ADAN_ElanTypeQ_Margras,BoostAway_Time_Maximum,0x1c0,f32,4.0,disc,verified
UN_e005_ADAN_ElanTypeQ_Margras,BoostAway_Time_Minimum,0x1bc,f32,1.0,defaulted-on-disc,layout-derived
UN_e005_ADAN_ElanTypeQ_Margras,ChargeDelay,0x23c,f32,3.0,defaulted-on-disc,layout-derived
UN_e005_ADAN_ElanTypeQ_Margras,ChargeDelay_Break,0x240,f32,10.0,defaulted-on-disc,layout-derived
UN_e005_ADAN_ElanTypeQ_Margras,ChargeSpeed,0x244,f32,25.0,defaulted-on-disc,layout-derived
UN_e005_ADAN_ElanTypeQ_Margras,Color_B,0x048,f32,0.10000000149011612,disc,verified
UN_e005_ADAN_ElanTypeQ_Margras,Color_G,0x044,f32,0.5,disc,verified
UN_e005_ADAN_ElanTypeQ_Margras,Color_R,0x040,f32,1.0,disc,verified
UN_e005_ADAN_ElanTypeQ_Margras,CruisingVelocity,0x0a4,f32,700.0,disc,verified
UN_e005_ADAN_ElanTypeQ_Margras,CutoffTimeMax,0x19c,f32,5.0,disc,verified
UN_e005_ADAN_ElanTypeQ_Margras,CutoffTimeMin,0x198,f32,0.6000000238418579,disc,verified
UN_e005_ADAN_ElanTypeQ_Margras,DecPitchFactor,0x128,f32,30.0,defaulted-on-disc,layout-derived
UN_e005_ADAN_ElanTypeQ_Margras,Deceleration,0x0ac,f32,400.0,disc,verified
UN_e005_ADAN_ElanTypeQ_Margras,DefencePoint,0x2bc,f32,0.0002500000118743628,disc,verified
UN_e005_ADAN_ElanTypeQ_Margras,Delay,0x248,f32,0.0,defaulted-on-disc,layout-derived
UN_e005_ADAN_ElanTypeQ_Margras,DelayAdjustment,0x24c,f32,0.0,defaulted-on-disc,layout-derived
UN_e005_ADAN_ElanTypeQ_Margras,DestroyMotionTime,0x270,f32,5.0,disc,verified
UN_e005_ADAN_ElanTypeQ_Margras,DragFactorThreshold,0x110,f32,0.5,defaulted-on-disc,layout-derived
UN_e005_ADAN_ElanTypeQ_Margras,DryMass,0x274,f32,100.0,defaulted-on-disc,layout-derived
UN_e005_ADAN_ElanTypeQ_Margras,ExplosionSE,0x284,word,601,defaulted-on-disc,layout-derived
UN_e005_ADAN_ElanTypeQ_Margras,FCSRange,0x2a4,f32,2500.0,disc,verified
UN_e005_ADAN_ElanTypeQ_Margras,FiringRange,0x2a8,f32,0.0,defaulted-on-disc,layout-derived
UN_e005_ADAN_ElanTypeQ_Margras,GrossMass,0x278,f32,200.0,defaulted-on-disc,layout-derived
UN_e005_ADAN_ElanTypeQ_Margras,HP,0x054,f32,3200.0,disc,verified
UN_e005_ADAN_ElanTypeQ_Margras,HQRatio,0x058,f32,1.0,defaulted-on-disc,layout-derived
UN_e005_ADAN_ElanTypeQ_Margras,HoldPosition,0x1c4,word,1,defaulted-on-disc,layout-derived
UN_e005_ADAN_ElanTypeQ_Margras,HoldPosition_BackRatio,0x1dc,f32,0.5,defaulted-on-disc,layout-derived
UN_e005_ADAN_ElanTypeQ_Margras,HoldPosition_CancelTime,0x1e4,f32,20.0,disc,verified
UN_e005_ADAN_ElanTypeQ_Margras,HoldPosition_CutoffRatio,0x1e0,f32,0.5,defaulted-on-disc,layout-derived
UN_e005_ADAN_ElanTypeQ_Margras,HoldPosition_LengthMax,0x1cc,f32,400.0,disc,verified
UN_e005_ADAN_ElanTypeQ_Margras,HoldPosition_LengthMin,0x1c8,f32,150.0,defaulted-on-disc,layout-derived
UN_e005_ADAN_ElanTypeQ_Margras,HoldPosition_MaximumTime,0x1d4,f32,10.0,defaulted-on-disc,layout-derived
UN_e005_ADAN_ElanTypeQ_Margras,HoldPosition_MinimumTime,0x1d0,f32,5.0,defaulted-on-disc,layout-derived
UN_e005_ADAN_ElanTypeQ_Margras,HoldPosition_SideRatio,0x1d8,f32,0.30000001192092896,defaulted-on-disc,layout-derived
UN_e005_ADAN_ElanTypeQ_Margras,HomingResistAdjustment,0x230,f32,0.6000000238418579,defaulted-on-disc,layout-derived
UN_e005_ADAN_ElanTypeQ_Margras,IsDestructible,0x064,word,1,defaulted-on-disc,layout-derived
UN_e005_ADAN_ElanTypeQ_Margras,JumpIn,0x288,word,4294967295,defaulted-on-disc,layout-derived
UN_e005_ADAN_ElanTypeQ_Margras,JumpOut,0x28c,word,4294967295,defaulted-on-disc,layout-derived
UN_e005_ADAN_ElanTypeQ_Margras,LowerHPSE,0x294,word,4294967295,defaulted-on-disc,layout-derived
UN_e005_ADAN_ElanTypeQ_Margras,LowerHPThresholdRatio,0x298,f32,0.30000001192092896,defaulted-on-disc,layout-derived
UN_e005_ADAN_ElanTypeQ_Margras,MaxValue,0x238,f32,100.0,defaulted-on-disc,layout-derived
UN_e005_ADAN_ElanTypeQ_Margras,MaximumBank_Normal,0x100,f32,1.0471975803375244,defaulted-on-disc,layout-derived
UN_e005_ADAN_ElanTypeQ_Margras,MaximumVelocity,0x0a0,f32,1000.0,disc,verified
UN_e005_ADAN_ElanTypeQ_Margras,MinimumVelocity,0x09c,f32,100.0,disc,verified
UN_e005_ADAN_ElanTypeQ_Margras,MountedFCS,0x2ac,word,1,defaulted-on-disc,layout-derived
UN_e005_ADAN_ElanTypeQ_Margras,MountedShieldGenerator,0x070,word,0,defaulted-on-disc,layout-derived
UN_e005_ADAN_ElanTypeQ_Margras,PitchDragFactor,0x108,f32,3.0,defaulted-on-disc,layout-derived
UN_e005_ADAN_ElanTypeQ_Margras,PowerCutConsumeShield,0x13c,f32,0.0,defaulted-on-disc,layout-derived
UN_e005_ADAN_ElanTypeQ_Margras,PowerCutDeceleration,0x140,f32,0.0,defaulted-on-disc,layout-derived
UN_e005_ADAN_ElanTypeQ_Margras,RadarRange,0x2a0,f32,7000.0,disc,verified
UN_e005_ADAN_ElanTypeQ_Margras,ResistanceParalyze,0x084,f32,0.9800000190734863,disc,verified
UN_e005_ADAN_ElanTypeQ_Margras,ResistanceToExplosion,0x07c,f32,0.10000000149011612,defaulted-on-disc,layout-derived
UN_e005_ADAN_ElanTypeQ_Margras,ResistanceToOptics,0x074,f32,0.10000000149011612,defaulted-on-disc,layout-derived
UN_e005_ADAN_ElanTypeQ_Margras,ResistanceToPlayer,0x080,f32,1.0,defaulted-on-disc,layout-derived
UN_e005_ADAN_ElanTypeQ_Margras,ResistanceToShell,0x078,f32,0.10000000149011612,defaulted-on-disc,layout-derived
UN_e005_ADAN_ElanTypeQ_Margras,ReverseThrust_Acc,0x120,f32,2.0,defaulted-on-disc,layout-derived
UN_e005_ADAN_ElanTypeQ_Margras,ReverseThrust_Vc,0x118,f32,-0.5,disc,verified
UN_e005_ADAN_ElanTypeQ_Margras,RollDragFactor,0x10c,f32,3.0,defaulted-on-disc,layout-derived
UN_e005_ADAN_ElanTypeQ_Margras,SELength,0x29c,f32,2000.0,defaulted-on-disc,layout-derived
UN_e005_ADAN_ElanTypeQ_Margras,ShieldRatio,0x05c,f32,1.0,defaulted-on-disc,layout-derived
UN_e005_ADAN_ElanTypeQ_Margras,ShipEnvironmentSE,0x290,word,4294967295,defaulted-on-disc,layout-derived
UN_e005_ADAN_ElanTypeQ_Margras,SideRoll,0x16c,word,1,defaulted-on-disc,layout-derived
UN_e005_ADAN_ElanTypeQ_Margras,SideRoll_Length,0x174,f32,750.0,disc,verified
UN_e005_ADAN_ElanTypeQ_Margras,SideRoll_Time,0x170,f32,0.5,defaulted-on-disc,layout-derived
UN_e005_ADAN_ElanTypeQ_Margras,SideThrustAcceleration,0x0fc,f32,1000.0,defaulted-on-disc,layout-derived
UN_e005_ADAN_ElanTypeQ_Margras,SideThrustVelocity_Max,0x0f8,f32,1000.0,disc,verified
UN_e005_ADAN_ElanTypeQ_Margras,SideThruster,0x280,word,4294967295,defaulted-on-disc,layout-derived
UN_e005_ADAN_ElanTypeQ_Margras,Size_Radius,0x050,f32,19.0,disc,verified
UN_e005_ADAN_ElanTypeQ_Margras,Size_X,0x030,f32,24.0,disc,verified
UN_e005_ADAN_ElanTypeQ_Margras,Size_Y,0x034,f32,15.0,disc,verified
UN_e005_ADAN_ElanTypeQ_Margras,Size_Z,0x038,f32,38.0,disc,verified
UN_e005_ADAN_ElanTypeQ_Margras,Slalom,0x1e8,word,0,defaulted-on-disc,layout-derived
UN_e005_ADAN_ElanTypeQ_Margras,Slalom_CutoffRatio,0x1ec,f32,0.0,disc,verified
UN_e005_ADAN_ElanTypeQ_Margras,Slalom_TurnCount_Max,0x1f4,f32,0.0,defaulted-on-disc,layout-derived
UN_e005_ADAN_ElanTypeQ_Margras,Slalom_TurnCount_Min,0x1f0,f32,0.0,disc,verified
UN_e005_ADAN_ElanTypeQ_Margras,SolidCutoff,0x220,word,1,defaulted-on-disc,layout-derived
UN_e005_ADAN_ElanTypeQ_Margras,SolidCutoff_LengthMax,0x22c,f32,2500.0,disc,verified
UN_e005_ADAN_ElanTypeQ_Margras,SolidCutoff_LengthMin,0x228,f32,500.0,disc,verified
UN_e005_ADAN_ElanTypeQ_Margras,SolidCutoff_Ratio,0x224,f32,0.5,defaulted-on-disc,layout-derived
UN_e005_ADAN_ElanTypeQ_Margras,Through,0x1f8,word,1,defaulted-on-disc,layout-derived
UN_e005_ADAN_ElanTypeQ_Margras,Through_AngleMaximum,0x204,f32,1.0471975803375244,defaulted-on-disc,layout-derived
UN_e005_ADAN_ElanTypeQ_Margras,Through_AngleMinimum,0x200,f32,0.5235987901687622,defaulted-on-disc,layout-derived
UN_e005_ADAN_ElanTypeQ_Margras,Through_CutoffRatio,0x1fc,f32,0.5,defaulted-on-disc,layout-derived
UN_e005_ADAN_ElanTypeQ_Margras,Through_LengthMaximum,0x21c,f32,500.0,disc,verified
UN_e005_ADAN_ElanTypeQ_Margras,Through_LengthMinimum,0x218,f32,250.0,defaulted-on-disc,layout-derived
UN_e005_ADAN_ElanTypeQ_Margras,Through_Time1Max,0x208,f32,2.0,defaulted-on-disc,layout-derived
UN_e005_ADAN_ElanTypeQ_Margras,Through_Time1Min,0x20c,f32,5.0,defaulted-on-disc,layout-derived
UN_e005_ADAN_ElanTypeQ_Margras,Through_Time2Max,0x210,f32,1.5,disc,verified
UN_e005_ADAN_ElanTypeQ_Margras,Through_Time2Min,0x214,f32,5.0,defaulted-on-disc,layout-derived
UN_e005_ADAN_ElanTypeQ_Margras,Thruster,0x27c,word,512,defaulted-on-disc,layout-derived
UN_e005_ADAN_ElanTypeQ_Margras,ThrusterRatio,0x060,f32,1.0,defaulted-on-disc,layout-derived
UN_e005_ADAN_ElanTypeQ_Margras,TurnAttack,0x18c,word,1,defaulted-on-disc,layout-derived
UN_e005_ADAN_ElanTypeQ_Margras,TurnAttack_CutoffRatio,0x190,f32,0.4000000059604645,disc,verified
UN_e005_ADAN_ElanTypeQ_Margras,TurnAttack_DoubleRatio,0x194,f32,0.30000001192092896,disc,verified
UN_e005_ADAN_ElanTypeQ_Margras,TurnAttack_DoubleTimeMax,0x1a4,f32,2.5,disc,verified
UN_e005_ADAN_ElanTypeQ_Margras,TurnAttack_DoubleTimeMin,0x1a0,f32,0.5,disc,verified
UN_e005_ADAN_ElanTypeQ_Margras,TurnAway,0x1a8,word,1,defaulted-on-disc,layout-derived
UN_e005_ADAN_ElanTypeQ_Margras,TurnAway_Time_Maximum,0x1b4,f32,2.0,defaulted-on-disc,layout-derived
UN_e005_ADAN_ElanTypeQ_Margras,TurnAway_Time_Minimum,0x1b0,f32,0.5,defaulted-on-disc,layout-derived
UN_e005_ADAN_ElanTypeQ_Margras,Turn_AngularVelocity,0x1ac,f32,2.094395160675049,disc,verified
UN_e005_ADAN_ElanTypeQ_Margras,UsingChaffRatio,0x234,f32,0.4000000059604645,defaulted-on-disc,layout-derived
UN_e005_ADAN_ElanTypeQ_Margras,YawDragFactor,0x104,f32,3.0,disc,verified
UN_e006_ADAN_Vindicator_Margras,AA_AxisMode_Max,0x134,f32,0.0,defaulted-on-disc,layout-derived
UN_e006_ADAN_Vindicator_Margras,AA_AxisMode_Min,0x138,f32,0.0,defaulted-on-disc,layout-derived
UN_e006_ADAN_Vindicator_Margras,AA_PitchMinus_Max,0x0c8,f32,0.1745329350233078,disc,verified
Can't render this file because it is too large.

View File

@@ -0,0 +1,459 @@
# Challenge / EX missions — the stage set, the GamePart graph, and the kind field
**Status:** ✅ for the static structure (stage set, GamePart ids, the config-section
switch) and for the **unlock mechanism** — a bit test against a *cleared-stage* mask,
now **read live off the running game and matching the profile's progress exactly**
(§5.6). 🟡 for the stage-field crash mechanism, for word B's writer, and for which
mission consumes which bit; ❔ how the challenge menu is actually entered.
**Method:** static analysis (`.pe` strings/pointers + DuckDB disassembly + disc
records), then confirmed on the running title via the container-safe
[`--hid=file` pad](../../../xenia-canary-native/src/xenia/hid/file/file_input_driver.h)
and a live guest-memory read.
**Evidence:** [`captures/challenge-map.txt`](captures/challenge-map.txt),
[`captures/challenge-screen-config.txt`](captures/challenge-screen-config.txt),
[`structures/achievements.md`](structures/achievements.md);
`examples/challenge_map.rs`, `examples/challenge_screen.rs`.
## Why this was worth doing
The Route-B unit harvest is complete for the **story** campaign (68 of 110 units,
7 115 defaulted-on-disc values) and stalled there: the remaining units are `*_EX4` /
`*_EX5` / `*EX` variants that only the challenge missions field. The save's stage
field addresses those stage records — but setting it to **27** boots to the title and
then the emulator **exits during the load**, while 116 all load normally. This note
answers *what the challenge missions are on disc* and *why the stage field alone is
not enough*, so the next runtime attempt is targeted rather than another probe sweep.
## 1. The disc has exactly 29 stage records, in three families ✅
`StageResource` (schema `0x3c9ae32e`) in `dat/GP_MAIN_GAME_E.pak`:
| ids | count | `BackGroundID` | reading |
|---|---|---|---|
| `S01``S16` | 16 | Lebendorf … PD (real locations) | the **story** campaign |
| `S18``S23` | 6 | `Original` (all six) | the **tutorial** missions |
| `S24``S29` | 6 | Anastasis, Hargenteen ×2, Planet_Lebendorf, Lebendorf, Earth | the **challenge** missions |
| `Test` | 1 | Earth | developer stage |
`S17` does not exist. This **matches `weapon.tbl`'s sprite-key set exactly**
`stage01…16` + `tutorial01…06` + `challenge01…06` (recorded in
[static screen config](#)) — which is an independent confirmation of the three-way
split: 16 + 6 + 6 + Test = 29.
The `Original` six carry ~43 tokens each while the story and challenge records carry
5159, consistent with tutorials having no squadron/route/formation tables.
## 2. `GP_CHALLENGE.pak` is a screen, not mission data ✅
151 entries, **0 IDXD objects** — sprites and layout only. So challenge missions are
*not* a separate data set: they are ordinary `StageResource` records in
`GP_MAIN_GAME_E.pak`, reached through a different menu. That is why the EX unit
rosters show up in the same `EnumUnit_S<NN>` tables the story stages use.
## 3. The GamePart id table — the game's whole screen graph ✅
`.rdata` pointer array at **`0x820A1630`**, 29 entries, index = GamePart id:
```
0 GP_TITLE 10 GP_BUNK 20 GP_STAGE_CLEAR
1 GP_ADVERTISE_DEMO 11 GP_READY_ROOM 21 GP_MISSION_LOG
2 GP_SELECT_STORAGE 12 GP_HANGAR 22 GP_GAMEOVER
3 GP_LOAD 13 GP_ARSENAL 23 GP_DEBRIEFING
4 GP_SAVE 14 GP_PILOT_LOG 24 GP_DIALOG
5 GP_EXTRAS 15 GP_SYSTEM 25 GP_TUTORIAL
6 GP_MOVIE_THEATER 16 GP_DEMO *26 GP_CHALLENGE
7 GP_MISSION_SELECT 17 GP_MAIN_GAME 27 GP_LEADERBOARD
8 GP_OPTIONS 18 GP_SELECTOR 28 GP_TEST
9 GP_MOVIE 19 GP_PAUSE_MENU
```
The indices are **not inferred from position** — the image carries the factory
registration text, e.g.
`silph::GamePartTask::RegisterToFactory<26, class silph::GamePart_ChallengeMission>::RegisterToFactory is failed!`
and `<10, … GamePart_Bunk>`, both of which agree with this table. So `GP_CHALLENGE`
is GamePart **26** and `GP_TUTORIAL` is **25**.
## 4. A mission-**kind** field selects the stage config section ✅
Immediately before the array above, `0x820A1600``0x820A162C` holds the stage-config
key list: `BASE_INFO`, `StageResource`, `Background`, `PlayerUnit`,
`EQUIIP_LIMITATION` *(sic)*, `MODEL_PATH`, `NEW_ITEM`, `LOADING`, `CHALLENGE`,
`EXTRA`, `FILE`.
The stage loader picks **one of the last three** by a field at **`object + 144`**, and
the same three-way switch appears at two independent sites (`0x82184df0` inside the
all-keys config reader, and `0x82185ed0` in `sub_82185E80`):
```asm
lwz r11, 144(r30)
cmpi r11, 3 ; == 3 -> "EXTRA" (0x820A2160)
beq extra
addi r11, r11, -5
cmpli r11, 1 ; == 5 or 6 -> "CHALLENGE" (0x820A2154)
bgt file ; otherwise -> "FILE" (0x820A2168)
```
Two further sites (`0x82186a60`, `0x82186cb8`) classify the same field as
`{3, 5, 6}` versus everything else — i.e. **EXTRA and CHALLENGE together are "not an
ordinary story load"**. Every write to `+144` inside this class (`0x82184b10`,
`0x82185d48`, `0x82186ab4`) only *clears* it, and nothing in the image stores an
immediate 3/5/6 into it, so the kind is **supplied from outside the class** — by
whichever GamePart launches the mission — not derived from the stage number.
> ⚠️ **Withdrawn:** this section used to add "the field is initialised to 0 in the
> constructor `sub_821783D8`, alongside `+148 = 0`, `+132 = 2`, `+136/+140 = -1`".
> That linked two code regions on nothing more than both touching `+144`/`+148`, and
> a snapshot **refutes it**: `sub_821783D8` initialises the **static** at
> `0x828F3EC0` (its first act is `InitializeCriticalSection(obj, 256)`), and in an
> in-flight snapshot that object's `+144` holds `0x000B1C8B` and `+592` a float —
> not a kind and not flags. So **the object owning the kind field is unidentified**.
> Scanning the snapshot for it (kind ∈ {0,3,5,6} at `+144`, stage at `+148`,
> pointers at `+0`/`+52`/`+604`) returns only matches inside the executable's own
> static data — `10` at `+148` is far too common to discriminate. The **switch
> itself stands**: it is direct disassembly at two sites. Only the constructor
> attribution was wrong.
**Confirmed on screen instead:** launching a story stage from MISSION SELECT reaches
a READY ROOM carrying an **"EXTRA" watermark**
([capture](captures/ready-room-extra-mode.png)) — the `EXTRA` config section, i.e.
kind **3**, visible in the UI. That is independent evidence the kind field means what
§4 says, even though its owning object is not pinned.
### 4.1 Why the stage-field probe crashes 🟡
That gives a mechanism for the observed failure: patching the save's stage field to
27 selects the `S27` **record** while the kind stays **0**, so the loader reads the
`FILE` section for a stage whose config lives under `CHALLENGE`. A missing section
then propagates into the load, and the title exits. It fits the evidence (116 fine,
every 2429 the same failure, `/dev/shm` empty and disk free, so not the resource
trap) but it is **not proven** — proving it needs a run.
**Cheap untried discriminator, no new tooling:** patch the stage field to **1823**
(the tutorials). If those also die, the failure tracks "record outside the story
range", and the kind field is the likely gate. It has never been tested — only
116 and 2429 were.
## 5. The challenge screen, its six missions, and the unlock gate ✅
The factory registration sites (`0x8280C000``0x8280F800`) give **id → creator** for
22 of the 24 registered parts, and the creators bound each class's code block. So
`GamePart_ChallengeMission`'s methods are **`0x82187E60``0x8218CF10`** (between
`GP_BUNK`'s creator `0x82187E38` and its own `0x8218CF10`). Resolving every string
that range references gives the screen's whole config schema:
```
MISSIONS · MISSION_ID · RECORD_TYPE · REQUIREMENT · REQUIREMENT_DESC
NORMAL_BUTTON · GRAY_BUTTON · THUMBNAIL · STAGE_DESC · TEXT_STAGE
TEXT_RECORD · NEW_STAGE · "Always" · "Time" · BASE_INFO
```
**The record itself is on disc**, in `tables.pak` (schema `54a10697`, one copy per
language — English is entry #64), *not* in `GP_CHALLENGE.pak`. It names six missions:
| slot | `MISSION_ID` | buttons / thumbnail | record |
|---|---|---|---|
| 1 | `TimeAttack` | `Button_TimeAttack{,_Gray}`, `Thumbnail_TimeAttack` | `Time` |
| 2 | `ScoreAttack` | `Button_ScoreAttack{,_Gray}` | `Points` |
| 36 | `Extra01``Extra04` | `Button_Extra0N{,_Gray}` | — |
`GRAY_BUTTON` is the locked art, `NORMAL_BUTTON` the unlocked art — so the screen
always shows all six and greys out what you have not earned. Full dump:
[`captures/challenge-screen-config.txt`](captures/challenge-screen-config.txt).
### 5.1 The gate is a bit test against a progress bitfield ✅
`0x82189870` builds the list, and `0x82189970``0x821899D8` is the availability
decision for each mission:
```asm
lookup REQUIREMENT in the mission record ; bl 0x82448C50
absent -> AVAILABLE
== "Always" -> AVAILABLE ; strcmp, bl 0x825EDD20
else n = atoi(v) ; bl 0x825EDCD0
n == 0 -> LOCKED
n < 24 -> bit n of word A
n >= 24 -> bit (n-24) of word B
bit set ? AVAILABLE : LOCKED
```
Word A and word B are read from a **singleton** (`0x821707C0`; the object pointer
lives at the global `0x828F48B0`, with a construct-once flag at `0x828F48BC`) at
offsets **`+80`** and **`+1956`**. So challenge
availability is one bit in a progress bitfield, and `REQUIREMENT` is that bit's index
**not** a stage number, a score, or a difficulty.
### 5.2 The bit space is CLEARED STAGES ✅
Word A has exactly **one** writer in the image, and finding it settles the question.
Scanning all 22 callers of the `+80` struct copier (`0x82175110`) for one that stores
to the copy's **word 0** gives a single hit, `0x821C1820`, inside
**`GamePart_StageClear`** (`0x821C09D8``0x821C29F0`):
```asm
if (this+1004 & 0x20000) skip ; already recorded
this+80 = 1
copy local = singleton->progress ; bl 0x82175110, src = obj+80
x = this+84
local.word0 |= 1 << x ; slw r10, r26, r10
if changed: singleton->set(local) ; bl 0x8216FF70 (assign + async persist)
```
**`this+84` is the stage number**, and two independent uses prove it:
- `0x821C1760` indexes a **20-byte record array** with it — `this + (x+7)*20`, whose
records are written as three words plus an 8-byte timestamp, i.e. the shape of the
savegame's 16×20-byte `SHAB` table;
- `0x821C1EEC` and `0x821C1924` pass it as the **index into the config key `STAGE`**
(`0x820A2540`) — the debriefing config's `px_deb_stage01…16` sprite list.
So **word A is a "stage cleared" bitmask**, and a challenge mission's `REQUIREMENT n`
means **"stage `n` has been cleared"**. The `< 24` / `>= 24` split then lines up with
the disc's own stage numbering from §1: story `1``16` and tutorial `18``23` sit in
word A, and the challenge stages `24``29` are exactly word B's bits `0``5`.
> ⚠️ **Third revision of this claim — the first two were wrong, and how they went
> wrong is worth keeping.** An earlier pass read the split at 24 as "the game's 24
> achievements" because `ACHIEVEMENTS_REQUIREMENTS` has exactly 24 entries. That is a
> **coincidence**: the achievement count and the first challenge stage id are both 24
> for unrelated reasons. The achievement work itself stands — the `XACH` table, the
> 1000G self-check, and the `XACHIEVEMENT_DETAILS`/XAM enumeration are all solid, and
> are documented in [structures/achievements.md](structures/achievements.md) — it just
> **does not gate the challenge missions**. The lesson: a numeric coincidence is not a
> join; find the writer.
### 5.3 What the six requirement values are ✅/🟡
The record's numeric tokens are `16`, `25`, `26`, `27`, `29`, with `24`/`28` already
in the pool earlier (they double as font metrics) and so **deduped away** if used.
IDXD dedup makes positional key/value pairing unsound in general — but `16` sits
*immediately* before `REQUIREMENT` (tokens 96 → 97), the documented value-before-key
adjacency, so the first mission's value is well-supported.
**`TimeAttack` requires stage 16 cleared** — the final story mission. That is the
natural gate, and it is what the *first* reading of these numbers suggested before the
achievement detour talked me out of it.
The other five values are `≥ 24`, i.e. **challenge stages 2429**: the challenge
missions chain off each other. 🟡 on the exact pairing (which mission needs which),
which needs the record's binary index section rather than the string pool.
🟡 **Word B has no known writer.** `GamePart_StageClear` sets `1 << x` into word A
unconditionally, which for a challenge stage (`x ≥ 24`) would land on word A bits
2429, *not* word B — so clearing a challenge mission must be recorded by a different
path, presumably one that also stores its `Time`/`Points` record. Not yet found.
**Negative worth keeping:** the requirement *text* (`TimeAttackRequirement`,
`Extra01Requirement`, …) is **not** in `GP_CHALLENGE.pak` — building a `TextIndex`
over it yields **0 entries** and its only wordy payload is embedded font copyright.
The config's `PATH` is `dat\GP_CHALLENGE.pak+eng\`, a per-language branch, so those
keys resolve through a naming scheme the current loader does not reproduce. Reading
them would say in plain English what each mission asks for — worth one more attempt
via `hash::TOC_NAME_SCHEMES`.
### 5.4 Where the mask lives, and one lead refuted
### 5.5 Both gate words are one record — and it is **not** the savegame ✅
`0x82175110` copies the record at singleton `+80` in full: two words, an 8-byte pair, a
184-byte `memcpy`, 8 words from `+200`, then **816 bytes at `+232`, 816 more at
`+1048`**, a sub-object at `+1864`, and a final word at **`+1876`**. So the record runs
`+0 … ~+1880`, i.e. singleton `+80 … +1960` — which means
- **word A** is record `+0` (singleton `+80`), and
- **word B is record `+1876`** (singleton `+1956`).
That answers §5.3's "word B has no known writer": there is no separate `stw` because
the whole record is copied out, modified and assigned back as a unit
(`0x8216FF70` → compare `0x822C3708`, assign `0x82170650`, then spawn a worker
`0x821700A8` under a lock which retries a commit `0x822C33B8` up to five times).
`GamePart_Debriefing` uses the same get/modify/set to accumulate saturating career
counters at `+200``+224` (the last 64-bit) — the quantities the achievement
requirements test.
**And the record is not what `savedata` holds.** Two independent checks:
- **Size.** Every real save on disk is a **276-byte** container deflating to a
**545-byte** payload. The progress record is **~1 880 bytes**. It does not fit.
- **Files.** After many sessions the game's content tree holds only
`game0N/savedata` + `game0N/__thumbnail.png` per slot and the three
`Headers/*.header` — no second data file anywhere.
Also checked and negative: the savegame object is `*(*(this+4)) + 304`, and the
singleton's holder address (`0x828F48B0`) is referenced **nowhere but inside the
accessor itself**, so `this+4` is a different holder — the save block is not a window
into this record.
⚠️ **So hand-editing a save cannot unlock the challenge missions.** That kills the
operational hope this section previously carried; the earlier savegame-editing win
does not extend here.
### 5.6 CONFIRMED ON THE RUNNING GAME ✅
Read live from a booted title (`tools/re-capture/gpoke.py r32`):
```
0x828F40C0 = 0x00000002 word A
0x828F4814 = 0x00000000 word B
```
**Word A = 2 = bit 1 set.** The profile's save is *Stage 02, "At Standby"* — i.e.
**stage 01 cleared** — so the mask is exactly one bit, at the index of the one
cleared stage, **1-based**. Reproduced on two separate cold boots. That confirms, on
the running game and against a known progress state:
- the singleton really is the static object at `0x828F4070`;
- word A is a **cleared-stage bitmask**, not achievements, not a stage number;
- bit index = **stage id, 1-based** — so `TimeAttack`'s `REQUIREMENT 16` is
"clear stage 16", the last story mission;
- word B is the challenge half and is `0` on a story-only profile, as expected.
Both words were then poked (`0xFFFFFFFF` / `0x3F`) and read back OK.
**MISSION SELECT shows the mask directly** ([capture](captures/mission-select-stage01-only.png)).
With word A = 2 the screen lists `Stage01` **selectable, with a High Score and Best
Time**, and `Stage02``Stage08` **greyed out**. One cleared stage, one selectable
entry, at the bit index that names it — the semantics are visible on screen, not
inferred.
**And the mask drives that screen.** Two runs, identical navigation, fresh boot each:
| run | word A | MISSION SELECT |
|---|---|---|
| control | `0x00000002` (untouched) | opens; Stage01 selectable, rest greyed |
| poked | `0xFFFFFFFF` | `MmAllocatePhysicalMemoryEx` fails on a 128 MB request, guest throws, Xenia shows "Disc Read Error" |
So the earlier failure was **caused by the poke**, and by a careless one: `0xFFFFFFFF`
claims stages that do not exist (`0`, `17`, `24``31` in word A). Poking only real
story ids (`0x0001FFFE` = stages 116) does **not** blow the heap. That the list
screen changes behaviour with the mask is itself confirmation that word A feeds it.
**Poking real stage ids unlocks the story campaign in the menu** ✅. With word A =
`0x0001FFFE` (stages 116) every entry `Stage01``Stage16` is selectable
([capture](captures/mission-select-all-story-unlocked.png)) where the control had only
`Stage01`; `Stage16` reads *"Lonely Blue Planet — NO RECORD"*. So **any story stage
can now be launched from the menu**, with no save editing, by poking one word.
**But MISSION SELECT is story-only** ❌. With word B = `0x3F` (challenge stages 2429
marked cleared) the list still **saturates at `Stage16`**
([capture](captures/mission-select-ends-at-stage16.png)) — the cursor stops there and
further presses do nothing. That matches the data: the debriefing config declares
exactly `px_deb_stage01…16`, so the list length is capped by the disc, not by the
mask. **The challenge missions cannot be reached through this screen**, and word B
does not feed it.
**What the poke did not do (yet):** `EXTRAS` still shows only `MISSION SELECT /
MOVIE THEATER / BACK` — no challenge entry — although the menu was built 26 s
*after* the poke, so this is not staleness. Entering `MISSION SELECT` then failed,
and the log gives the real reason: **`MmAllocatePhysicalMemoryEx` could not satisfy
a 128 MB request** (`parent free 30633/131072 pages`), the guest threw a C++
exception, and Xenia surfaced it as its generic *"Disc Read Error"* dialog. It is
preceded by `BaseHeap::Release failed because address is not a region start`, a
failed release that leaks the range. So that is an emulator/heap problem on the way
into the screen, **not** evidence about the gate. Open: repeat without the poke to
see whether `MISSION SELECT` fails the same way regardless.
### 5.7 What *would* work — static addresses for a live write
The singleton is a **static object at `0x828F4070`** (`0x8216F650`:
`addis 0x828F` + `addi …, 16496`), with the holder at `0x828F48B0` pointing at it. So
the two gate words are at fixed guest addresses, no scanning required:
| word | guest VA | meaning |
|---|---|---|
| A | **`0x828F40C0`** | cleared stages, bit = stage id (`< 24`) |
| B | **`0x828F4814`** | cleared stages, bit = stage id 24 (challenge 2429) |
Canary maps guest RAM into `/dev/shm`, which the project already reads live
([`tools/re-capture/gmem.py`](../../tools/re-capture/gmem.py)). Writing
`0xFFFF` into word A and `0x3F` into word B while the title sits on a menu should
open all six challenge missions **without playing the campaign** — and that is the
route to the last 42 `*_EX4`/`*_EX5` units. Untested: it needs a run, and the run
needs gamepad input to reach the menu.
**The obvious lead is dead.** The three `stw`s to `+1956` in `0x822AF278` /
`sub_822C8748` looked promising because they sit in the save serializer's code region
— they are on a **different object**. That one is fetched through `0x822CEB30`, has a
`+2652` flag the code checks first, and stores **string pointers** at `+1956`/`+2024`
(built by `0x822D35F8`); a pointer `AND`ed with `1 << n` would be meaningless as a
gate. So **nothing in the image stores to this singleton's `+1956` field-wise**, which
means it is filled by a bulk copy or by a path not yet found.
Two candidates remain and they need different levers: the **save** (hand-write it) or
the **Xbox profile** (the emulator's profile data). Note that XEX imports are resolved
**by ordinal**, so the absence of `XamUser*` name strings in the `.pe` is not evidence
against the profile route. `+80` at least is handled as a small struct by address
(`addi r4, obj, 80` → copy helper `0x82175110`, written back via `0x8216FF70`), which
is what a serialised value object looks like.
### 5.8 The part id is never persisted — differential search ✅ (negative)
`sub_821749C0` creates a part from `slot+12`, and no literal 26 exists anywhere, so
the id is computed. It can still be found by **differential search**, because the id
is *known* at each screen from §3: `GP_EXTRAS` = 5, `GP_MISSION_SELECT` = 7. Snapshot
both screens and intersect (`tools/re-capture/diff_words.py`):
```
scanned 171 MB of allocated guest memory
addresses reading 5 on EXTRAS and 7 on MISSION SELECT: 4
0x708FFBEC 0x708FFCBC 0x708FFDAC 0x708FFE20
```
**All four are guest stack** (thread stacks sit at `0x709…` in the same run's log).
So the requested part id exists only as a **stack argument in flight** — there is no
persistent field holding it, which is consistent with finding no literal store and
means **there is nothing stable to poke**. Forcing a transition to GamePart 26 needs
the caller's context, i.e. an emulator-side hook rather than a memory write.
**Every memory-and-menu route to the challenge missions is now closed** (§5.4, §5.6,
§5.7, this section). What remains is the genuine in-game unlock — an in-mission
attainment, per `AVSCRIPT_COMMAND_ATTAINMENT_CHALLENGE_MISSION_CARGO_SCORE` — or a
Canary patch that forces the transition in emulator code.
## 6. The unlock condition, from the strings ❔
Three strings say challenge missions are *announced*, not menu-browsed:
- `AVSCRIPT_COMMAND_ATTAINMENT_CHALLENGE_MISSION_CARGO_SCORE` — a mission-script
command that grants challenge-mission attainment from a **cargo score**;
- `DLG_CHALLENGE_MISSION_AVAILABLE` — the "a challenge mission is now available"
dialog;
- `DLG_GO_CHALLENGE_MISSION_MENU` — the prompt that takes you to GamePart 26.
So the gate is plausibly an in-mission achievement, not a title-menu state — which is
consistent with `Game Status = GAME_CLEAR` unlocking nothing (measured, refuted).
Neither dialog key is reachable by xref: they are selected **by index** through a
config lookup, exactly like the save screen's sprite keys, so there is nothing to
grep back to. Naming the flag needs the challenge-availability check disassembled
from the screen side.
## 7. What this makes actionable
`roster_target` against the harvested CSV, with the stage families now named:
| stage | family | roster units not yet read |
|---|---|---|
| `S27` | challenge | `f003_ArrowHead_EX4`, `e107_AAFrigate_EX4`, `e010_Attacker_S_EX4`, `e011_Attacker_B_EX4`, `e007_Turret_EX4`, `e008_TurretPlus_EX4` |
| `S28` | challenge | `f004_DeltaSaber_A_Player`, `f001_DeltaSaber_T_EX5(_el)`, `f003_ArrowHead_EX5`, `f104_Battleship_EX5`, `f105_Cruiser_EX5` |
| `S25` | challenge | `e101_SDBattleshipEX`, `e102_BattleshipEX`, `e105_CruiserEX`, `e108_ASFrigateEX` |
| `S29` | challenge | `e101_SDBattleshipEX`, `e102_BattleshipEX`, `e105_CruiserEX` |
| `S24` | challenge | `e105_CruiserEX`, `e106_DestroyerEX` |
| `S10` | **story** | `e005_ADAN_ElanTypeQ_Margras` |
⚠️ **The `S10` row contradicts "the story campaign is complete."** One story stage
still fields a unit that has never been read. Either `S10` was never flown (it is the
smallest stage container on the disc — 7 XBG7 resources — so it may be a cutscene
stage that is not flyable), or the completeness claim was one unit optimistic. Flagged
rather than resolved: it costs one ordinary story run to settle.
Remaining coverage: 42 units missing, of which the rosters above account for ~23. The
rest are not in any stage roster table and need a different lever.
## Reproduce
```bash
cargo run --release -q -p sylpheed-formats --example challenge_map -- <disc-root>
cargo run --release -q -p sylpheed-formats --example challenge_screen -- <disc-root>
python3 xenia-rs/zq.py dis 0x82184df0 0x82184e30 # the three-way section switch
python3 xenia-rs/zq.py dis 0x82185ed0 0x82185f10 # the same switch, second site
python3 xenia-rs/zq.py dis 0x82189860 0x821899f0 # the challenge availability gate
```

View File

@@ -420,3 +420,319 @@ measurement specified above.
**The clock ratio has now been measured three times in three flights: 1.260, 1.311,
1.383.** It is not a property of the machine but of the moment, so any rate probe
must bracket its own phases.
## Roll, re-measured about the forward axis — the withdrawal is reversed ✅
The earlier roll result was withdrawn because the *measurement* was wrong, not the
input: watching a non-forward row of the rotation matrix sees **any** rotation that
moves that row, and pitch moves it as much as roll, which is why two different stick
axes produced the same rates. `tools/re-capture/roll_axis.py` measures rotation
**about** the forward axis instead — express the new up-vector in the old
`(up, right)` basis and take `atan2(u_new·w_old, u_new·u_old)`, so any component
along forward (what pitch produces) is dropped by construction.
Run on stage 02 with the container-safe file pad, which matters here: the pad state
is written whole, so the stick is deflected on **exactly one axis** (`lx=32767`,
every other channel exactly 0) while the throttle trigger is held in the same write.
5-second settles, 8-second dwells, and each phase bracketed by HUD-clock screenshots
because the clock ratio is a property of the moment (1.260/1.311/1.383 previously).
| phase | swept | wall | HUD clock | per **game** second | definition |
|---|---|---|---|---|---|
| min speed (`LT`) | 1 856.9° | 8.01 s | 01:20.64 → 01:30.63 (×1.247) | **185.9 °/s** | `AV_Roll_Min` **200** |
| max speed (`RT`) | 1 181.2° | 8.02 s | 01:37.11 → 01:46.88 (×1.218) | **120.9 °/s** | `AV_Roll_Max` **125** |
**Roll DOES depend on speed**, and the withdrawn claim ("roll shows no speed
dependence, unlike pitch") is **reversed**. Roll behaves exactly like pitch: the rate
cap falls as speed rises, and `_Min`/`_Max` mean *at minimum / at maximum speed*
the same rule the pitch work established, now confirmed on a second axis rather than
contradicted by it.
Both measurements land **just under** their caps (93 % and 97 %), which is the right
side for a rate limit. 🟡 The shortfall is not explained: candidates are the craft not
being exactly at min/max speed after 5 s, and the swept-angle sum slightly
undercounting at 20 Hz. Neither is worth a claim without measuring it.
Method note: the tell that the *old* result was broken was two conditions agreeing
too well (pitch ≈ roll). The tell that this one is sound is that they now **disagree
in the direction the definitions predict**, on two independently-bracketed phases.
Data: [`captures/roll-about-forward-axis.csv`](captures/roll-about-forward-axis.csv).
## Which input drives which axis — and the yaw answer ✅
`tools/re-capture/axis_probe.py` holds one channel at a time (file pad, so every
other channel is exactly zero) and decomposes the result into **all three** rotation
components at once, rather than measuring one axis through a row that moves under any
rotation — the flaw that once made roll and pitch produce identical numbers.
**The row labelling is measured, not assumed.** `entities2` pins row 2 = forward
against velocity; the probe pins the other two by comparing **world-Y** in flight:
```
row world-Y means: [0.469, 0.883, -0.000] forward = row 2
-> up = row 1, right = row 0 CONFIDENT
```
That is the **opposite** of the D3D convention an earlier run assumed, which means
that run's *yaw* and *pitch* columns were swapped — under the correct labels its
`ly+` reading of 154.1 °/wall-s is **pitch**, which is what a left-stick Y axis
should do.
| input | roll | yaw | pitch | |
|---|---|---|---|---|
| `rx` right-stick X | 0.0 | 0.0 | 0.0 | nothing |
| `ry` right-stick Y | 0.0 | 0.0 | 0.0 | nothing |
| `LB` | 0.0 | 0.0 | 0.0 | nothing |
| `RB` | 0.0 | 0.0 | 0.0 | nothing (it is the nose gun) |
| `lx` left-stick X | 209.8 | ~0 | ~10 | **roll** |
| `ly` left-stick Y | ~0 | ~0 | 154.1 | **pitch** |
These zeros are **trustworthy**, unlike the previous attempt's: the four unknown
inputs were measured *first*, each passing a liveness check, and the run aborted the
moment the craft stopped moving instead of reporting the clean zeros a destroyed
craft produces.
**So no pad input yaws the craft directly.** `AV_Yaw_*` (45/25) exists in the unit
definitions but nothing on the right stick or the shoulders drives it — the earlier
*"no input found"* is upgraded from "we could not find one" to **"the remaining
candidates measurably do nothing"**. The natural reading is that yaw is a
*consequence* of banking rather than a commanded axis, which a reimplementation
should model as such.
🟡 Not covered: the **d-pad** (the tactical map) and the **face buttons** (fire /
weapon select). Neither is a plausible flight axis, but neither was measured here.
Data: [`captures/axis-probe-rows-pinned.csv`](captures/axis-probe-rows-pinned.csv).
## ⚠️ Angular-rate probes measure a MOVING speed — the dwell bleeds it
Measuring pitch with the rows properly pinned gave, for
`UN_f001_TCAF_DeltaSaber_T_Player` (whose own disc values are
`AV_PitchPlus_Min` **150°**, `AV_PitchPlus_Max` **70°**):
| phase | swept | clock | per game-s | cap |
|---|---|---|---|---|
| min speed (`LT`) | 1 391.0° / 8.00 s | ×1.326 | **131.1** | 150 |
| max speed (`RT`) | 989.0° / 8.05 s | ×1.318 | **93.2** | 70 — **133 % of cap** |
A rate 33 % *above* a cap is not a finding, it is a broken instrument, so read the
HUD speed off the same bracketing screenshots that gave the clock:
```
slow phase 102 -> 18
fast phase 1193 -> 589
```
**The speed is not constant during the dwell — pitching bleeds it hard**, halving it
in 8 seconds. The rate cap is itself speed-dependent, so as the craft slowed its cap
*rose*, and the 8-second average necessarily lands between the max-speed cap and a
mid-speed one. The 133 % is fully explained by the instrument.
**This weakens the roll numbers above as well.** They were taken the same way, so
`120.9` vs `AV_Roll_Max` `125` is *consistent* but is **not a tight test** — the true
max-speed roll cap could be lower and still produce that average. The min-speed
figures are less affected (there is little speed left to lose).
**How to measure it properly** (not yet done): keep the dwell to ~12 s so the speed
barely moves, or sample the HUD speed continuously and fit rate against
*instantaneous* speed rather than assuming the endpoint. The second is strictly
better and gives the whole rate-vs-speed curve rather than two points.
**And a flight-model finding in its own right:** *turning costs speed*, steeply —
1 193 → 589 under 8 s of full pitch, with the throttle still held at maximum. A
reimplementation that treats the throttle as setting a speed the craft simply holds
will be wrong during manoeuvres. Data:
[`captures/pitch-rate-speed-bleed.csv`](captures/pitch-rate-speed-bleed.csv).
## ⚠️ Polling faster than the guest updates manufactures a curve
Trying to fit rate against *instantaneous* speed (`rate_curve.py`, one long hold so
the speed-bleed sweeps the range) produced a beautifully clean result that is
entirely an artefact:
```
speed 0- 599 rate 29.4 speed 1199-1798 rate 279.8
speed 599-1199 rate 174.9 speed 1798-2398 rate 317.5
```
Rate rising with speed, and speeds to **4 795** when the craft's maximum is 1 200.
The cause: **20 Hz polling is faster than the guest updates these fields**, so a
per-read delta is either exactly zero (no update yet) or a whole frame's worth
divided by a fraction of a frame. In that run **111 of 352 reads were zero on BOTH
channels** — position and attitude update on the same frame, so the two are
perfectly correlated, and dividing each by the short wall `dt` produced a tidy
correlation out of nothing.
**Fix: aggregate over windows spanning many frames** (0.5 s here). The sum of
`|Δ|` over a window is right however the updates fall inside it.
**This does NOT affect the swept-total probes** (`roll_axis.py`, `rate_probe.py`):
they already summed over the whole dwell, which is immune for the same reason. Only
per-sample instantaneous rates were ever wrong.
🟡 **The windowed re-run is not yet a result.** It gives plausible magnitudes
(speed 320935, rate 80149 °/wall-s) but still shows rate *rising* with speed,
against the definition's `AV_PitchPlus_Min` 150 > `_Max` 70. Two disqualifiers: it
ran on an instance where the craft was already tumbling from the previous sweep, so
the row pinning reported **WEAK — craft may not be level**, and the starting speed
was mid-range rather than maximum. A clean answer needs a **fresh flight**, pinning
CONFIDENT, one sweep, nothing before it. Recorded as an open question rather than a
finding — the definitions' `_Min`/`_Max` meaning is exactly what is in doubt, so a
measurement taken through a doubtful instrument cannot settle it.
Data: [aliased, for reference](captures/rate-curve-aliased-BAD.csv) ·
[windowed](captures/rate-curve-windowed.csv).
## The clean sweep: magnitudes agree, the LAW does not follow 🟡
Fresh flight, row pinning **CONFIDENT** (margin 0.413), one sweep and nothing before
it — the conditions the previous attempt lacked. Clock 01:16.08 → 01:41.46 (×1.26).
Binned by speed, both converted to game units:
| speed (game) | measured °/game-s | linear interpolation of the caps |
|---|---|---|
| ~435 | 100.8 | 125.6 |
| ~572 | 113.8 | 115.7 |
| ~709 | 126.3 | 105.7 |
| ~846 | 83.1 | 95.7 |
| ~983 | 72.7 | 85.8 |
(prediction = `AV_PitchPlus_Min` 150 at `MinimumVelocity` 100 → `_Max` 70 at
`MaximumVelocity` 1200, interpolated linearly.)
**What this supports:** the magnitudes are right — measured 73126 °/game-s across
speeds 4001 050 against a predicted 86126 — and the high-speed end falls, as a
speed-dependent cap should.
**What it does not support:** the interpolation *law*. The scatter is ±25 %, the two
fastest bins hold only 1 and 2 windows each (they are the first moments before the
speed bled), and the slowest bin disagrees in the wrong direction (100.8 measured vs
125.6 predicted). A sweep that is *driven* by the speed bleeding cannot spend long at
either extreme, which is exactly where the law is most testable.
**The design that would settle it**, and why this one cannot: hold a *settled*
throttle, pitch for only **~1 second**, and read the rate — the speed barely moves
inside a 1 s burst, so each burst yields one honest `(speed, rate)` point. Repeat at
`LT` / neutral / `RT` for three clean points at known speeds, instead of one smeared
sweep. Data:
[`captures/pitch-rate-curve-clean.csv`](captures/pitch-rate-curve-clean.csv).
## ✅ Short bursts at settled speeds: the law's SHAPE is confirmed, clock-free
The design the sweep could not provide: settle the throttle, measure the settled
speed, then pitch for **one second** so the speed barely moves inside the burst.
Three throttle settings, two repeats each, row pin **CONFIDENT**, fresh flight.
| throttle | burst speed (wall) | rate (°/wall-s) |
|---|---|---|
| `LT` min | ~105 | 113.6, 109.5 |
| none, cruise | ~383 | 100.2, 88.4 |
| `RT` max | ~1 483 | 52.2, 70.5 |
**Rate falls monotonically with speed** — 111.5 → 94.3 → 61.4 — measured at three
*known, settled* speeds rather than smeared across a bleeding one.
**And the decisive comparison needs no clock at all.** Absolute rates depend on the
run's clock ratio, but the **min:max ratio cancels it**:
```
measured min:max = 1.82
AV_PitchMinus_Min/Max 75/40 = 1.88 -> 3.0 % apart
AV_PitchPlus_Min/Max 150/70 = 2.14 -> 15.1 % apart
```
So two things follow, neither resting on a clock measurement:
1. **`_Min` / `_Max` really do mean "at minimum / at maximum speed"**, with the rate
interpolating between them — the shape is confirmed to 3 %.
2. **`ly+` drives pitch-MINUS**, not pitch-plus. The craft has asymmetric pitch
authority (75/40 down versus 150/70 up), and the ratio picks the pair cleanly.
🟡 **Absolute magnitudes are still open** — this run did not bracket the HUD clock, so
deg/*game*-second cannot be computed from it, and choosing a ratio that makes the
numbers fit would be circular. The probe now takes clock screenshots at both ends so
the next run closes it. Data:
[`captures/pitch-burst-settled-speeds.csv`](captures/pitch-burst-settled-speeds.csv).
## Both axes, three settled speeds: shape ✅, absolute scale 🟡
Repeat of the burst design with the HUD clock bracketed (01:01.86 → 02:10.61) and
roll measured in the same flight. Clock ≈ ×1.26.
| axis | min speed | cruise | max speed | min:max | definition ratio |
|---|---|---|---|---|---|
| pitch | 87.0 °/game-s @ 85 | 82.8 @ 308 | 47.0 @ 1 077 | **1.85** | `PitchMinus` 75/40 = **1.88** |
| roll | 125.0 @ 76 | 119.0 @ 291 | 82.6 @ 1 023 | **1.51** | `Roll` 200/125 = **1.60** |
**The shape is confirmed on both axes** — the rate interpolates between `_Min` (at
minimum speed) and `_Max` (at maximum speed), matching to **1.6 %** for pitch and
**5.6 %** for roll. These ratios are **clock-independent**, so they stand regardless
of the conversion. Pitch's ratio also re-confirms `ly+` = **pitch-minus** (1.88)
rather than plus (2.14), reproducing the earlier run's 1.82.
**The absolute scale does not match, and not in the same direction:**
```
pitch measured / predicted = 1.15, 1.21, 1.07 (consistently OVER)
roll measured / predicted = 0.62, 0.64, 0.60 (consistently UNDER, and very flat)
```
**A clock error cannot explain this** — it would scale both axes the same way, and
these go opposite. So the discrepancy is per-axis. Two candidates, neither measured:
- a **1-second burst may not complete the angular-acceleration ramp**, which would
*under*-read — that fits roll's remarkably constant 0.62 but not pitch's excess;
- a **per-axis multiplier** applied to the cap that is not yet identified.
Recorded as an open question. The next measurement that would separate them is cheap:
**1 s versus 3 s bursts at the same throttle** — if the ramp is the cause, the longer
burst reads higher. Data:
[pitch](captures/pitch-burst-final.csv) · [roll](captures/roll-burst-final.csv).
## Note on the WEAK-pin guard
The roll run refused to start: after the pitch bursts the craft was no longer level,
so the row pin came back **WEAK** (margin 0.109). That refusal is correct for pitch
and yaw — but **roll is immune to the up/right labelling**, which is exactly what the
guard's own message says, so `ALLOW_WEAK_PIN=1` is the documented and legitimate
override there. It is worth having a guard that states its own exception.
## The ramp test is inconclusive — and says what the next tool must be 🟡
To separate "a 1 s burst never reaches the steady rate" from "a per-axis multiplier",
the cheapest test measures *inside* one hold: successive 0.25 s windows of a single
3 s press, so speed, attitude and starting conditions are held constant by
construction. Two repeats, roll, at cruise:
```
rep0 rate 16 59 325 209 130 322 238 110 310 151 183
speed 390 234 574 341 223 526 387 191 511 247 312
rep1 rate 52 181 231 246 236 169 368 195 181 255 182
speed 447 372 378 418 385 233 590 307 312 419 306
```
**Not usable.** At 0.25 s the windows do not contain enough guest updates to average,
so both channels swing by 3× window to window — the same aliasing that once
manufactured a rate-vs-speed curve, reappearing at finer resolution. The first window
is the lowest in *both* repeats, which is what a ramp would look like, but the
sequence never plateaus, so the signal cannot be separated from the sampling.
**Widening the window does not rescue it**: 0.5 s windows average well enough (that is
what the earlier sweep used), but a hold long enough to contain several of them bleeds
speed, and speed is the very variable under test. The two effects are entangled at
this observation rate.
**So this residual needs a different instrument, not another script.** Live-RAM polling
samples an unsynchronised snapshot; what the question wants is the craft's angular
velocity *as the guest computes it, once per frame*. That is a Canary-side hook —
the same shape as the existing F10 ship-capture patch — and the rebuild toolchain
already makes it cheap. Recorded as the recommendation rather than attempted as a
seventh variation of the same measurement.
Data: [`captures/roll-ramp-inconclusive.csv`](captures/roll-ramp-inconclusive.csv).

View File

@@ -0,0 +1,182 @@
# Achievements — the 24-entry table, and where the earned state comes from
**Conf.:** ✅ for the table and for the XAM read path; 🟡 for the requirement list's
bit numbering.
**Spec:** [`tools/xach_dump.py`](../../../tools/xach_dump.py) ·
[`examples/achievements_map.rs`](../../../crates/sylpheed-formats/examples/achievements_map.rs)
**Evidence:** [`captures/achievements-xach.txt`](../captures/achievements-xach.txt)
> **Retracted here, and it matters:** earlier drafts of this file claimed the
> challenge missions gate on this achievement mask. **They do not.** That gate reads a
> *cleared-stage* bitmask whose sole writer is `GamePart_StageClear` — see
> [challenge-mission-gate §5.2](../challenge-mission-gate.md). The link was invented
> out of a numeric coincidence: `ACHIEVEMENTS_REQUIREMENTS` has 24 entries and the
> challenge gate splits its two words at 24, for entirely unrelated reasons (24 is
> also the first challenge stage's id). Everything below is what survived checking the
> writer.
The title reads its earned-achievement state from the console, and separately carries
a disc table of what each achievement requires. Both are useful to the
reimplementation on their own terms.
## 1. The table: 24 achievements, 1000G ✅
The XEX embeds an SPA/XDBF resource. `XACH` sits at `.pe` offset **`0x8FBCBC`**:
| field | type | notes |
|---|---|---|
| magic | `char[4]` | `XACH` |
| version | `u32` | 1 |
| size | `u32` | 874 |
| count | `u16` | **24** |
then `count` records of **36 bytes**:
| off | type | field |
|---|---|---|
| 0 | `u16` | achievement id (1…24) |
| 2 | `u16` | name string id |
| 4 | `u16` | unlocked-description string id |
| 6 | `u16` | locked-description string id |
| 8 | `u32` | image id |
| 12 | `u16` | gamerscore |
| 14 | `u16` | pad (0) |
| 16 | `u32` | flags (`0x0C` throughout) |
| 20 | — | 16 bytes of zeroes |
Strings come from one `XSTR` section per language (7 present); each is
`magic[4] "XSTR"`, `version u32`, `size u32`, `count u16`, then entries of
`id u16, len u16, len bytes ASCII`. English is table **#5**.
**The stride and field offsets are self-checked**: the 24 gamerscores sum to
**exactly 1000**, the retail total. A wrong stride does not add up to a round 1000.
Ids run `1…24` with no gaps.
> **Correction 1 of 2 (same day).** This file first said "bit `n` ↔ achievement id `n+1`",
> i.e. 0-based bits. That was wrong-headed: the only place the image is *observed*
> turning an achievement into a bit does `1 << dwId` with `dwId` 1-based (§3.1), so
> the bit index is the **id itself** and bit 0 is unused. The 0-based reading came
> from assuming the config list's position was the bit, which §3 no longer supports.
## 2. The 24 achievements ✅
Ids run `1…24` with no gaps. Where the image turns one into a bit it uses `1 << dwId`
(§3.1), so the id is the bit and bit 0 is unused — but note that this mask is *not*
the challenge gate's (§4).
| id | G | name |
|---|---|---|
| 1 | 20 | Space Combat Award |
| 2 | 20 | Schlos Base Defense Award |
| 3 | 20 | Aegis of the People Medal |
| 4 | 20 | TCAF Luna Medal |
| 5 | 40 | TCAF Mars Medal |
| 6 | 50 | Soldier's Charm Amulet |
| 7 | 20 | White Griffons Patch |
| 8 | 30 | TCAF Jupiter Medal |
| 9 | 40 | Furious Pursuit Badge |
| 10 | 30 | Solo Aerospace Combat Award |
| 11 | 30 | Operation Nebula Blaze Award |
| 12 | 40 | Guilty Roses Patch |
| 13 | 30 | Super Battleship Slayer Patch |
| 14 | 40 | TCAF Terra Medal |
| 15 | 40 | Hellfires Patch |
| 16 | 50 | Night Ravens Patch |
| 17 | 40 | Solar System Defense Award |
| 18 | 40 | Special Operations Medal |
| 19 | 30 | 1,000 Units Destroyed Medal |
| 20 | 70 | 10,000 Units Destroyed Medal |
| 21 | 50 | Ship Hunter Award |
| 22 | 70 | Gigaton Club Patch |
| 23 | 80 | Weapon Lord Patch |
| 24 | 100 | TCAF Pilot's Commendation |
## 3. The game evaluates them itself, from a disc config ✅ (numbering 🟡)
`GamePart_Debriefing` (`0x8218CF38``0x82191B18`, bounded by the factory creator
thunks either side) runs `sub_8218F9A8` after a mission:
```asm
for i = 0, 1, 2, :
node = child(ACHIEVEMENTS_REQUIREMENTS, i, &x) ; bl 0x82448338
bit = 1 << x
if (this+208 & bit) continue ; already awarded
if (evaluate(this, node)) ; bl 0x8218FAB0
this+208 |= bit
```
The list is on disc — `tables.pak` entry **#16**, schema `744c0519`, the
`GP_DEBRIEFING_PILOTLOG.pak+eng` config — and its entries are literally
`ACHIEVEMENT01``ACHIEVEMENT24`, in order.
⚠️ **`x` is not proven to be the loop index.** `0x82448338` walks a 12-byte child
array and writes the child entry's **first word** to the out-parameter; whether that
word is the ordinal, an explicit id, or a name hash is not pinned. Since §3.1 shows
the image elsewhere shifting by a **1-based achievement id**, `x` is most likely the
id too — but this file previously stated "the list index is the bit index" as fact,
and that is withdrawn.
### 3.1 The earned state comes from XAM — the console profile, not the save ✅
The same class enumerates a buffer of **36-byte** records (`0x8218F888`):
- the record count is a byte count divided by 36 — via the multiply-high magic
**`0x38E38E39`** plus `srawi 3`, which is the standard unsigned `/36` sequence, so
the stride is confirmed by the arithmetic and not just by inspection;
- field **`+0`** is used as a shift amount (`1 << id`), field **`+32`** is tested for
bit **`0x00020000`**;
- the buffer is produced asynchronously: a handle at `this+100` is waited on
(`0x824AA330(h, -1)`) then closed (`0x824AA3E0`).
That is exactly the XDK's `XACHIEVEMENT_DETAILS`
`{ DWORD dwId; PWSTR pwszLabel, pwszDescription, pwszUnachieved; DWORD dwImageId,
dwCred; FILETIME ftAchieved; DWORD dwFlags; }` = 36 bytes, with
`XACHIEVEMENT_DETAILS_ACHIEVED == 0x00020000` — fetched through the
`XamUserCreateAchievementEnumerator` / `XEnumerate` pattern.
**So the title does not persist earned achievements itself: it asks the console.**
The masks it builds (`this+208`, `+736`, `+740`) are `1 << dwId`, i.e. **bit = the
1-based achievement id**, bit 0 unused.
For anything that wants to unlock achievement-gated content, the lever is therefore
the **emulator's profile achievement data**, not the 545-byte savegame.
The record also carries each requirement's **type and parameters**:
`StageClear`(`Stage`), `MissionObjective`, `Item`, `Rank` (`S`),
`ShootDownAircrafts`(`Count` 1000 / 10000), `ShootDownShips`(`Count` 100),
`ShootDownWeight`(`MegaTons`), `GetAllWeapons`, `GetAllAchievements`.
**That set independently confirms the list's ORDER** (not the bit numbering): the last
five types line up with ids 1924 exactly as the XACH table names them — 1 000 units,
10 000 units, 100 warships, one gigaton, all Delta Saber equipment, and finally the
meta `GetAllAchievements` → id 24, `TCAF Pilot's Commendation`, the 100G one. So the
config list is in achievement-id order; what it does **not** settle is whether the bit
the code shifts by is that position or the id (see the ⚠️ above).
⚠️ `GetAllAchievements` and `GetAllWeapons` **look like debug cheats and are not**
they are requirement *types* in the achievement table. Worth stating because the
strings sit next to genuinely debug-looking ones in the image.
## 4. What this is *not*: the challenge-mission gate ✅ (refuted)
`GamePart_ChallengeMission` gates each mission on a bit of a word at singleton `+80`.
That word has exactly one writer in the image — `0x821C1820` in `GamePart_StageClear`
— and it sets `1 << (this+84)` where `this+84` is the **stage number** (it also
indexes a 20-byte per-stage record array and the debriefing config's `STAGE` sprite
list). So that word is a **cleared-stage** mask, not this achievement mask, and the
`< 24` / `>= 24` split is the disc's stage numbering (story 116 and tutorial 1823
below 24; challenge 2429 above), not a count of achievements.
Nothing observed copies the Debriefing's achievement masks (`+208`, `+736`, `+740`)
into that word, which is the check that should have been done before the claim was
made rather than after.
## Reproduce
```bash
python3 tools/xach_dump.py "<disc>/…/Project Sylpheed ….pe"
cargo run --release -q -p sylpheed-formats --example achievements_map -- <disc-root>
python3 xenia-rs/zq.py dis 0x8218f9a8 0x8218fa60 # the requirement walk
python3 xenia-rs/zq.py dis 0x8218f888 0x8218f990 # the XACHIEVEMENT_DETAILS scan
```

186
tools/re-capture/axis_probe.py Executable file
View File

@@ -0,0 +1,186 @@
#!/usr/bin/env python3
"""Which pad input drives which rotation axis — all three measured at once.
The control mapping was left with **yaw ❔ "no input found"**: `AV_Yaw_*` exists in
the definitions (45/25) but no stick appeared to yaw. That conclusion came from
probes that measured one axis at a time using a non-forward matrix row, which is
exactly the flaw that made roll and pitch produce identical numbers — a row like
that moves under *any* rotation, so "this input yaws" and "this input pitches"
cannot be told apart by it.
So decompose properly. For each small step, with the previous frame's orthonormal
rows `(f, u, w)` = forward, up, right:
roll = atan2(u_new . w_old, u_new . u_old) rotation of UP about forward
yaw = atan2(f_new . w_old, f_new . f_old) rotation of FORWARD about up
pitch = atan2(f_new . u_old, f_new . f_old) rotation of FORWARD about right
Each drops the components the other two produce, so one held input yields three
numbers and the axis it actually drives is whichever is large.
Every candidate is held with the file pad, which writes the WHOLE state at once —
so `rx` really means right-stick-X with every other channel exactly zero, which a
quantising virtual stick could not promise.
⚠️ TWO THINGS THIS PROBE DOES NOT YET HANDLE, both learned the hard way:
1. **The craft can DIE mid-run.** Holding a stick at full deflection spins the ship
at 150-200 deg/s in a live combat mission; two minutes of that and the first run
ended on GAME OVER. Inputs measured after that point are meaningless, and the
symptom is only "0 player candidates, best displacement 0.000" *afterwards* --
the earlier rows still look fine. Check the player is alive BETWEEN inputs, or
probe somewhere nothing shoots back.
2. **Which row is UP and which is RIGHT is not pinned.** `entities2` measures that
row 2 is forward (against velocity), but rows 0 and 1 are assigned by the D3D
convention, not by evidence. Roll is unaffected -- rotation of either non-forward
row within their shared plane is roll either way -- but YAW and PITCH swap if the
convention is wrong, so this probe's last two columns are named on an assumption.
Usage: axis_probe.py <out.csv> [dwell_s]
"""
import json
import math
import os
import struct
import sys
import time
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import speed_law # noqa: E402
PAD_FILE = os.environ.get("XENIA_PAD_FILE", "/tmp/xenia_pad.txt")
# Candidates. Sticks at full deflection, shoulders as buttons. Triggers are the
# throttle (already settled) and are left out.
# Order matters: the craft can be shot down mid-run, so the UNKNOWN inputs go
# first while it is healthy and the already-established ones (lx = roll) go last
# as controls. A row measured after death looks like a clean zero.
CANDIDATES = [
("rx+", {"rx": 32767}),
("ry+", {"ry": 32767}),
("LB", {"press": "LB"}),
("RB", {"press": "RB"}),
("lx+", {"lx": 32767}),
("ly+", {"ly": 32767}),
]
def pad_state(**kw):
parts = [f"{k}={v}" for k, v in kw.items() if v is not None]
tmp = PAD_FILE + ".tmp"
with open(tmp, "w") as f:
f.write(" ".join(parts))
os.replace(tmp, PAD_FILE)
def rows_at(fd, off, cfg):
base = off + cfg["rot_delta"]
out = []
for r in range(3):
v = struct.unpack(">3f", os.pread(fd, 12, base + r * cfg["rot_stride"]))
n = math.sqrt(sum(c * c for c in v)) or 1.0
out.append(tuple(c / n for c in v))
return out
def dot(a, b):
return sum(a[i] * b[i] for i in range(3))
def alive(w, off, cfg, secs=1.0):
"""Is the craft still flying? A destroyed craft stops moving, and every
subsequent input then measures a clean, meaningless zero — the failure mode
that invalidated this probe's first run (it ended on GAME OVER and only said
so afterwards)."""
p0 = struct.unpack(">3f", os.pread(w.fd, 12, off))
time.sleep(secs)
p1 = struct.unpack(">3f", os.pread(w.fd, 12, off))
return math.dist(p0, p1) > 1.0
def pin_rows(w, off, cfg):
"""Which non-forward row is UP and which is RIGHT?
`entities2` measures row 2 = forward against the velocity vector, but the other
two are assigned by the D3D convention rather than evidence — and yaw and pitch
SWAP if that is wrong, so naming them without this test is a guess.
Discriminator: in level flight the craft's up-vector points along world +Y and
its right-vector lies near the horizontal plane. Sample at neutral and compare
|world-Y| across the rows."""
f_i = cfg["fwd_row"]
acc = [0.0, 0.0, 0.0]
n = 0
for _ in range(20):
time.sleep(0.05)
rs = rows_at(w.fd, off, cfg)
for r in range(3):
acc[r] += rs[r][1] # world Y component
n += 1
ys = [a / n for a in acc]
cand = [r for r in (0, 1, 2) if r != f_i]
up_i = max(cand, key=lambda r: abs(ys[r]))
right_i = [r for r in cand if r != up_i][0]
print(f"# row world-Y means: {[round(y,3) for y in ys]} (forward = row {f_i})")
margin = abs(ys[up_i]) - abs(ys[right_i])
ok = margin > 0.3
print(f"# -> up = row {up_i}, right = row {right_i} margin {margin:.3f} "
f"{'CONFIDENT' if ok else 'WEAK — craft is not level'}")
if not ok and os.environ.get("ALLOW_WEAK_PIN") != "1":
sys.exit("refusing to measure with a WEAK row pin — fly level first, or set "
"ALLOW_WEAK_PIN=1 if the caller genuinely does not care "
"(roll is immune to the labelling; pitch and yaw are not)")
return up_i, right_i
def main():
out_csv = sys.argv[1]
dwell = float(sys.argv[2]) if len(sys.argv) > 2 else 5.0
cfg = json.load(open("/tmp/nav-live.json"))
w, off, nm = speed_law.find_player()
if not w:
sys.exit("player entity not found")
print(f"# locked on {nm}")
f_i = cfg["fwd_row"]
if not alive(w, off, cfg):
sys.exit("craft is not moving at the start — not in flight, or already dead")
u_i, w_i = pin_rows(w, off, cfg)
rows = []
print(f"# {'input':<6} {'roll':>9} {'yaw':>9} {'pitch':>9} (deg over the dwell)")
for label, state in CANDIDATES:
pad_state() # neutral
time.sleep(2.0)
prev = rows_at(w.fd, off, cfg)
pad_state(**state)
t0 = time.time()
sw = [0.0, 0.0, 0.0]
while time.time() - t0 < dwell:
time.sleep(0.05)
cur = rows_at(w.fd, off, cfg)
roll = math.degrees(math.atan2(dot(cur[u_i], prev[w_i]), dot(cur[u_i], prev[u_i])))
yaw = math.degrees(math.atan2(dot(cur[f_i], prev[w_i]), dot(cur[f_i], prev[f_i])))
pitch = math.degrees(math.atan2(dot(cur[f_i], prev[u_i]), dot(cur[f_i], prev[f_i])))
for k, v in enumerate((roll, yaw, pitch)):
sw[k] += abs(v)
prev = cur
pad_state()
wall = time.time() - t0
if not alive(w, off, cfg):
print(f"# CRAFT STOPPED MOVING after {label} — everything from here is "
f"meaningless, aborting rather than reporting zeros")
break
rate = [v / wall for v in sw]
rows.append((label, *[round(v, 2) for v in rate]))
print(f"# {label:<6} {rate[0]:9.1f} {rate[1]:9.1f} {rate[2]:9.1f} deg/wall-s")
time.sleep(1.5)
with open(out_csv, "w") as f:
f.write("input,roll_deg_s,yaw_deg_s,pitch_deg_s\n")
for r in rows:
f.write(",".join(str(x) for x in r) + "\n")
print(f"# wrote {out_csv}")
if __name__ == "__main__":
raise SystemExit(main())

45
tools/re-capture/bin/screenshot Executable file
View File

@@ -0,0 +1,45 @@
#!/usr/bin/env bash
# Grab the GAME's image, not the emulator window.
#
# Every pixel oracle in this toolkit (at_menu, at_title, wait_not_black, the
# HUD readers) was measured against a bare 1280x720 game image. xenia's window
# is a GTK window with a menu bar, so the game surface actually starts ~25 px
# down — and when it does, all of those constants read the wrong pixels. That
# is not a hypothetical: a run sat on a plainly visible MAIN MENU for 300 s
# reporting "no main menu", and a later one missed the title screen entirely and
# let the attract movie loop for ten minutes.
#
# The offset is not guessed: it is the xenia window's height minus 720. So this
# works whether or not the menu bar is there, and needs no per-display constant.
# Put this directory first on PATH and existing scripts keep working unchanged.
set -u
REAL=/usr/local/bin/screenshot
OUT="${1:-}"
if [ -z "$OUT" ]; then
dir="${HOME:-/tmp}/shots"; mkdir -p "$dir"
n_file="$dir/.counter"; n=$(( $(cat "$n_file" 2>/dev/null || echo 0) + 1 )); echo "$n" > "$n_file"
OUT="$dir/shot-$(printf '%04d' "$n").png"
fi
RAW="$(mktemp /tmp/shot-raw-XXXXXX.png)"
trap 'rm -f "$RAW"' EXIT
"$REAL" "$RAW" >/dev/null || exit 1
# "…": ("xenia_canary" "Xenia_canary") 1280x745+0+0 +0+0
geo=$(xwininfo -root -children 2>/dev/null \
| grep '"xenia_canary"' | grep -oE '[0-9]+x[0-9]+\+-?[0-9]+\+-?[0-9]+' | head -1)
if [ -z "$geo" ]; then # no window found — hand back the raw grab
cp "$RAW" "$OUT"; echo "$OUT"; exit 0
fi
W=${geo%%x*}; rest=${geo#*x}; H=${rest%%+*}; rest=${rest#*+}; X=${rest%%+*}; Y=${rest#*+}
OFF=$(( H - 720 )); [ "$OFF" -lt 0 ] && OFF=0
TOP=$(( Y + OFF ))
[ "$TOP" -lt 0 ] && TOP=0
if [ "$OFF" -eq 0 ] && [ "$TOP" -eq 0 ] && [ "$X" -eq 0 ]; then
cp "$RAW" "$OUT"; echo "$OUT"; exit 0
fi
# Height is whatever is still on screen: a 1280x745 window on a 720-high root
# loses its last rows, and every oracle point in use sits well above them.
convert "$RAW" -crop "${W}x720+${X}+${TOP}" +repage "$OUT" 2>/dev/null \
|| cp "$RAW" "$OUT"
echo "$OUT"

113
tools/re-capture/burst_probe.py Executable file
View File

@@ -0,0 +1,113 @@
#!/usr/bin/env python3
"""Angular rate at a KNOWN, SETTLED speed — short bursts instead of one sweep.
Why not a sweep: turning bleeds speed hard (1 193 -> 589 in 8 s at full throttle),
so a long hold averages a rate over a moving speed and lands between two different
caps. A sweep *driven* by that bleed also cannot dwell at either extreme, which is
exactly where a speed-dependent law is most testable.
So: settle the throttle, measure the settled speed, then pitch for only ~1 second.
The speed barely moves inside a burst, so each burst is one honest `(speed, rate)`
point. Three throttle settings give three clean points at known speeds.
Row pinning is done ONCE while the craft is still level — the pin decides which
matrix ROW is up and which is right, which is a property of the layout, not of the
current attitude, so it stays valid after the craft has been thrown around.
Accumulation is windowed over the whole burst (never per-read): polling is faster
than the guest updates these fields, so a per-read delta is either zero or a whole
frame divided by a fraction of one — that aliasing once manufactured an entire
rate-vs-speed curve.
Usage: burst_probe.py <roll|pitch> <out.csv> [burst_s] [repeats]
"""
import json
import math
import os
import struct
import sys
import time
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import speed_law # noqa: E402
from axis_probe import alive, pad_state, pin_rows, rows_at, dot # noqa: E402
DRIVER = {"roll": {"lx": 32767}, "pitch": {"ly": 32767}}
THROTTLES = [("min", {"lt": 255}), ("cruise", {}), ("max", {"rt": 255})]
def pos_at(fd, off):
return struct.unpack(">3f", os.pread(fd, 12, off))
def measure(w, off, cfg, axis, u_i, w_i, f_i, hold, secs):
"""Accumulate path length and swept angle over one interval."""
prev_r, prev_p = rows_at(w.fd, off, cfg), pos_at(w.fd, off)
t0 = time.time()
path = swept = 0.0
while time.time() - t0 < secs:
time.sleep(0.02)
cur_r, cur_p = rows_at(w.fd, off, cfg), pos_at(w.fd, off)
path += math.dist(cur_p, prev_p)
if axis == "roll":
d = math.atan2(dot(cur_r[u_i], prev_r[w_i]), dot(cur_r[u_i], prev_r[u_i]))
else:
d = math.atan2(dot(cur_r[f_i], prev_r[u_i]), dot(cur_r[f_i], prev_r[f_i]))
swept += abs(math.degrees(d))
prev_r, prev_p = cur_r, cur_p
span = time.time() - t0
return path / span, swept / span
def main():
axis, out_csv = sys.argv[1], sys.argv[2]
burst = float(sys.argv[3]) if len(sys.argv) > 3 else 1.0
reps = int(sys.argv[4]) if len(sys.argv) > 4 else 3
cfg = json.load(open("/tmp/nav-live.json"))
w, off, nm = speed_law.find_player()
if not w:
sys.exit("player entity not found")
f_i = cfg["fwd_row"]
if not alive(w, off, cfg):
sys.exit("craft is not moving")
u_i, w_i = pin_rows(w, off, cfg) # once, while level
print(f"# locked on {nm}, {axis} in {burst}s bursts x{reps}")
# ⚠️ Gap in the first run of this probe: it did NOT bracket the HUD clock, so
# the absolute deg/GAME-second could not be computed and only the (clock-free)
# min:max ratio was usable. Screenshot the HUD at the start and end.
import subprocess
subprocess.run(["screenshot", "/sylph-home/re/shots/burst_clock_a.png"],
capture_output=True)
rows = []
for tname, tstate in THROTTLES:
for rep in range(reps):
pad_state(**tstate)
time.sleep(6.0) # settle the throttle, wings level-ish
settled, _ = measure(w, off, cfg, axis, u_i, w_i, f_i, None, 1.0)
pad_state(**tstate, **DRIVER[axis])
bspeed, rate = measure(w, off, cfg, axis, u_i, w_i, f_i, None, burst)
pad_state(**tstate)
rows.append((tname, rep, round(settled, 1), round(bspeed, 1), round(rate, 2)))
print(f"# {tname:<6} rep{rep} settled {settled:7.1f} "
f"during {bspeed:7.1f} rate {rate:6.1f} deg/wall-s")
if not alive(w, off, cfg):
print("# CRAFT STOPPED MOVING — aborting")
pad_state()
break
else:
continue
break
pad_state()
subprocess.run(["screenshot", "/sylph-home/re/shots/burst_clock_b.png"],
capture_output=True)
with open(out_csv, "w") as f:
f.write("throttle,rep,settled_speed,burst_speed,rate_deg_per_wall_s\n")
for r in rows:
f.write(",".join(str(x) for x in r) + "\n")
print(f"# wrote {out_csv}")
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,137 @@
#!/usr/bin/env bash
# Probe the challenge-mission gate on the running game.
#
# The gate (docs/re/challenge-mission-gate.md): GamePart_ChallengeMission tests a
# CLEARED-STAGE bitmask on a static singleton at guest 0x828F4070 —
# word A 0x828F40C0 bit = stage id, for ids < 24 (story 1-16, tutorial 18-23)
# word B 0x828F4814 bit = stage id - 24 (challenge 24-29)
# so setting every bit should make all six challenge missions available without
# playing the campaign.
#
# POKE=1 (default) sets both words; POKE=0 runs the identical navigation without
# touching them. Run it BOTH ways: the first poked run hit a Xenia heap failure on
# the way into MISSION SELECT, and only the control says whether that failure has
# anything to do with the poke.
#
# Runs as ONE blocking foreground call on purpose: setsid'd processes are reaped
# at turn boundaries, so a session split across calls loses its emulator.
#
# Usage: [POKE=0|1] [TAG=name] challenge_probe.sh [boot_timeout_s]
set -u
export HOME=/sylph-home/re
export DISPLAY=:99
export SDL_AUDIODRIVER=dummy
export XENIA_PAD_FILE=/tmp/xenia_pad.txt
HERE="$(cd "$(dirname "$0")" && pwd)"
pad() { python3 "$HERE/pad.py" "$@"; }
poke() { python3 "$HERE/gpoke.py" "$@"; }
SHOTS="$HOME/shots"
BOOT_TIMEOUT="${1:-400}"
POKE="${POKE:-1}"
POKE_A="${POKE_A:-0x0001FFFE}" # stages 1-16 cleared
POKE_B="${POKE_B:-0x0000003F}" # challenge stages 24-29 cleared
TAG="${TAG:-$([ "$POKE" = 1 ] && echo poked || echo control)}"
mkdir -p "$SHOTS"
say() { echo "[$(date +%H:%M:%S)] $*"; }
shot() { screenshot "$SHOTS/chal-$TAG-$1.png" >/dev/null 2>&1; }
# --- clean slate -------------------------------------------------------------
pkill -9 -x xenia_canary 2>/dev/null
sleep 1
rm -f /dev/shm/xenia_* 2>/dev/null
: > "$XENIA_PAD_FILE"
LOG="$HOME/canary.stdout"
# --- launch ------------------------------------------------------------------
say "launching canary (lavapipe, file pad) — POKE=$POKE tag=$TAG"
run-canary --audio --apu=sdl --log_mask=13 \
--logged_profile_slot_0_xuid=E0300000EFBEA3D4 \
--hid=file --pad_file="$XENIA_PAD_FILE" &
# Xvfb keeps the LAST instance's framebuffer until the new one draws, so a
# screenshot taken seconds after launch shows the PREVIOUS run's screen. That is
# how a control run once reported "MAIN MENU reached after 1s" against a menu
# belonging to a process that no longer existed. Blank the root, and refuse to
# believe any screen oracle until the emulator has had time to draw its own.
xsetroot -solid black 2>/dev/null || true
LAUNCH_GRACE=40
# --- reach the MAIN MENU, verifying instead of pressing blind ----------------
# Two oracles, both sampled from real screenshots:
# title screen : green "PRESS (A) BUTTON" glyph at (625,618)
# main menu : the "NEW GAME" text at (648,221) is pure white (254,254,254),
# where the title has the yellow planet (208,189,88)
# The first control run pressed A while the ATTRACT MOVIE happened to show a
# greenish pixel at the title-glyph spot, then navigated a menu that was never
# open and reported "0 failures" for a screen it never reached. Verify the menu.
px() { convert /tmp/nav-probe.png -format \
"%[fx:int(255*p{$1}.r)] %[fx:int(255*p{$1}.g)] %[fx:int(255*p{$1}.b)]" info: 2>/dev/null; }
# A screen is identified by a PATTERN of sampled points, never by one pixel. A
# single "is (648,221) white?" test matched a white loading flash, and the run
# then navigated a menu that was not on screen -- the same class of mistake as
# trusting the stale framebuffer. Require the menu's contrast: white "NEW GAME"
# text AND the dark blue panel behind it.
at_menu() {
screenshot /tmp/nav-probe.png >/dev/null 2>&1 || return 1
read -r r g b < <(px "648,221") # NEW GAME text: white
[ -n "${r:-}" ] || return 1
[ "$r" -gt 230 ] && [ "$g" -gt 230 ] && [ "$b" -gt 230 ] || return 1
read -r r2 g2 b2 < <(px "560,300") # panel left of LOAD GAME: dark blue
[ -n "${r2:-}" ] || return 1
[ "$r2" -lt 120 ] && [ "$b2" -gt "$r2" ]
}
at_title() {
screenshot /tmp/nav-probe.png >/dev/null 2>&1 || return 1
read -r r g b < <(px "625,618")
[ -n "${g:-}" ] && [ "$g" -gt 130 ] && [ $((g - r)) -gt 45 ] && [ $((g - b)) -gt 45 ]
}
say "waiting for the main menu (up to ${BOOT_TIMEOUT}s)"
MENU=0
for i in $(seq 1 "$BOOT_TIMEOUT"); do
if [ "$i" -lt "$LAUNCH_GRACE" ]; then sleep 1; continue; fi
if at_menu && sleep 1 && at_menu; then say "MAIN MENU reached after ${i}s"; MENU=1; break; fi
if at_title; then say " title visible — tapping A"; pad tap A 0.25; sleep 2; fi
sleep 1
done
[ "$MENU" = 1 ] || { say "TIMEOUT: never reached the main menu"; shot 00-timeout; exit 1; }
shot 01-mainmenu
say "gate words:"
poke r32 0x828F40C0 1
poke r32 0x828F4814 1
if [ "$POKE" = 1 ]; then
# Default to REAL stage ids only. 0xFFFFFFFF claims stages that do not exist
# (0, 17, and 24-31 in word A), and that run blew the guest heap:
# MmAllocatePhysicalMemoryEx could not satisfy 128 MB and the guest threw.
# Word A bits 1..16 = the story campaign; 18..23 would be the tutorials.
say "poking word A = $POKE_A, word B = $POKE_B"
poke w32 0x828F40C0 "$POKE_A"
poke w32 0x828F4814 "$POKE_B"
else
say "control run — leaving the words untouched"
fi
# --- main menu -> EXTRAS -----------------------------------------------------
# NEW GAME / LOAD GAME / TUTORIAL / OPTIONS / EXTRAS
for _ in 1 2 3 4; do pad dpad down 0.06; sleep 0.35; done
sleep 0.5
pad tap A 0.15
sleep 3
shot 02-extras
# --- EXTRAS -> MISSION SELECT ------------------------------------------------
ALLOC_BEFORE=$(grep -c 'MmAllocatePhysicalMemoryEx: Allocation failed' "$LOG" 2>/dev/null | head -1)
pad tap A 0.15
sleep 4
shot 03-missionselect
ALLOC_AFTER=$(grep -c 'MmAllocatePhysicalMemoryEx: Allocation failed' "$LOG" 2>/dev/null | head -1)
say "MmAllocatePhysicalMemoryEx failures: before=$ALLOC_BEFORE after=$ALLOC_AFTER"
say "guest C++ exceptions: $(grep -c 'Guest attempted to throw a C++ exception' "$LOG" 2>/dev/null | head -1)"
say "shots: $SHOTS/chal-$TAG-*.png"
say "done — emulator left running"

74
tools/re-capture/diff_words.py Executable file
View File

@@ -0,0 +1,74 @@
#!/usr/bin/env python3
"""Find guest addresses holding one value in snapshot A and another in snapshot B.
The classic differential search, which is how you locate a field whose ADDRESS is
unknown but whose VALUE is known at two moments. Used here for the GamePart slot:
`GP_EXTRAS` = 5 on the extras screen, `GP_MISSION_SELECT` = 7 on the next one, so
the slot is a word that reads 5 then 7.
Both snapshots are sparse copies of `/dev/shm/xenia_memory_*`, so the ~4.6 GB of
holes cost nothing: walk A's allocated extents with SEEK_DATA and only look at B
where A already matched.
diff_words.py <snapA> <valA> <snapB> <valB> [--near <va> <span>]
"""
import os
import struct
import sys
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from gmem import off_to_vas # noqa: E402
def main():
if len(sys.argv) < 5:
print(__doc__)
return 1
pa, va_, pb, vb_ = sys.argv[1], int(sys.argv[2], 0), sys.argv[3], int(sys.argv[4], 0)
pat_a = struct.pack(">I", va_)
fa, fb = open(pa, "rb"), open(pb, "rb")
size = min(os.path.getsize(pa), os.path.getsize(pb))
hits, scanned = [], 0
off = 0
while off < size:
try:
off = os.lseek(fa.fileno(), off, os.SEEK_DATA)
except OSError:
break
if off >= size:
break
try:
end = min(os.lseek(fa.fileno(), off, os.SEEK_HOLE), size)
except OSError:
end = size
while off < end:
n = min(1 << 22, end - off)
fa.seek(off)
buf = fa.read(n)
scanned += len(buf)
i = 0
while True:
i = buf.find(pat_a, i)
if i < 0:
break
p = off + i
if p % 4 == 0:
fb.seek(p)
if fb.read(4) == struct.pack(">I", vb_):
hits.append(p)
i += 1
off += n
print(f"scanned {scanned/1e6:.0f} MB of allocated guest memory")
print(f"addresses reading {va_} in A and {vb_} in B: {len(hits)}")
for h in hits[:40]:
vas = off_to_vas(h)
print(f" file off {h:#012x} guest VA {[hex(v) for v in vas][:2]}")
if len(hits) > 40:
print(f" … and {len(hits)-40} more")
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -145,7 +145,18 @@ def main():
return
if cmd == "self":
# The "_Player" suffix is STAGE-SPECIFIC. Stage 02 fields
# UN_f002_TCAF_DeltaSaber_W_Player, but stage 01 fields
# UN_f001_TCAF_DeltaSaber_T with no suffix at all, and this filter then
# found nothing while the game was visibly flying. Fall back to the
# player's craft class and prefer the instance that is actually moving —
# a mission holds more than one.
me = [e for e in ents if "Player" in e[1]]
if not me:
me = [e for e in ents if "DeltaSaber" in e[1] or "ArrowHead" in e[1]]
me.sort(key=lambda e: -e[3]) # fastest first: the flown one
if me:
print(f"# no *_Player entity; falling back to craft class -> {me[0][1]}")
if not me:
sys.exit("player entity not found")
off, nm, pos, sp = me[0]

View File

@@ -0,0 +1,69 @@
#!/usr/bin/env bash
# Locate the GamePart "slot" in guest memory by DIFFERENTIAL SEARCH.
#
# sub_821749C0 creates a part from slot+12 (the requested part id) and stores the
# result at *(slot+16). The id is computed from a menu selection -- no literal 26
# exists anywhere -- so it cannot be found statically. But the id is KNOWN at each
# screen from the GamePart table (docs/re/challenge-mission-gate.md section 3):
#
# GP_EXTRAS = 5 GP_MISSION_SELECT = 7
#
# so the slot is simply the word that reads 5 on the EXTRAS screen and 7 on the
# MISSION SELECT screen. Snapshot both, intersect, and the candidates are few.
#
# Usage: find_partslot.sh [boot_timeout_s]
set -u
export HOME=/sylph-home/re DISPLAY=:99 SDL_AUDIODRIVER=dummy
export XENIA_PAD_FILE=/tmp/xenia_pad.txt
HERE="$(cd "$(dirname "$0")" && pwd)"
pad() { python3 "$HERE/pad.py" "$@"; }
poke() { python3 "$HERE/gpoke.py" "$@"; }
BOOT_TIMEOUT="${1:-400}"
say() { echo "[$(date +%H:%M:%S)] $*"; }
px() { convert /tmp/nav-probe.png -format \
"%[fx:int(255*p{$1}.r)] %[fx:int(255*p{$1}.g)] %[fx:int(255*p{$1}.b)]" info: 2>/dev/null; }
at_menu() {
screenshot /tmp/nav-probe.png >/dev/null 2>&1 || return 1
read -r r g b < <(px "648,221"); [ -n "${r:-}" ] || return 1
[ "$r" -gt 230 ] && [ "$g" -gt 230 ] && [ "$b" -gt 230 ] || return 1
read -r r2 g2 b2 < <(px "560,300"); [ -n "${r2:-}" ] || return 1
[ "$r2" -lt 120 ] && [ "$b2" -gt "$r2" ]
}
at_title() {
screenshot /tmp/nav-probe.png >/dev/null 2>&1 || return 1
read -r r g b < <(px "625,618"); [ -n "${g:-}" ] || return 1
[ "$g" -gt 130 ] && [ $((g - r)) -gt 45 ] && [ $((g - b)) -gt 45 ]
}
snap() { SHM=$(ls /dev/shm/xenia_memory_* 2>/dev/null | head -1); cp --sparse=always "$SHM" "$1"; }
pkill -9 -x xenia_canary 2>/dev/null; sleep 1
rm -f /dev/shm/xenia_* 2>/dev/null; : > "$XENIA_PAD_FILE"
say "launching"
run-canary --audio --apu=sdl --log_mask=13 \
--logged_profile_slot_0_xuid=E0300000EFBEA3D4 \
--hid=file --pad_file="$XENIA_PAD_FILE" &
xsetroot -solid black 2>/dev/null || true
MENU=0
for i in $(seq 1 "$BOOT_TIMEOUT"); do
[ "$i" -lt 40 ] && { sleep 1; continue; }
if at_menu && sleep 1 && at_menu; then say "MAIN MENU after ${i}s"; MENU=1; break; fi
at_title && { say " title — tapping A"; pad tap A 0.25; sleep 2; }
sleep 1
done
[ "$MENU" = 1 ] || { say "TIMEOUT"; exit 1; }
say "-> EXTRAS (GamePart 5)"
for _ in 1 2 3 4; do pad dpad down 0.06; sleep 0.35; done
sleep 0.5; pad tap A 0.15; sleep 4
screenshot "$HOME/shots/slot-01-extras.png" >/dev/null
snap /sylph-home/re/snap-extras.bin; say "snapshot A (EXTRAS) taken"
say "-> MISSION SELECT (GamePart 7)"
pad tap A 0.15; sleep 5
screenshot "$HOME/shots/slot-02-missionselect.png" >/dev/null
snap /sylph-home/re/snap-msel.bin; say "snapshot B (MISSION SELECT) taken"
say "intersecting: word == 5 in A and == 7 in B"
python3 "$HERE/diff_words.py" /sylph-home/re/snap-extras.bin 5 /sylph-home/re/snap-msel.bin 7
say "done — emulator left running"

37
tools/re-capture/fly_stage.sh Executable file
View File

@@ -0,0 +1,37 @@
#!/usr/bin/env bash
# Launch any STORY stage from MISSION SELECT and snapshot guest RAM in flight.
#
# This is the LAUNCH half; the navigation lives in nav_to_flight.sh so a Canary
# that is already up (a boot to the title costs minutes under lavapipe) can be
# driven without paying for a second one.
#
# Usage: fly_stage.sh <stage 1..16> [boot_timeout_s]
set -u
STAGE="${1:?usage: fly_stage.sh <stage 1..16>}"
BOOT_TIMEOUT="${2:-400}"
# DISPLAY is overridable: the container entrypoint's own Xvfb owns :99's lock as
# root, and once that server dies an unprivileged relaunch cannot clear
# /tmp/.X99-lock — so a session may have to bring its display up elsewhere.
export HOME=/sylph-home/re DISPLAY="${DISPLAY:-:99}" SDL_AUDIODRIVER=dummy
export XENIA_PAD_FILE=/tmp/xenia_pad.txt
HERE="$(cd "$(dirname "$0")" && pwd)"
say() { echo "[$(date +%H:%M:%S)] $*"; }
pkill -9 -x xenia_canary 2>/dev/null; sleep 1
rm -f /dev/shm/xenia_* 2>/dev/null
: > "$XENIA_PAD_FILE"
say "launching canary for stage $STAGE"
# CANARY_EXTRA lets a caller add cvars (e.g. --frame_probe_log=...) without
# forking this script; unquoted on purpose so it can carry several.
# shellcheck disable=SC2086
run-canary --audio --apu=sdl --log_mask=13 \
--logged_profile_slot_0_xuid=E0300000EFBEA3D4 \
--hid=file --pad_file="$XENIA_PAD_FILE" ${CANARY_EXTRA:-} &
# Xvfb keeps the previous instance's framebuffer until the new one draws.
xsetroot -solid black 2>/dev/null || true
# Nothing is on screen for the first ~40 s, so do not start polling into it.
sleep 40
exec "$HERE/nav_to_flight.sh" "$STAGE" "$BOOT_TIMEOUT" --snapshot

109
tools/re-capture/frame_burst.py Executable file
View File

@@ -0,0 +1,109 @@
#!/usr/bin/env python3
"""Drive stick bursts while Canary samples the craft's transform ONCE PER FRAME.
Why this exists. Every earlier rate measurement polled guest RAM from the host
through /dev/shm. That read is unsynchronised with the guest: adjacent samples
are separated by an unknown number of guest updates, so short windows alias --
`ramp_probe.py` got 3x swings between neighbouring 0.25 s windows and could not
tell "the rate ramps up after the stick goes over" from "the sampler is lying".
See docs/re/flight-speed-law.md, which names a Canary-side hook as the tool the
residual needs.
That hook now exists (`--frame_probe_log`, sampled in VdSwap, one line per guest
frame). This script only has to point it at the player craft and drive the pad,
recording when each hold starts and ends in the SAME clock the probe stamps its
lines with, so the analysis can cut the log at the exact frame the stick moved.
Usage: frame_burst.py <events.csv> [hold_s] [repeats]
Env: XENIA_FRAME_PROBE control file (default /tmp/xenia_frame_probe.txt)
"""
import os
import struct
import sys
import time
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import gmem # noqa: E402
import speed_law # noqa: E402
from axis_probe import pad_state # noqa: E402
PROBE = os.environ.get("XENIA_FRAME_PROBE", "/tmp/xenia_frame_probe.txt")
# The transform block sits BELOW the position triple: three 16-byte-strided rows
# starting at pos-112, position at pos+0 (nav-live.json: rot_delta -112,
# rot_stride 16). One region covers both.
ROT_DELTA = -112
REGION_LEN = 128
# Full deflection on each axis, plus the throttle settings the burst design uses.
BURSTS = [
("roll_cruise", {"lx": 32767}),
("pitch_cruise", {"ly": 32767}),
]
def write_regions(regions):
tmp = PROBE + ".tmp"
with open(tmp, "w") as f:
f.write("# written by frame_burst.py\n")
for va, ln in regions:
f.write(f"{va:08x} {ln}\n")
os.replace(tmp, PROBE)
def pos_at(fd, off):
return struct.unpack(">3f", os.pread(fd, 12, off))
def main():
out_csv = sys.argv[1]
hold = float(sys.argv[2]) if len(sys.argv) > 2 else 3.0
reps = int(sys.argv[3]) if len(sys.argv) > 3 else 2
w, off, nm = speed_law.find_player()
if not w:
sys.exit("player entity not found")
base_off = off + ROT_DELTA
va = gmem.primary_va(base_off)
if va is None:
sys.exit(f"file offset {base_off:#x} has no guest VA")
print(f"# locked on {nm}: transform block at VA {va:#010x} (+{REGION_LEN})")
write_regions([(va, REGION_LEN)])
# The probe only starts emitting once the emulator notices the control file,
# which is at most one frame. Give it a moment, then prove the craft is
# actually flying before spending the run on it.
time.sleep(1.0)
p0 = pos_at(w.fd, off)
time.sleep(1.0)
p1 = pos_at(w.fd, off)
moved = sum((p1[i] - p0[i]) ** 2 for i in range(3)) ** 0.5
if moved < 1.0:
sys.exit(f"craft is not moving ({moved:.3f}) — not in flight, or dead")
print(f"# craft is flying ({moved:.1f} units/s)")
rows = []
for rep in range(reps):
for label, state in BURSTS:
pad_state()
time.sleep(5.0)
t_pre = time.time()
pad_state(**state)
t_on = time.time()
time.sleep(hold)
pad_state()
t_off = time.time()
rows.append((rep, label, f"{t_pre:.6f}", f"{t_on:.6f}", f"{t_off:.6f}"))
print(f"# rep{rep} {label}: held {t_on:.3f} -> {t_off:.3f}", flush=True)
time.sleep(2.0)
pad_state()
with open(out_csv, "w") as f:
f.write("rep,label,t_pre,t_on,t_off\n")
for r in rows:
f.write(",".join(str(x) for x in r) + "\n")
print(f"# wrote {out_csv}")
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,59 @@
#!/usr/bin/env bash
# One blocking foreground session: boot to flight, then measure angular rate with
# the per-frame probe instead of host polling.
#
# Everything (Xvfb, openbox, xenia) is a plain child of THIS script. Anything
# `setsid`'d is reaped at the agent's turn boundary — see the session-lifetime
# note in launch_mission.sh — so the whole run has to fit in one call.
#
# Usage: frame_session.sh [stage] [hold_s] [repeats]
set -u
STAGE="${1:-10}"; HOLD="${2:-3.0}"; REPS="${3:-2}"
export HOME=/sylph-home/re DISPLAY="${DISPLAY:-:99}" SDL_AUDIODRIVER=dummy
export XENIA_PAD_FILE=/tmp/xenia_pad.txt
export XENIA_FRAME_PROBE=/tmp/xenia_frame_probe.txt
HERE="$(cd "$(dirname "$0")" && pwd)"
OUT="$HOME/frameprobe"; mkdir -p "$OUT"
LOG="$OUT/frame-stage$STAGE.log"
say() { echo "[$(date +%H:%M:%S)] $*"; }
ensure_display() {
if ! xdpyinfo -display "$DISPLAY" >/dev/null 2>&1; then
rm -f "/tmp/.X${DISPLAY#:}-lock" 2>/dev/null || true
nohup bash -c 'Xvfb "$0" -screen 0 1280x720x24 -ac -nolisten tcp \
+extension GLX +extension RANDR >/tmp/xvfb${DISPLAY#:}.log 2>&1
echo "$(date +%T) XVFB EXIT $? (128+N means signal N)" >>/tmp/xvfb-exit.log' \
"$DISPLAY" </dev/null >/dev/null 2>&1 &
for _ in $(seq 1 50); do xdpyinfo -display "$DISPLAY" >/dev/null 2>&1 && break; sleep 0.2; done
nohup env DISPLAY="$DISPLAY" HOME=/sylph-home openbox </dev/null >/tmp/openbox${DISPLAY#:}.log 2>&1 &
sleep 1
fi
xdpyinfo -display "$DISPLAY" >/dev/null 2>&1 || { say "DISPLAY UNAVAILABLE"; exit 1; }
}
ensure_display
# A stale control file would make the probe log the PREVIOUS run's addresses from
# the first frame, which land in whatever occupies them now.
rm -f "$XENIA_FRAME_PROBE"
# REUSE=1 drives a Canary that is already up (it must have been launched with the
# same --frame_probe_log). Booting to the title costs minutes under lavapipe and
# a run that only failed to navigate should not pay for it again.
if [ "${REUSE:-0}" = 1 ] && pgrep -x xenia_canary >/dev/null; then
say "re-using the running emulator; navigating to stage $STAGE"
"$HERE/nav_to_flight.sh" "$STAGE" 300 || { say "navigation failed ($?)"; exit 2; }
else
rm -f "$LOG"
say "booting to flight on stage $STAGE with the per-frame probe armed"
CANARY_EXTRA="--frame_probe_log=$LOG --frame_probe=$XENIA_FRAME_PROBE" \
"$HERE/fly_stage.sh" "$STAGE" 300 || { say "boot failed ($?)"; exit 2; }
fi
say "arming the probe on the player craft and driving the bursts"
python3 "$HERE/frame_burst.py" "$OUT/events-stage$STAGE.csv" "$HOLD" "$REPS"
rc=$?
# Stop sampling before the emulator is torn down, so the log ends cleanly.
rm -f "$XENIA_FRAME_PROBE"
say "frame log: $LOG ($(wc -l < "$LOG" 2>/dev/null || echo 0) lines)"
exit $rc

76
tools/re-capture/gpoke.py Executable file
View File

@@ -0,0 +1,76 @@
#!/usr/bin/env python3
"""WRITE to the live guest memory of a running Xenia Canary.
The read-side companion is `gmem.py`, and this shares its guest-VA → file-offset
table. Canary backs the whole guest address space with one shared-memory file
(`/dev/shm/xenia_memory_*`), so a write here lands in the running guest with no
debugger and no pause.
gpoke.py w32 <va> <value> [...] write big-endian u32s at consecutive VAs
gpoke.py r32 <va> [n] read back n big-endian u32s (verify)
Values and addresses accept `0x` form. Every write prints the before/after word,
because a poke you cannot see is a poke you cannot trust.
⚠️ This mutates a running game. It is a research tool: there is no undo, and a
wrong address will corrupt whatever it lands on. Read back before believing.
"""
import os
import struct
import sys
from gmem import MAP, va_to_off # noqa: F401 (MAP re-exported for callers)
def shm_path():
cands = [f"/dev/shm/{n}" for n in os.listdir("/dev/shm") if n.startswith("xenia_memory")]
if not cands:
raise SystemExit("no /dev/shm/xenia_memory_* — is Canary running?")
if len(cands) > 1:
raise SystemExit(f"several guest images, refusing to guess: {cands}")
return cands[0]
def read32(f, va):
f.seek(va_to_off(va))
return struct.unpack(">I", f.read(4))[0]
def main():
if len(sys.argv) < 3:
print(__doc__)
return 1
cmd = sys.argv[1]
path = shm_path()
if cmd == "r32":
va = int(sys.argv[2], 0)
n = int(sys.argv[3], 0) if len(sys.argv) > 3 else 1
with open(path, "rb") as f:
for i in range(n):
a = va + 4 * i
print(f" {a:#010x} = {read32(f, a):#010x}")
return 0
if cmd == "w32":
va = int(sys.argv[2], 0)
vals = [int(v, 0) for v in sys.argv[3:]]
if not vals:
print("nothing to write")
return 1
with open(path, "r+b") as f:
for i, v in enumerate(vals):
a = va + 4 * i
before = read32(f, a)
f.seek(va_to_off(a))
f.write(struct.pack(">I", v))
f.flush()
after = read32(f, a)
ok = "OK" if after == v else "!! MISMATCH"
print(f" {a:#010x}: {before:#010x} -> {after:#010x} {ok}")
return 0
print(f"unknown command {cmd!r}")
return 1
if __name__ == "__main__":
raise SystemExit(main())

134
tools/re-capture/nav_to_flight.sh Executable file
View File

@@ -0,0 +1,134 @@
#!/usr/bin/env bash
# Drive an ALREADY-RUNNING Canary from the title/attract movie to flight on any
# story stage. Split out of fly_stage.sh so a live emulator can be re-used: a
# boot to the title costs minutes under lavapipe, and a run that only failed to
# navigate should not have to pay for it twice.
#
# Poking ONE word marks every story stage cleared, so MISSION SELECT will launch
# any of them — 0x828F40C0 = 0x0001FFFE (cleared-stage mask, bits 1..16); see
# docs/re/challenge-mission-gate.md. Nothing is written to disc.
#
# Usage: nav_to_flight.sh <stage 1..16> [menu_timeout_s] [--snapshot]
set -u
STAGE="${1:?usage: nav_to_flight.sh <stage 1..16>}"
MENU_TIMEOUT="${2:-400}"
SNAPSHOT="${3:-}"
# DISPLAY is overridable: the container entrypoint's own Xvfb owns :99's lock as
# root, and once that server dies an unprivileged relaunch cannot clear
# /tmp/.X99-lock — so a session may have to bring its display up elsewhere.
export HOME=/sylph-home/re DISPLAY="${DISPLAY:-:99}" SDL_AUDIODRIVER=dummy
export XENIA_PAD_FILE=/tmp/xenia_pad.txt
HERE="$(cd "$(dirname "$0")" && pwd)"
# bin/screenshot crops the emulator's menu bar away so a saved shot looks like
# the bare game image; the oracles below no longer depend on that, but the shots
# kept for evidence are easier to compare against older ones.
export PATH="$HERE/bin:$PATH"
pad() { python3 "$HERE/pad.py" "$@"; }
poke() { python3 "$HERE/gpoke.py" "$@"; }
SHOTS="$HOME/shots"; mkdir -p "$SHOTS"
say() { echo "[$(date +%H:%M:%S)] $*"; }
shot() { screenshot "$SHOTS/fly$STAGE-$1.png" >/dev/null 2>&1; }
screen() { python3 "$HERE/screen_id.py" /tmp/nav-probe.png --json 2>/dev/null; }
# Screens are identified by WHOLE-IMAGE statistics (screen_id.py), never by named
# pixels. The pixel oracles this replaces were measured against a bare 1280x720
# game image, and xenia's window puts a menu bar above it on some displays -- so
# every constant read ~25 px too high, silently. That cost one run 300 s staring
# at a visible MAIN MENU and another ten minutes of attract movie.
# One grab per poll, classified once: a screenshot costs seconds while lavapipe
# has every core, so asking three separate questions per second does not work.
which_screen() {
screenshot /tmp/nav-probe.png >/dev/null 2>&1 || { echo none; return; }
screen | python3 -c 'import json,sys; print(json.load(sys.stdin)["screen"])' 2>/dev/null || echo none
}
pgrep -x xenia_canary >/dev/null || { say "no xenia_canary running"; exit 1; }
say "waiting for the main menu"
MENU=0
LAST=
for i in $(seq 1 "$MENU_TIMEOUT"); do
NOW=$(which_screen)
[ "$NOW" != "$LAST" ] && { say " screen: $NOW"; LAST=$NOW; }
# Two consecutive reads, because a movie frame can momentarily look like a menu.
if [ "$NOW" = menu ] && sleep 1 && [ "$(which_screen)" = menu ]; then
say "MAIN MENU after ${i}s"; MENU=1; break
fi
[ "$NOW" = title ] && { say " title — tapping A"; pad tap A 0.25; sleep 2; }
sleep 1
done
[ "$MENU" = 1 ] || { say "TIMEOUT: no main menu"; shot 00-timeout; exit 1; }
say "poking the cleared-stage mask so every story stage is selectable"
poke w32 0x828F40C0 0x0001FFFE
# main menu -> EXTRAS -> MISSION SELECT
for _ in 1 2 3 4; do pad dpad down 0.06; sleep 0.35; done
sleep 0.5; pad tap A 0.15; sleep 3; shot 01-extras
pad tap A 0.15; sleep 4; shot 02-missionselect
# the list starts on Stage01
say "stepping to Stage$STAGE"
for _ in $(seq 2 "$STAGE"); do pad dpad down 0.06; sleep 0.30; done
sleep 1; shot 03-selected
pad tap A 0.20; sleep 3; shot 04-after-A
pad tap A 0.20; sleep 3; shot 05-after-A2
# Wait for a screen instead of sleeping a guess. The fixed sleeps below used to be
# enough and then were not: one run's stage load ran long, the script pressed START
# into a black loading screen, and every later step went to nothing while the shots
# recorded a plausible-looking sequence. Same lesson as the menu oracles.
wait_not_black() { # $1 = timeout s
for _ in $(seq 1 "$1"); do
if screenshot /tmp/nav-probe.png >/dev/null 2>&1; then
lit=$(screen | python3 -c 'import json,sys; d=json.load(sys.stdin); print(1 if d.get("r",0)+d.get("g",0)+d.get("b",0) > 20 else 0)' 2>/dev/null)
[ "$lit" = 1 ] && return 0
fi
sleep 1
done
return 1
}
# Selecting a stage lands on the mission BRIEFING (A: Continue), which leads to the
# READY ROOM (TAKE OFF / BRIEFINGS / HANGAR / ...) with BRIEFINGS pre-highlighted.
# A snapshot taken at the briefing yields ZERO unit-definition objects — they are
# instantiated at stage load proper — so the run has to reach flight.
# START (Skip) clears the briefing. `A` does NOT: it pages through the brief and
# ten taps still left the run sitting on a briefing screen, twice. One START press
# lands on the READY ROOM.
say "waiting for the stage to finish loading"
if ! wait_not_black 180; then
say "STILL BLACK after 180s — the load hung (this happens; check the log for"
say " 'PhysicalHeap::Release failed'), aborting instead of pressing into nothing"
shot 06-stuck-black
exit 2
fi
sleep 3
say "skipping the briefing (START, not A)"
pad tap START 0.30; sleep 6
shot 06-readyroom
say "READY ROOM -> TAKE OFF (one up from the pre-highlighted BRIEFINGS)"
pad dpad up 0.06; sleep 1; shot 07-takeoff-hl
pad tap A 0.20; sleep 8
# TAKE OFF starts its own load, and it hangs black just as the stage load can --
# guarding only the first transition left a run pressing A into a black screen and
# then reporting "player entity not found" from a game that never reached flight.
say "waiting for the take-off load"
if ! wait_not_black 180; then
say "STILL BLACK after take-off — load hung, aborting"
shot 08-stuck-black
exit 2
fi
for _ in $(seq 1 6); do pad tap A 0.20; sleep 4; done
say "waiting for flight to settle"
sleep 25
shot 08-flight
if [ "$SNAPSHOT" = "--snapshot" ]; then
SHM=$(ls /dev/shm/xenia_memory_* 2>/dev/null | head -1)
if [ -n "$SHM" ]; then
OUT="$HOME/snap-stage$STAGE.bin"
cp --sparse=always "$SHM" "$OUT" && say "snapshot -> $OUT ($(du -h "$OUT" | cut -f1) actual)"
fi
fi
say "shots: $SHOTS/fly$STAGE-*.png"

70
tools/re-capture/pad.py Executable file
View File

@@ -0,0 +1,70 @@
#!/usr/bin/env python3
"""Drive Xenia Canary's `--hid=file` pad — the container-safe controller.
Replaces the old `vgamepad` path, which created its device through `/dev/uinput`.
Input devices are not namespaced, so that device registered with the HOST's input
stack and every scripted press leaked to the user's desktop. This one writes a
text file the emulator polls; nothing leaves the container.
pad.py set "press=A" set the pad state and leave it held
pad.py clear release everything
pad.py tap A [secs] press, hold `secs` (default 0.10), release
pad.py dpad down [secs] one menu step (default 0.06 — longer
auto-repeats and overshoots)
pad.py hold "lt=255" secs hold an arbitrary state for `secs`
Buttons: UP DOWN LEFT RIGHT START BACK LS RS LB RB A B X Y.
Path from $XENIA_PAD_FILE, default /tmp/xenia_pad.txt (matches --pad_file).
Menu conventions in this game: A = OK, B = Back, Y = Gallery/extra, X = Delete.
"""
import os
import sys
import time
PAD = os.environ.get("XENIA_PAD_FILE", "/tmp/xenia_pad.txt")
def write(state: str):
# The driver re-parses on any (mtime-ns, size) change, so a plain rewrite is
# enough — but write through a temp + rename so a poll can never observe a
# half-written file.
tmp = PAD + ".tmp"
with open(tmp, "w") as f:
f.write(state)
os.replace(tmp, PAD)
def main():
a = sys.argv[1:]
if not a:
print(__doc__)
return 1
cmd = a[0]
if cmd == "set":
write(a[1])
elif cmd == "clear":
write("")
elif cmd == "tap":
secs = float(a[2]) if len(a) > 2 else 0.10
write(f"press={a[1]}")
time.sleep(secs)
write("")
elif cmd == "dpad":
secs = float(a[2]) if len(a) > 2 else 0.06
write(f"press={a[1].upper()}")
time.sleep(secs)
write("")
elif cmd == "hold":
write(a[1])
time.sleep(float(a[2]))
write("")
else:
print(f"unknown command {cmd!r}")
return 1
return 0
if __name__ == "__main__":
raise SystemExit(main())

94
tools/re-capture/ramp_probe.py Executable file
View File

@@ -0,0 +1,94 @@
#!/usr/bin/env python3
"""Does the angular rate RAMP after the stick goes over?
Burst measurements came out a strikingly flat 0.60-0.64x of the roll cap and
1.07-1.21x of the pitch cap. A clock error cannot do that (it would move both the
same way), so something per-axis is going on, and the cheapest candidate is that a
short burst never reaches the steady rate — an angular-acceleration ramp would make
a 1-second burst under-read.
Rather than compare separate bursts (which differ in speed, attitude and starting
conditions), measure INSIDE one hold: successive 0.25 s windows of a single 3 s
press. A ramp shows up as the first windows reading low and the later ones
plateauing, with everything else held constant by construction.
Windows, never per-read deltas: polling outruns the guest's update of these fields.
Usage: ramp_probe.py <roll|pitch> <out.csv> [hold_s] [repeats]
"""
import json
import math
import os
import struct
import sys
import time
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import speed_law # noqa: E402
from axis_probe import alive, pad_state, pin_rows, rows_at, dot # noqa: E402
DRIVER = {"roll": {"lx": 32767}, "pitch": {"ly": 32767}}
WIN = 0.25
def pos_at(fd, off):
return struct.unpack(">3f", os.pread(fd, 12, off))
def main():
axis, out_csv = sys.argv[1], sys.argv[2]
hold = float(sys.argv[3]) if len(sys.argv) > 3 else 3.0
reps = int(sys.argv[4]) if len(sys.argv) > 4 else 2
cfg = json.load(open("/tmp/nav-live.json"))
w, off, nm = speed_law.find_player()
if not w:
sys.exit("player entity not found")
f_i = cfg["fwd_row"]
if not alive(w, off, cfg):
sys.exit("craft is not moving")
u_i, w_i = pin_rows(w, off, cfg)
print(f"# locked on {nm}, {axis} ramp over {hold}s in {WIN}s windows")
rows = []
for rep in range(reps):
pad_state() # cruise: least speed bleed of the three
time.sleep(6.0)
prev_r, prev_p = rows_at(w.fd, off, cfg), pos_at(w.fd, off)
pad_state(**DRIVER[axis])
t0 = wt0 = time.time()
path = swept = 0.0
seq = []
while time.time() - t0 < hold:
time.sleep(0.02)
now = time.time()
cur_r, cur_p = rows_at(w.fd, off, cfg), pos_at(w.fd, off)
path += math.dist(cur_p, prev_p)
if axis == "roll":
d = math.atan2(dot(cur_r[u_i], prev_r[w_i]), dot(cur_r[u_i], prev_r[u_i]))
else:
d = math.atan2(dot(cur_r[f_i], prev_r[u_i]), dot(cur_r[f_i], prev_r[f_i]))
swept += abs(math.degrees(d))
prev_r, prev_p = cur_r, cur_p
if now - wt0 >= WIN:
span = now - wt0
seq.append((round(now - t0, 2), round(path / span, 1), round(swept / span, 1)))
wt0, path, swept = now, 0.0, 0.0
pad_state()
for t, sp, rt in seq:
rows.append((rep, t, sp, rt))
print(f"# rep{rep}: " + " ".join(f"{rt:.0f}" for _, _, rt in seq))
print(f"# speed " + " ".join(f"{sp:.0f}" for _, sp, _ in seq))
if not alive(w, off, cfg):
print("# CRAFT STOPPED MOVING — aborting")
break
time.sleep(2.0)
with open(out_csv, "w") as f:
f.write("rep,t,speed,rate_deg_per_wall_s\n")
for r in rows:
f.write(",".join(str(x) for x in r) + "\n")
print(f"# wrote {out_csv}")
if __name__ == "__main__":
raise SystemExit(main())

117
tools/re-capture/rate_curve.py Executable file
View File

@@ -0,0 +1,117 @@
#!/usr/bin/env python3
"""Angular rate as a FUNCTION of speed — fitted, not sampled at two endpoints.
The two-point probe (`rate_probe.py`) assumed the craft held the speed its throttle
selects. It does not: full pitch bleeds 1 193 -> 589 in eight seconds *with the
throttle still at maximum*, so an 8-second average is taken over a moving speed and
lands between two different caps. That is how a measured pitch rate came out 33 %
ABOVE its own cap.
Turn the bug into the instrument. Because the speed bleeds on its own, one long hold
sweeps the whole range, so sampling position *and* attitude together gives
`(speed, rate)` pairs across it — the entire curve from a single phase, with no
assumption about what speed the craft is at.
Speed comes from the position deltas (no HUD OCR needed). Both speed and rate are
per wall-second here; the run's clock ratio converts both to game units afterwards
and cancels out of the SHAPE of the curve.
⚠️ SAMPLE IN WINDOWS, NOT PER READ. Polling at 20 Hz is faster than the guest
updates these fields, so a per-read delta is either exactly zero (no update yet) or
a whole frame's worth divided by a fraction of a frame. In a first run 111 of 352
reads were zero on BOTH channels — position and attitude update on the same frame,
so the two are perfectly correlated, and dividing each by the short wall dt
manufactured a clean "rate rises with speed" curve out of pure aliasing. Summing
|delta| over a window that spans many frames is immune: the total is right however
the updates fall inside it. (This is also why the swept-total probes were never
affected — only per-sample instantaneous rates were.)
Usage: rate_curve.py <roll|pitch> <out.csv> [dwell_s]
"""
import json
import math
import os
import struct
import subprocess
import sys
import time
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import speed_law # noqa: E402
from axis_probe import alive, pad_state, pin_rows, rows_at, dot # noqa: E402
DRIVER = {"roll": {"lx": 32767}, "pitch": {"ly": 32767}}
def shot(n):
subprocess.run(["screenshot", f"/sylph-home/re/shots/{n}.png"], capture_output=True)
def pos_at(fd, off):
return struct.unpack(">3f", os.pread(fd, 12, off))
def main():
axis, out_csv = sys.argv[1], sys.argv[2]
dwell = float(sys.argv[3]) if len(sys.argv) > 3 else 16.0
cfg = json.load(open("/tmp/nav-live.json"))
w, off, nm = speed_law.find_player()
if not w:
sys.exit("player entity not found")
f_i = cfg["fwd_row"]
if not alive(w, off, cfg):
sys.exit("craft is not moving")
u_i, w_i = pin_rows(w, off, cfg)
print(f"# locked on {nm}, sweeping {axis}")
# Start at maximum speed and let the turn bleed it: one hold, whole range.
pad_state(rt=255)
time.sleep(6.0)
shot(f"curve_{axis}_a")
prev_r, prev_p, prev_t = rows_at(w.fd, off, cfg), pos_at(w.fd, off), time.time()
pad_state(rt=255, **DRIVER[axis])
t0 = time.time()
rows = []
WIN = 0.5 # seconds per emitted point: many guest frames
win_t0, win_path, win_swept = time.time(), 0.0, 0.0
while time.time() - t0 < dwell:
time.sleep(0.02)
now = time.time()
cur_r, cur_p = rows_at(w.fd, off, cfg), pos_at(w.fd, off)
win_path += math.dist(cur_p, prev_p)
if axis == "roll":
d = math.atan2(dot(cur_r[u_i], prev_r[w_i]), dot(cur_r[u_i], prev_r[u_i]))
else:
d = math.atan2(dot(cur_r[f_i], prev_r[u_i]), dot(cur_r[f_i], prev_r[f_i]))
win_swept += abs(math.degrees(d))
prev_r, prev_p = cur_r, cur_p
if now - win_t0 >= WIN:
span = now - win_t0
rows.append((round(now - t0, 2), round(win_path / span, 1),
round(win_swept / span, 2)))
win_t0, win_path, win_swept = now, 0.0, 0.0
pad_state()
shot(f"curve_{axis}_b")
with open(out_csv, "w") as f:
f.write("t,speed_units_per_wall_s,rate_deg_per_wall_s\n")
for r in rows:
f.write(",".join(str(x) for x in r) + "\n")
# Bin by speed so the shape is visible without any curve-fitting assumption.
print(f"# {len(rows)} windows of {WIN}s; rate binned by speed (per WALL second):")
lo = min(r[1] for r in rows)
hi = max(r[1] for r in rows)
print(f"# speed swept {lo:.0f} -> {hi:.0f} units/wall-s")
nb = 6
for b in range(nb):
a = lo + (hi - lo) * b / nb
z = lo + (hi - lo) * (b + 1) / nb
sel = [r[2] for r in rows if a <= r[1] < z]
if sel:
print(f"# speed {a:7.0f}-{z:7.0f} n={len(sel):3d} rate {sum(sel)/len(sel):6.1f}")
print(f"# wrote {out_csv}; read HUD TIME off curve_{axis}_[ab] for the clock")
if __name__ == "__main__":
raise SystemExit(main())

94
tools/re-capture/rate_probe.py Executable file
View File

@@ -0,0 +1,94 @@
#!/usr/bin/env python3
"""Angular rate cap for one axis, at minimum and maximum speed.
Generalises `roll_axis.py`. Two things it does that the probes before it did not:
* **Pins the matrix rows.** `entities2` measures row 2 = forward against velocity,
but up-vs-right was previously taken from the D3D convention — and measuring
world-Y in flight showed the convention is *backwards* here (up = row 1, right =
row 0). Roll is immune to that (rotation of either non-forward row within their
shared plane is roll either way), but **pitch is not**, so a pitch number taken
without this pinning is naming an axis by assumption.
* **Brackets each phase with the HUD clock.** Rates are per GAME second and the
clock ratio is a property of the moment — 1.260, 1.311, 1.383 and 1.247/1.218
across five flights — so it cannot be assumed and must be read per phase.
roll driven by lx, measured as rotation of UP about forward
pitch driven by ly, measured as rotation of FORWARD toward up
Usage: rate_probe.py <roll|pitch> <out.csv> [dwell_s]
Then read HUD TIME off the `rate_<axis>_<phase>_[ab]` screenshots for the ratio.
"""
import json
import math
import os
import struct
import subprocess
import sys
import time
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import speed_law # noqa: E402
from axis_probe import alive, pad_state, pin_rows, rows_at, dot # noqa: E402
DRIVER = {"roll": {"lx": 32767}, "pitch": {"ly": 32767}}
def shot(n):
subprocess.run(["screenshot", f"/sylph-home/re/shots/{n}.png"], capture_output=True)
def main():
axis = sys.argv[1]
out_csv = sys.argv[2]
dwell = float(sys.argv[3]) if len(sys.argv) > 3 else 8.0
if axis not in DRIVER:
sys.exit(f"axis must be one of {list(DRIVER)}")
cfg = json.load(open("/tmp/nav-live.json"))
w, off, nm = speed_law.find_player()
if not w:
sys.exit("player entity not found")
print(f"# locked on {nm}, measuring {axis}")
f_i = cfg["fwd_row"]
if not alive(w, off, cfg):
sys.exit("craft is not moving — not in flight, or already dead")
u_i, w_i = pin_rows(w, off, cfg)
rows = []
for label, trig in (("slow", "lt"), ("fast", "rt")):
pad_state(**{trig: 255})
time.sleep(5.0) # settle: 2 s was shown wrong twice
shot(f"rate_{axis}_{label}_a")
prev = rows_at(w.fd, off, cfg)
pad_state(**{trig: 255}, **DRIVER[axis])
t0, swept, seq = time.time(), 0.0, []
while time.time() - t0 < dwell:
time.sleep(0.05)
cur = rows_at(w.fd, off, cfg)
if axis == "roll":
d = math.atan2(dot(cur[u_i], prev[w_i]), dot(cur[u_i], prev[u_i]))
else:
d = math.atan2(dot(cur[f_i], prev[u_i]), dot(cur[f_i], prev[f_i]))
d = math.degrees(d)
swept += abs(d)
seq.append((round(time.time() - t0, 3), round(d, 4)))
prev = cur
pad_state(**{trig: 255})
wall = seq[-1][0]
shot(f"rate_{axis}_{label}_b")
rows += [(label, *s) for s in seq]
print(f"# {label:<5} wall {wall:5.2f}s {axis} swept {swept:7.1f}deg "
f"rate {swept / wall:6.1f} deg/wall-s")
if not alive(w, off, cfg):
print("# CRAFT STOPPED MOVING — aborting rather than reporting zeros")
break
pad_state()
with open(out_csv, "w") as f:
f.write(f"phase,t,d{axis}_deg\n")
for r in rows:
f.write(",".join(str(x) for x in r) + "\n")
print(f"# wrote {out_csv} — read HUD TIME off rate_{axis}_*_[ab] for the clock")
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,63 @@
#!/usr/bin/env bash
# Surgical rebuild of xenia_canary inside the sylph box.
#
# The image has no cmake/ninja/clang of its own and the build/ cache cannot be
# re-configured (see project_sylph_canary_patch_toolchain memory), so a full
# `ninja` is impossible: several TUs need dev headers this image lacks. What DOES
# work is compiling only the objects that changed, refreshing their archive, and
# re-running the link command lifted out of the generated ninja file.
#
# Usage: rebuild.sh <ninja object path> [more objects...]
# e.g. rebuild.sh src/xenia/kernel/CMakeFiles/xenia-kernel.dir/Release/xboxkrnl/xboxkrnl_video.cc.o
# Objects are paths relative to build/. The archive each belongs to is derived
# from the .dir name (xenia-kernel.dir -> obj/Linux/Release/libxenia-kernel.a).
set -eu
PROJ="${PROJECT_DIR:-/home/fabi/RE - Project Sylpheed}"
BUILD="$PROJ/xenia-canary-native/build"
PFX=/sylph-home/re/toolchain
export PATH="$PFX/usr/lib/llvm-18/bin:/sylph-home/.local/bin:$PATH"
export LD_LIBRARY_PATH="$PFX/usr/lib/x86_64-linux-gnu:$PFX/usr/lib/llvm-18/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
[ $# -ge 1 ] || { echo "usage: rebuild.sh <object.o> [...]" >&2; exit 2; }
cd "$BUILD"
echo "[rebuild] compiling $# object(s)"
ninja -f build-Release.ninja "$@"
declare -A LIBS=()
for o in "$@"; do
d="${o#*CMakeFiles/}"; d="${d%%.dir/*}"
lib="obj/Linux/Release/lib$d.a"
[ -f "$lib" ] || { echo "[rebuild] no archive $lib for $o" >&2; exit 3; }
ar r "$lib" "$o"
LIBS["$lib"]=1
done
for lib in "${!LIBS[@]}"; do ranlib "$lib"; echo "[rebuild] refreshed $lib"; done
# Lift the link command out of the generated ninja rather than hard-coding it, so
# it stays correct if the object or library list ever changes.
python3 - <<'PY' > /tmp/xenia_link.sh
import re, shlex
NJ = "CMakeFiles/impl-Release.ninja"
txt = open(NJ).read()
m = re.search(r"^build bin/Linux/Release/xenia_canary: (\S+) (.*?)(?=\n\S|\n\n)",
txt, re.M | re.S)
assert m, "link build statement not found"
head = m.group(2)
# $in = the explicit inputs, up to the first | (implicit deps)
ins = head.split("\n")[0].split(" | ")[0].split(" || ")[0].strip()
body = m.group(0)
var = dict(re.findall(r"^ (\w+) = (.*)$", body, re.M))
cmd = (f'{shlex.quote("/sylph-home/re/toolchain/usr/lib/llvm-18/bin/clang++")} '
f'{var["FLAGS"]} {var["LINK_FLAGS"]} {ins} -o {var["TARGET_FILE"]} '
f'{var.get("LINK_PATH","")} {var["LINK_LIBRARIES"]}')
print(cmd)
PY
echo "[rebuild] linking bin/Linux/Release/xenia_canary"
# The image ships runtime sonames only (libgtk-3.so.0, not libgtk-3.so) and no
# libstdc++fs.a; linkshim/ supplies both as symlinks + an empty archive.
LIBRARY_PATH="$PFX/linkshim${LIBRARY_PATH:+:$LIBRARY_PATH}" bash /tmp/xenia_link.sh
ls -la bin/Linux/Release/xenia_canary
echo "[rebuild] done"

View File

@@ -0,0 +1,101 @@
#!/usr/bin/env python3
"""Roll rate measured ABOUT THE FORWARD AXIS, so pitch cannot leak into it.
The previous attempt watched a non-forward row of the rotation matrix and got
numbers within a few per cent of the pitch run — because pitch moves that row as
much as roll does. The fix is to measure the rotation *in the plane perpendicular
to forward*: express the new up-vector in the OLD (up, right) basis and take
`atan2(u_new·w_old, u_new·u_old)`. Any component along forward — which is what
pitch produces — is dropped by construction.
Each phase is bracketed by HUD screenshots so the mission clock converts wall
seconds to game seconds within this run (the ratio has measured 1.260, 1.311 and
1.383 in three flights, so it cannot be assumed).
Usage: roll_axis.py <out.csv> [dwell_s]
"""
import json, math, os, struct, subprocess, sys, time
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import speed_law
PAD_FILE = os.environ.get("XENIA_PAD_FILE", "/tmp/xenia_pad.txt")
def pad_state(**kw):
"""Write the WHOLE pad state at once.
The file pad is a snapshot, not a set of independent channels: each write
replaces everything, so holding a trigger *while* deflecting a stick has to be
one write. (`vgamepad`'s per-channel calls are what this replaces; that device
also leaked to the host, see the driver header.) Values are exact — no virtual
stick quantisation — which is what makes a single-axis roll hold trustworthy.
"""
parts = [f"{k}={v}" for k, v in kw.items() if v is not None]
tmp = PAD_FILE + ".tmp"
with open(tmp, "w") as f:
f.write(" ".join(parts))
os.replace(tmp, PAD_FILE)
def pad_neutral():
pad_state()
def shot(n):
subprocess.run(["screenshot", f"/sylph-home/re/shots/{n}.png"], capture_output=True)
def rows_at(fd, off, cfg):
base = off + cfg["rot_delta"]
out = []
for r in range(3):
v = struct.unpack(">3f", os.pread(fd, 12, base + r * cfg["rot_stride"]))
n = math.sqrt(sum(c * c for c in v)) or 1.0
out.append(tuple(c / n for c in v))
return out
def dot(a, b):
return sum(a[i] * b[i] for i in range(3))
def main():
out_csv = sys.argv[1]
dwell = float(sys.argv[2]) if len(sys.argv) > 2 else 8.0
cfg = json.load(open("/tmp/nav-live.json"))
w, off, nm = speed_law.find_player()
if not w:
sys.exit("player entity not found after retries")
print(f"# locked on {nm}")
f_i = cfg.get("fwd_row", 0)
u_i, w_i = [r for r in (0, 1, 2) if r != f_i]
rows = []
for label, (trig, tv) in (("slow", ("LT", 1.0)), ("fast", ("RT", 1.0))):
# settle at the target speed with the stick CENTRED (5 s: 2 s was shown
# wrong twice this session)
pad_state(**{trig.lower(): 255})
time.sleep(5.0)
shot(f"rollax_{label}_a")
# full left-stick X, everything else exactly zero, trigger still held
pad_state(**{trig.lower(): 255, "lx": 32767})
t0, prev, swept = time.time(), rows_at(w.fd, off, cfg), 0.0
seq = []
while time.time() - t0 < dwell:
time.sleep(0.05)
cur = rows_at(w.fd, off, cfg)
# roll = rotation of `up` within the OLD (up, right) plane
d = math.degrees(math.atan2(dot(cur[u_i], prev[w_i]), dot(cur[u_i], prev[u_i])))
swept += abs(d)
seq.append((round(time.time() - t0, 3), round(d, 4)))
prev = cur
pad_state(**{trig.lower(): 255})
wall = seq[-1][0]
shot(f"rollax_{label}_b")
rows += [(label, *s) for s in seq]
print(f"# {label:<5} wall {wall:5.2f}s roll swept {swept:7.1f}deg "
f"rate {swept / wall:6.1f} deg/wall-s")
pad_neutral()
with open(out_csv, "w") as f:
f.write("phase,t,droll_deg\n")
for r in rows:
f.write(",".join(str(x) for x in r) + "\n")
print("# read HUD TIME off rollax_slow_a/b and rollax_fast_a/b for this run's clock ratio")
if __name__ == "__main__":
main()

88
tools/re-capture/screen_id.py Executable file
View File

@@ -0,0 +1,88 @@
#!/usr/bin/env python3
"""Identify which game screen a screenshot shows, without fixed pixel positions.
The nav scripts used to test named pixels ("648,221 is white"). That works only
while the game image sits at a known place on the root window, and it does not:
xenia's GTK window has a menu bar, so on some displays the image is ~25 px lower
and every constant reads the wrong row. The failure is silent and expensive — a
run sat on a plainly visible MAIN MENU for 300 s reporting "no main menu", and a
later one missed the title screen so the attract movie looped for ten minutes.
So identify screens by WHOLE-IMAGE statistics instead, which no vertical shift
(or scale, or letterbox) can move:
green fraction of pixels that are the game's green UI text/HUD colour
white fraction of near-white pixels
mean per-channel mean
Measured on known-good captures of each screen (1280x720, no menu bar):
title green 0.11% white 7.4% mean (62, 75, 84) blue-ish, bright
main menu green 0.04% white 3.5% mean (25, 38, 70) dark, strongly blue
in flight green 1.2% white 3.4% mean (54, 39, 37) green HUD everywhere
movie green 0% white varies no green at all
Usage: screen_id.py <png> [--json]
Prints one of: title | menu | flight | other, plus the features.
"""
import json
import subprocess
import sys
# Downscaled first: the features are area fractions, so 320x180 gives the same
# answer for a fraction of the work (a full-size pure-python pass costs seconds,
# and these oracles are polled once a second).
W, H = 320, 180
def features(path):
raw = subprocess.run(
["convert", path, "-alpha", "off", "-resize", f"{W}x{H}!", "-depth", "8",
"rgb:-"], capture_output=True).stdout
n = len(raw) // 3
if not n:
return None
green = white = 0
sr = sg = sb = 0
for i in range(0, n * 3, 3):
r, g, b = raw[i], raw[i + 1], raw[i + 2]
sr += r; sg += g; sb += b
if g > 130 and g - r > 45 and g - b > 45:
green += 1
if r > 230 and g > 230 and b > 230:
white += 1
return {"green": green / n, "white": white / n,
"r": sr / n, "g": sg / n, "b": sb / n}
def classify(f):
if f is None:
return "none"
# Flight first: the HUD paints far more green than any menu.
if f["green"] > 0.004:
return "flight"
# The main menu is dark and strongly blue, with the five white labels.
if f["b"] - f["r"] > 30 and f["r"] < 45 and 0.015 < f["white"] < 0.075:
return "menu"
# The title is brighter, still blue-ish, and carries the green PRESS A text.
if f["green"] > 0.0004 and f["b"] > 60 and f["white"] > 0.03:
return "title"
return "other"
def main():
path = sys.argv[1]
f = features(path)
name = classify(f)
if "--json" in sys.argv:
print(json.dumps({"screen": name, **(f or {})}))
elif f:
print(f"{name} green={f['green']:.4f} white={f['white']:.4f} "
f"mean=({f['r']:.1f},{f['g']:.1f},{f['b']:.1f})")
else:
print(name)
return 0
if __name__ == "__main__":
raise SystemExit(main())

94
tools/xach_dump.py Executable file
View File

@@ -0,0 +1,94 @@
#!/usr/bin/env python3
"""Dump the title's Xbox 360 achievement table (XACH) out of the decrypted `.pe`.
The XEX embeds an SPA/XDBF resource holding the achievement definitions and one
string table per language. `GamePart_Debriefing` awards these (it walks the
`ACHIEVEMENTS_REQUIREMENTS` config list and sets bit *i* for entry *i*), and
`GamePart_ChallengeMission` gates each challenge mission on a bit of the same
space — so this table names the bits.
Layout, derived here and self-checked (see below):
XDBF section header : magic[4] "XACH", version u32, size u32, count u16
record, 36 bytes : id u16, name_id u16, unlocked_desc_id u16,
locked_desc_id u16, image_id u32, gamerscore u16,
pad u16, flags u32, then 16 bytes of zeroes
XSTR section header : magic[4] "XSTR", version u32, size u32, count u16
string entry : id u16, len u16, `len` bytes of ASCII
Self-check: the 24 records' gamerscore sums to **1000**, the retail total — a
wrong stride or field offset does not add up to a round 1000.
Usage: xach_dump.py <path-to.pe> [--lang-index N]
"""
import struct
import sys
def find_all(buf, needle):
out, i = [], 0
while True:
i = buf.find(needle, i)
if i < 0:
return out
out.append(i)
i += 1
def parse_xstr(d, off):
count = struct.unpack_from(">H", d, off + 12)[0]
o, table = off + 14, {}
for _ in range(count):
sid, ln = struct.unpack_from(">HH", d, o)
table[sid] = d[o + 4 : o + 4 + ln].decode("ascii", "replace")
o += 4 + ln
return table
def main():
if len(sys.argv) < 2:
print(__doc__)
return 1
d = open(sys.argv[1], "rb").read()
# The real XACH section is the one whose header count is small (the other
# "XACH" hit is the XDBF entry table, which merely *names* it).
xach = None
for off in find_all(d, b"XACH"):
ver, size, count = struct.unpack_from(">IIH", d, off + 4)
if ver == 1 and 0 < count < 256 and size >= count * 36:
xach = (off, count)
break
if xach is None:
print("no XACH section found", file=sys.stderr)
return 2
off, count = xach
# One XSTR per language. Default to English: the only table whose achievement
# strings are pure 7-bit ASCII (every other localisation carries accents or
# multi-byte text, which our ASCII decode turns into replacement chars).
strs = [parse_xstr(d, o) for o in find_all(d, b"XSTR") if len(d) - o > 16]
ids = [struct.unpack_from(">HHHH", d, off + 14 + i * 36)[1:] for i in range(count)]
lang = int(sys.argv[sys.argv.index("--lang-index") + 1]) if "--lang-index" in sys.argv else None
if lang is None:
def ascii_score(t):
txt = "".join(t.get(s, "") for rec in ids for s in rec)
return (txt.count("<EFBFBD>") == 0 and len(txt) > 0, len(txt))
lang = max(range(len(strs)), key=lambda i: ascii_score(strs[i]))
S = strs[lang]
print(f"XACH @0x{off:X} {count} achievements (string table #{lang} of {len(strs)})\n")
total = 0
for i in range(count):
b = off + 14 + i * 36
aid, nid, did, lid = struct.unpack_from(">HHHH", d, b)
gs = struct.unpack_from(">H", d, b + 12)[0]
total += gs
print(f"id {aid:2d} | bit {aid - 1:2d} | {gs:3d}G | {S.get(nid, '?')}")
print(f" unlocked: {S.get(did, '?')}")
print(f" locked : {S.get(lid, '?')}")
print(f"\ntotal gamerscore = {total}" + (" [OK: retail total]" if total == 1000 else " [!! expected 1000]"))
return 0
if __name__ == "__main__":
raise SystemExit(main())