Share aircraft and item catalog with asset generation
build / mod (push) Failing after 20s

Co-Authored-By: Codex <noreply@openai.com>
This commit is contained in:
2026-11-04 15:54:31 +03:00
parent cb73cb7ccc
commit eea1b3692e
9 changed files with 519 additions and 64 deletions
+1
View File
@@ -35,6 +35,7 @@ jobs:
- name: Проверка generated assets
run: |
python3 -m pip install --user -r tools/requirements.txt
python3 tools/gen_catalog.py --check
python3 tools/gen_models.py --check
python3 -m unittest discover -s tools -p test_model_bounds.py
- name: Тесты ядра без SDK
+2 -31
View File
@@ -52,37 +52,8 @@ impl Unit {
}
use DroneKind::*;
/// The raid entries come first, in the order stored by existing scenario files.
pub const AIRCRAFT: &[Aircraft] = &[
Aircraft { kind: Gerbera, launch_item: None, raid: true },
Aircraft { kind: Shahed, launch_item: Some("airdef:launch_shahed"), raid: true },
Aircraft { kind: Geran3, launch_item: Some("airdef:launch_geran3"), raid: true },
Aircraft { kind: Lancet, launch_item: Some("airdef:launch_lancet"), raid: true },
Aircraft { kind: Kh101, launch_item: Some("airdef:launch_kh101"), raid: true },
Aircraft { kind: Kalibr, launch_item: Some("airdef:launch_kalibr"), raid: true },
Aircraft { kind: Kh22, launch_item: Some("airdef:launch_kh22"), raid: true },
Aircraft { kind: Zircon, launch_item: Some("airdef:launch_zircon"), raid: true },
Aircraft { kind: Iskander, launch_item: Some("airdef:launch_iskander"), raid: true },
Aircraft { kind: Kinzhal, launch_item: Some("airdef:launch_kinzhal"), raid: true },
Aircraft { kind: Oreshnik, launch_item: Some("airdef:launch_oreshnik"), raid: true },
Aircraft { kind: OreshnikBlock, launch_item: None, raid: false },
Aircraft { kind: OreshnikSub, launch_item: None, raid: false },
];
macro_rules! units {
($( $kind:ident => $item:literal ),+ $(,)?) => {
pub const UNITS: &[Unit] = &[$(Unit { kind: SamKind::$kind, deploy_item: $item }),+];
};
}
units! {
Patriot => "airdef:deploy_patriot", Gepard => "airdef:deploy_gepard",
Pantsir => "airdef:deploy_pantsir", S400 => "airdef:deploy_s400",
S500 => "airdef:deploy_s500", IrisT => "airdef:deploy_irist",
Nasams => "airdef:deploy_nasams", Buk => "airdef:deploy_buk",
Tor => "airdef:deploy_tor", P1sun => "airdef:deploy_p1sun",
Radar => "airdef:deploy_radar", Depot => "airdef:deploy_depot",
Tzm => "airdef:deploy_tzm", Ew => "airdef:deploy_ew", Light => "airdef:deploy_light",
}
// Generated entries keep the historical raid and unit ordering.
include!("catalog_entries.rs");
/// Existing positional serialization makes the first entries' order part of the save format.
const fn raid_count() -> usize {
+34
View File
@@ -0,0 +1,34 @@
// Generated by tools/gen_catalog.py from catalog/content.json. Do not edit.
pub const AIRCRAFT: &[Aircraft] = &[
Aircraft { kind: Gerbera, launch_item: None, raid: true },
Aircraft { kind: Shahed, launch_item: Some("airdef:launch_shahed"), raid: true },
Aircraft { kind: Geran3, launch_item: Some("airdef:launch_geran3"), raid: true },
Aircraft { kind: Lancet, launch_item: Some("airdef:launch_lancet"), raid: true },
Aircraft { kind: Kh101, launch_item: Some("airdef:launch_kh101"), raid: true },
Aircraft { kind: Kalibr, launch_item: Some("airdef:launch_kalibr"), raid: true },
Aircraft { kind: Kh22, launch_item: Some("airdef:launch_kh22"), raid: true },
Aircraft { kind: Zircon, launch_item: Some("airdef:launch_zircon"), raid: true },
Aircraft { kind: Iskander, launch_item: Some("airdef:launch_iskander"), raid: true },
Aircraft { kind: Kinzhal, launch_item: Some("airdef:launch_kinzhal"), raid: true },
Aircraft { kind: Oreshnik, launch_item: Some("airdef:launch_oreshnik"), raid: true },
Aircraft { kind: OreshnikBlock, launch_item: None, raid: false },
Aircraft { kind: OreshnikSub, launch_item: None, raid: false },
];
pub const UNITS: &[Unit] = &[
Unit { kind: SamKind::Patriot, deploy_item: "airdef:deploy_patriot" },
Unit { kind: SamKind::Gepard, deploy_item: "airdef:deploy_gepard" },
Unit { kind: SamKind::Pantsir, deploy_item: "airdef:deploy_pantsir" },
Unit { kind: SamKind::S400, deploy_item: "airdef:deploy_s400" },
Unit { kind: SamKind::S500, deploy_item: "airdef:deploy_s500" },
Unit { kind: SamKind::IrisT, deploy_item: "airdef:deploy_irist" },
Unit { kind: SamKind::Nasams, deploy_item: "airdef:deploy_nasams" },
Unit { kind: SamKind::Buk, deploy_item: "airdef:deploy_buk" },
Unit { kind: SamKind::Tor, deploy_item: "airdef:deploy_tor" },
Unit { kind: SamKind::P1sun, deploy_item: "airdef:deploy_p1sun" },
Unit { kind: SamKind::Radar, deploy_item: "airdef:deploy_radar" },
Unit { kind: SamKind::Depot, deploy_item: "airdef:deploy_depot" },
Unit { kind: SamKind::Tzm, deploy_item: "airdef:deploy_tzm" },
Unit { kind: SamKind::Ew, deploy_item: "airdef:deploy_ew" },
Unit { kind: SamKind::Light, deploy_item: "airdef:deploy_light" },
];
+394
View File
@@ -0,0 +1,394 @@
{
"aircraft": [
{
"kind": "Gerbera",
"item": null,
"raid": true
},
{
"kind": "Shahed",
"item": "launch_shahed",
"raid": true
},
{
"kind": "Geran3",
"item": "launch_geran3",
"raid": true
},
{
"kind": "Lancet",
"item": "launch_lancet",
"raid": true
},
{
"kind": "Kh101",
"item": "launch_kh101",
"raid": true
},
{
"kind": "Kalibr",
"item": "launch_kalibr",
"raid": true
},
{
"kind": "Kh22",
"item": "launch_kh22",
"raid": true
},
{
"kind": "Zircon",
"item": "launch_zircon",
"raid": true
},
{
"kind": "Iskander",
"item": "launch_iskander",
"raid": true
},
{
"kind": "Kinzhal",
"item": "launch_kinzhal",
"raid": true
},
{
"kind": "Oreshnik",
"item": "launch_oreshnik",
"raid": true
},
{
"kind": "OreshnikBlock",
"item": null,
"raid": false
},
{
"kind": "OreshnikSub",
"item": null,
"raid": false
}
],
"units": [
{
"kind": "Patriot",
"item": "deploy_patriot"
},
{
"kind": "Gepard",
"item": "deploy_gepard"
},
{
"kind": "Pantsir",
"item": "deploy_pantsir"
},
{
"kind": "S400",
"item": "deploy_s400"
},
{
"kind": "S500",
"item": "deploy_s500"
},
{
"kind": "IrisT",
"item": "deploy_irist"
},
{
"kind": "Nasams",
"item": "deploy_nasams"
},
{
"kind": "Buk",
"item": "deploy_buk"
},
{
"kind": "Tor",
"item": "deploy_tor"
},
{
"kind": "P1sun",
"item": "deploy_p1sun"
},
{
"kind": "Radar",
"item": "deploy_radar"
},
{
"kind": "Depot",
"item": "deploy_depot"
},
{
"kind": "Tzm",
"item": "deploy_tzm"
},
{
"kind": "Ew",
"item": "deploy_ew"
},
{
"kind": "Light",
"item": "deploy_light"
}
],
"items": [
{
"id": "designator",
"title": "Целеуказатель",
"color": [
220,
50,
50
]
},
{
"id": "launch_shahed",
"title": "Пуск: Шахед-136",
"color": [
90,
92,
96
]
},
{
"id": "launch_geran3",
"title": "Пуск: Герань-3",
"color": [
30,
30,
34
]
},
{
"id": "deploy_patriot",
"title": "Развернуть Patriot",
"color": [
76,
86,
58
]
},
{
"id": "deploy_gepard",
"title": "Развернуть Гепард",
"color": [
104,
84,
54
]
},
{
"id": "deploy_pantsir",
"title": "Развернуть Панцирь-С1",
"color": [
82,
96,
60
]
},
{
"id": "deploy_s400",
"title": "Развернуть С-400",
"color": [
60,
80,
50
]
},
{
"id": "deploy_s500",
"title": "Развернуть С-500",
"color": [
108,
110,
80
]
},
{
"id": "launch_lancet",
"title": "Пуск: Ланцет-3",
"color": [
70,
78,
66
]
},
{
"id": "launch_kh101",
"title": "Пуск: Х-101",
"color": [
84,
88,
92
]
},
{
"id": "launch_kalibr",
"title": "Пуск: Калибр",
"color": [
150,
154,
156
]
},
{
"id": "launch_kh22",
"title": "Пуск: Х-22",
"color": [
226,
228,
222
]
},
{
"id": "launch_zircon",
"title": "Пуск: Циркон",
"color": [
52,
54,
52
]
},
{
"id": "launch_iskander",
"title": "Пуск: Искандер-М",
"color": [
108,
110,
80
]
},
{
"id": "launch_kinzhal",
"title": "Пуск: Кинжал",
"color": [
60,
62,
60
]
},
{
"id": "launch_oreshnik",
"title": "Пуск: Орешник",
"color": [
230,
120,
40
]
},
{
"id": "deploy_irist",
"title": "Развернуть IRIS-T SLM",
"color": [
76,
86,
58
]
},
{
"id": "deploy_nasams",
"title": "Развернуть NASAMS",
"color": [
76,
86,
58
]
},
{
"id": "deploy_buk",
"title": "Развернуть Бук-М2",
"color": [
82,
96,
60
]
},
{
"id": "deploy_tor",
"title": "Развернуть Тор-М2",
"color": [
108,
110,
80
]
},
{
"id": "deploy_p1sun",
"title": "Развернуть P1-SUN",
"color": [
52,
54,
52
]
},
{
"id": "deploy_radar",
"title": "Развернуть РЛС дальнего обнаружения",
"color": [
108,
110,
80
]
},
{
"id": "deploy_depot",
"title": "Развернуть склад боеприпасов",
"color": [
92,
78,
48
]
},
{
"id": "deploy_tzm",
"title": "Развернуть ТЗМ",
"color": [
82,
96,
60
]
},
{
"id": "deploy_ew",
"title": "Развернуть станцию РЭБ",
"color": [
108,
110,
80
]
},
{
"id": "deploy_light",
"title": "Развернуть прожектор",
"color": [
52,
54,
52
]
},
{
"id": "mover",
"title": "Пульт перемещения",
"color": [
60,
140,
220
]
},
{
"id": "flare_gun",
"title": "Осветительная ракета",
"color": [
255,
240,
180
]
},
{
"id": "remover",
"title": "Свернуть ПВО",
"color": [
200,
200,
200
]
}
]
}
+2 -1
View File
@@ -34,6 +34,7 @@
Команды:
- `python3 -m pip install -r tools/requirements.txt` — зависимости генератора (закреплён Pillow).
- `python3 tools/gen_catalog.py` — обновить Rust-константы после изменений `catalog/content.json`; `--check` проверяет актуальность.
- `python3 tools/gen_models.py` — перегенерировать.
- `python3 tools/gen_models.py --check` — только сравнить, выход 1 при различиях.
@@ -52,7 +53,7 @@
- `tools/airdef_assets/bounds.py` экспортирует габариты итоговых моделей для headless-навигации; authored overrides входят в расчёт.
- `tools/gen_tex.py` отвечает за вывод процедурных текстур и иконок; переиспользуемые примитивы палитры также находятся здесь.
- `tools/airdef_assets/texture_catalog.py` связывает стабильную палитру и процедурные функции с текстурами моделей, не смешивая каталог с геометрией.
- `tools/airdef_assets/icons.py` содержит каталог предметов и чистую функцию рендера иконки с явными параметрами `(name, color, seed)`.
- `catalog/content.json` задаёт идентичности техники и предметов и оформление предметов; `tools/airdef_assets/icons.py` читает его и содержит чистую функцию рендера иконки с явными параметрами `(name, color, seed)`.
- `tools/airdef_assets/pipeline.py` проверяет manifest, рассчитывает безопасный план обновления и откатывает записи при исключении.
- `airdef-mod/source-assets/assets/airdef/` — источник авторских переопределений. Генератор объединяет его с процедурными ресурсами во временном дереве, проверяет ссылки/JSON/PNG, затем устанавливает runtime-файлы.
- `airdef-mod/assets/` и `airdef-mod/data/` — упакованные generated output. Не редактируйте их вручную; добавляйте авторские исходники в `source-assets`.
+25
View File
@@ -0,0 +1,25 @@
"""Shared content identities and item presentation used by runtime and assets."""
import json
from pathlib import Path
PATH = Path(__file__).resolve().parents[2] / "catalog" / "content.json"
def load():
data = json.loads(PATH.read_text(encoding="utf-8"))
items = data["items"]
ids = [item["id"] for item in items]
if len(ids) != len(set(ids)):
raise ValueError("duplicate item ID in content catalog")
referenced = []
for group in (data["aircraft"], data["units"]):
kinds = [entry["kind"] for entry in group]
if len(kinds) != len(set(kinds)):
raise ValueError("duplicate kind in content catalog")
referenced.extend(entry["item"] for entry in group if entry["item"] is not None)
if len(referenced) != len(set(referenced)) or not set(referenced) <= set(ids):
raise ValueError("duplicate or unregistered content item")
for item in items:
if len(item["color"]) != 3 or any(not isinstance(c, int) or not 0 <= c <= 255 for c in item["color"]):
raise ValueError(f"invalid icon color for {item['id']}")
return data
+4 -32
View File
@@ -6,38 +6,10 @@ from PIL import Image
from .texture_catalog import clamp
# Предметы: (id, название, цвет иконки)
ITEMS = [
("designator", "Целеуказатель", (220, 50, 50)),
("launch_shahed", "Пуск: Шахед-136", (90, 92, 96)),
("launch_geran3", "Пуск: Герань-3", (30, 30, 34)),
("deploy_patriot", "Развернуть Patriot", (76, 86, 58)),
("deploy_gepard", "Развернуть Гепард", (104, 84, 54)),
("deploy_pantsir", "Развернуть Панцирь-С1", (82, 96, 60)),
("deploy_s400", "Развернуть С-400", (60, 80, 50)),
("deploy_s500", "Развернуть С-500", (108, 110, 80)),
("launch_lancet", "Пуск: Ланцет-3", (70, 78, 66)),
("launch_kh101", "Пуск: Х-101", (84, 88, 92)),
("launch_kalibr", "Пуск: Калибр", (150, 154, 156)),
("launch_kh22", "Пуск: Х-22", (226, 228, 222)),
("launch_zircon", "Пуск: Циркон", (52, 54, 52)),
("launch_iskander", "Пуск: Искандер-М", (108, 110, 80)),
("launch_kinzhal", "Пуск: Кинжал", (60, 62, 60)),
("launch_oreshnik", "Пуск: Орешник", (230, 120, 40)),
("deploy_irist", "Развернуть IRIS-T SLM", (76, 86, 58)),
("deploy_nasams", "Развернуть NASAMS", (76, 86, 58)),
("deploy_buk", "Развернуть Бук-М2", (82, 96, 60)),
("deploy_tor", "Развернуть Тор-М2", (108, 110, 80)),
("deploy_p1sun", "Развернуть P1-SUN", (52, 54, 52)),
("deploy_radar", "Развернуть РЛС дальнего обнаружения", (108, 110, 80)),
("deploy_depot", "Развернуть склад боеприпасов", (92, 78, 48)),
("deploy_tzm", "Развернуть ТЗМ", (82, 96, 60)),
("deploy_ew", "Развернуть станцию РЭБ", (108, 110, 80)),
("deploy_light", "Развернуть прожектор", (52, 54, 52)),
("mover", "Пульт перемещения", (60, 140, 220)),
("flare_gun", "Осветительная ракета", (255, 240, 180)),
("remover", "Свернуть ПВО", (200, 200, 200)),
]
# Presentation follows the shared content catalog; order keeps generated assets stable.
from .catalog import load as load_catalog
ITEMS = [(item["id"], item["title"], tuple(item["color"])) for item in load_catalog()["items"]]
# Иконки 16×16 без сглаживания. Каждый предмет рисуется своей функцией: категория задаёт основу
+45
View File
@@ -0,0 +1,45 @@
#!/usr/bin/env python3
"""Generate Rust content entries from the shared catalog; --check detects drift."""
import argparse
from pathlib import Path
import re
from airdef_assets.catalog import load
OUTPUT = Path(__file__).resolve().parents[1] / "airdef-mod/src/sim/catalog_entries.rs"
def generate(data):
lines = ["// Generated by tools/gen_catalog.py from catalog/content.json. Do not edit.",
"pub const AIRCRAFT: &[Aircraft] = &["]
for entry in data["aircraft"]:
kind, item = entry["kind"], entry["item"]
if not re.fullmatch(r"[A-Z][A-Za-z0-9]*", kind):
raise ValueError(f"invalid Rust kind {kind!r}")
if item is not None and not re.fullmatch(r"[a-z0-9_]+", item):
raise ValueError(f"invalid item ID {item!r}")
item_expr = f'Some("airdef:{item}")' if item is not None else "None"
lines.append(f' Aircraft {{ kind: {kind}, launch_item: {item_expr}, raid: {str(entry["raid"]).lower()} }},')
lines += ["];", "", "pub const UNITS: &[Unit] = &["]
for entry in data["units"]:
kind, item = entry["kind"], entry["item"]
if not re.fullmatch(r"[A-Z][A-Za-z0-9]*", kind) or not re.fullmatch(r"[a-z0-9_]+", item):
raise ValueError(f"invalid unit {entry!r}")
lines.append(f' Unit {{ kind: SamKind::{kind}, deploy_item: "airdef:{item}" }},')
return "\n".join(lines + ["];", ""])
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--check", action="store_true")
args = parser.parse_args()
generated = generate(load())
if args.check:
if not OUTPUT.exists() or OUTPUT.read_text(encoding="utf-8") != generated:
parser.exit(1, f"{OUTPUT} is stale; run python3 tools/gen_catalog.py\n")
else:
OUTPUT.write_text(generated, encoding="utf-8")
if __name__ == "__main__":
main()
+12
View File
@@ -2,6 +2,8 @@
import hashlib
import importlib.util
import json
import subprocess
import os
import sys
import tempfile
@@ -22,6 +24,16 @@ gen_models = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(gen_models)
class ContentCatalogTests(unittest.TestCase):
def test_catalog_rust_entries_are_current_and_registered_items_match_assets(self):
root = TOOLS.parent
subprocess.run([sys.executable, str(TOOLS / "gen_catalog.py"), "--check"], check=True)
language = json.loads((root / "airdef-mod/assets/airdef/lang/ru_ru.json").read_text(encoding="utf-8"))
for name, title, _ in icons.ITEMS:
self.assertEqual(language[f"item.airdef.{name}"], title)
self.assertTrue((root / "airdef-mod/data/airdef/horizon/item" / (name + ".json")).is_file())
class ManifestTests(unittest.TestCase):
def test_manifest_rejects_traversal_and_duplicate_paths(self):
with tempfile.TemporaryDirectory() as tmp: