Guns - gunpack'и и whitelist¶
Gun-секция админ-панели управляет оружейными паками (gunpack). Это самая болезненная часть документации потому что вокруг неё гора граблей: конфликты между гангами, между глушителями, проблемы с 3D и шейдерами.
Что такое gunpack¶
Gunpack - это набор custom-моделей оружия + текстуры + weapons.meta patch. Не одна пушка, а пак на 5-30 пушек. Юзеры могут:
- Установить весь gunpack одной кнопкой («Поставить FiveM Realism пак»).
- Выставить whitelist - взять из gunpack'а X 3 пушки, из gunpack'а Y 5 пушек, из gunpack'а Z 1 пушку. Pick'n'mix.
Поэтому в админке два under-section'а: catalog gunpack'ов и whitelist-config.
Структура таблиц¶
erDiagram
gunpacks ||--o{ gunpack_guns : "contains"
gunpacks {
uuid id PK
text name
text author
text cover_url
text rpf_url
int popularity
bool is_deleted
}
gunpack_guns {
uuid id PK
uuid gunpack_id FK
text internal_name
text display_name
text model_glb_url
text preview_png_url
jsonb meta_patch
}
internal_name - это внутренний ID GTA для оружия, например WEAPON_PISTOL или WEAPON_ASSAULTRIFLE_MK2. Один gunpack может содержать много пушек с одним internal_name (например 3 разные модели Pistol - но юзер выбирает только одну).
Workflow «залить gunpack»¶
flowchart TD
Start[admin кидает gunpack.zip] --> Parse[adminGunpackUpload
распаковка ZIP, парсинг weapons.meta]
Parse --> Models[Per-gun: extract .ydr → .glb через renderer]
Models --> Previews[Per-gun: render .png через headless Three.js]
Previews --> Upload[Upload в R2:
gunpacks/<id>/pack.rpf
gunpacks/<id>/<internal>.glb
gunpacks/<id>/<internal>.png]
Upload --> Insert[Insert gunpacks row + gunpack_guns rows]
Insert --> Done
Время - 5-20 минут на gunpack из 20 пушек, в основном из-за 3D-render'а (см. история 3D).
public async Task<GunpackUploadResultDto> AdminGunpackUploadAsync(GunpackUploadDraftDto draft)
{
using var temp = new TempDirectoryScope();
await ExtractZipAsync(draft.ZipPath, temp.Path);
var manifest = ParseWeaponsManifest(Path.Combine(temp.Path, "weapons.meta"));
var guns = new List<GunpackGunDto>();
foreach (var gun in manifest.Guns)
{
// 1. Конвертим .ydr в .glb для viewer'а
var glbBytes = await _ydrConverter.ConvertAsync(
Path.Combine(temp.Path, gun.YdrFileName));
// 2. Render PNG превью через headless renderer
var pngBytes = await _previewRenderer.RenderAsync(glbBytes);
// 3. Upload оба в R2
var glbUrl = await _r2.UploadAsync(
$"gunpacks/{draft.GunpackId}/{gun.InternalName}.glb", glbBytes);
var pngUrl = await _r2.UploadAsync(
$"gunpacks/{draft.GunpackId}/{gun.InternalName}.png", pngBytes);
guns.Add(new GunpackGunDto(
InternalName: gun.InternalName,
DisplayName: gun.DisplayName,
ModelGlbUrl: glbUrl,
PreviewPngUrl: pngUrl,
MetaPatch: gun.MetaPatchJson));
}
// 4. Upload .rpf со всеми ассетами
var rpfUrl = await _r2.UploadAsync(
$"gunpacks/{draft.GunpackId}/pack.rpf",
Path.Combine(temp.Path, "pack.rpf"));
// 5. Insert в Supabase
await _supa.InsertGunpackAsync(draft.GunpackId, draft.Name, draft.Author, rpfUrl);
foreach (var g in guns)
await _supa.InsertGunpackGunAsync(draft.GunpackId, g);
return new GunpackUploadResultDto(draft.GunpackId, guns.Count);
}
CRUD-операции¶
| Handler | Действие |
|---|---|
adminGunpackList |
Список всех gunpack'ов (для админ-таблицы) |
adminGunpackPatch |
Обновить name/author/cover |
adminGunpackDelete |
Soft-delete (is_deleted = true) |
adminGunpackGunPatch |
Обновить display name / meta_patch одной пушки |
adminGunpackGunDelete |
Удалить одну пушку из gunpack'а |
adminGunpackUpload |
См. workflow выше |
adminGunpackQueueList / adminGunpackQueueRemove |
Управление очередью загрузок |
adminUploadGunpackCover |
Поменять cover-картинку без re-upload всего пака |
Whitelist (для юзеров с PRO-подпиской)¶
Юзеры с PRO-подпиской могут собрать свой custom-пак из чужих gunpack'ов. Эта функция реализуется через selected_guns_installs таблицу - список «эта пушка из этого gunpack'а». При установке SelectedGunsInstallAsync:
- Качаем
pack.rpfнужного gunpack'а. - Извлекаем только файлы конкретной пушки (
weapon_pistol.ydr,weapon_pistol+hi.ydr, текстуры). - Мерджим
weapons.metapatch с уже-применёнными. - Результат - единый
hunter_guns_selected.rpfвdlcpacks/. Подробности в гайде по конфликтам.
gunpack_whitelist флаги¶
В gunpacks row есть is_whitelist_allowed - bool. Не все gunpack'и можно «разбирать» - некоторые авторы запрещают cherry-pick (только целиком). Админ выставляет в adminGunpackPatch. Если false, UI не показывает «Добавить в selected» кнопку.
Конфликты между gunpack'ами¶
Юзер не может поставить два gunpack'а одновременно - последний overwrites'ит. Это обусловлено архитектурой: оба пишут в dlcpacks/hunter_guns/ под одним именем, и gtaversion.dat указывает на один entry в dlclist.xml.
В админке это видно в Conflicts sub-tab - таблица показывает какие пушки overlap'ятся между gunpack'ами по internal_name. Это помогает админу понять «если юзер ставит пак X, что он теряет из пака Y».
Анимированные ганпаки (kind = animated)¶
Особый вид пака от друзей проекта: пушки с анимациями. Он не ставится в target DLC, а целиком ложится в update.rpf по рецепту его авторов. Логика - MiamiGraphics.Core/Services/AnimatedGunpackService.cs, оркестрация - GunpackInstaller.InstallAnimatedAsync, схема - миграция 0364.
Что внутри бандла¶
gunpack.rpf → update.rpf:/gunpack.rpf (корень, рядом с content.xml)
guns_anim.ytyp → update.rpf:/guns_anim.ytyp (корень; переименовывать нельзя)
dlc_patch/mp*/** → update.rpf:/dlc_patch/mp*/** (замена ванильных файлов; чего нет - добавляется)
Прописи в content.xml (обе - в <dataFiles> и в filesToEnable changeSet'а CCS_TITLE_UPDATE_STREAMING, как у залазов):
<Item>
<filename>update:/gunpack.rpf</filename>
<fileType>RPF_FILE</fileType>
<locked value="true"/><disabled value="true"/><persistent value="true"/><overlay value="true"/>
</Item>
<Item>
<filename>update:/guns_anim.ytyp</filename>
<fileType>DLC_ITYP_REQUEST</fileType>
<locked value="true"/><disabled value="true"/><persistent value="true"/><overlay value="true"/>
</Item>
Заливка¶
Admin → Guns → «Новый ганпак» → тип Анимированный → выбрать .zip / .rar (или папку) с бандлом. Верхняя папка архива (gpanim/…) находится по gunpack.rpf автоматически. Пайплайн (AdminGunpackQueueService.ProcessAnimatedAsync): проверка раскладки → каждый .rpf проверяется глазами игры (RpfEncryptionCheck) → пушки по моделям в корне gunpack.rpf сопоставляются с whitelist (иконки берутся из preview_url whitelist'а, GLB не рендерятся) → канонический bundle.zip → R2 gunpacks/{id}/anim/bundle.zip → строка gunpacks с kind='animated', anim_bundle_*; weapons_rpf_url остаётся пустым, поэтому старый клиент честно отказывается ставить такой пак.
Установка и откат у игрока¶
- Что стоит сейчас - снимается: обычный пак пересборкой DLC, анимированный - по своему снимку. Установка «поверх» = откат старого + чистая установка нового, лишних файлов не остаётся.
- Бандл скачивается с проверкой SHA (те же зеркала, что у обычных паков), распаковывается в
workdir\Gunpacks\install-<id>\anim\patch_files. - Из живого
update.rpfснимается снимок каждого файла, который пак заменит (anim_snapshot\files\**+snapshot.json), затемRpfInjectEngine.InjectPatchкладёт всё поверх живого архива (без отката на чистый - редукс и оверлеи остаются). Перед пересборкой делаетсяupdate.rpf.preinstall, при ошибке он возвращается. - Состояние:
installed_gunpack.jsonсkind = animatedиanimSnapshotDir. Для пересборки выбранных пушек и сверки DLC такой пак считается «пака в DLC нет». - «Удалить» возвращает файлы из снимка (
Replace), удаляет то, чего до пака не было (Delete, включаяgunpack.rpfиguns_anim.ytyp), снимает обе прописи. Нет снимка - файл берётся из чистогоupdate.rpf. - Установка/кастомизация редукса восстанавливают чистый
update.rpf- пак переприменяется (ReapplyAnimatedGunpackIfInstalledAsync, до live-оверлеев) с новым снимком поверх нового редукса. Полная пересборка без переприменения (восстановление чистого руками) -Reconcileвидит, чтоgunpack.rpfв архиве нет, и сбрасывает состояние.
Таймер релиза (release_at)¶
Любой пак можно опубликовать заранее: в форме заливки (и в DetailStage) задаётся дата/время по МСК, хранится в UTC. До этого момента пак виден, карточка и страница показывают обратный отсчёт, кнопка «Установить» скрыта, а мост отказывает в установке (error.gunpackNotReleasedYet). Время сверяется с сервером (RPC server_now), не с часами игрока.