World Interaction System (WI-001–WI-007): практическое руководство

Актуальность сверки: 19 июля 2026 года.
Статус: по текущему дереву исходников WI-001–WI-007 реализованы и статически просмотрены; работоспособность не подтверждена, выпуск запрещён до прохождения WI-008.
Область: клиент Last Chaos, GameServer, server/client SQLite-каталоги и PostgreSQL lc_game.

[!WARNING] Этот документ описывает текущий исходный код, а не готовый production-релиз. Сборка, ECC-генерация EntitiesMP, тесты, запуск клиента/сервера и миграции не выполнялись. PostgreSQL-миграции 00240032 и новые SQLite-схемы, согласно журналу прогресса, в рабочие базы не применялись; production-контент WI отсутствует. Настоящая рецензия также была только статической: SQLite/PostgreSQL не открывались и SQL из примеров не исполнялся.

Содержание

  1. Назначение и модель доверия
  2. Как использовать
  3. Как сейчас всё работает
  4. Как поддерживать: архитектура и карта файлов
  5. Данные и базы
  6. Установка схем и эксплуатационный lifecycle
  7. Как добавлять новый контент
  8. Практические SQLite-примеры
  9. Диагностика и troubleshooting
  10. Безопасность и защита от эксплойтов
  11. Ограничения и release gate
  12. Глоссарий
  13. Источники сверки

Быстрая навигация по шести обязательным вопросам

  1. Как использовать систему.
  2. Каково текущее поведение.
  3. Как сопровождать реализацию.
  4. Где и какие данные хранятся.
  5. Как устанавливать и обновлять схемы.
  6. Как расширять систему новым контентом.

1. Назначение и модель доверия

Система объединяет:

Главное правило: клиент показывает состояние и отправляет намерение; решение и commit принадлежат серверу. Preview, локально активная кнопка, countdown или наличие visual не являются разрешением. Перед commit сервер повторно проверяет actor/target, area/layer, дистанцию, LoS, права, состояние, revision, inventory, NAS, labor и предметный контекст.

1.1 Матрица покрытия WI-001–WI-007

Пункт Использование Текущее поведение Сопровождение Расширение
WI-001 Очки работы Operation/labor WI-001 Labor/actability
WI-002 NPC actions NPC context WI-002 NPC action
WI-003 Объекты мира Graph/objects WI-003 Graph/object
WI-004 Timed crafting Timed crafting WI-004 Recipe
WI-005 Лут трупа Corpse loot WI-005 Loot/policy
WI-006 Housing Housing WI-006 Housing/decor
WI-007 Cultivation/resources Cultivation/resources WI-007 Seed/crop/resource

2. Как использовать

2.1 Игроку

WI-001: Очки работы

WI-002: Контекстные действия NPC

  1. Использовать обычный world-click/выбор NPC. Player.es подходит к цели и запрашивает discovery у сервера.
  2. Сервер возвращает до 16 действий. Для живых NPC текущий набор: Shop, Storage, Auction, Repair, Teleport, Craft.
  3. Действия 1–9 можно выбрать клавишами 19; все 16 доступны кликом по иконке. Для позиций 10–16 цифровая подсказка намеренно не рисуется.
  4. Disabled-иконка показывает server-provided причину блокировки в tooltip.
  5. После выбора сервер заново строит список и проверяет target/revision. Только затем клиентский adapter открывает существующее окно магазина, склада, аукциона, ремонта, портала или крафта.

Сессия закрывается при смене цели, смерти, warp/logout, смене area/layer, выходе из серверной дистанции или stale revision. Живой NPC обслуживается в пределах server range 8.0; клиент не пытается заменить эту проверку собственной геометрией.

WI-003: Интерактивные объекты мира

  1. Кликнуть по visual объекта.
  2. Клиент подходит примерно на 4 единицы и отправляет defaultActionId, который ранее опубликовал сервер.
  3. Сервер проверяет object identity, generation/revision, текущую фазу, action binding, target profile, LoS/дистанцию и requirement graph.
  4. Результат — server commit новой фазы либо открытие craft UI для workbench. Клиент не выбирает следующий state.

WI-004: Timed crafting

  1. Открыть «Ремесленскую книгу» через подтверждённое действие Craft у NPC или через WI-003 workbench.
  2. Выбрать рецепт и нажать Ок.
  3. Сервер проверит станцию, рецепт, actability/level, материалы, NAS, labor и inventory revision; затем зарезервирует ресурсы и запустит job.
  4. Во время Running UI показывает оставшиеся секунды; повторный start не отправляется.
  5. Отмена во время Running запрашивает cancel. Возвращаются только ещё не зафиксированные ресурсы.
  6. В Claimable текст просит освободить inventory и снова нажать Ок: эта же кнопка выполняет claim.
  7. Если inventory заполнен, результат остаётся durable и claimable; новый предмет не должен теряться или создаваться дважды.

WI-005: Лут трупа

  1. Кликнуть по мёртвому NPC, для которого сервер успешно опубликовал corpse container.
  2. В контекстном списке выбрать View loot или Take all.
  3. В окне «Подбор предметов»:
    • клик по обычной строке — взять один entry;
    • Поднять все — серверный take-all; список entry клиент не задаёт;
    • при roll: обычный клик — Need, Shift+кликGreed, Ctrl+кликPass.
  4. Take-all может завершиться частично, например при заполнении inventory. Stale revision должен привести к обновлению snapshot, а не к локальному угадыванию результата.

Если policy отсутствует или публикация container не удалась, новая смерть остаётся на legacy ground-drop path. Для durable policy труп может быть восстановлен после restart только в устойчивом world/zone/area scope; reusable instance без стабильной identity fail closed возвращается к ground drop.

WI-006: Housing

Размещение:

  1. Выбрать Template стрелками/ID, Owner (Character, Account, Guild, Family) и Rotation.
  2. Позиция берётся из текущей позиции персонажа, а не из произвольной точки окна.
  3. Нажать Preview и дождаться успешного ответа сервера.
  4. Не меняя draft и не отходя, нажать Place.
  5. Placement — отдельный authoritative request: успешный preview не резервирует место и не гарантирует commit.

Управление выбранным plot:

Восемь текущих прав: View, Contribute, Decorate, ManageAccess, Remove, Plant, Care, Harvest. UI не принимает неизвестные биты. Для public subject ID должен быть 0, для остальных — положительный.

WI-007: Cultivation и ресурсы

Посадка:

  1. Выбрать seed из server-published списка.
  2. Нажать Capture current point: фиксируются текущие x/z/layer персонажа.
  3. При необходимости ввести Rotation.
  4. Нажать Plant selected seed. Если персонаж отошёл или сменил layer, точку надо захватить заново.

Уход и сбор:

Countdown строится из serverNowUnixMs и authoritative deadline. Достигнув нуля, клиент показывает ожидание, но не переводит стадию: нужен новый server upsert. Mature entity отображается как готовая к сбору.

2.2 Контент-дизайнеру

Рабочий порядок всегда такой:

  1. Выбрать отдельный TEST ID range и проверить отсутствие коллизий.
  2. Подготовить server static rows и все логические ссылки на существующие items, skills, t_npc, crafts.
  3. Подготовить client visual rows с теми же template/state/stage/decoration/visual IDs.
  4. Для cultivation скопировать exact visual asset и object-state binding rows также в server compact.db: server loader сверяет identity и требует SKA resource_kind=1.
  5. Проверить FK (PRAGMA foreign_key_check) и предметные loader-инварианты из разделов 5 и 7.
  6. Увеличить world_interaction_schema_versions.content_revision при server gameplay content change и world_visual_catalog_metadata.catalog_revision в каждой изменённой visual copy (client и обязательном cultivation mirror на server).
  7. Не создавать runtime rows вручную: operations, jobs, objects, plots, loot containers и plants создаёт сервер.

2.3 Оператору/администратору

3. Как сейчас всё работает

3.1 Общий packet/data flow

world click / UI button
  -> client controller validates only local shape/pending state
  -> CNetwork writer (existing transport)
  -> GameServer bounded do_* packet handler
  -> area/domain service re-resolves actor and target
  -> static immutable SQLite snapshot + PostgreSQL repository transaction
  -> authoritative result/upsert/snapshot
  -> SessionStateExten bounded reader
  -> client manager (state/visual) -> controller -> XML UI

Новые extend-группы: MSG_EX_WORLD_INTERACTION_LABOR, _NPC, _OBJECT, _HOUSING, _CULTIVATION; corpse V2 остаётся в MSG_EX_LOOT_SYSTEM, timed craft — в существующем MSG_CUSTOM_MESSAGE/MSG_CRAFT. Старые numeric values не переопределялись; новые значения добавлены в конец диапазонов.

3.2 WI-001: operation/labor

3.3 WI-002: NPC context

3.4 WI-003: graph/objects

3.5 WI-004: timed crafting

3.6 WI-005: corpse loot

3.7 WI-006: housing

3.8 WI-007: cultivation/resources

3.9 Restart/recovery order

При включении area текущий порядок:

  1. InteractiveObjectArea::Restore();
  2. CorpseLootArea::Restore();
  3. HousingArea::Restore();
  4. CultivationArea::Restore().

Затем visibility отправляет object, housing и cultivation snapshots. Каждую секунду идут timed craft, object timers, corpse expiry/roll и cultivation growth/resource respawn ticks; раз в минуту освобождаются истёкшие labor reservations. При disable/level teardown area state очищается; PostgreSQL остаётся источником recovery.

4. Как поддерживать: архитектура и карта файлов

Слой Основные файлы Ответственность
Server startup GameServer/Server.cpp, Craft.cpp Порядок загрузки catalog, инициализация craft
Server scheduling ServerTimer.cpp, Area.cpp 1-second ticks, minute recovery, area restore/snapshot
WI-001 Operations/labor WorldInteractionOperation.*, LaborCatalog.*, LaborService.*, CharacterLabor.*, CharacterActability.* Idempotency, wallets, ledger, regeneration, actability
WI-002 NPC NpcContextAction.*, NpcInteractionService.*, NpcInteractionPackets.*, doFuncNpcInteraction.cpp Discovery/session/execute
WI-003 Objects InteractiveObjectCatalog.*, InteractiveObjectRepository.*, InteractiveObjectArea.*, packets/handler Graph, persistent states, timers, replication
WI-004 Craft CraftCatalog.*, TimedCraftRepository.*, TimedCraftService.*, TimedCraftPackets.*, Craft.* Recipe generation, escrow, job, claim
WI-005 Corpse CorpseLootCatalog.*, CorpseLootArea.*, CorpseLootRepository.*, packets/handler Publication, claims, rolls, expiry
WI-006 Housing HousingCatalog.*, HousingGeometry.*, HousingRepository.*, HousingArea.*, packets/handler Geometry, placement, stages, rights, decor
WI-007 Cultivation CultivationCatalog.*, CultivationRepository.*, CultivationArea.*, packets/handler Plant/growth/harvest/resource respawn
Shared opcodes ShareLib/MessageType.h Server wire enums
Client network Engine/Network/MessageDefine.h, CNetwork.cpp, SessionStateExten.cpp Writers, bounded readers, routing
Client state/controller NpcInteractionController.*, WorldInteractionObjectManager.*, CorpseLootController.*, HousingManager/Controller.*, CultivationManager/Controller.*, Custom/Craft.* Local projection, pending fences, UI calls
Client UI UIInteractActions.*, UILootWindow.*, UIHousing.*, UICultivation.*, UIPlayerInfo.* Presentation/input only
XML interactActions.xml, lootWindow.xml, housing.xml, cultivation.xml, Craft.xml, PlayerInfo.xml Layout и реальные labels/buttons
World entry points EntitiesMP/Player.es, Enemy.es, PlayerWeapons.es, ModelHolder2.es, ModelHolder3.es Click/ray target и visual overrides
Static schemas WI System/world_interaction*_sqlite_schema.sql Authoring contracts
Durable schema db/migrations/lc_game/0024..0032 PostgreSQL runtime state

4.1 Сопровождение по WI-001–WI-007

Пункт Что сохранять при изменениях Обязательная соседняя сверка
WI-001 Idempotency request key, character-first split, wallet/ledger transaction, absolute actability sync Labor SQLite, PostgreSQL 0024/0025, packets и PlayerInfo.xml
WI-002 Повторный server discovery, session revision, teardown и максимум 16 bounded actions Legacy NPC flags/services, client icons/adapters и corpse routing
WI-003 Immutable validated snapshot, area ownership, generation/revision и persistent timer recovery Action/graph/object rows, PostgreSQL 0026, packet replication и visual binding
WI-004 Pinned recipe generation, escrow, durable claim и inventory revision fence Обе recipe copies, PostgreSQL 0027, NPC/workbench entry и craft UI
WI-005 Publication-before-fallback, frozen eligibility, exact-once claim и partial take-all Policy/action rows, PostgreSQL 0028, corpse V2 packets и legacy ground drop
WI-006 Server geometry, occupancy transaction, named rights и exact decor source item Housing SQLite, PostgreSQL 0029/0030, UI/click entry и stage/decor visuals
WI-007 Единственный growth/respawn scheduler, object revision, claim lease и housing rights Все три server schemas, PostgreSQL 0032, mirrored SKA visuals и UI/click entry

Любое изменение packet/shared behavior сверять на client и server. Изменение runtime persistence сверять с соответствующей PostgreSQL migration и recovery path; изменение static identity — с server/client SQLite-копиями. Нельзя считать loader/schema row активной только потому, что она существует в DDL: отдельно учитывать перечисленные в разделах 3 и 5 неиспользуемые bindings и fail-closed semantics.

4.2 Startup loading

Фактический server order в Server.cpp: базовые item/character managers -> LaborCatalog -> InteractiveObjectCatalog -> legacy item proto list -> CorpseLootCatalog -> HousingCatalog -> CultivationCatalog; позднее mCraft.Init() загружает CraftCatalog. Такой порядок обязателен из-за cross-validation cultivation с object/loot/housing/labor и corpse с item protos.

Client DatabaseManager открывается из Engine.cpp; WI visual managers загружают tables лениво при первом snapshot/upsert. Их catalogLoadAttempted_ не сбрасывается при обычном scope reset, поэтому замена client DB внутри процесса не является поддерживаемым reload workflow.

Поддерживайте границы слоёв: packet handler только разбирает bounded payload и вызывает domain API; catalog не пишет runtime; repository не управляет UI/area pointers; area/service владеет gameplay orchestration; client manager хранит read-only projection; controller ставит pending fence; XML/UI только отображает и отправляет намерение.

4.3 Правило EntitiesMP и virtual interfaces

  1. Для любой сущности проекта EntitiesMP первичным исходником является соответствующий *.es; сгенерированные .cpp/.h — только output ECC, их вручную не редактировать.
  2. Engine/UI/network не должны включать generated EntitiesMP headers, делать cast к generated classes или обращаться к generated properties/offsets.
  3. Связь из Engine/UI/network идёт только через virtual API non-generated base Engine/Entities/Entity.h; реализация override находится в *.es.
  4. Текущие примеры: CEntity::InitializeWorldInteractionVisual() с overrides в ModelHolder2.es/ModelHolder3.es; CEntity::GetNpcTeleportFlags() с override в Enemy.es.
  5. После изменения .es нужна разрешённая ECC regeneration; до неё generated Player.cpp может оставаться устаревшим.

5. Данные и базы

5.1 Что нельзя путать

Хранилище Ownership Что хранит Что не хранит
Server compact.db (SQLite) content team/server deployment Gameplay definitions, recipes, policies, authored spawns, labor profiles; для cultivation также зеркальные visual IDs Балансы, jobs, claims, plots, plants
Client compact.db (SQLite) client content/package Новые WI visual tables; кроме них в этой же legacy DB уже находятся recipes/categories/items и другие presentation data Runtime-права, балансы, authoritative доступность action
PostgreSQL lc_game GameServer Durable operations, reservations, wallets, object instances, jobs, loot, housing, cultivation Authoring resource paths и production content definitions

visual_asset_id — общий числовой контракт, но server SQLite не должен получать путь от клиента. revision runtime-объекта не равен content_revision catalog. generation отличает incarnation/pinned catalog; он не заменяет revision.

5.2 Server SQLite: таблицы и ключевые поля

Core/labor/graph

Таблица Ключевые поля
world_interaction_schema_versions component, version, content_revision; housing generation читает world_interaction revision
world_interaction_target_profiles target_kind, max_range, LoS/dead flags; loader сейчас использует не все schema-поля
world_interaction_requirement_sets / requirements match_mode, ordered requirement_kind, typed value columns, invert_result
world_labor_profiles initial/max, tick, online/offline rates, offline cap, enabled
world_labor_profile_tiers max/rate deltas или replacements, basis-point multipliers
world_labor_modifier_rules source kind/id, stacking group/priority, deltas/multipliers
world_labor_cost_profiles source, fixed/minimum cost, actability reward parameters
world_actability_levels (actability_id, level), cumulative EXP threshold и multipliers
world_interaction_graphs/nodes/edges/node_effects entry/failure, execution budget, typed nodes/edges/effects
world_interaction_actions icon/text IDs, target/requirements/graph, optional skill/labor, cast/cooldown/flags, enabled
world_interaction_object_templates/states/state_actions/spawns initial state, persistence/radius, visual/timer, action transition, stable authored placement
world_interaction_action_crafts один action -> один crafts.id workbench binding

