docs: present Shacraft Core and its documentation in English

This commit is contained in:
Emil
2026-09-14 17:58:30 +03:00
parent b6ba064339
commit 26748912c6
18 changed files with 643 additions and 620 deletions
+111 -111
View File
@@ -1,188 +1,188 @@
# Память, хранение и восстановление Shacraft Core
# Shacraft Core memory, storage, and recovery
Статус: план новой реализации с принятыми решениями D008–D009. Этот файл задаёт инварианты и критерии проверки; он не утверждает, что перечисленные механизмы уже реализованы. Публичный API находится в [CONTRACT.md](CONTRACT.md), принятые решения — в [DECISIONS.md](DECISIONS.md). Дополнительные предложения отдельно перечислены в конце.
Status: implementation plan incorporating decisions D008–D009. This file defines invariants and verification criteria; it does not claim that all mechanisms listed here have already been implemented. The public API is in [CONTRACT.md](CONTRACT.md), and accepted decisions are in [DECISIONS.md](DECISIONS.md). Additional proposals are listed separately at the end.
## 1. Главная цель и граница обещаний
## 1. Primary goal and scope of guarantees
Главная цель ядра — ограничить серверную RAM при большом числе независимых матчей на одинаковых картах. Неизменённые блоки общего шаблона хранятся один раз. Матч владеет только собственными изменениями. Данные неактивных областей и история операций остаются на диске.
The core's primary goal is to bound server RAM when many independent matches use identical maps. Unchanged blocks in a shared template are stored once. A match owns only its changes. Inactive areas and operation history remain on disk.
Нельзя заранее обещать процент экономии относительно Paper или конкретное число игроков на гигабайт. Экономия зависит от разнообразия блоков, числа одновременно активных секций, степени изменения карт, сущностей, клиентской видимости и нагрузки. Успех подтверждают воспроизводимые измерения вместе с функциональными ограничениями реализации.
Do not promise a percentage saving relative to Paper or a specific number of players per gigabyte in advance. Savings depend on block diversity, the number of simultaneously active sections, how much maps change, entities, client visibility, and workload. Success is demonstrated by reproducible measurements alongside the implementation's functional limitations.
Бюджет процесса включает больше, чем блоки: кэш секций, временные декодированные секции, индекс хранилища, реестр блоков, метаданные миров, историю, очереди сети, игроков и сущности. Ограничение одного кэша не означает ограничения всей RAM.
The process budget includes more than blocks: the section cache, temporary decoded sections, storage indexes, block registry, world metadata, history, network queues, players, and entities. Bounding one cache does not bound all RAM.
## 2. Адресация и формат секции
## 2. Addressing and section format
Единица хранения — секция 16 × 16 × 16, то есть 4096 ячеек. Координаты мира остаются знаковыми `i32` из контракта. Для каждой оси использовать `div_euclid(16)` и `rem_euclid(16)`: блок `-1` попадает в секцию `-1`, локальную координату `15`. Индекс ячейки: `x + 16*z + 256*y`. Этот порядок фиксируется версией формата.
The storage unit is a 16 × 16 × 16 section, or 4,096 cells. World coordinates remain signed `i32` values as specified by the contract. Use `div_euclid(16)` and `rem_euclid(16)` on each axis: block `-1` belongs to section `-1` at local coordinate `15`. The cell index is `x + 16*z + 256*y`. The format version fixes this order.
В первом этапе секция выбирает компактную uniform/palette кодировку:
In the first stage, a section chooses a compact uniform/palette encoding:
- `Uniform(BlockId)`: все 4096 блоков одинаковы; массив индексов отсутствует.
- `Paletted`: уникальные глобальные `BlockId` и плотно упакованные индексы палитры. Для `P > 1` требуется `ceil(log2(P))` бит на ячейку.
- `Uniform(BlockId)`: all 4,096 blocks are identical; there is no index array.
- `Paletted`: unique global `BlockId` values and densely packed palette indices. For `P > 1`, each cell requires `ceil(log2(P))` bits.
Будущее расширение `Dense` может хранить 4096 глобальных `u32`, если палитра вместе с индексами занимает больше места. Это требует явной версии/тега кодировки и пока не входит в принятый минимальный контракт.
A future `Dense` extension may store 4,096 global `u32` values when the palette and its indices take more space. This requires an explicit encoding version/tag and is not yet part of the accepted minimum contract.
Без заголовков и выравнивания палитра занимает `4*P + 4096*ceil(log2(P))/8` байт. При двух состояниях это 520 байт, при 16 — 2112, при 256 — 5120. При 4096 уникальных состояниях получается 22528 байт, поэтому прямой массив в 16384 байта выгоднее. Это расчёт полезных данных, а не RSS и не размер объекта Rust.
Without headers or alignment, a palette occupies `4*P + 4096*ceil(log2(P))/8` bytes. That is 520 bytes for two states, 2,112 for 16, and 5,120 for 256. With 4,096 unique states it reaches 22,528 bytes, making a direct 16,384-byte array smaller. This calculates payload size, not RSS or Rust object size.
При чтении одного блока не нужно распаковывать всю секцию. Изменение использует временный массив на 4096 значений и затем заново выбирает кодировку. Палитра после редактирования должна удалять неиспользуемые состояния. Дисковое сжатие можно добавить после замеров; оно не заменяет ограничение кэшей.
Reading one block should not require decoding the entire section. An edit uses a temporary array of 4,096 values, then selects the encoding again. Editing must remove unused states from the palette. Disk compression can be added after measurement; it does not replace cache limits.
Декодер проверяет версию, длины, размер палитры, диапазон каждого индекса и существование глобальных `BlockId`. Повреждённая секция возвращает ошибку с её идентификатором; подмена повреждения воздухом запрещена. Кодировка имеет фиксированный порядок байтов и контроль целостности. Контрольная сумма обнаруживает повреждение, но не является защитой от намеренной подмены.
The decoder validates the version, lengths, palette size, range of every index, and existence of global `BlockId` values. A corrupt section returns an error identifying the section; silently substituting air is prohibited. The encoding has a fixed byte order and integrity checks. A checksum detects corruption but does not protect against deliberate tampering.
## 3. Общий шаблон и независимые миры
## 3. Shared templates and independent worlds
Шаблон — неизменяемый снимок состояния мира, а не ссылка на его текущее изменяемое состояние. Внутренние идентификаторы мира, снимка и секции отличаются от пользовательских имён. `WorldInfo.template` может показывать имя источника, но внутренние данные обязаны хранить точный `snapshot_id` и ревизию источника.
A template is an immutable snapshot of a world, not a reference to its current mutable state. Internal world, snapshot, and section IDs are distinct from user-facing names. `WorldInfo.template` may display the source name, but internal data must retain the exact `snapshot_id` and source revision.
Мир содержит ссылку на базовый снимок и собственную таблицу замен секций. Чтение ищет замену мира, затем секцию в снимке, затем возвращает воздух. Явная замена на полностью воздушную секцию обязательна: отсутствие замены означает наследование, поэтому удаление всех блоков шаблона нельзя представлять отсутствующей записью.
A world holds a reference to its base snapshot and its own section override table. A read looks for a world override, then a section in the snapshot, and otherwise returns air. An explicit all-air section override is required: absence of an override means inheritance, so removing all template blocks cannot be represented by a missing entry.
При первом изменении секции из шаблона создаётся новая секция мира. Сама секция шаблона никогда не изменяется. Одинаковые ссылки на неизменяемые секции могут использовать один объект кэша. Возврат секции к точному содержимому шаблона позволяет удалить замену после проверки равенства.
The first edit to a template section creates a new section owned by the world. The template section itself never changes. Identical references to immutable sections can share one cached object. Restoring a section to the exact template content allows its override to be removed after checking equality.
Практическая схема MVP:
Practical MVP design:
1. Неизменяемые секции хранятся по идентификаторам на диске.
2. Снимок содержит дисковый индекс `координата секции → идентификатор секции`.
3. Мир содержит `snapshot_id`, текущую ревизию и дисковый индекс собственных замен.
4. При `create_world(template=source)` снимок берётся строго из одной зафиксированной ревизии источника. Для неизменившегося источника ранее созданный снимок можно переиспользовать.
5. Если нового снимка ещё нет, индекс эффективных секций копируется потоково по ссылкам, внутри согласованной транзакции. Сами блоки и содержимое секций не копируются в RAM или на диск повторно.
1. Immutable sections are stored on disk by ID.
2. A snapshot contains an on-disk index mapping `section coordinate → section ID`.
3. A world contains a `snapshot_id`, its current revision, and an on-disk index of its own overrides.
4. `create_world(template=source)` takes a snapshot from exactly one committed source revision. A previously created snapshot may be reused if the source has not changed.
5. If no snapshot exists for that revision, the index of effective sections is copied incrementally by reference within a consistent transaction. Blocks and section contents are not copied into RAM or duplicated on disk.
Создание нового снимка таким способом может потребовать времени и места, пропорциональных числу ссылок на секции. Обещать `O(1)` для первого fork нельзя. Более сложный постоянный индекс с разделяемыми страницами возможен позже, если измерения покажут необходимость. Не следует заменять эту схему бесконечной цепочкой родительских миров: глубокая цепочка ухудшает чтения и усложняет сборку мусора.
Creating a snapshot this way can take time and space proportional to the number of section references. The first fork must not be promised as `O(1)`. A more complex persistent index with shared pages can be added later if measurements show a need. Do not replace this design with an unbounded chain of parent worlds: deep chains slow reads and complicate garbage collection.
Проверка изоляции: изменить источник после fork, изменить один из двух дочерних миров, перезапустить процесс и убедиться, что три состояния различаются именно ожидаемым образом. Снимок остаётся доступным, даже если источник сброшен или впоследствии удалён.
Isolation check: modify the source after a fork, modify one of two child worlds, restart the process, and verify that all three states differ exactly as expected. The snapshot remains available even if the source is reset or later deleted.
## 4. Ограничение памяти
## 4. Bounding memory
`cache_sections` ограничивает число сохранённых в кэше неизменяемых секций. Ноль явно отклоняется при открытии, как принято в CONTRACT. Для первой реализации используется LRU декодированных секций: массив на 4096 `u32` имеет известный размер 16384 байта полезной нагрузки, а объекты и индекс LRU учитываются дополнительно. Хранение палитр непосредственно в кэше — последующая оптимизация; компактная дисковая кодировка сама по себе не уменьшает декодированный кэш.
`cache_sections` limits the number of immutable sections retained in the cache. Opening with zero is explicitly rejected, as accepted in CONTRACT. The first implementation uses an LRU of decoded sections: an array of 4,096 `u32` values has a known 16,384-byte payload, with objects and the LRU index counted separately. Storing palettes directly in the cache is a later optimization; compact disk encoding alone does not shrink a decoded cache.
Дополнительно контролировать байты кэша: учитывать буфер секции, палитру и метаданные записи, показывать их отдельно от полезной нагрузки. Ключ кэша — идентификатор неизменяемой секции. Кэш только по имени мира и координате легко оставляет устаревшие данные после reset.
Also track cache bytes: account for the section buffer, palette, and entry metadata, and report these separately from payload size. The cache key is the immutable section ID. A cache keyed only by world name and coordinates can easily retain stale data after reset.
Временные буферы транзакции не попадают в бесконечный «грязный» кэш. Обработка секций идёт ограниченными порциями; завершённые записи передаются дисковому транзакционному механизму. Полученный через API `Vec<BlockChange>` уже занимает RAM, поэтому потоковая внутренняя запись не отменяет ограничения размера запроса.
Temporary transaction buffers must not enter an unbounded dirty cache. Sections are processed in bounded batches, with completed writes passed to the disk transaction mechanism. A `Vec<BlockChange>` received through the API already occupies RAM, so streaming internal writes does not remove the need for request size limits.
Историю операций, дедупликацию и индекс секций нельзя загружать целиком при старте. Используется дисковый SQL-индекс с ограниченным кэшем страниц SQLite. Настроить и измерять бюджет страниц и временные данные. Выбор библиотеки сам по себе не доказывает соблюдение бюджета.
Operation history, deduplication records, and section indexes must not be loaded in full at startup. Use an on-disk SQL index with a bounded SQLite page cache. Configure and measure the page and temporary-data budgets. Choosing a library does not by itself prove that a budget is respected.
Метаданные не бесплатны. `registry() -> &[String]` из контракта предполагает реестр в RAM; `list_worlds()` создаёт полный список. Для MVP задать явные ограничения на число миров и состояний и длину строк, отразить их в документации. При выходе за эти границы потребуется API с постраничной выдачей.
Metadata is not free. The contract's `registry() -> &[String]` implies an in-memory registry; `list_worlds()` creates a complete list. Define and document explicit MVP limits on world count, state count, and string length. Exceeding those boundaries will require a paginated API.
Принятые ограничения:
Accepted limits:
- одна операция редактирования — не более 32768 уникальных позиций;
- `read_region` — максимум 262144 ячейки объёма, как в контракте; считать произведение через проверяемую широкую арифметику до выделения памяти;
- имя мира соответствует `[A-Za-z0-9_-]{1,64}`; координаты каждой оси находятся в диапазоне `[-30000000,30000000]`;
- сервер дополнительно ограничивает размер JSON до десериализации, частоту операций, сетевые очереди и число одновременных запросов;
- максимальное количество сущностей и объём очереди изменений задаются отдельно от кэша блоков.
- At most 32,768 unique positions in one edit operation.
- `read_region` covers at most 262,144 cells, as specified by the contract; compute the product with checked wide arithmetic before allocating memory.
- A world name matches `[A-Za-z0-9_-]{1,64}`; coordinates on each axis fall within `[-30000000,30000000]`.
- The server additionally bounds JSON size before deserialization, operation frequency, network queues, and concurrent requests.
- Maximum entity count and change queue size are defined separately from the block cache.
Для ещё не зафиксированного ограничения `operation_id` предлагается от 1 до 128 байт UTF-8. Точные пределы реестра и остальных очередей задаются в реализации и документируются. Размеры должны быть проверены на реальном workload. Ограничение `read_region` относится к объёму области, а не только к числу возвращённых непустых блоков.
For the not-yet-finalized `operation_id` limit, 1 to 128 UTF-8 bytes is proposed. Exact registry and other queue limits are defined and documented in the implementation. Sizes must be tested with a real workload. The `read_region` limit applies to the volume of the region, not just the number of nonempty blocks returned.
## 5. Дисковая история и атомарность
## 5. On-disk history and atomicity
Минимальные логические сущности хранилища: реестр блоков, миры, снимки, ссылки снимков на секции, замены секций миров, неизменяемые секции, операции и изменения операций. История хранит старые и новые состояния затронутых ячеек, тип операции, ревизии, идентификатор и отпечаток нормализованного запроса. История нужна для undo и идемпотентных повторов; обычный журнал сообщений сервера её не заменяет.
The minimum logical storage entities are the block registry, worlds, snapshots, snapshot-to-section references, world section overrides, immutable sections, operations, and operation changes. History stores the previous and new state of each affected cell, operation type, revisions, ID, and a fingerprint of the normalized request. History supports undo and idempotent retries; an ordinary server message log does not replace it.
Дедупликация находится на диске с уникальным ключом `(world_id, operation_id)`. Не держать все идентификаторы операций в `HashMap`. Для поиска отменяемой операции и последующих изменений ячеек нужны дисковые индексы, а не чтение всего журнала на каждый undo.
Deduplication is stored on disk with the unique key `(world_id, operation_id)`. Do not retain all operation IDs in a `HashMap`. Finding the operation to undo and subsequent cell changes requires disk indexes, rather than scanning the entire log for every undo.
Одна успешная мутация атомарно фиксирует:
One successful mutation atomically commits:
1. Новые секции и изменения ссылок мира.
2. Новую ревизию принятой операции, включая no-op.
3. Запись операции, её отпечаток и точный результат для повторной выдачи.
4. Данные истории, необходимые для безопасного undo.
1. New sections and changes to world references.
2. The new revision of the accepted operation, including a no-op.
3. The operation record, its fingerprint, and the exact result to return on replay.
4. History data required for safe undo.
Подтверждение успеха клиенту отправляется после долговечной фиксации всех четырёх частей. Ошибка записи не должна оставлять новую ревизию с прежними блоками или блоки без записи дедупликации. Выделение нового `BlockId` также долговечно: переоткрытие хранилища не перенумеровывает реестр; `air` всегда имеет ID 0.
The client receives a success acknowledgment after all four parts are durably committed. A write failure must not leave a new revision with old blocks, or changed blocks without a deduplication record. Allocating a new `BlockId` is also durable: reopening the store does not renumber the registry, and `air` always has ID 0.
По D008 используется SQLite с WAL и `synchronous=FULL`; собственный WAL не реализуется. Секции остаются собственным версионированным бинарным форматом внутри транзакционного хранилища. Проверить фактическое применение настроек соединения. Не приравнивать запись в буфер ОС к долговечному commit. Гарантии долговечности предполагают исправную файловую систему и устройство, корректно исполняющее запросы синхронизации.
Decision D008 selects SQLite with WAL and `synchronous=FULL`; a custom WAL is not implemented. Sections retain an original, versioned binary format inside the transactional store. Verify that the connection settings actually take effect. Writing to an OS buffer is not a durable commit. Durability guarantees assume a healthy filesystem and device that correctly honor synchronization requests.
На одном пути хранилища допускается один владелец записи. `&mut WorldStore` защищает только конкретный объект Rust: второй процесс или второй независимо открытый объект отклоняется эксклюзивной advisory-блокировкой файла. Блокировка сохраняется весь срок жизни `WorldStore`; сам факт существования lock-файла не означает занятую блокировку. Второе открытие не должно молча порождать две независимые картины метаданных.
A storage path permits one writer. `&mut WorldStore` protects only a particular Rust object: a second process or independently opened object is rejected by an exclusive advisory file lock. The lock remains held for the lifetime of `WorldStore`; the existence of a lock file alone does not mean that the lock is held. A second open must not silently create two independent views of the metadata.
## 6. Ревизии и идемпотентность
## 6. Revisions and idempotency
Ревизия — монотонный номер состояния одного мира. Хотя API использует `u64`, хранение ограничено неотрицательным диапазоном SQLite `i64`: следующая ревизия выше `i64::MAX` возвращает явную ошибку. Оборот к нулю запрещён. Reset не возвращает ревизию к нулю и не позволяет старому запросу случайно пройти проверку нового состояния.
A revision is a monotonically increasing state number for one world. Although the API uses `u64`, storage is limited to the nonnegative range of SQLite `i64`: a next revision above `i64::MAX` returns an explicit error. Wrapping to zero is prohibited. Reset does not return the revision to zero or allow an old request to accidentally pass validation against a new state.
Порядок обработки `edit` и `undo`:
Processing order for `edit` and `undo`:
1. Проверить размеры, формат идентификаторов и нормализовать запрос. Повторяющиеся позиции в `changes` рекомендуется отвергать, чтобы не зависеть от порядка дублей.
2. Найти `(world_id, operation_id)` в долговечной таблице. При совпадающем отпечатке вернуть сохранённый `EditResult`, установив `replayed=true`. Это выполняется до сравнения с текущей ревизией: нормальный повтор после потерянного ответа должен работать.
3. При существующем идентификаторе с другим содержимым вернуть конфликт идемпотентности и ничего не менять.
4. Для новой операции проверить `expected_revision` в той же транзакции, что и изменение. При несовпадении вернуть конфликт с текущей ревизией.
5. Проверить все позиции и `BlockId`, применить и долговечно зафиксировать результат, затем отправить ответ и событие изменения.
1. Validate sizes and ID formats, then normalize the request. Rejecting duplicate positions in `changes` is recommended to avoid dependence on duplicate ordering.
2. Look up `(world_id, operation_id)` in the durable table. If the fingerprint matches, return the stored `EditResult` with `replayed=true`. This happens before comparing against the current revision: a normal retry after a lost response must work.
3. If the ID exists with different content, return an idempotency conflict and change nothing.
4. For a new operation, check `expected_revision` in the same transaction as the mutation. If it differs, return a conflict with the current revision.
5. Validate all positions and `BlockId` values, apply and durably commit the result, then send the response and change event.
Отпечаток включает метод и все его значимые аргументы, включая `expected_revision`; порядок уникальных позиций канонизируется. Область уникальности идентификатора — стабильный внутренний ID мира, а не имя, которое в будущем может быть переиспользовано.
The fingerprint includes the method and all meaningful arguments, including `expected_revision`; the order of unique positions is canonicalized. An ID's uniqueness is scoped to the stable internal world ID, not a name that might later be reused.
Принятая семантика первого этапа: каждая новая успешно принятая edit-операция повышает ревизию, даже при `changed=0`. Такая операция также ставит границу для консервативного undo и сохраняет долговечный результат дедупликации. Точный повтор операции не увеличивает ревизию.
Accepted first-stage semantics: every newly accepted successful edit increments the revision, even when `changed=0`. Such an operation also establishes a boundary for conservative undo and stores a durable deduplication result. An exact replay does not increment the revision.
Повтор возвращает ревизию исходной операции, которая может быть меньше текущей. Клиент не должен откатывать свою текущую ревизию по такому ответу. Ошибка после commit, но до доставки ответа, означает неопределённость для вызывающего кода: безопасный повтор использует тот же идентификатор и те же аргументы.
A replay returns the original operation's revision, which may be lower than the current revision. The client must not roll back its current revision based on that response. An error after commit but before response delivery leaves the caller uncertain; a safe retry uses the same ID and arguments.
Автоматическое удаление истории меняет гарантию дедупликации. До появления явной политики хранения ID и операций история долговечно сохраняется. Уменьшать её срок незаметно нельзя; ограничение RAM достигается дисковым хранением, а не забыванием уже подтверждённых запросов.
Automatically deleting history changes the deduplication guarantee. Until an explicit retention policy exists for IDs and operations, history remains durable. Its retention period must not be shortened silently; RAM is bounded by disk storage, not by forgetting previously acknowledged requests.
## 7. Безопасный undo
## 7. Safe undo
Undo — новая атомарная операция с собственной ревизией и `operation_id`; старый журнал не переписывается. Цель обязана принадлежать тому же миру и содержать реально применённые изменения.
Undo is a new atomic operation with its own revision and `operation_id`; it does not rewrite the old log. Its target must belong to the same world and contain changes that were actually applied.
Принятое консервативное правило MVP (D009): отмена допустима, только если текущая ревизия мира совпадает с результирующей ревизией целевой правки. Последующая правка даже другой ячейки блокирует отмену. Одного сравнения текущего `BlockId` с `after` недостаточно: последовательность «камень → воздух → камень» возвращает тот же блок, но означает чужую более позднюю работу. Выборочная отмена непересекающихся изменений — последующее расширение с отдельными дисковыми индексами происхождения изменений.
The accepted conservative MVP rule (D009) allows undo only when the current world revision matches the target edit's resulting revision. A later edit, even to a different cell, blocks undo. Comparing the current `BlockId` with `after` alone is insufficient: the sequence “stone → air → stone” restores the same block but represents someone else's later work. Selective undo of nonoverlapping changes is a later extension requiring separate on-disk indexes of change provenance.
При конфликте хотя бы одной ячейки отмена целиком отклоняется с описанием конфликта. Частичная отмена не является поведением по умолчанию. Новая отмена уже отменённой операции получает конфликт; точный повтор того же undo возвращает сохранённый результат. Отмену самого undo можно добавить отдельно, после определения семантики; MVP должен явно сообщать об отсутствии поддержки.
A conflict in even one cell rejects the entire undo with a conflict description. Partial undo is not the default behavior. A new undo of an already undone operation receives a conflict; an exact replay of the same undo returns the stored result. Undoing an undo may be added separately after defining its semantics; the MVP must explicitly report that it is unsupported.
Reset ставит барьер истории для отмен: undo операций до reset отклоняется. Это удобно выразить монотонной `epoch` мира, записанной вместе с операциями. Дедупликационные записи прежней эпохи сохраняются: повтор старой операции возвращает старый результат, но не применяет её заново.
Reset establishes an undo history barrier: undo of pre-reset operations is rejected. A monotonically increasing world `epoch`, recorded alongside operations, is a useful representation. Deduplication records from previous epochs remain: replaying an old operation returns its old result without applying it again.
## 8. Fork, reset и жизненный цикл
## 8. Fork, reset, and lifecycle
Fork пинует снимок определённой ревизии источника в одной согласованной операции. Не делать последовательность «узнать ревизию → читать секции без защиты → создать мир»: между действиями источник может измениться. История источника не становится историей дочернего мира; ревизия нового мира может начинаться с нуля при сохранённой ссылке на ревизию снимка.
A fork pins a snapshot of a specific source revision in one consistent operation. Do not use the sequence “read revision → read unprotected sections → create world”: the source can change between steps. Source history does not become child-world history; a new world's revision may start at zero while retaining a reference to the snapshot revision.
Reset возвращает мир к его закреплённому шаблону, а мир без шаблона — к воздуху. Он атомарно удаляет собственные замены, увеличивает ревизию и эпоху, фиксирует операцию reset и инвалидирует соответствующие производные кэши. Снимки, закреплённые другими мирами, не меняются.
Reset returns a world to its pinned template, or to air if it has no template. It atomically removes the world's overrides, increments the revision and epoch, records the reset operation, and invalidates the relevant derived caches. Snapshots pinned by other worlds do not change.
Сервер на reset должен уведомить клиентов о необходимости нового снимка/синхронизации. Событие с пустым `changes` само по себе не удалит уже отображаемые блоки. Сервер также должен согласовать перенос игроков, сущности и состояние арены; ядро блоков не может решать это за игровой слой.
On reset, the server must notify clients that a new snapshot/resynchronization is required. An event with empty `changes` alone will not remove blocks already displayed. The server must also coordinate player relocation, entities, and arena state; the block core cannot make those decisions for the game layer.
Освобождение секций и снимков выполняется только после проверки долговечных ссылок из миров, снимков и нужной истории. Обход связей и удаление идут порциями. Нельзя удалять снимок только потому, что имя его источника больше не существует. До реализации проверенной сборки мусора безопаснее оставлять недостижимые данные на диске и показывать их объём в метриках.
Sections and snapshots may be released only after checking durable references from worlds, snapshots, and required history. Reference traversal and deletion proceed in batches. A snapshot must not be deleted merely because its source name no longer exists. Until verified garbage collection is implemented, retaining unreachable data on disk and reporting its size in metrics is safer.
## 9. Сбои и восстановление
## 9. Failures and recovery
После открытия хранилища проверить версию формата, согласованность метаданных и завершить восстановление до выдачи обслуживающих запросов. Незавершённая транзакция не видна. Завершённая и подтверждённая операция сохраняется после аварийного завершения процесса.
After opening a store, validate the format version and metadata consistency, and complete recovery before serving requests. An incomplete transaction is invisible. A completed, acknowledged operation survives a process crash.
Восстановление WAL выполняет SQLite. Приложение не переписывает и не обрезает его самостоятельно. Ошибки целостности базы, неизвестная версия формата секции и неправильная контрольная сумма требуют явной ошибки и сохранения файлов для диагностики. Не «лечить» такие ошибки удалением данных или созданием пустого мира.
SQLite performs WAL recovery. The application does not rewrite or truncate it itself. Database integrity errors, unknown section format versions, and incorrect checksums require explicit errors and preservation of files for diagnosis. Do not “repair” these errors by deleting data or creating an empty world.
Checkpoint выполняется механизмом SQLite, чтобы авария оставляла согласованное состояние. Приложение не удаляет файлы WAL/SHM вручную. `flush()` возвращает ошибки синхронизации/checkpoint и не маскирует их; его точные гарантии должны быть совместимы с commit-before-ack, а не подменять его.
SQLite performs checkpointing so that a crash leaves a consistent state. The application does not delete WAL/SHM files manually. `flush()` returns synchronization/checkpoint errors without hiding them; its precise guarantees must be compatible with commit-before-ack rather than replacing it.
Обязательные сценарии проверок:
Required verification scenarios:
- остановка процесса до записи, посередине записи, после commit и до ответа;
- повтор операции после каждого такого сбоя;
- исчерпание дискового пространства и отказ записи/синхронизации;
- обрезанный хвост и повреждение середины журнала;
- падение во время snapshot, reset, регистрации блока и checkpoint;
- два открытия одного пути, отрицательные и граничные координаты;
- повтор ID с другим запросом, конфликт ревизии, undo с ABA и undo после reset;
- отказ открытия при кэше 0, вытеснение при 1 и небольшом обычном лимите, последующее переоткрытие.
- Process termination before a write, during a write, and after commit but before the response.
- Replaying the operation after each such failure.
- Exhausted disk space and write/synchronization failures.
- A truncated tail and corruption in the middle of the log.
- Crashes during snapshot, reset, block registration, and checkpoint.
- Two opens of the same path; negative and boundary coordinates.
- Reusing an ID for a different request, revision conflicts, ABA undo, and undo after reset.
- Rejection of a zero-sized cache; eviction with a limit of 1 and with a small ordinary limit, followed by reopening.
Тест с завершением процесса проверяет process-crash recovery, но не имитирует достоверно отключение питания или поведение аппаратного кэша диска. Эти границы нужно указывать рядом с результатом.
A process-termination test verifies process-crash recovery, but does not faithfully simulate power loss or hardware disk-cache behavior. State these boundaries alongside the results.
## 10. Метрики и сравнение с Paper
## 10. Metrics and comparison with Paper
`stats()` должен возвращать стабильные именованные поля с единицами измерения. Полезный минимальный набор: `cache_sections`, `cache_limit_sections`, `cache_payload_bytes`, `cache_hits`, `cache_misses`, `cache_evictions`, `transient_peak_bytes`, `world_count`, `snapshot_count`, `registry_states`, `history_operations`, `storage_bytes`, `wal_bytes`, `dirty_overlay_sections`, `commit_latency_ms`, `recovery_duration_ms`. Счётчик bytes обязан указывать, измеряется ли фактическое выделение или оценка полезной нагрузки.
`stats()` should return stable named fields with units. A useful minimum set is `cache_sections`, `cache_limit_sections`, `cache_payload_bytes`, `cache_hits`, `cache_misses`, `cache_evictions`, `transient_peak_bytes`, `world_count`, `snapshot_count`, `registry_states`, `history_operations`, `storage_bytes`, `wal_bytes`, `dirty_overlay_sections`, `commit_latency_ms`, and `recovery_duration_ms`. A byte counter must state whether it measures actual allocation or estimated payload.
Снаружи измерять RSS/PSS процесса, пиковую RAM, cgroup memory при наличии, CPU, чтение/запись диска, задержку тика и административных операций. Память файлового кэша ОС не следует автоматически объявлять «сэкономленной» или складывать с RSS без объяснения методики.
Measure process RSS/PSS, peak RAM, cgroup memory where available, CPU, disk reads/writes, and tick and administrative-operation latency externally. OS file-cache memory must not automatically be claimed as “saved” or added to RSS without explaining the methodology.
Нагрузочные сценарии:
Load scenarios:
1. Один и тот же шаблон и 1, 10, 100 независимых миров без изменений.
2. Те же миры с одинаковым фиксированным числом изменённых секций, затем с долей изменений 1%, 10% и 100%.
3. Последовательное и случайное движение активной области через карту больше кэша.
4. Длительное редактирование с растущей дисковой историей при постоянном активном наборе секций.
5. Чередование fork/reset и перезапусков, проверка содержимого после каждого этапа.
1. The same template with 1, 10, and 100 independent, unchanged worlds.
2. The same worlds with an identical fixed number of edited sections, then with 1%, 10%, and 100% edited.
3. Sequential and random movement of the active area across a map larger than the cache.
4. Sustained editing with growing on-disk history and a constant active set of sections.
5. Alternating forks/resets and restarts, checking content after each stage.
Фиксировать seed, карту, число миров, игроков/ботов, дистанцию видимости, набор сущностей, длительность прогрева и замера. Сначала сравнивать варианты собственного ядра: прямой массив против палитры, копирование против общего шаблона, разные лимиты кэша. Это помогает связать эффект с конкретным решением.
Record the seed, map, number of worlds, players/bots, view distance, entity set, warmup duration, and measurement duration. First compare variants of the project's own core: direct arrays versus palettes, copying versus a shared template, and different cache limits. This helps attribute an effect to a specific decision.
При сравнении с Paper записать точные версии серверов, Minecraft, Java и Rust, параметры JVM, оборудование, ОС, плагины, способ создания/копирования миров и одинаковый сценарий игроков. Отдельно показывать режим хранения одинаковых блоков и полноценный игровой сценарий. Если Shacraft не выполняет освещение, AI, redstone, генерацию или другие функции сценария Paper, прямо перечислить различия: такой замер не доказывает превосходство при равной функциональности.
When comparing with Paper, record exact server, Minecraft, Java, and Rust versions; JVM settings; hardware; OS; plugins; how worlds were created/copied; and an identical player scenario. Report identical-block storage and complete gameplay scenarios separately. If Shacraft does not perform lighting, AI, redstone, generation, or other functions present in the Paper scenario, list those differences explicitly: that measurement does not demonstrate superiority at equal functionality.
Публиковать исходные команды и сырые результаты, медиану и разброс нескольких прогонов, а также задержки и I/O рядом с RAM. Уменьшение памяти ценой неприемлемого дискового доступа или задержек — измеренный компромисс, а не автоматически успех.
Publish the original commands and raw results, the median and spread across multiple runs, and latency and I/O alongside RAM. Reducing memory at the cost of unacceptable disk access or latency is a measured tradeoff, not automatically a success.
## 11. Проверка контрактов перед следующими этапами
## 11. Contract review before subsequent stages
Часть первоначальных замечаний уже принята в CONTRACT.md и DECISIONS D008–D009; остальные относятся к будущим сетевым этапам:
Some original comments have already been incorporated into CONTRACT.md and DECISIONS D008–D009; the others concern future networking stages:
- Уточнить snapshot/revision в семантике `template`; полезно добавить их в `WorldInfo`.
- Обновить управляющий HTTP-метод `world.reset`, чтобы он тоже требовал уже принятые в Rust API `expected_revision` и `operation_id`. Для retry create/fork определить идемпотентность и ревизию источника.
- Зафиксировать правила no-op и точные пределы строк/реестра; остальных принятых инвариантов это не отменяет.
- Ввести машиночитаемые коды ошибок: неизвестный мир/блок, конфликт ревизии, конфликт ID, конфликт undo, превышение лимита, повреждённое хранилище, отказ записи.
- Уточнить события reset/resync, согласованный снимок `welcome` и доставку изменений после его ревизии; иначе клиент может пропустить изменение между снимком и подпиской.
- Для server inputs явно проверять конечность чисел, границы координат, размер сообщений, частоту запросов, дальность взаимодействия и права на каждый мир. Токен управления не должен попадать в публичные ответы или логи.
- `manifest_hash` подтверждает только заявленную версию ресурсов. Он не доказывает отсутствие модификаций клиента; игровая проверка действий остаётся на сервере.
- Clarify snapshot/revision in `template` semantics; adding them to `WorldInfo` would be useful.
- Update the control HTTP method `world.reset` to require `expected_revision` and `operation_id`, as already accepted in the Rust API. Define idempotency and the source revision for create/fork retries.
- Specify no-op rules and exact string/registry limits; this does not override other accepted invariants.
- Introduce machine-readable error codes: unknown world/block, revision conflict, ID conflict, undo conflict, exceeded limit, corrupt store, and write failure.
- Clarify reset/resync events, a consistent `welcome` snapshot, and delivery of changes after its revision; otherwise, the client can miss a change between the snapshot and subscription.
- Explicitly validate finite numbers, coordinate bounds, message size, request frequency, interaction reach, and per-world permissions for server inputs. The control token must not appear in public responses or logs.
- `manifest_hash` confirms only the declared resource version. It does not prove that the client is unmodified; gameplay validation remains the server's responsibility.
Порядок реализации: компактная секция и её проверки → долговечная атомарная операция и recovery → ревизии/дедупликация/undo → snapshot/fork/reset → ограниченный кэш и метрики → нагрузочные измерения. В каждом этапе сначала сохраняются корректность и восстановление, затем добавляется оптимизация.
Implementation order: compact sections and their tests → durable atomic operations and recovery → revisions/deduplication/undo → snapshot/fork/reset → bounded cache and metrics → load measurements. Each stage preserves correctness and recovery first, then adds optimization.
## Источники по долговечности
## Durability references
[SQLite WAL](https://sqlite.org/wal.html) и [PRAGMA synchronous](https://sqlite.org/pragma.html) описывают синхронизацию WAL при каждом commit в режиме FULL. Это выбранная настройка; корректность нашей схемы и восстановления всё равно проверяется отдельно.
[SQLite WAL](https://sqlite.org/wal.html) and [PRAGMA synchronous](https://sqlite.org/pragma.html) describe WAL synchronization on every commit in FULL mode. This is the selected configuration; the correctness of our schema and recovery still needs separate verification.