new customization
This commit is contained in:
@@ -0,0 +1,354 @@
|
||||
# Совместимость CVPatch и расширенной кастомизации
|
||||
|
||||
Документ описывает фактический набор графических MPQ MoonWell для клиента
|
||||
**WoW 3.3.5a, build 12340**, их порядок загрузки, конфликты DBC, сборку слоя
|
||||
совместимости и серверные требования парикмахерской.
|
||||
|
||||
## Исходные данные и происхождение
|
||||
|
||||
Старые архивы `patch-ruRU-A..W` относятся к **CVPatch (Collection Visual
|
||||
Patch)** от Vary/vvladoon. Найденная тема оригинального проекта:
|
||||
|
||||
<https://forum.uwow.biz/threads/cvpatch-collection-visual-patch-lich-king-3-3-5.296264/>
|
||||
|
||||
Источник новых архивов утрачен. Единственная сохранённая подпись находится в
|
||||
`new customization mpqs/readme.txt`:
|
||||
|
||||
- `patch-a-001.mpq` — `Model and Textures NPC`;
|
||||
- `patch-b-002.mpq` — `Characters HD The War Within`;
|
||||
- `patch-k.mpq` — `Interface`.
|
||||
|
||||
Новые архивы нельзя считать официальным или полностью автономным комплектом
|
||||
для 3.3.5a: в них отсутствовал совместимый `BarberShopStyle.dbc`, а часть DBC
|
||||
содержит данные более новых версий игры.
|
||||
|
||||
## Установленная часть CVPatch
|
||||
|
||||
| Патч | Назначение | Зависимости и пересечения |
|
||||
|---|---|---|
|
||||
| A | Основные модели существ, гуманоидов и транспорта | Основа для старого B |
|
||||
| B | Альтернативные модели существ, гуманоидов и транспорта | Требует A |
|
||||
| E | Текстуры экипировки в повышенном разрешении | Независимый |
|
||||
| H | HD-модели игровых персонажей Leeviathan | Перекрывается новыми X/Y на одинаковых путях |
|
||||
| I | Новые модели и текстуры брони и оружия | Изначально требовал H; его item-поля сохраняются при слиянии |
|
||||
| M | Карты классических подземелий и пещер | Независимый |
|
||||
| S | Эффекты заклинаний и способностей | Содержит собственные spell DBC |
|
||||
| T | Текстуры зданий и зон | Независимый |
|
||||
| U | Окружение Низины Арати и Ущелья Песни Войны | Независимый |
|
||||
| W | Вода | Независимый |
|
||||
|
||||
Старые буквенные имена не меняются. X, Y и Z специально выбраны после W,
|
||||
чтобы новый комплект и исправления имели приоритет без переименования CVPatch.
|
||||
|
||||
## Новые исходные архивы
|
||||
|
||||
### `patch-a-001.mpq` → `patch-ruRU-X.MPQ`
|
||||
|
||||
Размер исходника — около 2,49 GiB, `(listfile)` содержит 29 495 файлов:
|
||||
|
||||
- 16 023 запечённых NPC-текстуры в `Textures\bakednpctextures`;
|
||||
- 13 465 файлов моделей, skin, анимаций и текстур в `Character`;
|
||||
- модели стандартных рас и ряда NPC-рас: Vrykul, Naga, Taunka, Forest Troll и
|
||||
других;
|
||||
- 28 120 BLP, 1 245 ANIM, 94 SKIN и 29 M2;
|
||||
- 7 DBC.
|
||||
|
||||
DBC внутри X:
|
||||
|
||||
| DBC | Строк | Роль |
|
||||
|---|---:|---|
|
||||
| `CharacterFacialHairStyles.dbc` | 272 | Штатный объём лицевой кастомизации |
|
||||
| `CharHairGeosets.dbc` | 370 | Штатный объём причёсок |
|
||||
| `CharSections.dbc` | 10 060 | Секции текстур персонажей |
|
||||
| `CreatureDisplayInfoExtra.dbc` | 15 477 | Дополнительные параметры отображения существ |
|
||||
| `EmotesTextSound.dbc` | 782 | Звуки текстовых эмоций |
|
||||
| `HelmetGeosetVisData.dbc` | 21 | Скрытие geoset под шлемами |
|
||||
| `SpellVisualKitModelAttach.dbc` | 594 | Крепление моделей визуальных эффектов |
|
||||
|
||||
X нужен текущему комбинированному комплекту для NPC-моделей и запечённых
|
||||
текстур. Для новых причёсок главным является Y, который загружается позже и
|
||||
перекрывает одноимённые таблицы X.
|
||||
|
||||
### `patch-b-002.mpq` → `patch-ruRU-Y.MPQ`
|
||||
|
||||
Размер исходника — около 2,80 GiB, `(listfile)` содержит 36 191 файл:
|
||||
|
||||
- 36 180 файлов в `Character`;
|
||||
- HD-модели и текстуры десяти стандартных игровых рас WotLK;
|
||||
- 34 934 BLP, 1 021 ANIM, 125 BONE, 80 SKIN и 20 M2;
|
||||
- 11 DBC с существенно расширенной кастомизацией.
|
||||
|
||||
DBC внутри Y:
|
||||
|
||||
| DBC | Строк | Роль |
|
||||
|---|---:|---|
|
||||
| `CharacterFacialHairStyles.dbc` | 3 616 | Бороды, серьги, рога и другие лицевые варианты |
|
||||
| `CharHairGeosets.dbc` | 854 | Geoset и номера причёсок |
|
||||
| `CharHairTextures.dbc` | 177 | Текстуры волос |
|
||||
| `CharSections.dbc` | 614 212 | Расширенные секции кожи, лица, волос и их сочетаний |
|
||||
| `CharVariations.dbc` | 62 | Наборы вариаций персонажей |
|
||||
| `ChrRaces.dbc` | 41 | Описания рас, включая данные за пределами штатного WotLK |
|
||||
| `CreatureDisplayInfo.dbc` | 24 282 | Display ID моделей |
|
||||
| `CreatureDisplayInfoExtra.dbc` | 15 477 | Раса, пол, внешность и предметные поля display ID |
|
||||
| `CreatureFamily.dbc` | 40 | Семейства существ |
|
||||
| `CreatureModelData.dbc` | 1 353 | Пути и параметры моделей |
|
||||
| `HelmetGeosetVisData.dbc` | 65 | Расширенные правила отображения под шлемами |
|
||||
|
||||
Y обязателен для расширенной кастомизации. Z не содержит копии всех этих
|
||||
таблиц и не заменяет Y.
|
||||
|
||||
Наличие современных race ID в DBC не добавляет новые игровые расы в ядро или
|
||||
интерфейс 3.3.5a. Текущая интеграция поддерживает расширение внешности десяти
|
||||
штатных рас. Новые игровые расы потребуют отдельной доработки клиента, UI,
|
||||
сетевого протокола и сервера.
|
||||
|
||||
### `patch-k.mpq` — не устанавливается
|
||||
|
||||
Размер — около 53 MiB, `(listfile)` содержит 673 файла только в `Interface`:
|
||||
|
||||
- 547 BLP;
|
||||
- 68 SKIN и 24 M2 для экранов входа и выбора персонажа;
|
||||
- 15 Lua, 12 XML, TOC и вспомогательные файлы.
|
||||
|
||||
Архив меняет `Interface\Glues`, `Interface\GlueXML`, модели экранов входа и
|
||||
создания персонажа. Он намеренно исключён из сборки, потому что перекрывает
|
||||
кастомный интерфейс MoonWell. Устанавливать его вручную поверх текущего клиента
|
||||
нельзя без отдельного разбора и слияния UI.
|
||||
|
||||
## Итоговый порядок загрузки
|
||||
|
||||
В локализованной цепочке `Data\ruRU` используется следующий порядок:
|
||||
|
||||
1. штатные архивы клиента, включая `patch-ruRU-3.MPQ`;
|
||||
2. старые CVPatch `patch-ruRU-A..W`;
|
||||
3. `patch-ruRU-X.MPQ` — неизменённая копия `patch-a-001.mpq`;
|
||||
4. `patch-ruRU-Y.MPQ` — неизменённая копия `patch-b-002.mpq`;
|
||||
5. `patch-ruRU-Z.MPQ` — сгенерированный слой совместимости MoonWell.
|
||||
|
||||
Z является последней инстанцией для конфликтующих DBC. Нельзя переносить его
|
||||
раньше X/Y или устанавливать другой locale MPQ после Z, который содержит те же
|
||||
таблицы.
|
||||
|
||||
## Содержимое `patch-ruRU-Z.MPQ`
|
||||
|
||||
В текущей сборке Z содержит:
|
||||
|
||||
- `BarberShopStyle.dbc` — расширенная таблица парикмахерской;
|
||||
- `CreatureDisplayInfoExtra.dbc` — результат слияния новых display ID и
|
||||
предметных полей CVPatch I;
|
||||
- `SpellVisualKitModelAttach.dbc` — более полная версия из CVPatch S;
|
||||
- `CreatureModelData.dbc` — сгенерированное слияние штатных, CVPatch и новых
|
||||
моделей;
|
||||
- `CreatureDisplayInfo.dbc` — сгенерированное слияние display ID;
|
||||
- `WXLFileData.csv` и `WXLSpellOverrides.tsv` для WarcraftXL.
|
||||
|
||||
Последние четыре файла добавляются упаковщиком автоматически; поэтому состав
|
||||
папки `src\Data\ruRU\patch-ruRU-Z` и готового MPQ не обязан совпадать буквально.
|
||||
|
||||
### Правила слияния DBC
|
||||
|
||||
`tool/scripts/build-customization-compat.ps1` выполняет следующие операции:
|
||||
|
||||
- берёт штатную базу из `patch-ruRU-3.MPQ`;
|
||||
- сохраняет уникальные изменения `CreatureModelData` из CVPatch A;
|
||||
- сохраняет уникальные изменения `CreatureDisplayInfo` из CVPatch B;
|
||||
- отдаёт приоритет Y при реальном конфликте одной и той же строки;
|
||||
- переносит изменённые item-поля `CreatureDisplayInfoExtra` из CVPatch I в
|
||||
новые строки Y;
|
||||
- восстанавливает `SpellVisualKitModelAttach.dbc` из CVPatch S;
|
||||
- генерирует расширенный `BarberShopStyle.dbc`;
|
||||
- обновляет входные `CreatureModelData.dbc` и `CreatureDisplayInfo.dbc` для
|
||||
генератора MoonWell creatures.
|
||||
|
||||
Так Z не позволяет Y случайно уничтожить старые изменения существ, экипировки
|
||||
и эффектов, а генерация MoonWell creatures не уничтожает новые display ID.
|
||||
|
||||
## Расширенная парикмахерская
|
||||
|
||||
### Причина исходного падения
|
||||
|
||||
Y расширил:
|
||||
|
||||
- `CharHairGeosets.dbc` с 370 до 854 строк;
|
||||
- `CharacterFacialHairStyles.dbc` с 272 до 3 616 строк.
|
||||
|
||||
При этом исходные новые MPQ не содержали `BarberShopStyle.dbc`. Клиент продолжал
|
||||
использовать штатную таблицу на 491 строку. При выборе нового варианта функция
|
||||
`GetBarberShopTotalCost` не находила запись, после чего WoW падал с
|
||||
`ERROR #132 / ACCESS_VIOLATION`. В зафиксированном крэше вызов шёл от
|
||||
`BarberShopFrameSelector1Next`.
|
||||
|
||||
### Как сформирована новая таблица
|
||||
|
||||
Генератор:
|
||||
|
||||
- сохраняет все 491 штатную строку и исходную строковую таблицу байт-в-байт;
|
||||
- добавляет отсутствующие причёски из `CharHairGeosets.dbc` Y;
|
||||
- добавляет отсутствующую растительность и украшения из
|
||||
`CharacterFacialHairStyles.dbc` Y;
|
||||
- работает только с сочетаниями расы, пола и типа, уже существующими в штатном
|
||||
`BarberShopStyle.dbc`, то есть с поддерживаемыми расами WotLK;
|
||||
- сохраняет штатные строки типа 3 для особой кастомизации рыцарей смерти;
|
||||
- назначает новые уникальные ID после максимального штатного ID;
|
||||
- проверяет отсутствие дубликатов и полноту покрытия.
|
||||
|
||||
Результат:
|
||||
|
||||
- 3 765 строк всего;
|
||||
- 152 новые причёски;
|
||||
- 3 122 новых варианта растительности, серёг и других особенностей;
|
||||
- максимальный номер лицевого варианта — 255, что помещается в поле внешности
|
||||
персонажа 3.3.5a.
|
||||
|
||||
В исходном комплекте нет названий новых стилей. Поэтому сейчас они отображаются
|
||||
как `New hairstyle N` и `New appearance N`. Это только подписи: на выбор модели
|
||||
и сохранение внешности они не влияют.
|
||||
|
||||
## Что требуется клиенту и серверу
|
||||
|
||||
| Компонент | Клиент | Сервер |
|
||||
|---|---|---|
|
||||
| `patch-ruRU-X.MPQ` | Да, для текущего комплекта NPC/персонажей | Нет |
|
||||
| `patch-ruRU-Y.MPQ` | Да, для моделей, текстур и основных customization DBC | Нет |
|
||||
| `patch-ruRU-Z.MPQ` | Да, для совместимости и исправления парикмахерской | Нет как MPQ |
|
||||
| Сгенерированный `BarberShopStyle.dbc` | Уже находится внутри Z | Да, отдельным DBC |
|
||||
| Остальные клиентские DBC из X/Y | Загружаются из MPQ | Для текущих десяти рас не копируются |
|
||||
|
||||
Серверную копию нужно положить в каталог DBC worldserver, обычно:
|
||||
|
||||
```text
|
||||
<server>/data/dbc/BarberShopStyle.dbc
|
||||
```
|
||||
|
||||
После замены требуется полный перезапуск worldserver: DBC загружаются при
|
||||
старте. Клиентская и серверная копии `BarberShopStyle.dbc` должны быть одним и
|
||||
тем же сгенерированным файлом. Без серверной копии клиент больше не должен
|
||||
падать при переключении, но сервер может отклонить применение нового стиля или
|
||||
неверно рассчитать его стоимость.
|
||||
|
||||
Готовая серверная копия после запуска генератора находится в:
|
||||
|
||||
```text
|
||||
build/customization-compat/server-dbc/BarberShopStyle.dbc
|
||||
```
|
||||
|
||||
Тот же отслеживаемый исходник для Z находится в:
|
||||
|
||||
```text
|
||||
src/Data/ruRU/patch-ruRU-Z/DBFilesClient/BarberShopStyle.dbc
|
||||
```
|
||||
|
||||
Не следует копировать на сервер `ChrRaces.dbc`, `CharSections.dbc` или весь
|
||||
набор DBC из Y без отдельной задачи и проверки ядра.
|
||||
|
||||
## Сборка
|
||||
|
||||
### Предварительные условия
|
||||
|
||||
В `.env` должен быть задан корректный `WOW_HOME`. В клиенте должны находиться:
|
||||
|
||||
- штатный `Data\ruRU\patch-ruRU-3.MPQ`;
|
||||
- CVPatch A, B, I и S, используемые генератором как источники слияния;
|
||||
- остальные CVPatch, если они должны остаться в итоговой установке.
|
||||
|
||||
В репозитории должны оставаться:
|
||||
|
||||
- `new customization mpqs\patch-a-001.mpq`;
|
||||
- `new customization mpqs\patch-b-002.mpq`;
|
||||
- `tool\target\release\mpqread.exe`.
|
||||
|
||||
### Пересоздание DBC совместимости
|
||||
|
||||
```powershell
|
||||
.\tool\scripts\build-customization-compat.ps1
|
||||
```
|
||||
|
||||
Скрипт обновляет:
|
||||
|
||||
```text
|
||||
assets/dbc/3.3.5a/CreatureModelData.dbc
|
||||
assets/dbc/3.3.5a/CreatureDisplayInfo.dbc
|
||||
src/Data/ruRU/patch-ruRU-Z/DBFilesClient/CreatureDisplayInfoExtra.dbc
|
||||
src/Data/ruRU/patch-ruRU-Z/DBFilesClient/SpellVisualKitModelAttach.dbc
|
||||
src/Data/ruRU/patch-ruRU-Z/DBFilesClient/BarberShopStyle.dbc
|
||||
build/customization-compat/server-dbc/BarberShopStyle.dbc
|
||||
```
|
||||
|
||||
### Упаковка MPQ
|
||||
|
||||
```powershell
|
||||
.\tool\target\release\tool.exe .\src .\dist
|
||||
```
|
||||
|
||||
Упаковщик:
|
||||
|
||||
- собирает Z из исходников и сгенерированных MoonWell DBC;
|
||||
- копирует `patch-a-001.mpq` в `dist\Data\ruRU\patch-ruRU-X.MPQ`;
|
||||
- копирует `patch-b-002.mpq` в `dist\Data\ruRU\patch-ruRU-Y.MPQ`;
|
||||
- не добавляет `patch-k.mpq`.
|
||||
|
||||
Для полной локальной сборки и синхронизации клиента можно использовать:
|
||||
|
||||
```powershell
|
||||
.\run.ps1 -Env local
|
||||
```
|
||||
|
||||
Этот сценарий также собирает WarcraftXL, останавливает блокирующие процессы и
|
||||
синхронизирует весь `dist` в `WOW_HOME`. Для проверки только изменения Z можно
|
||||
собрать архив упаковщиком и скопировать лишь
|
||||
`dist\Data\ruRU\patch-ruRU-Z.MPQ`.
|
||||
|
||||
Сборка этих архивов не должна автоматически изменять `manifest.json`. Манифест
|
||||
обновляется владельцем проекта отдельно после локальной проверки.
|
||||
|
||||
## Исходники MPQ и установленные копии
|
||||
|
||||
Файлы в `new customization mpqs` — источники сборки. В готовом клиенте
|
||||
используются их копии X и Y.
|
||||
|
||||
Технически уже установленный клиент может запускаться без исходников в
|
||||
репозитории, если X/Y остаются в `Data\ruRU`. Однако удаление исходников сломает
|
||||
повторную сборку `dist`, восстановление клиента и генерацию совместимости.
|
||||
Поэтому `patch-a-001.mpq` и `patch-b-002.mpq` нужно хранить.
|
||||
|
||||
Z не консолидирует многогигабайтные модели и текстуры X/Y. Удаление X или Y из
|
||||
клиента не освобождается наличием текущих DBC в Z.
|
||||
|
||||
## Проверка после сборки
|
||||
|
||||
Минимальный сценарий локальной проверки:
|
||||
|
||||
1. Полностью закрыть WoW перед заменой MPQ.
|
||||
2. Проверить наличие X, Y и Z в `Data\ruRU`.
|
||||
3. Запустить клиент и открыть создание персонажа.
|
||||
4. Проверить оба пола и основные расы, пролистать причёски и лицевые варианты.
|
||||
5. Создать тестового персонажа, войти в мир и проверить модель после повторного
|
||||
входа.
|
||||
6. Открыть парикмахерскую и несколько раз нажать вперёд/назад у каждого
|
||||
селектора.
|
||||
7. Проверить отображение стоимости и отсутствие `ERROR #132`.
|
||||
8. Применить новый стиль, выйти из мира и войти снова.
|
||||
9. Проверить старую броню, NPC-модели и эффекты заклинаний, которые могли быть
|
||||
затронуты конфликтующими DBC.
|
||||
|
||||
Для шага 8 сервер уже должен использовать сгенерированный
|
||||
`BarberShopStyle.dbc`.
|
||||
|
||||
## Диагностика
|
||||
|
||||
| Симптом | Вероятная причина |
|
||||
|---|---|
|
||||
| Новых причёсок нет | Отсутствует Y или более поздний MPQ перекрыл его `CharHairGeosets`/`CharSections` |
|
||||
| Модель отсутствует или отображается неверно | X/Y не скопированы полностью либо повреждены многогигабайтные архивы |
|
||||
| Падение при переключении в парикмахерской | Z отсутствует, загружен раньше Y или его `BarberShopStyle.dbc` перекрыт |
|
||||
| Вариант выбирается, но не применяется | На сервере остался штатный `BarberShopStyle.dbc` или worldserver не перезапущен |
|
||||
| Исчезли старые NPC/display ID | Z не пересобран после изменения источников CVPatch или Y |
|
||||
| Проблемы со старой экипировкой | Потеряны item-поля CVPatch I в `CreatureDisplayInfoExtra.dbc` |
|
||||
| Изменился экран входа/создания персонажа | `patch-k.mpq` был установлен вручную |
|
||||
|
||||
Перед заменой рабочего Z рекомендуется сохранять предыдущий архив. Последняя
|
||||
локальная резервная копия, созданная во время исправления парикмахерской:
|
||||
|
||||
```text
|
||||
build/customization-compat/client-backup/patch-ruRU-Z.before-barbershop.MPQ
|
||||
```
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,410 @@
|
||||
param(
|
||||
[string]$ProjectRoot = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path,
|
||||
[string]$ClientPath
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
function Get-EnvValue([string]$Path, [string]$Name) {
|
||||
$line = Get-Content -LiteralPath $Path | Where-Object { $_ -match "^$([regex]::Escape($Name))=" } | Select-Object -First 1
|
||||
if ($line) { return $line.Substring($Name.Length + 1).Trim() }
|
||||
return $null
|
||||
}
|
||||
|
||||
if (-not $ClientPath) {
|
||||
$ClientPath = Get-EnvValue (Join-Path $ProjectRoot '.env') 'WOW_HOME'
|
||||
}
|
||||
if (-not $ClientPath -or -not (Test-Path -LiteralPath (Join-Path $ClientPath 'Data\ruRU'))) {
|
||||
throw 'A valid WoW 3.3.5 client path is required.'
|
||||
}
|
||||
|
||||
$reader = Join-Path $ProjectRoot 'tool\target\release\mpqread.exe'
|
||||
if (-not (Test-Path -LiteralPath $reader)) {
|
||||
throw "MPQ reader not found: $reader"
|
||||
}
|
||||
|
||||
$work = Join-Path $ProjectRoot 'build\customization-compat'
|
||||
New-Item -ItemType Directory -Path $work -Force | Out-Null
|
||||
|
||||
function Export-MpqFile([string]$Archive, [string]$VirtualPath, [string]$OutputName) {
|
||||
$output = Join-Path $work $OutputName
|
||||
& $reader $Archive $VirtualPath $output 2>$null
|
||||
if ($LASTEXITCODE -ne 0 -or -not (Test-Path -LiteralPath $output)) {
|
||||
throw "Cannot extract $VirtualPath from $Archive"
|
||||
}
|
||||
return $output
|
||||
}
|
||||
|
||||
function Read-U32([byte[]]$Bytes, [int]$Offset) {
|
||||
return [BitConverter]::ToUInt32($Bytes, $Offset)
|
||||
}
|
||||
|
||||
function Write-U32([byte[]]$Bytes, [int]$Field, [uint32]$Value) {
|
||||
[BitConverter]::GetBytes($Value).CopyTo($Bytes, $Field * 4)
|
||||
}
|
||||
|
||||
function Read-Wdbc([string]$Path) {
|
||||
[byte[]]$bytes = [IO.File]::ReadAllBytes($Path)
|
||||
if ($bytes.Length -lt 20 -or [Text.Encoding]::ASCII.GetString($bytes, 0, 4) -ne 'WDBC') {
|
||||
throw "Not a WDBC file: $Path"
|
||||
}
|
||||
$count = Read-U32 $bytes 4
|
||||
$fields = Read-U32 $bytes 8
|
||||
$recordSize = Read-U32 $bytes 12
|
||||
$stringSize = Read-U32 $bytes 16
|
||||
$recordsEnd = 20 + ($count * $recordSize)
|
||||
if ($recordsEnd + $stringSize -ne $bytes.Length) {
|
||||
throw "Invalid WDBC size: $Path"
|
||||
}
|
||||
$records = [Collections.Generic.Dictionary[uint32,byte[]]]::new()
|
||||
for ($index = 0; $index -lt $count; $index++) {
|
||||
[byte[]]$row = New-Object byte[] $recordSize
|
||||
[Array]::Copy($bytes, 20 + ($index * $recordSize), $row, 0, $recordSize)
|
||||
$records.Add((Read-U32 $row 0), $row)
|
||||
}
|
||||
[byte[]]$strings = New-Object byte[] $stringSize
|
||||
[Array]::Copy($bytes, $recordsEnd, $strings, 0, $stringSize)
|
||||
return [pscustomobject]@{
|
||||
Path = $Path
|
||||
Fields = [uint32]$fields
|
||||
RecordSize = [uint32]$recordSize
|
||||
Records = $records
|
||||
Strings = $strings
|
||||
}
|
||||
}
|
||||
|
||||
function Read-WdbcRows([string]$Path) {
|
||||
[byte[]]$bytes = [IO.File]::ReadAllBytes($Path)
|
||||
if ($bytes.Length -lt 20 -or [Text.Encoding]::ASCII.GetString($bytes, 0, 4) -ne 'WDBC') {
|
||||
throw "Not a WDBC file: $Path"
|
||||
}
|
||||
$count = Read-U32 $bytes 4
|
||||
$fields = Read-U32 $bytes 8
|
||||
$recordSize = Read-U32 $bytes 12
|
||||
$stringSize = Read-U32 $bytes 16
|
||||
$recordsEnd = 20 + ($count * $recordSize)
|
||||
if ($recordsEnd + $stringSize -ne $bytes.Length) {
|
||||
throw "Invalid WDBC size: $Path"
|
||||
}
|
||||
$rows = New-Object Collections.Generic.List[byte[]]
|
||||
for ($index = 0; $index -lt $count; $index++) {
|
||||
[byte[]]$row = New-Object byte[] $recordSize
|
||||
[Array]::Copy($bytes, 20 + ($index * $recordSize), $row, 0, $recordSize)
|
||||
$rows.Add($row)
|
||||
}
|
||||
[byte[]]$strings = New-Object byte[] $stringSize
|
||||
[Array]::Copy($bytes, $recordsEnd, $strings, 0, $stringSize)
|
||||
return [pscustomobject]@{
|
||||
Path = $Path
|
||||
Fields = [uint32]$fields
|
||||
RecordSize = [uint32]$recordSize
|
||||
Rows = $rows
|
||||
Strings = $strings
|
||||
}
|
||||
}
|
||||
|
||||
function Get-StringBytes($Table, [uint32]$Offset) {
|
||||
if ($Offset -eq 0) { return ,([byte[]]@()) }
|
||||
if ($Offset -ge $Table.Strings.Length) { throw "Bad string offset $Offset in $($Table.Path)" }
|
||||
$end = [int]$Offset
|
||||
while ($end -lt $Table.Strings.Length -and $Table.Strings[$end] -ne 0) { $end++ }
|
||||
[byte[]]$value = New-Object byte[] ($end - [int]$Offset)
|
||||
if ($value.Length) { [Array]::Copy($Table.Strings, $Offset, $value, 0, $value.Length) }
|
||||
return ,$value
|
||||
}
|
||||
|
||||
function Get-RowSignature($Table, [byte[]]$Row, [int[]]$StringFields) {
|
||||
$parts = New-Object Collections.Generic.List[string]
|
||||
for ($field = 0; $field -lt $Table.Fields; $field++) {
|
||||
$value = Read-U32 $Row ($field * 4)
|
||||
if ($StringFields -contains $field) {
|
||||
$parts.Add('s:' + [Convert]::ToBase64String((Get-StringBytes $Table $value)))
|
||||
} else {
|
||||
$parts.Add('u:' + $value)
|
||||
}
|
||||
}
|
||||
return $parts -join '|'
|
||||
}
|
||||
|
||||
function Select-DeltaMerge($Base, $Old, $New, [int[]]$StringFields, [string]$Label) {
|
||||
$ids = [Collections.Generic.HashSet[uint32]]::new()
|
||||
foreach ($id in $Base.Records.Keys) { [void]$ids.Add($id) }
|
||||
foreach ($id in $Old.Records.Keys) { [void]$ids.Add($id) }
|
||||
foreach ($id in $New.Records.Keys) { [void]$ids.Add($id) }
|
||||
$selected = [Collections.Generic.Dictionary[uint32,object]]::new()
|
||||
$oldOnly = 0
|
||||
$newChanges = 0
|
||||
$conflicts = 0
|
||||
foreach ($id in $ids) {
|
||||
$baseRow = $null; $oldRow = $null; $newRow = $null
|
||||
[void]$Base.Records.TryGetValue($id, [ref]$baseRow)
|
||||
[void]$Old.Records.TryGetValue($id, [ref]$oldRow)
|
||||
[void]$New.Records.TryGetValue($id, [ref]$newRow)
|
||||
$baseSig = if ($baseRow) { Get-RowSignature $Base $baseRow $StringFields } else { $null }
|
||||
$oldSig = if ($oldRow) { Get-RowSignature $Old $oldRow $StringFields } else { $null }
|
||||
$newSig = if ($newRow) { Get-RowSignature $New $newRow $StringFields } else { $null }
|
||||
$oldChanged = $oldRow -and ($null -eq $baseRow -or $oldSig -ne $baseSig)
|
||||
$newChanged = $newRow -and ($null -eq $baseRow -or $newSig -ne $baseSig)
|
||||
if ($oldChanged -and $newChanged -and $oldSig -ne $newSig) { $conflicts++ }
|
||||
if ($newChanged) {
|
||||
$selected.Add($id, [pscustomobject]@{ Table = $New; Row = $newRow })
|
||||
$newChanges++
|
||||
} elseif ($oldChanged) {
|
||||
$selected.Add($id, [pscustomobject]@{ Table = $Old; Row = $oldRow })
|
||||
$oldOnly++
|
||||
} elseif ($newRow) {
|
||||
$selected.Add($id, [pscustomobject]@{ Table = $New; Row = $newRow })
|
||||
} elseif ($oldRow) {
|
||||
$selected.Add($id, [pscustomobject]@{ Table = $Old; Row = $oldRow })
|
||||
} else {
|
||||
$selected.Add($id, [pscustomobject]@{ Table = $Base; Row = $baseRow })
|
||||
}
|
||||
}
|
||||
Write-Host "${Label}: rows=$($selected.Count), retained-old=$oldOnly, applied-new=$newChanges, same-id-conflicts=$conflicts"
|
||||
return $selected
|
||||
}
|
||||
|
||||
function Select-DisplayExtraMerge($Base, $OldItem, $New) {
|
||||
$ids = [Collections.Generic.HashSet[uint32]]::new()
|
||||
foreach ($id in $OldItem.Records.Keys) { [void]$ids.Add($id) }
|
||||
foreach ($id in $New.Records.Keys) { [void]$ids.Add($id) }
|
||||
$selected = [Collections.Generic.Dictionary[uint32,object]]::new()
|
||||
$itemFieldsApplied = 0
|
||||
foreach ($id in $ids) {
|
||||
$baseRow = $null; $oldRow = $null; $newRow = $null
|
||||
[void]$Base.Records.TryGetValue($id, [ref]$baseRow)
|
||||
[void]$OldItem.Records.TryGetValue($id, [ref]$oldRow)
|
||||
[void]$New.Records.TryGetValue($id, [ref]$newRow)
|
||||
if ($newRow) {
|
||||
[byte[]]$merged = $newRow.Clone()
|
||||
if ($oldRow) {
|
||||
for ($field = 8; $field -le 18; $field++) {
|
||||
$oldValue = Read-U32 $oldRow ($field * 4)
|
||||
$baseValue = if ($baseRow) { Read-U32 $baseRow ($field * 4) } else { 0 }
|
||||
if (-not $baseRow -or $oldValue -ne $baseValue) {
|
||||
Write-U32 $merged $field $oldValue
|
||||
$itemFieldsApplied++
|
||||
}
|
||||
}
|
||||
}
|
||||
$selected.Add($id, [pscustomobject]@{ Table = $New; Row = $merged })
|
||||
} else {
|
||||
$selected.Add($id, [pscustomobject]@{ Table = $OldItem; Row = $oldRow })
|
||||
}
|
||||
}
|
||||
Write-Host "CreatureDisplayInfoExtra: rows=$($selected.Count), retained item fields from patch I=$itemFieldsApplied"
|
||||
return $selected
|
||||
}
|
||||
|
||||
function Write-Wdbc($Template, $Selected, [int[]]$StringFields, [string]$Path) {
|
||||
$stringOffsets = [Collections.Generic.Dictionary[string,uint32]]::new([StringComparer]::Ordinal)
|
||||
$strings = New-Object Collections.Generic.List[byte]
|
||||
$strings.Add(0)
|
||||
$stringOffsets.Add('', 0)
|
||||
$rows = New-Object Collections.Generic.List[byte[]]
|
||||
foreach ($id in ($Selected.Keys | Sort-Object)) {
|
||||
$choice = $Selected[$id]
|
||||
[byte[]]$row = $choice.Row.Clone()
|
||||
foreach ($field in $StringFields) {
|
||||
$sourceOffset = Read-U32 $row ($field * 4)
|
||||
[byte[]]$value = Get-StringBytes $choice.Table $sourceOffset
|
||||
$key = [Convert]::ToBase64String($value)
|
||||
[uint32]$targetOffset = 0
|
||||
if (-not $stringOffsets.TryGetValue($key, [ref]$targetOffset)) {
|
||||
$targetOffset = [uint32]$strings.Count
|
||||
if ($value.Length) { $strings.AddRange($value) }
|
||||
$strings.Add(0)
|
||||
$stringOffsets.Add($key, $targetOffset)
|
||||
}
|
||||
Write-U32 $row $field $targetOffset
|
||||
}
|
||||
$rows.Add($row)
|
||||
}
|
||||
$parent = Split-Path -Parent $Path
|
||||
New-Item -ItemType Directory -Path $parent -Force | Out-Null
|
||||
$stream = [IO.File]::Open($Path, [IO.FileMode]::Create, [IO.FileAccess]::Write)
|
||||
try {
|
||||
$writer = New-Object IO.BinaryWriter($stream)
|
||||
$writer.Write([Text.Encoding]::ASCII.GetBytes('WDBC'))
|
||||
$writer.Write([uint32]$rows.Count)
|
||||
$writer.Write([uint32]$Template.Fields)
|
||||
$writer.Write([uint32]$Template.RecordSize)
|
||||
$writer.Write([uint32]$strings.Count)
|
||||
foreach ($row in $rows) { $writer.Write($row) }
|
||||
$writer.Write($strings.ToArray())
|
||||
$writer.Flush()
|
||||
} finally {
|
||||
$stream.Dispose()
|
||||
}
|
||||
}
|
||||
|
||||
function Write-ExtendedBarberShopStyle($Base, $HairGeosets, $FacialHairStyles, [string]$Path) {
|
||||
if ($Base.Fields -ne 40 -or $Base.RecordSize -ne 160) {
|
||||
throw "Unexpected BarberShopStyle schema in $($Base.Path)"
|
||||
}
|
||||
if ($HairGeosets.Fields -ne 6 -or $HairGeosets.RecordSize -ne 24) {
|
||||
throw "Unexpected CharHairGeosets schema in $($HairGeosets.Path)"
|
||||
}
|
||||
if ($FacialHairStyles.Fields -ne 8 -or $FacialHairStyles.RecordSize -ne 32) {
|
||||
throw "Unexpected CharacterFacialHairStyles schema in $($FacialHairStyles.Path)"
|
||||
}
|
||||
|
||||
$templates = [Collections.Generic.Dictionary[string,byte[]]]::new([StringComparer]::Ordinal)
|
||||
$existing = [Collections.Generic.HashSet[string]]::new([StringComparer]::Ordinal)
|
||||
[uint32]$nextId = 1
|
||||
foreach ($row in $Base.Rows) {
|
||||
$id = Read-U32 $row 0
|
||||
if ($id -ge $nextId) { $nextId = $id + 1 }
|
||||
$type = Read-U32 $row 4
|
||||
if ($type -ne 0 -and $type -ne 2) { continue }
|
||||
$race = Read-U32 $row (37 * 4)
|
||||
$gender = Read-U32 $row (38 * 4)
|
||||
$style = Read-U32 $row (39 * 4)
|
||||
$groupKey = "${race}:${gender}:${type}"
|
||||
if (-not $templates.ContainsKey($groupKey)) { $templates.Add($groupKey, $row) }
|
||||
[void]$existing.Add("${groupKey}:${style}")
|
||||
}
|
||||
|
||||
$targets = New-Object Collections.Generic.List[object]
|
||||
$targetKeys = [Collections.Generic.HashSet[string]]::new([StringComparer]::Ordinal)
|
||||
foreach ($row in $HairGeosets.Rows) {
|
||||
$race = Read-U32 $row 4
|
||||
$gender = Read-U32 $row 8
|
||||
$style = Read-U32 $row 12
|
||||
$groupKey = "${race}:${gender}:0"
|
||||
$targetKey = "${groupKey}:${style}"
|
||||
if ($templates.ContainsKey($groupKey) -and $targetKeys.Add($targetKey)) {
|
||||
$targets.Add([pscustomobject]@{ Race = $race; Gender = $gender; Type = 0; Style = $style; GroupKey = $groupKey; TargetKey = $targetKey })
|
||||
}
|
||||
}
|
||||
foreach ($row in $FacialHairStyles.Rows) {
|
||||
$race = Read-U32 $row 0
|
||||
$gender = Read-U32 $row 4
|
||||
$style = Read-U32 $row 8
|
||||
$groupKey = "${race}:${gender}:2"
|
||||
$targetKey = "${groupKey}:${style}"
|
||||
if ($templates.ContainsKey($groupKey) -and $targetKeys.Add($targetKey)) {
|
||||
$targets.Add([pscustomobject]@{ Race = $race; Gender = $gender; Type = 2; Style = $style; GroupKey = $groupKey; TargetKey = $targetKey })
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($groupKey in $templates.Keys) {
|
||||
if (-not ($targets | Where-Object { $_.GroupKey -eq $groupKey } | Select-Object -First 1)) {
|
||||
throw "No customization rows found for BarberShopStyle group $groupKey"
|
||||
}
|
||||
}
|
||||
|
||||
$rows = New-Object Collections.Generic.List[byte[]]
|
||||
foreach ($row in $Base.Rows) { $rows.Add($row.Clone()) }
|
||||
$strings = New-Object Collections.Generic.List[byte]
|
||||
$strings.AddRange($Base.Strings)
|
||||
$newStringOffsets = [Collections.Generic.Dictionary[string,uint32]]::new([StringComparer]::Ordinal)
|
||||
$addedHair = 0
|
||||
$addedFacial = 0
|
||||
|
||||
foreach ($target in ($targets | Sort-Object Race, Gender, Type, Style)) {
|
||||
if ($existing.Contains($target.TargetKey)) { continue }
|
||||
[byte[]]$row = $templates[$target.GroupKey].Clone()
|
||||
Write-U32 $row 0 $nextId
|
||||
Write-U32 $row 39 ([uint32]$target.Style)
|
||||
|
||||
# patch-ruRU-3 stores its localized display name in locale slot 8
|
||||
# (field 10). New names are intentionally generic because the source
|
||||
# customization archive contains no BarberShopStyle names.
|
||||
for ($field = 2; $field -le 17; $field++) { Write-U32 $row $field 0 }
|
||||
$label = if ($target.Type -eq 0) {
|
||||
"New hairstyle $([uint32]$target.Style + 1)"
|
||||
} else {
|
||||
"New appearance $([uint32]$target.Style + 1)"
|
||||
}
|
||||
[uint32]$nameOffset = 0
|
||||
if (-not $newStringOffsets.TryGetValue($label, [ref]$nameOffset)) {
|
||||
$nameOffset = [uint32]$strings.Count
|
||||
$strings.AddRange([Text.Encoding]::UTF8.GetBytes($label))
|
||||
$strings.Add(0)
|
||||
$newStringOffsets.Add($label, $nameOffset)
|
||||
}
|
||||
Write-U32 $row 10 $nameOffset
|
||||
|
||||
$rows.Add($row)
|
||||
[void]$existing.Add($target.TargetKey)
|
||||
$nextId++
|
||||
if ($target.Type -eq 0) { $addedHair++ } else { $addedFacial++ }
|
||||
}
|
||||
|
||||
foreach ($target in $targets) {
|
||||
if (-not $existing.Contains($target.TargetKey)) {
|
||||
throw "Missing generated BarberShopStyle row $($target.TargetKey)"
|
||||
}
|
||||
}
|
||||
|
||||
$parent = Split-Path -Parent $Path
|
||||
New-Item -ItemType Directory -Path $parent -Force | Out-Null
|
||||
$stream = [IO.File]::Open($Path, [IO.FileMode]::Create, [IO.FileAccess]::Write)
|
||||
try {
|
||||
$writer = New-Object IO.BinaryWriter($stream)
|
||||
$writer.Write([Text.Encoding]::ASCII.GetBytes('WDBC'))
|
||||
$writer.Write([uint32]$rows.Count)
|
||||
$writer.Write([uint32]$Base.Fields)
|
||||
$writer.Write([uint32]$Base.RecordSize)
|
||||
$writer.Write([uint32]$strings.Count)
|
||||
foreach ($row in $rows) { $writer.Write($row) }
|
||||
$writer.Write($strings.ToArray())
|
||||
$writer.Flush()
|
||||
} finally {
|
||||
$stream.Dispose()
|
||||
}
|
||||
|
||||
Write-Host "BarberShopStyle: rows=$($rows.Count), added-hair=$addedHair, added-appearance=$addedFacial"
|
||||
}
|
||||
|
||||
$locale = Join-Path $ClientPath 'Data\ruRU'
|
||||
$baseArchive = Join-Path $locale 'patch-ruRU-3.MPQ'
|
||||
$oldA = Join-Path $locale 'patch-ruRU-A.mpq'
|
||||
$oldB = Join-Path $locale 'patch-ruRU-B.mpq'
|
||||
$oldI = Join-Path $locale 'patch-ruRU-I.mpq'
|
||||
$oldS = Join-Path $locale 'patch-ruRU-S.mpq'
|
||||
$newB = Join-Path $ProjectRoot 'new customization mpqs\patch-b-002.mpq'
|
||||
|
||||
$baseModel = Read-Wdbc (Export-MpqFile $baseArchive 'DBFilesClient\CreatureModelData.dbc' 'base-CreatureModelData.dbc')
|
||||
$baseDisplay = Read-Wdbc (Export-MpqFile $baseArchive 'DBFilesClient\CreatureDisplayInfo.dbc' 'base-CreatureDisplayInfo.dbc')
|
||||
$baseExtra = Read-Wdbc (Export-MpqFile $baseArchive 'DBFilesClient\CreatureDisplayInfoExtra.dbc' 'base-CreatureDisplayInfoExtra.dbc')
|
||||
$oldModel = Read-Wdbc (Export-MpqFile $oldA 'DBFilesClient\CreatureModelData.dbc' 'old-CreatureModelData.dbc')
|
||||
$oldDisplay = Read-Wdbc (Export-MpqFile $oldB 'DBFilesClient\CreatureDisplayInfo.dbc' 'old-CreatureDisplayInfo.dbc')
|
||||
$oldExtra = Read-Wdbc (Export-MpqFile $oldI 'DBFilesClient\CreatureDisplayInfoExtra.dbc' 'old-CreatureDisplayInfoExtra.dbc')
|
||||
$newModel = Read-Wdbc (Export-MpqFile $newB 'DBFilesClient\CreatureModelData.dbc' 'new-CreatureModelData.dbc')
|
||||
$newDisplay = Read-Wdbc (Export-MpqFile $newB 'DBFilesClient\CreatureDisplayInfo.dbc' 'new-CreatureDisplayInfo.dbc')
|
||||
$newExtra = Read-Wdbc (Export-MpqFile $newB 'DBFilesClient\CreatureDisplayInfoExtra.dbc' 'new-CreatureDisplayInfoExtra.dbc')
|
||||
$spellAttach = Export-MpqFile $oldS 'DBFilesClient\SpellVisualKitModelAttach.dbc' 'SpellVisualKitModelAttach.dbc'
|
||||
$baseBarber = Read-WdbcRows (Export-MpqFile $baseArchive 'DBFilesClient\BarberShopStyle.dbc' 'base-BarberShopStyle.dbc')
|
||||
$newHairGeosets = Read-WdbcRows (Export-MpqFile $newB 'DBFilesClient\CharHairGeosets.dbc' 'new-CharHairGeosets.dbc')
|
||||
$newFacialHair = Read-WdbcRows (Export-MpqFile $newB 'DBFilesClient\CharacterFacialHairStyles.dbc' 'new-CharacterFacialHairStyles.dbc')
|
||||
|
||||
$modelSelected = Select-DeltaMerge $baseModel $oldModel $newModel @(2) 'CreatureModelData'
|
||||
$displaySelected = Select-DeltaMerge $baseDisplay $oldDisplay $newDisplay @(6,7,8,9) 'CreatureDisplayInfo'
|
||||
$extraSelected = Select-DisplayExtraMerge $baseExtra $oldExtra $newExtra
|
||||
|
||||
$mergedModel = Join-Path $work 'merged-CreatureModelData.dbc'
|
||||
$mergedDisplay = Join-Path $work 'merged-CreatureDisplayInfo.dbc'
|
||||
$mergedExtra = Join-Path $work 'merged-CreatureDisplayInfoExtra.dbc'
|
||||
$extendedBarber = Join-Path $work 'extended-BarberShopStyle.dbc'
|
||||
Write-Wdbc $newModel $modelSelected @(2) $mergedModel
|
||||
Write-Wdbc $newDisplay $displaySelected @(6,7,8,9) $mergedDisplay
|
||||
Write-Wdbc $newExtra $extraSelected @(20) $mergedExtra
|
||||
Write-ExtendedBarberShopStyle $baseBarber $newHairGeosets $newFacialHair $extendedBarber
|
||||
|
||||
$baseTarget = Join-Path $ProjectRoot 'assets\dbc\3.3.5a'
|
||||
$compatTarget = Join-Path $ProjectRoot 'src\Data\ruRU\patch-ruRU-Z\DBFilesClient'
|
||||
New-Item -ItemType Directory -Path $compatTarget -Force | Out-Null
|
||||
Copy-Item -LiteralPath $mergedModel -Destination (Join-Path $baseTarget 'CreatureModelData.dbc') -Force
|
||||
Copy-Item -LiteralPath $mergedDisplay -Destination (Join-Path $baseTarget 'CreatureDisplayInfo.dbc') -Force
|
||||
Copy-Item -LiteralPath $mergedExtra -Destination (Join-Path $compatTarget 'CreatureDisplayInfoExtra.dbc') -Force
|
||||
Copy-Item -LiteralPath $spellAttach -Destination (Join-Path $compatTarget 'SpellVisualKitModelAttach.dbc') -Force
|
||||
Copy-Item -LiteralPath $extendedBarber -Destination (Join-Path $compatTarget 'BarberShopStyle.dbc') -Force
|
||||
|
||||
$serverTarget = Join-Path $work 'server-dbc'
|
||||
New-Item -ItemType Directory -Path $serverTarget -Force | Out-Null
|
||||
Copy-Item -LiteralPath $extendedBarber -Destination (Join-Path $serverTarget 'BarberShopStyle.dbc') -Force
|
||||
|
||||
Write-Host "Compatibility sources updated in $baseTarget and $compatTarget"
|
||||
Write-Host "Matching server DBC staged in $serverTarget"
|
||||
+42
-7
@@ -119,6 +119,35 @@ fn stage_loose_assets(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn stage_customization_archives(
|
||||
project_root: &Path,
|
||||
output_dir: &Path,
|
||||
) -> Result<(), Box<dyn Error>> {
|
||||
let source_dir = project_root.join("new customization mpqs");
|
||||
let locale_dir = output_dir.join("Data").join("ruRU");
|
||||
let archives = [
|
||||
("patch-a-001.mpq", "patch-ruRU-X.MPQ"),
|
||||
("patch-b-002.mpq", "patch-ruRU-Y.MPQ"),
|
||||
];
|
||||
|
||||
for (source_name, target_name) in archives {
|
||||
let source = source_dir.join(source_name);
|
||||
if !source.is_file() {
|
||||
println!(
|
||||
"Customization archive not staged (missing): {}",
|
||||
source.display()
|
||||
);
|
||||
continue;
|
||||
}
|
||||
fs::create_dir_all(&locale_dir)?;
|
||||
let target = locale_dir.join(target_name);
|
||||
fs::copy(&source, &target)?;
|
||||
println!("Staged customization archive: {}", target.display());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
let args: Vec<String> = env::args().collect();
|
||||
|
||||
@@ -167,20 +196,26 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||
}
|
||||
let sub_name = sub.file_name().to_string_lossy().to_string();
|
||||
let output_mpq = output_dir.join("Data").join(&name).join(&sub_name);
|
||||
build_mpq(&sub_path, &output_mpq, None)?;
|
||||
// The final ruRU Z layer is the compatibility authority for customization,
|
||||
// legacy visual-patch DBCs and MoonWell custom creature rows.
|
||||
let creature_files = if name.eq_ignore_ascii_case("ruRU")
|
||||
&& sub_name.eq_ignore_ascii_case("patch-ruRU-Z")
|
||||
{
|
||||
generated_creatures.as_ref()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
build_mpq(&sub_path, &output_mpq, creature_files)?;
|
||||
}
|
||||
} else {
|
||||
// Regular patch dir, e.g. patch-4/ or patch-Z/
|
||||
let output_mpq = output_dir.join("Data").join(&name);
|
||||
let creature_files = if name.eq_ignore_ascii_case("patch-Z") {
|
||||
generated_creatures.as_ref()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
build_mpq(&path, &output_mpq, creature_files)?;
|
||||
build_mpq(&path, &output_mpq, None)?;
|
||||
}
|
||||
}
|
||||
|
||||
stage_customization_archives(&project_root, &output_dir)?;
|
||||
|
||||
println!("\nAll MPQ archives built successfully.");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user