Skip to main content

vimp_engine_core/
debug.rs

1//! Курированный дамп мира для отладки (`plan/done/ai-debug/stage_4.md`).
2//! `EngineSim::serialize_state` уже умеет дампить всё — но в сыром
3//! serde-формате rapier, который нечитаем ни человеком, ни нейросетью.
4//! Здесь — второй, отдельный дамп: только то, по чему диагностируют
5//! «танк в стене», «снаряд без коллайдера», «бот не видит нав-граф».
6//!
7//! Формат — стабильный JSON (ключи в camelCase, как в остальных
8//! JSON-мостах ядра). Порядок записей детерминирован (сортировка по
9//! индексу дескриптора/ключу ячейки), иначе два прогона одного сценария
10//! дадут разный текст и диф дампов станет бесполезным.
11
12use rapier2d::prelude::*;
13use serde_json::{Value, json};
14
15use crate::map::GameMap;
16use crate::nav::navigation::NavigationSystem;
17use crate::nav::spatial::SpatialGrid;
18use crate::physics::round2;
19use crate::rng::Rng;
20
21/// Полный дамп движковой половины симуляции.
22#[allow(clippy::too_many_arguments)]
23pub fn engine_json(
24    world: &PhysicsWorld,
25    map: &Option<GameMap>,
26    nav: &Option<NavigationSystem>,
27    spatial: &SpatialGrid,
28    rng: &Rng,
29    accumulator: f32,
30    time_step: f32,
31) -> Value {
32    json!({
33        "bodies": bodies_json(world),
34        "colliders": colliders_json(world),
35        "map": map_json(map),
36        "nav": nav_json(nav),
37        "spatial": spatial_json(spatial),
38        "rng": { "state": rng.state().to_string() },
39        "step": {
40            "timeStep": time_step,
41            "accumulator": accumulator,
42        },
43    })
44}
45
46// Тела: тег движка знает только младший байт user_data (crate::physics),
47// остальное — игровая раскладка, поэтому user_data отдаётся целиком
48// строкой (u128 не влезает в число JSON) — игра восстановит свой gameId.
49fn bodies_json(world: &PhysicsWorld) -> Value {
50    let mut rows: Vec<(u32, Value)> = world
51        .rigid_bodies()
52        .map(|(handle, body)| {
53            let translation = body.translation();
54            let linvel = body.linvel();
55
56            (
57                handle.into_raw_parts().0,
58                json!({
59                    "handle": handle.into_raw_parts().0,
60                    "tag": (body.user_data & 0xff) as u64,
61                    "userData": body.user_data.to_string(),
62                    "translation": [round2(translation.x), round2(translation.y)],
63                    "rotation": round2(body.rotation().angle()),
64                    "linvel": [round2(linvel.x), round2(linvel.y)],
65                    "angvel": round2(body.angvel()),
66                    "mass": round2(body.mass()),
67                    "bodyType": format!("{:?}", body.body_type()),
68                    "ccd": body.is_ccd_enabled(),
69                }),
70            )
71        })
72        .collect();
73
74    rows.sort_by_key(|(index, _)| *index);
75
76    Value::Array(rows.into_iter().map(|(_, row)| row).collect())
77}
78
79// Коллайдеры: маски групп — в hex, потому что в конфигах игр они и
80// задаются битами, а десятичное 4294901762 не сверить глазами.
81fn colliders_json(world: &PhysicsWorld) -> Value {
82    let mut rows: Vec<(u32, Value)> = world
83        .colliders
84        .iter()
85        .map(|(handle, collider)| {
86            let collision = collider.collision_groups();
87            let solver = collider.solver_groups();
88            let mut row = json!({
89                "handle": handle.into_raw_parts().0,
90                "isSensor": collider.is_sensor(),
91                "collisionGroups": groups_hex(collision),
92                "solverGroups": groups_hex(solver),
93                "parent": collider.parent().map(|body| body.into_raw_parts().0),
94            });
95
96            merge(&mut row, shape_json(collider.shape()));
97
98            (handle.into_raw_parts().0, row)
99        })
100        .collect();
101
102    rows.sort_by_key(|(index, _)| *index);
103
104    Value::Array(rows.into_iter().map(|(_, row)| row).collect())
105}
106
107// Форма коллайдера: движок раскрывает только те примитивы, из которых
108// строит тела сам (cuboid статики карты, ball снарядов игр) — прочее
109// остаётся именем типа, дампу этого достаточно.
110fn shape_json(shape: &dyn Shape) -> Value {
111    if let Some(cuboid) = shape.as_cuboid() {
112        let half = cuboid.half_extents;
113
114        return json!({
115            "shape": "cuboid",
116            "halfExtents": [round2(half.x), round2(half.y)],
117        });
118    }
119
120    if let Some(ball) = shape.as_ball() {
121        return json!({ "shape": "ball", "radius": round2(ball.radius) });
122    }
123
124    json!({ "shape": format!("{:?}", shape.shape_type()) })
125}
126
127fn map_json(map: &Option<GameMap>) -> Value {
128    let Some(map) = map else {
129        return Value::Null;
130    };
131
132    let rows = map.grid.len();
133    let cols = map.grid.first().map(|row| row.len()).unwrap_or(0);
134
135    json!({
136        "setId": map.set_id,
137        "step": map.step,
138        "grid": { "rows": rows, "cols": cols },
139        "physicsStatic": map.physics_static,
140        "staticBodies": map.static_body_count(),
141        "dynamicBodies": map.dynamic_body_count(),
142        "respawns": map
143            .respawns
144            .iter()
145            .map(|(team, points)| (team.clone(), Value::from(points.len())))
146            .collect::<serde_json::Map<String, Value>>(),
147    })
148}
149
150fn nav_json(nav: &Option<NavigationSystem>) -> Value {
151    let Some(nav) = nav else {
152        return Value::Null;
153    };
154
155    json!({
156        "nodes": nav.node_count(),
157        "edges": nav.edge_count(),
158        "gridStep": nav.grid_step(),
159    })
160}
161
162fn spatial_json(spatial: &SpatialGrid) -> Value {
163    let cells = spatial.cell_counts();
164
165    json!({
166        "cellSize": spatial.cell_size(),
167        "cells": cells.len(),
168        "entities": cells.iter().map(|(_, _, count)| count).sum::<usize>(),
169        "cellCounts": cells
170            .iter()
171            .map(|(cx, cy, count)| json!({ "cell": [cx, cy], "count": count }))
172            .collect::<Vec<Value>>(),
173    })
174}
175
176fn groups_hex(groups: InteractionGroups) -> String {
177    format!(
178        "0x{:08x}/0x{:08x}",
179        groups.memberships.bits(),
180        groups.filter.bits()
181    )
182}
183
184// дописывает поля формы в объект строки коллайдера
185fn merge(target: &mut Value, extra: Value) {
186    let (Some(target), Some(extra)) = (target.as_object_mut(), extra.as_object()) else {
187        return;
188    };
189
190    for (key, value) in extra {
191        target.insert(key.clone(), value.clone());
192    }
193}