DLC Import¶

DLC Import — секция админ-панели для импорта существующих DLC-папок (dlcpacks/<name>/) в каталог как mod'ов. Используется когда у админа на руках уже установленный мод в виде готового dlcpack'а и не хочется делать diff-against-baseline.
Когда нужно¶
Типичный сценарий: автор моддинга прислал ZIP «вот мой dlc.rpf на ENB + текстуры + sounds, поставил у себя, работает». Это не redux (там diff против чистого update.rpf), это dlcpack. Импортирующий пайплайн просто:
- Открывает
dlc.rpfчерез RageLib. - Парсит
setup2.xmlчтобы понять что это за DLC и какой<deviceName>. - Заливает .rpf целиком в R2.
- Создаёт 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¶
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». Сценарий:
- Скачиваем
dlc.rpfчерезAssetCache. mkdir dlcpacks/<deviceName>/.- Copy в
dlcpacks/<deviceName>/dlc.rpf. - Проверяем
update/update.rpf:/common/data/dlclist.xml— если нет entry<Item>dlcpacks:/<deviceName>/</Item>, добавляем через RageLib editor. - Done. Запускай GTA.
DLC-импорты дополняют redux, не конфликтуют с ним (если только не трогают одни и те же файлы внутри update.rpf, что редко).
Limitations¶
- Нельзя импортить DLC, который сам меняет
update.rpf(некоторые «всё-в-одном» паки делают это черезcontent.xmloverlay). Для них надо использовать redux-flow с diff'ом. - Не поддерживается DLC внутри DLC (некоторые корявые паки делают
dlc.rpf → nested.rpf → setup2.xml). У нас всегда первый уровень. - Soft-delete не удаляет файлы из R2. Если хочется реально освободить место — отдельная утилита
r2_gc.ps1(см. r2-layout).