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

DLC Import

Экран импорта брони из DLC RPF — выбор файла и инжект

DLC Import — секция админ-панели для импорта существующих DLC-папок (dlcpacks/<name>/) в каталог как mod'ов. Используется когда у админа на руках уже установленный мод в виде готового dlcpack'а и не хочется делать diff-against-baseline.

Когда нужно

Типичный сценарий: автор моддинга прислал ZIP «вот мой dlc.rpf на ENB + текстуры + sounds, поставил у себя, работает». Это не redux (там diff против чистого update.rpf), это dlcpack. Импортирующий пайплайн просто:

  1. Открывает dlc.rpf через RageLib.
  2. Парсит setup2.xml чтобы понять что это за DLC и какой <deviceName>.
  3. Заливает .rpf целиком в R2.
  4. Создаёт row в dlc_imports (отдельная таблица от redux_items).

Юзер потом ставит этот DLC = просто copy в dlcpacks/<name>/ и добавление в dlclist.xml.

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

create table dlc_imports (
  id            uuid primary key,
  name          text not null,    -- "Hunter ENB v2"
  device_name   text not null,    -- из setup2.xml, типа "hunterenb"
  description   text,
  rpf_url       text not null,    -- R2 link на dlc.rpf
  size_bytes    bigint,
  uploaded_by   text,             -- user_id
  created_at    timestamptz default now(),
  is_deleted    bool default false
);

device_name критичен — это уникальное имя DLC в GTA. Два DLC с одинаковым device_name не уживутся (один перетрёт другой в RAGE filesystem mount). Поэтому при импорте сверяем — если уже есть запись с таким device_name, админу показываем warning: «Этот DLC конфликтует с уже импортированным X».

Pipeline

HunterGraphics.Shell/Bridge/AppBridge.cs
public async Task<DlcImportResultDto> AdminDlcImportAsync(string dlcRpfPath, string displayName, string description)
{
    // 1. Открываем .rpf, ищем setup2.xml
    using var archive = ArchiveOpener.Open(dlcRpfPath);
    var setupEntry = archive.FindFile("setup2.xml")
        ?? throw new InvalidOperationException("setup2.xml не найден — это не валидный DLC RPF");

    var xml = XDocument.Parse(await setupEntry.ReadAsTextAsync());
    var deviceName = xml.Root!.Element("deviceName")?.Value
        ?? throw new InvalidOperationException("setup2.xml без <deviceName>");

    // 2. Conflict check
    var existing = await _supa.GetDlcImportByDeviceNameAsync(deviceName);
    if (existing is not null && !existing.IsDeleted)
        throw new InvalidOperationException($"DLC с deviceName='{deviceName}' уже импортирован: {existing.Name}");

    // 3. Считаем sha + size
    var sha = await Sha256OfFileAsync(dlcRpfPath);
    var size = new FileInfo(dlcRpfPath).Length;

    // 4. Upload в R2
    var rpfUrl = await _r2.UploadAsync($"dlc-imports/{deviceName}/dlc.rpf", dlcRpfPath);

    // 5. Insert
    var id = Guid.NewGuid().ToString();
    await _supa.InsertDlcImportAsync(new DlcImportRow {
        Id = id, Name = displayName, DeviceName = deviceName,
        Description = description, RpfUrl = rpfUrl, SizeBytes = size,
    });

    return new DlcImportResultDto(id, deviceName, rpfUrl);
}

Установка у юзера

Юзер видит DLC в каталоге, жмёт «Install». Сценарий:

  1. Скачиваем dlc.rpf через AssetCache.
  2. mkdir dlcpacks/<deviceName>/.
  3. Copy в dlcpacks/<deviceName>/dlc.rpf.
  4. Проверяем update/update.rpf:/common/data/dlclist.xml — если нет entry <Item>dlcpacks:/<deviceName>/</Item>, добавляем через RageLib editor.
  5. Done. Запускай GTA.

DLC-импорты дополняют redux, не конфликтуют с ним (если только не трогают одни и те же файлы внутри update.rpf, что редко).

Limitations

  • Нельзя импортить DLC, который сам меняет update.rpf (некоторые «всё-в-одном» паки делают это через content.xml overlay). Для них надо использовать redux-flow с diff'ом.
  • Не поддерживается DLC внутри DLC (некоторые корявые паки делают dlc.rpf → nested.rpf → setup2.xml). У нас всегда первый уровень.
  • Soft-delete не удаляет файлы из R2. Если хочется реально освободить место — отдельная утилита r2_gc.ps1 (см. r2-layout).

Дальше: Injector →