Fix the UI-event fuel trap on a second large raid order
build / mod (push) Failing after 22s

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) <noreply@moonshot.cn>
This commit is contained in:
2026-11-04 21:49:25 +03:00
parent 61aaa0bf35
commit d45629ae29
19 changed files with 1907 additions and 43 deletions
+6
View File
@@ -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::<usize>().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("Нет игрока")?;
+8 -3
View File
@@ -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<u32> = self.sams.iter().filter(|s| s.alive()).map(|s| s.id).collect();
let mut seen: HashSet<u32> = 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
}
+113 -13
View File
@@ -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<Option<(DVec3, u32)>> {
let mut radars: Vec<(u32, DVec3)> = self.known_units().into_iter()
pub(super) fn radar_strikes(&self, kinds: &[(DroneKind, u32)], known: &[(u32, DVec3)]) -> Vec<Option<(DVec3, u32)>> {
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<u32>) {
fn choose_target(&mut self, kind: DroneKind, object: DVec3, aim: Aim, known: &[(u32, DVec3)]) -> (DVec3, Option<u32>) {
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<_>>(), 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);
@@ -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"
+13
View File
@@ -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"
+34 -2
View File
@@ -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 <n>` — 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 не увеличивались.
+21 -1
View File
@@ -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).
+1 -1
View File
@@ -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-статус не меняется после реализации.
+51 -3
View File
@@ -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 и мод остановился.
+47 -17
View File
@@ -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 `<checkout>/horizon/target/debug/cargo-horizon-mod horizon-mod test --hmod <checkout>/airdef-mod/target/horizon-mod/airdef.hmod budget`\nrunning 1 scripts against <checkout>/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"
}
@@ -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
@@ -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
@@ -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
@@ -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"
@@ -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"
@@ -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"
}
}
@@ -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",
+4 -2
View File
@@ -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:
+1 -1
View File
@@ -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,