Skip to content

fix(yaml): bit-perfect YAML round-trip cs1=cs2=cs3 + автоматизация прогона#3282

Merged
bylins merged 6 commits into
masterfrom
feat/yaml-roundtrip-bitperfect
May 17, 2026
Merged

fix(yaml): bit-perfect YAML round-trip cs1=cs2=cs3 + автоматизация прогона#3282
bylins merged 6 commits into
masterfrom
feat/yaml-roundtrip-bitperfect

Conversation

@kvirund
Copy link
Copy Markdown
Collaborator

@kvirund kvirund commented May 17, 2026

Контекст

Серия фиксов на YAML round-trip начала жить как #3279. Тот PR ушёл в master большим пакетом из 22 коммитов:

22 коммита из #3279 (уже в master)
668ec111a feat(yaml): флаг -S <out_dir> для load->save round-trip
5de14c474 tools(yaml): yaml_world_diff.py + yaml_roundtrip_test.sh
9c59e87ae feat(yaml): ResaveWorld пропускает under_construction-зоны
3dc3399d7 fix(yaml): SaveObjects пишет в short_desc long-описание, а не inventory-имя
c22af2d43 fix(yaml): SaveMobs пишет в aliases полный alias-список, а не npc_name
27377fa9f fix(yaml): mob/enhanced/spells использует SplMem, не SplKnw
71a0802f9 refactor(yaml): вынести Koi8rYamlEmitter в отдельный header
9827acebc fix(yaml): эмиттер должен квотить скаляры с YAML-индикаторами в начале (#3273)
fd2bfa7d7 fix(yaml): vnum пишется в SaveTriggers/SaveMobs/SaveObjects
84523c51f fix(yaml): SaveMobs пишет mob_type/race/resistances/saves и не теряет attributes
c0ae5d201 fix(yaml): SaveMobs очищает special_buf перед tascii, обрезает trailing space
57da048be fix(yaml): SaveZone конвертирует cmd.argN обратно из rnum в vnum
26994ab23 fix(yaml): literal block использует strip-chomping (|-), без trailing \n
7540d7eec fix(converter): lowercase первого слова в trigger script'ах
2235c5be6 fix(converter): trim trailing whitespace в script-строках, drop пустой arglist
abad4c042 fix(yaml): literal block use explicit indent indicator (|-2)
0290619a6 fix(yaml): SaveMobs пишет race (пропускался ранее)
f3baf9239 fix(yaml): literal block выбирает clip/strip по trailing newline значения
0275a046c fix(converter): откат rstrip в _normalize_script
770688e2d fix(yaml): ResaveWorld компилируется без HAVE_YAML и HAVE_SQLITE
7aac3f83e fix(tests): `--quick` пропускает sqlite-сборку при отсутствии sqlite3
7e4508b8d fix(yaml): SaveX обновляет index.yaml + чинит exit-keywords/descriptions

После этого мерджа в логе round-trip'а оставалось 6 строк диффа (только OBJ-чексуммы жидкостных контейнеров + 2 зоны со сломанными ссылками). Этот PR закрывает остаток до bit-perfect-а cs1=cs2=cs3 и собирает все стадии тестирования в одну команду.

Что нового в этом PR (6 коммитов поверх master)

  • Koi8rYamlEmitter — литеральный блок forced при любом \n в значении (plain/quoted scalar теряют embedded \n на reload), 4-сторонний выбор chomping по count'у trailing \n (|+2 / |-2 / |2) для бит-точного round-trip'а описаний/триггер-скриптов.
  • SaveTriggers — пустые cmd сохраняются как одиночный пробел; на reload ParseTriggerScript::TrimRight возвращает "", count node'ов cmd_list сохраняется.
  • SaveObjectsextra_descriptions эмитятся в обратном порядке (LoadObjects делает prepend, иначе порядок флипает на каждом round-trip'е); keywords обёрнуты в IncreaseIndent чтобы content литерала попадал в правильную колонку.
  • text_id::Init() перенесён в BootWorld(). До этого справочник int↔name для ObjVal-ключей инициализировался только в BootMudDataBase() (comm.cpp:807), а -S <out_dir> и -c (scheck) возвращаются до него. В результате SaveObjects фильтровал все POTION_* ключи как «нерезолвимые» и extra_values блок не писался → ~100 жидкостных контейнеров расходились по чексумме.
  • lib.template/world/zon/{1,40}.zon — убраны 8 ссылок на отсутствующие vnum'ы (mob 1127, obj 1053/1069/1900/1903/1906/1909, mob 27073). ResolveZoneCmd при boot'е помечал их * и они тихо терялись при resave, ломая cs3.
  • tools/run_load_tests.sh — добавлен run_roundtrip_test() (resave → swap → reboot → cs3); вызывается после YAML checksums и до admin API, чтобы admin API запускался последним (он рекреирует мир). FULL_WORLD_ARCHIVE теперь обязательная переменная; extract идёт через --strip-components=1 (работает с любым top-dir-ом архива); SQLite авто-скипается если pkg-config sqlite3 не находит dev-headers; [ -n "$X_BIN" ][ -x "$X_BIN" ] чтобы не пытаться стартовать несуществующий бинарь.

Результат

=== CHECKSUM COMPARISON ===
Small_Legacy_checksums vs Small_YAML_checksums: MATCH
Small_YAML_checksums  vs Small_YAML_RoundTrip:  MATCH
Full_Legacy_checksums vs Full_YAML_checksums:   MATCH
Full_YAML_checksums   vs Full_YAML_RoundTrip:   MATCH

Admin API: 4/4 PASSED (Small/Full × Legacy/YAML).

Запуск одной командой:

FULL_WORLD_ARCHIVE=~/worlds/world.20260427.cleaned.tgz ./tools/run_load_tests.sh

Использован архив world.20260427.cleaned.tgz — это мастер-архив world.20260427.tgz с вычищенными M ... -1 ... командами в зонах 822/823 (41 строка, см. issue-тред #3273). Оригинальный архив тоже почистится через любой OLC zedit save.

Test plan

  • CI: build всех вариантов (legacy/yaml/sqlite × debug/release)
  • CI: unit-тесты GoogleTest
  • Локально: ./tools/run_load_tests.sh (с указанием FULL_WORLD_ARCHIVE) — все 4 MATCH'а + admin API PASSED
  • Локально: ./tools/run_load_tests.sh --quick — small world MATCH'и + admin API

kvirund added 6 commits May 17, 2026 15:58
…ping

`Koi8rYamlEmitter::Value` теперь определяет режим литерального блока
по количеству trailing '\n' (без content -> keep |+2; 0 -> strip |-2;
1 -> clip |2; >=2 -> keep |+2), и переключается в literal-block для
любого значения с embedded '\n', даже если caller не передал
literal=true (plain/quoted scalar их теряет на reload).

Закрывает классы расхождений: embedded \n в коротких именах комнат,
пустые literal-блоки ("\n" в описаниях), и multi-\n в текстах справки.
…sc в обратном порядке, exit-keywords с правильным indent

* SaveTriggers: пустые `cmd` сериализуются как " " (одиночный пробел);
  ParseTriggerScript на reload TrimRight'ит обратно в "", сохраняя
  количество node'ов cmd_list. Без этого whitespace-only строки из
  legacy `.trg` теряются и чексумма триггера сдвигается.
* SaveObjects: extra_descriptions эмитятся в обратном порядке.
  LoadObjects делает prepend (а не append), поэтому in-memory список
  reverse'ом относительно файла. Эмитя в обратном — получаем стабильный
  round-trip, без флипа порядка на каждом save.
* SaveRooms (и SaveObjects ex_desc): для `keywords` оборачиваем
  `yaml.Value` в IncreaseIndent/DecreaseIndent — чтобы content
  литерального блока попадал в правильную колонку (parent_indent + 2),
  иначе многострочные keyword'ы не парсились на reload.
Команды зон 1 и 40 ссылались на mob/obj vnum'ы, которых нет в small world
(в `lib.template/world/mob/` только 1.mob/2.mob/40.mob, аналогично obj/).
ResolveZoneCmdVnumArgsToRnums при boot'е помечал такие команды как '*',
и они тихо терялись при пересохранении -- что ломало YAML round-trip.

Удалено 8 dangling-команд:
* zon/1.zon: `O 1053` (obj отсутствует), `M 1127` (mob отсутствует),
  `O 1069` (obj отсутствует).
* zon/40.zon: `G 1900/1903/1906/1909` (obj'ы отсутствуют),
  `M 27073` (mob отсутствует).
`run_load_tests.sh` теперь оркестрирует все этапы одной командой:
setup -> build -> load tests -> YAML round-trip -> admin API.

* Новая стадия `run_roundtrip_test()`: для YAML-сборки запускает
  `circle -S world_v2`, подменяет world на свежесохранённый, и
  переиспользует `run_test` для boot'а с `-W` -> получает cs3.
  Сравнение cs2 vs cs3 добавлено в CHECKSUM COMPARISON. Стадия
  работает как для small, так и для full.
* Admin API тесты сдвинуты в самый конец (они ремэйкают мир, поэтому
  всё order-sensitive должно идти до них). Round-trip между checksums
  и admin API.
* `FULL_WORLD_ARCHIVE` теперь обязательная переменная для full-world
  тестов -- никакого hardcoded `~/repos/world.tgz`. Small-world прогоны
  её игнорируют.
* Extraction архива через `tar --strip-components=1`, чтобы скрипт
  работал с любым top-dir'ом архива (`lib/`, `world.YYYYMMDD/`, ...).
* Auto-skip SQLite если `pkg-config sqlite3` не находит dev-headers
  и не указан явно `--loader=sqlite`. Раньше скрипт падал, требуя
  `libsqlite3-dev` сразу.

`should_run_test` тоже понимает round-trip-стадию (как и admin API,
игнорирует FILTER_CHECKSUMS).
…tra_values в '-S' режиме

Справочник int->name для ObjVal-ключей (POTION_SPELL*_NUM/LVL, POTION_PROTO_VNUM)
инициализируется в `text_id::Init()`, который зовётся только из
`BootMudDataBase()` (comm.cpp:807). А режимы `-S <out_dir>` и `-c` (scheck)
возвращаются раньше -- между `BootWorld()` и `BootMudDataBase()`.

В итоге YAML round-trip терял `extra_values` блок:
* `ConvertDrinkconSkillField` на boot копирует значения potion-прототипа в
  kLiquidContainer/kFountain через `SetPotionValueKey(POTION_SPELL1_NUM, ...)`
  и т.п. -- m_values заполняется.
* `SaveObjects` итерирует `obj->get_all_values()` и через `text_id::ToStr`
  пытается перевести ключ в строку -- но справочник пустой, ToStr возвращает
  "", и все ключи отфильтровываются.
* В YAML записывается всё, кроме `extra_values`. На cs3 boot конвертер уже
  не сработает (spec_param=-1 после прошлого boot'а), m_values пустой,
  чексумма расходится -- ~100 жидкостных контейнеров в full world.

Перенос `text_id::Init()` в `GameLoader::BootWorld` гарантирует, что справочник
готов до любого пути, способного сериализовать объекты. Init использует
unordered_map::insert, так что повторный вызов из `BootMudDataBase()`
безопасен (no-op для уже добавленных пар).
…uch file'

LEGACY_BIN/SQLITE_BIN/YAML_BIN задаются статически наверху скрипта, поэтому
`[ -n "$BIN" ]` всегда truthy. Когда соответствующий лоадер скипается
(auto-skip sqlite или filter loader/world), run_test/run_admin_api_test всё
равно пытаются стартовать несуществующий бинарь:

    line 658: /home/kvirund/repos/mud/build_sqlite/circle: No such file
              or directory

Меняем на `-x` (файл существует и исполняется) -- ровно то, что нужно
для гейта запуска.
@bylins bylins merged commit 6433bc6 into master May 17, 2026
20 checks passed
@bylins bylins deleted the feat/yaml-roundtrip-bitperfect branch May 17, 2026 15:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants