Документация Loreset

Loreset — Headless CMS для иерархического и полиморфного игрового контента. На этой странице собрана документация по текущей версии платформы: спецификация языка схем LSL, форматы экспорта (bundle-v1 и versions.json) и описание реализованных подсистем.

Обзор платформы

Платформа построена по принципу API-first: единственный интерфейс — REST API v1 с JWT-аутентификацией. Все ответы возвращаются в едином контракте:

{ "success": true, "data": { … }, "message": null, "errors": null }

Общая схема компонентов:

Frontend SPA ──JWT Bearer──> REST API (v1)
                               │
                ┌──────────────┼──────────────────┐
                │              │                  │
           AclManager     LslValidator       ExportBuilder
                │              │                  │
                └──────> ItemStorage         Publisher (S3/local)
                               │                  │
                          I18nIndexer          CDN (S3)
                               │
                         i18n_registry

Данные хранятся по принципу Hybrid Storage: метаданные, связи и версии — в реляционных таблицах (SQLite или MySQL), сами объекты с произвольной вложенностью — в JSON-документах. JSON-колонки хранятся как TEXT для переносимости между СУБД.

Ключевые понятия

ПонятиеОписание
ПроектИгра. Содержит коллекции, участников, ассеты и настройки CDN.
Коллекция«Справочник» — именованный набор записей, описанный LSL-схемой. Код уникален в рамках проекта. Флаг is_singleton ограничивает коллекцию одной записью (например, глобальные настройки баланса).
СхемаИммутабельная версия LSL-описания коллекции. Публикация новой версии сдвигает HEAD-указатель current_schema_id, старые версии остаются доступными.
Запись (item)Не содержит данных — только статус (draftpublishedarchived) и HEAD-указатель на актуальную ревизию.
РевизияИммутабельный JSON-снапшот данных записи. Каждая правка добавляет ревизию с version + 1. История никогда не переписывается.
ПубликацияВыгрузка версионированного контент-бандла и индекса versions.json на CDN проекта.

Реализованные возможности

Текущая версия платформы включает следующие подсистемы:

В планах: плагин для Godot, пакет для Unity, миграции данных при изменении схем, фоновая индексация i18n, экспорт переводов в .po / .xliff, refresh-токены.

Loreset Schema Language (LSL)

LSL — язык описания схем коллекций. Он базируется на JSON Schema, но расширен игровыми типами данных и UI-хинтами, по которым админка автоматически генерирует формы редактирования. Схема — обычный JSON-файл, который удобно хранить в Git (Schema-as-Code).

Минимальная схема коллекции:

{
  "type": "object",
  "properties": {
    "title": { "type": "i18n_string", "ui": { "label": "Название" } },
    "trigger_event": {
      "type": "enum",
      "options": ["first_login", "loot_drop", "level_complete"]
    },
    "is_hidden": { "type": "boolean" }
  },
  "required": ["title", "trigger_event"]
}

Переиспользуемые блоки выносятся в $defs и подключаются через {"$ref": "#/$defs/Name"}. Каждому полю можно задать объект ui с подписью (label) и компонентом отображения (component) — например, searchable_select, sortable_list, tabs.

Типы данных LSL

ТипЗначение в данных
integer, float, booleanсоответствующие скаляры
string, text, richtextстрока (text — многострочный, richtext — с тегами-ссылками <ref/> на другие записи)
timestampunix time (int) или дата-строка
durationчисло секунд
assetid ассета (строка)
i18n_string, i18n_textобъект {"_i18n": true, "en": "...", "ru": "..."}
enumодно из options (скаляры или объекты {value, label})
relationid записи целевой коллекции (target); при value_field: "id" проверяется существование цели
arrayсписок, каждый элемент валидируется по items
dictionaryобъект «ключ → значение», значения валидируются по схеме values
unionобъект с полем-дискриминатором; вариант выбирается по variants
objectвложенный объект с properties / required

relation — связь между коллекциями

"hero": {
  "type": "relation",
  "target": "heroes",
  "value_field": "id",
  "display_template": "{{id}}. {{name}} ({{class}})",
  "filters": { "is_playable": { "eq": true } },
  "ui": { "label": "Герой", "component": "searchable_select" }
}

union — полиморфные структуры

Ключевой для игр тип: поле принимает одну из нескольких структур в зависимости от значения дискриминатора.

// Схема
{
  "type": "union",
  "discriminator": "reward_type",
  "variants": {
    "currency": { "$ref": "#/$defs/RewardCurrency" },
    "item":     { "$ref": "#/$defs/RewardItem" }
  }
}

