Skip to main content

Rust Integration

1. Summary

Goal: Webhook API для интеграции с игровыми Rust серверами. Плагин на сервере отправляет события (подключение, время игры, сбор ресурсов, убийства, крафт), бекенд обновляет прогресс квестов и управляет сессиями игроков.

User Value: Игровая активность в Rust засчитывается в квесты GOLOOT. Пользователь играет на сервере и автоматически получает награды за выполненные задания без ручного подтверждения.


2. Business Logic

Webhook Event Types

PLAYER_CONNECTED — Игрок подключился к серверу

  • Создаёт сессию RustPlayerSession (lazy: только если есть активные квесты)
  • Возвращает список активных квестов для плагина

PLAYER_DISCONNECTED — Игрок отключился

  • Закрывает активную сессию
  • Финализирует прогресс TIME квестов

TIME_UPDATE — Периодическое обновление времени (каждые 5 мин)

  • Обновляет totalMinutes в сессии
  • Увеличивает прогресс TIME квестов
  • Поддерживает pendingMinutes для recovery после краша
  • Timestamp-based calculation: сравнивает userQuest.startedAt vs session.connectedAt для правильного расчёта времени
Фикс: квест взят после подключения

Если игрок взял квест "Наиграй 5 минут" после 200 минут игры, засчитывается только время с момента взятия квеста, а не всё время сессии. Это предотвращает автоматическое выполнение квестов при их получении.

Lazy Session Creation Fix: При создании сессии "лениво" (после взятия квеста), система ищет PLAYER_CONNECTED webhook в логах для определения реального времени подключения. Поле sessionCreatedAt хранит время создания записи в БД для аудита, а connectedAt — реальное время подключения игрока.

Event → Quests & Achievements

Не все события триггерят и квесты, и достижения. Таблица покрытия:

Event TypeКвестыДостиженияПримечание
PLAYER_CONNECTED✅ sessionТолько управление сессиями
PLAYER_DISCONNECTED✅ sessionТолько управление сессиями
TIME_UPDATEВремя игры (минуты)
COMMAND_EXECUTEDВыполнение команд
RESOURCE_GATHEREDСбор ресурсов
CONTAINER_LOOTEDОткрытие контейнеров
ITEM_LOOTEDЛут предметов
ANIMAL_KILLEDУбийство животных
SCIENTIST_KILLEDУбийство NPC
ITEM_CRAFTEDКрафт предметов
FISH_CAUGHTРыбалка
ITEM_RECYCLEDПереработка
EXPLOSIVE_USEDИспользование взрывчатки
TEA_BREWEDВарка чая на MixingTable
FARMING_HARVESTСбор урожая (ягоды, тыквы, кукуруза)
PIE_COOKEDВыпечка пирогов в печи
SKILL_UPGRADEDПрокачка скиллов
SKILL_MAXED✅ (incr)✅ (abs)Максимальный уровень скилла
SKILL_TREE_COMPLETEDПолная прокачка ветки
Достижения vs Квесты
  • Квесты — временные задания сезона, активируются игроком
  • Достижения — постоянные награды, триггерятся автоматически

Достижения используют те же conditions что и квесты (rustEventType, rustServerId, etc.), но обрабатываются параллельно через AchievementProgressService.

MVP History

MVP начинался только с TIME квестов (наиграть X минут). События COMMAND, GATHER, LOOT_*, KILL_ANIMAL, KILL_SCIENTIST, CRAFT, RECYCLE, SKILL_UPGRADED, SKILL_MAXED, SKILL_TREE_COMPLETED добавлены позже. EXPLOSIVE_USED добавлен с гранулярными типами взрывчатки (ракеты, гранаты).

Core Mechanics

1. Steam → User Linking

Webhook содержит steamId. Система находит пользователя через SteamLinkingService.findUserBySteamId().

Если пользователь не найден

Webhook обрабатывается успешно (status: PROCESSED), но прогресс не засчитывается. Это не ошибка — игрок может играть без привязки к GOLOOT.

2. Lazy Session Creation

Сессия RustPlayerSession создаётся только при наличии активных квестов:

Игрок подключился → Есть активные Rust квесты?
├─ Да → Создать сессию, вернуть список квестов
└─ Нет → Не создавать сессию, вернуть пустой список

Если игрок берёт квест после подключения — сессия создаётся при следующем TIME_UPDATE.

3. Atomic Progress Updates

Все методы обновления прогресса используют атомарный increment:

UPDATE user_quests SET "rustLootCount" = "rustLootCount" + 1
Race Condition Protection

Плагин может отправлять параллельные webhook-и (несколько событий одновременно). Атомарный increment предотвращает потерю данных при concurrent writes.

4. Season Status Check

Прогресс квестов обновляется только при Season.status = ACTIVE:

Webhook → isSeasonActive()?
├─ Да → Обновить прогресс
└─ Нет (COUNTDOWN/COMPLETED) → Вернуть пустой список квестов

5. Frontend Online Status Polling

TMA проверяет статус "на сервере" каждые 30 секунд через GET /api/users/rust-online-status. SSE отложен на будущее.

Category Matching

Квесты поддерживают гибкие условия через категории:

Quest TypeПоддерживаемые значения
GATHERANY, ORE, конкретный shortname
LOOT_CONTAINERANY, ANY_BARREL, ANY_CRATE, ANY_SPECIAL, конкретный тип
KILL_ANIMALКатегории: ANY, PREDATOR, HERBIVORE, SMALL, WATER (shark), JUNGLE (crocodile, tiger, panther, snake). Конкретные: BEAR, POLAR_BEAR, WOLF, BOAR, STAG, CHICKEN, SHARK, TIGER, PANTHER, CROCODILE, SNAKE
KILL_SCIENTISTГранулярные: OIL_RIG, OIL_RIG_HEAVY, BRADLEY_GUARD, BRADLEY_HEAVY, CARGO_SHIP, TUNNEL_DWELLER, MILITARY_TUNNEL, UNDERWATER, EXCAVATOR, JUNKPILE, PATROL, SILO. Техника: BRADLEY_APC, PATROL_HELICOPTER. Мета: ANY, HEAVY, BRADLEY, OIL_RIG_ALL, VEHICLES
CRAFTТолько конкретные shortnames (без категорий): rifle.ak, ammo.rifle, explosive.timed
RECYCLE (shortname)null (ANY) или конкретный shortname: metalpipe, roadsigns, gears
RECYCLE (recyclerType)ANY, SAFE_ZONE, RADTOWN
EXPLOSIVE_USEDМета-категории: ANY, ROCKET, GRENADE, C4, SATCHEL, SURVEY. Гранулярные: BASIC_ROCKET, HV_ROCKET, FIRE_ROCKET, MLRS_ROCKET, F1_GRENADE, BEANCAN_GRENADE, HE_GRENADE_LAUNCHER, EXPLOSIVE_AMMO
WeaponANY, MELEE, RANGED, EXPLOSIVE, конкретный shortname

Protection

ДействиеRate LimitAuthValidation
WebhookPlugin API KeyRustWebhookPayloadSchema
Get tasksPlugin API KeySteamId param
List serversgeneralAdmin JWT
Create servermutationsAdmin JWTCreateRustServerSchema
Update servermutationsAdmin JWTUpdateRustServerSchema
Delete servermutationsAdmin JWTDeleteRustServerSchema
Reveal credentialsmutationsAdmin JWT + Password + 2FARevealCredentialsSchema
Get logsgeneralAdmin JWTGetServerLogsSchema
Plugin Auth

Плагин аутентифицируется через Bearer token в заголовке Authorization. API Key уникален для каждого сервера и генерируется при создании. Rate limit не применяется — плагин считается доверенным после аутентификации.

Edge Cases

Что происходит при ошибках:

СитуацияПоведение системы
Steam ID не найденWebhook успешно обработан, прогресс игнорируется
Сервер деактивирован403 Forbidden, Server is deactivated
Невалидный API Key401 Unauthorized, Invalid API key
Нет активного сезонаКвесты не обновляются, сессия не создаётся
Дубликат событияАтомарный increment, идемпотентность не гарантирована
Краш плагинаpendingMinutes в следующем TIME_UPDATE для recovery
Квест взят после подключенияСчитается только время с момента взятия (timestamp-based calculation)
Переподключение с активным квестомНакапливается старый прогресс + новое время сессии
Сервер с x10 рейтами (GATHER)Квесты выполняются в 10 раз быстрее (by design)
Tea buff при добычеБонус засчитывается в прогресс (by design)
Instant mining (SkillTree)Засчитывается через кастомный хук OnInstantGatherTriggered
Backend Error Codes (для API/тестов)
КодHTTPКонтекст
Missing or invalid Authorization header401Нет Bearer token
Invalid API key401Неверный API key
Server is deactivated403Сервер выключен
Server not found404Сервер не существует
Cannot delete server with related data409Есть связанные логи/сессии/квесты

Troubleshooting

"Плагин показывает квесты, но backend возвращает пустой список"

Возможные причины:

  1. Сезон неактивен - проверить SELECT * FROM seasons WHERE "isActive" = true

    • Backend возвращает [] если нет активного сезона
    • Плагин кэширует старый список квестов локально
  2. Quest не привязан к серверу - проверить Quest.rustServerId

    SELECT id, title, "rustServerId" FROM quests WHERE title = 'имя квеста';
  3. UserQuest статус не IN_PROGRESS - проверить статус

    SELECT status FROM user_quests WHERE "questId" = 'xxx' AND "userId" = 'yyy';
  4. Логи плагина из разных файлов - плагин создаёт новый файл лога в 00:00

    • Старые логи могут показывать состояние до активации сезона/взятия квеста
Диагностика через логи

Ищите в логах плагина:

  • Webhook TIME_UPDATE success: {"activeQuests": [...]} - что вернул backend
  • Syncing quests for 1 players without quests - backend вернул []
  • Quest 'X' completed - квест выполнен корректно

3. ADR (Architectural Decisions)

Почему Lazy Session Creation?

Проблема: Сессии занимают место в БД и требуют обработки при каждом TIME_UPDATE. Большинство игроков не имеют активных Rust квестов.

Решение: Создавать сессию только при наличии активных квестов. getOrCreateSession() создаёт сессию при первом TIME_UPDATE после взятия квеста.

Альтернативы (отклонены):

  • Создавать сессию всегда при подключении — лишняя нагрузка, много неиспользуемых записей
  • Не хранить сессии, вычислять время из логов — сложный запрос, медленно

Последствия: Меньше записей в БД, но небольшая задержка при первом квесте (создание сессии).

sessionCreatedAt для аудита

Поле sessionCreatedAt добавлено для различения:

  • connectedAtреальное время подключения игрока (из PLAYER_CONNECTED webhook)
  • sessionCreatedAt — когда создана запись в БД (для аудита lazy creation)

При lazy creation система ищет последний PLAYER_CONNECTED webhook с подходящим steamId и использует его createdAt для connectedAt. Если webhook не найден — используется fallback на текущее время.

Почему Atomic Increment?

Проблема: Плагин может отправить несколько webhook-ов одновременно (игрок быстро собирает ресурсы). При обычном read-modify-write возможна потеря данных:

Thread 1: Read count = 5
Thread 2: Read count = 5
Thread 1: Write count = 6
Thread 2: Write count = 6 // Потеряли +1!

Решение: PostgreSQL атомарный increment через Prisma: { increment: amount }. SQL гарантирует атомарность.

Последствия: Корректный подсчёт при любой concurrency, но два запроса к БД (increment + check completion).

Почему Category Matching?

Проблема: Квесты должны поддерживать гибкие условия: "убей любого хищника" или "собери руду".

Решение: Иерархия категорий в rust.types.ts:

  • RUST_ANIMAL_CATEGORIES: PREDATOR → [BEAR, WOLF, BOAR]
  • RUST_RESOURCE_CATEGORIES: ORE → [metal.ore, sulfur.ore, hq.metal.ore]
  • RUST_LOOT_CONTAINER_PREFABS: ANY_CRATE → [crate_normal, crate_elite, ...]

Альтернативы (отклонены):

  • Списки в каждом квесте — дублирование, сложно менять категории
  • Regex в условиях — медленно, сложно отлаживать

Последствия: Гибкость настройки квестов, но требует синхронизации категорий между плагином и бекендом.

Почему Re-verification при Reveal Credentials?

Проблема: API Key сервера — критический секрет. Компрометация позволяет отправлять фейковые webhook-и.

Решение: Эндпоинт /reveal-credentials требует повторной верификации:

  1. Проверка пароля админа
  2. Проверка 2FA кода (если включена)
  3. Audit logging успешных запросов

Последствия: Защита от session hijacking, но неудобство при частом доступе к credentials.

Почему один OnEntityDeath хук в плагине?

Проблема: Oxide/uMod при наличии нескольких перегрузок OnEntityDeath (например: BaseNpc, SimpleShark, BaseCombatEntity) вызывает только самую общую — BaseCombatEntity. Специфичные хуки игнорируются.

Решение: Один унифицированный хук OnEntityDeath(BaseCombatEntity entity, HitInfo info) с определением типа entity через словари AnimalTypes и ScientistTypes.

Альтернативы (отклонены):

  • Отдельные хуки для каждого типа (OnEntityDeath(BaseNpc), OnEntityDeath(SimpleShark)) — Oxide не вызывает их при наличии BaseCombatEntity
  • Проверка типа через is BaseNpc — не покрывает SimpleShark (акулы) и BaseNPC2 (джунглевые животные Gen2)

Последствия: Унифицированная обработка всех entity типов через ShortPrefabName lookup, но требует полных словарей для классификации.

Reference

Подход взят из UltimateLeaderboard — один из наиболее надёжных плагинов для учёта статистики.

Почему timestamp-based расчёт времени для TIME квестов?

Проблема: Квест "Наиграй 5 минут" засчитывался сразу, если игрок взял его после 200 минут игры в текущей сессии. updateTimeProgress() использовал абсолютное время сессии без учёта когда квест был взят.

Решение: Сравнение двух timestamp-ов:

  • userQuest.startedAt — когда взят квест
  • session.connectedAt — когда началась сессия

Логика:

УсловиеРасчёт времениПример
Квест ПОСЛЕ подключенияВремя с момента взятия квестаСессия 200 мин → взял квест → 5 мин прогресса
Квест ДО подключенияСтарый прогресс + новое время сессииВзял квест → отключился (50 мин) → переподключился → +30 мин = 80 мин
Нет активной сессииСохранённый прогрессOffline tracking

Альтернативы (отклонены):

  • Добавить поле baseSessionMinutes в UserQuest — создаёт зависимость от session lifecycle, ломается при переподключениях
  • Сбрасывать прогресс при взятии квеста — несправедливо для игроков, которые переподключаются
  • Игнорировать проблему — недопустимо, т.к. позволяет мгновенное выполнение квестов

Последствия: Справедливый подсчёт времени для всех сценариев. Backward compatible: существующие квесты без сессии используют сохранённый прогресс.

Почему /goloot НЕ отправляет данные синхронно?

Проблема: Команда /goloot показывает прогресс квестов. Возникла идея: при вызове команды сразу отправлять накопленный буфер на backend, чтобы игрок видел "честный" прогресс на сервере.

Решение: Отклонено. Оставить текущую архитектуру с батчами.

Почему не нужно:

  1. Проблема не существует/goloot уже показывает реальный прогресс:

    // Прогресс = backend + локальный буфер
    totalMinutes = quest.CurrentMinutes + accumulatedMinutes;
  2. Данные не теряются — защита на всех уровнях:

    • OnPlayerDisconnected — flush при выходе
    • PendingSaveInterval (120 сек) — сохранение на диск
    • Unload() — flush при выгрузке плагина
  3. KISS — добавление синхронной отправки требует:

    • Lock на буфер (race condition с batch timer)
    • Очистка буфера после flush
    • Обработка ошибок отправки
    • Дублирование логики SendXxxUpdates()

Альтернативы (отклонены):

  • Flush буфера при /goloot — переусложнение ради психологического комфорта
  • Уменьшить batch interval до 15 сек — увеличит нагрузку на API в 4 раза

Последствия: Простая архитектура. Максимальная задержка синхронизации — 60 секунд (batch interval), что приемлемо для UX.

Почему техника использует KILL_SCIENTIST?

Проблема: Нужны квесты на уничтожение Bradley и Patrol Helicopter. Создавать новый тип квеста или расширить существующий?

Решение: Расширить KILL_SCIENTIST новыми категориями: BRADLEY_APC, PATROL_HELICOPTER, VEHICLES (мета).

Почему так:

  1. Переиспользование инфраструктуры — те же поля в Prisma (rustKillScientistType, rustKillScientistCount, rustKillWeaponType)
  2. Упрощение плагина — один webhook event SCIENTIST_KILLED, тот же буфер KillScientistBuffer
  3. UI готов — админка уже умеет показывать scientist types, добавили только новую группу "Техника"

Альтернативы (отклонены):

  • Отдельный тип квеста KILL_VEHICLE — дублирование полей, новый webhook event, больше кода
  • События BRADLEY_DESTROYED, HELICOPTER_DESTROYED — разные обработчики, сложнее поддерживать

Последствия: Техника — это "специальные NPC" с точки зрения системы квестов. Фильтр EXPLOSIVE добавлен для реалистичности (танк гранатой не убьёшь).

Почему точное совпадение через словари?

Проблема: prefab.Contains("bear") срабатывает и на bear, и на polarbear → некорректная классификация животного.

Решение: AnimalTypes.ContainsKey(shortName) — точное совпадение ключа в словаре с StringComparer.OrdinalIgnoreCase.

ShortPrefabName: "bear"      → AnimalTypes["bear"]      ✓ BEAR
ShortPrefabName: "polarbear" → AnimalTypes["polarbear"] ✓ POLAR_BEAR (не BEAR!)

Альтернативы (отклонены):

  • Contains() паттерн — ложные срабатывания на подстроках
  • Regex matching — медленнее, сложнее поддерживать

Последствия: Надёжная детекция без ложных срабатываний, но требует полного списка prefab shortnames в словаре.

Почему события буферизируются в плагине?

Проблема: High-frequency события создают огромную нагрузку. Пример: крафт 10× gunpowder генерирует событие каждую секунду. При 100 игроках это ~10,000 HTTP запросов в минуту.

Решение: Буферизация в памяти плагина + периодический batch flush на backend.

Как работает буферизация

Структура данных:

// Каждый тип события имеет свой буфер
public Dictionary<string, int> GatherBuffer { get; set; }
public Dictionary<string, int> CraftBuffer { get; set; }
public Dictionary<string, int> KillAnimalBuffer { get; set; }
// ... и другие

Накопление:

// При каждом событии — O(1) операция в памяти
session.CraftBuffer[itemShortname] += amount;

Flush по таймеру (каждые 60 сек):

timer.Every(GatherUpdateInterval, SendGatherUpdates);
// Один HTTP запрос с суммой: {gunpowder: 270}

Immediate flush (мгновенная отправка):

  • При completion квеста (игрок видит результат сразу)
  • При disconnect (данные не теряются)
  • При unload плагина (graceful shutdown)

Расчёт нагрузки (100 игроков):

МетрикаБез буферизацииС буферизацией
HTTP запросов/мин~10,000~100
Нагрузка на APIКритичнаяМинимальная
Задержка данных0 сек≤60 сек

Альтернативы (отклонены):

  • Каждое событие = HTTP запрос — перегрузка API, неприемлемо
  • Уменьшить interval до 10 сек — x6 нагрузка без реальной пользы для UX
  • WebSocket вместо HTTP — переусложнение, Rust плагины не поддерживают нативно

Последствия:

  • Максимальная задержка синхронизации: 60 секунд (настраивается через GatherUpdateInterval)
  • /goloot показывает реальный прогресс: backend_value + local_buffer
  • Данные защищены от потери: pending save каждые 120 сек, flush при disconnect
Паттерн

Это стандартный паттерн для high-frequency telemetry: accumulate → batch → flush. Используется в analytics SDK, game telemetry, metrics collectors.

Почему модификаторы добычи засчитываются?

Проблема: При добыче ресурсов плагин получает финальное количество (item.amount) после применения всех модификаторов:

  • Игровые модификаторы (Tea buff: +50% к добыче)
  • Серверные плагины (GatherManager, QuickSmelt с x2-x10 рейтами)

Вопрос: считать "честное" базовое значение или финальное с модификаторами?

Решение: Засчитывать финальное значение (все модификаторы учитываются).

Почему так:

  1. Архитектура Oxide/uMod:

    • Хуки (OnDispenserGather, OnDispenserBonus) вызываются ПОСЛЕ обработки Rust сервером
    • item.amount уже содержит финальное значение с ВСЕМИ модификаторами (Tea, IQRates, GatherManager)
    • Невозможно отличить Tea buff от плагинов рейтов — оба влияют на item.amount
    • Эмпирически проверено: при x5 IQRates → before=413 (уже с модификатором), after=0 (item consumed)
    • NextTick() не работает — к тому моменту item уже отдан игроку и amount=0
  2. Справедливость:

    • Игрок использовал ресурс (чай) → заслужил бонус к прогрессу
    • Игрок выбрал сервер с высокими рейтами → это часть игрового опыта
  3. Простота:

    • Не требует дополнительной конфигурации
    • Не требует знания о плагинах рейтов на сервере
    • Квесты работают одинаково на всех серверах

Альтернативы (отклонены):

ВариантПочему отклонён
Серверный нормализатор (gatherRateMultiplier в RustServer)Требует ручной настройки, легко забыть обновить при смене рейтов
Расчёт базового значения в плагинеНевозможно — Oxide не предоставляет base value
Хук до применения модификаторовOxide вызывает хуки после обработки

Последствия:

  • Квесты на серверах с x10 рейтами выполняются в 10 раз быстрее
  • Админы должны учитывать рейты сервера при создании квестов
  • Рекомендация: создавать разные квесты для серверов с разными рейтами
Рекомендации для админов

При создании GATHER квестов для серверов с высокими рейтами:

  • x2 рейты: targetValue * 2 (1000 дерева → 2000)
  • x5 рейты: targetValue * 5
  • Или использовать отдельные квесты для разных категорий серверов

Почему нужен хук OnInstantGatherTriggered?

Проблема: При прокачанном скилле моментальной добычи (Instant Mine/Chop/Skin из SkillTree) квесты на сбор засчитывали только ~10-15% ресурсов. Пример: добыто 1100 металла, засчитано 150.

Причина: SkillTree при срабатывании instant buff полностью обходит стандартные Oxide хуки:

  • OnDispenserGather — НЕ вызывается
  • OnDispenserBonus — НЕ вызывается

Вместо этого SkillTree напрямую:

  1. Извлекает все ресурсы из dispenser.containedItems
  2. Применяет множители (yield, night bonus, luck)
  3. Выдаёт предметы через GiveItem()
  4. Уничтожает ноду (health = 0)

Решение: Подписаться на кастомный хук SkillTree:

// GoLootTracker.cs
private void OnInstantGatherTriggered(BasePlayer player, ResourceDispenser dispenser, Item item, string pluginName)
{
if (player == null || item == null) return;
AccumulateGather(player, item.info.shortname, item.amount, "DISPENSER");
}

Альтернативы (отклонены):

ВариантПочему отклонён
Патчить SkillTreeЭто сторонний плагин, нет контроля над обновлениями
Использовать OnItemAddedToContainerЛовит ВСЕ предметы, включая лут и крафт — сложная фильтрация
Отслеживать уничтожение нодыНе даёт информацию о полученных ресурсах

Последствия:

  • Все ресурсы от instant mining засчитываются корректно
  • Работает с любыми множителями SkillTree (yield, luck, night bonus)
  • Требует: SkillTree должен вызывать OnInstantGatherTriggered (уже делает в текущей версии)
Зависимость от SkillTree API

Если SkillTree изменит или уберёт хук OnInstantGatherTriggered — instant mining перестанет засчитываться. Мониторить changelog SkillTree при обновлениях.

Как работает DLC маппинг?

Проблема: DLC скины имеют уникальные shortnames, отличающиеся от базового предмета:

  • rocket.launcher — базовый RPG
  • rocket.launcher.dragon — DLC "Frightening Dragon"
  • rocket.launcher.rpg7 — DLC "RPG-7"

Квест "Скрафти 5× RPG" с rustCraftItemShortname = "rocket.launcher" не засчитывал DLC варианты.

Решение: Использовать официальный Rust API ItemDefinition.isRedirectOf + fallback маппинг.

Как это работает:

┌─────────────────────────────────┐
│ ItemDefinition │
│ (rocket.launcher.dragon) │
│ │
│ shortname: "rocket.launcher.dragon"
│ isRedirectOf: ──────────────────┼──► ItemDefinition (rocket.launcher)
└─────────────────────────────────┘

Facepunch при создании DLC предмета должен устанавливать isRedirectOf на базовый ItemDefinition. Однако не все DLC имеют это поле (баг/недоработка Facepunch).

Известные DLC без isRedirectOf
  • rocket.launcher.rpg7 — обнаружено эмпирически 2025-02
  • Другие добавлять по мере обнаружения в DlcFallbackMapping

Метод в плагине:

// Fallback для DLC где Facepunch не заполнил isRedirectOf
private static readonly Dictionary<string, string> DlcFallbackMapping =
new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
{ "rocket.launcher.rpg7", "rocket.launcher" },
// Добавлять другие DLC по мере обнаружения
};

private string GetBaseShortname(ItemDefinition itemDef)
{
// Приоритет 1: Официальный Rust API
if (itemDef.isRedirectOf != null)
return itemDef.isRedirectOf.shortname.ToLower();

// Приоритет 2: Fallback маппинг
string shortname = itemDef.shortname.ToLower();
if (DlcFallbackMapping.TryGetValue(shortname, out string baseShortname))
return baseShortname;

return shortname;
}

