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

Бронежилеты (armor packs)

Армор-секция админ-панели — про custom-броники: модели и текстуры жилетов на персонажа в GTA. По структуре похожа на gunpack'и, но проще — нет проблем conflicts, нет meta-merge.

Структура

erDiagram
  armor_packs ||--o{ armor_pieces : contains
  armor_packs {
    uuid id PK
    text name
    text author
    text cover_url
    text rpf_url
  }
  armor_pieces {
    uuid id PK
    uuid armor_pack_id FK
    text name
    text glb_url
    text png_url
    int  display_order
  }

Каждый «armor_piece» — это одна модель жилета (level 1 / level 2 / level 3 / fancy). Юзер выбирает один из пака, устанавливает.

Workflow upload

HunterGraphics.Shell/Bridge/AppBridge.cs (армор pipeline)
public async Task<ArmorUploadResultDto> AdminArmorUploadAsync(ArmorUploadDraftDto draft)
{
    using var temp = new TempDirectoryScope();
    await ExtractZipAsync(draft.ZipPath, temp.Path);

    // armor.zip содержит:
    //  ├── manifest.json (list pieces)
    //  ├── armor_lvl1.ydr
    //  ├── armor_lvl1.png
    //  └── ...
    var manifest = JsonSerializer.Deserialize<ArmorManifestDto>(
        await File.ReadAllTextAsync(Path.Combine(temp.Path, "manifest.json")));

    var pieces = new List<ArmorPieceDto>();
    foreach (var piece in manifest.Pieces)
    {
        var glbBytes = await _ydrConverter.ConvertAsync(
            Path.Combine(temp.Path, piece.YdrFile));
        var glbUrl = await _r2.UploadAsync(
            $"armor/{draft.PackId}/{piece.Name}.glb", glbBytes);

        // PNG превью либо берём готовое из ZIP, либо рендерим
        byte[] pngBytes;
        if (File.Exists(Path.Combine(temp.Path, piece.PngFile)))
            pngBytes = await File.ReadAllBytesAsync(Path.Combine(temp.Path, piece.PngFile));
        else
            pngBytes = await _previewRenderer.RenderAsync(glbBytes);
        var pngUrl = await _r2.UploadAsync(
            $"armor/{draft.PackId}/{piece.Name}.png", pngBytes);

        pieces.Add(new ArmorPieceDto(piece.Name, glbUrl, pngUrl, piece.DisplayOrder));
    }

    // Заливаем .rpf со всеми ассетами для install'а
    var rpfUrl = await _r2.UploadAsync(
        $"armor/{draft.PackId}/pack.rpf",
        Path.Combine(temp.Path, "pack.rpf"));

    await _supa.InsertArmorPackAsync(draft.PackId, draft.Name, rpfUrl);
    foreach (var p in pieces)
        await _supa.InsertArmorPieceAsync(draft.PackId, p);

    return new ArmorUploadResultDto(draft.PackId, pieces.Count);
}

Почему проще gunpack'ов

  • Нет meta-patch'а. Армор не редактирует weapons.meta — это просто 3D-модель в одном componentpeds.ymf слоте. Один armor_pack ставится — выбранный piece становится «moments».
  • Нет conflicts между паками. Юзер всегда поставит один armor_pack за раз. Старый pack просто replace'ится.
  • Single .rpf install. В отличие от gunpack'ов с whitelist'ом, армор всегда ставится целиком — установка простая, без cherry-pick.

Install pipeline

ArmorInstallAsync короче gunpack'а:

public async Task<InjectResultDto> ArmorInstallAsync(string packId, string pieceId)
{
    using var _mtx = await UpdateRpfMutex.AcquireAsync("armor-install");

    var pack = await _supa.GetArmorPackAsync(packId);
    var piece = await _supa.GetArmorPieceAsync(pieceId);

    // 1. Download pack.rpf в кеш
    var rpfPath = await _assetCache.GetOrDownloadAsync(pack.RpfUrl);

    // 2. Copy в dlcpacks/hunter_armor/
    var dest = Path.Combine(_paths.DlcPacksDir, "hunter_armor", "dlc.rpf");
    AtomicCopy(rpfPath, dest);

    // 3. Update dlclist.xml — добавляем hunter_armor если его нет
    await _dlclistUpdater.EnsureEntryAsync("dlcpacks:/hunter_armor/");

    return new InjectResultDto(true, null);
}

3D-preview в UI работает через стандартный glb-viewer. Никаких специальных шейдеров (см. 3D-render history — это касалось ганов).

Что в UI

Админ-таблица показывает все армор-паки, кликабельный список piece'ов. Кнопки:

  • Создать pack — модал с drop zone для ZIP'а.
  • Pack → Edit — поменять name, author, cover.
  • Piece → Edit display_order — переставить порядок отображения у юзера.
  • Piece → Delete — убрать конкретный piece из пака. Также убирает его из R2.

Дальше: DLC Import →