// Валидные данные
{ "reward_type": "currency", "currency": "gold", "amount": 500, "func": "add" }

dictionary — карты «ключ → значение»

"currency_wallet": {
  "type": "dictionary",
  "keys": { "type": "relation", "target": "currencies" },
  "values": { "type": "integer" },
  "ui": { "label": "Кошелёк (Валюта → Количество)" }
}

Правила валидации данных

Данные записи проверяются по текущей схеме коллекции перед каждым сохранением; невалидные данные не сохраняются.

{
  "success": false,
  "message": "Item data does not match the collection schema.",
  "errors": {
    "$.trigger_event": "Value is not in the list of allowed options.",
    "$.rewards[0].amount": "Expected an integer."
  }
}

Контент-бандл (loreset/bundle-v1)

Единый самодостаточный JSON для игровых движков: схемы, опубликованные записи и плоский словарь переводов. Используется и для экспорта в клиент, и для переноса контента между средами (Dev → Prod).

{
  "format": "loreset/bundle-v1",
  "project": "heliostorm",
  "version": 7,
  "generated_at": "2026-07-08T12:00:00+00:00",
  "checksum": "sha256:ab12…",
  "schemas": {
    "bonuses": { "version": 2, "schema": { … } }
  },
  "items": {
    "bonuses": [
      {
        "id": "93a01703-…",
        "data": {
          "title": { "en": "Welcome Pack", "ru": "Приветственный набор" },
          "trigger_event": "first_login"
        }
      }
    ]
  },
  "i18n": {
    "bonuses.93a01703-….title": { "en": "Welcome Pack", "ru": "Приветственный набор" }
  }
}

Экспорт — GET /v1/projects/{id}/export. Импорт — POST /v1/projects/{id}/import с телом бандла: отсутствующие коллекции создаются, изменившиеся схемы публикуются новой версией, записи вставляются или обновляются по id, повторный импорт того же бандла — no-op. Всё выполняется в одной транзакции.

Индекс версий на CDN (versions.json)

При каждой успешной публикации рядом с бандлом bundle-v{N}.json перезаписывается файл versions.json — индекс всех успешных публикаций проекта. Игровой клиент одним запросом статического файла узнаёт актуальную версию контента и URL бандла, не обращаясь к API:

<base_url>/<prefix>/versions.json
// например: https://cdn.example.com/content/versions.json

Формат файла

{
  "format": "loreset/versions-v1",
  "project": "my-rpg",
  "generated_at": 1783508000,
  "latest": 7,
  "versions": [
    {
      "version": 7,
      "file": "bundle-v7.json",
      "url": "https://cdn.example.com/content/bundle-v7.json",
      "checksum": "sha256:ab12…",
      "items_count": 132,
      "published_at": 1783508000
    },
    {
      "version": 6,
      "file": "bundle-v6.json",
      "url": "https://cdn.example.com/content/bundle-v6.json",
      "checksum": "sha256:cd34…",
      "items_count": 130,
      "published_at": 1783420000
    }
  ]
}

Поля верхнего уровня

ПолеТипОписание
formatstringМаркер формата, всегда loreset/versions-v1
projectstringКод проекта
generated_atintUnix-время генерации индекса
latestintНомер последней (актуальной) версии бандла
versionsarrayСписок успешных публикаций, новые сверху

Элемент массива versions

ПолеТипОписание
versionintНомер версии бандла (монотонно растёт в рамках проекта)
filestringИмя файла бандла; публичная ссылка собирается как <base_url>/<prefix>/<file>
urlstringURL файла, сохранённый на момент публикации
checksumstringКонтрольная сумма (sha256:…), совпадает с полем checksum внутри бандла
items_countintКоличество записей в бандле
published_atintUnix-время публикации
Важно: публикации со статусом failed в индекс не попадают. Если выгрузка versions.json не удалась, вся публикация помечается как failed.

Рекомендуемый сценарий обновления на клиенте

  1. Скачать versions.json.
  2. Сравнить latest с версией локальной копии бандла.
  3. Если версия новее — скачать файл записи с version == latest и проверить checksum.
  4. Любую сетевую ошибку трактовать мягко: продолжать работу на текущей версии.

Записи и ревизии

Запись хранит только статус и HEAD-указатель; данные лежат в иммутабельных ревизиях. Любое изменение — правка, перевод, rollback, импорт — создаёт новую ревизию и сдвигает HEAD.