Таблицы world_interaction_skill_actions и world_interaction_npc_bindings присутствуют в schema, но текущий object loader их не читает. Не считать их активными bindings.

Craft/loot/housing/cultivation

Домен Таблицы
Legacy craft content crafts, craft_materials, craft_products; сервер реально читает actability_id, actability_limit, delay, recommend_level, cost, labor_point, craft_npc, material/product grade/rate
Loot world_interaction_loot_packs, world_interaction_loot_entries, world_interaction_corpse_policies, world_interaction_npc_corpse_policies, world_interaction_corpse_policy_actions
Housing world_building_templates, world_building_stages, world_building_stage_materials, world_building_interiors, anchors/cultivation areas, territory/scopes/polygons/vertices/category rules, decoration templates
Cultivation world_cultivation_profiles/stages/planting_bindings, world_housing_cultivation_rules, environment rules, world_resource_profiles/spawns

Подтверждённые enum/domain values

Поле Значения текущего кода
target kind 0 None, 1 Character, 2 Npc, 3 InteractiveObject, 4 Corpse, 5 Building, 6 Cultivation, 7 GatheringObject (runtime operation также имеет 8 HousingPlot, 9 HousingDecoration, но SQLite target schema допускает только 0–7)
graph node_kind 0 Entry, 1 Requirement, 2 Branch, 3 Delay, 4 SkillEffect, 5 SetObjectState, 6 Complete, 7 Fail; schema 8–19 пропускает CHECK, но loader их отклонит
edge outcome_kind 0 Any, 1 Success, 2 Failure; schema 3–5 loader отклонит
requirement 0 CharacterLevel, 1 CharacterAlive, 2 ItemCount, 3 ObjectState, 4 ObjectLifecycle, 5 Zone, 6 Area, 7 Layer, 8 Distance, 9 CharacterFlag, 10 QuestState, 11 SkillKnown, 12 Permission, 13 WorldState
requirement match_mode 0 all, 1 any
labor modifier source 0 AccountTier, 1 CharacterTag, 2 Buff, 3 Item, 4 WorldState
labor cost source 0 None, 1 Fixed, 2 Action, 3 Recipe; cultivation/housing требуют positive Fixed
loot claim_scope 0 Owner, 1 Party, 2 Expedition, 3 Contributors
loot action role 0 Open, 1 TakeAll
loot roll_mode 0 weighted draws with replacement (кроме уже использованного ненулевого unique_group), 1 without replacement, 2 without replacement + need/greed/pass
housing category 0 House, 1 Farm, 2 Scarecrow
footprint/area shape 1 Circle, 2 Box
anchor kind 1 Floor (implicit anchor_id=0), 2 Wall, 3 Ceiling, 4 Socket; authored anchor rows допускают 2–4
owner kind 1 Character, 2 Account, 3 Guild, 4 Family
access subject 0 Public, 1 Account, 2 Character, 3 Guild, 4 Family
permissions bits 0..7: View, Contribute, Decorate, ManageAccess, Remove, Plant, Care, Harvest
cultivation access 1 Housing, 2 PublicWorld

Generic object evaluator поддерживает requirements 0,1,3,4,5,6,7,8; остальные fail closed. Cultivation дополнительно допускает SkillKnown(11) в собственном validator/evaluator path, но запрещает ItemCount/flag/quest/permission/world-state в action graph этой версии.

5.3 Client SQLite visual catalog

Таблица Контракт
world_visual_catalog_metadata единственная row id=1, положительный catalog_revision; текущие managers его не сравнивают по wire
world_visual_assets positive ID, kind/path, optional animations, scale/offset/rotation, flags/enabled
world_building_stage_visuals exact (building_template_id, stage_id) -> visual_asset_id, одна complete row на template
world_object_state_visuals exact (object_template_id, state_id) -> visual_asset_id
world_housing_decoration_visuals exact decoration template -> visual asset

Schema допускает resource kinds 1–7 (SkaModel, StaticModel, Billboard, Effect, TerrainDecoration, Particle, Composite), но текущая фактическая поддержка уже:

Синхронизация identity различается по доменам:

5.4 PostgreSQL durable runtime

Migration Основные таблицы/изменения
0024 world_interaction_operations, inventory/currency reservations; unique character ownership constraint
0025 account/character labor, reservations, ledger
0026 interactive_object_instances, scheduled transitions
0027 craft jobs/material escrow/output/deliveries; t_characters.a_inventory_revision, t_inven.a_origin_key
0028 corpse containers/entries/participants/requests/claims/rolls/votes
0029 families, plots, occupancy, buildings
0030 construction, contributions, five permission tables, decorations
0031 operation events table, предназначенная для append-only audit
0032 cultivated entities, occupancy/usage, growth jobs, care actions, resource states, gathering claims

world_interaction_operation_events создан миграцией как таблица, предназначенная для append-only истории, но DDL не запрещает UPDATE/DELETE, а writer в текущих GameServer WI units не найден. Не полагаться на неё как на полный или технически неизменяемый audit trail до отдельного подтверждения прав БД и writer-а.

Подтверждённые operation kinds для диагностики: craft 4, corpse 5, housing place/contribute/permission/decor/remove 6..10, plant/care/harvest/remove/gather 11..15.

Ключевые runtime contracts:

Runtime row Identity/revision/FK contract
operation unique (actor_character_id, request_key), FK actor (character, account), target kind/id и state/revision
inventory/currency/labor reservation FK на ту же operation/owner; active inventory slot уникален; labor split зафиксирован character-first
labor wallet/ledger account и character rows раздельны; pool_kind=0 account, 1 character; persisted cursor/revision
interactive object object_id, scope, phase, generation>0, revision; authored source tuple уникален; transition хранит expected revision/deadline
craft job/output owner + operation, recipe и pinned catalog generation, job revision/state; output имеет unique delivery UUID/serial/origin
corpse source identity и container generation/revision; participant IDs frozen; entry/claim uniqueness запрещает double award
housing plot scope/owner/revision + unique occupancy cells; building связан с plot и interactive object; access principals разнесены по таблицам
cultivation/resource cultivated entity связан с interactive object и optional plot; authoritative revision остаётся у object; growth/respawn/claim имеют отдельные deadlines/lease

Подтверждённые runtime state values, полезные при read-only диагностике: craft 1 Running, 3 Claimable, 4 Delivered, 5 Cancelled, 6 Failed; corpse entry 0 Available, 1 Reserved, 2 Awarded; roll choice 0 None, 1 Pass, 2 Greed, 3 Need; resource state 0 Active, 1 Depleted; cultivation access 1 Housing, 2 PublicWorld. Для остальных таблиц не интерпретировать весь разрешённый CHECK-диапазон как реализованный enum.

6. Установка схем и эксплуатационный lifecycle

6.1 Точный порядок трёх SQLite schema files

[!CAUTION] Каждый schema file уже содержит BEGIN IMMEDIATE; ... COMMIT;. Не оборачивать .read/execution этих файлов во внешнюю transaction. Применять каждый файл отдельно и только после byte-for-byte backup.

Скрипты используют CREATE TABLE IF NOT EXISTS: это bootstrap schema, а не универсальный ALTER-upgrader. Если таблица с тем же именем уже существует в другой форме, повторный запуск молча не исправит её definition. Перед применением сравнить sqlite_master staging-копии с source DDL; сами scripts не содержат production rows.

Server compact.db:

  1. WI System/world_interaction_sqlite_schema.sql;
  2. WI System/world_interaction_client_sqlite_schema.sql;
  3. WI System/world_interaction_cultivation_sqlite_schema.sql.

Шаг 2 на server DB не опечатка: CultivationCatalog в текущем коде читает world_visual_assets и world_object_state_visuals из того же server DatabaseManager, чтобы сверить exact visual ID и потребовать SKA kind.

Client compact.db:

  1. WI System/world_interaction_client_sqlite_schema.sql.

Main/cultivation gameplay schemas клиенту не нужны. Строки visual catalog, используемые cultivation, должны быть идентично подготовлены в server и client copies.

Server DB path строится простой конкатенацией startup AppPath и config Data SQLite/Path, key берётся из Data SQLite/Encrypt Key; автоматического path join/normalization нет, поэтому Path должен быть именно ожидаемым deployment suffix. Client сначала ищет physical Data\compact.db, затем может читать архивную immutable copy; physical/archive DB пробуется сначала как plain, затем с project SQLCipher key. Не редактировать archive-backed DB во время работы клиента и не раскрывать ключ из исходников/конфигурации.

6.2 PostgreSQL — отдельный поток, не SQLite

Применять только к lc_game PostgreSQL в lexical order:

  1. 0024_world_interaction_operations.sql;
  2. 0025_labor_pools.sql;
  3. 0026_interactive_objects.sql;
  4. 0027_timed_crafting.sql;
  5. 0028_corpse_loot.sql;
  6. 0029_housing_core.sql;
  7. 0030_housing_construction_and_access.sql;
  8. 0031_world_interaction_audit.sql;
  9. 0032_cultivation_and_resources.sql.

