Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ecf2824e44 | |||
| f86dd83e21 | |||
| df143b0bf1 | |||
| 583da559b4 | |||
| 1a9b80d7df | |||
| 29ac575197 | |||
| c768f27d84 |
@@ -1,46 +0,0 @@
|
|||||||
# MoonWell Client Sources — Agent Guide
|
|
||||||
|
|
||||||
## Проект
|
|
||||||
|
|
||||||
WoW-клиент на базе патча **3.3.5a (build 12340, WotLK)**. Репозиторий содержит UI-аддоны и
|
|
||||||
кастомные интерфейсы для приватного сервера MoonWell.
|
|
||||||
|
|
||||||
Стек: Lua 5.1, FrameXML (XML + Lua), WoW Addon API 3.3.5.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Навыки (Skills)
|
|
||||||
|
|
||||||
Перед тем как работать с UI — **прочитай соответствующий файл из `ai-skills/`**.
|
|
||||||
|
|
||||||
| Задача | Файл |
|
|
||||||
|--------|------|
|
|
||||||
| Верстка фреймов, XML, Lua CreateFrame, pixel-perfect из Figma, текстуры, шрифты, якоря | [`ai-skills/ui/SKILL.md`](ai-skills/ui/SKILL.md) |
|
|
||||||
| Виджеты, типы фреймов, Button/EditBox/ScrollFrame, события, SavedVariables, Blizzard API | [`ai-skills/ui/widgets-and-api.md`](ai-skills/ui/widgets-and-api.md) |
|
|
||||||
|
|
||||||
Читай **весь файл** скилла, а не отдельные секции — там есть ограничения API, типичные ошибки
|
|
||||||
и готовые шаблоны.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Ключевые ограничения (всегда держи в голове)
|
|
||||||
|
|
||||||
- `## Interface: 30300` — никаких retail-API
|
|
||||||
- Нет `SetSize()` → только `SetWidth()` / `SetHeight()`
|
|
||||||
- Нет `C_Timer`, `CreateFramePool`, `PixelUtil`
|
|
||||||
- Текстуры: только степени двойки, максимум 512×512 px, формат `.tga` / `.blp`
|
|
||||||
- XML-атрибуты: `relativeTo` (не `relativeKey`), `$parentX` (не `$parent.X`)
|
|
||||||
- Кнопки и ScrollFrame в XML должны иметь `name`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Структура аддона
|
|
||||||
|
|
||||||
```
|
|
||||||
AddonName/
|
|
||||||
├── AddonName.toc ← обязательно, имя файла = имя папки
|
|
||||||
├── AddonName.xml ← верстка (загружается первой)
|
|
||||||
└── AddonName.lua ← логика
|
|
||||||
```
|
|
||||||
|
|
||||||
Порядок файлов в TOC важен: XML раньше Lua.
|
|
||||||
@@ -1,535 +0,0 @@
|
|||||||
---
|
|
||||||
name: wow-335a-ui
|
|
||||||
description: >
|
|
||||||
Верстка и разработка UI-аддонов для World of Warcraft 3.3.5a (WotLK, build 12340).
|
|
||||||
Используй этот скилл при любой работе с интерфейсом WoW 3.3.5: создание фреймов
|
|
||||||
через XML или Lua CreateFrame(), позиционирование через SetPoint/якоря, слои
|
|
||||||
(BACKGROUND/ARTWORK/OVERLAY/HIGHLIGHT), страты (frameStrata), текстуры, шрифты,
|
|
||||||
кнопки, ScrollFrame, скрипты-события, структура аддона (TOC + XML + Lua),
|
|
||||||
SavedVariables, работа с Blizzard API патча 3.3.5. Также применяй когда речь идёт
|
|
||||||
об AzerothCore, кастомных интерфейсах для WoW-прайват серверов 3.3.5a, Eluna
|
|
||||||
или модификации FrameXML. ОБЯЗАТЕЛЬНО применяй при задачах pixel-perfect переноса
|
|
||||||
дизайна (Figma/PSD/скриншот) в WoW UI: пересчёт координат с учётом UIParent scale,
|
|
||||||
конвертация цветов HEX→RGBA, подбор шрифтов, подготовка текстур (степени двойки,
|
|
||||||
TexCoord-кроп), устранение расхождений между макетом и игровым интерфейсом.
|
|
||||||
---
|
|
||||||
|
|
||||||
# WoW 3.3.5a — Верстка UI (FrameXML / AddOn Development)
|
|
||||||
|
|
||||||
## Ключевые ограничения патча 3.3.5a
|
|
||||||
|
|
||||||
- **API версии**: `## Interface: 30300` (build 12340)
|
|
||||||
- **Lua версия**: Lua 5.1
|
|
||||||
- **Текстуры**: только степени двойки по ширине/высоте (8, 16, 32, 64, 128, 256, 512 px); максимум 512×512
|
|
||||||
- **Нет** `:SetSize()` — только `:SetWidth()` / `:SetHeight()` (или через XML `<Size>`)
|
|
||||||
- **Нет** `hooksecurefunc` для защищённых фреймов без taint
|
|
||||||
- Отсутствуют многие retail-API: нет `C_Timer`, нет `CreateFramePool`, нет `PixelUtil`
|
|
||||||
- Таймеры — через `OnUpdate` с накоплением `elapsed`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Pixel-Perfect: перенос дизайна из Figma/PSD в WoW UI
|
|
||||||
|
|
||||||
### Главная проблема: система координат WoW ≠ пиксели экрана
|
|
||||||
|
|
||||||
WoW рисует UI в **виртуальных единицах**, а не в экранных пикселях. Размер виртуального экрана зафиксирован движком на **768 единиц по высоте**, ширина зависит от соотношения сторон:
|
|
||||||
|
|
||||||
| Разрешение | Виртуальная ширина | Виртуальная высота |
|
|
||||||
|-----------|-------------------|-------------------|
|
|
||||||
| 1024×768 | 1024 | 768 |
|
|
||||||
| 1280×960 | 1024 | 768 |
|
|
||||||
| 1920×1080 | 1365 | 768 |
|
|
||||||
| 2560×1440 | 1365 | 768 |
|
|
||||||
|
|
||||||
> **Вывод**: высота UIParent всегда = 768. Всё, что сделано в Figma на 768px по высоте, переносится 1:1 без пересчёта. При другой высоте холста в Figma — нужен коэффициент.
|
|
||||||
|
|
||||||
### Пересчёт координат из Figma в WoW
|
|
||||||
|
|
||||||
```lua
|
|
||||||
-- Формула: wow_value = figma_value * (768 / figma_canvas_height)
|
|
||||||
-- Если макет сделан на 1080px:
|
|
||||||
local SCALE = 768 / 1080 -- ≈ 0.711
|
|
||||||
|
|
||||||
local function px(figma_px)
|
|
||||||
return math.floor(figma_px * SCALE + 0.5) -- округление к ближайшему
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Использование:
|
|
||||||
frame:SetWidth(px(400)) -- figma: 400px → wow: 284
|
|
||||||
frame:SetHeight(px(200)) -- figma: 200px → wow: 142
|
|
||||||
frame:SetPoint("TOPLEFT", UIParent, "TOPLEFT", px(50), -px(30))
|
|
||||||
```
|
|
||||||
|
|
||||||
> Если макет 768px в высоту — `SCALE = 1.0`, пересчёт не нужен.
|
|
||||||
|
|
||||||
### UIParent scale и как он ломает пиксели
|
|
||||||
|
|
||||||
Игрок может изменить масштаб UI в настройках. Это сдвигает все координаты.
|
|
||||||
|
|
||||||
```lua
|
|
||||||
-- Получить текущий масштаб UIParent
|
|
||||||
local uiScale = UIParent:GetScale() -- обычно 0.64–1.0
|
|
||||||
|
|
||||||
-- Перевести экранные пиксели в виртуальные единицы WoW
|
|
||||||
local function screenPxToWow(px)
|
|
||||||
local screenH = select(2, GetPhysicalScreenSize()) -- реальный размер экрана
|
|
||||||
return px * (768 / screenH) / uiScale
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Принудительно зафиксировать scale (использовать с умом!)
|
|
||||||
-- UIParent:SetScale(1.0) -- сломает все стандартные фреймы
|
|
||||||
```
|
|
||||||
|
|
||||||
**Рекомендация**: не трогай `UIParent:SetScale()`. Вместо этого рисуй в виртуальных единицах и принимай, что у разных игроков масштаб разный. Для абсолютного pixel-perfect на конкретном разрешении — дай опцию ручного масштабирования фрейма в настройках.
|
|
||||||
|
|
||||||
```lua
|
|
||||||
-- Масштабируй только свой фрейм
|
|
||||||
frame:SetScale(0.85)
|
|
||||||
-- Тогда все дочерние элементы тоже масштабируются
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Цвета: Figma HEX/RGBA → WoW RGBA
|
|
||||||
|
|
||||||
WoW принимает цвета в диапазоне **0.0–1.0** (не 0–255).
|
|
||||||
|
|
||||||
```lua
|
|
||||||
-- Конвертер HEX → WoW RGBA
|
|
||||||
local function hex(str, a)
|
|
||||||
str = str:gsub("#", "")
|
|
||||||
local r = tonumber(str:sub(1,2), 16) / 255
|
|
||||||
local g = tonumber(str:sub(3,4), 16) / 255
|
|
||||||
local b = tonumber(str:sub(5,6), 16) / 255
|
|
||||||
return r, g, b, a or 1.0
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Использование:
|
|
||||||
tex:SetVertexColor(hex("#FF6A00")) -- непрозрачный
|
|
||||||
tex:SetVertexColor(hex("#FF6A00", 0.8)) -- 80% непрозрачность
|
|
||||||
fs:SetTextColor(hex("#FFD100"))
|
|
||||||
frame:SetBackdropColor(hex("#1A1A2E", 0.95))
|
|
||||||
frame:SetBackdropBorderColor(hex("#4A90D9"))
|
|
||||||
```
|
|
||||||
|
|
||||||
**Figma opacity → WoW alpha**: значение из Figma (например, 75%) делится на 100 → `0.75`.
|
|
||||||
|
|
||||||
```lua
|
|
||||||
-- Figma: opacity 60%
|
|
||||||
frame:SetAlpha(0.60)
|
|
||||||
tex:SetAlpha(0.60)
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Шрифты: как приблизить к дизайну
|
|
||||||
|
|
||||||
WoW 3.3.5a поддерживает только `.ttf` шрифты, помещённые в папку аддона.
|
|
||||||
|
|
||||||
```lua
|
|
||||||
-- Кастомный шрифт из аддона
|
|
||||||
fs:SetFont("Interface\\AddOns\\MyAddon\\fonts\\MyFont.ttf", 14, "OUTLINE")
|
|
||||||
-- Флаги: "", "OUTLINE", "THICKOUTLINE", "MONOCHROME", "OUTLINE,MONOCHROME"
|
|
||||||
```
|
|
||||||
|
|
||||||
**Соответствие Figma → WoW размер шрифта**:
|
|
||||||
|
|
||||||
| Figma (макет 1080px) | WoW pt (при SCALE=0.711) |
|
|
||||||
|---------------------|--------------------------|
|
|
||||||
| 12 px | 9 |
|
|
||||||
| 14 px | 10 |
|
|
||||||
| 16 px | 11 |
|
|
||||||
| 18 px | 13 |
|
|
||||||
| 24 px | 17 |
|
|
||||||
| 32 px | 23 |
|
|
||||||
|
|
||||||
```lua
|
|
||||||
-- Автоматический пересчёт размера шрифта
|
|
||||||
local function fontSize(figma_pt)
|
|
||||||
return math.max(8, math.floor(figma_pt * SCALE + 0.5))
|
|
||||||
end
|
|
||||||
fs:SetFont("Fonts\\FRIZQT__.TTF", fontSize(18), "OUTLINE")
|
|
||||||
```
|
|
||||||
|
|
||||||
> **Минимальный читаемый размер в WoW**: 8 pt. Ниже — нечитаемо.
|
|
||||||
|
|
||||||
**Встроенные шрифты WoW 3.3.5a**:
|
|
||||||
```
|
|
||||||
Fonts\FRIZQT__.TTF — основной интерфейсный (без кириллицы!)
|
|
||||||
Fonts\MORPHEUS.ttf — декоративный (заголовки)
|
|
||||||
Fonts\ARIALN.TTF — Arial Narrow
|
|
||||||
Fonts\skurri.ttf — шрифт повреждений
|
|
||||||
```
|
|
||||||
|
|
||||||
> **Кириллица**: встроенные шрифты WoW не поддерживают кириллицу. Положи в аддон `.ttf` с кириллицей (например, PT Sans, Roboto) и подключи явно.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Текстуры: подготовка под ограничения движка
|
|
||||||
|
|
||||||
#### Требования к текстурам WoW 3.3.5a
|
|
||||||
- Ширина и высота — **степени двойки**: 2, 4, 8, 16, 32, 64, 128, 256, 512
|
|
||||||
- Максимум: **512×512 px**
|
|
||||||
- Формат: `.tga` (RGBA, 32-bit) или `.blp`
|
|
||||||
- Текстура не обязана быть квадратной: 512×64 — ок, 256×128 — ок
|
|
||||||
|
|
||||||
#### Как уместить большой элемент дизайна
|
|
||||||
|
|
||||||
Если элемент из Figma больше 512px — разрезай на части:
|
|
||||||
|
|
||||||
```
|
|
||||||
[512×256] [512×256] ← верхняя половина (левая + правая)
|
|
||||||
[512×256] [512×256] ← нижняя половина
|
|
||||||
```
|
|
||||||
|
|
||||||
```lua
|
|
||||||
-- Сборка из 4 текстур
|
|
||||||
local tl = frame:CreateTexture(nil, "BACKGROUND")
|
|
||||||
tl:SetTexture("Interface\\AddOns\\MyAddon\\bg_tl")
|
|
||||||
tl:SetPoint("TOPLEFT", frame, "TOPLEFT")
|
|
||||||
tl:SetWidth(256); tl:SetHeight(128)
|
|
||||||
|
|
||||||
local tr = frame:CreateTexture(nil, "BACKGROUND")
|
|
||||||
tr:SetTexture("Interface\\AddOns\\MyAddon\\bg_tr")
|
|
||||||
tr:SetPoint("TOPRIGHT", frame, "TOPRIGHT")
|
|
||||||
tr:SetWidth(256); tr:SetHeight(128)
|
|
||||||
-- ... и так далее
|
|
||||||
```
|
|
||||||
|
|
||||||
#### TexCoord — показать только часть текстуры
|
|
||||||
|
|
||||||
Если в одном файле несколько элементов (спрайт-лист), используй `SetTexCoord`:
|
|
||||||
|
|
||||||
```lua
|
|
||||||
-- SetTexCoord(left, right, top, bottom) — от 0.0 до 1.0
|
|
||||||
-- Пример: иконка занимает правый нижний квадрант 256×256 атласа
|
|
||||||
tex:SetTexCoord(0.5, 1.0, 0.5, 1.0)
|
|
||||||
|
|
||||||
-- Формула для ячейки в сетке (col/row с нуля):
|
|
||||||
local cols, rows = 4, 4
|
|
||||||
local col, row = 2, 1 -- третья колонка, вторая строка
|
|
||||||
tex:SetTexCoord(
|
|
||||||
col/cols, -- left
|
|
||||||
(col+1)/cols, -- right
|
|
||||||
row/rows, -- top
|
|
||||||
(row+1)/rows -- bottom
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
> **Важно**: при SetTexCoord текстура тайлится внутри заданного Size фрейма. SetAllPoints растягивает — SetWidth/SetHeight + SetPoint = точный размер.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Позиционирование: от Figma к SetPoint
|
|
||||||
|
|
||||||
В Figma позиция элемента — это X,Y от левого верхнего угла холста (или родителя). В WoW — якорная система.
|
|
||||||
|
|
||||||
**Алгоритм переноса**:
|
|
||||||
|
|
||||||
1. Определи ближайший угол/сторону в Figma (от чего считать)
|
|
||||||
2. Вычисли отступ от этой стороны
|
|
||||||
3. Пересчитай через `px()`
|
|
||||||
|
|
||||||
```lua
|
|
||||||
-- Figma (холст 1365×768):
|
|
||||||
-- Элемент: X=50, Y=30, W=200, H=100
|
|
||||||
-- → привязка от TOPLEFT UIParent
|
|
||||||
|
|
||||||
frame:SetWidth(px(200))
|
|
||||||
frame:SetHeight(px(100))
|
|
||||||
frame:SetPoint("TOPLEFT", UIParent, "TOPLEFT", px(50), -px(30))
|
|
||||||
-- ^^^ ^^^
|
|
||||||
-- X offset Y offset (отрицательный! вниз)
|
|
||||||
```
|
|
||||||
|
|
||||||
> **Знаки осей WoW**: X растёт вправо (+), Y растёт вверх (+). Отступ вниз от TOPLEFT — **отрицательный**.
|
|
||||||
|
|
||||||
| Figma anchor | WoW SetPoint |
|
|
||||||
|-------------|-------------|
|
|
||||||
| Top-Left | `"TOPLEFT", UIParent, "TOPLEFT", x, -y` |
|
|
||||||
| Top-Right | `"TOPRIGHT", UIParent, "TOPRIGHT", -x, -y` |
|
|
||||||
| Bottom-Left | `"BOTTOMLEFT", UIParent, "BOTTOMLEFT", x, y` |
|
|
||||||
| Bottom-Right | `"BOTTOMRIGHT", UIParent, "BOTTOMRIGHT", -x, y` |
|
|
||||||
| Center | `"CENTER", UIParent, "CENTER", x - W/2_canvas, y` |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Вспомогательный модуль: px-утилиты (вставь в начало Lua)
|
|
||||||
|
|
||||||
```lua
|
|
||||||
-- PixelHelper — pixel-perfect вёрстка из Figma
|
|
||||||
local PH = {}
|
|
||||||
|
|
||||||
PH.CANVAS_HEIGHT = 768 -- высота холста в Figma (измени если другая!)
|
|
||||||
|
|
||||||
function PH.scale()
|
|
||||||
return 768 / PH.CANVAS_HEIGHT
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Пересчёт px из Figma → WoW единицы
|
|
||||||
function PH.px(v)
|
|
||||||
return math.floor(v * PH.scale() + 0.5)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Цвет из HEX (#RRGGBB или #RRGGBBAA)
|
|
||||||
function PH.color(hex, alpha)
|
|
||||||
hex = hex:gsub("#","")
|
|
||||||
local r = tonumber(hex:sub(1,2),16)/255
|
|
||||||
local g = tonumber(hex:sub(3,4),16)/255
|
|
||||||
local b = tonumber(hex:sub(5,6),16)/255
|
|
||||||
local a = alpha or (hex:len() == 8 and tonumber(hex:sub(7,8),16)/255) or 1
|
|
||||||
return r, g, b, a
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Размер шрифта из Figma → WoW
|
|
||||||
function PH.font(pt)
|
|
||||||
return math.max(8, math.floor(pt * PH.scale() + 0.5))
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Установка якоря с автопересчётом
|
|
||||||
function PH.point(frame, point, relativeTo, relPoint, x, y)
|
|
||||||
frame:SetPoint(point, relativeTo, relPoint, PH.px(x or 0), PH.px(y or 0))
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Размер фрейма из Figma
|
|
||||||
function PH.size(frame, w, h)
|
|
||||||
frame:SetWidth(PH.px(w))
|
|
||||||
frame:SetHeight(PH.px(h))
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Экспорт глобально (или оставь локальным)
|
|
||||||
MyAddon_PH = PH
|
|
||||||
```
|
|
||||||
|
|
||||||
**Использование**:
|
|
||||||
```lua
|
|
||||||
local px = MyAddon_PH.px
|
|
||||||
local color = MyAddon_PH.color
|
|
||||||
local pxfont = MyAddon_PH.font
|
|
||||||
|
|
||||||
MyAddon_PH.size(frame, 400, 250)
|
|
||||||
MyAddon_PH.point(frame, "TOPLEFT", UIParent, "TOPLEFT", 50, -30)
|
|
||||||
frame:SetBackdropColor(color("#1A1A2E", 0.95))
|
|
||||||
fs:SetTextColor(color("#FFD100"))
|
|
||||||
fs:SetFont("Interface\\AddOns\\MyAddon\\fonts\\MyFont.ttf", pxfont(16), "OUTLINE")
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Чеклист pixel-perfect переноса
|
|
||||||
|
|
||||||
- [ ] Холст Figma = 768px в высоту (или задан `PH.CANVAS_HEIGHT`)
|
|
||||||
- [ ] Все размеры и отступы пересчитаны через `px()`
|
|
||||||
- [ ] Цвета конвертированы через `color()`
|
|
||||||
- [ ] Текстуры нарезаны до 512×512, размеры кратны степени двойки
|
|
||||||
- [ ] Шрифты `.ttf` подключены из папки аддона (если кириллица или кастомный шрифт)
|
|
||||||
- [ ] Размеры шрифтов пересчитаны через `font()`
|
|
||||||
- [ ] Y-отступы от верхних якорей — **отрицательные**
|
|
||||||
- [ ] Проверено на scale UIParent ≠ 1.0 (изменить в настройках игры для теста)
|
|
||||||
- [ ] Текстуры без SetAllPoints — точный SetWidth/SetHeight + SetPoint
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
```
|
|
||||||
MyAddon/
|
|
||||||
├── MyAddon.toc ← обязателен, имя файла = имя папки
|
|
||||||
├── MyAddon.xml ← верстка фреймов (опционально)
|
|
||||||
└── MyAddon.lua ← логика
|
|
||||||
```
|
|
||||||
|
|
||||||
### TOC-файл (3.3.5a)
|
|
||||||
|
|
||||||
```toc
|
|
||||||
## Interface: 30300
|
|
||||||
## Title: My Addon
|
|
||||||
## Notes: Описание аддона
|
|
||||||
## Author: Ilyas
|
|
||||||
## Version: 1.0
|
|
||||||
## SavedVariables: MyAddonDB
|
|
||||||
|
|
||||||
MyAddon.xml
|
|
||||||
MyAddon.lua
|
|
||||||
```
|
|
||||||
|
|
||||||
> Файлы загружаются **в порядке перечисления**. XML раньше Lua — фреймы существуют к моменту выполнения скриптов.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## XML-верстка
|
|
||||||
|
|
||||||
### Минимальный шаблон XML-файла
|
|
||||||
|
|
||||||
```xml
|
|
||||||
<Ui xmlns="http://www.blizzard.com/wow/ui/"
|
|
||||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
|
||||||
xsi:schemaLocation="http://www.blizzard.com/wow/ui/ ..\FrameXML\UI.xsd">
|
|
||||||
|
|
||||||
<Script file="MyAddon.lua"/>
|
|
||||||
|
|
||||||
<Frame name="MyAddon_Frame" parent="UIParent" toplevel="true" enableMouse="true" movable="true">
|
|
||||||
<Size>
|
|
||||||
<AbsDimension x="300" y="200"/>
|
|
||||||
</Size>
|
|
||||||
<Anchors>
|
|
||||||
<Anchor point="CENTER"/>
|
|
||||||
</Anchors>
|
|
||||||
<Backdrop bgFile="Interface\DialogFrame\UI-DialogBox-Background"
|
|
||||||
edgeFile="Interface\DialogFrame\UI-DialogBox-Border"
|
|
||||||
tile="true">
|
|
||||||
<BackgroundInsets>
|
|
||||||
<AbsInset left="11" right="12" top="12" bottom="11"/>
|
|
||||||
</BackgroundInsets>
|
|
||||||
<TileSize><AbsValue val="32"/></TileSize>
|
|
||||||
<EdgeSize><AbsValue val="32"/></EdgeSize>
|
|
||||||
</Backdrop>
|
|
||||||
<Layers>
|
|
||||||
<Layer level="ARTWORK">
|
|
||||||
<FontString name="$parentTitle" inherits="GameFontNormalLarge" text="Заголовок">
|
|
||||||
<Anchors>
|
|
||||||
<Anchor point="TOP"><Offset><AbsDimension x="0" y="-16"/></Offset></Anchor>
|
|
||||||
</Anchors>
|
|
||||||
</FontString>
|
|
||||||
</Layer>
|
|
||||||
</Layers>
|
|
||||||
<Frames>
|
|
||||||
<Button name="$parentClose" inherits="UIPanelCloseButton">
|
|
||||||
<Anchors>
|
|
||||||
<Anchor point="TOPRIGHT"><Offset><AbsDimension x="4" y="4"/></Offset></Anchor>
|
|
||||||
</Anchors>
|
|
||||||
</Button>
|
|
||||||
</Frames>
|
|
||||||
<Scripts>
|
|
||||||
<OnLoad>MyAddon_Frame_OnLoad(self)</OnLoad>
|
|
||||||
<OnMouseDown>self:StartMoving()</OnMouseDown>
|
|
||||||
<OnMouseUp>self:StopMovingOrSizing()</OnMouseUp>
|
|
||||||
</Scripts>
|
|
||||||
</Frame>
|
|
||||||
|
|
||||||
</Ui>
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Точки якоря (FRAMEPOINT)
|
|
||||||
|
|
||||||
| Точка | Расположение |
|
|
||||||
|-------|-------------|
|
|
||||||
| `TOPLEFT` | Верхний левый угол |
|
|
||||||
| `TOP` | Верхний центр |
|
|
||||||
| `TOPRIGHT` | Верхний правый угол |
|
|
||||||
| `LEFT` | Левый центр |
|
|
||||||
| `CENTER` | Центр |
|
|
||||||
| `RIGHT` | Правый центр |
|
|
||||||
| `BOTTOMLEFT` | Нижний левый угол |
|
|
||||||
| `BOTTOM` | Нижний центр |
|
|
||||||
| `BOTTOMRIGHT` | Нижний правый угол |
|
|
||||||
|
|
||||||
### SetPoint через Lua
|
|
||||||
|
|
||||||
```lua
|
|
||||||
-- frame:SetPoint(point, relativeTo, relativePoint, offsetX, offsetY)
|
|
||||||
frame:SetPoint("TOPLEFT", UIParent, "TOPLEFT", 10, -10)
|
|
||||||
frame:SetPoint("CENTER", UIParent, "CENTER", 0, 0)
|
|
||||||
frame:SetPoint("BOTTOMRIGHT", anotherFrame, "TOPRIGHT", 0, 5)
|
|
||||||
|
|
||||||
-- Сброс якорей перед переустановкой
|
|
||||||
frame:ClearAllPoints()
|
|
||||||
frame:SetPoint("CENTER", UIParent, "CENTER")
|
|
||||||
```
|
|
||||||
|
|
||||||
> **Правило**: фрейм должен иметь достаточно якорей для однозначного определения позиции **и** размера. Либо Size + один якорь, либо два якоря задающих оба измерения.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Слои и страты
|
|
||||||
|
|
||||||
### Draw Layers (внутри фрейма, сверху вниз по видимости)
|
|
||||||
|
|
||||||
```
|
|
||||||
BACKGROUND (0) → BORDER (1) → ARTWORK (2) → OVERLAY (3) → HIGHLIGHT (4)
|
|
||||||
```
|
|
||||||
|
|
||||||
- `HIGHLIGHT` — автоматически показывается при наведении мыши (требует `enableMouse="true"`)
|
|
||||||
- FontString всегда рисуется **поверх** Texture в том же слое
|
|
||||||
- Порядок текстур в одном слое **не гарантирован** → разноси по слоям
|
|
||||||
|
|
||||||
### Frame Strata (между фреймами)
|
|
||||||
|
|
||||||
```
|
|
||||||
WORLD → BACKGROUND → LOW → MEDIUM → HIGH → DIALOG → FULLSCREEN → FULLSCREEN_DIALOG → TOOLTIP
|
|
||||||
```
|
|
||||||
|
|
||||||
```lua
|
|
||||||
frame:SetFrameStrata("DIALOG")
|
|
||||||
frame:SetFrameLevel(5) -- внутри страты (0–255)
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Виджеты, события, Blizzard API
|
|
||||||
|
|
||||||
> Полный справочник — в `references/widgets-and-api.md`.
|
|
||||||
> Читай его когда нужны: типы фреймов, Button/EditBox/ScrollFrame/StatusBar, события OnEvent/OnUpdate, SavedVariables, пути к текстурам, типичные ошибки.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Шаблонный аддон «с нуля» (минимальный рабочий пример)
|
|
||||||
|
|
||||||
**MyAddon.toc**
|
|
||||||
```toc
|
|
||||||
## Interface: 30300
|
|
||||||
## Title: MyAddon
|
|
||||||
## SavedVariables: MyAddonDB
|
|
||||||
MyAddon.lua
|
|
||||||
```
|
|
||||||
|
|
||||||
**MyAddon.lua**
|
|
||||||
```lua
|
|
||||||
local ADDON_NAME = "MyAddon"
|
|
||||||
|
|
||||||
local f = CreateFrame("Frame", ADDON_NAME.."_Frame", UIParent)
|
|
||||||
f:SetWidth(250)
|
|
||||||
f:SetHeight(150)
|
|
||||||
f:SetPoint("CENTER")
|
|
||||||
f:SetFrameStrata("DIALOG")
|
|
||||||
f:SetMovable(true)
|
|
||||||
f:EnableMouse(true)
|
|
||||||
f:SetBackdrop({
|
|
||||||
bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background",
|
|
||||||
edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Border",
|
|
||||||
tile = true, tileSize = 32, edgeSize = 32,
|
|
||||||
insets = {left=11, right=12, top=12, bottom=11}
|
|
||||||
})
|
|
||||||
f:Hide()
|
|
||||||
|
|
||||||
-- Заголовок
|
|
||||||
local title = f:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
|
|
||||||
title:SetPoint("TOP", f, "TOP", 0, -12)
|
|
||||||
title:SetText("My Addon")
|
|
||||||
|
|
||||||
-- Закрытие
|
|
||||||
local close = CreateFrame("Button", nil, f, "UIPanelCloseButton")
|
|
||||||
close:SetPoint("TOPRIGHT", f, "TOPRIGHT", 4, 4)
|
|
||||||
close:SetScript("OnClick", function() f:Hide() end)
|
|
||||||
|
|
||||||
-- Перетаскивание
|
|
||||||
f:SetScript("OnMouseDown", function(self, btn)
|
|
||||||
if btn == "LeftButton" then self:StartMoving() end
|
|
||||||
end)
|
|
||||||
f:SetScript("OnMouseUp", function(self) self:StopMovingOrSizing() end)
|
|
||||||
|
|
||||||
-- Инициализация
|
|
||||||
f:RegisterEvent("ADDON_LOADED")
|
|
||||||
f:SetScript("OnEvent", function(self, event, name)
|
|
||||||
if event == "ADDON_LOADED" and name == ADDON_NAME then
|
|
||||||
MyAddonDB = MyAddonDB or {}
|
|
||||||
print("|cff00ff00"..ADDON_NAME.."|r загружен. /myaddon")
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
|
|
||||||
-- Слэш-команда
|
|
||||||
SLASH_MYADDON1 = "/myaddon"
|
|
||||||
SlashCmdList["MYADDON"] = function()
|
|
||||||
if f:IsShown() then f:Hide() else f:Show() end
|
|
||||||
end
|
|
||||||
```
|
|
||||||
@@ -1,256 +0,0 @@
|
|||||||
# WoW 3.3.5a — Виджеты, события и Blizzard API
|
|
||||||
|
|
||||||
## Создание фреймов через Lua
|
|
||||||
|
|
||||||
### CreateFrame
|
|
||||||
|
|
||||||
```lua
|
|
||||||
-- CreateFrame(frameType, name, parent, template)
|
|
||||||
local f = CreateFrame("Frame", "MyAddon_MainFrame", UIParent)
|
|
||||||
f:SetWidth(300)
|
|
||||||
f:SetHeight(200)
|
|
||||||
f:SetPoint("CENTER", UIParent, "CENTER", 0, 0)
|
|
||||||
f:SetFrameStrata("DIALOG")
|
|
||||||
f:SetBackdrop({
|
|
||||||
bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background",
|
|
||||||
edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Border",
|
|
||||||
tile = true, tileSize = 32, edgeSize = 32,
|
|
||||||
insets = { left = 11, right = 12, top = 12, bottom = 11 }
|
|
||||||
})
|
|
||||||
f:SetBackdropColor(0, 0, 0, 0.8)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Типы фреймов (frameType)
|
|
||||||
|
|
||||||
| Тип | Назначение |
|
|
||||||
|-----|-----------|
|
|
||||||
| `Frame` | Базовый контейнер |
|
|
||||||
| `Button` | Кнопка |
|
|
||||||
| `EditBox` | Поле ввода текста |
|
|
||||||
| `ScrollFrame` | Прокручиваемая область |
|
|
||||||
| `StatusBar` | Полоска (HP, мана, XP) |
|
|
||||||
| `Cooldown` | Кулдаун-свип на иконке |
|
|
||||||
| `GameTooltip` | Тултип |
|
|
||||||
| `MessageFrame` | Прокручиваемые сообщения |
|
|
||||||
| `Model` | 3D-модель |
|
|
||||||
| `MovieFrame` | Видео |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Текстуры
|
|
||||||
|
|
||||||
```lua
|
|
||||||
local tex = frame:CreateTexture(nil, "ARTWORK")
|
|
||||||
tex:SetTexture("Interface\\AddOns\\MyAddon\\images\\icon")
|
|
||||||
tex:SetAllPoints(frame)
|
|
||||||
tex:SetWidth(32)
|
|
||||||
tex:SetHeight(32)
|
|
||||||
tex:SetPoint("CENTER", frame, "CENTER")
|
|
||||||
tex:SetVertexColor(1, 0.5, 0, 1)
|
|
||||||
tex:SetAlpha(0.8)
|
|
||||||
|
|
||||||
-- Вырезка части текстуры
|
|
||||||
tex:SetTexCoord(0, 0.5, 0, 0.5)
|
|
||||||
```
|
|
||||||
|
|
||||||
## FontString (текст)
|
|
||||||
|
|
||||||
```lua
|
|
||||||
local fs = frame:CreateFontString(nil, "OVERLAY", "GameFontNormal")
|
|
||||||
fs:SetPoint("CENTER", frame, "CENTER")
|
|
||||||
fs:SetText("Привет, Азерот!")
|
|
||||||
fs:SetTextColor(1, 1, 0, 1)
|
|
||||||
fs:SetFont("Fonts\\FRIZQT__.TTF", 14, "OUTLINE")
|
|
||||||
```
|
|
||||||
|
|
||||||
### Стандартные шаблоны шрифтов
|
|
||||||
|
|
||||||
| Шаблон | Применение |
|
|
||||||
|--------|-----------|
|
|
||||||
| `GameFontNormal` | Обычный текст интерфейса |
|
|
||||||
| `GameFontNormalLarge` | Заголовки |
|
|
||||||
| `GameFontNormalSmall` | Мелкий текст |
|
|
||||||
| `GameFontHighlight` | Подсвеченный |
|
|
||||||
| `GameFontDisable` | Неактивный (серый) |
|
|
||||||
| `NumberFontNormal` | Числа |
|
|
||||||
| `ChatFontNormal` | Чат |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Button
|
|
||||||
|
|
||||||
```lua
|
|
||||||
local btn = CreateFrame("Button", "MyAddon_Btn", frame, "UIPanelButtonTemplate")
|
|
||||||
btn:SetWidth(80)
|
|
||||||
btn:SetHeight(22)
|
|
||||||
btn:SetPoint("BOTTOM", frame, "BOTTOM", 0, 10)
|
|
||||||
btn:SetText("Нажми")
|
|
||||||
btn:SetScript("OnClick", function(self)
|
|
||||||
print("Нажато!")
|
|
||||||
end)
|
|
||||||
|
|
||||||
btn:SetNormalTexture("Interface\\Buttons\\UI-Panel-Button-Up")
|
|
||||||
btn:SetPushedTexture("Interface\\Buttons\\UI-Panel-Button-Down")
|
|
||||||
btn:SetHighlightTexture("Interface\\Buttons\\UI-Panel-Button-Highlight")
|
|
||||||
```
|
|
||||||
|
|
||||||
### Шаблоны кнопок
|
|
||||||
|
|
||||||
| Шаблон | Вид |
|
|
||||||
|--------|-----|
|
|
||||||
| `UIPanelButtonTemplate` | Стандартная кнопка интерфейса |
|
|
||||||
| `UIPanelCloseButton` | Крестик закрытия окна |
|
|
||||||
| `UIMenuButtonStretchTemplate` | Растягиваемая кнопка меню |
|
|
||||||
| `SecureActionButtonTemplate` | Для действий в бою (protected) |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ScrollFrame
|
|
||||||
|
|
||||||
```lua
|
|
||||||
local scroll = CreateFrame("ScrollFrame", "MyAddon_Scroll", frame, "UIPanelScrollFrameTemplate")
|
|
||||||
scroll:SetPoint("TOPLEFT", frame, "TOPLEFT", 8, -30)
|
|
||||||
scroll:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", -26, 8)
|
|
||||||
|
|
||||||
local child = CreateFrame("Frame", "MyAddon_ScrollChild", scroll)
|
|
||||||
child:SetWidth(scroll:GetWidth())
|
|
||||||
child:SetHeight(400)
|
|
||||||
scroll:SetScrollChild(child)
|
|
||||||
```
|
|
||||||
|
|
||||||
## EditBox
|
|
||||||
|
|
||||||
```lua
|
|
||||||
local eb = CreateFrame("EditBox", "MyAddon_Input", frame, "InputBoxTemplate")
|
|
||||||
eb:SetWidth(200)
|
|
||||||
eb:SetHeight(20)
|
|
||||||
eb:SetPoint("CENTER", frame, "CENTER")
|
|
||||||
eb:SetAutoFocus(false)
|
|
||||||
eb:SetMaxLetters(255)
|
|
||||||
eb:SetScript("OnEnterPressed", function(self)
|
|
||||||
local text = self:GetText()
|
|
||||||
self:ClearFocus()
|
|
||||||
end)
|
|
||||||
```
|
|
||||||
|
|
||||||
## StatusBar
|
|
||||||
|
|
||||||
```lua
|
|
||||||
local bar = CreateFrame("StatusBar", nil, frame)
|
|
||||||
bar:SetWidth(200)
|
|
||||||
bar:SetHeight(16)
|
|
||||||
bar:SetPoint("CENTER", frame, "CENTER")
|
|
||||||
bar:SetStatusBarTexture("Interface\\TargetingFrame\\UI-StatusBar")
|
|
||||||
bar:SetStatusBarColor(0, 1, 0, 1)
|
|
||||||
bar:SetMinMaxValues(0, 100)
|
|
||||||
bar:SetValue(75)
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Скрипты-события
|
|
||||||
|
|
||||||
```lua
|
|
||||||
frame:RegisterEvent("PLAYER_ENTERING_WORLD")
|
|
||||||
frame:RegisterEvent("CHAT_MSG_SAY")
|
|
||||||
|
|
||||||
frame:SetScript("OnEvent", function(self, event, ...)
|
|
||||||
if event == "PLAYER_ENTERING_WORLD" then
|
|
||||||
-- вход в мир
|
|
||||||
elseif event == "CHAT_MSG_SAY" then
|
|
||||||
local msg, author = ...
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Ключевые события
|
|
||||||
|
|
||||||
| Событие | Когда |
|
|
||||||
|---------|-------|
|
|
||||||
| `PLAYER_ENTERING_WORLD` | Вход в мир / reload |
|
|
||||||
| `ADDON_LOADED` | Аддон загружен (arg1 == имя) |
|
|
||||||
| `PLAYER_LOGIN` | После входа |
|
|
||||||
| `PLAYER_LOGOUT` | При выходе |
|
|
||||||
| `VARIABLES_LOADED` | SavedVariables загружены |
|
|
||||||
| `UI_ERROR_MESSAGE` | Ошибка интерфейса |
|
|
||||||
|
|
||||||
### OnUpdate — таймер
|
|
||||||
|
|
||||||
```lua
|
|
||||||
local elapsed_total = 0
|
|
||||||
frame:SetScript("OnUpdate", function(self, elapsed)
|
|
||||||
elapsed_total = elapsed_total + elapsed
|
|
||||||
if elapsed_total >= 1.0 then
|
|
||||||
elapsed_total = 0
|
|
||||||
-- действие каждую секунду
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## SavedVariables
|
|
||||||
|
|
||||||
```lua
|
|
||||||
-- В TOC: ## SavedVariables: MyAddonDB
|
|
||||||
frame:RegisterEvent("ADDON_LOADED")
|
|
||||||
frame:SetScript("OnEvent", function(self, event, addonName)
|
|
||||||
if event == "ADDON_LOADED" and addonName == "MyAddon" then
|
|
||||||
MyAddonDB = MyAddonDB or { position = { "CENTER", 0, 0 } }
|
|
||||||
local p = MyAddonDB.position
|
|
||||||
self:ClearAllPoints()
|
|
||||||
self:SetPoint(p[1], UIParent, p[1], p[2], p[3])
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
|
|
||||||
frame:SetScript("OnMouseUp", function(self)
|
|
||||||
self:StopMovingOrSizing()
|
|
||||||
local point, _, _, x, y = self:GetPoint()
|
|
||||||
MyAddonDB.position = { point, x, y }
|
|
||||||
end)
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Полезные Blizzard-утилиты (3.3.5a)
|
|
||||||
|
|
||||||
```lua
|
|
||||||
_G["MyAddon_Frame"]:Show()
|
|
||||||
IsAddOnLoaded("MyAddon")
|
|
||||||
UnitName("player")
|
|
||||||
UnitClass("target")
|
|
||||||
UnitHealth("player"), UnitHealthMax("player")
|
|
||||||
DEFAULT_CHAT_FRAME:AddMessage("|cffff0000Красный текст|r")
|
|
||||||
print("Белый текст")
|
|
||||||
UnitAffectingCombat("player")
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Часто используемые пути к текстурам Blizzard
|
|
||||||
|
|
||||||
```
|
|
||||||
Interface\DialogFrame\UI-DialogBox-Background
|
|
||||||
Interface\DialogFrame\UI-DialogBox-Border
|
|
||||||
Interface\DialogFrame\UI-DialogBox-Gold-Background
|
|
||||||
Interface\Tooltips\UI-Tooltip-Background
|
|
||||||
Interface\Tooltips\UI-Tooltip-Border
|
|
||||||
Interface\Buttons\UI-Panel-Button-Up
|
|
||||||
Interface\Buttons\UI-Panel-Button-Down
|
|
||||||
Interface\Icons\INV_Misc_QuestionMark
|
|
||||||
Interface\TargetingFrame\UI-StatusBar
|
|
||||||
Interface\ChatFrame\UI-ChatIcon-ScrollDown-Up
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Типичные ошибки и решения
|
|
||||||
|
|
||||||
| Ошибка | Причина | Решение |
|
|
||||||
|--------|---------|---------|
|
|
||||||
| `attempt to index global 'X' (nil)` | Фрейм не создан | Проверь порядок файлов в TOC |
|
|
||||||
| `SetPoint anchor family connection` | Циклические якоря | Переписать цепочку якорей |
|
|
||||||
| `Script ran too long` | Бесконечный цикл OnUpdate | Early return или счётчик |
|
|
||||||
| Текстура не отображается | Размер не кратен 2 / >512px | Пересохрани в TGA/BLP |
|
|
||||||
| `bad argument #1 (string expected)` | nil вместо строки | `if val then` |
|
|
||||||
| Frame не кликается | Нет `enableMouse="true"` | Добавить атрибут |
|
|
||||||
@@ -47,7 +47,7 @@ if (!(Test-Path $TOOL)) {
|
|||||||
|
|
||||||
# --- Build MPQ archives into dist/
|
# --- Build MPQ archives into dist/
|
||||||
Write-Host "Building MPQ archives from src/ -> dist/..."
|
Write-Host "Building MPQ archives from src/ -> dist/..."
|
||||||
& $TOOL $SRC_DIR $DIST_DIR
|
& $TOOL build-mpq $SRC_DIR $DIST_DIR
|
||||||
if ($LASTEXITCODE -ne 0) {
|
if ($LASTEXITCODE -ne 0) {
|
||||||
Write-Error "MPQ build failed!"
|
Write-Error "MPQ build failed!"
|
||||||
exit 1
|
exit 1
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
+3
-1
@@ -1 +1,3 @@
|
|||||||
target/
|
target/
|
||||||
|
.sqlx/
|
||||||
|
.env
|
||||||
Generated
+171
-8
@@ -8,6 +8,15 @@ version = "2.0.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
|
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "aho-corasick"
|
||||||
|
version = "1.1.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "allocator-api2"
|
name = "allocator-api2"
|
||||||
version = "0.2.21"
|
version = "0.2.21"
|
||||||
@@ -132,9 +141,9 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap"
|
name = "clap"
|
||||||
version = "4.6.0"
|
version = "4.6.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351"
|
checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"clap_builder",
|
"clap_builder",
|
||||||
"clap_derive",
|
"clap_derive",
|
||||||
@@ -154,9 +163,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap_derive"
|
name = "clap_derive"
|
||||||
version = "4.6.0"
|
version = "4.6.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a"
|
checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"heck",
|
"heck",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
@@ -297,6 +306,29 @@ version = "1.0.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0"
|
checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "env_filter"
|
||||||
|
version = "1.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "32e90c2accc4b07a8456ea0debdc2e7587bdd890680d71173a15d4ae604f6eef"
|
||||||
|
dependencies = [
|
||||||
|
"log",
|
||||||
|
"regex",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "env_logger"
|
||||||
|
version = "0.11.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0621c04f2196ac3f488dd583365b9c09be011a4ab8b9f37248ffcc8f6198b56a"
|
||||||
|
dependencies = [
|
||||||
|
"anstream",
|
||||||
|
"anstyle",
|
||||||
|
"env_filter",
|
||||||
|
"jiff",
|
||||||
|
"log",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "equivalent"
|
name = "equivalent"
|
||||||
version = "1.0.2"
|
version = "1.0.2"
|
||||||
@@ -459,6 +491,30 @@ version = "1.0.17"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
|
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jiff"
|
||||||
|
version = "0.2.27"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "392c70591e8749fe235ddaf513e6f58b26bce3dcc16524cecc8936f75afa161e"
|
||||||
|
dependencies = [
|
||||||
|
"jiff-static",
|
||||||
|
"log",
|
||||||
|
"portable-atomic",
|
||||||
|
"portable-atomic-util",
|
||||||
|
"serde_core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jiff-static"
|
||||||
|
version = "0.2.27"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "47b605b0c050d845fc355bb11eb3f9a8deddc218ea60c76e61aa1f2adfb2c96a"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "js-sys"
|
name = "js-sys"
|
||||||
version = "0.3.91"
|
version = "0.3.91"
|
||||||
@@ -519,9 +575,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "log"
|
name = "log"
|
||||||
version = "0.4.29"
|
version = "0.4.30"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
|
checksum = "616ec5685824bcc94416c6d4a7a446eea774a31efd7062c8480ba6fd06d7a6e5"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lru"
|
name = "lru"
|
||||||
@@ -568,6 +624,17 @@ dependencies = [
|
|||||||
"simd-adler32",
|
"simd-adler32",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mio"
|
||||||
|
version = "1.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"wasi",
|
||||||
|
"windows-sys 0.61.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-bigint"
|
name = "num-bigint"
|
||||||
version = "0.4.6"
|
version = "0.4.6"
|
||||||
@@ -674,6 +741,12 @@ dependencies = [
|
|||||||
"base64ct",
|
"base64ct",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pin-project-lite"
|
||||||
|
version = "0.2.17"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pkcs1"
|
name = "pkcs1"
|
||||||
version = "0.7.5"
|
version = "0.7.5"
|
||||||
@@ -712,6 +785,15 @@ version = "1.13.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49"
|
checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "portable-atomic-util"
|
||||||
|
version = "0.2.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618"
|
||||||
|
dependencies = [
|
||||||
|
"portable-atomic",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ppv-lite86"
|
name = "ppv-lite86"
|
||||||
version = "0.2.21"
|
version = "0.2.21"
|
||||||
@@ -848,6 +930,35 @@ dependencies = [
|
|||||||
"bitflags",
|
"bitflags",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex"
|
||||||
|
version = "1.12.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276"
|
||||||
|
dependencies = [
|
||||||
|
"aho-corasick",
|
||||||
|
"memchr",
|
||||||
|
"regex-automata",
|
||||||
|
"regex-syntax",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex-automata"
|
||||||
|
version = "0.4.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
|
||||||
|
dependencies = [
|
||||||
|
"aho-corasick",
|
||||||
|
"memchr",
|
||||||
|
"regex-syntax",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex-syntax"
|
||||||
|
version = "0.8.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rsa"
|
name = "rsa"
|
||||||
version = "0.9.10"
|
version = "0.9.10"
|
||||||
@@ -961,6 +1072,16 @@ dependencies = [
|
|||||||
"digest",
|
"digest",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "signal-hook-registry"
|
||||||
|
version = "1.4.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
|
||||||
|
dependencies = [
|
||||||
|
"errno",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "signature"
|
name = "signature"
|
||||||
version = "2.2.0"
|
version = "2.2.0"
|
||||||
@@ -983,6 +1104,16 @@ version = "1.15.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
|
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "socket2"
|
||||||
|
version = "0.6.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"windows-sys 0.61.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "spin"
|
name = "spin"
|
||||||
version = "0.9.8"
|
version = "0.9.8"
|
||||||
@@ -1055,19 +1186,51 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokio"
|
||||||
|
version = "1.52.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"libc",
|
||||||
|
"mio",
|
||||||
|
"parking_lot",
|
||||||
|
"pin-project-lite",
|
||||||
|
"signal-hook-registry",
|
||||||
|
"socket2",
|
||||||
|
"tokio-macros",
|
||||||
|
"windows-sys 0.61.2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokio-macros"
|
||||||
|
version = "2.7.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tool"
|
name = "tool"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"clap",
|
||||||
|
"env_logger",
|
||||||
|
"log",
|
||||||
|
"tokio",
|
||||||
"walkdir",
|
"walkdir",
|
||||||
"wow-mpq",
|
"wow-mpq",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typenum"
|
name = "typenum"
|
||||||
version = "1.19.0"
|
version = "1.20.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
|
checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-ident"
|
name = "unicode-ident"
|
||||||
|
|||||||
@@ -4,5 +4,9 @@ version = "0.1.0"
|
|||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
clap = { version = "4.6.1", features = ["derive"] }
|
||||||
|
env_logger = "0.11.10"
|
||||||
|
log = "0.4.30"
|
||||||
|
tokio = { version = "1.52.3", features = ["full"] }
|
||||||
walkdir = "2.5.0"
|
walkdir = "2.5.0"
|
||||||
wow-mpq = "0.6.4"
|
wow-mpq = "0.6.4"
|
||||||
|
|||||||
@@ -0,0 +1,100 @@
|
|||||||
|
use clap::Args;
|
||||||
|
use log::info;
|
||||||
|
use std::{
|
||||||
|
error::Error,
|
||||||
|
fs,
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
};
|
||||||
|
use walkdir::WalkDir;
|
||||||
|
use wow_mpq::{ArchiveBuilder, FormatVersion, ListfileOption, compression::flags};
|
||||||
|
|
||||||
|
#[derive(Args, Debug)]
|
||||||
|
pub struct BuildMpqCommand {
|
||||||
|
input_dir: PathBuf,
|
||||||
|
|
||||||
|
destination_dir: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn execute(args: BuildMpqCommand) -> Result<(), Box<dyn Error>> {
|
||||||
|
let data_dir = args.input_dir.join("Data");
|
||||||
|
if !data_dir.exists() {
|
||||||
|
return Err(format!("Data directory not found at {}", data_dir.display()).into());
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("Found Data directory at {}", data_dir.display());
|
||||||
|
|
||||||
|
for entry in fs::read_dir(&data_dir)? {
|
||||||
|
let entry = entry?;
|
||||||
|
let path = entry.path();
|
||||||
|
|
||||||
|
if !path.is_dir() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let dir_name = entry.file_name().to_string_lossy().to_string();
|
||||||
|
|
||||||
|
if dir_name.starts_with("ruRU") {
|
||||||
|
for sub_entry in fs::read_dir(&path)? {
|
||||||
|
let sub_entry = sub_entry?;
|
||||||
|
let sub_path = sub_entry.path();
|
||||||
|
|
||||||
|
let sub_name = sub_entry.file_name().to_string_lossy().to_string();
|
||||||
|
let destination = args
|
||||||
|
.destination_dir
|
||||||
|
.join("Data")
|
||||||
|
.join(&dir_name)
|
||||||
|
.join(&sub_name);
|
||||||
|
|
||||||
|
let _ = build_mpq(&sub_path, &destination);
|
||||||
|
}
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let destination = args.destination_dir.join("Data").join(&dir_name);
|
||||||
|
|
||||||
|
let _ = build_mpq(&path, &destination)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_mpq(input_dir: &Path, destination: &Path) -> Result<(), Box<dyn Error>> {
|
||||||
|
info!(
|
||||||
|
"Packing MPQ from {} to {}",
|
||||||
|
input_dir.display(),
|
||||||
|
destination.display()
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut destination = destination.to_path_buf();
|
||||||
|
destination.set_extension("MPQ");
|
||||||
|
|
||||||
|
if let Some(parent) = destination.parent() {
|
||||||
|
fs::create_dir_all(parent)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut builder = ArchiveBuilder::new()
|
||||||
|
.version(FormatVersion::V2)
|
||||||
|
.block_size(7)
|
||||||
|
.default_compression(flags::ZLIB)
|
||||||
|
.listfile_option(ListfileOption::Generate);
|
||||||
|
|
||||||
|
let base = input_dir.canonicalize()?;
|
||||||
|
|
||||||
|
for entry in WalkDir::new(&base)
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|e| e.ok())
|
||||||
|
.filter(|e| e.file_type().is_file())
|
||||||
|
{
|
||||||
|
let absolute_path = entry.path().canonicalize()?;
|
||||||
|
let relative_path = absolute_path.strip_prefix(&base)?;
|
||||||
|
|
||||||
|
builder = builder.add_file(&absolute_path, &relative_path.to_string_lossy().to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
let _ = builder.build(&destination);
|
||||||
|
|
||||||
|
info!("Successfully packed {}", destination.display());
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
pub mod build_mpq;
|
||||||
+31
-101
@@ -1,111 +1,41 @@
|
|||||||
use std::env;
|
mod commands;
|
||||||
|
|
||||||
|
use clap::{Parser, Subcommand};
|
||||||
|
use log::{error, info};
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
use std::fs;
|
|
||||||
use std::path::{Path, PathBuf};
|
|
||||||
|
|
||||||
use walkdir::WalkDir;
|
#[derive(Parser, Debug)]
|
||||||
use wow_mpq::compression::flags;
|
struct Cli {
|
||||||
use wow_mpq::{ArchiveBuilder, FormatVersion, ListfileOption};
|
#[command(subcommand)]
|
||||||
|
command: Commands,
|
||||||
/// Checks if a directory name looks like a WoW locale folder (e.g. ruRU, enUS, deDE).
|
|
||||||
fn is_locale_dir(name: &str) -> bool {
|
|
||||||
if name.len() != 4 {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
let b = name.as_bytes();
|
|
||||||
b[0].is_ascii_lowercase()
|
|
||||||
&& b[1].is_ascii_lowercase()
|
|
||||||
&& b[2].is_ascii_uppercase()
|
|
||||||
&& b[3].is_ascii_uppercase()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_mpq(input_dir: &Path, output_path: &Path) -> Result<(), Box<dyn Error>> {
|
#[derive(Subcommand, Debug)]
|
||||||
let mut output = output_path.to_path_buf();
|
enum Commands {
|
||||||
output.set_extension("MPQ");
|
BuildMpq(commands::build_mpq::BuildMpqCommand),
|
||||||
|
|
||||||
if let Some(parent) = output.parent() {
|
|
||||||
fs::create_dir_all(parent)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let base = input_dir.canonicalize()?;
|
|
||||||
println!("Packing: {} -> {}", base.display(), output.display());
|
|
||||||
|
|
||||||
let mut builder = ArchiveBuilder::new()
|
|
||||||
.version(FormatVersion::V2)
|
|
||||||
.block_size(7) // 64 KB sectors
|
|
||||||
.default_compression(flags::ZLIB)
|
|
||||||
.listfile_option(ListfileOption::Generate);
|
|
||||||
|
|
||||||
for entry in WalkDir::new(&base)
|
|
||||||
.into_iter()
|
|
||||||
.filter_map(|e| e.ok())
|
|
||||||
.filter(|e| e.file_type().is_file())
|
|
||||||
{
|
|
||||||
let full_path = entry.path().canonicalize()?;
|
|
||||||
let rel_path = match full_path.strip_prefix(&base) {
|
|
||||||
Ok(p) => p,
|
|
||||||
Err(_) => {
|
|
||||||
eprintln!("Skipping (prefix mismatch): {}", full_path.display());
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let archive_path = rel_path.to_string_lossy().replace('\\', "/");
|
|
||||||
println!(" Adding: {}", archive_path);
|
|
||||||
builder = builder.add_file(&full_path, &archive_path);
|
|
||||||
}
|
|
||||||
|
|
||||||
builder.build(&output)?;
|
|
||||||
println!("Created: {}", output.display());
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() -> Result<(), Box<dyn Error>> {
|
#[tokio::main]
|
||||||
let args: Vec<String> = env::args().collect();
|
async fn main() {
|
||||||
|
env_logger::builder()
|
||||||
|
.filter_level(log::LevelFilter::Debug)
|
||||||
|
.format_target(false)
|
||||||
|
.format_timestamp(None)
|
||||||
|
.init();
|
||||||
|
|
||||||
|
let args = Cli::parse();
|
||||||
|
|
||||||
|
if let Err(err) = run(args).await {
|
||||||
|
error!("{}", err);
|
||||||
|
|
||||||
if args.len() != 3 {
|
|
||||||
eprintln!("Usage: tool <src_dir> <output_dir>");
|
|
||||||
eprintln!();
|
|
||||||
eprintln!(" src_dir — root of source tree (must contain Data/)");
|
|
||||||
eprintln!(" output_dir — destination root; MPQs are placed mirroring the src structure");
|
|
||||||
std::process::exit(1);
|
std::process::exit(1);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
let src_dir = PathBuf::from(&args[1]);
|
|
||||||
let output_dir = PathBuf::from(&args[2]);
|
async fn run(args: Cli) -> Result<(), Box<dyn Error>> {
|
||||||
|
info!("Starting tool...");
|
||||||
let data_dir = src_dir.join("Data");
|
|
||||||
if !data_dir.exists() {
|
return match args.command {
|
||||||
return Err(format!("Data/ not found inside src_dir: {}", src_dir.display()).into());
|
Commands::BuildMpq(args) => commands::build_mpq::execute(args),
|
||||||
}
|
};
|
||||||
|
|
||||||
for entry in fs::read_dir(&data_dir)? {
|
|
||||||
let entry = entry?;
|
|
||||||
let path = entry.path();
|
|
||||||
if !path.is_dir() {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let name = entry.file_name().to_string_lossy().to_string();
|
|
||||||
|
|
||||||
if is_locale_dir(&name) {
|
|
||||||
// e.g. ruRU/ — iterate its immediate subdirs, each is a patch source
|
|
||||||
for sub in fs::read_dir(&path)? {
|
|
||||||
let sub = sub?;
|
|
||||||
let sub_path = sub.path();
|
|
||||||
if !sub_path.is_dir() {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
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)?;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Regular patch dir, e.g. patch-4/ or patch-Z/
|
|
||||||
let output_mpq = output_dir.join("Data").join(&name);
|
|
||||||
build_mpq(&path, &output_mpq)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
println!("\nAll MPQ archives built successfully.");
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user