Перейти к содержанию

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).

HunterGraphics.Shell/Bridge/AppBridge.cs (упрощено)
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:

  1. Качаем pack.rpf нужного gunpack'а.
  2. Извлекаем только файлы конкретной пушки (weapon_pistol.ydr, weapon_pistol+hi.ydr, текстуры).
  3. Мерджим weapons.meta patch с уже-применёнными.
  4. Результат — единый 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».

Дальше: Бронежилеты →