Покрытие в плагине:

EventМетодDLC маппинг
CRAFTGetBaseShortname(item.info)
RECYCLEGetBaseShortname(item.info)
KILL_ANIMAL/SCIENTIST (weapon)GetWeaponShortname()GetBaseShortname()
LOOT_ITEMGetBaseShortname(itemDef)✅ (DLC не дропается из контейнеров, но защита есть)
GATHER (resources/tools)Не нуженDLC версий ресурсов/инструментов не существует
FISHНе нуженDLC версий рыбы не существует

Почему гибридный подход:

  • isRedirectOf — основной источник, автоматически работает для ~95% DLC
  • DlcFallbackMapping — резервный вариант для DLC, где Facepunch забыл заполнить поле
  • Fallback применяется ко всем событиям через единую точку входа GetBaseShortname()

Последствия:

  • Автоматическая поддержка большинства DLC через isRedirectOf
  • Ручной fallback для проблемных DLC (добавлять по мере обнаружения)
  • Квесты на базовые предметы засчитывают DLC варианты
Как обнаружить проблемный DLC

В логах плагина будет: [CRAFT_MAPPED] raw=xxx.yyy → mapped=xxx.yyy (без изменения). Если shortname не изменился — добавить в DlcFallbackMapping.

Почему дифференцированные retention periods?

Проблема: Webhook логи накапливаются очень быстро. На сервере с 100 игроками:

Батчинг отправляет только при наличии данных

Пустые батчи (GATHER, LOOT, FISH, etc.) не отправляются — webhook создаётся только если буфер содержит события. TIME_UPDATE — единственный тип, который отправляется всегда (каждые 5 мин).

Реалистичная нагрузка (зависит от активности игроков):

АктивностьИгроковWebhook/игрок/часВсего/час
AFK/база (только TIME_UPDATE)4012480
Обычная игра (строительство, редкий фарм)40301,200
Активный гринд (квесты, фарм, крафт)20701,400
TOTAL100~3,080/час

В день: ~74k записей (~1.3 GB с индексами и payload)

Пиковая нагрузка (все игроки активны): ~7,000 webhook/час (~168k/день)

Без cleanup БД вырастет до десятков GB за месяц.

Решение: Дифференцированные retention periods в зависимости от статуса лога:

СтатусRetentionПричина
PROCESSED7 днейУспешные события — только для недавнего аудита
FAILED30 днейОшибки требуют длительного исследования
PENDING1 деньЗависшие события — аномалия требующая внимания

Альтернативы (отклонены):

  • Единый retention 7 дней для всех — теряем FAILED логи для debug
  • Партиционирование таблицы — переусложнение, Prisma не поддерживает нативно
  • Архивация в S3 — дополнительная инфраструктура, избыточно для логов
  • Без cleanup — БД вырастет до критического размера

Последствия:

  • Стабилизация БД на ~10 GB (7 дней PROCESSED + 30 дней FAILED)
  • FAILED логи доступны месяц для анализа patterns
  • Warning при >100 PENDING старше 1 дня — индикатор проблем с обработкой
PENDING Alert

Если cleanup находит >100 зависших PENDING логов старше 1 дня — это сигнал о системной проблеме:

  • Сбой webhook processing service
  • Deadlock в обработке
  • Неожиданный формат payload

Требуется ручное расследование через Admin Panel → Rust Server Logs.


4. Architecture

Webhook Flow

Session Management

Key Components

КомпонентПутьОписание
WebhookControllerbackend/src/domains/rust-integration/controllers/webhook.controller.tsHTTP handlers
WebhookServicebackend/src/domains/rust-integration/services/webhook.service.tsРоутинг событий по типу
RustQuestProgressServicebackend/src/domains/rust-integration/services/rust-quest-progress.service.tsОбновление прогресса квестов
PlayerSessionServicebackend/src/domains/rust-integration/services/player-session.service.tsУправление сессиями
SteamLinkingServicebackend/src/domains/rust-integration/services/steam-linking.service.tsSteam ↔ User mapping
WebhookCleanupJobbackend/src/domains/rust-integration/jobs/webhook-cleanup.job.tsАвтоматическая очистка старых логов (daily at 03:00 UTC)
Plugin Auth Middlewarebackend/src/domains/rust-integration/middleware/plugin-auth.middleware.tsAPI Key verification
Webhook Routesbackend/src/domains/rust-integration/routes/webhook.routes.tsPlugin API
Admin Routesbackend/src/domains/rust-integration/routes/admin-rust.routes.tsServer management
Typesbackend/src/domains/rust-integration/types/rust.types.tsEvent types, categories