Не открывать эти files SQLite и не «адаптировать» PostgreSQL DDL вручную. Project migration stream использует schema_migrations; до применения проверить backup, текущую версию, согласованность t_characters, права DDL и свободное окно обслуживания. В этом документе команды миграции намеренно не выполнялись.

Файлы 00240032 не содержат собственных BEGIN/COMMIT. Project tool Server/Sources/tools/db_migrate.cpp читает каждый файл, исполняет его и вставляет соответствующую row в schema_migrations внутри одной pqxx::work transaction на файл. При ошибке текущий файл и его migration marker должны откатиться вместе; уже committed предыдущие migrations не откатываются. Внешний migration tool обязан обеспечить эквивалентную per-file atomicity и не помечать version применённой раньше успешного DDL. Обратных down-migrations эти файлы не предоставляют.

6.3 Backup/update workflow

  1. Остановить запись приложения штатным deployment-процессом.
  2. Сделать согласованный PostgreSQL backup и byte-copy обеих compact.db; сохранить сведения о SQLCipher/key/version отдельно от backup.
  3. На staging-копии открыть DB тем же SQLCipher-compatible tooling. Plain sqlite3 может принять encrypted file за повреждённый или создать неверную копию.
  4. Применить schema files отдельно в указанном порядке.
  5. Внести content rows одной собственной BEGIN IMMEDIATE ... COMMIT/ROLLBACK transaction.
  6. Выполнить read-only SELECT, PRAGMA foreign_key_check, сверку server/client IDs и наличие ресурсов в package roots.
  7. Увеличить revisions только вместе с согласованным content set.
  8. Развернуть server/client copies атомарной заменой; для клиента обновить physical/package archive source, а не immutable runtime view.
  9. После отдельного разрешения пользователя выполнить startup/build/runtime verification из WI-008. До подтверждённого gate feature bindings держать выключенными.

7. Как добавлять новый контент

7.1 WI-002: NPC action

Существующее действие 1–6:

  1. Выбрать Shop/Storage/Auction/Repair/Teleport/Craft.
  2. В server NPC content установить соответствующий legacy macro flag (NPC_SHOPPER, NPC_KEEPER, NPC1_TRADEAGENT, NPC_REFINER, NPC_ZONEMOVER, NPC1_FACTORY|NPC1_CRAFTING). Не копировать числовой bit из другого branch.
  3. Подготовить обязательный legacy service content (shop/storage/auction/portal/craft NPC data).
  4. Убедиться, что client icon catalog содержит icon ID 1–6; иначе action будет отфильтрован и пустая session закроется.
  5. Проверить targetable NPC и adapter flow.

Новый тип действия: одного INSERT в world_interaction_npc_bindings недостаточно. Нужны согласованные изменения NpcContextActionId client/server, server Build/validation, packet compatibility, client adapter/UI, teardown и icon. Для content-defined действия предпочтительнее WI-003 object action, если это не legacy NPC service.

7.2 WI-003: Graph и authored object

  1. Создать target profile (target_kind=3).
  2. При необходимости создать requirement set только из реально поддерживаемых requirements.
  3. Создать graph, globally unique node IDs и acyclic edges; хотя schema допускает больше, использовать node kinds 0–7 и outcomes 0–2.
  4. Создать action, затем object template/states.
  5. Связать action с текущим state; transition задавать либо SetObjectState, либо next_state_id, а при обоих значения обязаны совпасть.
  6. Для workbench не задавать transition и добавить world_interaction_action_crafts.
  7. Создать authored spawn со стабильным ID/scope.
  8. Добавить exact client object-state visual binding.

7.3 WI-004: Recipe

  1. Добавить одинаковые crafts/craft_materials/craft_products rows в server и client content copies.
  2. Использовать существующие item IDs, category IDs и NPC proto; recipe должен иметь 1–64 materials и 1–64 products.
  3. delay — миллисекунды, максимум loader 23h; rate — 0..100; grade не ниже -1; cost/labor неотрицательны.
  4. Если actability_limit>0, actability_id должен быть положительным и иметь ожидаемый character actability content.
  5. NPC recipe: craft_npc должен совпасть с подтверждённым Craft NPC.
  6. Workbench recipe: создать no-transition object action и mapping action_id -> craft_id.
  7. Не менять active recipe без учёта pinned generation уже запущенных jobs.

7.4 WI-005: Loot/corpse policy

  1. Создать loot pack и entries с существующими items.
  2. Выбрать roll mode; mode 2 не совмещать с persist_across_restart=1.
  3. Создать corpse policy, scope/lifetime и Open/TakeAll action rows/roles.
  4. Привязать t_npc.a_index через world_interaction_npc_corpse_policies.
  5. Начинать с memory-only policy (persist=0), затем отдельно проверять durable/restart.
  6. Проверить fallback: отключённый binding или failed publication не должен одновременно оставить container и ground drop.

world_interaction_target_profiles.max_range, LoS и generic graph у corpse action не управляют текущим corpse runtime: фактический discovery range равен 8.0, после чего CorpseLootArea повторно проверяет собственные distance/area/layer/generation/revision/eligibility. Не пытаться изменить corpse range только правкой target profile.

7.5 WI-006: Housing template и decoration

  1. Создать territory, scope, минимум один простой inclusion polygon; exclusion polygons — отдельно.
  2. Добавить category rule и ownership masks (bits 0..3: character/account/guild/family).
  3. Создать building template и линейную stage chain с ровно одной complete stage.
  4. Каждая constructible stage (не initial) обязана иметь action с skill_id, positive fixed labor profile и материалы; required_actions <= material line count.
  5. Для House cultivation area отсутствует; Farm/Scarecrow обязана иметь area внутри footprint.
  6. При decor создать interior или допустимый farm/scarecrow plot path; floor использует anchor 0, wall/ceiling/socket — authored anchor.
  7. Добавить client stage/decor visuals с exact IDs и is_complete identity.

Для construction action target profile/graph должны существовать из-за FK общей action schema, но текущий housing handler не исполняет generic graph. Gameplay задают stage binding, skill, fixed labor/materials, housing rights/revisions и фиксированный server placement/interaction range. Не помещать housing reward/state mutation в generic graph в расчёте на его выполнение.

7.6 WI-007: Seed/crop/resource

  1. Создать persistent object template без generic automatic transitions.
  2. Для crop — contiguous stages от order 0 до одной mature stage; mature: duration/water/care = 0 и no next.
  3. Все crop actions должны быть distinct, target_kind=6, synchronous, без cast/cooldown/flags, с positive fixed labor profile.
  4. Добавить seed binding и хотя бы один usable placement rule: world environment и/или housing rule.
  5. Для housing rule category mask должен включать crop category, а shape не выходить за published building cultivation area/footprint.
  6. Для resource — active/depleted states, target_kind=7 gather action, loot pack, respawn min/max и минимум один authored spawn.
  7. Для обоих добавить SKA visual rows в server и client DB с exact template/state/asset IDs.

7.7 WI-001: Labor profile и actability

  1. Создать positive enabled world_labor_profiles.id; самый маленький enabled ID становится default profile.
  2. Настроить initial/max, tick rates и offline cap.
  3. Создать fixed cost profile для housing/cultivation или Recipe/Action source для поддерживающего domain.
  4. Для reward указать actability_id и basis-point rate; levels задаются cumulative required_total_exp.
  5. Не путать actability с generic requirement_kind: craft requirement использует crafts.actability_id/actability_limit; reward — labor cost profile.
  6. Modifiers считать неактивными до подтверждения caller, задающего runtime context.

7.8 Общий validation checklist

Это условия будущего content/release review, а не утверждение о выполненных проверках текущего кандидата:

8. Практические SQLite-примеры

Обязательное покрытие Пример
Generic world object + action graph + visual binding 8.2
Living NPC action без выдуманного raw flag 8.8
Recipe caveat + workbench action mapping 8.3
Corpse loot policy 8.4
Housing territory/template/stages + decoration 8.5
Cultivation/seed/growth + authored resource/respawn 8.6
Отдельная сверка visual identity 8.7

Все SQL blocks в этом разделе предназначены только для SQLite compact.db. Они не являются PostgreSQL migrations и не должны исполняться в lc_game; PostgreSQL lifecycle отдельно описан в 5.4 и 6.2.

Все жёстко заданные ID ниже — TEST ONLY из диапазона 910000–969999 и обозначают только rows, создаваемые внутри того же примера. Они не являются production IDs; перед использованием диапазон всё равно надо зарезервировать и проверить на коллизии. Любая внешняя logical reference, которую пример сам не создаёт (item, skill, NPC, icon, legacy craft category, world/zone/area/layer или localization text), записана как именованный bind-параметр :.... Подставлять догадку или ID из другой revision нельзя: параметр должен разрешаться в конкретной целевой copy, даже когда PRAGMA foreign_key_check не способен это проверить.