# Создание записи (валидируется по текущей схеме)
curl -X POST https://api.loreset.dev/v1/collections/<uuid>/items \
  -H "Authorization: Bearer <token>" -H 'Content-Type: application/json' \
  -d '{
    "status": "draft",
    "data": {
      "title": { "_i18n": true, "en": "Welcome Pack", "ru": "Приветственный набор" },
      "trigger_event": "first_login",
      "rewards": [
        { "reward_type": "currency", "currency": "gold", "amount": 500, "func": "add" }
      ]
    }
  }'

Rollback не удаляет историю: данные старой ревизии копируются в новую, HEAD сдвигается на неё. Было v1…v3 — станет v4 с данными выбранной ревизии:

curl -X POST https://api.loreset.dev/v1/items/<uuid>/rollback \
  -H "Authorization: Bearer <token>" -H 'Content-Type: application/json' \
  -d '{"revision_id": "<uuid-старой-ревизии>"}'

Статусы записи: draftpublishedarchived (переходы произвольные). В экспортный бандл попадают только published. В коллекции-синглтоне можно создать лишь одну запись.

Локализация (i18n)

Переводы живут внутри данных записи — любое поле типа i18n_string / i18n_text хранится как объект с флагом _i18n:

"title": { "_i18n": true, "en": "Welcome Pack", "ru": "Приветственный набор" }

При каждой новой ревизии индексатор обходит JSON и пересобирает плоский реестр переводов: item_id + field_path + locale → value. Путь записывается в нотации rewards[0].title. Правка перевода — версионируемое изменение: создаётся новая ревизия записи.

CSV-пайплайн для Google Sheets

Экспорт отдаёт CSV с одной строкой на переводимое поле и колонкой на локаль:

collection,item_id,field_path,en,ru
bonuses,93a01703-…,title,Welcome Pack,Приветственный набор
bonuses,93a01703-…,rewards[0].title,Sword,

Файл загружается в Google Sheets, переводчики заполняют пустые ячейки, затем CSV импортируется обратно. Правила импорта:

Модель доступа (ACL)

Четыре уровня доступа, каждый включает предыдущие:

УровеньЧто позволяет
viewчтение записей, схем, реестра переводов, экспорт
translate+ правка переводов (точечная и CSV-импорт)
edit+ создание/правка/удаление записей, rollback, смена статуса, загрузка ассетов
manage+ структура (коллекции, схемы), участники, ACL, настройки проекта, импорт, публикация

Источники прав (в порядке приоритета):

  1. Супер-админ — полный доступ ко всему.
  2. Роль в проекте — базовый уровень: viewer → view, translator → translate, content_manager → edit, admin → manage.
  3. Переопределение на коллекцию — перекрывает роль проекта для конкретного справочника. Значение none полностью скрывает коллекцию: она исчезает из списков, любые запросы дают 403.

Пример: у контент-менеджера стоит none на справочник «Квесты» — он редактирует всё, кроме квестов. У зрителя стоит edit на «Бонусы» — он может править только бонусы.

Публикация на CDN

У каждого проекта свои настройки CDN в поле cdn_settings:

{
  "driver": "s3",
  "bucket": "my-rpg-content",
  "region": "eu-central-1",
  "endpoint": "https://s3.example.com",
  "base_url": "https://cdn.example.com",
  "prefix": "content/"
}

Драйвер s3 работает с любым S3-совместимым хранилищем (AWS S3, MinIO, DO Spaces, Selectel и т.п., включая кастомный endpoint и path-style). Драйвер local кладёт файлы в локальное хранилище — удобно для разработки. Секреты маскируются во всех ответах API.

Публикация (уровень manage):

curl -X POST https://api.loreset.dev/v1/projects/1/publish \
  -H "Authorization: Bearer <token>"

Что происходит:

  1. Выделяется следующий монотонный version.
  2. Собирается бандл loreset/bundle-v1 из опубликованных записей.
  3. Файл bundle-v{N}.json выгружается на CDN через драйвер проекта.
  4. Рядом перезаписывается индекс versions.json.
  5. В историю публикаций пишется запись с URL, чексуммой и количеством записей.

История публикаций доступна по GET /v1/projects/{id}/publications — список отсортирован по версии, новые сверху. При сбое выгрузки создаётся запись со статусом failed, и такая публикация не попадает в versions.json.

Живой пример: демо-игра Heliostorm (Godot 4.6) использует все описанные механизмы — бандл, versions.json и горячее обновление контента с CDN.