5. Database Schema

Models

МодельОписаниеКлючевые поля
RustServerКонфигурация сервераname, apiKey, ipAddress, isActive
RustWebhookLogЛог webhook-ов (аудит)serverId, eventType, steamId, payload, status
RustPlayerSessionСессия игрокаserverId, userId, steamId, connectedAt, sessionCreatedAt, totalMinutes, isActive

RustWebhookLog Statuses

СтатусОписание
PENDINGПолучен, ожидает обработки
PROCESSEDУспешно обработан
FAILEDОшибка при обработке
DUPLICATEДубликат (идемпотентность)
IGNOREDИгнорирован (нет активных квестов)

Webhook Log Retention Policy

Автоматическая очистка

WebhookCleanupJob выполняется ежедневно в 03:00 UTC для предотвращения неконтролируемого роста БД.

Retention periods:

  • PROCESSED логи: 7 дней
  • FAILED логи: 30 дней (для исследования проблем)
  • PENDING логи: 1 день (зависшие события)

Производительность:

  • Рост БД без cleanup: ~1.5 GB/день (высоконагруженный сервер)
  • После cleanup: стабилизация на ~10 GB (7 дней логов)
  • Проверка зависших: если >100 PENDING старше 1 дня → warning в логи

Ручной запуск:

const job = new WebhookCleanupJob();
await job.runNow(); // { totalDeleted: 50000 }

Relationships

Key Indexes

rust_servers:
@@index([apiKey]) -- Быстрый lookup при auth
@@index([isActive]) -- Фильтрация активных серверов

rust_webhook_logs:
@@index([serverId]) -- Логи по серверу
@@index([steamId]) -- Логи по игроку
@@index([eventType]) -- Фильтрация по типу события
@@index([status]) -- Фильтрация по статусу
@@index([createdAt]) -- Хронологическая сортировка

rust_player_sessions:
@@index([serverId, isActive]) -- Активные сессии на сервере
@@index([userId, isActive]) -- Активные сессии пользователя
@@index([steamId, isActive]) -- Быстрый lookup по Steam ID

6. API Endpoints

МетодЭндпоинтОписаниеAuth
POST/rust/webhookОбработка события от плагинаBearer API Key
GET/rust/tasks/:steamIdАктивные квесты игрокаBearer API Key
Формат ответа webhook
{
"success": true,
"message": "Time updated, 1 quests completed",
"data": {
"completedQuests": ["quest_id_1"],
"activeQuests": [
{
"userQuestId": "uq_123",
"questId": "q_456",
"title": "Наиграй 60 минут",
"type": "TIME",
"targetMinutes": 60,
"currentMinutes": 60
}
]
}
}

Plugin Configuration

Log Levels

Плагин GoLootTracker поддерживает настраиваемые уровни логирования через конфиг и консольную команду.

Команда: goloot.loglevel <0-2>

LevelНазваниеОписаниеПример вывода
0OFFЛогирование отключено
1INFOБизнес-события (default)[GoLootTracker] [QUARRY] Player gathered 5x hq.metal.ore
2DEBUGINFO + диагностика[GoLootTracker] [DEBUG] Sending GATHER updates: 3 entries

Конфиг (oxide/config/GoLootTracker.json):

{
"Log Level": 1
}

INFO (Level 1) — production default:

  • Подключение/отключение игроков
  • Квестовые действия: GATHER, CRAFT, KILL, LOOT, RECYCLE, FISH, EXPLOSIVE
  • Завершение квестов
  • Инициализация плагина

DEBUG (Level 2) — для диагностики:

  • Отправка буферов на backend
  • Webhook запросы и ответы
  • Инициализация сессий
  • AFK статус изменения
  • Синхронизация квестов
Когда использовать DEBUG

Включай Level 2 когда:

  • Квест не засчитывается и нужно понять почему
  • Проверить что webhook доходит до backend
  • Диагностика проблем с сессиями

  • Quests — RUST квесты как категория квестов
  • Steam Verification — верификация Steam аккаунта для Rust квестов
  • Seasons — прогресс квестов только при ACTIVE сезоне