Каждый mutating snippet рассчитан только на отдельную disposable/staging-копию уже установленной schema и заканчивается ROLLBACK. Не заменять ROLLBACK на COMMIT в приведённом тексте: где DDL предоставляет enabled, примеры намеренно вставляют enabled=0; однако TEST paths, непроверенные bind values и таблицы без такого переключателя всё равно способны остановить server startup или сломать catalog. Для сохраняемого теста сначала подготовить отдельный reviewed вариант с доказанными logical refs/resources, выбранным TEST range, явным enable-step и cleanup plan, и только затем осознанно использовать COMMIT в отдельном файле.

Schema files из раздела 6 нельзя помещать внутрь этих snippets: у каждого schema script уже есть собственные BEGIN IMMEDIATE/COMMIT, поэтому schema install будет отдельным committed шагом и финальный ROLLBACK примера его не отменит. PRAGMA foreign_keys=ON задаётся до BEGIN; в SQLite попытка изменить этот режим внутри transaction не имеет нужного эффекта. Пустой результат PRAGMA foreign_key_check означает только отсутствие нарушений объявленных FK — это не C++ loader test, не проверка logical refs и не runtime test.

Порядок INSERT в примерах намеренный. FK graph -> entry/failure node и object template -> initial state объявлены DEFERRABLE INITIALLY DEFERRED, поэтому graph/template можно вставить перед node/state, но все parents обязаны появиться до commit/проверки. Self-FK stage -> next stage не deferrable; приведённые stage chains вставляются одним multi-row statement, где весь target set существует к завершению statement. Если разбивать такой INSERT, вставлять terminal stage раньше referencing stage либо проектировать отдельную корректную последовательность — не отключать FK.

8.1 Labor и actability

Target DB: server compact.db.
Prerequisites: установлена main WI schema; выбран свободный range.
Verification SELECT: три SELECT ниже проверяют profile, cost и ordered actability levels; затем выполняется FK check.
Rollback/cleanup: snippet всегда завершает transaction через ROLLBACK; для отдельно reviewed committed staging rows порядок cleanup указан после блока.

PRAGMA foreign_keys = ON;
BEGIN IMMEDIATE;

INSERT INTO world_labor_profiles (
    id, code, account_initial, account_max, character_initial, character_max,
    tick_seconds, account_online_per_tick, account_offline_per_tick,
    character_online_per_tick, character_offline_per_tick, max_offline_ticks,
    spend_policy, allow_combined_balance, overflow_policy, enabled
) VALUES (
    910001, 'TEST_LABOR', 100, 100, 20, 20,
    60, 1, 1, 1, 1, 1440,
    0, 1, 0, 0
);

INSERT INTO world_labor_cost_profiles (
    id, code, cost_source, fixed_cost, pool_policy, minimum_cost,
    actability_id, actability_exp_per_labor_bps,
    vocation_points_per_labor_bps, grant_character_exp
) VALUES (910010, 'TEST_FIXED_5', 1, 5, 0, 0, 910100, 10000, 0, 0);

INSERT INTO world_actability_levels (
    actability_id, level, required_total_exp,
    labor_cost_multiplier_bps, action_time_multiplier_bps, reward_multiplier_bps
) VALUES
    (910100, 0, 0,   10000, 10000, 10000),
    (910100, 1, 100,  9500,  9500, 10500);

UPDATE world_interaction_schema_versions
SET content_revision = content_revision + 1
WHERE component = 'world_interaction';

-- Verification SELECT:
SELECT id, code, account_max, character_max, enabled
FROM world_labor_profiles WHERE id = 910001;
SELECT * FROM world_labor_cost_profiles WHERE id = 910010;
SELECT * FROM world_actability_levels WHERE actability_id = 910100 ORDER BY level;
PRAGMA foreign_key_check;

ROLLBACK;

Committed cleanup order: actability levels -> cost profile -> profile. Не удалять profile, пока durable wallets с этим ID существуют.

8.2 Generic graph/object и visual binding

Target DB: server compact.db для gameplay rows; client compact.db для visual rows. Visual rows также зеркалируются в server DB, если этот template будет использовать cultivation validation.
Prerequisites: main + visual schema; реальный packaged .smc вместо TEST path; существующие scope/text/icon bind values и проверенные coordinates.
Verification SELECT: server SELECT проверяют graph nodes, state/action transition и spawn; visual SELECT проверяет exact state/asset binding.
Rollback/cleanup: оба независимых блока заканчиваются ROLLBACK; cleanup order для отдельно committed staging set приведён после них.

-- SERVER compact.db
PRAGMA foreign_keys = ON;
BEGIN IMMEDIATE;

INSERT INTO world_interaction_target_profiles
    (id, target_kind, max_range, require_line_of_sight)
VALUES (920001, 3, 4.0, 1);

INSERT INTO world_interaction_graphs
    (id, code, entry_node_id, max_transitions, timeout_ms, enabled)
VALUES (920010, 'TEST_TOGGLE_GRAPH', 920011, 8, 30000, 0);

INSERT INTO world_interaction_graph_nodes (id, graph_id, node_kind)
VALUES (920011, 920010, 0), (920012, 920010, 6);
INSERT INTO world_interaction_graph_edges
    (graph_id, from_node_id, edge_order, to_node_id, outcome_kind, delay_ms, weight)
VALUES (920010, 920011, 0, 920012, 0, 0, 1);

INSERT INTO world_interaction_actions (
    id, code, icon_id, name_text_id, description_text_id,
    target_profile_id, graph_id, cast_time_ms, cooldown_ms, server_flags, enabled
) VALUES (
    920020, 'TEST_TOGGLE', :toggle_icon_id,
    :toggle_name_text_id, :toggle_description_text_id,
    920001, 920010, 0, 0, 0, 0
);

INSERT INTO world_interaction_object_templates
    (id, code, initial_state_id, persistent, interaction_radius, enabled)
VALUES (920100, 'TEST_SWITCH', 920101, 1, 4.0, 0);
INSERT INTO world_interaction_object_states
    (id, object_template_id, state_order, visual_asset_id)
VALUES
    (920101, 920100, 0, 920501),
    (920102, 920100, 1, 920502);
INSERT INTO world_interaction_object_state_actions
    (object_template_id, object_state_id, action_id, binding_order, next_state_id, exclusive_use)
VALUES (920100, 920101, 920020, 0, 920102, 1);

INSERT INTO world_interaction_object_spawns (
    id, object_template_id, world_id, zone_id, area_id, layer_id,
    position_x, position_y, position_z, rotation, enabled
) VALUES (
    920200, 920100, :world_id, :zone_id, :area_id, :layer_id,
    :position_x, :position_y, :position_z, :rotation, 0
);

UPDATE world_interaction_schema_versions
SET content_revision = content_revision + 1
WHERE component = 'world_interaction';

-- Verification SELECT:
SELECT g.id, n.id AS node_id, n.node_kind
FROM world_interaction_graphs g
JOIN world_interaction_graph_nodes n ON n.graph_id = g.id
WHERE g.id = 920010 ORDER BY n.id;
SELECT osa.object_template_id, osa.object_state_id, osa.action_id, osa.next_state_id
FROM world_interaction_object_state_actions osa
WHERE osa.object_template_id = 920100;
SELECT * FROM world_interaction_object_spawns WHERE id = 920200;
PRAGMA foreign_key_check;
ROLLBACK;
-- CLIENT compact.db; применить тот же visual subset и к server compact.db,
-- если он нужен CultivationCatalog для strict visual validation.
PRAGMA foreign_keys = ON;
BEGIN IMMEDIATE;

INSERT INTO world_visual_assets
    (id, resource_kind, resource_path, scale, enabled)
VALUES
    (920501, 1, 'Data/WorldInteraction/TEST_switch_off.smc', 1.0, 0),
    (920502, 1, 'Data/WorldInteraction/TEST_switch_on.smc', 1.0, 0);
INSERT INTO world_object_state_visuals
    (object_template_id, state_id, visual_asset_id)
VALUES
    (920100, 920101, 920501),
    (920100, 920102, 920502);
UPDATE world_visual_catalog_metadata
SET catalog_revision = catalog_revision + 1 WHERE id = 1;

-- Verification SELECT:
SELECT osv.*, a.resource_kind, a.resource_path
FROM world_object_state_visuals osv
JOIN world_visual_assets a ON a.id = osv.visual_asset_id
WHERE osv.object_template_id = 920100;
PRAGMA foreign_key_check;
ROLLBACK;

