From 786ff9f8c550eaee0d2a2233cb993d9eeed212ab Mon Sep 17 00:00:00 2001 From: Teakowa Date: Wed, 4 Nov 2026 03:15:03 +0800 Subject: [PATCH] feat(sim): model Set Slow Motion as no-effect across the P9 range P9 measured factors 10, 200, 0 and -50 over three client runs (2.25.0.1). Total Time Elapsed stays 16 ms per tick and Wait rounds up to whole ticks, as at 100%. With P8 this covers -50 to 200, so the gate becomes that range instead of a list of points; a factor outside it stays not_modeled. Records the P9 capture and adds a P9 fixture test. Updates architecture, findings and trace-format docs. Fixes #143 --- crates/athena-sim/src/clock.rs | 23 ++++++++++------ crates/athena-sim/tests/fixtures.rs | 6 ++++ crates/athena-sim/tests/match_clock.rs | 29 ++++++++++++-------- docs/architecture.md | 2 +- docs/findings.md | 2 +- docs/trace-format.md | 2 +- probes/p9-slow-motion-extremes/capture-2.txt | 29 ++++++++++++++++++++ probes/p9-slow-motion-extremes/capture-3.txt | 29 ++++++++++++++++++++ probes/p9-slow-motion-extremes/capture.json | 20 +++++++++----- probes/p9-slow-motion-extremes/capture.txt | 29 ++++++++++++++++++++ 10 files changed, 140 insertions(+), 31 deletions(-) create mode 100644 probes/p9-slow-motion-extremes/capture-2.txt create mode 100644 probes/p9-slow-motion-extremes/capture-3.txt create mode 100644 probes/p9-slow-motion-extremes/capture.txt diff --git a/crates/athena-sim/src/clock.rs b/crates/athena-sim/src/clock.rs index 48169d6..2dd4a75 100644 --- a/crates/athena-sim/src/clock.rs +++ b/crates/athena-sim/src/clock.rs @@ -3,15 +3,17 @@ //! read it (athena#120). An unpaused clock advances each tick by the //! tick length: a `Pause Match Time` stops `Total Time Elapsed` and //! the match countdown alike. `Set Slow Motion` is recorded only at -//! the factors P8 measured (`MEASURED_SLOW_MOTION`): there `Total Time -//! Elapsed` and a `Wait(.., Ignore Condition)` read as at 100%. Any -//! other factor stops `not_modeled` at the next tick or `Wait`. +//! the range P8 and P9 measured (`MEASURED_SLOW_MOTION`): there `Total +//! Time Elapsed` and a `Wait(.., Ignore Condition)` read as at 100%. +//! Any other factor stops `not_modeled` at the next tick or `Wait`. //! `Match Time` before any `Set Match Time` is //! unanswerable — the lobby's phase limit is no input the scenario //! format carries — and a countdown reaching zero is the phase //! boundary the simulator does not run. The countdown and pause //! are modeled, not measured (`docs/findings.md`). +use std::ops::RangeInclusive; + use workshop_rs::Value; use crate::eval::{eval, number}; @@ -20,9 +22,12 @@ use crate::step::World; use crate::unsupported; use crate::{Change, Finite, Stop, Val}; -/// The `Set Slow Motion` percents P8 measured: the 100 control and the -/// 50 and 90 factors (`probes/p8-slow-motion-wait/capture.json`). -const MEASURED_SLOW_MOTION: [f64; 3] = [100.0, 50.0, 90.0]; +/// The `Set Slow Motion` percents P8 and P9 measured: 100, 50 and 90 +/// (`probes/p8-slow-motion-wait`), then 10, 200, 0 and -50 +/// (`probes/p9-slow-motion-extremes`). The script timeline read as at +/// 100% at every one, so the range between the extremes is taken as +/// measured, clamped inputs included (#143). +const MEASURED_SLOW_MOTION: RangeInclusive = -50.0..=200.0; /// The match clock: how far the run's game time has come, whether /// `Pause Match Time` holds it, the countdown `Match Time` reports @@ -37,7 +42,7 @@ pub(crate) struct Clock { /// scenario format carries, so the read has no start value. pub match_time: Option, /// The last `Set Slow Motion` percent; `100` is the client's - /// normal rate. Recorded only; it scales nothing (P8). + /// normal rate. Recorded only; it scales nothing (P8, P9). pub slow_motion: Finite, /// The game-time seconds elapsed; `Total Time Elapsed` reads the /// start offset plus this. @@ -56,8 +61,8 @@ impl Clock { } } - /// Stop unless the last `Set Slow Motion` factor is one P8 measured; - /// the simulator's 16 ms tick mapping holds only for those. + /// Stop unless the last `Set Slow Motion` factor is inside the range P8 and P9 + /// measured; the simulator's 16 ms tick mapping holds only there. pub(crate) fn require_measured_slow_motion(&self) -> Result<(), Stop> { let percent = self.slow_motion.get(); if MEASURED_SLOW_MOTION.contains(&percent) { diff --git a/crates/athena-sim/tests/fixtures.rs b/crates/athena-sim/tests/fixtures.rs index e52ba1f..0b4c68a 100644 --- a/crates/athena-sim/tests/fixtures.rs +++ b/crates/athena-sim/tests/fixtures.rs @@ -252,6 +252,12 @@ fn p8_slow_motion_wait() { check_probe_for("p8-slow-motion-wait", 12.0); } +#[test] +fn p9_slow_motion_extremes() { + // Four factors of about 1.2 s each, after a 3 s start. + check_probe_for("p9-slow-motion-extremes", 12.0); +} + #[test] fn p7_hero_settings() { check_probe("p7-hero-settings"); diff --git a/crates/athena-sim/tests/match_clock.rs b/crates/athena-sim/tests/match_clock.rs index db07cac..95a3089 100644 --- a/crates/athena-sim/tests/match_clock.rs +++ b/crates/athena-sim/tests/match_clock.rs @@ -1,9 +1,9 @@ //! The match clock (athena#120): `Pause`/`Unpause Match Time`, //! `Set Match Time` and `Set Slow Motion` write it, `Match Time` and //! `Total Time Elapsed` read it, and every write records a -//! `matchClock` change. `Set Slow Motion` is recorded only at the -//! factors P8 measured (100, 50, 90), where `Total Time Elapsed` and -//! `Wait` rounding read as at 100%; any other factor is `not_modeled`. +//! `matchClock` change. `Set Slow Motion` is recorded only inside +//! the range P8 and P9 measured (-50 to 200), where `Total Time Elapsed` +//! and `Wait` rounding read as at 100%; any other factor is `not_modeled`. use athena_sim::{Change, Outcome, Val, run_scenario, run_text}; @@ -200,12 +200,16 @@ fn set_slow_motion_records_the_factor_and_leaves_game_time_unscaled() { ); } -/// Factors P8 did not measure. -const UNMEASURED: [u32; 2] = [10, 200]; +/// Factors just outside the measured range, and far outside it. +const UNMEASURED: [i32; 3] = [201, -51, 1000]; + +/// Factors P8 and P9 measured: the 100 control, P8's 50 and 90, and +/// P9's 10 and the clamped 200, 0 and -50. +const MEASURED: [i32; 7] = [100, 50, 90, 10, 200, 0, -50]; /// Assert the run stopped `not_modeled` on an unmeasured slow-motion /// factor, naming it. -fn assert_unmeasured_stop(trace: &athena_sim::Trace, factor: u32) { +fn assert_unmeasured_stop(trace: &athena_sim::Trace, factor: i32) { let Outcome::NotModeled(not_modeled) = &trace.outcome else { panic!("{factor}%: expected NotModeled, got {:?}", trace.outcome); }; @@ -226,8 +230,9 @@ fn assert_stopped_at_tick(trace: &athena_sim::Trace, tick: u64) { #[test] fn waits_under_a_measured_factor_round_up_to_whole_ticks() { - // P8 (measured): at 50% and 90% a `Wait(0.1)` still takes 112 ms. - for factor in [50, 90] { + // P8 and P9 (measured): at every measured factor a `Wait(0.1)` + // still takes 112 ms. + for factor in MEASURED { let trace = run( &format!( " Set Slow Motion({factor}); @@ -273,7 +278,7 @@ fn a_suspending_wait_until_under_an_unmeasured_factor_is_not_modeled() { #[test] fn a_wait_until_timeout_under_a_measured_factor_keeps_the_16_ms_ticks() { - for factor in [50, 90] { + for factor in MEASURED { let trace = run( &format!( " Set Slow Motion({factor}); @@ -303,16 +308,16 @@ fn an_unmeasured_factor_stops_the_clock_even_with_no_wait() { fn a_paused_clock_under_an_unmeasured_factor_is_not_modeled() { let trace = run( " Pause Match Time; - Set Slow Motion(10);", + Set Slow Motion(201);", 1.0, ); - assert_unmeasured_stop(&trace, 10); + assert_unmeasured_stop(&trace, 201); } /// A rule that sets `factor` in the tick after `waiter` suspends at /// 100%: the resume path must stop at the wake, not run the stale /// schedule. -fn slow_rule(factor: u32) -> String { +fn slow_rule(factor: i32) -> String { format!( r#" rule ("slow") {{ diff --git a/docs/architecture.md b/docs/architecture.md index 7f17c1d..5fb17fc 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -123,6 +123,6 @@ The simulator makes these choices only to stay deterministic; each needs a probe - **Empty world, modeled not measured** — the geometry reads (athena#77, the `athena#75` decision); no capture covers them and the goal excludes real geometry: - The run has no map geometry, and the trace marks it as `geometry: "emptyWorld"` so the viewer can warn that geometry-dependent results are not real. A raycast never hits and answers by the catalog's documented no-hit return: `Ray Cast Hit Position` the end position, `Ray Cast Hit Player` `Null`, `Ray Cast Hit Normal` the direction from the end back to the start — a zero-length cast is `not_modeled`. `Is In Line of Sight` is true; `Nearest Walkable Position` yields its input; `Is On Ground` is true and `Is In Air`/`Is On Wall`/`Is In Spawn Room` are false — an empty world has none of what they ask about. `Is In Spawn Room` is also a version-2 `state` read: a player that declares `state` answers `state.isInSpawnRoom` instead of the empty world's `false` (athena#89). - `Teleport` writes the player's scenario `position` to the evaluated destination (`Null` is the origin, the catalog's `zeroAsNull` coercion) and records a `world` change with `field` `position`; `Position Of` reads the new value. `Apply Impulse` changes velocity, which needs physics, and stays `not_modeled`, as does `Is Standing`. `Is Moving` and `Eye Position` are version-2 `state` reads instead (athena#89): a player that declares `state` answers `state.isMoving`/`state.eyePosition`, and a player without `state` reports a `missing_input`. -- **Measured** — `Set Slow Motion` (P8): at factors 50 and 90 `Total Time Elapsed` still advances 16 ms per tick, a `Wait(.., Ignore Condition)` rounds up to whole 16 ms ticks and the Inspector timestamps read as at 100%. Measured at 50% and 90% with the 100% control; the simulator records the write as a `matchClock` change and runs the clock and `Wait`s on unscaled 16 ms ticks at those factors only. Any other factor ends the run `not_modeled` at the next tick, `Wait` or `Wait Until` that would use it. Not measured: the wall-clock ratio and the cause of the unchanged readings (the game was seen to slow, by eye, with no external timer). +- **Measured** — `Set Slow Motion` (P8, P9): at factors 10, 50, 90, 100, 200, 0 and -50 `Total Time Elapsed` still advances 16 ms per tick, a `Wait(.., Ignore Condition)` rounds up to whole 16 ms ticks (16, 100 and 1000 ms take 16, 112 and 1008 ms in P9) and the Inspector timestamps read as at 100%. The factors 200, 0 and -50 lie outside the 0 to 100 percent the wiki documents and read as at 100% too. The simulator records the write as a `matchClock` change and runs the clock and `Wait`s on unscaled 16 ms ticks for every factor from -50 to 200, the measured range. A factor outside that range ends the run `not_modeled` at the next tick, `Wait` or `Wait Until` that would use it. Not measured: factors outside the range, the wall-clock ratio and the cause of the unchanged readings (the game was seen to slow, by eye, with no external timer). - **Modeled, not measured** — the rest of the match clock (athena#120); no capture covers it: - `Pause`/`Unpause Match Time` and `Set Match Time` write the run's clock: an unpaused clock advances each tick by 16 ms, on `Total Time Elapsed` and the `Match Time` countdown alike; `Pause` holds it. `Match Time` has no start value — the lobby's phase limit is not an input the scenario format carries — so a read before a `Set Match Time` is `not_modeled`, and a countdown reaching zero ends the run, the phase boundary it crosses being unmodeled. Every write records a `matchClock` change. diff --git a/docs/findings.md b/docs/findings.md index 6dd98d8..288a7bb 100644 --- a/docs/findings.md +++ b/docs/findings.md @@ -59,4 +59,4 @@ Simulator choices no capture backs yet — the detail lives in [architecture.md] - State cells and stored modifiers (athena#116): the version-2 `state` map gains a write side — a state-write action writes a cell over the declared map, a write defines a key the scenario never supplied, a read after a write returns the written value, and a read before any write of an undeclared key stays `missing_input`; every write records a `state` change (`player`, `key`, `old`, `new`). The writes, each by its catalog sentence: `setUltCharge` writes `ultCharge` ("sets the ultimate charge for one or more players as a percentage of maximum charge"), the `Number|Boolean` parameter reading `False`/`True` as `0`/`1`; `setHealth` ("sets the health of a player or players … This action only has an effect on living players. For dead players, use the Resurrect action instead") writes `health`, gated by the `isDead` read — a dead target is skipped, an unanswerable `isDead` is a `missing_input`; `setMaxHealth` ("as a percentage of their max health … will ensure that a player's current health will not exceed the new max health") writes `maxHealth` off the current-max read and clamps `health`; `kill`, `respawn` and `resurrect` write `isDead` — `respawn` also `health` to the `maxHealth` read ("with full health"), its spawn relocation not simulated — and `setFacing` `To World` writes the scenario `facing` to the direction's angles (`To Player` stays `not_modeled`: the catalog does not state the composition). The stored modifiers record a `modifier` change (action, player, evaluated non-target arguments) and land on the player's `modifiers` — nothing reads them back and no readable value moves: `setUltEnabled` → `ultEnabled`, `setAbility1Enabled`/`setAbility2Enabled` → `ability1Enabled`/`ability2Enabled`, `setDamageDealt`/`setDamageReceived`/`setHealingDealt`/`setHealingReceived` → `damageDealt`/`damageReceived`/`healingDealt`/`healingReceived` (scales on damage the model does not run), `setMoveSpeed`/`setGravity`/`setKnockbackReceived` → `moveSpeed`/`gravity`/`knockbackReceived` (movement modifiers no modeled read consumes — `isMoving`/`getSpeed` are scenario state, never derived from them), `setAllowedHeroes` → `allowedHeroes` (no hero read consults the list), `startForcingHero`/`stopForcingCurrentHero` → `forcedHero` written/removed, and `startForcingSpawn` → `forcedSpawnRoom` on each player of the team. The rest of PR #104's list — and the sibling ids in the same families (`stopDamageOverTime`, `stopAllDamageOverTime`, `stopAllHealingOverTime`, `stopFacing`, the player-variable `Chase *`) — stays `not_modeled`, each needing a model the run does not carry: the health-pool ids and composition (`addHealthPool`, `removeHealthPool`), the damage/heal-over-time and damage-modification ids (`startDamageOverTime`, `stopDamageOverTime`, `stopAllDamageOverTime`, `startHealingOverTime`, `stopHealingOverTime`, `stopAllHealingOverTime`, `startDamageModification`), the roster (`createDummyBot`, `destroyAllDummies`, `removeFromGame`), the entity ids (`createIcon`, `destroyAllIcons`) and text ids (`createInWorldText`, `createProgressBarInWorldText`, `destroyInWorldText`, `destroyProgressBarInWorldText`, `destroyAllInWorldText`), button and throttle forcing (`forceButtonPress`, `startForcingButton`, `stopForcingButton`, `startForcingThrottle`, `stopForcingThrottle`, `startThrottleInDirection`, `stopThrottleInDirection`), `cancelPrimaryAction` (the stun's duration is unstated), the dynamics of position, velocity and facing (`applyImpulse`, `startAcceleration`, `stopAcceleration`, `startFacing`, `stopFacing`, `startForcingPosition`, `stopForcingPosition`, `attachTo`, `detach`), the match clock and match state (`pauseMatchTime`, `unpauseMatchTime`, `setMatchTime`, `setSlowMotion`, `declareTeamVictory`, `restartMatch`), and the `Chase *` schedules (`chasePlayerVariableAtRate`, `chasePlayerVariableOverTime`, `chaseGlobalVariableAtRate`, `chaseGlobalVariableOverTime`). No capture covers any of these; modeled, not measured. - Entity-creating display actions (athena#118): every display-entity create is recorded — `createHudText`, `progressBarHud`, `createInWorldText` and `createProgressBarInWorldText` on `hud`, `createEffect`, `createBeamEffect`, `createIcon`, `createProjectileEffect` and `playEffect` on `effects` — each with the catalog sentence's arguments and the evaluated `Visible To` recipients. Two id spaces follow the catalog's sentences: the four text creates share the `TextId` space `lastTextId` and the text destroys read (`getNumberOfTextIds` counts the live elements of those kinds), and the durable entity creates share the `EntityId` space `lastCreatedEntity` reads — a `playEffect` hands out none ("the last created entity" needs a create, not a play) and dies on its tick. `entityExists` answers the `Player|EntityId` union: a scenario player exists for the whole run; a number answers from the entity table — live entity of that id or not, so a text id or unknown id is a plain `false`, `Null` is `false`, and anything else is `not_modeled`. The destroy scoping is the catalog's: `destroyEffect`/`destroyAllEffects` name both `createEffect` and `createBeamEffect`, `destroyIcon`/`destroyAllIcons` the icons, and the two in-world destroy pairs their own kinds; a `projectileEffect` is durable but no destroy action names it, and `getNumberOfEntityIds` counts only `effect`/`beam`/`icon` — the kinds the catalog's sentence lists. The catalog defines no `Set ...`/`Update ...` action taking a text or entity id, so nothing mutates an element after creation. No capture covers any of this; modeled, not measured. - Player events (athena#47): scenario `events` and the modeled state changes both drive `Event::Player` rules — a `Damage` raises `Player Took Damage` on the target and `Player Dealt Damage` on a player dealer, a hit that empties the `health` pool adds `Player Died` on the target and `Player Dealt Final Blow` on a player dealer, and a `Heal` raises `Player Received Healing`/`Player Dealt Healing`, delivered in occurrence order; a `Null` dealer raises no dealt-side event and binds `Attacker` to `null`. A rule's `team`/player filters apply to the event's player as for an injected event, and a busy instance queues the event until its run is idle. A raised event binds `Attacker`, `Victim` and `Event Damage`/`Event Healing` to its cause; an event value the cause does not bind (`Event Was Critical Hit`, `Event Was Environment`, `Event Was Health Pack`) is `not_modeled` — no scenario `values` path exists for a raised event, so the read is never a `missing_input`. One liveness read gates `Damage`, `Heal` and `Resurrect`: the `isDead` read — a state cell or the declared `state.isDead` — that `Is Alive`/`Is Dead`/`Set Player Health` share, with the health pool deciding only where `isDead` cannot answer; a dead target's pools do not move and no events raise. A killing `Damage` writes the `isDead` cell; the revive's health-pool refill follows the catalog's split — `Respawn` refills unconditionally ("with full health, even if they were already alive"), `Resurrect` only when it actually revives a dead target, a living target keeping its partial health — so a revived player can die again. One tick's raised events are bounded at 1024 against a triggering rule's own `Damage`/`Heal` fan-out. No capture covers any of it; modeled, not measured. -- The match clock (athena#120): `Pause`/`Unpause Match Time`, `Set Match Time` and `Set Slow Motion` write a clock the run carries — a paused flag, a `Match Time` countdown and the last slow-motion percent. An unpaused clock advances each tick by 16 ms, on `Total Time Elapsed` and the countdown alike; a paused clock stands. `Set Slow Motion` is recorded only (P8): `Total Time Elapsed` stays 16 ms per tick and a `Wait(.., Ignore Condition)` rounds up to whole 16 ms ticks under 50% and 90% as at 100% (measured at 50% and 90% with the 100% control). Any other factor is `not_modeled`. The wall-clock ratio and the cause are not measured (the game was seen to slow by eye, with no external timer). The pause and the countdown are modeled, not measured. `Total Time Elapsed` is the 15 ms start offset plus the elapsed game time; `Match Time` reads the countdown — a read before any `Set Match Time` is `not_modeled`, the lobby's phase limit being no input the scenario format carries, and a countdown reaching zero stops the run, the phase boundary it crosses being unmodeled. Every write records a `matchClock` change (`action`, `field` — `paused`, `matchTime` or `slowMotion` — `old`, `new`). A clock argument that evaluates non-finite is `not_modeled` too. Status expiries compare `until` against the clock-driven `now`, so `Pause Match Time` freezes a `Has Status` expiry — the coupling is modeled, not measured; `Set Slow Motion` does not move it at the measured factors, since no clock a rule can read moves. The `Is Match In Overtime` read stays `not_modeled`. +- The match clock (athena#120): `Pause`/`Unpause Match Time`, `Set Match Time` and `Set Slow Motion` write a clock the run carries — a paused flag, a `Match Time` countdown and the last slow-motion percent. An unpaused clock advances each tick by 16 ms, on `Total Time Elapsed` and the countdown alike; a paused clock stands. `Set Slow Motion` is recorded only (P8, P9): `Total Time Elapsed` stays 16 ms per tick and a `Wait(.., Ignore Condition)` rounds up to whole 16 ms ticks under every measured factor as at 100% (measured at 10, 50, 90, 200, 0 and -50 with the 100% control, so the measured range is -50 to 200, clamped inputs included). A factor outside that range is `not_modeled`. The wall-clock ratio and the cause are not measured (the game was seen to slow by eye, with no external timer). The pause and the countdown are modeled, not measured. `Total Time Elapsed` is the 15 ms start offset plus the elapsed game time; `Match Time` reads the countdown — a read before any `Set Match Time` is `not_modeled`, the lobby's phase limit being no input the scenario format carries, and a countdown reaching zero stops the run, the phase boundary it crosses being unmodeled. Every write records a `matchClock` change (`action`, `field` — `paused`, `matchTime` or `slowMotion` — `old`, `new`). A clock argument that evaluates non-finite is `not_modeled` too. Status expiries compare `until` against the clock-driven `now`, so `Pause Match Time` freezes a `Has Status` expiry — the coupling is modeled, not measured; `Set Slow Motion` does not move it in the measured range, since no clock a rule can read moves. The `Is Match In Overtime` read stays `not_modeled`. diff --git a/docs/trace-format.md b/docs/trace-format.md index eb7c0dd..4396b39 100644 --- a/docs/trace-format.md +++ b/docs/trace-format.md @@ -13,7 +13,7 @@ Each entry: | Field | Meaning | | --- | --- | | `tick` | The tick. | -| `time` | The tick's `Total Time Elapsed` in milliseconds: 15 + the run's elapsed game time (tick·16 at the normal rate). The match clock drives it — `Pause Match Time` holds it, so a paused clock repeats values across ticks; `Set Slow Motion` does not change it at the measured factors 50 and 90; any other factor ends the run `not_modeled`. | +| `time` | The tick's `Total Time Elapsed` in milliseconds: 15 + the run's elapsed game time (tick·16 at the normal rate). The match clock drives it — `Pause Match Time` holds it, so a paused clock repeats values across ticks; `Set Slow Motion` does not change it for a factor from -50 to 200, the range P8 and P9 measured; any other factor ends the run `not_modeled`. | | `rules` | The rule executions that started, resumed or ended on the tick, in execution order. See below. | | `changes` | The state changes the executions made, in execution order across rules. See below. | diff --git a/probes/p9-slow-motion-extremes/capture-2.txt b/probes/p9-slow-motion-extremes/capture-2.txt new file mode 100644 index 0000000..9db248c --- /dev/null +++ b/probes/p9-slow-motion-extremes/capture-2.txt @@ -0,0 +1,29 @@ +[00:00:03] P9m f=10 i=0 t_ms=16.00 +[00:00:03] P9m f=10 i=1 t_ms=32.00 +[00:00:03] P9m f=10 i=2 t_ms=48.00 +[00:00:03] P9m f=10 i=3 t_ms=64.00 +[00:00:04] P9w f=10 wait_ms=16 elapsed_ms=16.00 +[00:00:04] P9w f=10 wait_ms=100 elapsed_ms=112.00 +[00:00:05] P9w f=10 wait_ms=1000 elapsed_ms=1008.00 +[00:00:05] P9m f=200 i=0 t_ms=16.00 +[00:00:05] P9m f=200 i=1 t_ms=32.00 +[00:00:05] P9m f=200 i=2 t_ms=48.00 +[00:00:05] P9m f=200 i=3 t_ms=64.00 +[00:00:06] P9w f=200 wait_ms=16 elapsed_ms=16.00 +[00:00:06] P9w f=200 wait_ms=100 elapsed_ms=112.00 +[00:00:07] P9w f=200 wait_ms=1000 elapsed_ms=1008.00 +[00:00:07] P9m f=0 i=0 t_ms=16.00 +[00:00:07] P9m f=0 i=1 t_ms=32.00 +[00:00:07] P9m f=0 i=2 t_ms=48.00 +[00:00:07] P9m f=0 i=3 t_ms=64.00 +[00:00:08] P9w f=0 wait_ms=16 elapsed_ms=16.00 +[00:00:08] P9w f=0 wait_ms=100 elapsed_ms=112.00 +[00:00:09] P9w f=0 wait_ms=1000 elapsed_ms=1008.00 +[00:00:10] P9m f=-50 i=0 t_ms=16.00 +[00:00:10] P9m f=-50 i=1 t_ms=32.00 +[00:00:10] P9m f=-50 i=2 t_ms=48.00 +[00:00:10] P9m f=-50 i=3 t_ms=64.00 +[00:00:10] P9w f=-50 wait_ms=16 elapsed_ms=16.00 +[00:00:10] P9w f=-50 wait_ms=100 elapsed_ms=112.00 +[00:00:11] P9w f=-50 wait_ms=1000 elapsed_ms=1008.00 +[00:00:11] P9 done diff --git a/probes/p9-slow-motion-extremes/capture-3.txt b/probes/p9-slow-motion-extremes/capture-3.txt new file mode 100644 index 0000000..9db248c --- /dev/null +++ b/probes/p9-slow-motion-extremes/capture-3.txt @@ -0,0 +1,29 @@ +[00:00:03] P9m f=10 i=0 t_ms=16.00 +[00:00:03] P9m f=10 i=1 t_ms=32.00 +[00:00:03] P9m f=10 i=2 t_ms=48.00 +[00:00:03] P9m f=10 i=3 t_ms=64.00 +[00:00:04] P9w f=10 wait_ms=16 elapsed_ms=16.00 +[00:00:04] P9w f=10 wait_ms=100 elapsed_ms=112.00 +[00:00:05] P9w f=10 wait_ms=1000 elapsed_ms=1008.00 +[00:00:05] P9m f=200 i=0 t_ms=16.00 +[00:00:05] P9m f=200 i=1 t_ms=32.00 +[00:00:05] P9m f=200 i=2 t_ms=48.00 +[00:00:05] P9m f=200 i=3 t_ms=64.00 +[00:00:06] P9w f=200 wait_ms=16 elapsed_ms=16.00 +[00:00:06] P9w f=200 wait_ms=100 elapsed_ms=112.00 +[00:00:07] P9w f=200 wait_ms=1000 elapsed_ms=1008.00 +[00:00:07] P9m f=0 i=0 t_ms=16.00 +[00:00:07] P9m f=0 i=1 t_ms=32.00 +[00:00:07] P9m f=0 i=2 t_ms=48.00 +[00:00:07] P9m f=0 i=3 t_ms=64.00 +[00:00:08] P9w f=0 wait_ms=16 elapsed_ms=16.00 +[00:00:08] P9w f=0 wait_ms=100 elapsed_ms=112.00 +[00:00:09] P9w f=0 wait_ms=1000 elapsed_ms=1008.00 +[00:00:10] P9m f=-50 i=0 t_ms=16.00 +[00:00:10] P9m f=-50 i=1 t_ms=32.00 +[00:00:10] P9m f=-50 i=2 t_ms=48.00 +[00:00:10] P9m f=-50 i=3 t_ms=64.00 +[00:00:10] P9w f=-50 wait_ms=16 elapsed_ms=16.00 +[00:00:10] P9w f=-50 wait_ms=100 elapsed_ms=112.00 +[00:00:11] P9w f=-50 wait_ms=1000 elapsed_ms=1008.00 +[00:00:11] P9 done diff --git a/probes/p9-slow-motion-extremes/capture.json b/probes/p9-slow-motion-extremes/capture.json index 9be84fd..2240984 100644 --- a/probes/p9-slow-motion-extremes/capture.json +++ b/probes/p9-slow-motion-extremes/capture.json @@ -1,15 +1,21 @@ { "probe": "p9-slow-motion-extremes", "client": { - "version": "", - "date": "", + "version": "2.25.0.1 - 154504.1", + "date": "2026-11-04", "locale": "zh-CN" }, - "runs": 0, + "runs": 3, "probe_sha256": "33f9bad09938e955b2eaccb3b6e91dc571e92481c0021f8b81b7d780f93c33b5", "hypothesis": "Set Slow Motion has no effect on the script timeline at extreme factors: the documented maximum is 100, and the client's minimum is not documented, so the probe measures 10, 200, 0 and -50. At every factor Total Time Elapsed advances 16 ms per tick (P9m lines: 16, 32, 48, 64) and Wait rounds up to whole 16 ms ticks (P9w lines: 16->16, 100->112, 1000->1008), as at 100.", - "observed": [], - "result": "inconclusive", - "notes": "No captures yet; running the probe in the client is an owner step. The expected lines are hypotheses in expect.json. P9 calls Set Slow Motion itself; it must be the only rule loaded and it restores the factor to 100 at the end. Factors 0 and -50 run last: if the client stops the simulation at a factor, the script cannot restore 100, so leave the match and keep the Inspector log up to that point as evidence. A factor whose lines differ from the expected ones is a mismatch for that factor.", - "sources": [] + "observed": [ + "All three runs: at factors 10, 200, 0 and -50, P9m t_ms was 16, 32, 48, 64; P9w wait_ms 16, 100, 1000 yielded elapsed_ms 16, 112, 1008, respectively." + ], + "result": "match", + "notes": "The probe measures script time, not precise wall-clock duration. All three runs reached P9 done. Inspector logs were transferred unchanged from the Windows Workshop directory; files contain ASCII text.", + "sources": [ + "Log-2026-11-04-02-20-40.txt", + "Log-2026-11-04-02-23-54.txt", + "Log-2026-11-04-02-29-13.txt" + ] } diff --git a/probes/p9-slow-motion-extremes/capture.txt b/probes/p9-slow-motion-extremes/capture.txt new file mode 100644 index 0000000..9db248c --- /dev/null +++ b/probes/p9-slow-motion-extremes/capture.txt @@ -0,0 +1,29 @@ +[00:00:03] P9m f=10 i=0 t_ms=16.00 +[00:00:03] P9m f=10 i=1 t_ms=32.00 +[00:00:03] P9m f=10 i=2 t_ms=48.00 +[00:00:03] P9m f=10 i=3 t_ms=64.00 +[00:00:04] P9w f=10 wait_ms=16 elapsed_ms=16.00 +[00:00:04] P9w f=10 wait_ms=100 elapsed_ms=112.00 +[00:00:05] P9w f=10 wait_ms=1000 elapsed_ms=1008.00 +[00:00:05] P9m f=200 i=0 t_ms=16.00 +[00:00:05] P9m f=200 i=1 t_ms=32.00 +[00:00:05] P9m f=200 i=2 t_ms=48.00 +[00:00:05] P9m f=200 i=3 t_ms=64.00 +[00:00:06] P9w f=200 wait_ms=16 elapsed_ms=16.00 +[00:00:06] P9w f=200 wait_ms=100 elapsed_ms=112.00 +[00:00:07] P9w f=200 wait_ms=1000 elapsed_ms=1008.00 +[00:00:07] P9m f=0 i=0 t_ms=16.00 +[00:00:07] P9m f=0 i=1 t_ms=32.00 +[00:00:07] P9m f=0 i=2 t_ms=48.00 +[00:00:07] P9m f=0 i=3 t_ms=64.00 +[00:00:08] P9w f=0 wait_ms=16 elapsed_ms=16.00 +[00:00:08] P9w f=0 wait_ms=100 elapsed_ms=112.00 +[00:00:09] P9w f=0 wait_ms=1000 elapsed_ms=1008.00 +[00:00:10] P9m f=-50 i=0 t_ms=16.00 +[00:00:10] P9m f=-50 i=1 t_ms=32.00 +[00:00:10] P9m f=-50 i=2 t_ms=48.00 +[00:00:10] P9m f=-50 i=3 t_ms=64.00 +[00:00:10] P9w f=-50 wait_ms=16 elapsed_ms=16.00 +[00:00:10] P9w f=-50 wait_ms=100 elapsed_ms=112.00 +[00:00:11] P9w f=-50 wait_ms=1000 elapsed_ms=1008.00 +[00:00:11] P9 done