Redux — каталог модов¶
Это главная секция админ-панели. Тут заводятся все redux-моды (полные графические сборки), которые юзеры видят в Browse → 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:
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:
- Открывает оба
update.rpfчерез RageLib.GTA5. - Рекурсивно обходит файлы, считает SHA-256 каждого.
- Diff:
actions = []— что Replace, что Import, что Delete. - Для каждого actionа определяет «компонент» через
ComponentScanner(minimap, tracers, hud_reticle, и так далее). - Опционально рендерит GLB-превью если в моде есть armor
.ydr— через headless renderer. - Возвращает
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. Заполнение метаданных¶

Админ вводит:
- Name — например «Hunter Reborn 4.0».
- Author — например «Hunter».
- Description — markdown в textarea.
- Preview image — drop PNG, она зальётся в R2.
- Tags — visual style, time of day, season.
const form = useForm<ReduxItemDraft>({
defaultValues: {
name: '', author: '', description: '',
previewFile: null, tags: [],
},
});
Шаг 3. Upload в R2 + insert в Supabase¶

adminQueueAdd:
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-файлы не удаляются (на случай юзеров которые уже скачали и ссылаются на эту версию).
Featured picks¶
В каталоге сверху есть «Топ редаксы» секция — 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:
- Скачиваем
patch.zipиз R2. - Запускаем
ReduxParserPipeline.AnalyzeFromPatchZipAsync. - Re-upload компонентов под новой схемой.
- Update Supabase row.
Это тяжёлая операция (1-2 часа на полный каталог, ~200 модов). Запускается редко, только при schema migration.