Cleanup: spawn -> state action -> states -> template -> action -> edges -> nodes -> graph -> target; visual binding -> assets в каждой затронутой copy. Не удалять static identity, пока она нужна durable interactive_object_instances/transition recovery.

8.3 Timed recipe

Target DB: одинаковые recipe/material/product rows в server и client compact.db; optional workbench mapping только в server DB.
Prerequisites: реальные item IDs, existing craft category visible клиенту, Craft NPC или no-transition workbench action.
Verification SELECT: preflight проверяет packaged DDL, recipe SELECT — созданные связи, mapping SELECT — точное сопоставление action/recipe.
Rollback/cleanup: DDL preflight read-only и не требует rollback/cleanup; оба mutating templates заканчиваются ROLLBACK, cleanup order указан ниже.

Три новые WI schema не создают legacy crafts/craft_materials/craft_products, а их canonical SQLite DDL отсутствует в source SQL этого дерева. Текущие server/client readers подтверждают используемые ниже column names, но не доказывают отсутствие дополнительных NOT NULL columns в конкретной packaged DB. Поэтому сначала отдельно, read-only проверить обе целевые copies; если есть дополнительная колонка без default, расширить reviewed INSERT, а не рассчитывать на этот шаблон. Следующий mutating block — именно условный template после успешного preflight, а не доказанно переносимый INSERT для любой packaged DB:

-- Read-only DDL preflight; no transaction, rollback or cleanup is applicable.
SELECT name, sql
FROM sqlite_master
WHERE type = 'table'
  AND name IN ('crafts', 'craft_materials', 'craft_products')
ORDER BY name;
PRAGMA table_info('crafts');
PRAGMA table_info('craft_materials');
PRAGMA table_info('craft_products');
PRAGMA foreign_key_list('crafts');
PRAGMA foreign_key_list('craft_materials');
PRAGMA foreign_key_list('craft_products');
PRAGMA foreign_keys = ON;
BEGIN IMMEDIATE;

-- Bind values must be verified in both target copies before this template is prepared.
INSERT INTO crafts (
    id, actability_id, actability_limit, delay, recommend_level, cost,
    craft_c_category_id, labor_point, title, craft_npc
) VALUES (
    930001, -1, 0, 5000, 1, 100,
    :craft_category_id, 5, 'TEST timed recipe', :craft_npc_id
);

INSERT INTO craft_materials (id, amount, craft_id, item_id, require_grade)
VALUES (930011, 2, 930001, :material_item_id, -1);
INSERT INTO craft_products (id, amount, craft_id, item_grade_id, item_id, rate, use_grade)
VALUES (930021, 1, 930001, -1, :product_item_id, 100, 0);

-- Verification SELECT:
SELECT c.id, c.delay, c.cost, c.labor_point, c.craft_npc,
       m.item_id AS material, p.item_id AS product, p.rate
FROM crafts c
JOIN craft_materials m ON m.craft_id = c.id
JOIN craft_products p ON p.craft_id = c.id
WHERE c.id = 930001;

PRAGMA foreign_key_check;
ROLLBACK;

Optional server workbench mapping после создания action без next_state_id/SetObjectState. craft_id — logical reference без SQLite FK: recipe 930001 и bind-параметр :workbench_action_id должны реально существовать в той же server DB. Поскольку recipe snippet выше заканчивается ROLLBACK, для общего disposable test mapping надо вставить до того же ROLLBACK, а не запускать следующий block как будто recipe сохранился:

PRAGMA foreign_keys = ON;
BEGIN IMMEDIATE;
INSERT INTO world_interaction_action_crafts (action_id, craft_id)
VALUES (:workbench_action_id, 930001);
UPDATE world_interaction_schema_versions
SET content_revision = content_revision + 1
WHERE component = 'world_interaction';
-- Verification SELECT:
SELECT *
FROM world_interaction_action_crafts
WHERE action_id = :workbench_action_id;
PRAGMA foreign_key_check;
ROLLBACK;

TEST toggle action из 8.2 использовать нельзя: у него есть transition, поэтому runtime workbench его отвергнет. Cleanup recipe: action-craft mapping -> products -> materials -> crafts; active/pinned durable craft jobs сначала должны быть штатно завершены или обработаны утверждённым recovery/rollback plan, а не удалены вручную.

8.4 Corpse loot policy

Target DB: server compact.db.
Prerequisites: main schema; item/NPC/text/icon bind values существуют в соответствующих server/client copies.
Verification SELECT: policy JOIN проверяет NPC binding и обе action roles, loot JOIN — pack entry; затем выполняется FK check.
Rollback/cleanup: блок заканчивается ROLLBACK; отдельно committed cleanup выполняется в порядке после блока и только с учётом durable containers.

Target profile и minimal graph ниже удовлетворяют FK/общему action loader contract. Они не меняют фактический corpse range и не исполняются как corpse reward graph; эти semantics принадлежат NpcInteractionService/CorpseLootArea.

PRAGMA foreign_keys = ON;
BEGIN IMMEDIATE;

INSERT INTO world_interaction_target_profiles
    (id, target_kind, max_range, require_line_of_sight, allow_dead)
VALUES (940001, 4, 8.0, 0, 1);
INSERT INTO world_interaction_graphs
    (id, code, entry_node_id, max_transitions, timeout_ms, enabled)
VALUES (940010, 'TEST_CORPSE_UI_GRAPH', 940011, 4, 30000, 0);
INSERT INTO world_interaction_graph_nodes (id, graph_id, node_kind)
VALUES (940011, 940010, 0), (940012, 940010, 6);
INSERT INTO world_interaction_graph_edges
    (graph_id, from_node_id, edge_order, to_node_id, outcome_kind)
VALUES (940010, 940011, 0, 940012, 0);

INSERT INTO world_interaction_actions (
    id, code, icon_id, name_text_id, description_text_id,
    target_profile_id, graph_id, enabled
) VALUES
    (940020, 'TEST_CORPSE_OPEN', :corpse_open_icon_id, :corpse_open_name_text_id,
     :corpse_open_description_text_id, 940001, 940010, 0),
    (940021, 'TEST_CORPSE_ALL', :corpse_all_icon_id, :corpse_all_name_text_id,
     :corpse_all_description_text_id, 940001, 940010, 0);

INSERT INTO world_interaction_loot_packs
    (id, code, roll_mode, roll_count_min, roll_count_max, allow_empty)
VALUES (940100, 'TEST_CORPSE_PACK', 0, 1, 1, 0);
INSERT INTO world_interaction_loot_entries
    (loot_pack_id, entry_order, item_id, weight, min_count, max_count, min_grade, max_grade)
VALUES (940100, 0, :loot_item_id, 100, 1, 1, 0, 0);

INSERT INTO world_interaction_corpse_policies (
    id, code, lifetime_seconds, exclusive_claim_seconds, claim_scope,
    loot_pack_id, destroy_when_empty, persist_across_restart
) VALUES (940200, 'TEST_MEMORY_CORPSE', 120, 15, 0, 940100, 1, 0);
INSERT INTO world_interaction_corpse_policy_actions
    (corpse_policy_id, action_role, action_id)
VALUES (940200, 0, 940020), (940200, 1, 940021);
INSERT INTO world_interaction_npc_corpse_policies (npc_id, corpse_policy_id)
VALUES (:npc_proto_id, 940200);

UPDATE world_interaction_schema_versions
SET content_revision = content_revision + 1
WHERE component = 'world_interaction';

-- Verification SELECT:
SELECT p.id, p.persist_across_restart, b.npc_id, a.action_role, a.action_id
FROM world_interaction_corpse_policies p
JOIN world_interaction_npc_corpse_policies b ON b.corpse_policy_id = p.id
JOIN world_interaction_corpse_policy_actions a ON a.corpse_policy_id = p.id
WHERE p.id = 940200 ORDER BY a.action_role;
SELECT lp.id, le.entry_order, le.item_id, le.weight
FROM world_interaction_loot_packs lp
JOIN world_interaction_loot_entries le ON le.loot_pack_id = lp.id
WHERE lp.id = 940100;
PRAGMA foreign_key_check;
ROLLBACK;

Cleanup: NPC binding -> policy actions -> policy -> loot entries -> pack -> actions -> graph edges -> graph nodes -> graph -> target profile. Для committed durable policy не удалять definition, пока active PostgreSQL containers с этим policy ID нужны recovery.

8.5 Housing template, territory и decoration

Target DB: server compact.db + matching client visual rows.
Prerequisites: main/visual schema; все item/skill/text/icon/scope bind-параметры разрешаются в целевых copies; valid real coordinates/resources.
Verification SELECT: server SELECT проверяют ordered stages, polygon vertices и decor template; client SELECT — stage/decor visual bindings; оба блока завершаются FK check.
Rollback/cleanup: server и client blocks независимо заканчиваются ROLLBACK; cleanup для отдельно committed staging rows идёт от bindings/children к parents с проверкой durable references.

