From d45629ae29efb55632b43abbc61da8f6fdfa40fa Mon Sep 17 00:00:00 2001 From: chexit Date: Wed, 4 Nov 2026 21:49:25 +0300 Subject: [PATCH] Fix the UI-event fuel trap on a second large raid order The Windows log for #122 showed the mod stopping in the UI event (fuel budget 1e9, remaining 0) with known_units <- order_raid <- launch_wave <- ui_select: order_raid recomputed the planner snapshot known_units() for every drone, and the snapshot itself rescanned the whole force and the dedup list per intel entry. A second big Smart order cost drones x intel-log x force-size inside one event. Reproduced in actual WASM with measured per-callback fuel through the exact ui_select(BTN_LAUNCH) handler (diagnostic guest, trigger.patch): before, a 111-drone second order at revealed=13172/known=144 exhausted exactly 1e9 and stopped the mod (an 11-drone order at the same state: 247805865); after, the same scenario passes with max command fuel 32946177/1e9, failures=0, and /stats at tick 2602 byte-identical. A stacked single-point layout dies to one warhead (known=0, cheap order), so the scenario deploys spread clusters with a moved player. Fix: one known_units snapshot per order_raid passed to radar_strikes and choose_target; known_units is a single O(log + force) set pass; price_grid_on reuses its snapshot. Emitter/network targeting and carrier choice semantics are unchanged (native equivalence test against the former predicate; WASM checkpoint counters equal). Limits unchanged; HD/recording untouched; wave size unrestricted. Committed regressions: native snapshot-equivalence and second-smart-order tests; the heavy WASM fixture now orders two Smart raids through the wave screen's launch function (airdef preset + airdef raid) with EW and a combat intel log, four accepted orders asserted; the light contract covers preset and the empty-wave refusal. Core release 195 + 8 scenarios; adapter 65 + 1 boundary + 5 scenarios; light WASM 30 steps; heavy WASM 283 steps (tick max 182413686/300M, command max 6939810/1e9, failures 0). Windows GUI acceptance remains external and is not claimed. Co-Authored-By: Kimi Code (Moonshot AI) --- airdef-mod/src/lib.rs | 6 + airdef-mod/src/sim/ew.rs | 11 +- airdef-mod/src/sim/raid.rs | 126 ++- .../tests/budgets/wasm_flat_budget.toml | 45 ++ airdef-mod/tests/wasm_contract.toml | 13 + docs/CODEX-HANDOFF.md | 36 +- docs/ENGINE.md | 22 +- docs/IMPLEMENTATION-STATUS.md | 2 +- docs/RAID-STABILITY.md | 54 +- docs/measurements/flat-wasm-raid-budget.json | 64 +- docs/measurements/issue-122-ui-fuel/after.txt | 6 + .../issue-122-ui-fuel/before-scaling.txt | 10 + .../issue-122-ui-fuel/before-trap.txt | 7 + .../issue-122-ui-fuel/scenario-after.toml | 762 ++++++++++++++++++ .../issue-122-ui-fuel/scenario-before.toml | 688 ++++++++++++++++ .../issue-122-ui-fuel/summary.json | 67 ++ .../issue-122-ui-fuel/trigger.patch | 23 + tools/airdef.py | 6 +- tools/test_budget_report.py | 2 +- 19 files changed, 1907 insertions(+), 43 deletions(-) create mode 100644 docs/measurements/issue-122-ui-fuel/after.txt create mode 100644 docs/measurements/issue-122-ui-fuel/before-scaling.txt create mode 100644 docs/measurements/issue-122-ui-fuel/before-trap.txt create mode 100644 docs/measurements/issue-122-ui-fuel/scenario-after.toml create mode 100644 docs/measurements/issue-122-ui-fuel/scenario-before.toml create mode 100644 docs/measurements/issue-122-ui-fuel/summary.json create mode 100644 docs/measurements/issue-122-ui-fuel/trigger.patch diff --git a/airdef-mod/src/lib.rs b/airdef-mod/src/lib.rs index dc6585e..93bddf1 100644 --- a/airdef-mod/src/lib.rs +++ b/airdef-mod/src/lib.rs @@ -2391,6 +2391,12 @@ impl Mod for AirDef { self.launch_wave(&mut out)?; None } + ["preset", n] => { + // The wave screen's preset button for scripts: the same apply() the UI cycles. + let n = n.parse::().map_err(|_| "Номер готового состава: число".to_string())?; + self.wave.apply(n); + Some(format!("Готовый: {}", wave::PRESETS[self.wave.preset].name)) + } ["sites"] => { // The launch sites the radars have found, and the strike missiles (#101). let p = player().ok_or("Нет игрока")?; diff --git a/airdef-mod/src/sim/ew.rs b/airdef-mod/src/sim/ew.rs index 2e4e527..eefd90a 100644 --- a/airdef-mod/src/sim/ew.rs +++ b/airdef-mod/src/sim/ew.rs @@ -8,6 +8,7 @@ //! stations jammed (its next drones go for them), and from which bearings its //! raids were shot down or got through (its next raid comes from the best). use super::*; +use std::collections::HashSet; /// How far a station jams. pub const EW_RADIUS: f64 = 350.0; @@ -135,13 +136,17 @@ impl Sim { } /// Units the enemy knows of and would go for, newest first: (id, where). + /// One pass over the log: aliveness and duplicates by set, not by rescanning + /// the lists for every entry — a big log with many systems must not make the + /// planner's snapshot quadratic (#122). pub(super) fn known_units(&self) -> Vec<(u32, DVec3)> { + let alive: HashSet = self.sams.iter().filter(|s| s.alive()).map(|s| s.id).collect(); + let mut seen: HashSet = HashSet::new(); let mut out: Vec<(u32, DVec3)> = Vec::new(); for &(id, at, observed) in self.intel.revealed.iter().rev() { if self.tick.saturating_sub(observed) >= 6000 { continue; } - if !out.iter().any(|o| o.0 == id) && self.sams.iter().any(|s| s.id == id && s.alive()) { - out.push((id, at)); - } + if !seen.insert(id) { continue; } + if alive.contains(&id) { out.push((id, at)); } } out } diff --git a/airdef-mod/src/sim/raid.rs b/airdef-mod/src/sim/raid.rs index 765f4a2..f6901af 100644 --- a/airdef-mod/src/sim/raid.rs +++ b/airdef-mod/src/sim/raid.rs @@ -278,6 +278,10 @@ impl Sim { return 0; } let smart = order.aim == Aim::Smart; + // The planner's picture of the layout: one snapshot for the whole order. It is + // read-only while the plan is built, and recomputing it per drone made a big + // second order quadratic in the intel log and the force's size (#122). + let known = self.known_units(); // A smart enemy comes from where its raids got through best. let bearing = if smart { self.best_bearing() } else { self.rng.f() * TAU }; // The raid's sides (1, 2, or 4-5): its launch sites go there (#100). @@ -290,7 +294,7 @@ impl Sim { let grid = Grid { min: centre - DVec3::new(span, 0.0, span), cell, size: (2.0 * span / cell) as i32 + 1 }; let mut items = Vec::new(); // A smart raid goes for the radars too (a few strikers each); the others pick as they always did. - let radar_jobs = if smart { self.radar_strikes(&order.kinds) } else { Vec::new() }; + let radar_jobs = if smart { self.radar_strikes(&order.kinds, &known) } else { Vec::new() }; let mut index = 0usize; for (group, &(kind, count)) in order.kinds.iter().enumerate() { let spec = kind.spec(); @@ -306,7 +310,7 @@ impl Sim { index += 1; let (target, at_sam) = match job { Some((at, id)) => (at, Some(id)), - None => self.choose_target(kind, object, order.aim), + None => self.choose_target(kind, object, order.aim, &known), }; let aim = target + DVec3::new(self.rng.s() * 4.0, 0.0, self.rng.s() * 4.0); let dist = spec.ballistic.map_or(order.dist, |far| far.max(order.dist)) + self.rng.f() * 50.0; @@ -542,8 +546,8 @@ impl Sim { /// The strikes a smart raid sends at observed radars: for drones of `kinds` in order, /// the last observed place and id for those that go for one. Each radar /// gets two or three, the fast strikers first, and all together no more than about a quarter of the raid. - pub(super) fn radar_strikes(&self, kinds: &[(DroneKind, u32)]) -> Vec> { - let mut radars: Vec<(u32, DVec3)> = self.known_units().into_iter() + pub(super) fn radar_strikes(&self, kinds: &[(DroneKind, u32)], known: &[(u32, DVec3)]) -> Vec> { + let mut radars: Vec<(u32, DVec3)> = known.iter().copied() .filter(|(id, _)| self.sams.iter().any(|s| s.id == *id && s.alive() && s.kind == SamKind::Radar)) .collect(); radars.sort_by_key(|(id, _)| *id); @@ -568,7 +572,7 @@ impl Sim { /// What a drone goes for: an object, or (by its kind and the raid's aim) a system. Returns the /// point and the system's id. - fn choose_target(&mut self, kind: DroneKind, object: DVec3, aim: Aim) -> (DVec3, Option) { + fn choose_target(&mut self, kind: DroneKind, object: DVec3, aim: Aim, known: &[(u32, DVec3)]) -> (DVec3, Option) { let alive: Vec<&Sam> = self.sams.iter().filter(|s| s.alive()).collect(); if alive.is_empty() || kind.spec().decoy || aim == Aim::Objects { return (object, None); @@ -588,8 +592,7 @@ impl Sim { // station, a radar, a system that fired). // The planner has only what the enemy knows (#93): the radars (they emit) and the units that gave // themselves away; the rest of the player's layout is not in its hands. - let known = self.known_units(); - let alive: Vec<&Sam> = alive.iter().copied().filter(|s| Self::enemy_knows(s, &known)).collect(); + let alive: Vec<&Sam> = alive.iter().copied().filter(|s| Self::enemy_knows(s, known)).collect(); let pick_known = |list: &[&Sam], r: f64| -> Option<(DVec3, u32)> { let s = *list.get((r * list.len() as f64) as usize)?; known.iter().find(|(id, _)| *id == s.id).map(|(_, pos)| (*pos, s.id)) @@ -631,9 +634,8 @@ impl Sim { let mut prices = vec![1.0f32; (n * n) as usize]; // Known jamming stations: drones that navigate by satellite keep away. if kind.jammable() { - let known = self.known_units(); - for s in self.sams.iter().filter(|s| s.kind == SamKind::Ew && s.alive() && known.iter().any(|k| k.0 == s.id)) { - let pos = known.iter().find(|(id, _)| *id == s.id).unwrap().1; + for s in self.sams.iter().filter(|s| s.kind == SamKind::Ew && s.alive() && known_now.iter().any(|k| k.0 == s.id)) { + let pos = known_now.iter().find(|(id, _)| *id == s.id).unwrap().1; let (ci, cj) = grid.index(pos); let r = (EW_RADIUS / grid.cell).ceil() as i32; for i in (ci - r).max(0)..=(ci + r).min(n - 1) { @@ -979,16 +981,19 @@ mod tests { radar.radar_on = false; sim.sams.push(radar); let kinds = [(DroneKind::Kh22, 8)]; - assert!(sim.radar_strikes(&kinds).iter().all(Option::is_none)); + let none = Vec::new(); + assert!(sim.radar_strikes(&kinds, &none).iter().all(Option::is_none)); sim.tick = 20; sim.intel.revealed.push((1, old, sim.tick)); sim.sams[0].pos = DVec3::new(900.0, 70.0, 900.0); sim.tick = 40; - let jobs = sim.radar_strikes(&kinds); + let known = sim.known_units(); + let jobs = sim.radar_strikes(&kinds, &known); assert_eq!(jobs.iter().flatten().count(), 2); assert!(jobs.iter().flatten().all(|job| *job == (old, 1))); sim.tick = 6020; - assert!(sim.radar_strikes(&kinds).iter().all(Option::is_none)); + let known = sim.known_units(); + assert!(sim.radar_strikes(&kinds, &known).iter().all(Option::is_none)); } #[test] @@ -1044,6 +1049,101 @@ mod tests { check(&sim, "after adapting"); } + /// The snapshot the planner works from must not depend on how it is computed: + /// the set-based pass is checked here against the former per-entry rescan, over + /// duplicates, the dead, the aged-out, ids the force never had, and ordering. + #[test] + fn known_units_matches_the_naive_reference_on_a_fat_log() { + let mut sim = Sim::new(9); + let mut places = Vec::new(); + for (i, kind) in [SamKind::S400, SamKind::Ew, SamKind::Radar, SamKind::Tor, SamKind::Patriot].into_iter().enumerate() { + let id = sim.id(); + let mut sam = Sam::new(id, kind, DVec3::new(i as f64 * 100.0, 70.0, -50.0), 0.0); + // The second system is dead: known only while it lived. + if i == 1 { sam.health = 0.0; } + sim.sams.push(sam); + places.push((id, DVec3::new(i as f64 * 100.0, 70.0, -50.0))); + } + let naive = |sim: &Sim| { + let mut out: Vec<(u32, DVec3)> = Vec::new(); + for &(id, at, observed) in sim.intel.revealed.iter().rev() { + if sim.tick.saturating_sub(observed) >= 6000 { continue; } + if !out.iter().any(|o| o.0 == id) && sim.sams.iter().any(|s| s.id == id && s.alive()) { + out.push((id, at)); + } + } + out + }; + sim.tick = 7000; + let dead = places[1].0; + let (a, b, c, d, e) = (places[0].0, places[1].0, places[2].0, places[3].0, places[4].0); + // A long log: repeated observations (newest wins), one dead system, one never-seen + // system, one id the force does not have, and an entry that aged out. + for t in 0..200u64 { + sim.intel.revealed.push((a, DVec3::new(11.0, 70.0, t as f64), 1000 + t)); + sim.intel.revealed.push((c, DVec3::new(13.0, 70.0, t as f64), 1000 + t)); + if t % 7 == 0 { sim.intel.revealed.push((b, DVec3::new(12.0, 70.0, t as f64), 1000 + t)); } + if t % 11 == 0 { sim.intel.revealed.push((d, DVec3::new(14.0, 70.0, t as f64), 1000 + t)); } + if t % 13 == 0 { sim.intel.revealed.push((999_999, DVec3::new(1.0, 70.0, 0.0), 1000 + t)); } + } + sim.intel.revealed.push((e, DVec3::new(15.0, 70.0, 0.0), 2)); // aged out at tick 7000 + let expected = naive(&sim); + assert_eq!(sim.known_units(), expected); + assert_eq!(expected.iter().map(|k| k.0).collect::>(), vec![c, a, d]); + assert!(expected.iter().all(|k| k.0 != dead && k.0 != e && k.0 != 999_999)); + // The newest observation's place is the one kept. + assert_eq!(expected.iter().find(|k| k.0 == a).unwrap().1, DVec3::new(11.0, 70.0, 199.0)); + } + + /// The reported Windows stop (#122): the second big order from the UI path, with a fat + /// intel log and a large surviving force, must be accepted and launched like the first. + /// (The old per-drone planner snapshot was correct but quadratic; its cost is pinned by + /// the actual-WASM fuel measurements in docs/measurements/issue-122-ui-fuel.) + #[test] + fn a_second_smart_order_with_a_fat_intel_log_is_accepted_and_launched() { + let mut sim = Sim::new(31); + let mut ids = Vec::new(); + for i in 0..48 { + let id = sim.id(); + sim.sams.push(Sam::new(id, [SamKind::S400, SamKind::Patriot, SamKind::Nasams, SamKind::Pantsir][i % 4], DVec3::new((i % 8 * 40) as f64, 70.0, (i / 8 * 40) as f64), 0.0)); + ids.push(id); + } + // A fought-off first raid leaves a long log: launches, emitting radars, stations. + // Its entries sit in the past of the sim clock, like real ones. + sim.tick = 4000; + for t in 0..3000u64 { + sim.intel.revealed.push((ids[(t as usize) % ids.len()], DVec3::new(10.0, 70.0, t as f64), 1001 + t)); + } + sim.targets.push(DVec3::new(160.0, 70.0, 160.0)); + sim.target_health.push(100.0); + let mut ground = Ground::default(); + let land = Land(|_, _| 64); + let mut out = Out::default(); + for _ in 0..20 { + serve(&mut ground, &land, sim.tick); + sim.step(&mut ground, None, &mut out); + } + let mut order = RaidOrder::mix(36, 300.0); + order.aim = Aim::Smart; + order.approach = Approach::AllSides; + let total: u32 = order.kinds.iter().map(|k| k.1).sum(); + let first = sim.order_raid(&order, &sim.targets.clone(), &mut out); + assert_eq!(first, total); + while sim.planning() { + serve(&mut ground, &land, sim.tick); + sim.step(&mut ground, None, &mut out); + } + assert!(sim.queue.len() + sim.launched as usize >= total as usize, "the first order queued or launched every drone (queue {}, launched {})", sim.queue.len(), sim.launched); + // The second order — the path that stopped the mod on Windows — is accepted too. + let second = sim.order_raid(&order, &sim.targets.clone(), &mut out); + assert_eq!(second, total); + while sim.planning() { + serve(&mut ground, &land, sim.tick); + sim.step(&mut ground, None, &mut out); + } + assert!(sim.queue.len() + sim.launched as usize >= 2 * total as usize, "both orders queued or launched their drones (queue {}, launched {})", sim.queue.len(), sim.launched); + } + #[test] fn systems_ready_together_send_one_missile_at_a_drone() { let mut sim = Sim::new(3); diff --git a/airdef-mod/tests/budgets/wasm_flat_budget.toml b/airdef-mod/tests/budgets/wasm_flat_budget.toml index f62b379..bbaadbb 100644 --- a/airdef-mod/tests/budgets/wasm_flat_budget.toml +++ b/airdef-mod/tests/budgets/wasm_flat_budget.toml @@ -1017,3 +1017,48 @@ frames = 10 [[step]] command = "airdef stats" expect.feedback = "В воздухе: 184, ракет: 10, ПВО: 64, сбито: 777, поражений: 1499, разбились о препятствия: 0; удары по ПУ: пущено 0, уничтожено 0, подавлено 0, промахов 0, сбито ПВО 0; мир: 5353 секций (250 КБ), запросов 14405, неизвестно 0, лучей 327428 (ударов 6746, неизв. 213), клеток 2282312; взрывы в блоках: 1586 (подтверждено 0, без перемен 175, нет чанка 1330), чужих ответов 0" + +# The Windows #122 path: a Smart order through the UI launch function (`airdef raid` +# is the wave screen's ПУСК button handler) with a fat intel log and EW up — twice. +[[step]] +command = "airdef deploy ew" + +[[step]] +command = "airdef deploy ew" + +[[step]] +command = "airdef deploy ew" + +[[step]] +command = "airdef deploy ew" + +[[step]] +command = "airdef ew on" + +[[step]] +command = "airdef preset 1" +expect.feedback = "Готовый: Массированный удар" + +[[step]] +command = "airdef raid" +expect.chat = [] + +[[step]] +ticks = 400 +frames = 50 + +[[step]] +command = "airdef preset 1" +expect.feedback = "Готовый: Массированный удар" + +[[step]] +command = "airdef raid" +expect.chat = [] + +[[step]] +ticks = 800 +frames = 100 + +[[step]] +command = "airdef stats" +expect.feedback = "В воздухе: 11, ракет: 3, ПВО: 68, сбито: 936, поражений: 1853, разбились о препятствия: 0; удары по ПУ: пущено 0, уничтожено 0, подавлено 0, промахов 0, сбито ПВО 0; мир: 6643 секций (311 КБ), запросов 17355, неизвестно 0, лучей 393828 (ударов 9283, неизв. 978), клеток 2953257; взрывы в блоках: 1961 (подтверждено 0, без перемен 234, нет чанка 1727), чужих ответов 0" diff --git a/airdef-mod/tests/wasm_contract.toml b/airdef-mod/tests/wasm_contract.toml index 8fb5c6c..c7b5440 100644 --- a/airdef-mod/tests/wasm_contract.toml +++ b/airdef-mod/tests/wasm_contract.toml @@ -73,6 +73,19 @@ expect.error = "У комплекса нет активной РЛС" [[step]] command = "airdef deploy s500_launcher" +# The wave screen's preset button through the command boundary; an empty wave cannot launch. +[[step]] +command = "airdef preset 0" +expect.feedback = "Готовый: Рой Шахедов" + +[[step]] +command = "airdef preset 6" +expect.feedback = "Готовый: Пусто" + +[[step]] +command = "airdef raid" +expect.error = "Налёт пуст: выберите типы и число" + [[step]] command = "airdef wave mix 40 1500" diff --git a/docs/CODEX-HANDOFF.md b/docs/CODEX-HANDOFF.md index 3a61968..3416e26 100644 --- a/docs/CODEX-HANDOFF.md +++ b/docs/CODEX-HANDOFF.md @@ -16,6 +16,37 @@ подтвердила 1.6 как фактический голос P1-SUN (недостижимый `P1sun => 1.7` в старом match не в счёт); на это добавлена Rust-регрессия. Остаток: варианты комплексов и игровая приёмка. +### Продолжение #122 (второй налёт из UI) + +Точный лог Windows: `stopped in UI event (fuel budget 1000000000, remaining Some(0))`, +backtrace `known_units ← order_raid ← launch_wave ← ui_select` — другой класс, чем тик +`1f28f83`. Причина: `order_raid` пересчитывал снимок известных комплексов на каждом дроне, +а сам снимок был квадратичен по журналу и группировке. Исправление: один снимок на приказ +(+ O(журнал+группировка) через множества, `price_grid_on` переиспользует свой снимок); +семантика целеуказания и выбора носителей не менялась, сверка с прежним предикатом в +нативном тесте, `/stats` в WASM совпал побайтово. Лимиты не трогали, HD/запись не отключали, +размер волны не ограничивали. + +Воспроизведение в настоящем WASM (измеренный fuel): до — второй приказ 111 дронов исчерпывает +ровно 1e9 в событии (failures=1), тот же приказ 11 дронов — 247805865; после — тот же сценарий +целиком: max command 32946177/1e9, tick 236581288/300M, ошибок 0. Диагностический гость +(`trigger.patch` вызывает обработчик `ui_select(BTN_LAUNCH)`) и точный сценарий: +[issue-122-ui-fuel](measurements/issue-122-ui-fuel/summary.json). Воспроизвести удалось только +с разнесённой группировкой: стек в одной точке гибнет от одной боеголовки (`known=0`, приказ +дёшев) — у пользователя силы дожили до второго приказа. + +Закреплённые регрессии: нативные «эквивалентность снимка» и «второй умный приказ с толстым +журналом принят и запущен»; тяжёлый WASM fixture расширен двумя умными приказами через +`airdef preset` + `airdef raid` (функция кнопки ПУСК) — 4 принятых приказа, SHA fixture +обновлён; лёгкий контракт 30 шагов с `preset` и отказом пустой волны. Новая команда +`airdef preset ` — script-эквивалент кнопки готового состава. + +Последний общий прогон: core release **195 + 8 сценариев** / 7 ignored; адаптер **65 + 1 +boundary + 5 сценариев**; лёгкий WASM **30 шагов**; тяжёлый WASM **283 шага** (tick max +182413686/300M, command max 6939810/1e9, failures=0), отчёт +[flat-wasm-raid-budget](measurements/flat-wasm-raid-budget.json); 6 тестов отчёта budget +и build-тест прошли. Ни одно issue не закрыто; пуш не выполнялся. + ### Контрольная точка 3 ноября Код предыдущих четырёх задач интегрирован до `6e2d661`. Продолжение #102/обратного вывоза @@ -76,8 +107,9 @@ awaiting-backend. Выход через quit подтверждён. Это не ## Двойные налёты и плотность следов -Перед продолжением читать [RAID-STABILITY.md](RAID-STABILITY.md): один соответствующий класс -отключения теперь воспроизведён и исправлен (`1f28f83`), приёмка конкретного Windows-сеанса остаётся. +Перед продолжением читать [RAID-STABILITY.md](RAID-STABILITY.md): два соответствующих класса +отключения теперь воспроизведены и исправлены — тик (`1f28f83`) и событие UI на втором +приказе (#122, `order_raid`/known_units); приёмка конкретного Windows-сеанса остаётся. Текущий исправленный WASM stress проверяет две принятые волны, обслуживаемую плоскую геометрию и 184 воздушные цели в конкретный момент; новый [JSON](measurements/flat-wasm-raid-budget.json) заменяет прежнее недостаточное доказательство покрытия. Лимиты fuel/memory не увеличивались. diff --git a/docs/ENGINE.md b/docs/ENGINE.md index c044595..415734e 100644 --- a/docs/ENGINE.md +++ b/docs/ENGINE.md @@ -122,6 +122,13 @@ GPU не имитируется. Проверяются два принятых frame hook; эти вызовы не являются нагрузкой на GPU или измерением 10M frame fuel мода. Точный отчёт: [flat-world budget](measurements/flat-wasm-raid-budget.json). +С 4 ноября (#122) fixture расширен хвостом умных приказов через UI-путь: 4 РЭБ, `airdef preset 1` +(та же `apply()`, что у кнопки готового состава) и два `airdef raid` — это та же функция +`launch_wave`, что у кнопки ПУСК (`ui_select`), с боевым журналом разведки и EW; оба приказа +приняты (`expect.chat = []`), итого 4 принятых приказа, финальный `/stats` закреплён строкой. +Прогон исправления #122: 283 шага, 3302 tick; max fuel tick 182413686/300M, command 6939810/1e9, +ошибок 0. Отчёт: [flat-world budget](measurements/flat-wasm-raid-budget.json). + Fixture копируется из тех же байтов, чей SHA записан до сборки. Проверенные 2 принятия и точка 184/2102 заносятся только после success и совпадения `BUDGET_ASSERTED_SHA256` с точной версией файла. После изменения fixture нужно повторно проверить её и обновить SHA/метаданные; простой список @@ -137,11 +144,15 @@ API/WIT/артефакт, проверку build identity, сценарий, Was Отсутствие запрошенных счётчиков делает проверку неуспешной, даже если сценарий завершился. Это не время host/GPU и не peak memory; невызывавшиеся callbacks, включая init до включения профиля, не получают выдуманного нуля. Первый прогон: tick 2102 вызова, max 106132646/300000000; -command 69 вызовов, max 1013370/1000000000; ошибок нет. +command 69 вызовов, max 1013370/1000000000; ошибок нет. Прогон исправления #122 (283 шага, +включая два умных UI-приказа): tick 3302 вызова, max 182413686/300M; command 79 вызовов, +max 6939810/1e9; ошибок нет. Исторический отчёт без измерения fuel: [API27 budget report с поправкой покрытия](measurements/api27-wasm-budget.json). Актуальный чистый API29-прогон с измерением: [271 шаг, fuel и build identity](measurements/api29-wasm-budget.json). +Актуальный прогон с умными UI-приказами #122 и build identity: +[flat-world budget](measurements/flat-wasm-raid-budget.json). Проверены реальный WASM guest с переходами состояний/владельца/мира, сборка клиента и два коротких offscreen-профиля Metal. При `HORIZON_ACOUSTICS=1` + `HORIZON_NO_AUDIO=1` в игре получено @@ -234,3 +245,12 @@ GPU/готовый MP4 и платформенная приёмка. Эти ре 3602 тика прошли, max tick fuel = 135273690/300M, failures=0. Лимиты не менялись. Диагностический гость содержит только временный способ запуска точного меню-состава; это не исходный пользовательский Windows-сеанс. [Данные и воспроизведение](RAID-STABILITY.md). + +С профилем воспроизведён и второй класс — остановка в **событии UI** (не тике) на втором +крупном приказе: переснимок `known_units` на каждом дроне приказа давал `дроны × журнал × +группировка` за одно событие, 1e9 исчерпаны ровно (max command fuel 1000000000/1e9, +failures=1; состояние: 13172 записей журнала, 144 известных живых комплекса). Исправление +(#122, одним снимком на приказ и O(журнал+группировка) за проход) доводит тот же сценарий +до max command fuel 32946177/1e9, failures=0, `/stats` на тике 2602 совпал побайтово. +Это гостевые инструкции, не прирост FPS; приёмка Windows-сеанса внешняя. +[Данные и воспроизведение](RAID-STABILITY.md). diff --git a/docs/IMPLEMENTATION-STATUS.md b/docs/IMPLEMENTATION-STATUS.md index 82655df..cdd7a7f 100644 --- a/docs/IMPLEMENTATION-STATUS.md +++ b/docs/IMPLEMENTATION-STATUS.md @@ -48,7 +48,7 @@ | #119 | Три read-only луча в известной геометрии ослабляют непрямой blast для корпуса/узлов/объектов; проёмы дают частичное воздействие; прямой контакт сохранён | Unknown/stale: прежний урон без защиты, нулевое ожидание; partial blocks считаются занятыми кубами; GUI-проверка разрушения укрытия ещё нужна | | #120 | Конечное горение, powered/coast, сопротивление и снижение доступного манёвра, один остаточный pk по фактической скорости; swept-встреча; события burnout/energy loss; согласованное прекращение факела/дыма/звука | Игровой профиль, без аэродинамики и реальных параметров; сравнительная выборка 12 рейдов и расширенная сетка 48 рейдов (6 вариантов × 8 seed, батареи/дальности/классы) записаны в measurements; без подтверждённого дефекта параметры не менялись; прослушивание остаётся | | #121 | On/Off в команде и карте, save/replay; Off не создаёт own measurements и сохраняет сетевой приём; раскрытие работающего радара наблюдателем; SEAD использует last-known и возраст | Условный наблюдатель по расстоянию, без радиотехнической модели и ошибки пеленга; выбор на карте ещё не проверен в GUI | -| #122 | Воспроизведён tick fuel exhaustion на двух максимальных составах, HD; полный обход height-cache на каждом взрыве заменён удалением нужных ключей; тест эквивалентности и WASM421/3602 прошли, max135.3M/300M; callback diagnostics/profile | Требуется приёмка на Windows; без старого лога нельзя подтвердить ту же причину пользовательского сеанса; FPS/GPU не измерялись | +| #122 | Воспроизведены два класса: tick fuel exhaustion на двух максимальных составах, HD (полный обход height-cache на каждом взрыве заменён удалением нужных ключей; max135.3M/300M) и остановка в UI event на втором крупном приказе (переснимок known_units на каждом дроне заменён одним снимком на приказ и O(журнал+группировка); max command 32.9M/1e9, `/stats` до/после совпал); нативные тесты эквивалентности и второго приказа; WASM fixture с 4 принятыми приказами; callback diagnostics/profile | Требуется приёмка на Windows; без подтверждённого build identity нельзя утверждать ту же причину пользовательского сеанса; FPS/GPU не измерялись | Точные коммиты, формат сохранения, тесты и порядок продолжения: [CODEX-HANDOFF.md](CODEX-HANDOFF.md). Формальный open-статус не меняется после реализации. diff --git a/docs/RAID-STABILITY.md b/docs/RAID-STABILITY.md index e15146e..898255b 100644 --- a/docs/RAID-STABILITY.md +++ b/docs/RAID-STABILITY.md @@ -7,10 +7,58 @@ поэтому запись может сохраниться и без ручного копирования. Задача: [#122](https://gitea.maoniao.team/chexit/airdef/issues/122). -**Воспроизведена и исправлена одна конкретная причина отключения при таком классе нагрузки.** -Без старого лога не утверждается, что Windows-сеанс имел именно её; GPU/FPS отдельно не измерялись. +**Воспроизведены и исправлены две конкретные причины отключения в этом классе нагрузки.** +Первая — тик и кэш высот (`1f28f83`, ниже). Вторая — событие UI и второй приказ +(`order_raid`, см. раздел «Второй налёт из UI: событие, не тик»). Без старого лога не +утверждается, что Windows-сеанс имел именно их; GPU/FPS отдельно не измерялись. -## Найденная причина и фикс +## Второй налёт из UI: событие, не тик + +Точный лог Windows: `Horizon: mod airdef stopped in UI event (fuel budget 1000000000, +remaining Some(0))`; backtrace: `ew::Sim::known_units` ← `raid::Sim::order_raid` ← +`airdef::AirDef::launch_wave` ← `ui_select`. Прежний fix `1f28f83` — другой класс: тогда +мод останавливался в **тике** на 300M. + +**Причина.** `order_raid` для каждого дрона заново вычислял снимок известных комплексов +`known_units()` (`choose_target`), а сам `known_units()` на каждую запись журнала разведки +пересканировал весь список комплексов и список уже принятых. Журнал держит каждый пуск, +каждое раскрытие РЭБ и каждую emitting-РЛС за последние 6000 тиков. Стоимость второго +большого приказа росла как `дроны × журнал × размер группировки` внутри одного UI-события. + +**Воспроизведение** (настоящий WASM, измеренный fuel по callback, диагностический гость — +`trigger.patch` гоняет тот же обработчик `ui_select(BTN_LAUNCH)` → `launch_wave` → +`order_raid` с максимальным составом меню; метка callback — `command`, бюджет тот же 1e9): +разнесённая группировка 172 комплекса (40 кластеров × 4 тяжёлых + 12 РЭБ, игрок двигается +шагами сценария — стек в одной точке умирает от одной боеголовки и не воспроизводит), +первый налёт 111 дронов отбит, второй приказ на тике ~2600 при `revealed=13172`, +`known=144` живых. До fix: второй приказ исчерпывает ровно 1e9 и останавливает мод +(`before-trap.txt`); тот же приказ из 11 дронов в том же состоянии — 247805865 +(`before-scaling.txt`). После fix тот же сценарий проходит целиком: max command fuel +32946177/1e9, max tick 236581288/300M, ошибок 0; `/stats` на тике 2602 совпал побайтово +до/после, включая счётчики мира. + +**Фикс.** Один снимок `known_units` на весь приказ, передаётся в `radar_strikes` и +`choose_target`; сам `known_units` — один проход O(журнал + группировка) через множества; +`price_grid_on` переиспользует свой снимок. Семантика планирования не изменилась: +нативный тест сверяет новый снимок с прежним предикатом (дубликаты, погибшие, устаревшие, +чужие id, порядок «новые первыми»). Лимиты не менялись, HD/запись не отключались, +размер волны не ограничивался. + +**Регрессии.** Нативные: эквивалентность снимка и «второй умный приказ с толстым журналом +принят и запущен». Закреплённые WASM: тяжёлый fixture расширен — два умных приказа через +функцию кнопки ПУСК (`airdef preset 1` + `airdef raid` — тот же `launch_wave`, что и у +`ui_select`) с РЭБ и боевым журналом, оба приняты (`expect.chat = []`), 4 принятых приказа +в отчёте; лёгкий контракт проверяет `preset` и отказ пустой волны. Данные и точный +сценарий: [summary.json](measurements/issue-122-ui-fuel/summary.json), +[trigger.patch](measurements/issue-122-ui-fuel/trigger.patch), +[до](measurements/issue-122-ui-fuel/scenario-before.toml), +[после](measurements/issue-122-ui-fuel/scenario-after.toml). Patch — только для +диагностического гостя, в мод не входит. + +Приёмка конкретного Windows-сеанса остаётся внешней: нужны build identity и строки +`latest.log` вокруг остановки. Измерение fuel — не прирост FPS. + +## Первая причина и фикс (тик, `1f28f83`) Два максимальных состава меню (11 видов × 40, два принятых приказа), 64 ПВО, расстояние 300, HD и grid, обслуживаемый плоский мир: tick 1835 исчерпал ровно 300M fuel и мод остановился. diff --git a/docs/measurements/flat-wasm-raid-budget.json b/docs/measurements/flat-wasm-raid-budget.json index 0489992..6ba70b6 100644 --- a/docs/measurements/flat-wasm-raid-budget.json +++ b/docs/measurements/flat-wasm-raid-budget.json @@ -1,11 +1,11 @@ { "airdef": { - "diff_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", - "dirty": false, - "sha": "293236490b567866f7468a5bee5d61b0dcf8086d" + "diff_sha256": "143fbab5f1fd60981df9ef8ef3081b08a4102507f2588c6942924a7325b5139b", + "dirty": true, + "sha": "e6a70004406b34ea17884875ce236a5fb4b849e4" }, - "api_level": 27, - "artifact_sha256": "cb52fa5fd6beb266c63f2a5f2aa50553c24bc435a212c76142cc1bd228fd0aa6", + "api_level": 29, + "artifact_sha256": "c4ef7d759524eb053ccd55f0eccdcb49c91954201eb4e0ad923ee9b2828efc4e", "build_identity": { "checks": { "airdef_diff_sha256": true, @@ -17,15 +17,45 @@ "wit_sha256": true }, "matches": true, - "path": "airdef-mod/target/horizon-mod/build-identity.json" + "path": "/private/tmp/issue-122-raid/airdef-mod/target/horizon-mod/build-identity.json" }, "command": "test-budgets", "engine": { - "identity": "ed7615d362490f57bca5ac239c64eb9360913a4b", - "pin": "ed7615d362490f57bca5ac239c64eb9360913a4b" + "identity": "73b9d307048fc44d7f21fa3c829fc5a855d144a6", + "pin": "73b9d307048fc44d7f21fa3c829fc5a855d144a6" }, "exit_code": 0, - "output": " Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.39s\n Running `/horizon/target/debug/cargo-horizon-mod horizon-mod test --hmod /airdef-mod/target/horizon-mod/airdef.hmod budget`\nrunning 1 scripts against /airdef-mod/target/horizon-mod/airdef.hmod\ntest tests/wasm_budget.toml ... ok (271 steps)\ntest result: ok. 1 passed\n", + "measured_fuel": { + "available": true, + "callbacks": [ + { + "budget": 1000000000, + "callback": "command", + "calls": 79, + "failures": 0, + "max_used": 6939810, + "mod": "airdef" + }, + { + "budget": 10000000, + "callback": "frame", + "calls": 2202, + "failures": 0, + "max_used": 77, + "mod": "airdef" + }, + { + "budget": 300000000, + "callback": "tick", + "calls": 3302, + "failures": 0, + "max_used": 182413686, + "mod": "airdef" + } + ], + "scope": "guest callback fuel; excludes host CPU, memory and GPU time" + }, + "output": " Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.35s\n Running `/private/tmp/issue-122-raid/horizon/target/debug/cargo-horizon-mod horizon-mod test --hmod /private/tmp/issue-122-raid/airdef-mod/target/horizon-mod/airdef.hmod --fuel-profile budget`\nrunning 1 scripts against /private/tmp/issue-122-raid/airdef-mod/target/horizon-mod/airdef.hmod\nfuel airdef command: calls=79 max=6939810 budget=1000000000 failures=0\nfuel airdef frame: calls=2202 max=77 budget=10000000 failures=0\nfuel airdef tick: calls=3302 max=182413686 budget=300000000 failures=0\ntest tests/wasm_budget.toml ... ok (283 steps)\ntest result: ok. 1 passed\n", "runtime_limits": { "fuel_per_call": { "event": 1000000000, @@ -34,16 +64,16 @@ "tick": 300000000 }, "memory_bytes": 67108864, - "metering": "source_configured; measured_per_scenario: unavailable", + "metering": "source_configured", "source": "horizon/crates/horizon-mods/src/runtime.rs", "wasmtime_component_model": true, "wasmtime_fuel_metering": true }, "scenario": "tests/budgets/wasm_flat_budget.toml", "scenario_counts": { - "accepted_raids_asserted": 2, - "air_defense_units": 64, - "frames": 2052, + "accepted_raids_asserted": 4, + "air_defense_units": 68, + "frames": 2202, "layout_unit_limit": 2000, "peak_airborne_observed": null, "raid_requests": 2, @@ -51,13 +81,13 @@ "scripts": 1, "snapshot_airborne_asserted": 184, "snapshot_tick": 2102, - "steps": 271, - "ticks": 2102 + "steps": 283, + "ticks": 3302 }, "scenario_render_world": "flat at y=64", - "scenario_sha256": "bcc0359289ab20ab3de608bcc130f72d2cf88f9f6fbc470d7b6846e9db5d11df", + "scenario_sha256": "134cb4667691cf9faa639ed37120941d829cfc13ce45e9638582c409d9e0dd29", "schema": 1, "status": "passed", "wasmtime_version": "48.0.7", - "wit_sha256": "14c57b73f144888e04e27b77a6dfa29098c6b0c27c0383bb96aedaccb4c0a1d4" + "wit_sha256": "dded023a150a5f916bc5d935b7f891806728e0118eb7107ad0551d2aad8c70a5" } diff --git a/docs/measurements/issue-122-ui-fuel/after.txt b/docs/measurements/issue-122-ui-fuel/after.txt new file mode 100644 index 0000000..57cb266 --- /dev/null +++ b/docs/measurements/issue-122-ui-fuel/after.txt @@ -0,0 +1,6 @@ +running 1 scripts against /tmp/issue-122-raid/airdef-mod/target/horizon-mod/airdef.hmod +fuel airdef command: calls=179 max=32946177 budget=1000000000 failures=0 +fuel airdef frame: calls=377 max=77 budget=10000000 failures=0 +fuel airdef tick: calls=3802 max=236581288 budget=300000000 failures=0 +test tests/wasm_budget.toml ... ok (255 steps) +test result: ok. 1 passed diff --git a/docs/measurements/issue-122-ui-fuel/before-scaling.txt b/docs/measurements/issue-122-ui-fuel/before-scaling.txt new file mode 100644 index 0000000..9419c0c --- /dev/null +++ b/docs/measurements/issue-122-ui-fuel/before-scaling.txt @@ -0,0 +1,10 @@ +=== BEFORE: scaling witness — an 11-drone second order at the same state === +running 1 scripts against /tmp/issue-122-run/airdef-diag-before.hmod +fuel airdef command: calls=177 max=247805865 budget=1000000000 failures=0 +fuel airdef frame: calls=257 max=77 budget=10000000 failures=0 +fuel airdef tick: calls=2602 max=241372671 budget=300000000 failures=0 +test tests/wasm_budget.toml ... FAILED + step 229 (/airdef wave mix 1 300): feedback + expected: Some("PROBE") + got: Some("DIAG revealed=13172 known=144 sams=172 alive=144 planning=true queue=0") +error: 1 failed, 0 passed diff --git a/docs/measurements/issue-122-ui-fuel/before-trap.txt b/docs/measurements/issue-122-ui-fuel/before-trap.txt new file mode 100644 index 0000000..91a189c --- /dev/null +++ b/docs/measurements/issue-122-ui-fuel/before-trap.txt @@ -0,0 +1,7 @@ +=== BEFORE: second order (UI path) traps at the 1e9 event budget === +running 1 scripts against /tmp/issue-122-run/airdef-diag-before.hmod +fuel airdef command: calls=177 max=1000000000 budget=1000000000 failures=1 +fuel airdef frame: calls=257 max=77 budget=10000000 failures=0 +fuel airdef tick: calls=2602 max=241372671 budget=300000000 failures=0 +test tests/wasm_budget.toml ... ok (229 steps) +test result: ok. 1 passed diff --git a/docs/measurements/issue-122-ui-fuel/scenario-after.toml b/docs/measurements/issue-122-ui-fuel/scenario-after.toml new file mode 100644 index 0000000..9fa21c9 --- /dev/null +++ b/docs/measurements/issue-122-ui-fuel/scenario-after.toml @@ -0,0 +1,762 @@ +[[step]] +render-ground-y = 64 +ticks = 2 +frames = 2 +expect.commands = ["airdef"] +[[step]] +player = { x = 0.0, z = 0.0 } +command = "airdef deploy s400" +[[step]] +player = { x = 0.0, z = 0.0 } +command = "airdef deploy patriot" +[[step]] +player = { x = 0.0, z = 0.0 } +command = "airdef deploy nasams" +[[step]] +player = { x = 0.0, z = 0.0 } +command = "airdef deploy pantsir" +[[step]] +player = { x = 0.0, z = 0.0 } +command = "airdef deploy ew" +[[step]] +player = { x = 40.0, z = 0.0 } +command = "airdef deploy s400" +[[step]] +player = { x = 40.0, z = 0.0 } +command = "airdef deploy patriot" +[[step]] +player = { x = 40.0, z = 0.0 } +command = "airdef deploy nasams" +[[step]] +player = { x = 40.0, z = 0.0 } +command = "airdef deploy pantsir" +[[step]] +player = { x = 40.0, z = 0.0 } +command = "airdef deploy ew" +[[step]] +player = { x = 80.0, z = 0.0 } +command = "airdef deploy s400" +[[step]] +player = { x = 80.0, z = 0.0 } +command = "airdef deploy patriot" +[[step]] +player = { x = 80.0, z = 0.0 } +command = "airdef deploy nasams" +[[step]] +player = { x = 80.0, z = 0.0 } +command = "airdef deploy pantsir" +[[step]] +player = { x = 80.0, z = 0.0 } +command = "airdef deploy ew" +[[step]] +player = { x = 120.0, z = 0.0 } +command = "airdef deploy s400" +[[step]] +player = { x = 120.0, z = 0.0 } +command = "airdef deploy patriot" +[[step]] +player = { x = 120.0, z = 0.0 } +command = "airdef deploy nasams" +[[step]] +player = { x = 120.0, z = 0.0 } +command = "airdef deploy pantsir" +[[step]] +player = { x = 120.0, z = 0.0 } +command = "airdef deploy ew" +[[step]] +player = { x = 160.0, z = 0.0 } +command = "airdef deploy s400" +[[step]] +player = { x = 160.0, z = 0.0 } +command = "airdef deploy patriot" +[[step]] +player = { x = 160.0, z = 0.0 } +command = "airdef deploy nasams" +[[step]] +player = { x = 160.0, z = 0.0 } +command = "airdef deploy pantsir" +[[step]] +player = { x = 160.0, z = 0.0 } +command = "airdef deploy ew" +[[step]] +player = { x = 200.0, z = 0.0 } +command = "airdef deploy s400" +[[step]] +player = { x = 200.0, z = 0.0 } +command = "airdef deploy patriot" +[[step]] +player = { x = 200.0, z = 0.0 } +command = "airdef deploy nasams" +[[step]] +player = { x = 200.0, z = 0.0 } +command = "airdef deploy pantsir" +[[step]] +player = { x = 200.0, z = 0.0 } +command = "airdef deploy ew" +[[step]] +player = { x = 240.0, z = 0.0 } +command = "airdef deploy s400" +[[step]] +player = { x = 240.0, z = 0.0 } +command = "airdef deploy patriot" +[[step]] +player = { x = 240.0, z = 0.0 } +command = "airdef deploy nasams" +[[step]] +player = { x = 240.0, z = 0.0 } +command = "airdef deploy pantsir" +[[step]] +player = { x = 240.0, z = 0.0 } +command = "airdef deploy ew" +[[step]] +player = { x = 280.0, z = 0.0 } +command = "airdef deploy s400" +[[step]] +player = { x = 280.0, z = 0.0 } +command = "airdef deploy patriot" +[[step]] +player = { x = 280.0, z = 0.0 } +command = "airdef deploy nasams" +[[step]] +player = { x = 280.0, z = 0.0 } +command = "airdef deploy pantsir" +[[step]] +player = { x = 280.0, z = 0.0 } +command = "airdef deploy ew" +[[step]] +player = { x = 0.0, z = 40.0 } +command = "airdef deploy s400" +[[step]] +player = { x = 0.0, z = 40.0 } +command = "airdef deploy patriot" +[[step]] +player = { x = 0.0, z = 40.0 } +command = "airdef deploy nasams" +[[step]] +player = { x = 0.0, z = 40.0 } +command = "airdef deploy pantsir" +[[step]] +player = { x = 0.0, z = 40.0 } +command = "airdef deploy ew" +[[step]] +player = { x = 40.0, z = 40.0 } +command = "airdef deploy s400" +[[step]] +player = { x = 40.0, z = 40.0 } +command = "airdef deploy patriot" +[[step]] +player = { x = 40.0, z = 40.0 } +command = "airdef deploy nasams" +[[step]] +player = { x = 40.0, z = 40.0 } +command = "airdef deploy pantsir" +[[step]] +player = { x = 40.0, z = 40.0 } +command = "airdef deploy ew" +[[step]] +player = { x = 80.0, z = 40.0 } +command = "airdef deploy s400" +[[step]] +player = { x = 80.0, z = 40.0 } +command = "airdef deploy patriot" +[[step]] +player = { x = 80.0, z = 40.0 } +command = "airdef deploy nasams" +[[step]] +player = { x = 80.0, z = 40.0 } +command = "airdef deploy pantsir" +[[step]] +player = { x = 80.0, z = 40.0 } +command = "airdef deploy ew" +[[step]] +player = { x = 120.0, z = 40.0 } +command = "airdef deploy s400" +[[step]] +player = { x = 120.0, z = 40.0 } +command = "airdef deploy patriot" +[[step]] +player = { x = 120.0, z = 40.0 } +command = "airdef deploy nasams" +[[step]] +player = { x = 120.0, z = 40.0 } +command = "airdef deploy pantsir" +[[step]] +player = { x = 120.0, z = 40.0 } +command = "airdef deploy ew" +[[step]] +player = { x = 160.0, z = 40.0 } +command = "airdef deploy s400" +[[step]] +player = { x = 160.0, z = 40.0 } +command = "airdef deploy patriot" +[[step]] +player = { x = 160.0, z = 40.0 } +command = "airdef deploy nasams" +[[step]] +player = { x = 160.0, z = 40.0 } +command = "airdef deploy pantsir" +[[step]] +player = { x = 200.0, z = 40.0 } +command = "airdef deploy s400" +[[step]] +player = { x = 200.0, z = 40.0 } +command = "airdef deploy patriot" +[[step]] +player = { x = 200.0, z = 40.0 } +command = "airdef deploy nasams" +[[step]] +player = { x = 200.0, z = 40.0 } +command = "airdef deploy pantsir" +[[step]] +player = { x = 240.0, z = 40.0 } +command = "airdef deploy s400" +[[step]] +player = { x = 240.0, z = 40.0 } +command = "airdef deploy patriot" +[[step]] +player = { x = 240.0, z = 40.0 } +command = "airdef deploy nasams" +[[step]] +player = { x = 240.0, z = 40.0 } +command = "airdef deploy pantsir" +[[step]] +player = { x = 280.0, z = 40.0 } +command = "airdef deploy s400" +[[step]] +player = { x = 280.0, z = 40.0 } +command = "airdef deploy patriot" +[[step]] +player = { x = 280.0, z = 40.0 } +command = "airdef deploy nasams" +[[step]] +player = { x = 280.0, z = 40.0 } +command = "airdef deploy pantsir" +[[step]] +player = { x = 0.0, z = 80.0 } +command = "airdef deploy s400" +[[step]] +player = { x = 0.0, z = 80.0 } +command = "airdef deploy patriot" +[[step]] +player = { x = 0.0, z = 80.0 } +command = "airdef deploy nasams" +[[step]] +player = { x = 0.0, z = 80.0 } +command = "airdef deploy pantsir" +[[step]] +player = { x = 40.0, z = 80.0 } +command = "airdef deploy s400" +[[step]] +player = { x = 40.0, z = 80.0 } +command = "airdef deploy patriot" +[[step]] +player = { x = 40.0, z = 80.0 } +command = "airdef deploy nasams" +[[step]] +player = { x = 40.0, z = 80.0 } +command = "airdef deploy pantsir" +[[step]] +player = { x = 80.0, z = 80.0 } +command = "airdef deploy s400" +[[step]] +player = { x = 80.0, z = 80.0 } +command = "airdef deploy patriot" +[[step]] +player = { x = 80.0, z = 80.0 } +command = "airdef deploy nasams" +[[step]] +player = { x = 80.0, z = 80.0 } +command = "airdef deploy pantsir" +[[step]] +player = { x = 120.0, z = 80.0 } +command = "airdef deploy s400" +[[step]] +player = { x = 120.0, z = 80.0 } +command = "airdef deploy patriot" +[[step]] +player = { x = 120.0, z = 80.0 } +command = "airdef deploy nasams" +[[step]] +player = { x = 120.0, z = 80.0 } +command = "airdef deploy pantsir" +[[step]] +player = { x = 160.0, z = 80.0 } +command = "airdef deploy s400" +[[step]] +player = { x = 160.0, z = 80.0 } +command = "airdef deploy patriot" +[[step]] +player = { x = 160.0, z = 80.0 } +command = "airdef deploy nasams" +[[step]] +player = { x = 160.0, z = 80.0 } +command = "airdef deploy pantsir" +[[step]] +player = { x = 200.0, z = 80.0 } +command = "airdef deploy s400" +[[step]] +player = { x = 200.0, z = 80.0 } +command = "airdef deploy patriot" +[[step]] +player = { x = 200.0, z = 80.0 } +command = "airdef deploy nasams" +[[step]] +player = { x = 200.0, z = 80.0 } +command = "airdef deploy pantsir" +[[step]] +player = { x = 240.0, z = 80.0 } +command = "airdef deploy s400" +[[step]] +player = { x = 240.0, z = 80.0 } +command = "airdef deploy patriot" +[[step]] +player = { x = 240.0, z = 80.0 } +command = "airdef deploy nasams" +[[step]] +player = { x = 240.0, z = 80.0 } +command = "airdef deploy pantsir" +[[step]] +player = { x = 280.0, z = 80.0 } +command = "airdef deploy s400" +[[step]] +player = { x = 280.0, z = 80.0 } +command = "airdef deploy patriot" +[[step]] +player = { x = 280.0, z = 80.0 } +command = "airdef deploy nasams" +[[step]] +player = { x = 280.0, z = 80.0 } +command = "airdef deploy pantsir" +[[step]] +player = { x = 0.0, z = 120.0 } +command = "airdef deploy s400" +[[step]] +player = { x = 0.0, z = 120.0 } +command = "airdef deploy patriot" +[[step]] +player = { x = 0.0, z = 120.0 } +command = "airdef deploy nasams" +[[step]] +player = { x = 0.0, z = 120.0 } +command = "airdef deploy pantsir" +[[step]] +player = { x = 40.0, z = 120.0 } +command = "airdef deploy s400" +[[step]] +player = { x = 40.0, z = 120.0 } +command = "airdef deploy patriot" +[[step]] +player = { x = 40.0, z = 120.0 } +command = "airdef deploy nasams" +[[step]] +player = { x = 40.0, z = 120.0 } +command = "airdef deploy pantsir" +[[step]] +player = { x = 80.0, z = 120.0 } +command = "airdef deploy s400" +[[step]] +player = { x = 80.0, z = 120.0 } +command = "airdef deploy patriot" +[[step]] +player = { x = 80.0, z = 120.0 } +command = "airdef deploy nasams" +[[step]] +player = { x = 80.0, z = 120.0 } +command = "airdef deploy pantsir" +[[step]] +player = { x = 120.0, z = 120.0 } +command = "airdef deploy s400" +[[step]] +player = { x = 120.0, z = 120.0 } +command = "airdef deploy patriot" +[[step]] +player = { x = 120.0, z = 120.0 } +command = "airdef deploy nasams" +[[step]] +player = { x = 120.0, z = 120.0 } +command = "airdef deploy pantsir" +[[step]] +player = { x = 160.0, z = 120.0 } +command = "airdef deploy s400" +[[step]] +player = { x = 160.0, z = 120.0 } +command = "airdef deploy patriot" +[[step]] +player = { x = 160.0, z = 120.0 } +command = "airdef deploy nasams" +[[step]] +player = { x = 160.0, z = 120.0 } +command = "airdef deploy pantsir" +[[step]] +player = { x = 200.0, z = 120.0 } +command = "airdef deploy s400" +[[step]] +player = { x = 200.0, z = 120.0 } +command = "airdef deploy patriot" +[[step]] +player = { x = 200.0, z = 120.0 } +command = "airdef deploy nasams" +[[step]] +player = { x = 200.0, z = 120.0 } +command = "airdef deploy pantsir" +[[step]] +player = { x = 240.0, z = 120.0 } +command = "airdef deploy s400" +[[step]] +player = { x = 240.0, z = 120.0 } +command = "airdef deploy patriot" +[[step]] +player = { x = 240.0, z = 120.0 } +command = "airdef deploy nasams" +[[step]] +player = { x = 240.0, z = 120.0 } +command = "airdef deploy pantsir" +[[step]] +player = { x = 280.0, z = 120.0 } +command = "airdef deploy s400" +[[step]] +player = { x = 280.0, z = 120.0 } +command = "airdef deploy patriot" +[[step]] +player = { x = 280.0, z = 120.0 } +command = "airdef deploy nasams" +[[step]] +player = { x = 280.0, z = 120.0 } +command = "airdef deploy pantsir" +[[step]] +player = { x = 0.0, z = 160.0 } +command = "airdef deploy s400" +[[step]] +player = { x = 0.0, z = 160.0 } +command = "airdef deploy patriot" +[[step]] +player = { x = 0.0, z = 160.0 } +command = "airdef deploy nasams" +[[step]] +player = { x = 0.0, z = 160.0 } +command = "airdef deploy pantsir" +[[step]] +player = { x = 40.0, z = 160.0 } +command = "airdef deploy s400" +[[step]] +player = { x = 40.0, z = 160.0 } +command = "airdef deploy patriot" +[[step]] +player = { x = 40.0, z = 160.0 } +command = "airdef deploy nasams" +[[step]] +player = { x = 40.0, z = 160.0 } +command = "airdef deploy pantsir" +[[step]] +player = { x = 80.0, z = 160.0 } +command = "airdef deploy s400" +[[step]] +player = { x = 80.0, z = 160.0 } +command = "airdef deploy patriot" +[[step]] +player = { x = 80.0, z = 160.0 } +command = "airdef deploy nasams" +[[step]] +player = { x = 80.0, z = 160.0 } +command = "airdef deploy pantsir" +[[step]] +player = { x = 120.0, z = 160.0 } +command = "airdef deploy s400" +[[step]] +player = { x = 120.0, z = 160.0 } +command = "airdef deploy patriot" +[[step]] +player = { x = 120.0, z = 160.0 } +command = "airdef deploy nasams" +[[step]] +player = { x = 120.0, z = 160.0 } +command = "airdef deploy pantsir" +[[step]] +player = { x = 160.0, z = 160.0 } +command = "airdef deploy s400" +[[step]] +player = { x = 160.0, z = 160.0 } +command = "airdef deploy patriot" +[[step]] +player = { x = 160.0, z = 160.0 } +command = "airdef deploy nasams" +[[step]] +player = { x = 160.0, z = 160.0 } +command = "airdef deploy pantsir" +[[step]] +player = { x = 200.0, z = 160.0 } +command = "airdef deploy s400" +[[step]] +player = { x = 200.0, z = 160.0 } +command = "airdef deploy patriot" +[[step]] +player = { x = 200.0, z = 160.0 } +command = "airdef deploy nasams" +[[step]] +player = { x = 200.0, z = 160.0 } +command = "airdef deploy pantsir" +[[step]] +player = { x = 240.0, z = 160.0 } +command = "airdef deploy s400" +[[step]] +player = { x = 240.0, z = 160.0 } +command = "airdef deploy patriot" +[[step]] +player = { x = 240.0, z = 160.0 } +command = "airdef deploy nasams" +[[step]] +player = { x = 240.0, z = 160.0 } +command = "airdef deploy pantsir" +[[step]] +player = { x = 280.0, z = 160.0 } +command = "airdef deploy s400" +[[step]] +player = { x = 280.0, z = 160.0 } +command = "airdef deploy patriot" +[[step]] +player = { x = 280.0, z = 160.0 } +command = "airdef deploy nasams" +[[step]] +player = { x = 280.0, z = 160.0 } +command = "airdef deploy pantsir" +[[step]] +command = "airdef ew on" +[[step]] +command = "airdef wave mix 10 300" +[[step]] +ticks = 100 +frames = 5 +[[step]] +ticks = 50 +frames = 5 +[[step]] +ticks = 50 +frames = 5 +[[step]] +ticks = 50 +frames = 5 +[[step]] +ticks = 50 +frames = 5 +[[step]] +ticks = 50 +frames = 5 +[[step]] +ticks = 50 +frames = 5 +[[step]] +ticks = 50 +frames = 5 +[[step]] +ticks = 50 +frames = 5 +[[step]] +ticks = 50 +frames = 5 +[[step]] +ticks = 50 +frames = 5 +[[step]] +ticks = 50 +frames = 5 +[[step]] +ticks = 50 +frames = 5 +[[step]] +ticks = 50 +frames = 5 +[[step]] +ticks = 50 +frames = 5 +[[step]] +ticks = 50 +frames = 5 +[[step]] +ticks = 50 +frames = 5 +[[step]] +command = "airdef ew on" +[[step]] +ticks = 50 +frames = 5 +[[step]] +ticks = 50 +frames = 5 +[[step]] +ticks = 50 +frames = 5 +[[step]] +ticks = 50 +frames = 5 +[[step]] +ticks = 50 +frames = 5 +[[step]] +ticks = 50 +frames = 5 +[[step]] +ticks = 50 +frames = 5 +[[step]] +ticks = 50 +frames = 5 +[[step]] +ticks = 50 +frames = 5 +[[step]] +ticks = 50 +frames = 5 +[[step]] +ticks = 50 +frames = 5 +[[step]] +ticks = 50 +frames = 5 +[[step]] +ticks = 50 +frames = 5 +[[step]] +ticks = 50 +frames = 5 +[[step]] +ticks = 50 +frames = 5 +[[step]] +ticks = 50 +frames = 5 +[[step]] +command = "airdef ew on" +[[step]] +ticks = 50 +frames = 5 +[[step]] +ticks = 50 +frames = 5 +[[step]] +ticks = 50 +frames = 5 +[[step]] +ticks = 50 +frames = 5 +[[step]] +ticks = 50 +frames = 5 +[[step]] +ticks = 50 +frames = 5 +[[step]] +ticks = 50 +frames = 5 +[[step]] +ticks = 50 +frames = 5 +[[step]] +ticks = 50 +frames = 5 +[[step]] +ticks = 50 +frames = 5 +[[step]] +ticks = 50 +frames = 5 +[[step]] +ticks = 50 +frames = 5 +[[step]] +ticks = 50 +frames = 5 +[[step]] +ticks = 50 +frames = 5 +[[step]] +ticks = 50 +frames = 5 +[[step]] +ticks = 50 +frames = 5 +[[step]] +ticks = 50 +frames = 5 +[[step]] +ticks = 50 +frames = 5 +[[step]] +command = "airdef stats" +expect.feedback = "В воздухе: 0, ракет: 0, ПВО: 172, сбито: 308, поражений: 87, разбились о препятствия: 0; удары по ПУ: пущено 0, уничтожено 0, подавлено 0, промахов 0, сбито ПВО 0; мир: 8156 секций (382 КБ), запросов 16075, неизвестно 0, лучей 87524 (ударов 1830, неизв. 1567), клеток 1318224; взрывы в блоках: 239 (подтверждено 0, без перемен 37, нет чанка 202), чужих ответов 0" +[[step]] +command = "airdef wave mix 10 300" +[[step]] +ticks = 50 +frames = 5 +[[step]] +ticks = 50 +frames = 5 +[[step]] +ticks = 50 +frames = 5 +[[step]] +ticks = 50 +frames = 5 +[[step]] +ticks = 50 +frames = 5 +[[step]] +ticks = 50 +frames = 5 +[[step]] +ticks = 50 +frames = 5 +[[step]] +ticks = 50 +frames = 5 +[[step]] +ticks = 50 +frames = 5 +[[step]] +ticks = 50 +frames = 5 +[[step]] +ticks = 50 +frames = 5 +[[step]] +ticks = 50 +frames = 5 +[[step]] +ticks = 50 +frames = 5 +[[step]] +ticks = 50 +frames = 5 +[[step]] +ticks = 50 +frames = 5 +[[step]] +ticks = 50 +frames = 5 +[[step]] +ticks = 50 +frames = 5 +[[step]] +ticks = 50 +frames = 5 +[[step]] +ticks = 50 +frames = 5 +[[step]] +ticks = 50 +frames = 5 +[[step]] +ticks = 50 +frames = 5 +[[step]] +ticks = 50 +frames = 5 +[[step]] +ticks = 50 +frames = 5 +[[step]] +ticks = 50 +frames = 5 +[[step]] +command = "airdef stats" +expect.feedback = "В воздухе: 0, ракет: 0, ПВО: 172, сбито: 309, поражений: 87, разбились о препятствия: 0; удары по ПУ: пущено 0, уничтожено 0, подавлено 0, промахов 0, сбито ПВО 0; мир: 8557 секций (401 КБ), запросов 16511, неизвестно 0, лучей 89230 (ударов 2855, неизв. 2039), клеток 1326528; взрывы в блоках: 239 (подтверждено 0, без перемен 37, нет чанка 202), чужих ответов 0" diff --git a/docs/measurements/issue-122-ui-fuel/scenario-before.toml b/docs/measurements/issue-122-ui-fuel/scenario-before.toml new file mode 100644 index 0000000..e13f160 --- /dev/null +++ b/docs/measurements/issue-122-ui-fuel/scenario-before.toml @@ -0,0 +1,688 @@ +[[step]] +render-ground-y = 64 +ticks = 2 +frames = 2 +expect.commands = ["airdef"] +[[step]] +player = { x = 0.0, z = 0.0 } +command = "airdef deploy s400" +[[step]] +player = { x = 0.0, z = 0.0 } +command = "airdef deploy patriot" +[[step]] +player = { x = 0.0, z = 0.0 } +command = "airdef deploy nasams" +[[step]] +player = { x = 0.0, z = 0.0 } +command = "airdef deploy pantsir" +[[step]] +player = { x = 0.0, z = 0.0 } +command = "airdef deploy ew" +[[step]] +player = { x = 40.0, z = 0.0 } +command = "airdef deploy s400" +[[step]] +player = { x = 40.0, z = 0.0 } +command = "airdef deploy patriot" +[[step]] +player = { x = 40.0, z = 0.0 } +command = "airdef deploy nasams" +[[step]] +player = { x = 40.0, z = 0.0 } +command = "airdef deploy pantsir" +[[step]] +player = { x = 40.0, z = 0.0 } +command = "airdef deploy ew" +[[step]] +player = { x = 80.0, z = 0.0 } +command = "airdef deploy s400" +[[step]] +player = { x = 80.0, z = 0.0 } +command = "airdef deploy patriot" +[[step]] +player = { x = 80.0, z = 0.0 } +command = "airdef deploy nasams" +[[step]] +player = { x = 80.0, z = 0.0 } +command = "airdef deploy pantsir" +[[step]] +player = { x = 80.0, z = 0.0 } +command = "airdef deploy ew" +[[step]] +player = { x = 120.0, z = 0.0 } +command = "airdef deploy s400" +[[step]] +player = { x = 120.0, z = 0.0 } +command = "airdef deploy patriot" +[[step]] +player = { x = 120.0, z = 0.0 } +command = "airdef deploy nasams" +[[step]] +player = { x = 120.0, z = 0.0 } +command = "airdef deploy pantsir" +[[step]] +player = { x = 120.0, z = 0.0 } +command = "airdef deploy ew" +[[step]] +player = { x = 160.0, z = 0.0 } +command = "airdef deploy s400" +[[step]] +player = { x = 160.0, z = 0.0 } +command = "airdef deploy patriot" +[[step]] +player = { x = 160.0, z = 0.0 } +command = "airdef deploy nasams" +[[step]] +player = { x = 160.0, z = 0.0 } +command = "airdef deploy pantsir" +[[step]] +player = { x = 160.0, z = 0.0 } +command = "airdef deploy ew" +[[step]] +player = { x = 200.0, z = 0.0 } +command = "airdef deploy s400" +[[step]] +player = { x = 200.0, z = 0.0 } +command = "airdef deploy patriot" +[[step]] +player = { x = 200.0, z = 0.0 } +command = "airdef deploy nasams" +[[step]] +player = { x = 200.0, z = 0.0 } +command = "airdef deploy pantsir" +[[step]] +player = { x = 200.0, z = 0.0 } +command = "airdef deploy ew" +[[step]] +player = { x = 240.0, z = 0.0 } +command = "airdef deploy s400" +[[step]] +player = { x = 240.0, z = 0.0 } +command = "airdef deploy patriot" +[[step]] +player = { x = 240.0, z = 0.0 } +command = "airdef deploy nasams" +[[step]] +player = { x = 240.0, z = 0.0 } +command = "airdef deploy pantsir" +[[step]] +player = { x = 240.0, z = 0.0 } +command = "airdef deploy ew" +[[step]] +player = { x = 280.0, z = 0.0 } +command = "airdef deploy s400" +[[step]] +player = { x = 280.0, z = 0.0 } +command = "airdef deploy patriot" +[[step]] +player = { x = 280.0, z = 0.0 } +command = "airdef deploy nasams" +[[step]] +player = { x = 280.0, z = 0.0 } +command = "airdef deploy pantsir" +[[step]] +player = { x = 280.0, z = 0.0 } +command = "airdef deploy ew" +[[step]] +player = { x = 0.0, z = 40.0 } +command = "airdef deploy s400" +[[step]] +player = { x = 0.0, z = 40.0 } +command = "airdef deploy patriot" +[[step]] +player = { x = 0.0, z = 40.0 } +command = "airdef deploy nasams" +[[step]] +player = { x = 0.0, z = 40.0 } +command = "airdef deploy pantsir" +[[step]] +player = { x = 0.0, z = 40.0 } +command = "airdef deploy ew" +[[step]] +player = { x = 40.0, z = 40.0 } +command = "airdef deploy s400" +[[step]] +player = { x = 40.0, z = 40.0 } +command = "airdef deploy patriot" +[[step]] +player = { x = 40.0, z = 40.0 } +command = "airdef deploy nasams" +[[step]] +player = { x = 40.0, z = 40.0 } +command = "airdef deploy pantsir" +[[step]] +player = { x = 40.0, z = 40.0 } +command = "airdef deploy ew" +[[step]] +player = { x = 80.0, z = 40.0 } +command = "airdef deploy s400" +[[step]] +player = { x = 80.0, z = 40.0 } +command = "airdef deploy patriot" +[[step]] +player = { x = 80.0, z = 40.0 } +command = "airdef deploy nasams" +[[step]] +player = { x = 80.0, z = 40.0 } +command = "airdef deploy pantsir" +[[step]] +player = { x = 80.0, z = 40.0 } +command = "airdef deploy ew" +[[step]] +player = { x = 120.0, z = 40.0 } +command = "airdef deploy s400" +[[step]] +player = { x = 120.0, z = 40.0 } +command = "airdef deploy patriot" +[[step]] +player = { x = 120.0, z = 40.0 } +command = "airdef deploy nasams" +[[step]] +player = { x = 120.0, z = 40.0 } +command = "airdef deploy pantsir" +[[step]] +player = { x = 120.0, z = 40.0 } +command = "airdef deploy ew" +[[step]] +player = { x = 160.0, z = 40.0 } +command = "airdef deploy s400" +[[step]] +player = { x = 160.0, z = 40.0 } +command = "airdef deploy patriot" +[[step]] +player = { x = 160.0, z = 40.0 } +command = "airdef deploy nasams" +[[step]] +player = { x = 160.0, z = 40.0 } +command = "airdef deploy pantsir" +[[step]] +player = { x = 200.0, z = 40.0 } +command = "airdef deploy s400" +[[step]] +player = { x = 200.0, z = 40.0 } +command = "airdef deploy patriot" +[[step]] +player = { x = 200.0, z = 40.0 } +command = "airdef deploy nasams" +[[step]] +player = { x = 200.0, z = 40.0 } +command = "airdef deploy pantsir" +[[step]] +player = { x = 240.0, z = 40.0 } +command = "airdef deploy s400" +[[step]] +player = { x = 240.0, z = 40.0 } +command = "airdef deploy patriot" +[[step]] +player = { x = 240.0, z = 40.0 } +command = "airdef deploy nasams" +[[step]] +player = { x = 240.0, z = 40.0 } +command = "airdef deploy pantsir" +[[step]] +player = { x = 280.0, z = 40.0 } +command = "airdef deploy s400" +[[step]] +player = { x = 280.0, z = 40.0 } +command = "airdef deploy patriot" +[[step]] +player = { x = 280.0, z = 40.0 } +command = "airdef deploy nasams" +[[step]] +player = { x = 280.0, z = 40.0 } +command = "airdef deploy pantsir" +[[step]] +player = { x = 0.0, z = 80.0 } +command = "airdef deploy s400" +[[step]] +player = { x = 0.0, z = 80.0 } +command = "airdef deploy patriot" +[[step]] +player = { x = 0.0, z = 80.0 } +command = "airdef deploy nasams" +[[step]] +player = { x = 0.0, z = 80.0 } +command = "airdef deploy pantsir" +[[step]] +player = { x = 40.0, z = 80.0 } +command = "airdef deploy s400" +[[step]] +player = { x = 40.0, z = 80.0 } +command = "airdef deploy patriot" +[[step]] +player = { x = 40.0, z = 80.0 } +command = "airdef deploy nasams" +[[step]] +player = { x = 40.0, z = 80.0 } +command = "airdef deploy pantsir" +[[step]] +player = { x = 80.0, z = 80.0 } +command = "airdef deploy s400" +[[step]] +player = { x = 80.0, z = 80.0 } +command = "airdef deploy patriot" +[[step]] +player = { x = 80.0, z = 80.0 } +command = "airdef deploy nasams" +[[step]] +player = { x = 80.0, z = 80.0 } +command = "airdef deploy pantsir" +[[step]] +player = { x = 120.0, z = 80.0 } +command = "airdef deploy s400" +[[step]] +player = { x = 120.0, z = 80.0 } +command = "airdef deploy patriot" +[[step]] +player = { x = 120.0, z = 80.0 } +command = "airdef deploy nasams" +[[step]] +player = { x = 120.0, z = 80.0 } +command = "airdef deploy pantsir" +[[step]] +player = { x = 160.0, z = 80.0 } +command = "airdef deploy s400" +[[step]] +player = { x = 160.0, z = 80.0 } +command = "airdef deploy patriot" +[[step]] +player = { x = 160.0, z = 80.0 } +command = "airdef deploy nasams" +[[step]] +player = { x = 160.0, z = 80.0 } +command = "airdef deploy pantsir" +[[step]] +player = { x = 200.0, z = 80.0 } +command = "airdef deploy s400" +[[step]] +player = { x = 200.0, z = 80.0 } +command = "airdef deploy patriot" +[[step]] +player = { x = 200.0, z = 80.0 } +command = "airdef deploy nasams" +[[step]] +player = { x = 200.0, z = 80.0 } +command = "airdef deploy pantsir" +[[step]] +player = { x = 240.0, z = 80.0 } +command = "airdef deploy s400" +[[step]] +player = { x = 240.0, z = 80.0 } +command = "airdef deploy patriot" +[[step]] +player = { x = 240.0, z = 80.0 } +command = "airdef deploy nasams" +[[step]] +player = { x = 240.0, z = 80.0 } +command = "airdef deploy pantsir" +[[step]] +player = { x = 280.0, z = 80.0 } +command = "airdef deploy s400" +[[step]] +player = { x = 280.0, z = 80.0 } +command = "airdef deploy patriot" +[[step]] +player = { x = 280.0, z = 80.0 } +command = "airdef deploy nasams" +[[step]] +player = { x = 280.0, z = 80.0 } +command = "airdef deploy pantsir" +[[step]] +player = { x = 0.0, z = 120.0 } +command = "airdef deploy s400" +[[step]] +player = { x = 0.0, z = 120.0 } +command = "airdef deploy patriot" +[[step]] +player = { x = 0.0, z = 120.0 } +command = "airdef deploy nasams" +[[step]] +player = { x = 0.0, z = 120.0 } +command = "airdef deploy pantsir" +[[step]] +player = { x = 40.0, z = 120.0 } +command = "airdef deploy s400" +[[step]] +player = { x = 40.0, z = 120.0 } +command = "airdef deploy patriot" +[[step]] +player = { x = 40.0, z = 120.0 } +command = "airdef deploy nasams" +[[step]] +player = { x = 40.0, z = 120.0 } +command = "airdef deploy pantsir" +[[step]] +player = { x = 80.0, z = 120.0 } +command = "airdef deploy s400" +[[step]] +player = { x = 80.0, z = 120.0 } +command = "airdef deploy patriot" +[[step]] +player = { x = 80.0, z = 120.0 } +command = "airdef deploy nasams" +[[step]] +player = { x = 80.0, z = 120.0 } +command = "airdef deploy pantsir" +[[step]] +player = { x = 120.0, z = 120.0 } +command = "airdef deploy s400" +[[step]] +player = { x = 120.0, z = 120.0 } +command = "airdef deploy patriot" +[[step]] +player = { x = 120.0, z = 120.0 } +command = "airdef deploy nasams" +[[step]] +player = { x = 120.0, z = 120.0 } +command = "airdef deploy pantsir" +[[step]] +player = { x = 160.0, z = 120.0 } +command = "airdef deploy s400" +[[step]] +player = { x = 160.0, z = 120.0 } +command = "airdef deploy patriot" +[[step]] +player = { x = 160.0, z = 120.0 } +command = "airdef deploy nasams" +[[step]] +player = { x = 160.0, z = 120.0 } +command = "airdef deploy pantsir" +[[step]] +player = { x = 200.0, z = 120.0 } +command = "airdef deploy s400" +[[step]] +player = { x = 200.0, z = 120.0 } +command = "airdef deploy patriot" +[[step]] +player = { x = 200.0, z = 120.0 } +command = "airdef deploy nasams" +[[step]] +player = { x = 200.0, z = 120.0 } +command = "airdef deploy pantsir" +[[step]] +player = { x = 240.0, z = 120.0 } +command = "airdef deploy s400" +[[step]] +player = { x = 240.0, z = 120.0 } +command = "airdef deploy patriot" +[[step]] +player = { x = 240.0, z = 120.0 } +command = "airdef deploy nasams" +[[step]] +player = { x = 240.0, z = 120.0 } +command = "airdef deploy pantsir" +[[step]] +player = { x = 280.0, z = 120.0 } +command = "airdef deploy s400" +[[step]] +player = { x = 280.0, z = 120.0 } +command = "airdef deploy patriot" +[[step]] +player = { x = 280.0, z = 120.0 } +command = "airdef deploy nasams" +[[step]] +player = { x = 280.0, z = 120.0 } +command = "airdef deploy pantsir" +[[step]] +player = { x = 0.0, z = 160.0 } +command = "airdef deploy s400" +[[step]] +player = { x = 0.0, z = 160.0 } +command = "airdef deploy patriot" +[[step]] +player = { x = 0.0, z = 160.0 } +command = "airdef deploy nasams" +[[step]] +player = { x = 0.0, z = 160.0 } +command = "airdef deploy pantsir" +[[step]] +player = { x = 40.0, z = 160.0 } +command = "airdef deploy s400" +[[step]] +player = { x = 40.0, z = 160.0 } +command = "airdef deploy patriot" +[[step]] +player = { x = 40.0, z = 160.0 } +command = "airdef deploy nasams" +[[step]] +player = { x = 40.0, z = 160.0 } +command = "airdef deploy pantsir" +[[step]] +player = { x = 80.0, z = 160.0 } +command = "airdef deploy s400" +[[step]] +player = { x = 80.0, z = 160.0 } +command = "airdef deploy patriot" +[[step]] +player = { x = 80.0, z = 160.0 } +command = "airdef deploy nasams" +[[step]] +player = { x = 80.0, z = 160.0 } +command = "airdef deploy pantsir" +[[step]] +player = { x = 120.0, z = 160.0 } +command = "airdef deploy s400" +[[step]] +player = { x = 120.0, z = 160.0 } +command = "airdef deploy patriot" +[[step]] +player = { x = 120.0, z = 160.0 } +command = "airdef deploy nasams" +[[step]] +player = { x = 120.0, z = 160.0 } +command = "airdef deploy pantsir" +[[step]] +player = { x = 160.0, z = 160.0 } +command = "airdef deploy s400" +[[step]] +player = { x = 160.0, z = 160.0 } +command = "airdef deploy patriot" +[[step]] +player = { x = 160.0, z = 160.0 } +command = "airdef deploy nasams" +[[step]] +player = { x = 160.0, z = 160.0 } +command = "airdef deploy pantsir" +[[step]] +player = { x = 200.0, z = 160.0 } +command = "airdef deploy s400" +[[step]] +player = { x = 200.0, z = 160.0 } +command = "airdef deploy patriot" +[[step]] +player = { x = 200.0, z = 160.0 } +command = "airdef deploy nasams" +[[step]] +player = { x = 200.0, z = 160.0 } +command = "airdef deploy pantsir" +[[step]] +player = { x = 240.0, z = 160.0 } +command = "airdef deploy s400" +[[step]] +player = { x = 240.0, z = 160.0 } +command = "airdef deploy patriot" +[[step]] +player = { x = 240.0, z = 160.0 } +command = "airdef deploy nasams" +[[step]] +player = { x = 240.0, z = 160.0 } +command = "airdef deploy pantsir" +[[step]] +player = { x = 280.0, z = 160.0 } +command = "airdef deploy s400" +[[step]] +player = { x = 280.0, z = 160.0 } +command = "airdef deploy patriot" +[[step]] +player = { x = 280.0, z = 160.0 } +command = "airdef deploy nasams" +[[step]] +player = { x = 280.0, z = 160.0 } +command = "airdef deploy pantsir" +[[step]] +command = "airdef ew on" +[[step]] +command = "airdef wave mix 10 300" +[[step]] +ticks = 100 +frames = 5 +[[step]] +ticks = 50 +frames = 5 +[[step]] +ticks = 50 +frames = 5 +[[step]] +ticks = 50 +frames = 5 +[[step]] +ticks = 50 +frames = 5 +[[step]] +ticks = 50 +frames = 5 +[[step]] +ticks = 50 +frames = 5 +[[step]] +ticks = 50 +frames = 5 +[[step]] +ticks = 50 +frames = 5 +[[step]] +ticks = 50 +frames = 5 +[[step]] +ticks = 50 +frames = 5 +[[step]] +ticks = 50 +frames = 5 +[[step]] +ticks = 50 +frames = 5 +[[step]] +ticks = 50 +frames = 5 +[[step]] +ticks = 50 +frames = 5 +[[step]] +ticks = 50 +frames = 5 +[[step]] +ticks = 50 +frames = 5 +[[step]] +command = "airdef ew on" +[[step]] +ticks = 50 +frames = 5 +[[step]] +ticks = 50 +frames = 5 +[[step]] +ticks = 50 +frames = 5 +[[step]] +ticks = 50 +frames = 5 +[[step]] +ticks = 50 +frames = 5 +[[step]] +ticks = 50 +frames = 5 +[[step]] +ticks = 50 +frames = 5 +[[step]] +ticks = 50 +frames = 5 +[[step]] +ticks = 50 +frames = 5 +[[step]] +ticks = 50 +frames = 5 +[[step]] +ticks = 50 +frames = 5 +[[step]] +ticks = 50 +frames = 5 +[[step]] +ticks = 50 +frames = 5 +[[step]] +ticks = 50 +frames = 5 +[[step]] +ticks = 50 +frames = 5 +[[step]] +ticks = 50 +frames = 5 +[[step]] +command = "airdef ew on" +[[step]] +ticks = 50 +frames = 5 +[[step]] +ticks = 50 +frames = 5 +[[step]] +ticks = 50 +frames = 5 +[[step]] +ticks = 50 +frames = 5 +[[step]] +ticks = 50 +frames = 5 +[[step]] +ticks = 50 +frames = 5 +[[step]] +ticks = 50 +frames = 5 +[[step]] +ticks = 50 +frames = 5 +[[step]] +ticks = 50 +frames = 5 +[[step]] +ticks = 50 +frames = 5 +[[step]] +ticks = 50 +frames = 5 +[[step]] +ticks = 50 +frames = 5 +[[step]] +ticks = 50 +frames = 5 +[[step]] +ticks = 50 +frames = 5 +[[step]] +ticks = 50 +frames = 5 +[[step]] +ticks = 50 +frames = 5 +[[step]] +ticks = 50 +frames = 5 +[[step]] +ticks = 50 +frames = 5 +[[step]] +command = "airdef stats" +expect.feedback = "В воздухе: 0, ракет: 0, ПВО: 172, сбито: 308, поражений: 87, разбились о препятствия: 0; удары по ПУ: пущено 0, уничтожено 0, подавлено 0, промахов 0, сбито ПВО 0; мир: 8156 секций (382 КБ), запросов 16075, неизвестно 0, лучей 87524 (ударов 1830, неизв. 1567), клеток 1318224; взрывы в блоках: 239 (подтверждено 0, без перемен 37, нет чанка 202), чужих ответов 0" +[[step]] +command = "airdef wave mix 10 300" +expect.stopped = "exceeded its execution limit" diff --git a/docs/measurements/issue-122-ui-fuel/summary.json b/docs/measurements/issue-122-ui-fuel/summary.json new file mode 100644 index 0000000..7aed2b9 --- /dev/null +++ b/docs/measurements/issue-122-ui-fuel/summary.json @@ -0,0 +1,67 @@ +{ + "schema": 1, + "issue": 122, + "kind": "diagnostic_guest_ui_path_comparison_not_exact_user_session", + "user_report": { + "log": "Horizon: mod airdef stopped in UI event (fuel budget 1000000000, remaining Some(0))", + "backtrace": "airdef_core::sim::ew::Sim::known_units <- sim::raid::Sim::order_raid <- airdef::AirDef::launch_wave <- horizon_mod_sdk::Mod::ui_select <- ui-events select", + "windows_session_confirmed_same_cause": false + }, + "root_cause": { + "what": "order_raid recomputed the planner snapshot known_units() for every drone (choose_target), and known_units() itself rescanned the whole force and the dedup list per intel entry", + "growth": "drones x intel_log x force_size per UI event; the intel log holds every launch, EW disclosure, and radar disclosure of the last 6000 ticks", + "state_at_trap": { + "revealed_entries": 13172, + "known_alive_units": 144, + "sams_total": 172 + } + }, + "fix": { + "description": "one known_units snapshot per order_raid passed to radar_strikes and choose_target; known_units is a single O(R+S) set pass; price_grid_on reuses its snapshot", + "semantics": "identical planner results; native equivalence test against the former predicate; WASM checkpoint /airdef stats byte-identical before/after at tick 2602", + "limits_unchanged": true, + "wave_size_unrestricted": true, + "hd_record_untouched": true + }, + "setup": { + "defenses": "40 spread clusters x 4 heavy SAMs + 12 EW = 172, player moved per deploy step", + "first_raid": "airdef wave mix 10 (111 drones, Smart, AllSides) via the diagnostic UI-path trigger", + "second_raid": "same, at tick ~2600 after the first raid is fought off", + "note": "stacked single-point layouts die to one warhead (known=0, cheap order); the trap needs a surviving force, like the user's session" + }, + "before": { + "source": "e6a7000 + trigger.patch (diagnostic guest only)", + "second_order_111_drones": { + "callback": "command (the diagnostic trigger invokes the exact ui_select(BTN_LAUNCH) handler; the UI event and the command share the same 1e9 event budget)", + "max_used": 1000000000, + "budget": 1000000000, + "failures": 1, + "mod_stopped": true, + "log": "before-trap.txt" + }, + "scaling_witness_11_drones_same_state": { + "max_used": 247805865, + "budget": 1000000000, + "failures": 0, + "log": "before-scaling.txt" + } + }, + "after": { + "same_scenario_passed": true, + "steps": 255, + "max_command_fuel": 32946177, + "max_tick_fuel": 236581288, + "failures": 0, + "checkpoint_stats_equal_before_after": true, + "log": "after.txt" + }, + "artifacts": { + "trigger_patch": "trigger.patch", + "scenario_before": "scenario-before.toml", + "scenario_after": "scenario-after.toml", + "before_guest_sha256": "6d4525cd7aaa61dbf344593f9819be1b61e06654126e9e9088e57967dc657365", + "after_guest_sha256": "c4ef7d759524eb053ccd55f0eccdcb49c91954201eb4e0ad923ee9b2828efc4e", + "scenario_before_sha256": "398d099275e396deab269601e5adbcf325ab5e0ea6b540c1d03a477d6b0963d1", + "scenario_after_sha256": "3bc27338d2e6fb433f027a0cf1afc39ff9a94e1af5840454082af6d8d15aad7c" + } +} diff --git a/docs/measurements/issue-122-ui-fuel/trigger.patch b/docs/measurements/issue-122-ui-fuel/trigger.patch new file mode 100644 index 0000000..91ee1e6 --- /dev/null +++ b/docs/measurements/issue-122-ui-fuel/trigger.patch @@ -0,0 +1,23 @@ +diff --git a/airdef-mod/src/lib.rs b/airdef-mod/src/lib.rs +index dc6585eb3..73864e41e 100644 +--- a/airdef-mod/src/lib.rs ++++ b/airdef-mod/src/lib.rs +@@ -2351,8 +2351,16 @@ impl Mod for AirDef { + .nearest_target(DVec3::new(p.x, p.y, p.z)) + .unwrap_or(DVec3::new(p.x, p.y, p.z)); + if *kind == "mix" { +- let objects = self.objects(DVec3::new(p.x, p.y, p.z)); +- self.sim.order_raid(&wave::WaveConfig::mix(count, dist), &objects, &mut out); ++ // DIAGNOSTIC ONLY (issue #122): drive the exact UI launch path ++ // (Page::WaveGo, BTN_LAUNCH) with the maximum menu composition. ++ let mut wave = wave::WaveConfig::default(); ++ wave.counts = [40; wave::KINDS.len()]; ++ wave.dist = dist; ++ wave.approach = sim::Approach::AllSides; ++ wave.aim = sim::Aim::Smart; ++ self.wave = wave; ++ self.menu = Some(Page::WaveGo); ++ self.ui_select(BTN_LAUNCH, 0); + } else { + let kind = DroneKind::from_key(kind).ok_or( + "Тип: shahed, geran3, lancet, gerbera, kh101, kalibr, kh22, zircon, iskander, kinzhal, oreshnik или mix", diff --git a/tools/airdef.py b/tools/airdef.py index 0238756..f8ad24d 100644 --- a/tools/airdef.py +++ b/tools/airdef.py @@ -158,7 +158,7 @@ def test_wasm(): BUDGET_SCENARIO = "tests/budgets/wasm_flat_budget.toml" -BUDGET_ASSERTED_SHA256 = "bcc0359289ab20ab3de608bcc130f72d2cf88f9f6fbc470d7b6846e9db5d11df" +BUDGET_ASSERTED_SHA256 = "134cb4667691cf9faa639ed37120941d829cfc13ce45e9638582c409d9e0dd29" def fuel_measurements(output): @@ -280,8 +280,10 @@ def test_budgets(): report["status"] = "failed" raise RuntimeError("pinned runtime did not return requested callback fuel counters") # These assertions belong to this exact fixture, not to arbitrary edited scripts. + # Two `airdef wave oreshnik` orders plus two Smart orders through the UI launch + # path (`airdef preset` + `airdef raid`, the wave screen's ПУСК handler, #122). if result.returncode == 0 and report["scenario_sha256"] == BUDGET_ASSERTED_SHA256: - counts["accepted_raids_asserted"] = 2 + counts["accepted_raids_asserted"] = 4 counts["snapshot_airborne_asserted"] = 184 counts["snapshot_tick"] = 2102 except Exception as exc: diff --git a/tools/test_budget_report.py b/tools/test_budget_report.py index 33b68f2..25f6ce2 100644 --- a/tools/test_budget_report.py +++ b/tools/test_budget_report.py @@ -123,7 +123,7 @@ class BudgetReportFailureTests(unittest.TestCase): self.assertEqual(report["scenario"], airdef.BUDGET_SCENARIO) self.assertEqual(report["scenario_render_world"], "flat at y=64") counts = report["scenario_counts"] - self.assertEqual((counts["accepted_raids_asserted"], counts["snapshot_airborne_asserted"], counts["snapshot_tick"]), (2, 184, 2102)) + self.assertEqual((counts["accepted_raids_asserted"], counts["snapshot_airborne_asserted"], counts["snapshot_tick"]), (4, 184, 2102)) self.assertIsNone(counts["peak_airborne_observed"]) self.assertEqual(report["measured_fuel"]["callbacks"], [{ "mod": "airdef", "callback": "tick", "calls": 2102,