Rust Integration
1. Summary
Goal: Webhook API для интеграции с игровыми Rust серверами. Плагин на сервере отправляет события (подключение, время игры, сбор ресурсов, убийства, крафт), бекенд обновляет прогресс квестов и управляет сессиями игроков.
User Value: Игровая активность в Rust засчитывается в квесты GOLOOT. Пользователь играет на сервере и автоматически получает награды за выполненные задания без ручного подтверждения.
2. Business Logic
Webhook Event Types
- Session Events
- Action Events
- Loot Events
- Kill Events
- Craft Events
- Recycle Events
- Explosive Events
- Skill Events
PLAYER_CONNECTED — Игрок подключился к серверу
- Создаёт сессию
RustPlayerSession(lazy: только если есть активные квесты) - Возвращает список активных квестов для плагина
PLAYER_DISCONNECTED — Игрок отключился
- Закрывает активную сессию
- Финализирует прогресс TIME квестов
TIME_UPDATE — Периодическое обновление времени (каждые 5 мин)
- Обновляет
totalMinutesв сессии - Увеличивает прогресс TIME квестов
- Поддерживает
pendingMinutesдля recovery после краша - Timestamp-based calculation: сравнивает
userQuest.startedAtvssession.connectedAtдля правильного расчёта времени
Если игрок взял квест "Наиграй 5 минут" после 200 минут игры, засчитывается только время с момента взятия квеста, а не всё время сессии. Это предотвращает автоматическое выполнение квестов при их получении.
Lazy Session Creation Fix: При создании сессии "лениво" (после взятия квеста), система ищет PLAYER_CONNECTED webhook в логах для определения реального времени подключения. Поле sessionCreatedAt хранит время создания записи в БД для аудита, а connectedAt — реальное время подключения игрока.
COMMAND_EXECUTED — Игрок выполнил команду
- Команда нормализуется (lowercase, без ведущих
/) - Строгое сравнение с
rustTargetCommand - Атомарный increment счётчика выполнений
RESOURCE_GATHERED — Игрок собрал ресурсы
- Поддержка категорий:
ORE,ANY, или конкретный shortname - Фильтрация по способу добычи:
DISPENSER,COLLECTIBLE,QUARRY
При прокачанном скилле моментальной добычи (Instant Mine/Chop/Skin) стандартные Oxide хуки OnDispenserGather и OnDispenserBonus НЕ вызываются — SkillTree обходит их и выдаёт ресурсы напрямую.
Решение: GoLootTracker перехватывает кастомный хук OnInstantGatherTriggered, который SkillTree вызывает для каждого предмета при срабатывании instant buff.
// Кастомный хук SkillTree (не стандартный Oxide!)
private void OnInstantGatherTriggered(BasePlayer player, ResourceDispenser dispenser, Item item, string pluginName)
Важно: Без этого хука квесты на сбор засчитывают только обычные удары, но не ресурсы от instant mining.
Количество ресурсов (amount) приходит ПОСЛЕ применения всех модификаторов:
- ✅ Tea buff (+30-50% к добыче) — засчитывается
- ✅ Серверные плагины рейтов (GatherManager x2-x10) — засчитываются
Это by design: Oxide/uMod вызывает хуки после обработки сервером, невозможно получить базовое значение.
Для админов: При создании GATHER квестов учитывайте рейты сервера. Подробнее: ADR: Почему модификаторы засчитываются
Ресурсы с Mining Quarry и Giant Excavator засчитываются только пока игрок онлайн.
Почему: Используются нативные хуки OnQuarryGather/OnExcavatorGather (IQRates-подход) вместо ненадёжного OnItemRemovedFromContainer. Хуки засчитывают ресурсы оператору (тот кто включил), но BasePlayer.FindByID() находит только онлайн-игроков.
Fallback: Для Quarry — если оператор оффлайн, используется OwnerID (владелец). Для Excavator — fallback отсутствует (требует постоянного присутствия).
Для админов: Квесты типа "Добудь 1000 sulfur.ore с QUARRY" рассчитаны на активную игру, а не AFK-фарм.
CONTAINER_LOOTED — Игрок открыл/разбил контейнер
- Мета-категории:
ANY_BARREL,ANY_CRATE,ANY_SPECIAL - Конкретные типы:
BARREL,CRATE_ELITE,SUPPLY_DROP,HELI_CRATE
ITEM_LOOTED — Игрок взял предмет из контейнера
- Точное совпадение
itemShortname(case-insensitive) - Опциональный фильтр по контейнеру-источнику
При создании LOOT_ITEM квестов в админке доступен picker с каталогом предметов, разделённых на 14 категорий с глобальным поиском.
LOOT_ITEM квесты используют Container Delta для точного подсчёта взятых предметов — включая stackable (патроны, ресурсы, еда).
Как работает:
OnLootEntity→ snapshot содержимого контейнера- Игрок берёт предметы
OnLootEntityEnd→ delta = snapshot − текущее содержимое → засчитывается
Преимущества перед OnItemRemovedFromContainer:
- Корректный подсчёт stackable предметов (патроны, ресурсы)
- Нет проблемы с
item.amount = 0при стакинге - Если игрок положил предмет обратно → delta = 0
Эта же система используется для FISH квестов (SurvivalFishTrap).
ANIMAL_KILLED — Игрок убил животное
Категории:
ANY— любое животноеPREDATOR— хищники (bear, polar_bear, wolf, boar)HERBIVORE— травоядные (stag)SMALL— мелкие (chicken)WATER— водные (shark only)JUNGLE— джунгли (crocodile, tiger, panther, snake)
Конкретные типы: BEAR, POLAR_BEAR, WOLF, BOAR, STAG, CHICKEN, SHARK, TIGER, PANTHER, CROCODILE, SNAKE
Фильтр по оружию: ANY, MELEE, RANGED, или конкретный shortname
CROCODILE относится к JUNGLE, не к WATER — крокодилы спавнятся в джунглях (biome), а не в воде.
SCIENTIST_KILLED — Игрок убил учёного (NPC) или уничтожил технику
Гранулярные категории (1 prefab = 1 категория, возвращаются плагином):
OIL_RIG— синие учёные на нефтевышкеOIL_RIG_HEAVY— тяжёлые с чинука (locked crate)CARGO_SHIP,TUNNEL_DWELLER,MILITARY_TUNNEL,UNDERWATER,EXCAVATOR,JUNKPILE,PATROL,SILOBRADLEY_GUARD— обычные охранники BradleyBRADLEY_HEAVY— тяжёлые у Bradley
Техника (использует ту же систему KILL_SCIENTIST):
BRADLEY_APC— танк Bradley (НЕ охранники — только сам танк!)PATROL_HELICOPTER— патрульный вертолёт
Мета-категории (объединяют несколько гранулярных):
ANY— любой учёныйHEAVY=OIL_RIG_HEAVY+BRADLEY_HEAVYBRADLEY=BRADLEY_GUARD+BRADLEY_HEAVYOIL_RIG_ALL=OIL_RIG+OIL_RIG_HEAVYVEHICLES=BRADLEY_APC+PATROL_HELICOPTER
Фильтр по оружию: ANY, MELEE, RANGED, EXPLOSIVE, или конкретный shortname
Для техники доступна категория EXPLOSIVE:
- ammo.grenadelauncher.he, ammo.rocket.basic, ammo.rocket.hv, ammo.rocket.fire
- explosive.timed (C4), explosive.satchel
- grenade.beancan, grenade.f1
- ammo.rifle.explosive
CH47, ROAM, OUTBREAK и ARCTIC были удалены — невозможно выполнить квест.
CH47/ROAM— нельзя убить этих NPCOUTBREAK— использует тот же prefabscientistnpc_junkpile_pistolчто иJUNKPILE, невозможно различитьARCTIC—scientistnpc_roam(Arctic Research Base) не поддерживается — процедурная генерация, редко встречается
Почему ARCTIC не поддерживается, а ABANDONED_MILITARY_BASE — да?
После тестирования на реальных серверах выяснилось:
- Abandoned Military Base: спавнятся только
scientistnpc_roamtethered - Arctic Research Base: спавнятся только
scientistnpc_roam(без "tethered")
Это позволяет однозначно идентифицировать Abandoned Military Base по prefab. Arctic Research Base генерируется процедурно и встречается редко, поэтому не поддерживается.
Поддержка:
scientistnpc_roamtethered→ABANDONED_MILITARY_BASE✅scientistnpc_roam→ не поддерживается (Arctic) ❌
Prefab → Category Mapping
Гранулярные категории:
| Category | Prefabs | Описание |
|---|---|---|
OIL_RIG | scientistnpc_oilrig | Синие учёные на нефтевышке |
OIL_RIG_HEAVY | scientistnpc_heavy | Тяжёлые с чинука (locked crate) |
CARGO_SHIP | scientistnpc_cargo, scientistnpc_cargo_turret_* | Карго |
TUNNEL_DWELLER | npc_tunneldweller* | Обитатели тоннелей |
MILITARY_TUNNEL | scientistnpc_full_* | Военные тоннели |
UNDERWATER | npc_underwaterdweller | Подводные лаборатории |
EXCAVATOR | scientistnpc_excavator | Экскаватор |
JUNKPILE | scientistnpc_junkpile_pistol | Мусорные кучи (включая джунгли) |
PATROL | scientistnpc_patrol* | Патрульные на дорогах |
BRADLEY_GUARD | scientistnpc_bradley | Обычные охранники Bradley |
BRADLEY_HEAVY | scientistnpc_bradley_heavy | Тяжёлые у Bradley |
SILO | scientistnpc_roam_nvg_variant | Ракетная шахта (Nuclear Missile Silo) |
ABANDONED_MILITARY_BASE | scientistnpc_roamtethered | Заброшенная военная база |
Мета-категории (для квестов):
| Category | Включает | Описание |
|---|---|---|
ANY | Все категории | Любой учёный |
HEAVY | OIL_RIG_HEAVY, BRADLEY_HEAVY | Любой тяжёлый |
BRADLEY | BRADLEY_GUARD, BRADLEY_HEAVY | Любой у Bradley |
OIL_RIG_ALL | OIL_RIG, OIL_RIG_HEAVY | Любой на нефтевышке |
VEHICLES | BRADLEY_APC, PATROL_HELICOPTER | Любая техника |
Техника:
| Category | Prefabs | Описание |
|---|---|---|
BRADLEY_APC | bradleyapc | Танк Bradley |
PATROL_HELICOPTER | patrolhelicopter | Патрульный вертолёт |
Монументы в Rust генерируются процедурно при создании карты. Не все монументы гарантированно присутствуют на каждой карте!
Проблема: Если создать DAILY квест "Убей 5 NPC на Silo", а Nuclear Missile Silo не сгенерировалась на карте текущего wipe — квест невыполним.
Рекомендации для DAILY квестов:
| Категория | Безопасность | Причина |
|---|---|---|
OIL_RIG, OIL_RIG_HEAVY, OIL_RIG_ALL | ✅ Безопасно | Всегда есть на картах с океаном |
CARGO_SHIP | ✅ Безопасно | Event, не зависит от монументов |
JUNKPILE, PATROL | ✅ Безопасно | Дороги есть на всех картах |
TUNNEL_DWELLER | ✅ Безопасно | Подземные тоннели есть везде |
BRADLEY_GUARD, BRADLEY_HEAVY, BRADLEY | ✅ Безопасно | Launch Site обычно есть |
ANY, HEAVY | ✅ Безопасно | Мета-категории, всегда выполнимы |
SILO | ⚠️ Рискованно | ~50-70% карт, может отсутствовать |
EXCAVATOR | ⚠️ Рискованно | ~80% карт, может отсутствовать |
ABANDONED_MILITARY_BASE | ⚠️ Рискованно | ~50% карт, может отсутствовать |
MILITARY_TUNNEL | ⚠️ Рискованно | ~90% карт, может отсутствовать |
UNDERWATER | ⚠️ Рискованно | Требует diving gear, сложно для новичков |
Для WEEKLY/PERMANENT квестов можно использовать любые категории — больше времени на выполнение.
ITEM_CRAFTED — Игрок скрафтил предмет
- Точное совпадение
craftItemShortname(case-insensitive) - Атомарный increment по количеству
craftAmount - Без категорий — только конкретные shortnames
Примеры квестов:
- Скрафти 1× AK-47:
rustCraftItemShortname = "rifle.ak",rustCraftTargetAmount = 1 - Скрафти 100× патронов 5.56:
rustCraftItemShortname = "ammo.rifle",rustCraftTargetAmount = 100 - Скрафти C4:
rustCraftItemShortname = "explosive.timed",rustCraftTargetAmount = 1
При создании CRAFT квестов доступен выбор из 933 Rust предметов через ItemSelectModal с фильтрацией по 14 категориям (Weapons, Construction, Items, Resources, Attire, Tools, Medical, Food, Ammo, Traps, Misc, Components, Electrical, Fun).
Уровень верстака не отслеживается — игра сама требует нужный workbench для крафта.
CRAFT отслеживает только предметы из crafting queue (очереди крафта).
НЕ отслеживаются:
- Плавка в печке (metal.fragments, metal.refined, charcoal) — использует
OnOvenCookedhook - Scrap — получается из контейнеров и recycler
Отслеживаются:
- Оружие, инструменты, амуниция
- Взрывчатка (C4, ракеты)
- Медицина (бинты, шприцы)
- Gunpowder, Low Grade Fuel (крафтятся из компонентов)
- Компоненты (после изучения на Research Table)
Крафтящиеся компоненты
В vanilla Rust часть компонентов можно изучить на Research Table и крафтить на Workbench.
Крафтятся (после изучения):
| Shortname | Название | Workbench |
|---|---|---|
gears | Шестерни | WB3 |
metalblade | Металлическое лезвие | WB2 |
metalpipe | Металлическая труба | WB3 |
metalspring | Металлическая пружина | WB3 |
roadsigns | Дорожные знаки | WB3 |
sewingkit | Швейный набор | WB3 |
propanetank | Баллон с газом | WB2 |
НЕ крафтятся (loot only):
| Shortname | Название | Причина |
|---|---|---|
riflebody | Корпус винтовки | Только из ящиков |
smgbody | Корпус SMG | Только из ящиков |
semibody | Полу-корпус | Только из ящиков |
techparts | Техмусор | Только из ящиков |
tarp | Брезент | Только из ящиков |
rope | Верёвка | Не изучается |
sheetmetal | Листовой металл | Только из ящиков |
ITEM_RECYCLED — Игрок переработал предмет в Recycler
- Отслеживается через
OnRecyclerToggle+OnItemRecyclehooks - Точное совпадение
recycleItemShortname(case-insensitive) nullshortname = ANY (любой предмет)- Атомарный increment по количеству
recycleAmount - Количество рассчитывается с учётом stackability (как в UltimateLeaderboard)
Фильтр по типу переработчика (rustRecyclerType):
| Тип | Описание | Эффективность | Скорость |
|---|---|---|---|
ANY | Любой переработчик (default) | — | — |
SAFE_ZONE | SafeZone (Outpost, Bandit Camp) | 40% | 8 сек/предмет |
RADTOWN | Монументы (Launch Site, Dome, etc.) | 60% | 5 сек/предмет |
Определение типа: recycler.IsSafezoneRecycler() — возвращает true для SafeZone recyclers.
Примеры квестов:
- Переработай 100 любых предметов:
rustRecycleItemShortname = null,rustRecyclerType = null,rustRecycleTargetAmount = 100 - Переработай 50 труб на SafeZone:
rustRecycleItemShortname = "metalpipe",rustRecyclerType = "SAFE_ZONE",rustRecycleTargetAmount = 50 - Переработай 10 road signs на монументах:
rustRecycleItemShortname = "roadsigns",rustRecyclerType = "RADTOWN",rustRecycleTargetAmount = 10
OnRecyclerToggleзапоминает кто включил recycler (_recyclerToPlayerdictionary)OnItemRecycleполучает игрока по ID recycler, определяет тип переработчика черезIsSafezoneRecycler()- Данные буферизируются с композитным ключом
{shortname}_{safe|rad}и отправляются batch-ом каждые 60 секунд
Scrap, получаемый из recycler, НЕ отслеживается как отдельный предмет — отслеживается исходный предмет, который был переработан.
Популярные предметы для переработки
Компоненты (высокий выход scrap):
| Shortname | Название | Scrap |
|---|---|---|
riflebody | Корпус винтовки | 25 |
smgbody | Корпус SMG | 15 |
semibody | Полу-корпус | 15 |
techparts | Техмусор | 20 |
gears | Шестерни | 10 |
Распространённые предметы:
| Shortname | Название | Scrap |
|---|---|---|
roadsigns | Дорожные знаки | 5 |
metalpipe | Металлическая труба | 5 |
metalspring | Металлическая пружина | 10 |
propanetank | Баллон с газом | 5 |
sewingkit | Швейный набор | 5 |
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
Примеры квестов:
- Используй 50 любых ракет:
rustExplosiveType = "ROCKET",targetValue = 50 - Используй 10 MLRS ракет:
rustExplosiveType = "MLRS_ROCKET",targetValue = 10 - Используй 20 бобовых гранат:
rustExplosiveType = "BEANCAN_GRENADE",targetValue = 20 - Используй 100 любой взрывчатки:
rustExplosiveType = "ANY",targetValue = 100
Полная таблица shortname → Category
Мета-категории:
| Category | Shortnames | Описание |
|---|---|---|
ANY | * | Любая взрывчатка |
C4 | explosive.timed | Только C4 |
SATCHEL | explosive.satchel | Только satchel |
ROCKET | ammo.rocket.basic, ammo.rocket.hv, ammo.rocket.fire, ammo.rocket.mlrs | Любая ракета |
GRENADE | grenade.f1, grenade.beancan | Любая граната |
SURVEY | surveycharge | Survey charge |
Конкретные ракеты:
| Category | Shortname | Описание |
|---|---|---|
BASIC_ROCKET | ammo.rocket.basic | Обычная ракета |
HV_ROCKET | ammo.rocket.hv | Высокоскоростная ракета |
FIRE_ROCKET | ammo.rocket.fire | Зажигательная ракета |
MLRS_ROCKET | ammo.rocket.mlrs | MLRS ракета |
Конкретные гранаты:
| Category | Shortname | Описание |
|---|---|---|
F1_GRENADE | grenade.f1 | F1 граната |
BEANCAN_GRENADE | grenade.beancan | Бобовая граната |
Дополнительная взрывчатка:
| Category | Shortname | Описание |
|---|---|---|
HE_GRENADE_LAUNCHER | ammo.grenadelauncher.he | Граната из гранатомёта |
EXPLOSIVE_AMMO | ammo.rifle.explosive | Взрывные патроны |
Старые квесты с мета-категориями (ROCKET, GRENADE) продолжают работать — они матчатся со всеми соответствующими конкретными типами. Новые квесты могут использовать гранулярные типы для точного таргетинга.
Все сравнения shortname и targetType выполняются без учёта регистра: AMMO.ROCKET.MLRS === ammo.rocket.mlrs
SKILL_UPGRADED — Игрок прокачал скилл в SkillTree
- Интеграция с плагином SkillTree через hook
STOnNodeLevelUp - Отслеживает прокачку любого из 136 скиллов в 13 деревьях
- Поддержка трёх режимов: ANY (любой), TREE (дерево), SKILL (конкретный)
Payload поля:
tree— ID дерева ("Mining","Combat", etc.)skillName— buffKey скилла ("Mining_Yield","Duelist", etc.)levelCurrent— текущий уровень (после прокачки)levelMax— максимальный уровень скилла (1-5)upgradeCount— количество прокачек (всегда 1 при webhook)
Matching логика:
| Quest условие | Webhook tree | Webhook skillName | Match? |
|---|---|---|---|
ANY (tree=null) | любой | любой | ✓ |
TREE (tree="Mining") | "Mining" | любой | ✓ |
SKILL (tree="Mining", skillName="Mining_Yield") | "Mining" | "Mining_Yield" | ✓ |
Фильтр по уровню:
rustSkillTargetLevel— прогресс засчитывается только еслиlevelCurrent >= targetLevelrustSkillRequireMax— прогресс только еслиlevelCurrent == levelMax
Примеры квестов:
- Прокачай 10 любых скиллов:
tree=null, upgradeCount=10 - Прокачай 3 скилла Mining до max:
tree="Mining", requireMax=true, upgradeCount=3→ засчитываются РАЗНЫЕ скиллы - Прокачай Duelist до уровня 3:
tree="Combat", skillName="Duelist", targetLevel=3, upgradeCount=1 - Прокачай XP Catalyst до макс:
tree="Mining", skillName="XP_Catalyst", requireMax=true, upgradeCount=1
При создании SKILL_UPGRADED квестов в админке доступна визуальная fullscreen модалка выбора:
- 13 деревьев в виде горизонтальных табов
- 136 скиллов с иконками, группировка по тирам
- Для конкретного скилла (SKILL mode): кнопки выбора уровня 1-5 и "Любой" прямо в модалке
- Глобальный поиск, lazy loading изображений с fallback
Требуется установленный плагин SkillTree на Rust сервере. GoLootTracker перехватывает hook STOnNodeLevelUp и отправляет SKILL_UPGRADED webhook при каждой прокачке.
SKILL_MAXED — Игрок прокачал скилл до максимального уровня
- Срабатывает когда
levelCurrent == levelMax - Возвращает прогресс ветки:
maxedSkills/totalSkills - Используется для квестов и достижений с прогрессом (11/15 style)
Payload поля:
tree— ID дереваskillName— buffKey замакшенного скиллаmaxedSkills— сколько скиллов замакшено в этом деревеtotalSkills— всего скиллов в дереве (только enabled)activeUltimates— количество активных Ultimate скиллов (для Ultimate квестов)
Примеры:
- Квест "Замаксь 5 скиллов в Mining":
tree="Mining", targetValue=5 - Достижение "Мастер Mining (0/15)":
tree="Mining", targetValue=15→ прогресс обновляется при каждом maxe - Квест "Прокачай 3 Ultimate скилла":
rustSkillUltimateOnly=true, targetValue=3
Ultimate Quests — Специальный режим для квестов на Ultimate скиллы
Проблема: игрок может сделать respec (сбросить скиллы), прокачать тот же Ultimate снова и получить прогресс повторно.
Решение: флаг rustSkillUltimateOnly:
- Прогресс = текущее количество активных Ultimate (SET, не INCREMENT)
- При respec прогресс уменьшается вместе с количеством Ultimate
- Максимум 7 Ultimate (200 skill points / 26 per ultimate)
11 Ultimate скиллов:
Woodcutting_Ultimate,Mining_Ultimate,Combat_UltimateVehicle_Ultimate,Harvester_Ultimate,Medical_UltimateSkinning_Ultimate,Build_Craft_Ultimate,Scavengers_UltimateRaiding_Ultimate,Cooking_Ultimate
Рекомендуемые значения targetValue: 3 (лёгкий), 5 (средний), 7 (максимум)
SKILL_TREE_COMPLETED — Игрок полностью прокачал ветку скиллов
- Срабатывает когда
maxedSkills == totalSkills(все скиллы в дереве замакшены) - Один раз за сессию (дубликаты фильтруются
CompletedTreesThisSession) - Используется для бинарных достижений (0 → 1)
Payload поля:
tree— ID завершённого дереваtotalSkills— общее количество скилловmaxedSkills— = totalSkills
Пример:
- Достижение "Завершить ветку Mining":
tree="Mining"→ 0/1
Матрица типов квестов по скиллам
Quick reference для создания квестов в админке:
| Хочу квест | Режим в модалке | Event | targetProgress | Что означает |
|---|---|---|---|---|
| Прокачай любой скилл | ANY_SKILL | SKILL_UPGRADED | 1 | 1 любой скилл, любой уровень |
| Прокачай 5 любых скиллов | ANY_SKILL | SKILL_UPGRADED | 5 | 5 РАЗНЫХ скиллов |
| Прокачай 3 скилла до max | ANY_SKILL + ☑️ requireMax | SKILL_UPGRADED | 3 | 3 РАЗНЫХ скилла, каждый до max level |
| Прокачай скилл из Mining | TREE_SKILL (ветка) | SKILL_UPGRADED | 1 | 1 любой скилл в выбранной ветке |
| Прокачай 3 скилла в Combat | TREE_SKILL (ветка) | SKILL_UPGRADED | 3 | 3 РАЗНЫХ скилла в ветке |
| Прокачай Mining_Yield до ур. 3 | TREE_SKILL + скилл + уровень | SKILL_UPGRADED | 1 | Конкретный скилл до уровня 3 |
| Замаксь 5 скиллов в Mining | MAX_TREE | SKILL_MAXED | 5 | 5 из 9 скиллов Mining замакшены |
| Замаксь всю ветку Combat | MAX_TREE | SKILL_MAXED | 14 (auto) | Все 14 скиллов Combat замакшены |
| Прокачай 3 Ultimate | ☑️ Только Ultimate | SKILL_MAXED | 3 | 3 из 11 Ultimate (SET прогресс) |
| Прокачай 7 Ultimate | ☑️ Только Ultimate | SKILL_MAXED | 7 | 7 из 11 Ultimate (максимум) |
- SKILL_UPGRADED — инкрементальный: каждая подходящая прокачка увеличивает progress на 1
- SKILL_MAXED — абсолютный: progress = сколько скиллов УЖЕ замакшено в ветке
Пример разницы:
SKILL_UPGRADEDсtargetProgress=3: игрок прокачал скилл A→2, B→1, C→3 = progress 3/3 ✓SKILL_MAXEDсtargetProgress=3: игрок замаксил A, B, C = progress 3/3 ✓ (проверяется текущее состояние)
Показывается только для SKILL_UPGRADED без конкретного скилла.
Когда выбран конкретный скилл — целевой уровень определяет всё (чекбокс скрыт).
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 | — | ✅ | Полная прокачка ветки |
- Квесты — временные задания сезона, активируются игроком
- Достижения — постоянные награды, триггерятся автоматически
Достижения используют те же conditions что и квесты (rustEventType, rustServerId, etc.), но обрабатываются параллельно через AchievementProgressService.
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
Плагин может отправлять параллельные 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 | Поддерживаемые значения |
|---|---|
| GATHER | ANY, ORE, конкретный shortname |
| LOOT_CONTAINER | ANY, 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 |
| Weapon | ANY, MELEE, RANGED, EXPLOSIVE, конкретный shortname |
Protection
| Действие | Rate Limit | Auth | Validation |
|---|---|---|---|
| Webhook | — | Plugin API Key | RustWebhookPayloadSchema |
| Get tasks | — | Plugin API Key | SteamId param |
| List servers | general | Admin JWT | — |
| Create server | mutations | Admin JWT | CreateRustServerSchema |
| Update server | mutations | Admin JWT | UpdateRustServerSchema |
| Delete server | mutations | Admin JWT | DeleteRustServerSchema |
| Reveal credentials | mutations | Admin JWT + Password + 2FA | RevealCredentialsSchema |
| Get logs | general | Admin JWT | GetServerLogsSchema |
Плагин аутентифицируется через Bearer token в заголовке Authorization. API Key уникален для каждого сервера и генерируется при создании. Rate limit не применяется — плагин считается доверенным после аутентификации.
Edge Cases
Что происходит при ошибках:
| Ситуация | Поведение системы |
|---|---|
| Steam ID не найден | Webhook успешно обработан, прогресс игнорируется |
| Сервер деактивирован | 403 Forbidden, Server is deactivated |
| Невалидный API Key | 401 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 header | 401 | Нет Bearer token |
Invalid API key | 401 | Неверный API key |
Server is deactivated | 403 | Сервер выключен |
Server not found | 404 | Сервер не существует |
Cannot delete server with related data | 409 | Есть связанные логи/сессии/квесты |
Troubleshooting
"Плагин показывает квесты, но backend возвращает пустой список"
Возможные причины:
-
Сезон неактивен - проверить
SELECT * FROM seasons WHERE "isActive" = true- Backend возвращает
[]если нет активного сезона - Плагин кэширует старый список квестов локально
- Backend возвращает
-
Quest не привязан к серверу - проверить
Quest.rustServerIdSELECT id, title, "rustServerId" FROM quests WHERE title = 'имя квеста'; -
UserQuest статус не IN_PROGRESS - проверить статус
SELECT status FROM user_quests WHERE "questId" = 'xxx' AND "userId" = 'yyy'; -
Логи плагина из разных файлов - плагин создаёт новый файл лога в 00:00
- Старые логи могут показывать состояние до активации сезона/взятия квеста
Ищите в логах плагина:
Webhook TIME_UPDATE success: {"activeQuests": [...]}- что вернул backendSyncing quests for 1 players without quests- backend вернул[]Quest 'X' completed- квест выполнен корректно
3. ADR (Architectural Decisions)
Почему Lazy Session Creation?
Проблема: Сессии занимают место в БД и требуют обработки при каждом TIME_UPDATE. Большинство игроков не имеют активных Rust квестов.
Решение: Создавать сессию только при наличии активных квестов. getOrCreateSession() создаёт сессию при первом TIME_UPDATE после взятия квеста.
Альтернативы (отклонены):
- Создавать сессию всегда при подключении — лишняя нагрузка, много неиспользуемых записей
- Не хранить сессии, вычислять время из логов — сложный запрос, медленно
Последствия: Меньше записей в БД, но небольшая задержка при первом квесте (создание сессии).
Поле 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 требует повторной верификации:
- Проверка пароля админа
- Проверка 2FA кода (если включена)
- 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, но требует полных словарей для классификации.
Подход взят из 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, чтобы игрок видел "честный" прогресс на сервере.
Решение: Отклонено. Оставить текущую архитектуру с батчами.
Почему не нужно:
-
Проблема не существует —
/golootуже показывает реальный прогресс:// Прогресс = backend + локальный буфер
totalMinutes = quest.CurrentMinutes + accumulatedMinutes; -
Данные не теряются — защита на всех уровнях:
OnPlayerDisconnected— flush при выходеPendingSaveInterval(120 сек) — сохранение на дискUnload()— flush при выгрузке плагина
-
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 (мета).
Почему так:
- Переиспользование инфраструктуры — те же поля в Prisma (
rustKillScientistType,rustKillScientistCount,rustKillWeaponType) - Упрощение плагина — один webhook event
SCIENTIST_KILLED, тот же буферKillScientistBuffer - 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 рейтами)
Вопрос: считать "честное" базовое значение или финальное с модификаторами?
Решение: Засчитывать финальное значение (все модификаторы учитываются).
Почему так:
-
Архитектура 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
- Хуки (
-
Справедливость:
- Игрок использовал ресурс (чай) → заслужил бонус к прогрессу
- Игрок выбрал сервер с высокими рейтами → это часть игрового опыта
-
Простота:
- Не требует дополнительной конфигурации
- Не требует знания о плагинах рейтов на сервере
- Квесты работают одинаково на всех серверах
Альтернативы (отклонены):
| Вариант | Почему отклонён |
|---|---|
Серверный нормализатор (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 напрямую:
- Извлекает все ресурсы из
dispenser.containedItems - Применяет множители (yield, night bonus, luck)
- Выдаёт предметы через
GiveItem() - Уничтожает ноду (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 изменит или уберёт хук OnInstantGatherTriggered — instant mining перестанет засчитываться. Мониторить changelog SkillTree при обновлениях.
Как работает DLC маппинг?
Проблема: DLC скины имеют уникальные shortnames, отличающиеся от базового предмета:
rocket.launcher— базовый RPGrocket.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).
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 маппинг |
|---|---|---|
| CRAFT | GetBaseShortname(item.info) | ✅ |
| RECYCLE | GetBaseShortname(item.info) | ✅ |
| KILL_ANIMAL/SCIENTIST (weapon) | GetWeaponShortname() → GetBaseShortname() | ✅ |
| LOOT_ITEM | GetBaseShortname(itemDef) | ✅ (DLC не дропается из контейнеров, но защита есть) |
| GATHER (resources/tools) | Не нужен | DLC версий ресурсов/инструментов не существует |
| FISH | Не нужен | DLC версий рыбы не существует |
Почему гибридный подход:
isRedirectOf— основной источник, автоматически работает для ~95% DLCDlcFallbackMapping— резервный вариант для DLC, где Facepunch забыл заполнить поле- Fallback применяется ко всем событиям через единую точку входа
GetBaseShortname()
Последствия:
- Автоматическая поддержка большинства DLC через
isRedirectOf - Ручной fallback для проблемных 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) | 40 | 12 | 480 |
| Обычная игра (строительство, редкий фарм) | 40 | 30 | 1,200 |
| Активный гринд (квесты, фарм, крафт) | 20 | 70 | 1,400 |
| TOTAL | 100 | — | ~3,080/час |
В день: ~74k записей (~1.3 GB с индексами и payload)
Пиковая нагрузка (все игроки активны): ~7,000 webhook/час (~168k/день)
Без cleanup БД вырастет до десятков GB за месяц.
Решение: Дифференцированные retention periods в зависимости от статуса лога:
| Статус | Retention | Причина |
|---|---|---|
PROCESSED | 7 дней | Успешные события — только для недавнего аудита |
FAILED | 30 дней | Ошибки требуют длительного исследования |
PENDING | 1 день | Зависшие события — аномалия требующая внимания |
Альтернативы (отклонены):
- Единый retention 7 дней для всех — теряем FAILED логи для debug
- Партиционирование таблицы — переусложнение, Prisma не поддерживает нативно
- Архивация в S3 — дополнительная инфраструктура, избыточно для логов
- Без cleanup — БД вырастет до критического размера
Последствия:
- Стабилизация БД на ~10 GB (7 дней PROCESSED + 30 дней FAILED)
- FAILED логи доступны месяц для анализа patterns
- Warning при >100 PENDING старше 1 дня — индикатор проблем с обработкой
Если cleanup находит >100 зависших PENDING логов старше 1 дня — это сигнал о системной проблеме:
- Сбой webhook processing service
- Deadlock в обработке
- Неожиданный формат payload
Требуется ручное расследование через Admin Panel → Rust Server Logs.
4. Architecture
Webhook Flow
Session Management
Key Components
| Компонент | Путь | Описание |
|---|---|---|
| WebhookController | backend/src/domains/rust-integration/controllers/webhook.controller.ts | HTTP handlers |
| WebhookService | backend/src/domains/rust-integration/services/webhook.service.ts | Роутинг событий по типу |
| RustQuestProgressService | backend/src/domains/rust-integration/services/rust-quest-progress.service.ts | Обновление прогресса квестов |
| PlayerSessionService | backend/src/domains/rust-integration/services/player-session.service.ts | Управление сессиями |
| SteamLinkingService | backend/src/domains/rust-integration/services/steam-linking.service.ts | Steam ↔ User mapping |
| WebhookCleanupJob | backend/src/domains/rust-integration/jobs/webhook-cleanup.job.ts | Автоматическая очистка старых логов (daily at 03:00 UTC) |
| Plugin Auth Middleware | backend/src/domains/rust-integration/middleware/plugin-auth.middleware.ts | API Key verification |
| Webhook Routes | backend/src/domains/rust-integration/routes/webhook.routes.ts | Plugin API |
| Admin Routes | backend/src/domains/rust-integration/routes/admin-rust.routes.ts | Server management |
| Types | backend/src/domains/rust-integration/types/rust.types.ts | Event 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
- Plugin API
- Admin: Servers
- Admin: Operations
| Метод | Эндпоинт | Описание | Auth |
|---|---|---|---|
| POST | /rust/webhook | Обработка события от плагина | Bearer API Key |
| GET | /rust/tasks/:steamId | Активные квесты игрока | Bearer API Key |
{
"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 | Название | Описание | Пример вывода |
|---|---|---|---|
0 | OFF | Логирование отключено | — |
1 | INFO | Бизнес-события (default) | [GoLootTracker] [QUARRY] Player gathered 5x hq.metal.ore |
2 | DEBUG | INFO + диагностика | [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 статус изменения
- Синхронизация квестов
Включай Level 2 когда:
- Квест не засчитывается и нужно понять почему
- Проверить что webhook доходит до backend
- Диагностика проблем с сессиями
7. Related
- Quests — RUST квесты как категория квестов
- Steam Verification — верификация Steam аккаунта для Rust квестов
- Seasons — прогресс квестов только при ACTIVE сезоне