-- SERVER compact.db
PRAGMA foreign_keys = ON;
BEGIN IMMEDIATE;

INSERT INTO world_labor_cost_profiles
    (id, code, cost_source, fixed_cost, pool_policy, minimum_cost)
VALUES (950001, 'TEST_BUILD_5', 1, 5, 0, 0);
INSERT INTO world_interaction_target_profiles (id, target_kind, max_range)
VALUES (950010, 5, 12.0);
INSERT INTO world_interaction_graphs
    (id, code, entry_node_id, max_transitions, timeout_ms, enabled)
VALUES (950020, 'TEST_BUILD_GRAPH', 950021, 4, 30000, 0);
INSERT INTO world_interaction_graph_nodes (id, graph_id, node_kind)
VALUES (950021, 950020, 0), (950022, 950020, 6);
INSERT INTO world_interaction_graph_edges
    (graph_id, from_node_id, edge_order, to_node_id, outcome_kind)
VALUES (950020, 950021, 0, 950022, 0);
INSERT INTO world_interaction_actions (
    id, code, icon_id, name_text_id, description_text_id, target_profile_id,
    graph_id, skill_id, labor_cost_profile_id, enabled
) VALUES (
    950030, 'TEST_BUILD_ACTION', :construction_icon_id,
    :construction_name_text_id, :construction_description_text_id,
    950010, 950020, :construction_skill_id, 950001, 0
);

INSERT INTO world_build_territories (id, code, priority, permission_mask, enabled)
VALUES (950100, 'TEST_TERRITORY', 0, 1, 0);
INSERT INTO world_build_territory_scopes (
    id, territory_id, world_id, zone_id, area_id, y_layer, scope_order,
    minimum_height, maximum_height, maximum_slope
) VALUES (
    950110, 950100, :world_id, :zone_id, :area_id, :y_layer,
    0, -100.0, 100.0, 4.0
);
INSERT INTO world_build_territory_polygons
    (id, territory_id, territory_scope_id, polygon_order, is_exclusion)
VALUES (950120, 950100, 950110, 0, 0);
INSERT INTO world_build_territory_vertices (polygon_id, vertex_index, x, z)
VALUES
    (950120, 0, 90.0,  90.0), (950120, 1, 110.0, 90.0),
    (950120, 2, 110.0, 110.0), (950120, 3, 90.0, 110.0);
INSERT INTO world_build_category_rules
    (territory_id, category_id, max_total, max_per_owner, min_spacing, permission_mask)
VALUES (950100, 0, 10, 1, 1.0, 1);

INSERT INTO world_building_templates (
    id, code, category_id, blueprint_item_id, blueprint_item_count,
    placement_nas_cost, initial_stage_id, max_health,
    footprint_kind, footprint_radius, owner_limit, decoration_limit, enabled
) VALUES (950200, 'TEST_HOUSE', 0, :blueprint_item_id, 1, 0, 950201, 1000,
          1, 2.0, 1, 4, 0);
INSERT INTO world_building_stages (
    id, building_template_id, stage_order, action_id, build_time_seconds,
    required_actions, next_stage_id, visual_asset_id, is_complete
) VALUES
    (950201, 950200, 0, NULL,    0, 1, 950202, 950501, 0),
    (950202, 950200, 1, 950030,  0, 1, NULL,   950502, 1);
INSERT INTO world_building_stage_materials
    (stage_id, item_id, required_count, min_grade, material_order)
VALUES (950202, :construction_material_item_id, 1, 0, 0);
INSERT INTO world_building_interiors
    (building_template_id, min_x, max_x, min_z, max_z, min_h, max_h, complete_only)
VALUES (950200, -1.5, 1.5, -1.5, 1.5, 0.0, 3.0, 1);

INSERT INTO world_housing_decoration_templates (
    id, code, source_item_id, visual_asset_id, footprint_kind,
    footprint_radius, footprint_half_x, footprint_half_z, footprint_height,
    min_scale, max_scale, allowed_anchor_mask, socket_type, enabled
) VALUES (950300, 'TEST_FLOOR_DECOR', :decoration_item_id, 950503, 1,
          0.25, 0.0, 0.0, 1.0, 1.0, 1.0, 1, 0, 0);

UPDATE world_interaction_schema_versions
SET content_revision = content_revision + 1
WHERE component = 'world_interaction';
-- Verification SELECT:
SELECT * FROM world_building_stages WHERE building_template_id = 950200 ORDER BY stage_order;
SELECT polygon_id, vertex_index, x, z FROM world_build_territory_vertices WHERE polygon_id = 950120;
SELECT id, source_item_id, visual_asset_id
FROM world_housing_decoration_templates
WHERE id = 950300;
PRAGMA foreign_key_check;
ROLLBACK;
-- CLIENT compact.db
PRAGMA foreign_keys = ON;
BEGIN IMMEDIATE;
INSERT INTO world_visual_assets (id, resource_kind, resource_path, scale, enabled)
VALUES
    (950501, 1, 'Data/WorldInteraction/TEST_house_foundation.smc', 1.0, 0),
    (950502, 2, 'Data/WorldInteraction/TEST_house_complete.mdl',   1.0, 0),
    (950503, 1, 'Data/WorldInteraction/TEST_floor_decor.smc',     1.0, 0);
INSERT INTO world_building_stage_visuals
    (building_template_id, stage_id, visual_asset_id, is_complete)
VALUES (950200, 950201, 950501, 0), (950200, 950202, 950502, 1);
INSERT INTO world_housing_decoration_visuals (decoration_template_id, visual_asset_id)
VALUES (950300, 950503);
UPDATE world_visual_catalog_metadata
SET catalog_revision = catalog_revision + 1 WHERE id = 1;
-- Verification SELECT:
SELECT * FROM world_building_stage_visuals WHERE building_template_id = 950200;
SELECT *
FROM world_housing_decoration_visuals
WHERE decoration_template_id = 950300;
PRAGMA foreign_key_check;
ROLLBACK;

Cleanup выполняется от visual/bindings и territory vertices к родителям. Не удалять template, используемый PostgreSQL housing_buildings/housing_decorations recovery.

8.6 Crop, seed и authored resource

Target DB: server compact.db; visual subset — server и client compact.db.
Prerequisites: все три server schemas, visual schema client, все item/text/icon/scope bind-параметры существуют, resource paths реально packaged; PostgreSQL 0024–0032 применяются только до отдельно разрешённого runtime enable.
Verification SELECT: server SELECT проверяют ordered crop stages, seed binding и authored resource spawn; client SELECT — exact mirrored visuals; оба блока выполняют FK check.
Rollback/cleanup: server и client blocks независимо заканчиваются ROLLBACK; cleanup отдельно committed staging set приведён после блоков и не удаляет durable PostgreSQL rows.

-- SERVER compact.db: минимальный TEST crop + resource
PRAGMA foreign_keys = ON;
BEGIN IMMEDIATE;

INSERT INTO world_labor_cost_profiles
    (id, code, cost_source, fixed_cost, pool_policy, minimum_cost)
VALUES (960001, 'TEST_GATHER_1', 1, 1, 0, 0);
INSERT INTO world_interaction_target_profiles (id, target_kind, max_range)
VALUES (960010, 6, 12.0), (960011, 7, 12.0);
INSERT INTO world_interaction_graphs
    (id, code, entry_node_id, max_transitions, timeout_ms, enabled)
VALUES (960020, 'TEST_SYNC_GRAPH', 960021, 4, 30000, 0);
INSERT INTO world_interaction_graph_nodes (id, graph_id, node_kind)
VALUES (960021, 960020, 0), (960022, 960020, 6);
INSERT INTO world_interaction_graph_edges
    (graph_id, from_node_id, edge_order, to_node_id, outcome_kind)
VALUES (960020, 960021, 0, 960022, 0);
INSERT INTO world_interaction_actions (
    id, code, icon_id, name_text_id, description_text_id,
    target_profile_id, graph_id, labor_cost_profile_id,
    cast_time_ms, cooldown_ms, server_flags, enabled
) VALUES
    (960030, 'TEST_PLANT', :plant_icon_id, :plant_name_text_id, :plant_description_text_id,
     960010, 960020, 960001, 0, 0, 0, 0),
    (960031, 'TEST_CARE', :care_icon_id, :care_name_text_id, :care_description_text_id,
     960010, 960020, 960001, 0, 0, 0, 0),
    (960032, 'TEST_HARVEST', :harvest_icon_id, :harvest_name_text_id, :harvest_description_text_id,
     960010, 960020, 960001, 0, 0, 0, 0),
    (960033, 'TEST_REMOVE', :remove_icon_id, :remove_name_text_id, :remove_description_text_id,
     960010, 960020, 960001, 0, 0, 0, 0),
    (960034, 'TEST_GATHER', :gather_icon_id, :gather_name_text_id, :gather_description_text_id,
     960011, 960020, 960001, 0, 0, 0, 0);

