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

Redux — каталог модов

Это главная секция админ-панели. Тут заводятся все redux-моды (полные графические сборки), которые юзеры видят в Browse → Redux каталоге.

Таблица Redux в админке: имя, автор, версии, скачивания, статус

Что такое redux

Redux в нашей терминологии — это полная графическая сборка GTA V: timecycle, ENB, shaders, weather, lighting. Юзер ставит redux одной кнопкой, получает целостный визуальный пакет. В отличие от gunpack'ов (только оружие) или armor pack'ов (только броня), redux трогает много файлов в update.rpf и dlcpacks.

Каждый redux в БД — это redux_items row + одна или несколько redux_versions. Версионирование нужно потому что мод эволюционирует: автор выпускает v1, потом v2 с фиксами, потом v3 с новой ENB. Юзер может выбрать какую ставить.

Структура таблиц

erDiagram
  redux_items ||--o{ redux_versions : "has many"
  redux_items ||--o{ redux_featured_picks : "may be featured"

  redux_items {
    uuid    id PK
    text    name
    text    author
    text    description
    text    preview_url
    text    glb_url
    int     popularity
    bool    is_deleted
    timestamptz created_at
  }
  redux_versions {
    uuid    id PK
    uuid    redux_id FK
    text    version_label
    text    patch_url
    text    sha256
    int    size_bytes
    jsonb   component_urls
    timestamptz created_at
  }

component_urls — JSON map типа {"minimap": "https://cdn.../minimap.bin", "tracers": "https://cdn.../tracers.bin"}. Используется при customize-импорте — юзер берёт отдельные компоненты с разных redux'ов.

Workflow «загрузить новый redux»

Самый частый сценарий для админа. Состоит из 4 шагов:

Шаг 1. Analyze

Админ кидает в UI 2 файла: чистый update.rpf (baseline) и модифицированный update.rpf (мод). UI вызывает adminReduxAnalyze:

HunterGraphics.Shell/Bridge/AppBridge.cs
public async Task<ReduxAnalysisDto> AdminReduxAnalyzeAsync(string cleanRpfPath, string modRpfPath, string modName)
{
    var pipeline = new ReduxParserPipeline(_logger);
    var analysis = await pipeline.AnalyzeAsync(cleanRpfPath, modRpfPath, modName);
    return analysis;
}

ReduxParserPipeline.AnalyzeAsync:

  1. Открывает оба update.rpf через RageLib.GTA5.
  2. Рекурсивно обходит файлы, считает SHA-256 каждого.
  3. Diff: actions = [] — что Replace, что Import, что Delete.
  4. Для каждого actionа определяет «компонент» через ComponentScanner (minimap, tracers, hud_reticle, и так далее).
  5. Опционально рендерит GLB-превью если в моде есть armor .ydr — через headless renderer.
  6. Возвращает ReduxAnalysisDto:
public sealed record ReduxAnalysisDto(
    List<PatchAction> Actions,            // что менять
    List<DetectedComponent> Components,   // что extractable
    string? GlbDataUri,                   // 3D preview если есть
    long EstimatedPatchSize);

UI показывает превью: «27 файлов изменено, 3 удалено, 2 новых. Извлекаем: minimap, hud_reticle. Размер patch.zip — 218 МБ».

Шаг 2. Заполнение метаданных

Форма загрузки Redux: имя, ID, автор, ссылка, описание, медиа

Админ вводит:

  • Name — например «Hunter Reborn 4.0».
  • Author — например «Hunter».
  • Description — markdown в textarea.
  • Preview image — drop PNG, она зальётся в R2.
  • Tags — visual style, time of day, season.
HunterGraphics.Shell/ui/src/admin/ReduxUploadForm.tsx
const form = useForm<ReduxItemDraft>({
  defaultValues: {
    name: '', author: '', description: '',
    previewFile: null, tags: [],
  },
});

Шаг 3. Upload в R2 + insert в Supabase

Раздел «Скриншоты компонентов» в форме upload — превью каждого extractable компонента

adminQueueAdd:

HunterGraphics.Shell/Bridge/AppBridge.cs
public async Task AdminQueueAddAsync(ReduxUploadItemDto item)
{
    // 1. Собираем patch.zip из analysis result
    var patchZipPath = await _patchZipBuilder.BuildAsync(item.Analysis);

    // 2. Считаем SHA patch'а
    var sha = await Sha256OfFileAsync(patchZipPath);

    // 3. Upload в R2 — patch, preview, glb, per-component
    await _r2.UploadAsync($"redux/{item.Id}/v1/patch.zip", patchZipPath);
    await _r2.UploadAsync($"redux/{item.Id}/preview.png", item.PreviewBytes);
    if (item.GlbBytes is not null)
        await _r2.UploadAsync($"redux/{item.Id}/preview.glb", item.GlbBytes);

    foreach (var comp in item.Analysis.Components)
        await _r2.UploadAsync(
            $"redux/{item.Id}/v1/components/{comp.Type}.bin",
            comp.ExtractedBytes);

    // 4. Insert в Supabase
    await _supa.InsertReduxItemAsync(item);
    await _supa.InsertReduxVersionAsync(...);
}

R2-пути жестко детерминированы: redux/<reduxId>/v<n>/patch.zip, redux/<reduxId>/v<n>/components/<type>.bin. Это позволяет donor-cache знать заранее URL для конкретной версии без лишних DB-запросов.

Шаг 4. Заявка в очереди

Если у админа много модов на залив (распространённый сценарий — раз в месяц приходит 5-10 новых), он добавляет их в очередь:

  • adminQueueList — get all pending uploads.
  • adminQueueRun — start sequential upload.
  • adminQueueCancel — abort current.

Очередь хранится в памяти (не persisted между запусками лаунчера). Если админ закрыл лаунчер — pending заявки потеряны. Это intentionally — потому что upload-state включает MemoryStream'ы с тяжёлыми payload'ами, persist на диск дорог.

Версионирование

«Hunter Reborn» может выйти в v1, v1.5, v2. Все три версии хранятся в redux_versions и доступны юзеру через dropdown в UI. По дефолту показываем последнюю.

public async Task<JsonElement> AdminVersionUpsertAsync(JsonElement payload)
{
    var reduxId = payload.GetProperty("reduxId").GetString()!;
    var versionLabel = payload.GetProperty("versionLabel").GetString()!;
    var patchPath = payload.GetProperty("patchPath").GetString()!;

    // upload в R2 в новый под-путь
    var url = await _r2.UploadAsync($"redux/{reduxId}/v{versionLabel}/patch.zip", patchPath);

    // insert новую version row
    return await _supa.InsertReduxVersionAsync(reduxId, versionLabel, url);
}

adminVersionDelete — softdelete. Сама row помечается is_deleted = true, но R2-файлы не удаляются (на случай юзеров которые уже скачали и ссылаются на эту версию).

В каталоге сверху есть «Топ редаксы» секция — redux_featured_picks table. Админ через adminFeaturedPickSet ставит до 6 модов в spotlight (с display_order). adminFeaturedPickDelete убирает из топа.

Recalculate patch sizes

Иногда после миграции R2 или ручного fixup размеры в БД отстают от реальных. Кнопка «Пересчитать размеры» вызывает adminRecalculateReduxPatchSizes — HEAD-request на каждый patch_url, обновляет size_bytes.

public async Task<int> AdminRecalculateReduxPatchSizesAsync()
{
    var versions = await _supa.GetAllReduxVersionsAsync();
    int updated = 0;
    foreach (var v in versions)
    {
        var resp = await _http.SendAsync(new HttpRequestMessage(HttpMethod.Head, v.PatchUrl));
        var size = resp.Content.Headers.ContentLength ?? 0;
        if (size != v.SizeBytes)
        {
            await _supa.UpdateReduxVersionSizeAsync(v.Id, size);
            updated++;
        }
    }
    return updated;
}

Rebuild components

adminRebuildReduxComponents — re-parse всех existing redux'ов и пересоздать component_urls map'ы. Используется когда мы добавили новый тип компонента (например начали извлекать armor отдельно) и хотим заставить старые redux'ы тоже его иметь.

Pipeline:

  1. Скачиваем patch.zip из R2.
  2. Запускаем ReduxParserPipeline.AnalyzeFromPatchZipAsync.
  3. Re-upload компонентов под новой схемой.
  4. Update Supabase row.

Это тяжёлая операция (1-2 часа на полный каталог, ~200 модов). Запускается редко, только при schema migration.

Дальше: Guns →