первые наработки по рендеру
This commit is contained in:
Vendored
+72
@@ -0,0 +1,72 @@
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"presentation": {
|
||||
"echo": true,
|
||||
"reveal": "always",
|
||||
"panel": "shared",
|
||||
"clear": false,
|
||||
"showReuseMessage": false
|
||||
},
|
||||
"tasks": [
|
||||
{
|
||||
"label": "Bake: WMO cache (force)",
|
||||
"type": "process",
|
||||
"command": "c:\\Godot 4.6\\Godot_v4.6.1-stable_win64.exe",
|
||||
"args": [
|
||||
"--headless",
|
||||
"--path", "${workspaceFolder}",
|
||||
"--script", "res://src/tools/bake_wmo_cache.gd",
|
||||
"--", "--map", "Azeroth", "--force"
|
||||
],
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "Bake: M2 cache (force)",
|
||||
"type": "process",
|
||||
"command": "c:\\Godot 4.6\\Godot_v4.6.1-stable_win64.exe",
|
||||
"args": [
|
||||
"--headless",
|
||||
"--path", "${workspaceFolder}",
|
||||
"--script", "res://src/tools/bake_m2_cache.gd",
|
||||
"--", "--map", "Azeroth", "--force"
|
||||
],
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "Bake: terrain cache (force)",
|
||||
"type": "process",
|
||||
"command": "c:\\Godot 4.6\\Godot_v4.6.1-stable_win64.exe",
|
||||
"args": [
|
||||
"--headless",
|
||||
"--path", "${workspaceFolder}",
|
||||
"--script", "res://src/tools/bake_adt_terrain_cache.gd",
|
||||
"--", "--map", "Azeroth", "--force"
|
||||
],
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "Bake: all (skip existing)",
|
||||
"type": "process",
|
||||
"command": "c:\\Godot 4.6\\Godot_v4.6.1-stable_win64.exe",
|
||||
"args": [
|
||||
"--headless",
|
||||
"--path", "${workspaceFolder}",
|
||||
"--script", "res://src/tools/bake_all.gd",
|
||||
"--", "--map", "Azeroth"
|
||||
],
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "Bake: all (force)",
|
||||
"type": "process",
|
||||
"command": "c:\\Godot 4.6\\Godot_v4.6.1-stable_win64.exe",
|
||||
"args": [
|
||||
"--headless",
|
||||
"--path", "${workspaceFolder}",
|
||||
"--script", "res://src/tools/bake_all.gd",
|
||||
"--", "--map", "Azeroth", "--force"
|
||||
],
|
||||
"problemMatcher": []
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -15,13 +15,15 @@
|
||||
- читать и рендерить воду из `MH2O`;
|
||||
- стримить мир чанками с LOD в runtime;
|
||||
- стримить мир в **editor preview** по мере движения камеры в 3D viewport;
|
||||
- читать `WMO` в отдельный `Dictionary` и собирать его в `Node3D` вручную.
|
||||
- читать `WDT` и собирать состав мира по данным карты, а не по сканированию папки;
|
||||
- читать `WMO` и автоматически расставлять его в мире через world-level registry с дедупом по `unique_id` (WMO на границе тайлов не дублируется);
|
||||
- подхватывать глобальный WMO из `WDT.MWMO/MODF` для WMO-only карт (инстансы);
|
||||
- читать `M2` (MD20 + SKIN00) и ставить его через `MultiMesh` из ADT doodad placements.
|
||||
|
||||
Что пока не доведено до полноценного world renderer:
|
||||
|
||||
- автоматическая подстановка `WMO` и `M2` в мир из `ADT/WDT`;
|
||||
- вода без простого базового материала: нет нормального water shader, depth fade, shoreline blending, анимации;
|
||||
- world-level `WDT` loader;
|
||||
- M2 renderer статический: material flags (cull/unshaded), blend modes 2+, billboards для листвы и UV-transform'ы не применяются; анимация/скиннинг не парсятся;
|
||||
- импорт обратно в форматы WoW;
|
||||
- полноценные инструменты редактирования мира в editor.
|
||||
|
||||
@@ -38,10 +40,14 @@
|
||||
- поддержка основных сжатых форматов, достаточная для terrain textures.
|
||||
- `ADTLoader`
|
||||
- `MTEX`, `MMDX/MMID`, `MWMO/MWID`;
|
||||
- `MDDF`, `MODF`;
|
||||
- `MDDF`, `MODF` (с `unique_id`, `flags`, `doodad_set`, `name_set`);
|
||||
- `MCVT`, `MCNR`, `MCLY`, `MCAL`;
|
||||
- `holes`;
|
||||
- `MH2O`.
|
||||
- `WDTLoader`
|
||||
- `MVER`, `MPHD` (flags карты: global_wmo / big_alpha / sort_by_size_class);
|
||||
- `MAIN` — таблица присутствия 64×64 ADT-тайлов;
|
||||
- `MWMO` + `MODF` — глобальный WMO для инстансных/WMO-only карт.
|
||||
- `ADTBuilder`
|
||||
- сборка terrain в `Node3D`;
|
||||
- terrain material с 4 слоями;
|
||||
@@ -51,19 +57,32 @@
|
||||
- `StreamingWorld`
|
||||
- runtime streaming тайлов и чанков;
|
||||
- editor streaming вокруг камеры viewport;
|
||||
- отдельные `ADT`-узлы под `Terrain` в дереве сцены.
|
||||
- состав мира берётся из `WDT` (fallback на папочный скан, если `.wdt` отсутствует);
|
||||
- отдельные `ADT`-узлы под `Terrain` в дереве сцены;
|
||||
- world-level WMO registry с refcount-дедупом по `unique_id`: WMO живёт пока на него ссылается хотя бы один загруженный тайл.
|
||||
- `WMOLoader` / `WMOBuilder`
|
||||
- root/group parsing;
|
||||
- `WMOBuilder` собирает mesh по группам;
|
||||
- интегрирован в `StreamingWorld` через world-level registry;
|
||||
- глобальный WMO из `WDT` ставится один раз при загрузке карты.
|
||||
- `M2Loader` / `M2Builder`
|
||||
- парсинг `MD20` + `SKIN00`: vertices/normals/UVs, materials (flags + blend_mode), texture combos, batches;
|
||||
- `M2Builder` собирает `ArrayMesh` + `StandardMaterial3D`;
|
||||
- в `StreamingWorld` расставляется через `MultiMesh` из `ADT.MDDF`.
|
||||
|
||||
### Частично реализовано
|
||||
|
||||
- `WMOLoader`
|
||||
- root/group parsing есть;
|
||||
- `WMOBuilder` собирает mesh по группам;
|
||||
- в общий world streaming пока не интегрировано.
|
||||
- `M2` renderer
|
||||
- статическая геометрия рендерится корректно;
|
||||
- material flags пока не применяются (cull_mode, unshaded, depth_write);
|
||||
- blend modes 2+ (additive / modulate) схлопнуты в обычный alpha;
|
||||
- билборды (leaf bones) не распознаются — листва деревьев плоская;
|
||||
- UV-трансформы (waterfalls, флаги, порталы) не применяются.
|
||||
|
||||
### Не реализовано
|
||||
|
||||
- `M2` renderer;
|
||||
- `WDT` streaming metadata;
|
||||
- скиннинг и анимации `M2` (bones, key frames);
|
||||
- particles / ribbons;
|
||||
- сохранение terrain/liquid/object placement обратно в WoW-форматы;
|
||||
- редакторские инструменты изменения ADT/WMO данных.
|
||||
|
||||
@@ -197,7 +216,28 @@ res://data/extracted
|
||||
|
||||
Читает root/group файлы `WMO` и возвращает `Dictionary` с `materials` и `groups`.
|
||||
|
||||
Это уже можно использовать вручную, но в world streaming оно пока не подключено.
|
||||
Подключён в `StreamingWorld` через world-level registry. Дедуп по `unique_id` из `ADT.MODF`, refcount по тайлам.
|
||||
|
||||
### `WDTLoader`
|
||||
|
||||
Читает `WDT` карты и возвращает `Dictionary`:
|
||||
|
||||
- `version` — `MVER` (ожидается 18);
|
||||
- `flags`, `has_global_wmo`, `big_alpha`, `sort_by_size_class` — из `MPHD`;
|
||||
- `tiles: Array[Dictionary]` — присутствующие тайлы `{x, y, flags}` из `MAIN`;
|
||||
- `global_wmo_name: String` + `global_wmo_placement: Dictionary` — если карта WMO-only.
|
||||
|
||||
`StreamingWorld` по умолчанию строит мир из этого списка. Папочный скан остаётся fallback'ом.
|
||||
|
||||
### `M2Loader`
|
||||
|
||||
Читает `.m2` (`MD20`) + `<basename>00.skin` и возвращает `Dictionary` с геометрией:
|
||||
|
||||
- `textures`, `materials` (`flags`, `blend_mode`), `texture_combos`;
|
||||
- `vertices`, `normals`, `uvs`, `indices`;
|
||||
- `batches: Array[Dictionary]` — `{index_start, index_count, material_id, texture_combo_index}`.
|
||||
|
||||
Кости, анимации и частицы пока не читаются.
|
||||
|
||||
## Export/bake scripts
|
||||
|
||||
@@ -481,19 +521,20 @@ Preview одного `ADT` прямо в editor.
|
||||
|
||||
- terrain текстурирование уже рабочее, но ещё не 1:1 как в клиенте/Noggit во всех edge cases;
|
||||
- water renderer сейчас базовый, без настоящих WoW water materials;
|
||||
- `WMO` читается отдельно, но world placement пока не подключён;
|
||||
- `M2` placements из `ADT` парсятся, но renderer нет;
|
||||
- `WDT` ещё не управляет составом мира;
|
||||
- `M2` рендерится статикой без билбордов, UV-анимаций и скелетной анимации;
|
||||
- старые `baked_terrain_v2` tile-кэши (до добавления `unique_id` в `MODF`) не дедупят WMO на границах тайлов до перезапечки через `bake_all.gd`;
|
||||
- проект сейчас ориентирован на просмотр и исследование данных, а не на полный round-trip import/export.
|
||||
|
||||
## Ближайшие логичные шаги
|
||||
|
||||
1. Подключить `WDT`, чтобы мир собирался не по сканированию папки, а по данным карты.
|
||||
2. Интегрировать `WMO` placement в `StreamingWorld`.
|
||||
3. Добавить `M2` loader/renderer.
|
||||
4. Довести water shader.
|
||||
5. Начать editor-side инструменты для редактирования terrain/liquid/object placement.
|
||||
6. Подготовить формат сохранения обратно в WoW-совместимые структуры.
|
||||
1. Довести M2 renderer:
|
||||
- **A.** Применять material flags: корректный `cull_mode`, `unshaded`, `depth_write`; blend modes 2+ (`additive`, `modulate`) вместо плоского alpha.
|
||||
- **B.** Билборды: парсить bones из `M2`, помечать billboard-флаги, для батчей ссылающихся на billboard-bone выдавать отдельные ноды с `BillboardMode = BILLBOARD_ENABLED`. Листва деревьев перестанет быть плоской.
|
||||
- **C.** UV-трансформы и vertex colors — custom shader, per-instance UV offset в `MultiMesh`. Waterfalls, порталы, флаги.
|
||||
2. Скелетная анимация и скиннинг `M2` (bones, key frames, attachments) — отдельный крупный кусок.
|
||||
3. Довести water shader (depth fade, shoreline blending, анимация).
|
||||
4. Editor-side инструменты редактирования terrain/liquid/object placement.
|
||||
5. Подготовить формат сохранения обратно в WoW-совместимые структуры.
|
||||
|
||||
## Быстрый старт
|
||||
|
||||
|
||||
@@ -0,0 +1,392 @@
|
||||
Godot Engine v4.6.1.stable.official.14d19694e - https://godotengine.org
|
||||
|
||||
Scanning ADT tiles...
|
||||
Found 368 unique WMO models from 687 tiles.
|
||||
WMOLoader: loaded 6 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/dungeon/md_shipwreck/transport_shipwreck.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/kalimdor/collidabledoodads/darkshore/wreckedelvendestroyer/elvendestroyerwreckback.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/kalimdor/collidabledoodads/darkshore/wreckedelvendestroyer/elvendestroyerwreckfront.wmo
|
||||
WMOLoader: loaded 2 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/buildings/guardtower/guardtower.wmo
|
||||
WMOLoader: loaded 6 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/buildings/duskwoodabandoned_humantwostory/duskwoodabandoned_humantwostory.wmo
|
||||
WMOLoader: loaded 2 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/buildings/chapel/duskwoodchapel.wmo
|
||||
WMOLoader: loaded 12 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/dungeon/md_spidermine/md_spidermine_nokobolds.wmo
|
||||
WMOLoader: loaded 5 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/dungeon/md_cryptonerm/md_cryptonerm.wmo
|
||||
WMOLoader: loaded 2 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/buildings/duskwoodabandoned_human_farm/duskwoodabandoned_human_farm.wmo
|
||||
WMOLoader: loaded 12 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/dungeon/md_spidermine/md_spidermine.wmo
|
||||
WMOLoader: loaded 2 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/dungeon/md_shipwreck/shipwreck_a.wmo
|
||||
WMOLoader: loaded 2 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/buildings/duskwood_human_farm/duskwood_human_farm.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/buildings/duskwood_stable/duskwood_stable.wmo
|
||||
WMOLoader: loaded 11 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/buildings/duskwood_inn/duskwood_inn.wmo
|
||||
WMOLoader: loaded 10 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/dungeon/ld_shadowfang/ld_shadowfang.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/khazmodan/buildings/greatwall/greatwall_wall_04.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/khazmodan/buildings/greatwall/greatwall_gate.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/buildings/stormwind/sw_harbor_lighthouse.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/dungeon/md_shipwreck/shipwreck_b.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/buildings/westfall_human_farm_burnt/westfallfarmhouseburnt.wmo
|
||||
[20/368] baked=20 skipped=0 failed=0
|
||||
WMOLoader: loaded 8 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/dungeon/md_mountaincave/md_mountaincave_medium.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/buildings/human_barn_silo/silo.wmo
|
||||
WMOLoader: loaded 3 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/buildings/westfall_barn/westfall_barn.wmo
|
||||
WMOLoader: loaded 38 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/dungeon/az_deadmines/az_deadmines_a.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/collidable doodads/wetlands/wetlandshumandock01/wetlandshumandock01.wmo
|
||||
WMOLoader: loaded 9 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/dungeon/az_deadmines/az_deadmines_c.wmo
|
||||
WMOLoader: loaded 3 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/buildings/duskwoodabandoned_barn/duskwoodabandoned_barn.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/collidable doodads/westfall/westfallgrainsilo01/westfallgrainsilo01.wmo
|
||||
WMOLoader: loaded 6 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/buildings/duskwood_humantwostory/duskwood_humantwostory.wmo
|
||||
WMOLoader: loaded 2 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/buildings/duskwoodabandoned_blacksmith/duskwoodabandoned_blacksmith.wmo
|
||||
WMOLoader: loaded 3 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/buildings/duskwood_barn/duskwood_barn.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/collidable doodads/duskwood/chasmbridge/duskwoodchasmbridge.wmo
|
||||
WMOLoader: loaded 5 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/dungeon/md_goldmine/md_goldmine_1room.wmo
|
||||
WMOLoader: loaded 2 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/buildings/duskwood_blacksmith/duskwood_blacksmith.wmo
|
||||
WMOLoader: loaded 5 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/buildings/duskwood_townhall_nowall/duskwood_townhall_nowall.wmo
|
||||
WMOLoader: loaded 27 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/dungeon/kz_gnomeragon/kz_gnomeragon.wmo
|
||||
WMOLoader: loaded 286 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/buildings/stormwind/stormwind.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/buildings/keepwall/wallpiece01.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/buildings/keepwall/wallpost01.wmo
|
||||
WMOLoader: loaded 4 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/transports/transport_ship/transportship.wmo
|
||||
[40/368] baked=40 skipped=0 failed=0
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/buildings/stormwind/sw_harbor_lowmain.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/buildings/stormwind/sw_harbor_lowstair.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/buildings/stormwind/sw_harbor_lowtwr.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/buildings/stormwind/sw_harbor_shipshelter.wmo
|
||||
WMOLoader: loaded 2 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/buildings/stormwind/sw_harbor_docks.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/buildings/stormwind/sw_harbor_lowstr.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/northrend/buildings/human/icebreaker/transport_icebreaker_ship_nomasts.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/buildings/stormwind/stormwindhangar.wmo
|
||||
WMOLoader: loaded 10 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/dungeon/md_goldmine/md_goldmine.wmo
|
||||
WMOLoader: loaded 2 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/buildings/westfall_human_farm/westfall_human_farm.wmo
|
||||
WMOLoader: loaded 6 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/buildings/westfall_humantwostory/westfall_humantwostory.wmo
|
||||
WMOLoader: loaded 2 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/buildings/moonbrook_human_farm/moonbrook_human_farm.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/buildings/westfall_stable/westfall_stable.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/buildings/westfall_lumbermill/westfall_lumbermill.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/buildings/prison_camp/prisonlonghouse.wmo
|
||||
WMOLoader: loaded 6 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/buildings/moonbrook_humantwostory/moonbrook_humantwostory.wmo
|
||||
WMOLoader: loaded 11 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/buildings/moonbrook_inn/moonbrook_inn.wmo
|
||||
WMOLoader: loaded 2 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/buildings/moonbrook_blacksmith/moonbrook_blacksmith.wmo
|
||||
WMOLoader: loaded 5 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/buildings/moonbrook_townhall_nowall/moonbrook_townhall_nowall.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/khazmodan/buildings/greatwall/greatwall_tower_01.wmo
|
||||
[60/368] baked=60 skipped=0 failed=0
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/collidable doodads/swampofsorrows/swampdock01/swampdock01.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/kalimdor/buildings/trollshop/trollshop01.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/collidable doodads/swampofsorrows/swampbridge01/swampbridge01.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/kalimdor/buildings/trollshoppingmall/trollshoppingmall01.wmo
|
||||
WMOLoader: loaded 10 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/dungeon/md_crypt/md_crypt.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/collidable doodads/duskwood/coveredbridge/duskwoodcoveredbridge.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/buildings/guardtower_ruined/ruinedhumanguardtower01.wmo
|
||||
WMOLoader: loaded 206 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/lorderon/undercity/undercity.wmo
|
||||
WMOLoader: loaded 26 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/buildings/duskwood_barracks/duskwood_barracks.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/lorderon/buildings/tirisfalglade/fenriskeep/fenriskeep.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/buildings/duskwood_human_farm_burnt/duskwoodfarmhouseburnt.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/buildings/duskwoodabandoned_lumbermill/duskwoodabandoned_lumbermill.wmo
|
||||
WMOLoader: loaded 5 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/buildings/duskwood_townhall/duskwood_townhall.wmo
|
||||
WMOLoader: loaded 2 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/buildings/gnomehut/gnomehut.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/kalimdor/collidabledoodads/stonetalon/landingpad/landingpad01.wmo
|
||||
WMOLoader: loaded 7 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/dungeon/md_mountaincave/md_icecave02.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/kalimdor/collidabledoodads/tanaris/trolltent/trolltentsand01.wmo
|
||||
WMOLoader: loaded 2 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/buildings/human_farm/farm.wmo
|
||||
WMOLoader: loaded 28 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/buildings/human_barracks/human_barracks.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/collidable doodads/elwynn/widebridge/elwynnwidebridge.wmo
|
||||
[80/368] baked=80 skipped=0 failed=0
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/collidable doodads/elwynn/lionbridge/elwynnlionbridge.wmo
|
||||
WMOLoader: loaded 5 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/buildings/westfall_magetower/westfall_magetower.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/collidable doodads/stranglethorn/stranglethornruins16/stranglethornruins16.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/collidable doodads/stranglethorn/stranglethornruins12/stranglethornruins12.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/collidable doodads/stranglethorn/stranglethornruins11/stranglethornruins11.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/collidable doodads/stranglethorn/stranglethornruins04/stranglethornruins04.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/kalimdor/collidabledoodads/darkshore/giantseaturtle02/giantseaturtle02.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/buildings/stranglethorn_ruins/stranglethorn_ruins12.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/kalimdor/collidabledoodads/darkshore/snakespine/snakespinegiantarch.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/buildings/stranglethorn_ruins/stranglethorn_ruins05.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/buildings/stranglethorn_ruins/stranglethorn_ruins16.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/buildings/stranglethorn_bootybay/bootybay_fortress2.wmo
|
||||
WMOLoader: loaded 46 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/buildings/stranglethorn_bootybay/bootybay.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/lorderon/buildings/tirisfalglade/mausoleum/mausoleumactive.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/lorderon/undercity/undercitywall01.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/lorderon/buildings/tirisfalglade/lorderonzeppelintower/lorderonzeppelin.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/lorderon/buildings/tirisfalglade/lorderonzeppelintower/lorderonzeppelin_nd_transport.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/buildings/keepwall/wallpieceruin02.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/buildings/keepwall/wallpostruined01.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/buildings/keepwall/wallposttunnel01.wmo
|
||||
[100/368] baked=100 skipped=0 failed=0
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/lorderon/collidabledoodads/arathihighlands/humanruinedbuildings/huruinedbuildingsmed01.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/lorderon/collidabledoodads/arathihighlands/humanruinedbuildings/huruinedbuildingssmall01.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/lorderon/collidabledoodads/arathihighlands/humanruinedbuildings/huruinedbuildingsmed02.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/buildings/keepwall/wallpiecestairs01.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/lorderon/collidabledoodads/arathihighlands/humanruinedbuildings/huruinedbuildingssmall02.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/lorderon/collidabledoodads/arathihighlands/humanruinedbuildings/huruinedbuildingsbig03.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/lorderon/collidabledoodads/arathihighlands/humanruinedbuildings/huruinedbuildingsbig02.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/northrend/dalaran/dalaran_sewer_oldworld_architecture.wmo
|
||||
WMOLoader: loaded 6 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/buildings/humantwostory/humantwostory.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/buildings/prison_camp/prisonguardtower.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/lorderon/buildings/alterac/greenhouse01.wmo
|
||||
WMOLoader: loaded 5 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/buildings/townhall/townhall.wmo
|
||||
WMOLoader: loaded 3 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/buildings/prison_camp/prisonhq.wmo
|
||||
WMOLoader: loaded 2 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/buildings/goldshireblacksmith/goldshireblacksmith.wmo
|
||||
WMOLoader: loaded 11 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/dungeon/md_goldmine/md_goldmine_variantb.wmo
|
||||
WMOLoader: loaded 2 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/dungeon/md_animalden/animalden.wmo
|
||||
WMOLoader: loaded 2 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/buildings/gnomehut/gnomehuttwostory.wmo
|
||||
WMOLoader: loaded 2 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/dungeon/md_caveden/md_iceden.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/kalimdor/buildings/trollhut/trollhut_desert.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/kalimdor/collidabledoodads/tanaris/trolltent/trolltentdesert01.wmo
|
||||
[120/368] baked=120 skipped=0 failed=0
|
||||
WMOLoader: loaded 13 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/dungeon/md_anvilmarpass/anvilmarpass.wmo
|
||||
WMOLoader: loaded 6 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/khazmodan/buildings/anvilmar/anvilmar.wmo
|
||||
WMOLoader: loaded 13 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/dungeon/md_mountaincave/md_icecave.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/collidable doodads/elwynn/abbeygate/abbeygate01.wmo
|
||||
WMOLoader: loaded 12 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/buildings/goldshireinn/goldshireinn.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/buildings/stable/stable.wmo
|
||||
WMOLoader: loaded 3 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/buildings/human_barn_silo/barn.wmo
|
||||
WMOLoader: loaded 22 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/dungeon/md_goldmine/md_goldmine_varianta.wmo
|
||||
WMOLoader: loaded 13 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/dungeon/md_crypt/md_crypt_c.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/collidable doodads/duskwood/opengrave/duskwoodopengrave.wmo
|
||||
WMOLoader: loaded 11 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/buildings/duskwoodabandoned_inn/duskwoodabandoned_inn.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/kalimdor/collidabledoodads/darkshore/darkshoredocks/darkshoredocks01.wmo
|
||||
WMOLoader: loaded 2 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/kalimdor/buildings/orczeppelinhouse/orczeppelinhouse.wmo
|
||||
WMOLoader: loaded 2 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/kalimdor/buildings/orctower/orctower.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/kalimdor/buildings/orcblacksmith/orcblacksmith.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/collidable doodads/stranglethorn/stranglethornarena/stranglegladiatorarena.wmo
|
||||
WMOLoader: loaded 2 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/dungeon/md_caveden/md_trollden.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/collidable doodads/stranglethorn/stranglethornarenagate/stranglearenagate01.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/collidable doodads/stranglethorn/ropebridge/ropebridgesmall.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/buildings/stranglethorn_bootybay/bootybay_railing.wmo
|
||||
[140/368] baked=140 skipped=0 failed=0
|
||||
WMOLoader: loaded 3 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/buildings/stranglethorn_bootybay/bootybay_house1.wmo
|
||||
WMOLoader: loaded 2 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/buildings/stranglethorn_bootybay/bootybay_house2.wmo
|
||||
WMOLoader: loaded 10 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/dungeon/md_pirateship/pirateship_plank.wmo
|
||||
WMOLoader: loaded 6 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/buildings/redridge_humantwostory/redridge_humantwostory.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/collidable doodads/redridge/redridgedocks02/redridgedocks02.wmo
|
||||
WMOLoader: loaded 5 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/buildings/duskwoodabandoned_townhall_nowall/duskwoodabandoned_townhall_nowall.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/buildings/keepwall/wallpieceruin01.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/buildings/keepwall/gateruined01.wmo
|
||||
WMOLoader: loaded 2 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/buildings/redridge_human_farm/redridge_human_farm.wmo
|
||||
WMOLoader: loaded 2 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/dungeon/md_ogremound/md_snogremound1room.wmo
|
||||
WMOLoader: loaded 2 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/dungeon/md_caveden/md_mushroomden.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/buildings/intactprisontower/intactprisontower.wmo
|
||||
WMOLoader: loaded 3 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/dungeon/md_cavetunnels/pvp_alterac_ent01.wmo
|
||||
WMOLoader: loaded 4 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/dungeon/md_mountaincave/md_mountaincave_medium_e.wmo
|
||||
WMOLoader: loaded 5 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/buildings/westfall_townhall_nowall/westfall_townhall_nowall.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/khazmodan/collidabledoodads/menethildocks/menethildocks.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/collidable doodads/ironforge/footbridge/ironfootbridge.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/collidable doodads/ironforge/ironforgeropebridge01/ironforgeropebridge01.wmo
|
||||
WMOLoader: loaded 2 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/khazmodan/buildings/dwarven_blacksmith/snow_blacksmith/snow_blacksmith.wmo
|
||||
WMOLoader: loaded 2 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/khazmodan/buildings/dwarven_1story_hovel/snow_hovel/hovel_machine.wmo
|
||||
[160/368] baked=160 skipped=0 failed=0
|
||||
WMOLoader: loaded 8 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/dungeon/md_mountaincave/md_icecave03.wmo
|
||||
WMOLoader: loaded 7 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/khazmodan/buildings/dwarven_inn/snow_inn/snow_inn.wmo
|
||||
WMOLoader: loaded 7 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/dungeon/md_mountaincave/md_mushroomcave02.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/buildings/redridge_stable/redridge_stable.wmo
|
||||
WMOLoader: loaded 14 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/buildings/nsabbey/nsabbey.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/collidable doodads/elwynn/abbeygate02/abbeygate02.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/buildings/worldtree/duskworldtree.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/kalimdor/buildings/nightelfmoonwell/dsnightelfmoonwellornate.wmo
|
||||
WMOLoader: loaded 9 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/dungeon/md_ogremound/md_ogremound.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/collidable doodads/stranglethorn/ropebridge/ropebridge.wmo
|
||||
WMOLoader: loaded 2 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/buildings/oilplatform/oilplatformsmall.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/kalimdor/buildings/trollhut/trollhut01.wmo
|
||||
WMOLoader: loaded 7 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/dungeon/md_mountaincave/md_mushroomcave03.wmo
|
||||
WMOLoader: loaded 13 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/buildings/scarlet_monestary/scarlet_monestary.wmo
|
||||
WMOLoader: loaded 9 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/dungeon/md_ogremound/md_snogremound.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/khazmodan/collidabledoodads/wetlandsfootbridge/wetlandsfootbridge01.wmo
|
||||
WMOLoader: loaded 11 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/buildings/westfall_inn/westfall_inn.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/lorderon/buildings/wetlands/bridgebigwhite/bridgebigwhite.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/buildings/keepwall/wallpiece03.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/buildings/keepwall/wallpiece02.wmo
|
||||
[180/368] baked=180 skipped=0 failed=0
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/buildings/keepwall/gate03.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/buildings/keepwall/wallpost02.wmo
|
||||
WMOLoader: loaded 4 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/transports/passengership/transportship_a.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/khazmodan/buildings/dwarven_stable/dwarven_stable.wmo
|
||||
WMOLoader: loaded 104 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/khazmodan/cities/ironforge/ironforge.wmo
|
||||
WMOLoader: loaded 2 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/khazmodan/buildings/dwarven_1story_hovel/snow_hovel/snow_hovel.wmo
|
||||
WMOLoader: loaded 2 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/khazmodan/buildings/dwarven_2story_hovel/snow_2story/snow_2story.wmo
|
||||
WMOLoader: loaded 13 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/dungeon/md_goldmine/md_goldmine_variante.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/khazmodan/buildings/searinggorge/darkirontower.wmo
|
||||
WMOLoader: loaded 45 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/dungeon/az_blackrock/blackrock.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/buildings/altarofstorms/altarofstorms.wmo
|
||||
WMOLoader: loaded 5 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/buildings/magetower/magetower.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/kalimdor/collidabledoodads/mulgore/taurentent01/taurentent01.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/buildings/duskwood_lumbermill/duskwood_lumbermill.wmo
|
||||
WMOLoader: loaded 9 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/dungeon/md_mountaincave/md_mushroomcave05.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/collidable doodads/stranglethorn/holdingpen/holdingpenbamboo.wmo
|
||||
WMOLoader: loaded 2 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/kalimdor/buildings/orctower/swamporctower.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/buildings/zulgurub/zulgurubentrance.wmo
|
||||
WMOLoader: loaded 9 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/dungeon/md_ogremound/md_grogremound.wmo
|
||||
WMOLoader: loaded 5 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/buildings/redridge_townhall_nowall/redridge_townhall_nowall.wmo
|
||||
[200/368] baked=200 skipped=0 failed=0
|
||||
WMOLoader: loaded 5 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/buildings/redridge_magetower/redridge_magetower.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/buildings/redridge_lumbermill/redridge_lumbermill.wmo
|
||||
WMOLoader: loaded 2 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/buildings/westfall_guardtower/westfall_guardtower.wmo
|
||||
WMOLoader: loaded 2 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/buildings/redridge_blacksmith/redridge_blacksmith.wmo
|
||||
WMOLoader: loaded 26 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/buildings/redridge_barracks/redridge_barracks.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/buildings/keepwall/gate01.wmo
|
||||
WMOLoader: loaded 11 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/buildings/duskwoodabandoned_inn/andorhalabandoned_inn.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/lorderon/collidabledoodads/arathihighlands/humanruinedbuildings/huruinedbuildingsbig01.wmo
|
||||
WMOLoader: loaded 4 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/dungeon/md_cavetunnels/md_cavetunnel01.wmo
|
||||
WMOLoader: loaded 3 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/buildings/guildhouses/guildhouseb.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/buildings/hillsbradterracewall/hillsbradterracewall.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/outland/humanbuildings/outlandruinedmagetower.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/outland/humanbuildings/outlandruinhumanbarracks01.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/lorderon/collidabledoodads/arathihighlands/durnholdedestroyedwalls/durnholdedestroyedwall.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/khazmodan/buildings/greatwall/greatwall_wall_02.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/khazmodan/buildings/greatwall/greatwall_wall_01.wmo
|
||||
WMOLoader: loaded 12 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/khazmodan/buildings/dwarven_barracks/wetlands_barracks/wet_dwarfbarracks.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/khazmodan/buildings/greatwall/greatwall_tower_02.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/lorderon/collidabledoodads/arathihighlands/stromguard/stromguard.wmo
|
||||
WMOLoader: loaded 5 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/buildings/duskwood_magetower/duskwood_magetower.wmo
|
||||
[220/368] baked=220 skipped=0 failed=0
|
||||
WMOLoader: loaded 2 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/khazmodan/buildings/dwarven_1story_hovel/mud_hovel/mud_hovel.wmo
|
||||
WMOLoader: loaded 2 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/khazmodan/buildings/dwarven_2story_hovel/mud_2story/mud_2story.wmo
|
||||
WMOLoader: loaded 8 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/dungeon/md_dwarven tunnels/smalltunnel.wmo
|
||||
WMOLoader: loaded 10 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/dungeon/md_dwarven tunnels/mediumtunnel.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/khazmodan/buildings/dwarven_snowstable/dwarven_snowstable.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/khazmodan/collidable doodads/golbolarplatform/golbolarplatform.wmo
|
||||
WMOLoader: loaded 4 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/dungeon/md_mountaincave/md_icecave04.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/khazmodan/buildings/searinggorge/darkironcrane.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/khazmodan/collidable doodads/searinggorge/searinggorgescaffold/searinggorgescaffold.wmo
|
||||
WMOLoader: loaded 4 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/dungeon/az_blackrock/blackrock2.wmo
|
||||
WMOLoader: loaded 21 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/kalimdor/buildings/orcfortress/blackrockorcfortress.wmo
|
||||
WMOLoader: loaded 2 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/kalimdor/buildings/trollburrow/trollburrow.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/buildings/lumbermill/lumbermill.wmo
|
||||
WMOLoader: loaded 19 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/dungeon/md_crypt/md_crypt_d.wmo
|
||||
WMOLoader: loaded 2 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/kalimdor/buildings/trollhut/trollhutsmall01.wmo
|
||||
WMOLoader: loaded 3 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/buildings/zulgurub/zulgurubcity.wmo
|
||||
WMOLoader: loaded 4 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/buildings/stranglethorn_ruins/stranglethorn_ruins02.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/buildings/ziggurat/trollziggurat.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/buildings/stranglethorn_ruins/stranglethorn_ruins04.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/buildings/stranglethorn_ruins/stranglethorn_ruins08.wmo
|
||||
[240/368] baked=240 skipped=0 failed=0
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/buildings/stranglethorn_ruins/stranglethorn_ruins09.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/buildings/stranglethorn_ruins/stranglethorn_ruins11.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/collidable doodads/stranglethorn/ropebridge/ropebridgeshort.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/buildings/stranglethorn_ruins/stranglethorn_ruins15.wmo
|
||||
WMOLoader: loaded 2 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/dungeon/md_ogremound/md_ogremound1room.wmo
|
||||
WMOLoader: loaded 12 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/dungeon/md_cryptschool/md_cryptschool.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/lorderon/buildings/tirisfalglade/uthershrine/uthershrine.wmo
|
||||
WMOLoader: loaded 5 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/khazmodan/buildings/dwarven_griffonaviary/dwarven_griffonaviary.wmo
|
||||
WMOLoader: loaded 2 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/khazmodan/buildings/dwarven_blacksmith/wetland_blacksmith/wetland_blacksmith.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/khazmodan/buildings/greatwall/greatwall_wall_03.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/lorderon/collidabledoodads/arathihighlands/wreckedbuildinghcorner02/wreckedbuildinghcorner02.wmo
|
||||
WMOLoader: loaded 16 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/dungeon/md_ruinedkeep/ruinedkeep.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/lorderon/collidabledoodads/arathihighlands/wreckedbuildinghcorner01/wreckedbuildinghcorner01.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/collidable doodads/redridge/redridgedocks01/redridgedocks01.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/collidable doodads/redridge/redridgedocks03/redridgedocks03.wmo
|
||||
WMOLoader: loaded 2 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/khazmodan/buildings/dwarven_snowtower/dwarven_snowtower.wmo
|
||||
WMOLoader: loaded 22 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/dungeon/md_dwarven tunnels/largetunnel.wmo
|
||||
WMOLoader: loaded 3 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/kalimdor/buildings/orcmedium/orcmedium.wmo
|
||||
WMOLoader: loaded 5 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/kalimdor/buildings/orcmagetower/orcmagetower.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/kalimdor/buildings/orckennel/orckennel.wmo
|
||||
[260/368] baked=260 skipped=0 failed=0
|
||||
WMOLoader: loaded 13 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/dungeon/md_mountaincave/md_mushroomcave.wmo
|
||||
WMOLoader: loaded 12 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/dungeon/az_karazahn/kharazan.wmo
|
||||
WMOLoader: loaded 7 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/lorderon/collidabledoodads/arathihighlands/humanruinedbuildings/huruinedbuildingssm01_winecellar.wmo
|
||||
WMOLoader: loaded 4 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/lorderon/collidabledoodads/arathihighlands/humanruinedbuildings/huruinedbuilding_collapsed.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/collidable doodads/stranglethorn/strangleruinstair/strangleruinstair.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/buildings/stranglethorn_ruins/stranglethorn_ruins17.wmo
|
||||
WMOLoader: loaded 2 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/kalimdor/buildings/trollhotel/troll_hotel01.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/buildings/stranglethorn_ruins/stranglethorn_ruins14.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/collidable doodads/stranglethorn/stranglethornruins01/stranglethornruins01.wmo
|
||||
WMOLoader: loaded 25 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/dungeon/md_stratholme_plaguewood_tunnel/md_stratholme_plaguewood_tunnel.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/lorderon/collidabledoodads/plaguelandbridge/plaguelandsbridge.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/collidable doodads/redridge/redridgedocks/redridgesmalldock01.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/buildings/keepwall/lowwallpost01.wmo
|
||||
WMOLoader: loaded 15 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/dungeon/md_ruinedkeep/ruinedkeep_crypt.wmo
|
||||
WMOLoader: loaded 2 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/dungeon/md_ruinedkeep/ruinedkeep_crypt_facade.wmo
|
||||
WMOLoader: loaded 2 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/khazmodan/buildings/dwarven_guardtower/dwarven_guardtower.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/lorderon/collidabledoodads/arathihighlands/bridge/arathistonebridge02.wmo
|
||||
WMOLoader: loaded 5 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/khazmodan/buildings/dwarven_tavern/wetlands_tavern/wet_tavern.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/kalimdor/collidabledoodads/dustwallowmarsh/crashedzippelin/final/crashedzippelin.wmo
|
||||
WMOLoader: loaded 7 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/khazmodan/buildings/thandolspan_bridge/thandolspan.wmo
|
||||
[280/368] baked=280 skipped=0 failed=0
|
||||
WMOLoader: loaded 8 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/dungeon/md_mountaincave/md_mushroomcave04.wmo
|
||||
WMOLoader: loaded 8 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/dungeon/md_dwarven tunnels/smalltunnel_snow.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/khazmodan/collidable doodads/lochmodan/dwarvensentinels/dwarvensentinel01.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/khazmodan/collidable doodads/lochmodan/dwarvensentinels/dwarvensentinel02.wmo
|
||||
WMOLoader: loaded 2 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/dungeon/md_caveden/md_warmden.wmo
|
||||
WMOLoader: loaded 9 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/dungeon/md_ogremound/md_brogremound.wmo
|
||||
WMOLoader: loaded 2 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/kalimdor/buildings/orctower/blackrockorctower.wmo
|
||||
WMOLoader: loaded 11 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/buildings/redridge_inn/redridge_inn.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/collidable doodads/redridge/redridgedamagedbridge/redridgedamagedbridge.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/collidable doodads/redridge/redridgedocks/redridgesmalldock02.wmo
|
||||
WMOLoader: loaded 2 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/dungeon/md_ogremound/md_grogremound1room.wmo
|
||||
WMOLoader: loaded 4 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/kalimdor/buildings/orcbarracks/abandonedorcbarracks.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/kalimdor/buildings/classicalelfruins/azrclassicalelfruin10.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/kalimdor/buildings/classicalelfruins/azrclassicalelfruin01.wmo
|
||||
WMOLoader: loaded 2 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/lorderon/buildings/easternplaguelands/undeadziggurat/undeadziggurat.wmo
|
||||
WMOLoader: loaded 4 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/dungeon/md_cavetunnels/md_warmcavetunnel01.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/kalimdor/buildings/nightelfhunterslodge/nightelfhunterslodge.wmo
|
||||
WMOLoader: loaded 3 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/kalimdor/buildings/nightelfsmallhouse/nightelfsmallhouse.wmo
|
||||
WMOLoader: loaded 5 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/dungeon/md_ogremound/md_ogremound_medium.wmo
|
||||
WMOLoader: loaded 3 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/dungeon/md_mountaincave/md_mountaincave_medium_d.wmo
|
||||
[300/368] baked=300 skipped=0 failed=0
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/collidable doodads/wetlands/wetlandswagon/final/wetlandsbustedwagon.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/khazmodan/collidable doodads/lochmodan/dwarvendam/dwarvendam01.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/khazmodan/collidable doodads/lochmodan/dwarvendock/dwarvendock01.wmo
|
||||
WMOLoader: loaded 8 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/khazmodan/buildings/dwarven_inn/wetlands_inn/wet_inn.wmo
|
||||
WMOLoader: loaded 23 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/dungeon/kz_uldaman/kz_uldaman_a.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/collidable doodads/redridge/redridgefootbridge/redridgefootbridge.wmo
|
||||
WMOLoader: loaded 2 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/outland/draenibuildings/outland_draeni_hut_1.wmo
|
||||
WMOLoader: loaded 3 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/outland/draenibuildings/draenihuthooded01.wmo
|
||||
WMOLoader: loaded 2 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/kalimdor/buildings/trollburrow/swamptrollburrow.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/collidable doodads/swampofsorrows/bridgestranglethorn/bridgestranglethorn.wmo
|
||||
WMOLoader: loaded 13 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/dungeon/md_goldmine/md_goldmine_variantd.wmo
|
||||
WMOLoader: loaded 3 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/kalimdor/buildings/orctower/abandonedorctower.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/outland/orcbuildings/outlandorctower04.wmo
|
||||
WMOLoader: loaded 2 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/dungeon/md_ogremound/md_brogremound1room.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/outland/darkportal/darkportal.wmo
|
||||
WMOLoader: loaded 12 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/lorderon/buildings/easternplaguelands/undeadslaughterhouse/slaughterhouse.wmo
|
||||
WMOLoader: loaded 5 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/dungeon/ld_stratholme/stratholme_a.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/lorderon/buildings/easternplaguelands/naxportal/epl_portal.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/buildings/worldtree/worldtree.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/kalimdor/buildings/trollhut/trolltent.wmo
|
||||
[320/368] baked=320 skipped=0 failed=0
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/lorderon/collidabledoodads/hinterlands/trollcitypieces/aerietrollcitylevel7.wmo
|
||||
WMOLoader: loaded 5 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/dungeon/md_mountaincave/md_mushroomcave08.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/khazmodan/buildings/wetlands_grimbatol/wetlands_grimbatol.wmo
|
||||
WMOLoader: loaded 5 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/dungeon/md_cryptsimpleent/md_cryptsimpleent.wmo
|
||||
WMOLoader: loaded 6 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/dungeon/kz_uldaman/kz_uldaman_c.wmo
|
||||
WMOLoader: loaded 36 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/dungeon/sunkentemple/az_sunkentemple.wmo
|
||||
WMOLoader: loaded 3 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/kalimdor/buildings/orcmedium/swamporcmedium.wmo
|
||||
WMOLoader: loaded 4 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/kalimdor/buildings/orcgreathall/swamporcgreathall.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/kalimdor/buildings/orcblacksmith/swamporcblacksmith.wmo
|
||||
WMOLoader: loaded 6 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/dungeon/md_mountaincave/md_mushroomcave07.wmo
|
||||
WMOLoader: loaded 26 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/buildings/westfall_barracks/westfall_barracks.wmo
|
||||
WMOLoader: loaded 2 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/buildings/westfall_blacksmith/westfall_blacksmith.wmo
|
||||
WMOLoader: loaded 2 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/buildings/prison_camp/prisonoubliette.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/buildings/westfall_stable/westfall_stableb.wmo
|
||||
WMOLoader: loaded 13 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/dungeon/md_hordemine/md_hordemine.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/outland/orcbuildings/outlandorctower02.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/outland/orcbuildings/outlandorctower01.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/outland/orcbuildings/outlandorcpigfarm01.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/kalimdor/pvp/collidabledoodads/netents/nightelftent02_pvp.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/kalimdor/buildings/nightelfhunterslodge/dsnightelfhunterslodge.wmo
|
||||
[340/368] baked=340 skipped=0 failed=0
|
||||
WMOLoader: loaded 7 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/dungeon/ld_stratholme/stratholme_c.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/kalimdor/buildings/classicalelfruins/classicalelfruin10.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/kalimdor/buildings/classicalelfruins/classicalelfruin01.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/kalimdor/buildings/classicalelfruins/classicalelfruin02.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/kalimdor/buildings/classicalelfruins/classicalelfruin03.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/kalimdor/buildings/classicalelfruins/classicalelfruin05.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/lorderon/collidabledoodads/hinterlands/trollcitypieces/aerietrollcitylevel1.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/lorderon/collidabledoodads/hinterlands/trollcitypieces/aerietrollcitylevel2.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/lorderon/collidabledoodads/hinterlands/trollcitypieces/aerietrollcitylevel3.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/lorderon/collidabledoodads/hinterlands/trollcitypieces/aerietrollcitylevel4.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/lorderon/collidabledoodads/hinterlands/trollcitypieces/aerietrollcitylevel5.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/lorderon/collidabledoodads/hinterlands/trollcitypieces/aerietrollcitylevel6.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/lorderon/collidabledoodads/hinterlands/trollcitypieces/aerietrollcityarena.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/lorderon/collidabledoodads/hinterlands/trollcitypieces/aerietrollcityampatheater.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/khazmodan/collidable doodads/lochmodan/excavationsite/excavationplatform02.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/khazmodan/collidable doodads/lochmodan/excavationsite/excavationplatform01.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/khazmodan/collidable doodads/lochmodan/excavationsite/excavationplatform03.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/kalimdor/collidabledoodads/darkshore/giantseaturtle01/giantseaturtle01.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/kalimdor/buildings/trollhut/trollhut_burn01.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/buildings/stranglethorn_bootybay/ratchet_dock.wmo
|
||||
[360/368] baked=360 skipped=0 failed=0
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/dungeon/md_shipwreck/shipwreck_c.wmo
|
||||
WMOLoader: loaded 6 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/dungeon/nd_necropolis/deathknightnecropolis.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/buildings/duskwood_human_farm/duskwood_human_farm_closed.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/dungeon/nd_necropolis/nd_necropolisteleport02.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/buildings/duskwood_barn/duskwood_barn_closed.wmo
|
||||
WMOLoader: loaded 7 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/lorderon/buildings/easternplaguelands/cathedral/cathedral.wmo
|
||||
WMOLoader: loaded 2 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/buildings/chapel/redridgechapel.wmo
|
||||
WMOLoader: loaded 1 groups from C:/Users/sindo/open-wc/data/extracted/world/wmo/azeroth/buildings/guardtower/guardtower_damaged.wmo
|
||||
[368/368] baked=368 skipped=0 failed=0
|
||||
Done. baked=368 skipped=0 failed=0
|
||||
@@ -0,0 +1,46 @@
|
||||
Godot Engine v4.6.1.stable.official.14d19694e - https://godotengine.org
|
||||
|
||||
Scanning ADT tiles...
|
||||
Found 2012 unique M2 models from 687 tiles.
|
||||
[50/2012] baked=50 skipped=0 failed=0
|
||||
[100/2012] baked=100 skipped=0 failed=0
|
||||
[150/2012] baked=150 skipped=0 failed=0
|
||||
[200/2012] baked=200 skipped=0 failed=0
|
||||
[250/2012] baked=250 skipped=0 failed=0
|
||||
[300/2012] baked=300 skipped=0 failed=0
|
||||
[350/2012] baked=350 skipped=0 failed=0
|
||||
[400/2012] baked=400 skipped=0 failed=0
|
||||
[450/2012] baked=450 skipped=0 failed=0
|
||||
[500/2012] baked=500 skipped=0 failed=0
|
||||
[550/2012] baked=550 skipped=0 failed=0
|
||||
[600/2012] baked=600 skipped=0 failed=0
|
||||
[650/2012] baked=650 skipped=0 failed=0
|
||||
[700/2012] baked=700 skipped=0 failed=0
|
||||
[750/2012] baked=750 skipped=0 failed=0
|
||||
[800/2012] baked=800 skipped=0 failed=0
|
||||
[850/2012] baked=850 skipped=0 failed=0
|
||||
[900/2012] baked=899 skipped=0 failed=1
|
||||
[950/2012] baked=949 skipped=0 failed=1
|
||||
[1000/2012] baked=999 skipped=0 failed=1
|
||||
[1050/2012] baked=1049 skipped=0 failed=1
|
||||
[1100/2012] baked=1099 skipped=0 failed=1
|
||||
[1150/2012] baked=1149 skipped=0 failed=1
|
||||
[1200/2012] baked=1199 skipped=0 failed=1
|
||||
[1250/2012] baked=1248 skipped=0 failed=2
|
||||
[1300/2012] baked=1298 skipped=0 failed=2
|
||||
[1350/2012] baked=1348 skipped=0 failed=2
|
||||
[1400/2012] baked=1398 skipped=0 failed=2
|
||||
[1450/2012] baked=1447 skipped=0 failed=3
|
||||
[1500/2012] baked=1497 skipped=0 failed=3
|
||||
[1550/2012] baked=1547 skipped=0 failed=3
|
||||
[1600/2012] baked=1597 skipped=0 failed=3
|
||||
[1650/2012] baked=1647 skipped=0 failed=3
|
||||
[1700/2012] baked=1697 skipped=0 failed=3
|
||||
[1750/2012] baked=1747 skipped=0 failed=3
|
||||
[1800/2012] baked=1797 skipped=0 failed=3
|
||||
[1850/2012] baked=1847 skipped=0 failed=3
|
||||
[1900/2012] baked=1897 skipped=0 failed=3
|
||||
[1950/2012] baked=1947 skipped=0 failed=3
|
||||
[2000/2012] baked=1997 skipped=0 failed=3
|
||||
[2012/2012] baked=2009 skipped=0 failed=3
|
||||
Done. baked=2009 skipped=0 failed=3
|
||||
@@ -520,6 +520,7 @@ static func _build_mesh_instance_from_payload(payload: Dictionary) -> MeshInstan
|
||||
static func _build_tile_water_root(data: Dictionary, origin_offset: Vector3) -> Node3D:
|
||||
var root := Node3D.new()
|
||||
root.name = "Water"
|
||||
var surfaces: Dictionary = {}
|
||||
|
||||
for chunk in data.get("chunks", []) as Array:
|
||||
if chunk.is_empty():
|
||||
@@ -527,13 +528,102 @@ static func _build_tile_water_root(data: Dictionary, origin_offset: Vector3) ->
|
||||
for liquid in chunk.get("liquids", []) as Array:
|
||||
if liquid.is_empty():
|
||||
continue
|
||||
var water_mesh := _build_liquid_mesh(chunk, liquid, origin_offset)
|
||||
if water_mesh:
|
||||
root.add_child(water_mesh)
|
||||
var liquid_id := int(liquid.get("liquid_id", 0))
|
||||
if not surfaces.has(liquid_id):
|
||||
surfaces[liquid_id] = {
|
||||
"verts": PackedVector3Array(),
|
||||
"nrms": PackedVector3Array(),
|
||||
"uvs": PackedVector2Array(),
|
||||
"indices": PackedInt32Array(),
|
||||
}
|
||||
_append_liquid_geometry(chunk, liquid, origin_offset, surfaces[liquid_id])
|
||||
|
||||
for liquid_id in surfaces.keys():
|
||||
var surface: Dictionary = surfaces[liquid_id]
|
||||
var verts: PackedVector3Array = surface["verts"]
|
||||
if verts.is_empty():
|
||||
continue
|
||||
|
||||
var arrays := []
|
||||
arrays.resize(Mesh.ARRAY_MAX)
|
||||
arrays[Mesh.ARRAY_VERTEX] = verts
|
||||
arrays[Mesh.ARRAY_NORMAL] = surface["nrms"]
|
||||
arrays[Mesh.ARRAY_TEX_UV] = surface["uvs"]
|
||||
arrays[Mesh.ARRAY_INDEX] = surface["indices"]
|
||||
|
||||
var mesh := ArrayMesh.new()
|
||||
mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays)
|
||||
mesh.surface_set_material(0, _build_liquid_material(int(liquid_id)))
|
||||
|
||||
var mi := MeshInstance3D.new()
|
||||
mi.mesh = mesh
|
||||
mi.name = "Liquid_%d" % int(liquid_id)
|
||||
root.add_child(mi)
|
||||
|
||||
return root if root.get_child_count() > 0 else null
|
||||
|
||||
|
||||
static func _append_liquid_geometry(
|
||||
chunk: Dictionary,
|
||||
liquid: Dictionary,
|
||||
origin_offset: Vector3,
|
||||
surface: Dictionary) -> void:
|
||||
|
||||
var liquid_mask: PackedByteArray = liquid.get("mask", PackedByteArray())
|
||||
var liquid_heights: PackedFloat32Array = liquid.get("heights", PackedFloat32Array())
|
||||
if liquid_mask.size() < 8 * 8 or liquid_heights.size() < 9 * 9:
|
||||
return
|
||||
|
||||
var chunk_origin: Vector3 = chunk.get("origin", Vector3.ZERO)
|
||||
var tile_local_origin := chunk_origin - origin_offset
|
||||
var verts: PackedVector3Array = surface["verts"]
|
||||
var nrms: PackedVector3Array = surface["nrms"]
|
||||
var uvs_arr: PackedVector2Array = surface["uvs"]
|
||||
var indices: PackedInt32Array = surface["indices"]
|
||||
|
||||
for z in range(8):
|
||||
for x in range(8):
|
||||
if liquid_mask[z * 8 + x] == 0:
|
||||
continue
|
||||
|
||||
var base := verts.size()
|
||||
var x0 := tile_local_origin.x + x * UNIT_SIZE
|
||||
var x1 := tile_local_origin.x + (x + 1) * UNIT_SIZE
|
||||
var z0 := tile_local_origin.z + z * UNIT_SIZE
|
||||
var z1 := tile_local_origin.z + (z + 1) * UNIT_SIZE
|
||||
var y00 := liquid_heights[z * 9 + x] - origin_offset.y
|
||||
var y10 := liquid_heights[z * 9 + x + 1] - origin_offset.y
|
||||
var y01 := liquid_heights[(z + 1) * 9 + x] - origin_offset.y
|
||||
var y11 := liquid_heights[(z + 1) * 9 + x + 1] - origin_offset.y
|
||||
|
||||
verts.append(Vector3(x0, y00, z0))
|
||||
verts.append(Vector3(x0, y01, z1))
|
||||
verts.append(Vector3(x1, y11, z1))
|
||||
verts.append(Vector3(x1, y10, z0))
|
||||
|
||||
nrms.append(Vector3.UP)
|
||||
nrms.append(Vector3.UP)
|
||||
nrms.append(Vector3.UP)
|
||||
nrms.append(Vector3.UP)
|
||||
|
||||
uvs_arr.append(Vector2(float(x) / 8.0, float(z) / 8.0))
|
||||
uvs_arr.append(Vector2(float(x) / 8.0, float(z + 1) / 8.0))
|
||||
uvs_arr.append(Vector2(float(x + 1) / 8.0, float(z + 1) / 8.0))
|
||||
uvs_arr.append(Vector2(float(x + 1) / 8.0, float(z) / 8.0))
|
||||
|
||||
indices.append(base)
|
||||
indices.append(base + 1)
|
||||
indices.append(base + 2)
|
||||
indices.append(base)
|
||||
indices.append(base + 2)
|
||||
indices.append(base + 3)
|
||||
|
||||
surface["verts"] = verts
|
||||
surface["nrms"] = nrms
|
||||
surface["uvs"] = uvs_arr
|
||||
surface["indices"] = indices
|
||||
|
||||
|
||||
static func _build_liquid_mesh(
|
||||
chunk: Dictionary,
|
||||
liquid: Dictionary,
|
||||
|
||||
@@ -5,7 +5,27 @@
|
||||
## add_child(node)
|
||||
class_name WMOBuilder
|
||||
|
||||
const M2_BUILDER_SCRIPT := preload("res://addons/mpq_extractor/loaders/m2_builder.gd")
|
||||
const M2_RIGHT_YAW_OFFSET := PI * 0.5
|
||||
|
||||
static var _texture_cache: Dictionary = {}
|
||||
# rel_path → Node3D prototype (built once, duplicated per usage)
|
||||
static var _m2_prototype_cache: Dictionary = {}
|
||||
# rel_path → true (resolved missing — skip)
|
||||
static var _m2_missing_cache: Dictionary = {}
|
||||
|
||||
|
||||
# Bake scripts should call this before SceneTree.quit() to release cached
|
||||
# Node3D prototypes (Node-derived objects aren't RefCounted, so the static
|
||||
# dict alone won't free them at exit).
|
||||
static func clear_caches() -> void:
|
||||
for proto in _m2_prototype_cache.values():
|
||||
if is_instance_valid(proto):
|
||||
(proto as Node).free()
|
||||
_m2_prototype_cache.clear()
|
||||
_m2_missing_cache.clear()
|
||||
_texture_cache.clear()
|
||||
_wmo_liquid_material_cache.clear()
|
||||
|
||||
# Returns a Node3D containing one MeshInstance3D per WMO group.
|
||||
static func build(data: Dictionary, extracted_dir: String = "") -> Node3D:
|
||||
@@ -23,7 +43,7 @@ static func build(data: Dictionary, extracted_dir: String = "") -> Node3D:
|
||||
for mat_def in materials:
|
||||
godot_materials.append(_build_material(mat_def, textures, extracted_dir))
|
||||
|
||||
# Build one MeshInstance3D per group
|
||||
# Build one MeshInstance3D per group, plus optional liquid mesh as sibling
|
||||
var groups: Array = data.get("groups", [])
|
||||
for gi in groups.size():
|
||||
var g: Dictionary = groups[gi]
|
||||
@@ -33,9 +53,117 @@ static func build(data: Dictionary, extracted_dir: String = "") -> Node3D:
|
||||
mesh_inst.name = "Group_%d" % gi
|
||||
root.add_child(mesh_inst)
|
||||
|
||||
var liquid_dict: Variant = g.get("liquid", null)
|
||||
if liquid_dict is Dictionary and not (liquid_dict as Dictionary).is_empty():
|
||||
var liquid_inst := _build_group_liquid(liquid_dict)
|
||||
if liquid_inst != null:
|
||||
liquid_inst.name = "Group_%d_Liquid" % gi
|
||||
root.add_child(liquid_inst)
|
||||
|
||||
# Doodad set 0 ($DefaultGlobal) — global decorations always active.
|
||||
# Non-default sets (Day/Night/etc) are not baked yet — see TODO below.
|
||||
_build_default_doodads(root, data, extracted_dir)
|
||||
|
||||
return root
|
||||
|
||||
|
||||
# Instantiates M2 doodads for set 0. Each placement is a M2 prototype
|
||||
# duplicated under the WMO root with a local Transform3D.
|
||||
static func _build_default_doodads(root: Node3D, data: Dictionary,
|
||||
extracted_dir: String) -> void:
|
||||
|
||||
var sets: Array = data.get("doodad_sets", [])
|
||||
var placements: Array = data.get("doodad_placements", [])
|
||||
if placements.is_empty() or sets.is_empty():
|
||||
return
|
||||
|
||||
var first_set: Dictionary = sets[0]
|
||||
var start: int = int(first_set.get("start", 0))
|
||||
var count: int = int(first_set.get("count", 0))
|
||||
if count <= 0:
|
||||
return
|
||||
|
||||
var doodad_root := Node3D.new()
|
||||
doodad_root.name = "Doodads"
|
||||
root.add_child(doodad_root)
|
||||
|
||||
var end: int = mini(start + count, placements.size())
|
||||
for i in range(start, end):
|
||||
var p: Dictionary = placements[i]
|
||||
var rel_path: String = str(p.get("name", "")).replace("\\", "/").to_lower()
|
||||
if rel_path.is_empty():
|
||||
continue
|
||||
# WoW 3.3.5 stores .mdx/.mdl in MODN; the actual file on disk is .m2.
|
||||
if rel_path.ends_with(".mdx") or rel_path.ends_with(".mdl"):
|
||||
rel_path = rel_path.get_basename() + ".m2"
|
||||
|
||||
var prototype: Node3D = _get_or_load_m2_prototype(rel_path, extracted_dir)
|
||||
if prototype == null:
|
||||
continue
|
||||
|
||||
var inst := prototype.duplicate() as Node3D
|
||||
if inst == null:
|
||||
continue
|
||||
inst.name = "%s_%d" % [rel_path.get_file().get_basename(), i]
|
||||
|
||||
var pos: Vector3 = p.get("pos", Vector3.ZERO)
|
||||
var rot_q: Quaternion = p.get("rot", Quaternion.IDENTITY)
|
||||
var scl: float = float(p.get("scale", 1.0))
|
||||
var basis := (Basis(rot_q) * Basis(Vector3.UP, M2_RIGHT_YAW_OFFSET)).scaled(Vector3.ONE * maxf(scl, 0.001))
|
||||
inst.transform = Transform3D(basis, pos)
|
||||
doodad_root.add_child(inst)
|
||||
|
||||
|
||||
static func _get_or_load_m2_prototype(rel_path: String,
|
||||
extracted_dir: String) -> Node3D:
|
||||
|
||||
if _m2_prototype_cache.has(rel_path):
|
||||
return _m2_prototype_cache[rel_path]
|
||||
if _m2_missing_cache.has(rel_path):
|
||||
return null
|
||||
|
||||
var prototype: Node3D = _build_raw_m2_prototype(rel_path, extracted_dir)
|
||||
if prototype != null:
|
||||
_m2_prototype_cache[rel_path] = prototype
|
||||
return prototype
|
||||
|
||||
# Fall back to pre-baked cache when the raw M2 is unavailable.
|
||||
var cache_paths := [
|
||||
"res://data/cache/m2_glb".path_join(rel_path.get_basename() + ".tscn"),
|
||||
"res://data/cache/m2_glb".path_join(rel_path.get_basename() + ".glb"),
|
||||
]
|
||||
for cache_path in cache_paths:
|
||||
if not ResourceLoader.exists(cache_path):
|
||||
continue
|
||||
var resource: Resource = load(cache_path)
|
||||
if resource is PackedScene:
|
||||
var node = (resource as PackedScene).instantiate()
|
||||
if node is Node3D:
|
||||
_m2_prototype_cache[rel_path] = node as Node3D
|
||||
return node as Node3D
|
||||
break
|
||||
|
||||
_m2_missing_cache[rel_path] = true
|
||||
return null
|
||||
|
||||
|
||||
static func _build_raw_m2_prototype(rel_path: String, extracted_dir: String) -> Node3D:
|
||||
if not ClassDB.class_exists("M2Loader"):
|
||||
return null
|
||||
|
||||
var abs_path := ProjectSettings.globalize_path(extracted_dir.path_join(rel_path))
|
||||
if not FileAccess.file_exists(abs_path):
|
||||
return null
|
||||
|
||||
var loader = ClassDB.instantiate("M2Loader")
|
||||
var m2_data: Dictionary = loader.call("load_m2", abs_path)
|
||||
if m2_data.is_empty() or (m2_data.get("vertices", PackedVector3Array()) as PackedVector3Array).is_empty():
|
||||
return null
|
||||
|
||||
var prototype: Node3D = M2_BUILDER_SCRIPT.build(m2_data, extracted_dir)
|
||||
return prototype
|
||||
|
||||
|
||||
static func _build_group_mesh(
|
||||
g: Dictionary,
|
||||
godot_mats: Array[StandardMaterial3D]) -> MeshInstance3D:
|
||||
@@ -138,6 +266,100 @@ static func _build_material(
|
||||
return mat
|
||||
|
||||
|
||||
static var _wmo_liquid_material_cache: Dictionary = {}
|
||||
|
||||
# WMO MLIQ → MeshInstance3D in WMO-local coordinates.
|
||||
# Corner & vertex grid are in WMO-local (Z-up); convert to Godot Y-up using the
|
||||
# same basis change as the WMO geometry: (x, y, z)_local → (-y, z, -x)_godot.
|
||||
static func _build_group_liquid(liquid: Dictionary) -> MeshInstance3D:
|
||||
var xverts: int = int(liquid.get("xverts", 0))
|
||||
var yverts: int = int(liquid.get("yverts", 0))
|
||||
var xtiles: int = int(liquid.get("xtiles", 0))
|
||||
var ytiles: int = int(liquid.get("ytiles", 0))
|
||||
if xverts <= 1 or yverts <= 1 or xtiles <= 0 or ytiles <= 0:
|
||||
return null
|
||||
|
||||
var corner: Vector3 = liquid.get("corner", Vector3.ZERO)
|
||||
var unit: float = float(liquid.get("unit_size", 4.1666666))
|
||||
var heights: PackedFloat32Array = liquid.get("heights", PackedFloat32Array())
|
||||
var tiles: PackedByteArray = liquid.get("tiles", PackedByteArray())
|
||||
if heights.size() < xverts * yverts:
|
||||
return null
|
||||
|
||||
var verts := PackedVector3Array()
|
||||
var indices := PackedInt32Array()
|
||||
|
||||
# Pre-compute Godot-space position for each grid vertex.
|
||||
# WMO local (lx, ly, lz) → Godot (-ly, lz, -lx).
|
||||
var pos := PackedVector3Array()
|
||||
pos.resize(xverts * yverts)
|
||||
for vy in yverts:
|
||||
for vx in xverts:
|
||||
var lx := corner.x + float(vx) * unit
|
||||
var ly := corner.y + float(vy) * unit
|
||||
var lz := heights[vy * xverts + vx]
|
||||
pos[vy * xverts + vx] = Vector3(-ly, lz, -lx)
|
||||
|
||||
for ty in ytiles:
|
||||
for tx in xtiles:
|
||||
var flag: int = 0
|
||||
if tiles.size() > ty * xtiles + tx:
|
||||
flag = int(tiles[ty * xtiles + tx])
|
||||
# wowdev: high bit (0x80) marks "don't render". Some sources also
|
||||
# treat 0x0F lower-nibble as "no liquid" — skip both cases.
|
||||
if (flag & 0x80) != 0:
|
||||
continue
|
||||
if (flag & 0x0F) == 0x0F:
|
||||
continue
|
||||
var i00 := ty * xverts + tx
|
||||
var i10 := ty * xverts + tx + 1
|
||||
var i01 := (ty + 1) * xverts + tx
|
||||
var i11 := (ty + 1) * xverts + tx + 1
|
||||
var base := verts.size()
|
||||
verts.append(pos[i00])
|
||||
verts.append(pos[i10])
|
||||
verts.append(pos[i11])
|
||||
verts.append(pos[i01])
|
||||
# Two triangles, winding chosen to face up after the (-y, z, -x) flip.
|
||||
indices.append(base + 0)
|
||||
indices.append(base + 1)
|
||||
indices.append(base + 2)
|
||||
indices.append(base + 0)
|
||||
indices.append(base + 2)
|
||||
indices.append(base + 3)
|
||||
|
||||
if verts.is_empty() or indices.is_empty():
|
||||
return null
|
||||
|
||||
var arrays := []
|
||||
arrays.resize(Mesh.ARRAY_MAX)
|
||||
arrays[Mesh.ARRAY_VERTEX] = verts
|
||||
arrays[Mesh.ARRAY_INDEX] = indices
|
||||
|
||||
var mesh := ArrayMesh.new()
|
||||
mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays)
|
||||
mesh.surface_set_material(0, _wmo_liquid_material(int(liquid.get("material_id", 0))))
|
||||
|
||||
var mi := MeshInstance3D.new()
|
||||
mi.mesh = mesh
|
||||
return mi
|
||||
|
||||
|
||||
static func _wmo_liquid_material(material_id: int) -> StandardMaterial3D:
|
||||
if _wmo_liquid_material_cache.has(material_id):
|
||||
return _wmo_liquid_material_cache[material_id]
|
||||
|
||||
var mat := StandardMaterial3D.new()
|
||||
mat.cull_mode = BaseMaterial3D.CULL_DISABLED
|
||||
mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
|
||||
mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
||||
# Default: bluish water. Without DBC liquid_type lookup we can't pick magma/slime
|
||||
# precisely yet — material_id from MLIQ is the WMO MOMT slot, not the DBC id.
|
||||
mat.albedo_color = Color(0.2, 0.4, 0.7, 0.6)
|
||||
_wmo_liquid_material_cache[material_id] = mat
|
||||
return mat
|
||||
|
||||
|
||||
static func _load_texture(rel_path: String, extracted_dir: String) -> Texture2D:
|
||||
if rel_path.is_empty() or extracted_dir.is_empty():
|
||||
return null
|
||||
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"baked":77,"done":false,"failed":0,"last_tile":"33_26","processed":77,"skipped":0,"total":172,"worker":0,"workers":4}
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"baked":79,"done":false,"failed":0,"last_tile":"33_35","processed":79,"skipped":0,"total":172,"worker":1,"workers":4}
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"baked":80,"done":false,"failed":0,"last_tile":"33_40","processed":80,"skipped":0,"total":172,"worker":2,"workers":4}
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"baked":78,"done":false,"failed":0,"last_tile":"33_33","processed":78,"skipped":0,"total":171,"worker":3,"workers":4}
|
||||
@@ -270,15 +270,16 @@ inline void adt_placement_pos_to_godot(float px, float pz, float py,
|
||||
gz = py;
|
||||
}
|
||||
|
||||
// MDDF/MODF Euler angles are stored in degrees using the same axis layout as
|
||||
// placement positions: Y-up, so rot[1] is yaw. Map each axis straight through
|
||||
// to Godot's Y-up Euler space — the position conversion is identity, so the
|
||||
// rotation conversion is identity too. rot[1] gets +180° later for WMOs.
|
||||
// MDDF/MODF Euler angles are stored in degrees in WoW render space (Y-up):
|
||||
// rot[0] — pitch around X, rot[1] — yaw around Y, rot[2] — roll around Z.
|
||||
// WoW's "0 yaw" faces a different direction than Godot's, so rot[1] needs
|
||||
// a fixed +90° offset (matches Blender wow.export importer). Other axes
|
||||
// map straight through.
|
||||
inline void adt_placement_rot_to_godot(float rx, float ry_yaw, float rz,
|
||||
float &gx, float &gy, float &gz) {
|
||||
constexpr float D2R = (float)(M_PI / 180.0);
|
||||
gx = rx * D2R;
|
||||
gy = ry_yaw * D2R;
|
||||
gy = (ry_yaw - 90.0f) * D2R;
|
||||
gz = rz * D2R;
|
||||
}
|
||||
|
||||
@@ -382,12 +383,19 @@ void ADTLoader::_parse_adt(const uint8_t *raw, size_t len, Dictionary &result) {
|
||||
float gx, gy, gz, rx, ry, rz;
|
||||
adt_placement_pos_to_godot(od[i].pos[0], od[i].pos[1], od[i].pos[2], gx, gy, gz);
|
||||
adt_placement_rot_to_godot(od[i].rot[0], od[i].rot[1], od[i].rot[2], rx, ry, rz);
|
||||
// MODF (WMO) needs an additional +90° yaw on top of the M2 offset
|
||||
// (so total WMO yaw offset = 0°). WMO local-forward differs from M2.
|
||||
ry -= (float)(M_PI * 0.5);
|
||||
|
||||
Dictionary p;
|
||||
p["name_id"] = (int)od[i].nameId;
|
||||
p["unique_id"] = (int)od[i].uniqueId;
|
||||
p["pos"] = Vector3(gx, gy, gz);
|
||||
p["rot"] = Vector3(rx, ry, rz);
|
||||
p["scale"] = 1.0f;
|
||||
p["flags"] = (int)od[i].flags;
|
||||
p["doodad_set"] = (int)od[i].doodadSet;
|
||||
p["name_set"] = (int)od[i].nameSet;
|
||||
wmo_placements.push_back(p);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,9 +38,13 @@ namespace godot {
|
||||
// Placement Dictionary (both M2 and WMO):
|
||||
// {
|
||||
// "name_id": int, # index into m2_names / wmo_names
|
||||
// "unique_id": int, # WMO only — MODF uniqueId (dedupe key across tiles)
|
||||
// "pos": Vector3, # Godot world coords
|
||||
// "rot": Vector3, # Euler angles (radians)
|
||||
// "scale": float, # M2 only (WMO always 1.0)
|
||||
// "flags": int, # WMO only
|
||||
// "doodad_set": int, # WMO only
|
||||
// "name_set": int, # WMO only
|
||||
// }
|
||||
//
|
||||
// Chunk Dictionary:
|
||||
|
||||
@@ -190,8 +190,10 @@ Dictionary M2Loader::parse_m2(const std::vector<uint8_t> &buf, const std::string
|
||||
// WoW model space (X right, Y forward, Z up) → Godot (X right, Y up, Z back)
|
||||
vertices[i] = Vector3( v.pos[0], v.pos[2], -v.pos[1]);
|
||||
normals[i] = Vector3( v.normal[0], v.normal[2], -v.normal[1]);
|
||||
// WoW V origin is top-left; Godot expects bottom-left → flip V
|
||||
uvs[i] = Vector2(v.texCoords[0], 1.0f - v.texCoords[1]);
|
||||
// WoW BLP textures and Godot 4 sampling both use V=0 at top
|
||||
// (D3D/Vulkan convention) — pass UVs through unchanged. Same as
|
||||
// wmo_loader's MOTV handling.
|
||||
uvs[i] = Vector2(v.texCoords[0], v.texCoords[1]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "adt_loader.h"
|
||||
#include "blp_loader.h"
|
||||
#include "m2_loader.h"
|
||||
#include "wdt_loader.h"
|
||||
|
||||
#include <godot_cpp/core/defs.hpp>
|
||||
#include <godot_cpp/godot.hpp>
|
||||
@@ -17,6 +18,7 @@ void initialize_mpq_extractor_module(ModuleInitializationLevel p_level) {
|
||||
ClassDB::register_class<ADTLoader>();
|
||||
ClassDB::register_class<BLPLoader>();
|
||||
ClassDB::register_class<M2Loader>();
|
||||
ClassDB::register_class<WDTLoader>();
|
||||
}
|
||||
|
||||
void uninitialize_mpq_extractor_module(ModuleInitializationLevel p_level) {
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
#include "wdt_loader.h"
|
||||
#include "wow_chunk_reader.h"
|
||||
|
||||
#include <godot_cpp/core/class_db.hpp>
|
||||
#include <godot_cpp/variant/utility_functions.hpp>
|
||||
#include <godot_cpp/variant/vector3.hpp>
|
||||
|
||||
#include <fstream>
|
||||
#include <cstring>
|
||||
|
||||
using namespace godot;
|
||||
|
||||
#pragma pack(push, 1)
|
||||
|
||||
struct MPHD {
|
||||
uint32_t flags;
|
||||
uint32_t something;
|
||||
uint32_t unused[6];
|
||||
};
|
||||
|
||||
struct MAINEntry {
|
||||
uint32_t flags; // bit 0 = has_adt
|
||||
uint32_t async_id;
|
||||
};
|
||||
|
||||
struct WDTMODFEntry {
|
||||
uint32_t nameId;
|
||||
uint32_t uniqueId;
|
||||
float pos[3];
|
||||
float rot[3];
|
||||
float bboxMin[3];
|
||||
float bboxMax[3];
|
||||
uint16_t flags;
|
||||
uint16_t doodadSet;
|
||||
uint16_t nameSet;
|
||||
uint16_t padding;
|
||||
};
|
||||
|
||||
#pragma pack(pop)
|
||||
|
||||
std::vector<uint8_t> WDTLoader::read_file(const std::string &path) {
|
||||
std::ifstream f(path, std::ios::binary | std::ios::ate);
|
||||
if (!f) return {};
|
||||
auto sz = f.tellg(); f.seekg(0);
|
||||
std::vector<uint8_t> buf(sz);
|
||||
f.read(reinterpret_cast<char *>(buf.data()), sz);
|
||||
return buf;
|
||||
}
|
||||
|
||||
std::string WDTLoader::to_std(const String &s) {
|
||||
return std::string(s.utf8().get_data());
|
||||
}
|
||||
|
||||
String WDTLoader::to_godot(const std::string &s) {
|
||||
return String(s.c_str());
|
||||
}
|
||||
|
||||
Dictionary WDTLoader::load_wdt(const String &path) {
|
||||
Dictionary result;
|
||||
const std::string p = to_std(path);
|
||||
std::vector<uint8_t> buf = read_file(p);
|
||||
if (buf.empty()) {
|
||||
UtilityFunctions::push_warning("WDTLoader: cannot open file: ", path);
|
||||
return result;
|
||||
}
|
||||
_parse_wdt(buf.data(), buf.size(), result);
|
||||
return result;
|
||||
}
|
||||
|
||||
Dictionary WDTLoader::load_wdt_from_bytes(const PackedByteArray &bytes) {
|
||||
Dictionary result;
|
||||
if (bytes.size() <= 0) return result;
|
||||
_parse_wdt(bytes.ptr(), (size_t)bytes.size(), result);
|
||||
return result;
|
||||
}
|
||||
|
||||
void WDTLoader::_parse_wdt(const uint8_t *raw, size_t len, Dictionary &result) {
|
||||
ChunkReader reader(raw, len);
|
||||
|
||||
uint32_t version = 0;
|
||||
uint32_t flags = 0;
|
||||
bool found_mphd = false;
|
||||
Array tiles;
|
||||
|
||||
WoWChunk c;
|
||||
while (reader.next(c)) {
|
||||
if (c.is("MVER") && c.size >= 4) {
|
||||
std::memcpy(&version, c.data, 4);
|
||||
} else if (c.is("MPHD") && c.size >= sizeof(MPHD)) {
|
||||
const MPHD &mphd = c.as<MPHD>();
|
||||
flags = mphd.flags;
|
||||
found_mphd = true;
|
||||
} else if (c.is("MAIN")) {
|
||||
const uint32_t expected = 64 * 64 * (uint32_t)sizeof(MAINEntry);
|
||||
if (c.size < expected) {
|
||||
UtilityFunctions::push_warning("WDTLoader: MAIN chunk too small: ", (int)c.size);
|
||||
continue;
|
||||
}
|
||||
const MAINEntry *entries = c.array<MAINEntry>();
|
||||
for (int y = 0; y < 64; ++y) {
|
||||
for (int x = 0; x < 64; ++x) {
|
||||
const MAINEntry &e = entries[y * 64 + x];
|
||||
if (!(e.flags & 0x1)) continue;
|
||||
Dictionary td;
|
||||
td["x"] = (int)x;
|
||||
td["y"] = (int)y;
|
||||
td["flags"] = (int)e.flags;
|
||||
tiles.push_back(td);
|
||||
}
|
||||
}
|
||||
} else if (c.is("MWMO") && c.size > 0) {
|
||||
// Global WMO filename (null-terminated).
|
||||
const char *s = reinterpret_cast<const char *>(c.data);
|
||||
size_t n = c.size;
|
||||
while (n > 0 && s[n - 1] == '\0') --n;
|
||||
result["global_wmo_name"] = String(std::string(s, n).c_str());
|
||||
} else if (c.is("MODF") && c.size >= sizeof(WDTMODFEntry)) {
|
||||
const WDTMODFEntry &e = c.as<WDTMODFEntry>();
|
||||
|
||||
float gx, gy, gz;
|
||||
wow_to_godot(e.pos[0], e.pos[1], e.pos[2], gx, gy, gz);
|
||||
|
||||
float rx, ry, rz;
|
||||
wow_rot_to_godot(e.rot[0], e.rot[1], e.rot[2], rx, ry, rz);
|
||||
// Global WMOs (MODF) get the same extra yaw as tile WMOs — see
|
||||
// the matching comment in adt_loader.cpp MODF loop.
|
||||
ry -= (float)(M_PI * 0.5);
|
||||
|
||||
Dictionary placement;
|
||||
placement["name_id"] = (int)e.nameId;
|
||||
placement["unique_id"] = (int)e.uniqueId;
|
||||
placement["pos"] = Vector3(gx, gy, gz);
|
||||
placement["rot"] = Vector3(rx, ry, rz);
|
||||
placement["flags"] = (int)e.flags;
|
||||
placement["doodad_set"] = (int)e.doodadSet;
|
||||
placement["name_set"] = (int)e.nameSet;
|
||||
result["global_wmo_placement"] = placement;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found_mphd) {
|
||||
UtilityFunctions::push_warning("WDTLoader: MPHD chunk missing");
|
||||
}
|
||||
|
||||
result["version"] = (int)version;
|
||||
result["flags"] = (int)flags;
|
||||
result["has_global_wmo"] = (flags & 0x1) != 0;
|
||||
result["big_alpha"] = (flags & 0x4) != 0;
|
||||
result["sort_by_size_class"] = (flags & 0x8) != 0;
|
||||
result["tiles"] = tiles;
|
||||
}
|
||||
|
||||
void WDTLoader::_bind_methods() {
|
||||
ClassDB::bind_method(D_METHOD("load_wdt", "path"), &WDTLoader::load_wdt);
|
||||
ClassDB::bind_method(D_METHOD("load_wdt_from_bytes", "bytes"), &WDTLoader::load_wdt_from_bytes);
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
#pragma once
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <cstdint>
|
||||
|
||||
#include <godot_cpp/classes/ref_counted.hpp>
|
||||
#include <godot_cpp/variant/dictionary.hpp>
|
||||
#include <godot_cpp/variant/array.hpp>
|
||||
#include <godot_cpp/variant/string.hpp>
|
||||
#include <godot_cpp/variant/packed_byte_array.hpp>
|
||||
|
||||
namespace godot {
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// WDTLoader
|
||||
//
|
||||
// Loads WoW 3.3.5a WDT map descriptors.
|
||||
//
|
||||
// Usage (GDScript):
|
||||
// var wdt = WDTLoader.new()
|
||||
// var data = wdt.load_wdt("C:/wow/Data/World/Maps/Azeroth/Azeroth.wdt")
|
||||
//
|
||||
// Return Dictionary:
|
||||
// {
|
||||
// "version": int, # MVER (expected 18)
|
||||
// "flags": int, # MPHD.flags
|
||||
// "has_global_wmo": bool, # flags & 0x1
|
||||
// "big_alpha": bool, # flags & 0x4
|
||||
// "sort_by_size_class": bool, # flags & 0x8
|
||||
// "tiles": Array[Dictionary], # one entry per ADT present (flag 0x1 in MAIN)
|
||||
// # { "x": int, "y": int, "flags": int }
|
||||
// # optional, only when has_global_wmo:
|
||||
// "global_wmo_name": String,
|
||||
// "global_wmo_placement": Dictionary, # MODF-like entry
|
||||
// }
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
class WDTLoader : public RefCounted {
|
||||
GDCLASS(WDTLoader, RefCounted)
|
||||
|
||||
public:
|
||||
Dictionary load_wdt(const String &path);
|
||||
Dictionary load_wdt_from_bytes(const PackedByteArray &bytes);
|
||||
|
||||
protected:
|
||||
static void _bind_methods();
|
||||
|
||||
private:
|
||||
static std::vector<uint8_t> read_file(const std::string &path);
|
||||
static std::string to_std(const String &s);
|
||||
static String to_godot(const std::string &s);
|
||||
|
||||
static void _parse_wdt(const uint8_t *raw, size_t len, Dictionary &result);
|
||||
};
|
||||
|
||||
} // namespace godot
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <godot_cpp/core/class_db.hpp>
|
||||
#include <godot_cpp/variant/utility_functions.hpp>
|
||||
#include <godot_cpp/variant/packed_string_array.hpp>
|
||||
#include <godot_cpp/variant/quaternion.hpp>
|
||||
|
||||
#include <fstream>
|
||||
#include <filesystem>
|
||||
@@ -85,8 +86,40 @@ struct MONREntry { float x, y, z; };
|
||||
struct MOTVEntry { float u, v; };
|
||||
struct MOCVEntry { uint8_t b, g, r, a; };
|
||||
|
||||
struct MLIQHeader { // Liquid header inside MLIQ chunk
|
||||
uint32_t xverts; // = xtiles + 1
|
||||
uint32_t yverts; // = ytiles + 1
|
||||
uint32_t xtiles;
|
||||
uint32_t ytiles;
|
||||
float corner[3]; // base position (WMO local, Z-up)
|
||||
uint16_t materialId;
|
||||
};
|
||||
|
||||
struct MLIQVertex { // 8 bytes per vertex (interpretation depends on liquid type)
|
||||
uint32_t data; // flow info or s,t for magma — we ignore
|
||||
float height;
|
||||
};
|
||||
|
||||
struct MODSEntry { // Doodad set: 32 bytes
|
||||
char name[20];
|
||||
uint32_t startDoodad; // first MODD index
|
||||
uint32_t nDoodads;
|
||||
uint32_t unused;
|
||||
};
|
||||
|
||||
struct MODDEntry { // Doodad placement: 40 bytes
|
||||
uint32_t nameOfsAndFlags; // low 24 = MODN byte offset, high 8 = flags
|
||||
float pos[3]; // WMO local (Z-up)
|
||||
float rot[4]; // quaternion (qx, qy, qz, qw) in WMO local
|
||||
float scale;
|
||||
uint8_t color[4]; // BGRA tint
|
||||
};
|
||||
|
||||
#pragma pack(pop)
|
||||
|
||||
// MLIQ tile size in WoW yards (same constant as ADT MCNK unit cells).
|
||||
static constexpr float WMO_LIQUID_UNIT_SIZE = 4.1666666f;
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Helpers
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
@@ -122,6 +155,14 @@ Dictionary WMOLoader::parse_root(const std::vector<uint8_t> &buf) {
|
||||
Array materials;
|
||||
std::vector<std::string> tex_strings;
|
||||
|
||||
// Two-pass: first collect raw chunks, then build (MODD/MODN/MODS may appear in any order).
|
||||
const uint8_t *modn_data = nullptr;
|
||||
size_t modn_size = 0;
|
||||
const MODSEntry *mods = nullptr;
|
||||
uint32_t mods_count = 0;
|
||||
const MODDEntry *modd = nullptr;
|
||||
uint32_t modd_count = 0;
|
||||
|
||||
while (reader.next(chunk)) {
|
||||
|
||||
if (chunk.is("MOTX")) {
|
||||
@@ -152,11 +193,69 @@ Dictionary WMOLoader::parse_root(const std::vector<uint8_t> &buf) {
|
||||
mat["texture1"] = tex_idx(mt[i].texUnit1);
|
||||
materials.push_back(mat);
|
||||
}
|
||||
} else if (chunk.is("MODS")) {
|
||||
mods = chunk.array<MODSEntry>();
|
||||
mods_count = chunk.count_of(sizeof(MODSEntry));
|
||||
} else if (chunk.is("MODN")) {
|
||||
modn_data = chunk.data;
|
||||
modn_size = chunk.size;
|
||||
} else if (chunk.is("MODD")) {
|
||||
modd = chunk.array<MODDEntry>();
|
||||
modd_count = chunk.count_of(sizeof(MODDEntry));
|
||||
}
|
||||
}
|
||||
|
||||
root["textures"] = textures;
|
||||
root["materials"] = materials;
|
||||
|
||||
// ── Doodad sets ─────────────────────────────────────────────────────────
|
||||
Array doodad_sets;
|
||||
for (uint32_t i = 0; i < mods_count; ++i) {
|
||||
Dictionary s;
|
||||
char buf[21]; std::memcpy(buf, mods[i].name, 20); buf[20] = 0;
|
||||
s["name"] = String(buf);
|
||||
s["start"] = (int)mods[i].startDoodad;
|
||||
s["count"] = (int)mods[i].nDoodads;
|
||||
doodad_sets.push_back(s);
|
||||
}
|
||||
root["doodad_sets"] = doodad_sets;
|
||||
|
||||
// ── Doodad placements ───────────────────────────────────────────────────
|
||||
// Position/rotation come in WMO local (Z-up). Convert to Godot Y-up the
|
||||
// same way as MOVT vertices: (lx, ly, lz) → (-ly, lz, -lx). Quaternion
|
||||
// vector part transforms as a vector under that basis change; the scalar
|
||||
// part is kept (signed `qw` → `qw`). The basis change has det = -1 which
|
||||
// reverses winding; for rotations of M2 prototypes that already live in
|
||||
// Godot-space (built via M2Builder), this gives the visually correct
|
||||
// orientation.
|
||||
Array doodad_placements;
|
||||
for (uint32_t i = 0; i < modd_count; ++i) {
|
||||
const MODDEntry &d = modd[i];
|
||||
|
||||
// Resolve doodad name from MODN by byte offset.
|
||||
uint32_t name_ofs = d.nameOfsAndFlags & 0x00FFFFFFu;
|
||||
String name;
|
||||
if (modn_data && name_ofs < modn_size) {
|
||||
const char *s = reinterpret_cast<const char *>(modn_data + name_ofs);
|
||||
// Bound by remaining MODN bytes.
|
||||
size_t max_len = modn_size - name_ofs;
|
||||
size_t len = 0;
|
||||
while (len < max_len && s[len] != 0) ++len;
|
||||
name = String(std::string(s, len).c_str());
|
||||
}
|
||||
|
||||
Dictionary p;
|
||||
p["name"] = name;
|
||||
p["flags"] = (int)((d.nameOfsAndFlags >> 24) & 0xFFu);
|
||||
p["pos"] = Vector3(-d.pos[1], d.pos[2], -d.pos[0]);
|
||||
p["rot"] = Quaternion(-d.rot[1], d.rot[2], -d.rot[0], d.rot[3]);
|
||||
p["scale"] = d.scale;
|
||||
p["color"] = Color(d.color[2] / 255.f, d.color[1] / 255.f,
|
||||
d.color[0] / 255.f, d.color[3] / 255.f);
|
||||
doodad_placements.push_back(p);
|
||||
}
|
||||
root["doodad_placements"] = doodad_placements;
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
@@ -214,7 +313,7 @@ Dictionary WMOLoader::parse_group(const std::vector<uint8_t> &buf) {
|
||||
auto *v = sc.array<MOTVEntry>();
|
||||
uvs.resize(n);
|
||||
for (uint32_t i = 0; i < n; ++i)
|
||||
uvs[i] = Vector2(v[i].u, v[i].v);
|
||||
uvs[i] = Vector2(v[i].u, 1.0f - v[i].v);
|
||||
|
||||
} else if (sc.is("MOVI")) {
|
||||
uint32_t n = sc.size / 2;
|
||||
@@ -244,6 +343,45 @@ Dictionary WMOLoader::parse_group(const std::vector<uint8_t> &buf) {
|
||||
b["flags"] = (int)ba[i].flags;
|
||||
batches.push_back(b);
|
||||
}
|
||||
} else if (sc.is("MLIQ") && sc.size >= sizeof(MLIQHeader)) {
|
||||
const auto &h = *reinterpret_cast<const MLIQHeader *>(sc.data);
|
||||
|
||||
size_t need = sizeof(MLIQHeader)
|
||||
+ (size_t)h.xverts * h.yverts * sizeof(MLIQVertex)
|
||||
+ (size_t)h.xtiles * h.ytiles;
|
||||
if (need <= sc.size && h.xverts > 0 && h.yverts > 0
|
||||
&& h.xtiles > 0 && h.ytiles > 0) {
|
||||
auto *verts = reinterpret_cast<const MLIQVertex *>(
|
||||
sc.data + sizeof(MLIQHeader));
|
||||
auto *tiles = sc.data + sizeof(MLIQHeader)
|
||||
+ (size_t)h.xverts * h.yverts * sizeof(MLIQVertex);
|
||||
|
||||
PackedFloat32Array heights;
|
||||
heights.resize((int)(h.xverts * h.yverts));
|
||||
{
|
||||
float *dst = heights.ptrw();
|
||||
for (uint32_t i = 0; i < h.xverts * h.yverts; ++i)
|
||||
dst[i] = verts[i].height;
|
||||
}
|
||||
|
||||
PackedByteArray tile_flags;
|
||||
tile_flags.resize((int)(h.xtiles * h.ytiles));
|
||||
std::memcpy(tile_flags.ptrw(), tiles,
|
||||
(size_t)h.xtiles * h.ytiles);
|
||||
|
||||
Dictionary liquid;
|
||||
liquid["xverts"] = (int)h.xverts;
|
||||
liquid["yverts"] = (int)h.yverts;
|
||||
liquid["xtiles"] = (int)h.xtiles;
|
||||
liquid["ytiles"] = (int)h.ytiles;
|
||||
// Corner in WMO local (Z-up). Builder converts to Godot.
|
||||
liquid["corner"] = Vector3(h.corner[0], h.corner[1], h.corner[2]);
|
||||
liquid["material_id"] = (int)h.materialId;
|
||||
liquid["unit_size"] = WMO_LIQUID_UNIT_SIZE;
|
||||
liquid["heights"] = heights;
|
||||
liquid["tiles"] = tile_flags;
|
||||
group["liquid"] = liquid;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -86,7 +86,9 @@ inline void wow_to_godot(float wx, float wy, float wz,
|
||||
gz = -(wx - 17066.666f);
|
||||
}
|
||||
|
||||
// WoW rotation (degrees, ZXY order) → Godot Euler (radians, XYZ)
|
||||
// WoW MDDF/MODF rotation (degrees, render-space Y-up: rx=pitch, ry=yaw, rz=roll)
|
||||
// → Godot Euler (radians). WoW's "0 yaw" differs from Godot's, so ry needs
|
||||
// a fixed +90° offset (matches Blender wow.export importer).
|
||||
#ifndef M_PI
|
||||
#define M_PI 3.14159265358979323846
|
||||
#endif
|
||||
@@ -95,8 +97,8 @@ inline void wow_rot_to_godot(float rx, float ry, float rz,
|
||||
{
|
||||
constexpr float D2R = (float)(M_PI / 180.0);
|
||||
gx = rx * D2R;
|
||||
gy = (rz - 90.0f) * D2R; // WoW Z-rot maps to Godot Y
|
||||
gz = -ry * D2R;
|
||||
gy = (ry - 90.0f) * D2R;
|
||||
gz = rz * D2R;
|
||||
}
|
||||
|
||||
// Collect null-separated strings from a chunk into a vector
|
||||
|
||||
@@ -19,10 +19,9 @@ update_interval = 0.1
|
||||
max_concurrent_tile_tasks = 4
|
||||
chunk_ops_per_tick = 64
|
||||
cached_tile_mesh_limit = 48
|
||||
use_baked_tile_cache = false
|
||||
editor_follow_view_camera = false
|
||||
enable_water = true
|
||||
terrain_cast_shadows = true
|
||||
m2_cast_shadows = null
|
||||
|
||||
[node name="Camera3D" type="Camera3D" parent="." unique_id=502573687]
|
||||
transform = Transform3D(1, 0, 0, 0, 0.707107, 0.707107, 0, -0.707107, 0.707107, 0, 300, 300)
|
||||
@@ -36,6 +35,9 @@ fast_mult = 8.0
|
||||
transform = Transform3D(0.866025, -0.353553, 0.353553, 0, 0.707107, 0.707107, -0.5, -0.612372, 0.612372, 0, 0, 0)
|
||||
light_energy = 1.5
|
||||
shadow_enabled = true
|
||||
directional_shadow_mode = 1
|
||||
directional_shadow_fade_start = 0.85
|
||||
directional_shadow_max_distance = 2200.0
|
||||
|
||||
[node name="WorldEnvironment" type="WorldEnvironment" parent="." unique_id=12906896]
|
||||
environment = SubResource("Environment_1")
|
||||
|
||||
@@ -10,6 +10,7 @@ const REQUIRED_BAKED_TILE_FORMAT_VERSION := 4
|
||||
|
||||
const TILE_SIZE := 533.33333
|
||||
const CHUNK_SIZE := TILE_SIZE / 16.0
|
||||
const M2_RIGHT_YAW_OFFSET := 0.0
|
||||
|
||||
@export var extracted_dir: String = "res://data/extracted"
|
||||
@export var map_name: String = "Azeroth"
|
||||
@@ -51,6 +52,14 @@ const CHUNK_SIZE := TILE_SIZE / 16.0
|
||||
@export var enable_m2_assets: bool = true
|
||||
@export var enable_m2_placeholders: bool = false
|
||||
@export var m2_placeholder_only_trees: bool = true
|
||||
## Dynamic doodad render radius in ADT tiles. Negative keeps all loaded M2s active.
|
||||
@export var m2_tile_radius: int = 2
|
||||
## Dynamic WMO render radius in ADT tiles. Negative keeps all loaded WMOs active.
|
||||
@export var wmo_tile_radius: int = 4
|
||||
## Godot visibility range applied to instantiated M2 MultiMesh nodes. <= 0 disables it.
|
||||
@export var m2_visibility_range: float = TILE_SIZE * 3.0
|
||||
## Godot visibility range applied to instantiated WMO trees. <= 0 disables it.
|
||||
@export var wmo_visibility_range: float = TILE_SIZE * 5.0
|
||||
@export var m2_cache_dir: String = "res://data/cache/m2_glb"
|
||||
@export var wmo_cache_dir: String = "res://data/cache/wmo_tscn"
|
||||
@export var terrain_cast_shadows: bool = false
|
||||
@@ -61,6 +70,7 @@ const CHUNK_SIZE := TILE_SIZE / 16.0
|
||||
var _builder
|
||||
var _map_dir := ""
|
||||
var _abs_map_dir := ""
|
||||
var _wdt_info: Dictionary = {}
|
||||
var _available_tiles: Dictionary = {}
|
||||
var _tile_states: Dictionary = {}
|
||||
var _tile_load_queue: Array = []
|
||||
@@ -91,6 +101,8 @@ var _wmo_prototype_cache: Dictionary = {}
|
||||
var _wmo_missing_cache: Dictionary = {}
|
||||
var _m2_scene_cache: Dictionary = {}
|
||||
var _m2_missing_cache: Dictionary = {}
|
||||
var _world_wmo_root: Node3D
|
||||
var _wmo_registry: Dictionary = {}
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
@@ -110,10 +122,11 @@ func _ready() -> void:
|
||||
_terrain_root.name = "Terrain"
|
||||
add_child(_terrain_root)
|
||||
_reset_terrain_root_children()
|
||||
_ensure_world_wmo_root()
|
||||
_set_editor_owner_recursive(_terrain_root)
|
||||
|
||||
_scan_available_tiles()
|
||||
if _available_tiles.is_empty():
|
||||
if _available_tiles.is_empty() and not _wdt_has_global_wmo():
|
||||
push_error("No ADT tiles found in %s" % _map_dir)
|
||||
return
|
||||
if _can_use_baked_tile_cache():
|
||||
@@ -121,6 +134,9 @@ func _ready() -> void:
|
||||
if not DirAccess.dir_exists_absolute(baked_dir):
|
||||
push_warning("Baked terrain cache not found: %s. Runtime will fall back to raw ADT builds." % baked_dir)
|
||||
|
||||
if enable_wmo:
|
||||
_place_global_wmo()
|
||||
|
||||
if Engine.is_editor_hint():
|
||||
_tick_editor_streaming()
|
||||
elif auto_position_camera:
|
||||
@@ -185,6 +201,64 @@ func _tick_editor_streaming() -> void:
|
||||
|
||||
|
||||
func _scan_available_tiles() -> void:
|
||||
_available_tiles.clear()
|
||||
_wdt_info = {}
|
||||
|
||||
if _load_tiles_from_wdt():
|
||||
print("StreamingWorld: loaded %d ADT tiles from WDT for %s" % [_available_tiles.size(), map_name])
|
||||
return
|
||||
|
||||
push_warning("StreamingWorld: falling back to directory scan for %s (no WDT)" % map_name)
|
||||
_load_tiles_from_directory()
|
||||
print("StreamingWorld: found %d ADT tiles by directory scan for %s" % [_available_tiles.size(), map_name])
|
||||
|
||||
|
||||
func _load_tiles_from_wdt() -> bool:
|
||||
if not ClassDB.class_exists("WDTLoader"):
|
||||
return false
|
||||
|
||||
var wdt_path_abs := _abs_map_dir.path_join("%s.wdt" % map_name)
|
||||
if not FileAccess.file_exists(wdt_path_abs):
|
||||
return false
|
||||
|
||||
var loader = ClassDB.instantiate("WDTLoader")
|
||||
if loader == null:
|
||||
return false
|
||||
|
||||
var data: Dictionary = loader.call("load_wdt", wdt_path_abs)
|
||||
if data.is_empty():
|
||||
push_warning("StreamingWorld: WDTLoader returned empty data for %s" % wdt_path_abs)
|
||||
return false
|
||||
|
||||
var tiles: Array = data.get("tiles", [])
|
||||
for tile_variant in tiles:
|
||||
if not (tile_variant is Dictionary):
|
||||
continue
|
||||
var entry: Dictionary = tile_variant
|
||||
var tx: int = int(entry.get("x", -1))
|
||||
var ty: int = int(entry.get("y", -1))
|
||||
if tx < 0 or tx > 63 or ty < 0 or ty > 63:
|
||||
continue
|
||||
|
||||
var adt_file := "%s_%d_%d.adt" % [map_name, tx, ty]
|
||||
var adt_abs := _abs_map_dir.path_join(adt_file)
|
||||
# WDT flags the tile as present, but the ADT may still be missing in
|
||||
# a partial extraction. Skip so downstream code never chases a ghost.
|
||||
if not FileAccess.file_exists(adt_abs):
|
||||
continue
|
||||
|
||||
var key := _tile_key(tx, ty)
|
||||
_available_tiles[key] = adt_abs
|
||||
_tile_min.x = min(_tile_min.x, tx)
|
||||
_tile_min.y = min(_tile_min.y, ty)
|
||||
_tile_max.x = max(_tile_max.x, tx)
|
||||
_tile_max.y = max(_tile_max.y, ty)
|
||||
|
||||
_wdt_info = data
|
||||
return not _available_tiles.is_empty()
|
||||
|
||||
|
||||
func _load_tiles_from_directory() -> void:
|
||||
var dir := DirAccess.open(_map_dir)
|
||||
if dir == null:
|
||||
push_error("Cannot open map directory: %s" % _map_dir)
|
||||
@@ -210,8 +284,6 @@ func _scan_available_tiles() -> void:
|
||||
_tile_max.x = max(_tile_max.x, tx)
|
||||
_tile_max.y = max(_tile_max.y, ty)
|
||||
|
||||
print("StreamingWorld: found %d ADT tiles for %s" % [_available_tiles.size(), map_name])
|
||||
|
||||
|
||||
func _refresh_streaming_targets(force: bool) -> void:
|
||||
var camera := _get_stream_camera()
|
||||
@@ -312,6 +384,7 @@ func _apply_streaming_target(wanted_tiles: Dictionary, focus_pos: Vector3) -> vo
|
||||
else:
|
||||
state["desired_lods"] = {}
|
||||
state["desired_tile_lod"] = -1
|
||||
state = _sync_detail_assets(state, focus_pos)
|
||||
_tile_states[key] = state
|
||||
|
||||
_rebuild_chunk_queues()
|
||||
@@ -592,25 +665,12 @@ func _finalize_loaded_tile(request: Dictionary, data: Dictionary) -> void:
|
||||
if water_root:
|
||||
tile_root.add_child(water_root)
|
||||
_set_editor_owner_recursive(water_root)
|
||||
if enable_m2_assets:
|
||||
_build_tile_m2_assets(
|
||||
tile_root,
|
||||
tile_origin,
|
||||
data.get("m2_names", PackedStringArray()),
|
||||
data.get("m2_placements", []))
|
||||
if enable_m2_placeholders:
|
||||
_build_tile_m2_placeholders(
|
||||
tile_root,
|
||||
tile_origin,
|
||||
data.get("m2_names", PackedStringArray()),
|
||||
data.get("m2_placements", []))
|
||||
if enable_wmo:
|
||||
_build_tile_wmos(
|
||||
tile_root,
|
||||
tile_origin,
|
||||
data.get("wmo_names", PackedStringArray()),
|
||||
data.get("wmo_placements", []))
|
||||
|
||||
var state := {
|
||||
"key": key,
|
||||
"tx": request["tx"],
|
||||
@@ -629,11 +689,18 @@ func _finalize_loaded_tile(request: Dictionary, data: Dictionary) -> void:
|
||||
"desired_tile_lod": -1,
|
||||
"wanted": true,
|
||||
"root": tile_root,
|
||||
"m2_names": data.get("m2_names", PackedStringArray()),
|
||||
"m2_placements": data.get("m2_placements", []),
|
||||
"m2_built": false,
|
||||
"wmo_names": data.get("wmo_names", PackedStringArray()),
|
||||
"wmo_placements": data.get("wmo_placements", []),
|
||||
"wmo_refs": [],
|
||||
}
|
||||
|
||||
# Use _last_focus_pos — already set correctly for both editor and game.
|
||||
state["desired_lods"] = _compute_desired_lods(state, _last_focus_pos)
|
||||
state["desired_tile_lod"] = -1 if not state["desired_lods"].is_empty() else _compute_desired_tile_lod(state, _last_focus_pos)
|
||||
state = _sync_detail_assets(state, _last_focus_pos)
|
||||
|
||||
_tile_states[key] = state
|
||||
_rebuild_chunk_queues()
|
||||
@@ -663,27 +730,14 @@ func _finalize_loaded_baked_tile(request: Dictionary, baked_tile: Resource) -> v
|
||||
|
||||
var tile_origin: Vector3 = baked_tile.get("tile_origin")
|
||||
tile_root.position = tile_origin
|
||||
if enable_m2_assets:
|
||||
var baked_m2_names_value: Variant = baked_tile.get("m2_names")
|
||||
var baked_m2_placements_value: Variant = baked_tile.get("m2_placements")
|
||||
var baked_m2_names: PackedStringArray = baked_m2_names_value if baked_m2_names_value is PackedStringArray else PackedStringArray()
|
||||
var baked_m2_placements: Array = baked_m2_placements_value if baked_m2_placements_value is Array else []
|
||||
_build_tile_m2_assets(
|
||||
tile_root,
|
||||
tile_origin,
|
||||
baked_m2_names,
|
||||
baked_m2_placements)
|
||||
if enable_wmo:
|
||||
var baked_wmo_names_value: Variant = baked_tile.get("wmo_names")
|
||||
var baked_wmo_placements_value: Variant = baked_tile.get("wmo_placements")
|
||||
var baked_wmo_names: PackedStringArray = baked_wmo_names_value if baked_wmo_names_value is PackedStringArray else PackedStringArray()
|
||||
var baked_wmo_placements: Array = baked_wmo_placements_value if baked_wmo_placements_value is Array else []
|
||||
_build_tile_wmos(
|
||||
tile_root,
|
||||
tile_origin,
|
||||
baked_wmo_names,
|
||||
baked_wmo_placements)
|
||||
|
||||
var state := {
|
||||
"key": key,
|
||||
"tx": request["tx"],
|
||||
@@ -702,9 +756,16 @@ func _finalize_loaded_baked_tile(request: Dictionary, baked_tile: Resource) -> v
|
||||
"desired_tile_lod": -1,
|
||||
"wanted": true,
|
||||
"root": tile_root,
|
||||
"m2_names": baked_m2_names,
|
||||
"m2_placements": baked_m2_placements,
|
||||
"m2_built": false,
|
||||
"wmo_names": baked_wmo_names,
|
||||
"wmo_placements": baked_wmo_placements,
|
||||
"wmo_refs": [],
|
||||
}
|
||||
|
||||
state["desired_tile_lod"] = _compute_desired_tile_lod(state, _last_focus_pos)
|
||||
state = _sync_detail_assets(state, _last_focus_pos)
|
||||
_tile_states[key] = state
|
||||
_rebuild_chunk_queues()
|
||||
if debug_streaming:
|
||||
@@ -910,6 +971,7 @@ func _release_tile(key: String) -> void:
|
||||
return
|
||||
|
||||
var state: Dictionary = _tile_states[key]
|
||||
_unregister_tile_wmos(state)
|
||||
var tile_lod_rid: RID = state.get("tile_lod_rid", RID())
|
||||
if tile_lod_rid.is_valid():
|
||||
_free_render_instance(tile_lod_rid)
|
||||
@@ -949,6 +1011,10 @@ func _clear_streamed_world() -> void:
|
||||
|
||||
_tile_states.clear()
|
||||
_clear_tile_mesh_cache()
|
||||
_clear_wmo_registry()
|
||||
_ensure_world_wmo_root()
|
||||
if enable_wmo:
|
||||
_place_global_wmo()
|
||||
|
||||
|
||||
func _reset_terrain_root_children() -> void:
|
||||
@@ -1121,16 +1187,110 @@ func _tile_mesh_cache_key(tile_key: String, lod: int) -> String:
|
||||
return "%s|%d" % [tile_key, lod]
|
||||
|
||||
|
||||
func _build_tile_wmos(tile_root: Node3D, tile_origin: Vector3, wmo_names: PackedStringArray, wmo_placements: Array) -> void:
|
||||
if wmo_names.is_empty() or wmo_placements.is_empty():
|
||||
func _ensure_world_wmo_root() -> void:
|
||||
if _world_wmo_root != null and is_instance_valid(_world_wmo_root):
|
||||
return
|
||||
_world_wmo_root = Node3D.new()
|
||||
_world_wmo_root.name = "WMOs"
|
||||
_terrain_root.add_child(_world_wmo_root)
|
||||
_set_editor_owner_recursive(_world_wmo_root)
|
||||
|
||||
|
||||
func _wdt_has_global_wmo() -> bool:
|
||||
return bool(_wdt_info.get("has_global_wmo", false)) and _wdt_info.has("global_wmo_name")
|
||||
|
||||
|
||||
func _place_global_wmo() -> void:
|
||||
if not _wdt_has_global_wmo():
|
||||
return
|
||||
if _wmo_registry.has("global"):
|
||||
return
|
||||
|
||||
var wmo_root := Node3D.new()
|
||||
wmo_root.name = "WMOs"
|
||||
tile_root.add_child(wmo_root)
|
||||
_set_editor_owner_recursive(wmo_root)
|
||||
_ensure_world_wmo_root()
|
||||
|
||||
for placement_variant in wmo_placements:
|
||||
var rel_path := String(_wdt_info.get("global_wmo_name", "")).replace("\\", "/")
|
||||
if rel_path.is_empty():
|
||||
return
|
||||
|
||||
var placement: Dictionary = _wdt_info.get("global_wmo_placement", {})
|
||||
if placement.is_empty():
|
||||
placement = {
|
||||
"pos": Vector3.ZERO,
|
||||
"rot": Vector3.ZERO,
|
||||
"scale": 1.0,
|
||||
}
|
||||
|
||||
var instance := _instantiate_wmo_world(rel_path, placement)
|
||||
if instance == null:
|
||||
push_warning("StreamingWorld: failed to load global WMO: %s" % rel_path)
|
||||
return
|
||||
|
||||
_world_wmo_root.add_child(instance)
|
||||
_apply_visibility_range_recursive(instance, wmo_visibility_range)
|
||||
_set_editor_owner_recursive(instance)
|
||||
_wmo_registry["global"] = {
|
||||
"node": instance,
|
||||
"refs": {"__global__": true},
|
||||
}
|
||||
|
||||
|
||||
func _sync_detail_assets(state: Dictionary, focus_pos: Vector3) -> Dictionary:
|
||||
if not bool(state.get("wanted", false)):
|
||||
_disable_tile_detail_assets(state)
|
||||
return state
|
||||
|
||||
var tile_root: Node = state.get("root", null)
|
||||
if tile_root == null or not is_instance_valid(tile_root):
|
||||
return state
|
||||
|
||||
if enable_wmo and _tile_within_detail_radius(state, focus_pos, wmo_tile_radius):
|
||||
var refs: Array = state.get("wmo_refs", [])
|
||||
if refs.is_empty():
|
||||
_register_tile_wmos(
|
||||
state,
|
||||
state.get("wmo_names", PackedStringArray()),
|
||||
state.get("wmo_placements", []))
|
||||
else:
|
||||
_unregister_tile_wmos(state)
|
||||
|
||||
if enable_m2_assets and _tile_within_detail_radius(state, focus_pos, m2_tile_radius):
|
||||
if not bool(state.get("m2_built", false)):
|
||||
state["m2_built"] = _build_tile_m2_assets(
|
||||
tile_root as Node3D,
|
||||
state.get("origin", Vector3.ZERO),
|
||||
state.get("m2_names", PackedStringArray()),
|
||||
state.get("m2_placements", []))
|
||||
else:
|
||||
_remove_tile_m2_assets(state)
|
||||
state["m2_built"] = false
|
||||
|
||||
return state
|
||||
|
||||
|
||||
func _disable_tile_detail_assets(state: Dictionary) -> void:
|
||||
_unregister_tile_wmos(state)
|
||||
_remove_tile_m2_assets(state)
|
||||
state["m2_built"] = false
|
||||
|
||||
|
||||
func _tile_within_detail_radius(state: Dictionary, focus_pos: Vector3, radius_tiles: int) -> bool:
|
||||
if radius_tiles < 0:
|
||||
return true
|
||||
var radius_sq := (float(radius_tiles) * TILE_SIZE) ** 2
|
||||
return _tile_dist_sq(state, focus_pos) <= radius_sq
|
||||
|
||||
|
||||
func _register_tile_wmos(state: Dictionary, wmo_names: PackedStringArray, wmo_placements: Array) -> void:
|
||||
var refs: Array = state.get("wmo_refs", [])
|
||||
if wmo_names.is_empty() or wmo_placements.is_empty():
|
||||
state["wmo_refs"] = refs
|
||||
return
|
||||
|
||||
_ensure_world_wmo_root()
|
||||
var tile_key: String = state["key"]
|
||||
|
||||
for i in wmo_placements.size():
|
||||
var placement_variant = wmo_placements[i]
|
||||
if not (placement_variant is Dictionary):
|
||||
continue
|
||||
var placement: Dictionary = placement_variant
|
||||
@@ -1138,17 +1298,83 @@ func _build_tile_wmos(tile_root: Node3D, tile_origin: Vector3, wmo_names: Packed
|
||||
if name_id < 0 or name_id >= wmo_names.size():
|
||||
continue
|
||||
|
||||
var unique_key := _wmo_unique_key(placement, tile_key, i)
|
||||
|
||||
if _wmo_registry.has(unique_key):
|
||||
var entry: Dictionary = _wmo_registry[unique_key]
|
||||
var entry_refs: Dictionary = entry["refs"]
|
||||
entry_refs[tile_key] = true
|
||||
refs.append(unique_key)
|
||||
continue
|
||||
|
||||
var rel_path: String = str(wmo_names[name_id]).replace("\\", "/")
|
||||
var instance := _instantiate_wmo(rel_path, placement, tile_origin)
|
||||
var instance := _instantiate_wmo_world(rel_path, placement)
|
||||
if instance == null:
|
||||
continue
|
||||
wmo_root.add_child(instance)
|
||||
|
||||
_world_wmo_root.add_child(instance)
|
||||
_apply_visibility_range_recursive(instance, wmo_visibility_range)
|
||||
_set_editor_owner_recursive(instance)
|
||||
_wmo_registry[unique_key] = {
|
||||
"node": instance,
|
||||
"refs": {tile_key: true},
|
||||
}
|
||||
refs.append(unique_key)
|
||||
|
||||
state["wmo_refs"] = refs
|
||||
|
||||
|
||||
func _build_tile_m2_assets(tile_root: Node3D, tile_origin: Vector3, m2_names: PackedStringArray, m2_placements: Array) -> void:
|
||||
if m2_names.is_empty() or m2_placements.is_empty():
|
||||
func _unregister_tile_wmos(state: Dictionary) -> void:
|
||||
var refs: Array = state.get("wmo_refs", [])
|
||||
if refs.is_empty():
|
||||
return
|
||||
var tile_key: String = state["key"]
|
||||
|
||||
for unique_key in refs:
|
||||
if not _wmo_registry.has(unique_key):
|
||||
continue
|
||||
var entry: Dictionary = _wmo_registry[unique_key]
|
||||
var entry_refs: Dictionary = entry["refs"]
|
||||
entry_refs.erase(tile_key)
|
||||
if entry_refs.is_empty():
|
||||
var node: Node = entry.get("node", null)
|
||||
if node:
|
||||
node.queue_free()
|
||||
_wmo_registry.erase(unique_key)
|
||||
|
||||
state["wmo_refs"] = []
|
||||
|
||||
|
||||
func _wmo_unique_key(placement: Dictionary, tile_key: String, idx: int) -> String:
|
||||
var uid: int = int(placement.get("unique_id", -1))
|
||||
# uniqueId is always set in vanilla/3.3.5a MODF data, but older baked caches
|
||||
# predating the ADTLoader unique_id field will miss it. Fall back to a
|
||||
# per-tile synthetic key — no cross-tile dedup, but also no collisions.
|
||||
if uid > 0:
|
||||
return "uid:%d" % uid
|
||||
return "tile:%s:%d" % [tile_key, idx]
|
||||
|
||||
|
||||
func _clear_wmo_registry() -> void:
|
||||
for entry_variant in _wmo_registry.values():
|
||||
if not (entry_variant is Dictionary):
|
||||
continue
|
||||
var entry: Dictionary = entry_variant
|
||||
var node: Node = entry.get("node", null)
|
||||
if node and is_instance_valid(node):
|
||||
node.queue_free()
|
||||
_wmo_registry.clear()
|
||||
if _world_wmo_root and is_instance_valid(_world_wmo_root):
|
||||
_world_wmo_root.queue_free()
|
||||
_world_wmo_root = null
|
||||
|
||||
|
||||
func _build_tile_m2_assets(tile_root: Node3D, tile_origin: Vector3, m2_names: PackedStringArray, m2_placements: Array) -> bool:
|
||||
if m2_names.is_empty() or m2_placements.is_empty():
|
||||
return false
|
||||
var existing_m2_root := tile_root.get_node_or_null("M2s")
|
||||
if existing_m2_root != null:
|
||||
return not existing_m2_root.is_queued_for_deletion()
|
||||
|
||||
var groups: Dictionary = {}
|
||||
for placement_variant in m2_placements:
|
||||
@@ -1169,14 +1395,15 @@ func _build_tile_m2_assets(tile_root: Node3D, tile_origin: Vector3, m2_names: Pa
|
||||
var pos: Vector3 = placement.get("pos", Vector3.ZERO) - tile_origin
|
||||
var rot: Vector3 = placement.get("rot", Vector3.ZERO)
|
||||
var scale_value: float = float(placement.get("scale", 1.0))
|
||||
var xform := Transform3D(Basis.from_euler(rot).scaled(Vector3.ONE * maxf(scale_value, 0.0001)), pos)
|
||||
var basis := Basis.from_euler(rot) * Basis(Vector3.UP, M2_RIGHT_YAW_OFFSET)
|
||||
var xform := Transform3D(basis.scaled(Vector3.ONE * maxf(scale_value, 0.0001)), pos)
|
||||
|
||||
if not groups.has(normalized):
|
||||
groups[normalized] = []
|
||||
(groups[normalized] as Array).append(xform)
|
||||
|
||||
if groups.is_empty():
|
||||
return
|
||||
return false
|
||||
|
||||
var m2_root := Node3D.new()
|
||||
m2_root.name = "M2s"
|
||||
@@ -1204,11 +1431,36 @@ func _build_tile_m2_assets(tile_root: Node3D, tile_origin: Vector3, m2_names: Pa
|
||||
if m2_cast_shadows
|
||||
else GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
|
||||
)
|
||||
if m2_visibility_range > 0.0:
|
||||
mmi.visibility_range_end = m2_visibility_range
|
||||
mmi.visibility_range_end_margin = CHUNK_SIZE
|
||||
m2_root.add_child(mmi)
|
||||
_set_editor_owner_recursive(mmi)
|
||||
|
||||
if m2_root.get_child_count() == 0:
|
||||
m2_root.queue_free()
|
||||
return false
|
||||
return true
|
||||
|
||||
|
||||
func _remove_tile_m2_assets(state: Dictionary) -> void:
|
||||
var tile_root: Node = state.get("root", null)
|
||||
if tile_root == null or not is_instance_valid(tile_root):
|
||||
return
|
||||
var m2_root := tile_root.get_node_or_null("M2s")
|
||||
if m2_root:
|
||||
m2_root.queue_free()
|
||||
|
||||
|
||||
func _apply_visibility_range_recursive(node: Node, range_end: float) -> void:
|
||||
if range_end <= 0.0:
|
||||
return
|
||||
if node is GeometryInstance3D:
|
||||
var geometry := node as GeometryInstance3D
|
||||
geometry.visibility_range_end = range_end
|
||||
geometry.visibility_range_end_margin = CHUNK_SIZE
|
||||
for child in node.get_children():
|
||||
_apply_visibility_range_recursive(child, range_end)
|
||||
|
||||
|
||||
func _get_or_load_m2_mesh(rel_path: String) -> Mesh:
|
||||
@@ -1317,23 +1569,23 @@ func _instantiate_m2(rel_path: String, placement: Dictionary, tile_origin: Vecto
|
||||
|
||||
node.name = rel_path.get_file().get_basename()
|
||||
node.position = placement.get("pos", Vector3.ZERO) - tile_origin
|
||||
node.rotation = placement.get("rot", Vector3.ZERO)
|
||||
node.rotation = placement.get("rot", Vector3.ZERO) + Vector3(0.0, M2_RIGHT_YAW_OFFSET, 0.0)
|
||||
var scale_value: float = float(placement.get("scale", 1.0))
|
||||
node.scale = Vector3.ONE * scale_value
|
||||
return node
|
||||
|
||||
|
||||
func _instantiate_wmo(rel_path: String, placement: Dictionary, tile_origin: Vector3) -> Node3D:
|
||||
func _instantiate_wmo_world(rel_path: String, placement: Dictionary) -> Node3D:
|
||||
var prototype := _get_or_load_wmo_prototype(rel_path)
|
||||
if prototype == null:
|
||||
return null
|
||||
|
||||
var instance := prototype.duplicate()
|
||||
instance.name = rel_path.get_file().get_basename()
|
||||
instance.position = placement.get("pos", Vector3.ZERO) - tile_origin
|
||||
var rot: Vector3 = placement.get("rot", Vector3.ZERO)
|
||||
rot.y += PI
|
||||
instance.rotation = rot
|
||||
# WMOs are parented to _terrain_root, whose position absorbs the editor
|
||||
# offset — use world-space placement.pos directly (no tile_origin subtraction).
|
||||
instance.position = placement.get("pos", Vector3.ZERO)
|
||||
instance.rotation = placement.get("rot", Vector3.ZERO)
|
||||
var scale_value: float = float(placement.get("scale", 1.0))
|
||||
instance.scale = Vector3.ONE * scale_value
|
||||
return instance
|
||||
|
||||
@@ -1,11 +1,29 @@
|
||||
extends SceneTree
|
||||
|
||||
## Usage:
|
||||
## godot --headless --path <project> --script res://src/tools/bake_adt_terrain_cache.gd -- \
|
||||
## --map Azeroth --jobs 4 --force
|
||||
##
|
||||
## `--jobs N` starts N separate Godot worker processes. Process isolation keeps
|
||||
## ResourceSaver/Image/loader state independent while using multiple CPU cores.
|
||||
|
||||
const ADT_BUILDER_SCRIPT := preload("res://addons/mpq_extractor/loaders/adt_builder.gd")
|
||||
const BAKED_TILE_SCRIPT := preload("res://src/resources/baked_adt_tile.gd")
|
||||
const PROGRESS_DIR := "res://data/cache/bake_progress"
|
||||
const PROGRESS_REPORT_INTERVAL_MS := 2000
|
||||
|
||||
var _builder
|
||||
var _loader
|
||||
var _image_cache: Dictionary = {}
|
||||
var _progress_path := ""
|
||||
var _progress_worker_index := -1
|
||||
var _progress_worker_count := 1
|
||||
var _progress_total := 0
|
||||
var _progress_processed := 0
|
||||
var _progress_baked := 0
|
||||
var _progress_skipped := 0
|
||||
var _progress_failed := 0
|
||||
var _progress_last_tile := ""
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
@@ -13,13 +31,13 @@ func _initialize() -> void:
|
||||
var map_name := _get_arg_value(args, "--map", "Azeroth")
|
||||
var extracted_dir := _normalize_res_path(_get_arg_value(args, "--extracted", "res://data/extracted"))
|
||||
var output_dir := _normalize_res_path(_get_arg_value(args, "--output", "res://data/cache/baked_terrain_v2"))
|
||||
var legacy_texture_size = _get_optional_int_arg(args, "--texture-size")
|
||||
var legacy_texture_size: Variant = _get_optional_int_arg(args, "--texture-size")
|
||||
var full_texture_size := maxi(
|
||||
256,
|
||||
_get_arg_value(
|
||||
args,
|
||||
"--full-texture-size",
|
||||
str(legacy_texture_size if legacy_texture_size != null else 2048)
|
||||
str(legacy_texture_size if legacy_texture_size != null else 4196)
|
||||
).to_int()
|
||||
)
|
||||
var coarse_texture_size := maxi(
|
||||
@@ -32,8 +50,15 @@ func _initialize() -> void:
|
||||
)
|
||||
var tile_x: Variant = _get_optional_int_arg(args, "--tile-x")
|
||||
var tile_y: Variant = _get_optional_int_arg(args, "--tile-y")
|
||||
var jobs := maxi(1, _get_arg_value(args, "--jobs", "1").to_int())
|
||||
var worker_index: Variant = _get_optional_int_arg(args, "--worker-index")
|
||||
var worker_count := maxi(1, _get_arg_value(args, "--worker-count", "1").to_int())
|
||||
var force := args.has("--force")
|
||||
|
||||
if jobs > 1 and worker_index == null:
|
||||
_run_parallel_controller(args, jobs)
|
||||
return
|
||||
|
||||
_builder = ADT_BUILDER_SCRIPT.new()
|
||||
if not ClassDB.class_exists("ADTLoader"):
|
||||
push_error("ADTLoader not found. Rebuild GDExtension first.")
|
||||
@@ -59,6 +84,9 @@ func _initialize() -> void:
|
||||
var skipped := 0
|
||||
var failed := 0
|
||||
var started_ms := Time.get_ticks_msec()
|
||||
var tile_index := 0
|
||||
var worker_total := _count_worker_tiles(dir.get_files(), map_name, tile_x, tile_y, worker_index, worker_count)
|
||||
_progress_setup(worker_index, worker_count, worker_total)
|
||||
|
||||
for file_name in dir.get_files():
|
||||
if not file_name.ends_with(".adt"):
|
||||
@@ -77,6 +105,11 @@ func _initialize() -> void:
|
||||
continue
|
||||
if tile_y != null and ty != tile_y:
|
||||
continue
|
||||
if worker_index != null:
|
||||
if tile_index % worker_count != int(worker_index):
|
||||
tile_index += 1
|
||||
continue
|
||||
tile_index += 1
|
||||
var source_res_path := map_dir.path_join(file_name)
|
||||
var source_abs_path := ProjectSettings.globalize_path(source_res_path)
|
||||
var output_res_path := output_dir.path_join(map_name).path_join("%s_%d_%d.res" % [map_name, tx, ty])
|
||||
@@ -84,12 +117,14 @@ func _initialize() -> void:
|
||||
|
||||
if not force and FileAccess.file_exists(output_abs_path):
|
||||
skipped += 1
|
||||
_progress_update(tx, ty, baked, skipped, failed)
|
||||
continue
|
||||
|
||||
var data: Dictionary = _loader.call("load_adt", source_abs_path)
|
||||
if data.is_empty() or not data.has("chunks"):
|
||||
push_warning("Bake failed to parse ADT: %s" % source_res_path)
|
||||
failed += 1
|
||||
_progress_update(tx, ty, baked, skipped, failed)
|
||||
continue
|
||||
|
||||
var full_payload: Dictionary = _builder.build_baked_tile_render_payload(
|
||||
@@ -108,6 +143,7 @@ func _initialize() -> void:
|
||||
if full_payload.is_empty():
|
||||
push_warning("Bake produced empty full mesh: %s" % source_res_path)
|
||||
failed += 1
|
||||
_progress_update(tx, ty, baked, skipped, failed)
|
||||
continue
|
||||
|
||||
var baked_tile: Resource = BAKED_TILE_SCRIPT.new()
|
||||
@@ -129,18 +165,223 @@ func _initialize() -> void:
|
||||
if save_err != OK:
|
||||
push_warning("Failed to save baked tile: %s (err=%d)" % [output_res_path, save_err])
|
||||
failed += 1
|
||||
_progress_update(tx, ty, baked, skipped, failed)
|
||||
continue
|
||||
|
||||
baked += 1
|
||||
_progress_update(tx, ty, baked, skipped, failed)
|
||||
if baked % 25 == 0:
|
||||
print("Baked %d tiles..." % baked)
|
||||
|
||||
var elapsed_ms := Time.get_ticks_msec() - started_ms
|
||||
print("Bake finished. baked=%d skipped=%d failed=%d elapsed=%.2fs" % [
|
||||
baked, skipped, failed, float(elapsed_ms) / 1000.0])
|
||||
_progress_finish(baked, skipped, failed)
|
||||
quit(0 if failed == 0 else 2)
|
||||
|
||||
|
||||
func _run_parallel_controller(args: PackedStringArray, jobs: int) -> void:
|
||||
var started_ms := Time.get_ticks_msec()
|
||||
var last_report_ms := 0
|
||||
var threads: Array[Thread] = []
|
||||
_prepare_progress_dir()
|
||||
|
||||
print("Starting parallel ADT bake: jobs=%d" % jobs)
|
||||
for worker_index in jobs:
|
||||
var thread := Thread.new()
|
||||
var err := thread.start(_run_worker_process.bind(args, worker_index, jobs))
|
||||
if err != OK:
|
||||
push_error("Failed to start bake worker thread %d (err=%d)" % [worker_index, err])
|
||||
quit(1)
|
||||
return
|
||||
threads.append(thread)
|
||||
|
||||
var finished := false
|
||||
while not finished:
|
||||
OS.delay_msec(250)
|
||||
var now := Time.get_ticks_msec()
|
||||
if now - last_report_ms >= PROGRESS_REPORT_INTERVAL_MS:
|
||||
_print_parallel_progress(jobs, started_ms)
|
||||
last_report_ms = now
|
||||
finished = true
|
||||
for thread in threads:
|
||||
if thread.is_alive():
|
||||
finished = false
|
||||
break
|
||||
|
||||
var failed := 0
|
||||
for thread in threads:
|
||||
var exit_code: int = int(thread.wait_to_finish())
|
||||
if exit_code != 0:
|
||||
failed += 1
|
||||
|
||||
var elapsed_ms := Time.get_ticks_msec() - started_ms
|
||||
_print_parallel_progress(jobs, started_ms)
|
||||
print("Parallel bake finished. jobs=%d failed_workers=%d elapsed=%.2fs" % [
|
||||
jobs, failed, float(elapsed_ms) / 1000.0])
|
||||
quit(0 if failed == 0 else 2)
|
||||
|
||||
|
||||
func _run_worker_process(args: PackedStringArray, worker_index: int, worker_count: int) -> int:
|
||||
var worker_args := PackedStringArray([
|
||||
"--headless",
|
||||
"--path",
|
||||
ProjectSettings.globalize_path("res://"),
|
||||
"--script",
|
||||
"res://src/tools/bake_adt_terrain_cache.gd",
|
||||
"--",
|
||||
])
|
||||
|
||||
var skip_next := false
|
||||
for i in args.size():
|
||||
if skip_next:
|
||||
skip_next = false
|
||||
continue
|
||||
var arg := args[i]
|
||||
if arg == "--jobs":
|
||||
skip_next = i + 1 < args.size()
|
||||
continue
|
||||
if arg == "--worker-index" or arg == "--worker-count":
|
||||
skip_next = i + 1 < args.size()
|
||||
continue
|
||||
worker_args.append(arg)
|
||||
|
||||
worker_args.append("--jobs")
|
||||
worker_args.append("1")
|
||||
worker_args.append("--worker-index")
|
||||
worker_args.append(str(worker_index))
|
||||
worker_args.append("--worker-count")
|
||||
worker_args.append(str(worker_count))
|
||||
|
||||
print("Worker %d/%d started" % [worker_index + 1, worker_count])
|
||||
var exit_code := OS.execute(OS.get_executable_path(), worker_args)
|
||||
print("Worker %d/%d finished with exit=%d" % [worker_index + 1, worker_count, exit_code])
|
||||
return exit_code
|
||||
|
||||
|
||||
func _prepare_progress_dir() -> void:
|
||||
DirAccess.make_dir_recursive_absolute(ProjectSettings.globalize_path(PROGRESS_DIR))
|
||||
var dir := DirAccess.open(PROGRESS_DIR)
|
||||
if dir == null:
|
||||
return
|
||||
for file_name in dir.get_files():
|
||||
if file_name.begins_with("adt_worker_") and file_name.ends_with(".json"):
|
||||
dir.remove(file_name)
|
||||
|
||||
|
||||
func _progress_setup(worker_index: Variant, worker_count: int, total: int) -> void:
|
||||
_progress_worker_index = int(worker_index) if worker_index != null else 0
|
||||
_progress_worker_count = worker_count
|
||||
_progress_total = total
|
||||
_progress_processed = 0
|
||||
_progress_baked = 0
|
||||
_progress_skipped = 0
|
||||
_progress_failed = 0
|
||||
_progress_last_tile = ""
|
||||
_progress_path = PROGRESS_DIR.path_join("adt_worker_%d.json" % _progress_worker_index)
|
||||
_progress_write(false)
|
||||
|
||||
|
||||
func _progress_update(tx: int, ty: int, baked: int, skipped: int, failed: int) -> void:
|
||||
_progress_processed += 1
|
||||
_progress_baked = baked
|
||||
_progress_skipped = skipped
|
||||
_progress_failed = failed
|
||||
_progress_last_tile = "%d_%d" % [tx, ty]
|
||||
_progress_write(false)
|
||||
|
||||
|
||||
func _progress_finish(baked: int, skipped: int, failed: int) -> void:
|
||||
_progress_baked = baked
|
||||
_progress_skipped = skipped
|
||||
_progress_failed = failed
|
||||
_progress_processed = _progress_total
|
||||
_progress_write(true)
|
||||
|
||||
|
||||
func _progress_write(done: bool) -> void:
|
||||
if _progress_path.is_empty():
|
||||
return
|
||||
var file := FileAccess.open(_progress_path, FileAccess.WRITE)
|
||||
if file == null:
|
||||
return
|
||||
file.store_string(JSON.stringify({
|
||||
"worker": _progress_worker_index,
|
||||
"workers": _progress_worker_count,
|
||||
"total": _progress_total,
|
||||
"processed": _progress_processed,
|
||||
"baked": _progress_baked,
|
||||
"skipped": _progress_skipped,
|
||||
"failed": _progress_failed,
|
||||
"last_tile": _progress_last_tile,
|
||||
"done": done,
|
||||
}))
|
||||
|
||||
|
||||
func _print_parallel_progress(jobs: int, started_ms: int) -> void:
|
||||
var now := Time.get_ticks_msec()
|
||||
var total := 0
|
||||
var processed := 0
|
||||
var baked := 0
|
||||
var skipped := 0
|
||||
var failed := 0
|
||||
var done := 0
|
||||
for worker_index in jobs:
|
||||
var path := PROGRESS_DIR.path_join("adt_worker_%d.json" % worker_index)
|
||||
if not FileAccess.file_exists(path):
|
||||
continue
|
||||
var file := FileAccess.open(path, FileAccess.READ)
|
||||
if file == null:
|
||||
continue
|
||||
var parsed: Variant = JSON.parse_string(file.get_as_text())
|
||||
if not (parsed is Dictionary):
|
||||
continue
|
||||
var row: Dictionary = parsed
|
||||
total += int(row.get("total", 0))
|
||||
processed += int(row.get("processed", 0))
|
||||
baked += int(row.get("baked", 0))
|
||||
skipped += int(row.get("skipped", 0))
|
||||
failed += int(row.get("failed", 0))
|
||||
if bool(row.get("done", false)):
|
||||
done += 1
|
||||
|
||||
var elapsed := maxf(0.001, float(now - started_ms) / 1000.0)
|
||||
var rate := float(processed) / elapsed
|
||||
print("Progress: %d/%d tiles, baked=%d skipped=%d failed=%d, workers done=%d/%d, %.2f tiles/s" % [
|
||||
processed, total, baked, skipped, failed, done, jobs, rate])
|
||||
|
||||
|
||||
func _count_worker_tiles(
|
||||
files: PackedStringArray,
|
||||
map_name: String,
|
||||
tile_x: Variant,
|
||||
tile_y: Variant,
|
||||
worker_index: Variant,
|
||||
worker_count: int) -> int:
|
||||
var count := 0
|
||||
var tile_index := 0
|
||||
for file_name in files:
|
||||
if not file_name.ends_with(".adt"):
|
||||
continue
|
||||
var parts := file_name.trim_suffix(".adt").split("_")
|
||||
if parts.size() != 3 or parts[0] != map_name:
|
||||
continue
|
||||
if not parts[1].is_valid_int() or not parts[2].is_valid_int():
|
||||
continue
|
||||
var tx := parts[1].to_int()
|
||||
var ty := parts[2].to_int()
|
||||
if tile_x != null and tx != tile_x:
|
||||
continue
|
||||
if tile_y != null and ty != tile_y:
|
||||
continue
|
||||
if worker_index != null and tile_index % worker_count != int(worker_index):
|
||||
tile_index += 1
|
||||
continue
|
||||
tile_index += 1
|
||||
count += 1
|
||||
return count
|
||||
|
||||
|
||||
func _get_arg_value(args: PackedStringArray, name: String, default_value: String) -> String:
|
||||
var index := args.find(name)
|
||||
if index >= 0 and index + 1 < args.size():
|
||||
|
||||
@@ -81,6 +81,7 @@ func _initialize() -> void:
|
||||
|
||||
var elapsed := float(Time.get_ticks_msec() - total_start) / 1000.0
|
||||
print("\nAll done in %.1fs." % elapsed)
|
||||
WMO_BUILDER_SCRIPT.clear_caches()
|
||||
quit(0)
|
||||
|
||||
|
||||
|
||||
@@ -104,6 +104,7 @@ func _initialize() -> void:
|
||||
print("[%d/%d] baked=%d skipped=%d failed=%d" % [i, total, baked, skipped, failed])
|
||||
|
||||
print("Done. baked=%d skipped=%d failed=%d" % [baked, skipped, failed])
|
||||
WMO_BUILDER_SCRIPT.clear_caches()
|
||||
quit(0)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user