INSERT INTO world_interaction_loot_packs
    (id, code, roll_mode, roll_count_min, roll_count_max, allow_empty)
VALUES (960100, 'TEST_HARVEST_PACK', 0, 1, 1, 0);
INSERT INTO world_interaction_loot_entries
    (loot_pack_id, entry_order, item_id, weight, min_count, max_count, min_grade, max_grade)
VALUES (960100, 0, :harvest_item_id, 100, 1, 1, 0, 0);

INSERT INTO world_interaction_object_templates
    (id, code, initial_state_id, persistent, interaction_radius, enabled)
VALUES
    (960200, 'TEST_CROP_OBJECT',     960201, 1, 4.0, 0),
    (960300, 'TEST_RESOURCE_OBJECT', 960301, 1, 4.0, 0);
INSERT INTO world_interaction_object_states
    (id, object_template_id, state_order, visual_asset_id)
VALUES
    (960201, 960200, 0, 960501), (960202, 960200, 1, 960502),
    (960301, 960300, 0, 960503), (960302, 960300, 1, 960504);

-- Server-side mirror used by CultivationCatalog strict visual validation.
INSERT INTO world_visual_assets (id, resource_kind, resource_path, scale, enabled)
VALUES
    (960501, 1, 'Data/WorldInteraction/TEST_crop_seed.smc', 1.0, 0),
    (960502, 1, 'Data/WorldInteraction/TEST_crop_mature.smc', 1.0, 0),
    (960503, 1, 'Data/WorldInteraction/TEST_ore_active.smc', 1.0, 0),
    (960504, 1, 'Data/WorldInteraction/TEST_ore_depleted.smc', 1.0, 0);
INSERT INTO world_object_state_visuals (object_template_id, state_id, visual_asset_id)
VALUES
    (960200, 960201, 960501), (960200, 960202, 960502),
    (960300, 960301, 960503), (960300, 960302, 960504);

INSERT INTO world_cultivation_profiles (
    id, code, object_template_id, category_id, placement_action_id,
    care_action_id, harvest_action_id, remove_action_id, loot_pack_id,
    allow_world_placement, allow_housing_placement, show_owner,
    placement_radius, maximum_slope, placement_flags, enabled
) VALUES (960400, 'TEST_CROP', 960200, 0, 960030,
           960031, 960032, 960033, 960100,
           1, 0, 1, 0.5, 4.0, 0, 0);
INSERT INTO world_cultivation_stages (
    id, cultivation_profile_id, object_template_id, stage_order,
    object_state_id, duration_seconds, required_water, required_care,
    next_stage_id, is_mature
) VALUES
    (960401, 960400, 960200, 0, 960201, 60, 0, 0, 960402, 0),
    (960402, 960400, 960200, 1, 960202,  0, 0, 0, NULL,   1);
INSERT INTO world_cultivation_planting_bindings
    (seed_item_id, cultivation_profile_id, consume_count)
VALUES (:seed_item_id, 960400, 1);
INSERT INTO world_cultivation_environment_rules (
    cultivation_profile_id, world_id, zone_id, area_id, layer_id,
    growth_time_multiplier_bps, placement_allowed
) VALUES (
    960400, :world_id, :zone_id, :area_id, :layer_id, 10000, 1
);

INSERT INTO world_resource_profiles (
    id, code, object_template_id, gather_action_id, loot_pack_id,
    depleted_state_id, respawn_seconds_min, respawn_seconds_max, enabled
) VALUES (960410, 'TEST_ORE', 960300, 960034, 960100, 960302, 60, 120, 0);
INSERT INTO world_resource_spawns (
    id, resource_profile_id, world_id, zone_id, area_id, layer_id,
    position_x, position_y, position_z, rotation, enabled
) VALUES (
    960420, 960410, :world_id, :zone_id, :area_id, :layer_id,
    :position_x, :position_y, :position_z, :rotation, 0
);

UPDATE world_interaction_schema_versions
SET content_revision = content_revision + 1
WHERE component = 'world_interaction';
UPDATE world_visual_catalog_metadata
SET catalog_revision = catalog_revision + 1
WHERE id = 1;

-- Verification SELECT:
SELECT p.id, p.code, s.id AS stage_id, s.stage_order, s.is_mature
FROM world_cultivation_profiles p
JOIN world_cultivation_stages s ON s.cultivation_profile_id = p.id
WHERE p.id = 960400 ORDER BY s.stage_order;
SELECT seed_item_id, cultivation_profile_id, consume_count
FROM world_cultivation_planting_bindings
WHERE cultivation_profile_id = 960400;
SELECT rp.id, rs.id AS spawn_id, rs.zone_id
FROM world_resource_profiles rp JOIN world_resource_spawns rs
  ON rs.resource_profile_id = rp.id WHERE rp.id = 960410;
PRAGMA foreign_key_check;
ROLLBACK;
-- CLIENT compact.db: exact mirror visual subset
PRAGMA foreign_keys = ON;
BEGIN IMMEDIATE;
INSERT INTO world_visual_assets (id, resource_kind, resource_path, scale, enabled)
VALUES
    (960501, 1, 'Data/WorldInteraction/TEST_crop_seed.smc', 1.0, 0),
    (960502, 1, 'Data/WorldInteraction/TEST_crop_mature.smc', 1.0, 0),
    (960503, 1, 'Data/WorldInteraction/TEST_ore_active.smc', 1.0, 0),
    (960504, 1, 'Data/WorldInteraction/TEST_ore_depleted.smc', 1.0, 0);
INSERT INTO world_object_state_visuals (object_template_id, state_id, visual_asset_id)
VALUES
    (960200, 960201, 960501), (960200, 960202, 960502),
    (960300, 960301, 960503), (960300, 960302, 960504);
UPDATE world_visual_catalog_metadata
SET catalog_revision = catalog_revision + 1 WHERE id = 1;
-- Verification SELECT:
SELECT v.object_template_id, v.state_id, v.visual_asset_id, a.resource_path
FROM world_object_state_visuals v
JOIN world_visual_assets a ON a.id = v.visual_asset_id
WHERE v.object_template_id IN (960200, 960300)
ORDER BY v.object_template_id, v.state_id;
PRAGMA foreign_key_check;
ROLLBACK;

Cleanup: resource spawn/profile, environment/seed/stages/profile, visual bindings/assets, states/templates, loot, actions/graph/targets/labor. Server и client visual cleanup выполнять в обеих copies; durable PostgreSQL entities предварительно не удалять вручную.

8.7 Проверка server/client visual identity без ATTACH

Encrypted/package copies могут требовать разного tooling, поэтому не использовать случайный ATTACH одной production DB к другой. Выполнить одинаковые read-only SELECT отдельно на подготовленных server/client copies и сравнить выгрузки по ключу:

Target DB: отдельные read-only server и client compact.db copies; не соединять их через ATTACH.
Prerequisites: SQLCipher-compatible tooling и заранее выбранные реальные bind IDs.
Verification SELECT: весь блок является набором verification SELECT для object, cultivation/resource и housing identities.
Rollback/cleanup: не применимо — блок не начинает transaction и ничего не изменяет.

-- Read-only verification SELECT; no rollback/cleanup is applicable.
-- Client DB; для cultivation/resources тот же result set обязан быть и в server DB.
SELECT v.object_template_id, v.state_id, v.visual_asset_id,
       a.resource_kind, a.resource_path
FROM world_object_state_visuals v
JOIN world_visual_assets a ON a.id = v.visual_asset_id
WHERE v.object_template_id IN (:template_id_1, :template_id_2)
ORDER BY v.object_template_id, v.state_id;

-- Server gameplay identity generic object (и основа проверки cultivation/resource).
SELECT object_template_id, id AS state_id, visual_asset_id
FROM world_interaction_object_states
WHERE object_template_id IN (:template_id_1, :template_id_2)
ORDER BY object_template_id, id;

-- Housing: выполнить на client DB и сопоставить IDs с server
-- world_building_stages/world_housing_decoration_templates.
SELECT building_template_id, stage_id, visual_asset_id, is_complete
FROM world_building_stage_visuals
WHERE building_template_id = :building_template_id
ORDER BY stage_id;
SELECT decoration_template_id, visual_asset_id
FROM world_housing_decoration_visuals
WHERE decoration_template_id = :decoration_template_id;

Для generic object server DB может не иметь mirror binding row, если template не относится к cultivation: тогда source of truth на server — world_interaction_object_states.visual_asset_id, а сравнивать надо его с client query. Для cultivation/resource mirror rows обязательны в обеих DB. Ни один из этих SELECT не проверяет наличие .smc/.mdl в package и не заменяет разрешённый впоследствии loader/runti