WoW Model Viewer
Your premiere tool for viewing, equipping and animating World of Warcraft models.
Loading...
Searching...
No Matches
ModelLoader.cpp
Go to the documentation of this file.
1#include "ModelLoader.h"
2#include "AppState.h"
3
4#include <algorithm>
5#include <cstring>
6#include <format>
7#include <map>
8#include <set>
9#include <string>
10#include <vector>
11
12#include "Logger.h"
13#include "Game.h"
14#include "WoWModel.h"
15#include "Attachment.h"
16#include "WoWItem.h"
17#include "CharDetails.h"
18#include "RaceInfos.h"
19#include "TextureManager.h"
20#include "database.h"
21#include "DB2Table.h"
22#include "string_utils.h"
23
24namespace ModelLoader
25{
26
27// ---- Utility --------------------------------------------------------------
28
29std::string wstringToString(const std::wstring& ws)
30{
31 std::string s;
32 s.reserve(ws.size());
33 for (wchar_t c : ws)
34 s.push_back(static_cast<char>(c & 0x7F));
35 return s;
36}
37
39{
40 if (!app.scene.root) return nullptr;
41 auto* att = app.scene.root->children.empty() ? nullptr : app.scene.root->children[0];
42 return att ? dynamic_cast<WoWModel*>(att->model()) : nullptr;
43}
44
45void resetCameraToModel(OrbitCamera& camera, const WoWModel* model, float fov)
46{
47 if (!model)
48 {
49 camera.reset();
50 return;
51 }
52
53 float zMin = 0.0f;
54 float zMax = 0.0f;
55 for (const auto& v : model->origVertices)
56 {
57 if (v.pos.z < zMin) zMin = v.pos.z;
58 if (v.pos.z > zMax) zMax = v.pos.z;
59 }
60 camera.resetFromBounds(zMin, zMax, fov);
61}
62
63void applySkin(WoWModel* model, int skinIndex, AppState& app)
64{
65 if (!model || skinIndex < 0 || skinIndex >= static_cast<int>(app.anim.skinEntries.size()))
66 return;
67
68 const auto& skin = app.anim.skinEntries[skinIndex];
69 model->setCreatureGeosetData(skin.creatureGeosetData);
70 for (size_t i = 0; i < skin.count; ++i)
71 {
72 if (skin.tex[i])
73 model->updateTextureList(skin.tex[i], skin.base + static_cast<int>(i));
74 }
75 app.anim.selectedSkin = skinIndex;
76}
77
78// ---- Animation control init -----------------------------------------------
79
81{
82 app.anim.animEntries.clear();
83 app.anim.skinEntries.clear();
84 app.anim.selectedAnimCombo = 0;
85 app.anim.selectedSkin = -1;
86 app.anim.blpSkin[0] = app.anim.blpSkin[1] = app.anim.blpSkin[2] = -1;
87 app.anim.animSpeed = 1.0f;
89 app.anim.selectedMouthAnim = -1;
90 app.anim.mouthSpeed = 1.0f;
91 app.anim.lockAnims = true;
92 app.anim.loopCount = 0;
93
94 if (!model || !model->animated || model->anims.empty())
95 return;
96
97 auto animsMap = model->getAnimsMap();
98 int standIndex = -1;
99
100 for (size_t i = 0; i < model->anims.size(); ++i)
101 {
102 AnimEntry e;
103 auto it = animsMap.find(model->anims[i].animID);
104 if (it != animsMap.end())
105 e.label = wstringToString(it->second) + " [" + std::to_string(i) + "]";
106 else
107 e.label = "Anim " + std::to_string(model->anims[i].animID) + " [" + std::to_string(i) + "]";
108 e.animIndex = static_cast<int>(i);
109 app.anim.animEntries.push_back(e);
110
111 if (model->anims[i].animID == 0 && standIndex < 0)
112 standIndex = static_cast<int>(app.anim.animEntries.size()) - 1;
113 }
114
115 if (standIndex >= 0)
116 app.anim.selectedAnimCombo = standIndex;
117
118 int useAnim = (standIndex >= 0) ? app.anim.animEntries[standIndex].animIndex : 0;
119 model->currentAnim = useAnim;
120 model->animManager->SetAnim(0, useAnim, 0);
121 model->animManager->SetSpeed(1.0f);
122 model->animManager->Play();
123
124 // Build skin list for creatures / items
125 const std::string fn = model->itemName();
126 bool isCreature = (fn.size() >= 8 && (fn.substr(0, 8) == "creature" || fn.substr(0, 8) == "Creature"));
127 bool isItem = (fn.size() >= 4 && (fn.substr(0, 4) == "item" || fn.substr(0, 4) == "Item"));
128
129 if (isCreature)
130 {
131 const auto* cdiTable = WOWDB.getTable("CreatureDisplayInfo");
132 const auto* cmdTable = WOWDB.getTable("CreatureModelData");
133 const auto* geoTable = WOWDB.getTable("CreatureDisplayInfoGeosetData");
134 const int targetFDID = model->gamefile->fileDataId();
135
136 if (cdiTable && cmdTable && geoTable)
137 for (const auto& cdiRow : *cdiTable)
138 {
139 auto cmdRow = cmdTable->getRow(cdiRow.getUInt("ModelID"));
140 if (!cmdRow || static_cast<int>(cmdRow.getUInt("FileDataID")) != targetFDID)
141 continue;
142
143 SkinEntry se;
144 size_t cnt = 0;
145 uint32_t texFDIDs[3] = {
146 cdiRow.getUInt("TextureVariationFileDataID1"),
147 cdiRow.getUInt("TextureVariationFileDataID2"),
148 cdiRow.getUInt("TextureVariationFileDataID3")
149 };
150 for (size_t s = 0; s < 3; ++s)
151 {
152 if (texFDIDs[s] != 0)
153 {
154 se.tex[s] = GAMEDIRECTORY.getFile(texFDIDs[s]);
155 if (se.tex[s]) ++cnt;
156 }
157 }
158 if (cnt == 0) continue;
160 se.count = cnt;
161
162 uint32_t cdi = cdiRow.recordID();
163 for (const auto& geoRow : *geoTable)
164 {
165 if (geoRow.getUInt("CreatureDisplayInfoID") != cdi)
166 continue;
167 int geoType = 100 * (static_cast<int>(geoRow.getUInt("GeosetIndex")) + 1);
168 int geoId = static_cast<int>(geoRow.getUInt("GeosetValue"));
169 if (geoId > 0) se.creatureGeosetData.insert(geoType + geoId);
170 }
171
172 se.label = "Skin " + std::to_string(app.anim.skinEntries.size());
173 app.anim.skinEntries.push_back(se);
174 }
175 }
176 else if (isItem)
177 {
178 const auto* idiTable = WOWDB.getTable("ItemDisplayInfo");
179 const auto* texFDTable = WOWDB.getTable("TextureFileData");
180 const auto* modFDTable = WOWDB.getTable("ModelFileData");
181 const int targetFDID = model->gamefile->fileDataId();
182
183 std::set<uint32_t> matchingModelResIDs;
184 if (idiTable && texFDTable && modFDTable)
185 {
186 for (const auto& mfdRow : *modFDTable)
187 {
188 if (static_cast<int>(mfdRow.getUInt("FileDataID")) == targetFDID)
189 matchingModelResIDs.insert(mfdRow.getUInt("ModelResourcesID"));
190 }
191
192 for (const auto& idiRow : *idiTable)
193 {
194 if (matchingModelResIDs.find(idiRow.getUInt("ModelResourcesID1")) == matchingModelResIDs.end())
195 continue;
196
197 uint32_t matResID = idiRow.getUInt("ModelMaterialResourcesID1");
198 for (const auto& tfdRow : *texFDTable)
199 {
200 if (tfdRow.getUInt("MaterialResourcesID") != matResID)
201 continue;
202 uint32_t fdid = tfdRow.getUInt("FileDataID");
203 if (fdid == 0) continue;
204 SkinEntry se;
205 se.tex[0] = GAMEDIRECTORY.getFile(fdid);
206 if (!se.tex[0]) continue;
208 se.count = 1;
209 se.label = "Skin " + std::to_string(app.anim.skinEntries.size());
210 app.anim.skinEntries.push_back(se);
211 }
212 }
213 }
214 }
215
216 if (!app.anim.skinEntries.empty())
217 applySkin(model, 0, app);
218}
219
220// ---- Character control init -----------------------------------------------
221
223{
225 if (!model) return;
226
227 auto& cd = model->cd;
228 cd.showUnderwear = true;
229 cd.showEars = true;
230 cd.showHair = true;
231 cd.showFacialHair = true;
232 cd.showFeet = false;
233 cd.autoHideGeosetsForHeadItems = true;
234 cd.eyeGlowType = EGT_DEFAULT;
235 model->bSheathe = false;
236 cd.reset(model);
237
238 const auto& infos = model->infos;
239 if (infos.raceID == -1 || infos.ChrModelID.empty())
240 return;
241
242 const auto* optTable = WOWDB.getTable("ChrCustomizationOption");
243 const auto* choiceTable = WOWDB.getTable("ChrCustomizationChoice");
244 if (!optTable || !choiceTable) return;
245 const uint32_t targetModelID = static_cast<uint32_t>(infos.ChrModelID[0]);
246
247 // Collect ALL options for this ChrModel (no ChrCustomizationID filter — matches wow.export)
248 struct OptionEntry { uint32_t id; uint32_t orderIndex; uint32_t flags; uint32_t categoryID; };
249 std::vector<OptionEntry> matchingOptions;
250 for (const auto& row : *optTable)
251 {
252 if (row.getUInt("ChrModelID") == targetModelID)
253 matchingOptions.push_back({ row.recordID(), row.getUInt("OrderIndex"), row.getUInt("Flags"),
254 row.getUInt("ChrCustomizationCategoryID") });
255 }
256 std::sort(matchingOptions.begin(), matchingOptions.end(),
257 [](const OptionEntry& a, const OptionEntry& b) { return a.orderIndex < b.orderIndex; });
258
259 // Collect all choices grouped by option, sorted by OrderIndex (matches wow.export)
260 struct ChoiceData { uint32_t id; uint32_t orderIndex; std::string name; };
261 std::map<uint32_t, std::vector<ChoiceData>> choicesByOption;
262 for (const auto& row : *choiceTable)
263 {
264 uint32_t optID = row.getUInt("ChrCustomizationOptionID");
265 std::string cname = row.getString("Name_Lang");
266 uint32_t orderIdx = row.getUInt("OrderIndex");
267 if (cname.empty())
268 cname = "Choice " + std::to_string(orderIdx);
269 choicesByOption[optID].push_back({ row.recordID(), orderIdx, std::move(cname) });
270 }
271 for (auto& [optID, choices] : choicesByOption)
272 {
273 std::sort(choices.begin(), choices.end(),
274 [](const ChoiceData& a, const ChoiceData& b) { return a.orderIndex < b.orderIndex; });
275 }
276
277 for (const auto& optEntry : matchingOptions)
278 {
279 unsigned int optionID = optEntry.id;
280
282 opt.optionID = optionID;
283 opt.categoryID = optEntry.categoryID;
284
285 // Option name: Name_lang, fallback to "Option " + OrderIndex (matches wow.export)
286 auto optRow = optTable->getRow(optionID);
287 if (optRow)
288 {
289 std::string n = optRow.getString("Name_Lang");
290 opt.name = n.empty() ? ("Option " + std::to_string(optEntry.orderIndex)) : n;
291 }
292 else
293 {
294 opt.name = "Option " + std::to_string(optEntry.orderIndex);
295 }
296
297 // Get choices directly from DB (not through CharDetails)
298 auto it = choicesByOption.find(optionID);
299 if (it == choicesByOption.end() || it->second.empty())
300 continue;
301
302 for (const auto& choice : it->second)
303 {
304 opt.choiceIDs.push_back(choice.id);
305 opt.choiceNames.push_back(choice.name);
306 }
307
308 // Set selected index from current customization state
309 unsigned int cur = cd.get(optionID);
310 opt.selectedIndex = 0;
311 for (size_t c = 0; c < opt.choiceIDs.size(); ++c)
312 {
313 if (opt.choiceIDs[c] == cur)
314 {
315 opt.selectedIndex = static_cast<int>(c);
316 break;
317 }
318 }
319
320 app.character.customizationOptions.push_back(std::move(opt));
321 }
322}
323
324// ---- Model control init ---------------------------------------------------
325
327{
328 app.browsers.geosetGroups.clear();
329 app.browsers.pcrState = {};
330
331 if (!model)
332 return;
333
334 std::map<size_t, size_t> meshToGroupIdx;
335 for (size_t i = 0; i < model->geosets.size(); ++i)
336 {
337 size_t mesh = model->geosets[i]->id / 100;
338 if (meshToGroupIdx.find(mesh) == meshToGroupIdx.end())
339 {
340 GeosetGroupEntry group;
341 group.meshId = mesh;
342 std::string groupName = WoWModel::getCGGroupName(static_cast<CharGeosets>(mesh));
343 group.name = groupName.empty() ? std::to_string(mesh) : groupName;
344 meshToGroupIdx[mesh] = app.browsers.geosetGroups.size();
345 app.browsers.geosetGroups.push_back(std::move(group));
346 }
347
348 GeosetEntry ge;
349 ge.index = i;
350 ge.id = model->geosets[i]->id;
351 ge.label = std::format("{} [{}, {}, {}]", i, mesh,
352 model->geosets[i]->id % 100, model->geosets[i]->id);
353 app.browsers.geosetGroups[meshToGroupIdx[mesh]].geosets.push_back(ge);
354 }
355
356 for (uint pcid : model->replacableParticleColorIDs)
357 {
358 if (pcid == 11) app.browsers.pcrState.hasSet[0] = true;
359 else if (pcid == 12) app.browsers.pcrState.hasSet[1] = true;
360 else if (pcid == 13) app.browsers.pcrState.hasSet[2] = true;
361 }
362}
363
364// ---- Equipment helpers ----------------------------------------------------
365
366static bool correctType(int type, int slot)
367{
368 if (type == IT_ALL)
369 return true;
370 switch (slot)
371 {
372 case CS_HEAD: return (type == IT_HEAD);
373 case CS_SHOULDER: return (type == IT_SHOULDER);
374 case CS_SHIRT: return (type == IT_SHIRT);
375 case CS_CHEST: return (type == IT_CHEST || type == IT_ROBE);
376 case CS_BELT: return (type == IT_BELT);
377 case CS_PANTS: return (type == IT_PANTS);
378 case CS_BOOTS: return (type == IT_BOOTS);
379 case CS_BRACERS: return (type == IT_BRACERS);
380 case CS_GLOVES: return (type == IT_GLOVES);
381 case CS_HAND_RIGHT: return (type == IT_RIGHTHANDED || type == IT_GUN || type == IT_THROWN ||
382 type == IT_2HANDED || type == IT_DAGGER);
383 case CS_HAND_LEFT: return (type == IT_LEFTHANDED || type == IT_BOW || type == IT_SHIELD ||
384 type == IT_2HANDED || type == IT_DAGGER || type == IT_OFFHAND);
385 case CS_CAPE: return (type == IT_CAPE);
386 case CS_TABARD: return (type == IT_TABARD);
387 case CS_QUIVER: return (type == IT_QUIVER);
388 default: return false;
389 }
390}
391
393{
394 app.character.equipFilteredItems.clear();
395 if (app.character.equipSlotToEdit < 0)
396 return;
397
398 std::string search = core::toLower(app.character.equipSearchBuf);
399 auto s = search.find_first_not_of(" \t\r\n");
400 auto e = search.find_last_not_of(" \t\r\n");
401 search = (s == std::string::npos) ? "" : search.substr(s, e - s + 1);
402
403 for (size_t i = 0; i < db.items.size(); ++i)
404 {
405 const auto& item = db.items[i];
406 if (item.id == 0)
407 continue;
408 if (!correctType(item.type, app.character.equipSlotToEdit))
409 continue;
410 if (!search.empty() && !core::containsIgnoreCase(item.name, search))
411 continue;
412 app.character.equipFilteredItems.push_back(i);
413 }
414}
415
416void tryToEquipItem(WoWModel* model, int id, AppState& app, const ItemDatabase& db)
417{
418 if (id == 0 || !model)
419 return;
420
421 ItemRecord itemr = db.getById(id);
422 if (itemr.name == db.items[0].name)
423 {
424 LOG_ERROR << "Cannot retrieve item from database (id " << id << ")";
425 return;
426 }
427
428 int itemSlot = itemr.slot();
429 if (itemSlot == -1)
430 {
431 LOG_ERROR << "Cannot determine slot for object " << itemr.name;
432 return;
433 }
434
435 WoWItem* item = model->getItem(static_cast<CharSlots>(itemSlot));
436 if (item)
437 {
438 item->setId(id);
439 app.character.equipSlotLevels[itemSlot] = 0;
440 }
441}
442
443// ---- Item Sets ------------------------------------------------------------
444
446{
447 if (app.browsers.itemSetsBuilt)
448 return;
449
450 app.browsers.itemSets.clear();
451
452 const auto* itemSetTable = WOWDB.getTable("ItemSet");
453 if (!itemSetTable) return;
454 for (const auto& row : *itemSetTable)
455 {
456 ItemSetEntry e;
457 e.id = static_cast<int>(row.recordID());
458 e.name = row.getString("Name_Lang");
459 if (!e.name.empty())
460 app.browsers.itemSets.push_back(e);
461 }
462
463 std::sort(app.browsers.itemSets.begin(), app.browsers.itemSets.end(),
464 [](const ItemSetEntry& a, const ItemSetEntry& b) { return a.name < b.name; });
465
466 app.browsers.itemSetsBuilt = true;
467 app.browsers.itemSetFilterDirty = true;
468 LOG_INFO << "Item sets loaded: " << app.browsers.itemSets.size();
469}
470
472{
473 app.browsers.itemSetFiltered.clear();
474
475 std::string search = core::toLower(std::string(app.browsers.itemSetSearchBuf));
476 auto s = search.find_first_not_of(" \t\r\n");
477 auto e = search.find_last_not_of(" \t\r\n");
478 search = (s == std::string::npos) ? "" : search.substr(s, e - s + 1);
479
480 for (size_t i = 0; i < app.browsers.itemSets.size(); ++i)
481 {
482 if (!search.empty() && !core::containsIgnoreCase(app.browsers.itemSets[i].name, search))
483 continue;
484 app.browsers.itemSetFiltered.push_back(i);
485 }
486
487 app.browsers.itemSetFilterDirty = false;
488}
489
490void applyItemSet(WoWModel* model, int setId, AppState& app, const ItemDatabase& db)
491{
492 if (!model || setId <= 0)
493 return;
494
495 const auto* itemSetTable = WOWDB.getTable("ItemSet");
496 if (!itemSetTable) return;
497 auto setRow = itemSetTable->getRow(static_cast<uint32_t>(setId));
498 if (!setRow)
499 {
500 LOG_ERROR << "Item set query failed for ID " << setId;
501 return;
502 }
503
504 for (const auto it : *model)
505 it->setId(0);
506 std::memset(app.character.equipSlotLevels, 0, sizeof(app.character.equipSlotLevels));
507
508 for (unsigned i = 0; i < 8; ++i)
509 {
510 std::string fieldName = "ItemID" + std::to_string(i + 1);
511 uint32_t itemID = setRow.getUInt(fieldName);
512 if (itemID == 0)
513 continue;
514 try
515 {
516 tryToEquipItem(model, static_cast<int>(itemID), app, db);
517 }
518 catch (const std::exception& e)
519 {
520 LOG_ERROR << "Failed to equip item set entry " << i
521 << " (id=" << itemID << "): " << e.what();
522 }
523 }
524
525 model->refresh();
526 LOG_INFO << "Applied item set ID " << setId;
527}
528
529// ---- Start Outfits --------------------------------------------------------
530
532{
533 app.browsers.startOutfits.clear();
534 app.browsers.startOutfitsBuilt = false;
536
537 if (!model) return;
538
539 const auto& infos = model->infos;
540 if (infos.raceID == -1)
541 return;
542
543 const auto* csoTable = WOWDB.getTable("CharStartOutfit");
544 const auto* chrClassTable = WOWDB.getTable("ChrClasses");
545 if (!csoTable || !chrClassTable) return;
546 const uint32_t targetRace = static_cast<uint32_t>(infos.raceID);
547 const uint32_t targetSex = static_cast<uint32_t>(infos.sexID);
548
549 for (const auto& row : *csoTable)
550 {
551 if (row.getUInt("raceID") != targetRace || row.getUInt("sexID") != targetSex)
552 continue;
553
554 auto classRow = chrClassTable->getRow(row.getUInt("classID"));
555 std::string className = classRow ? classRow.getString("Filename") : "";
556
558 e.name = className;
559 e.id = static_cast<int>(row.recordID());
560 if (!e.name.empty() && e.id > 0)
561 app.browsers.startOutfits.push_back(e);
562 }
563
564 std::sort(app.browsers.startOutfits.begin(), app.browsers.startOutfits.end(),
565 [](const StartOutfitEntry& a, const StartOutfitEntry& b) { return a.name < b.name; });
566
567 app.browsers.startOutfitsBuilt = true;
569 LOG_INFO << "Start outfits loaded: " << app.browsers.startOutfits.size();
570}
571
573{
574 app.browsers.startOutfitFiltered.clear();
575
576 std::string search = core::toLower(std::string(app.browsers.startOutfitSearchBuf));
577 auto s = search.find_first_not_of(" \t\r\n");
578 auto e = search.find_last_not_of(" \t\r\n");
579 search = (s == std::string::npos) ? "" : search.substr(s, e - s + 1);
580
581 for (size_t i = 0; i < app.browsers.startOutfits.size(); ++i)
582 {
583 if (!search.empty() && !core::containsIgnoreCase(app.browsers.startOutfits[i].name, search))
584 continue;
585 app.browsers.startOutfitFiltered.push_back(i);
586 }
587
589}
590
591void applyStartOutfit(WoWModel* model, int outfitId, AppState& app, const ItemDatabase& db)
592{
593 if (!model || outfitId <= 0)
594 return;
595
596 const auto* csoTable = WOWDB.getTable("CharStartOutfit");
597 if (!csoTable) return;
598 auto csoRow = csoTable->getRow(static_cast<uint32_t>(outfitId));
599 if (!csoRow)
600 {
601 LOG_ERROR << "Start outfit query failed for ID " << outfitId;
602 return;
603 }
604
605 for (const auto it : *model)
606 it->setId(0);
607 std::memset(app.character.equipSlotLevels, 0, sizeof(app.character.equipSlotLevels));
608
609 for (unsigned i = 0; i < 24; ++i)
610 {
611 std::string fieldName = "iitem" + std::to_string(i + 1);
612 uint32_t itemID = csoRow.getUInt(fieldName);
613 if (itemID == 0)
614 continue;
615 try
616 {
617 tryToEquipItem(model, static_cast<int>(itemID), app, db);
618 }
619 catch (const std::exception& ex)
620 {
621 LOG_ERROR << "Failed to equip start outfit entry " << i
622 << " (id=" << itemID << "): " << ex.what();
623 }
624 }
625
626 model->refresh();
627 LOG_INFO << "Applied start outfit ID " << outfitId;
628}
629
630// ---- Clear / Load model ---------------------------------------------------
631
633{
634 if (app.scene.root)
635 {
636 app.scene.root->delChildren();
637 app.scene.root->setModel(nullptr);
638 }
639
641 app.scene.isModel = false;
642 app.scene.isChar = false;
643
644 app.scene.selModel = nullptr;
645 app.anim.animEntries.clear();
646 app.anim.skinEntries.clear();
648 app.browsers.geosetGroups.clear();
649 app.browsers.pcrState = {};
650 app.anim.selectedAnimCombo = 0;
651 app.anim.selectedSkin = -1;
652 app.anim.animSpeed = 1.0f;
653 app.anim.autoAnimate = true;
654 app.character.equipSlotToEdit = -1;
655 app.character.equipFilteredItems.clear();
656 app.character.equipSearchBuf.clear();
657 std::memset(app.character.equipSlotLevels, 0, sizeof(app.character.equipSlotLevels));
658 app.exporting.exportAnimChecked.clear();
659 app.exporting.exportStatus.clear();
660 app.scene.isMounted = false;
661 app.browsers.startOutfits.clear();
662 app.browsers.startOutfitsBuilt = false;
663 app.browsers.startOutfitSearchBuf.clear();
664 app.browsers.startOutfitFiltered.clear();
666}
667
668void loadModel(GameFile* file, AppState& app, float fov)
669{
670 if (!file || !app.scene.root)
671 return;
672
673 LOG_INFO << "Loading model: " << file->fullname();
674
675 clearModel(app);
676
677 auto* model = new WoWModel(file, true);
678 if (!model->ok)
679 {
680 LOG_ERROR << "Model is not OK: " << file->fullname();
681 delete model;
682 return;
683 }
684
685 app.scene.root->addChild(model, 0, -1);
686
687 const std::string fn = file->fullname();
688 app.scene.isChar = (core::startsWithIgnoreCase(fn, "char") ||
689 core::startsWithIgnoreCase(fn, "alternate/char") ||
690 core::startsWithIgnoreCase(fn, "alternate\\char"));
691
692 if (app.scene.isChar)
693 {
694 model->addChild(new WoWItem(CS_SHIRT));
695 model->addChild(new WoWItem(CS_HEAD));
696 model->addChild(new WoWItem(CS_SHOULDER));
697 model->addChild(new WoWItem(CS_PANTS));
698 model->addChild(new WoWItem(CS_BOOTS));
699 model->addChild(new WoWItem(CS_CHEST));
700 model->addChild(new WoWItem(CS_TABARD));
701 model->addChild(new WoWItem(CS_BELT));
702 model->addChild(new WoWItem(CS_BRACERS));
703 model->addChild(new WoWItem(CS_GLOVES));
704 model->addChild(new WoWItem(CS_HAND_RIGHT));
705 model->addChild(new WoWItem(CS_HAND_LEFT));
706 model->addChild(new WoWItem(CS_CAPE));
707 model->addChild(new WoWItem(CS_QUIVER));
708 model->modelType = MT_CHAR;
709 model->charModelDetails.isChar = true;
710 }
711 else
712 {
713 model->addChild(new WoWItem(CS_HAND_RIGHT));
714 model->addChild(new WoWItem(CS_HAND_LEFT));
715 model->modelType = MT_NORMAL;
716 }
717
718 app.scene.isModel = true;
719
720 app.scene.selModel = model;
721 initAnimationControl(model, app);
722 initModelControl(model, app);
723 if (app.scene.isChar)
724 initCharacterControl(model, app);
725
726 resetCameraToModel(app.scene.camera, model, fov);
727
728 LOG_INFO << "Model loaded: " << model->name();
729}
730
731// ---- NPC Browser ----------------------------------------------------------
732
733void rebuildNpcFilter(AppState& app, const std::vector<NPCRecord>& npcList)
734{
735 app.browsers.npcFiltered.clear();
736
737 std::string search = core::toLower(std::string(app.browsers.npcSearchBuf));
738 auto s = search.find_first_not_of(" \t\r\n");
739 auto e = search.find_last_not_of(" \t\r\n");
740 search = (s == std::string::npos) ? "" : search.substr(s, e - s + 1);
741
742 for (size_t i = 0; i < npcList.size(); ++i)
743 {
744 const auto& npc = npcList[i];
745 if (npc.model == 0) continue;
746 if (!search.empty() && !core::containsIgnoreCase(npc.name, search))
747 continue;
748 app.browsers.npcFiltered.push_back(i);
749 }
750
751 app.browsers.npcFilterDirty = false;
752}
753
754void loadNPC(unsigned int creatureID, AppState& app, float fov)
755{
756 const auto* creatureTable = WOWDB.getTable("Creature");
757 if (!creatureTable) return;
758 auto creatureRow = creatureTable->getRow(creatureID);
759 if (!creatureRow)
760 {
761 LOG_ERROR << "NPC query failed for ID " << creatureID;
762 return;
763 }
764
765 uint32_t displayInfoID = creatureRow.getUInt("DisplayID1");
766 const auto* cdiTable = WOWDB.getTable("CreatureDisplayInfo");
767 if (!cdiTable) return;
768 auto cdiRow = cdiTable->getRow(displayInfoID);
769 if (!cdiRow)
770 {
771 LOG_ERROR << "NPC query failed for ID " << creatureID << " (no CreatureDisplayInfo for DisplayID1=" << displayInfoID << ")";
772 return;
773 }
774
775 const auto* cmdTable = WOWDB.getTable("CreatureModelData");
776 auto cmdRow = cmdTable ? cmdTable->getRow(cdiRow.getUInt("ModelID")) : DB2Row();
777 uint32_t fileDataID = cmdRow ? cmdRow.getUInt("FileDataID") : 0;
778 uint32_t extraId = cdiRow.getUInt("ExtendedDisplayInfoID");
779
780 if (extraId == 0)
781 {
782 GameFile* file = GAMEDIRECTORY.getFile(fileDataID);
783 if (!file) return;
784 loadModel(file, app, fov);
785
786 WoWModel* m = getLoadedModel(app);
787 if (m)
788 {
789 uint32_t texFDIDs[3] = {
790 cdiRow.getUInt("TextureVariationFileDataID1"),
791 cdiRow.getUInt("TextureVariationFileDataID2"),
792 cdiRow.getUInt("TextureVariationFileDataID3")
793 };
794 for (size_t i = 0; i < app.anim.skinEntries.size(); ++i)
795 {
796 bool match = true;
797 for (size_t t = 0; t < 3 && match; ++t)
798 {
799 if (app.anim.skinEntries[i].tex[t])
800 {
801 int fdid = app.anim.skinEntries[i].tex[t]->fileDataId();
802 if (texFDIDs[t] != 0)
803 match = (fdid == static_cast<int>(texFDIDs[t]));
804 else
805 match = false;
806 }
807 }
808 if (match)
809 {
810 applySkin(m, static_cast<int>(i), app);
811 break;
812 }
813 }
814 }
815 }
816 else
817 {
818 GameFile* file = GAMEDIRECTORY.getFile(RaceInfos::getHDModelForFileID(static_cast<int>(fileDataID)));
819 if (!file) return;
820 loadModel(file, app, fov);
821
822 WoWModel* m = getLoadedModel(app);
823 if (!m) return;
824
825 const auto* cdieTable = WOWDB.getTable("CreatureDisplayInfoExtra");
826 auto cdieRow = cdieTable ? cdieTable->getRow(extraId) : DB2Row();
827 if (cdieRow)
828 {
829 m->cd.set(CharDetails::SKIN_COLOR, static_cast<int>(cdieRow.getUInt("Skin")));
830 m->cd.set(CharDetails::FACE, static_cast<int>(cdieRow.getUInt("Face")));
831 m->cd.set(CharDetails::FACIAL_CUSTOMIZATION_STYLE, static_cast<int>(cdieRow.getUInt("HairStyle")));
832 m->cd.set(CharDetails::FACIAL_CUSTOMIZATION_COLOR, static_cast<int>(cdieRow.getUInt("HairColor")));
833 m->cd.set(CharDetails::ADDITIONAL_FACIAL_CUSTOMIZATION, static_cast<int>(cdieRow.getUInt("FacialHair")));
834 }
835
836 const auto* npcSlotTable = WOWDB.getTable("NpcModelItemSlotDisplayInfo");
837 static const std::map<int, CharSlots> ItemTypeToInternal = {
838 {0, CS_HEAD}, {1, CS_SHOULDER}, {2, CS_SHIRT}, {3, CS_CHEST}, {4, CS_BELT}, {5, CS_PANTS},
839 {6, CS_BOOTS}, {7, CS_BRACERS}, {8, CS_GLOVES}, {9, CS_TABARD}, {10, CS_CAPE}
840 };
841 if (npcSlotTable)
842 for (const auto& npcRow : *npcSlotTable)
843 {
844 if (npcRow.getUInt("NpcModelID") != extraId)
845 continue;
846 auto it = ItemTypeToInternal.find(static_cast<int>(npcRow.getUInt("ItemSlot")));
847 if (it != ItemTypeToInternal.end())
848 {
849 WoWItem* item = m->getItem(it->second);
850 if (item)
851 item->setDisplayId(static_cast<int>(npcRow.getUInt("ItemDisplayInfoID")));
852 }
853 }
854
855 m->cd.isNPC = true;
856 m->refresh();
857 }
858}
859
860// ---- Item Browser ---------------------------------------------------------
861
863{
864 app.browsers.itemBrowseFiltered.clear();
865
866 std::string search = core::toLower(std::string(app.browsers.itemBrowseSearchBuf));
867 auto s = search.find_first_not_of(" \t\r\n");
868 auto e = search.find_last_not_of(" \t\r\n");
869 search = (s == std::string::npos) ? "" : search.substr(s, e - s + 1);
870
871 for (size_t i = 0; i < db.items.size(); ++i)
872 {
873 const auto& item = db.items[i];
874 if (item.id == 0) continue;
875 if (!search.empty() && !core::containsIgnoreCase(item.name, search))
876 continue;
877 app.browsers.itemBrowseFiltered.push_back(i);
878 }
879
881}
882
883void loadItemModel(unsigned int itemId, AppState& app, float fov)
884{
885 try
886 {
887 const auto* imaTable = WOWDB.getTable("ItemModifiedAppearance");
888 const auto* iaTable = WOWDB.getTable("ItemAppearance");
889 const auto* idiTable = WOWDB.getTable("ItemDisplayInfo");
890 const auto* modFDTable = WOWDB.getTable("ModelFileData");
891 const auto* texFDTable = WOWDB.getTable("TextureFileData");
892 if (!imaTable || !iaTable || !idiTable || !modFDTable || !texFDTable) return;
893
894 uint32_t itemAppearanceID = 0;
895 for (const auto& row : *imaTable)
896 {
897 if (row.getUInt("ItemID") == itemId)
898 {
899 itemAppearanceID = row.getUInt("ItemAppearanceID");
900 break;
901 }
902 }
903 if (itemAppearanceID == 0) return;
904
905 auto iaRow = iaTable->getRow(itemAppearanceID);
906 if (!iaRow) return;
907
908 uint32_t displayInfoID = iaRow.getUInt("ItemDisplayInfoID");
909 auto idiRow = idiTable->getRow(displayInfoID);
910 if (!idiRow) return;
911
912 uint32_t modelResID = idiRow.getUInt("ModelResourcesID1");
913 uint32_t modelFDID = 0;
914 for (const auto& mfdRow : *modFDTable)
915 {
916 if (mfdRow.getUInt("ModelResourcesID") == modelResID)
917 {
918 modelFDID = mfdRow.getUInt("FileDataID");
919 break;
920 }
921 }
922 if (modelFDID == 0) return;
923
924 GameFile* file = GAMEDIRECTORY.getFile(modelFDID);
925 if (!file) return;
926
927 loadModel(file, app, fov);
928
929 WoWModel* m = getLoadedModel(app);
930 if (m)
931 {
932 uint32_t matResID = idiRow.getUInt("ModelMaterialResourcesID1");
933 for (const auto& tfdRow : *texFDTable)
934 {
935 if (tfdRow.getUInt("MaterialResourcesID") == matResID)
936 {
937 uint32_t texFDID = tfdRow.getUInt("FileDataID");
938 if (texFDID != 0)
939 {
940 GameFile* texFile = GAMEDIRECTORY.getFile(texFDID);
941 if (texFile)
943 }
944 break;
945 }
946 }
947 }
948 }
949 catch (...)
950 {
951 LOG_ERROR << "Exception loading item model for ID " << itemId;
952 }
953}
954
955// ---- Mounts ---------------------------------------------------------------
956
958{
960 return;
961
962 app.browsers.mountList.clear();
963 app.browsers.creatureModels.clear();
964 app.browsers.creatureModelNames.clear();
965
966 const auto* mountTable = WOWDB.getTable("Mount");
967 const auto* mxdTable = WOWDB.getTable("MountXDisplay");
968 if (mountTable && mxdTable)
969 for (const auto& mxdRow : *mxdTable)
970 {
971 uint32_t mountID = mxdRow.getUInt("MountID");
972 auto mountRow = mountTable->getRow(mountID);
973 MountEntry me;
974 me.displayID = static_cast<int>(mxdRow.getUInt("CreatureDisplayInfoID"));
975 me.name = mountRow ? mountRow.getString("Name_Lang") : "";
976 app.browsers.mountList.push_back(me);
977 }
978 std::sort(app.browsers.mountList.begin(), app.browsers.mountList.end(),
979 [](const MountEntry& a, const MountEntry& b) { return a.name < b.name; });
980 LOG_INFO << "Mount list: " << app.browsers.mountList.size() << " player mounts.";
981
982 std::vector<GameFile*> files;
983 GAMEDIRECTORY.getFilesForFolder(files, std::string("creature/"), std::string("m2"));
984 for (auto* gf : files)
985 {
986 app.browsers.creatureModels.push_back(gf);
987 std::string n = gf->fullname();
988 if (n.size() > 9)
989 n = n.substr(9);
990 app.browsers.creatureModelNames.push_back(n);
991 }
992 if (!app.browsers.creatureModels.empty())
993 {
994 std::vector<size_t> indices(app.browsers.creatureModels.size());
995 for (size_t i = 0; i < indices.size(); ++i) indices[i] = i;
996 std::sort(indices.begin(), indices.end(),
997 [&](size_t a, size_t b) { return app.browsers.creatureModelNames[a] < app.browsers.creatureModelNames[b]; });
998 std::vector<GameFile*> sortedFiles(app.browsers.creatureModels.size());
999 std::vector<std::string> sortedNames(app.browsers.creatureModelNames.size());
1000 for (size_t i = 0; i < indices.size(); ++i)
1001 {
1002 sortedFiles[i] = app.browsers.creatureModels[indices[i]];
1003 sortedNames[i] = app.browsers.creatureModelNames[indices[i]];
1004 }
1005 app.browsers.creatureModels = std::move(sortedFiles);
1006 app.browsers.creatureModelNames = std::move(sortedNames);
1007 }
1008 LOG_INFO << "Creature models: " << app.browsers.creatureModels.size() << " files.";
1009
1010 app.browsers.mountListBuilt = true;
1011 app.browsers.mountFilterDirty = true;
1012}
1013
1015{
1016 app.browsers.mountFiltered.clear();
1017
1018 std::string search = core::toLower(std::string(app.browsers.mountSearchBuf));
1019 auto s = search.find_first_not_of(" \t\r\n");
1020 auto e = search.find_last_not_of(" \t\r\n");
1021 search = (s == std::string::npos) ? "" : search.substr(s, e - s + 1);
1022
1023 if (app.browsers.mountTab == 0)
1024 {
1025 for (size_t i = 0; i < app.browsers.mountList.size(); ++i)
1026 {
1027 if (!search.empty() && !core::containsIgnoreCase(app.browsers.mountList[i].name, search))
1028 continue;
1029 app.browsers.mountFiltered.push_back(i);
1030 }
1031 }
1032 else
1033 {
1034 for (size_t i = 0; i < app.browsers.creatureModelNames.size(); ++i)
1035 {
1036 if (!search.empty() && !core::containsIgnoreCase(app.browsers.creatureModelNames[i], search))
1037 continue;
1038 app.browsers.mountFiltered.push_back(i);
1039 }
1040 }
1041
1042 app.browsers.mountFilterDirty = false;
1043}
1044
1045void mountCharacter(int displayID, GameFile* creatureFile, AppState& app, float fov)
1046{
1047 WoWModel* charModel = getLoadedModel(app);
1048 if (!charModel || !app.scene.isChar || !app.scene.root)
1049 return;
1050
1051 GameFile* modelFile = nullptr;
1052 int morphID = 0;
1053
1054 if (displayID > 0)
1055 {
1056 morphID = displayID;
1057 const auto* cdiTable = WOWDB.getTable("CreatureDisplayInfo");
1058 const auto* cmdTable = WOWDB.getTable("CreatureModelData");
1059 if (!cdiTable || !cmdTable) return;
1060 auto cdiRow = cdiTable->getRow(static_cast<uint32_t>(displayID));
1061 if (!cdiRow)
1062 {
1063 LOG_ERROR << "Mount display query failed for displayID " << displayID;
1064 return;
1065 }
1066 auto cmdRow = cmdTable->getRow(cdiRow.getUInt("ModelID"));
1067 uint32_t mountFDID = cmdRow ? cmdRow.getUInt("FileDataID") : 0;
1068 if (mountFDID == 0)
1069 {
1070 LOG_ERROR << "Mount display query failed for displayID " << displayID;
1071 return;
1072 }
1073 modelFile = GAMEDIRECTORY.getFile(mountFDID);
1074 }
1075 else if (creatureFile)
1076 {
1077 modelFile = creatureFile;
1078 }
1079
1080 if (!modelFile)
1081 return;
1082
1083 Attachment* charAtt = app.scene.root->children.empty() ? nullptr : app.scene.root->children[0];
1084 if (!charAtt)
1085 return;
1086
1087 auto* mountModel = new WoWModel(modelFile, false);
1088 if (!mountModel->ok)
1089 {
1090 LOG_ERROR << "Mount model failed to load.";
1091 delete mountModel;
1092 return;
1093 }
1094 mountModel->isMount = true;
1095
1096 app.scene.root->setModel(mountModel);
1097 charAtt->id = 0;
1098
1099 if (morphID > 0)
1100 {
1101 const auto* cdiTexTable = WOWDB.getTable("CreatureDisplayInfo");
1102 if (!cdiTexTable) return;
1103 auto cdiTexRow = cdiTexTable->getRow(static_cast<uint32_t>(morphID));
1104 if (cdiTexRow)
1105 {
1106 static const char* texFields[] = {
1107 "TextureVariationFileDataID1",
1108 "TextureVariationFileDataID2",
1109 "TextureVariationFileDataID3"
1110 };
1111 for (size_t t = 0; t < 3; ++t)
1112 {
1113 uint32_t texFDID = cdiTexRow.getUInt(texFields[t]);
1114 if (texFDID != 0)
1115 {
1116 GameFile* texFile = GAMEDIRECTORY.getFile(texFDID);
1117 if (texFile)
1118 mountModel->updateTextureList(texFile, TEXTURE_GAMEOBJECT1 + static_cast<int>(t));
1119 }
1120 }
1121 }
1122 }
1123
1124 charModel->bSheathe = true;
1125
1126 if (charModel->animLookups.size() > ANIMATION_MOUNT &&
1127 charModel->animLookups[ANIMATION_MOUNT] >= 0)
1128 {
1129 charModel->animManager->Stop();
1130 charModel->currentAnim = charModel->animLookups[ANIMATION_MOUNT];
1131 charModel->animManager->SetAnim(0, static_cast<short>(charModel->currentAnim), 0);
1132 charModel->animManager->Play();
1133 }
1134
1135 charModel->rot_ = charModel->pos_ = glm::vec3(0.0f);
1136 charModel->scale_ = 1.0f;
1137 mountModel->rot_.x = 0.0f;
1138
1139 app.scene.isMounted = true;
1140 app.scene.selModel = mountModel;
1141
1142 initAnimationControl(mountModel, app);
1143 initModelControl(mountModel, app);
1144
1145 resetCameraToModel(app.scene.camera, mountModel, fov);
1146 LOG_INFO << "Character mounted on: " << modelFile->fullname();
1147}
1148
1149void dismountCharacter(AppState& app, float fov)
1150{
1151 if (!app.scene.isMounted || !app.scene.root || !app.scene.isChar)
1152 return;
1153
1154 WoWModel* charModel = nullptr;
1155 Attachment* charAtt = app.scene.root->children.empty() ? nullptr : app.scene.root->children[0];
1156 if (charAtt)
1157 charModel = dynamic_cast<WoWModel*>(charAtt->model());
1158
1159 app.scene.root->setModel(nullptr);
1160 app.scene.isMounted = false;
1161
1162 if (charAtt)
1163 charAtt->id = 0;
1164
1165 if (charModel)
1166 {
1167 charModel->bSheathe = false;
1168 charModel->scale_ = 1.0f;
1169 charModel->rot_ = charModel->pos_ = glm::vec3(0.0f);
1170
1171 app.scene.selModel = charModel;
1172 initAnimationControl(charModel, app);
1173 initModelControl(charModel, app);
1174 resetCameraToModel(app.scene.camera, charModel, fov);
1175 }
1176
1177 LOG_INFO << "Character dismounted.";
1178}
1179
1180} // namespace ModelLoader
#define GAMEDIRECTORY
Definition Game.h:9
#define LOG_ERROR
Definition Logger.h:11
#define LOG_INFO
Definition Logger.h:10
TextureManager TEXTUREMANAGER
#define WOWDB
Definition WoWDatabase.h:65
void SetAnim(short index, unsigned int id, short loop)
void SetSpeed(float speed)
Definition AnimManager.h:97
Scene-graph node that attaches a Displayable to a parent bone slot.
Definition Attachment.h:21
Displayable * model() const
Definition Attachment.h:38
void set(uint chrCustomizationOptionID, uint chrCustomizationChoiceID)
@ FACIAL_CUSTOMIZATION_STYLE
Definition CharDetails.h:72
@ ADDITIONAL_FACIAL_CUSTOMIZATION
Definition CharDetails.h:74
@ FACIAL_CUSTOMIZATION_COLOR
Definition CharDetails.h:73
bool showUnderwear
Definition CharDetails.h:93
Lightweight handle to a single row in a DB2Table.
Definition DB2Table.h:27
Abstract base class representing a file within the game data archive.
Definition GameFile.h:12
const std::string & fullname() const
Definition GameFile.h:56
int fileDataId()
Definition GameFile.h:57
In-memory item database loaded from the CSV item list.
Definition database.h:41
std::vector< ItemRecord > items
Definition database.h:45
const ItemRecord & getById(int id) const
Definition database.cpp:97
const std::string & itemName() const
Definition manager.h:39
void clear()
Definition manager.h:128
Orbit camera that revolves around a target point.
Definition OrbitCamera.h:10
void reset()
Reset all parameters to defaults.
void resetFromBounds(float zMin, float zMax, float fovDegrees)
Reset the camera to frame a model whose bounding box spans [zMin, zMax].
static int getHDModelForFileID(int)
Get the HD model file ID for a given file ID.
Represents an equipped item on a character model.
Definition WoWItem.h:39
void setDisplayId(int id)
Definition WoWItem.cpp:112
void setId(int id)
Definition WoWItem.cpp:34
Core WoW .m2 model: geometry, animation, textures, and character data.
Definition WoWModel.h:50
std::vector< ModelAnimation > anims
Definition WoWModel.h:178
size_t currentAnim
Definition WoWModel.h:183
WoWItem * getItem(CharSlots slot)
std::map< int, std::wstring > getAnimsMap()
RaceInfos infos
Definition WoWModel.h:215
glm::vec3 pos_
Definition WoWModel.h:163
bool animated
Definition WoWModel.h:172
void updateTextureList(GameFile *tex, int special)
std::vector< uint > replacableParticleColorIDs
Definition WoWModel.h:114
void setCreatureGeosetData(std::set< GeosetNum > cgd)
glm::vec3 rot_
Definition WoWModel.h:164
std::vector< int16 > animLookups
Definition WoWModel.h:179
float scale_
Definition WoWModel.h:160
static std::string getCGGroupName(CharGeosets cg)
void refresh()
CharDetails cd
Definition WoWModel.h:214
bool bSheathe
Definition WoWModel.h:220
std::vector< ModelGeosetHD * > geosets
Definition WoWModel.h:149
AnimManager * animManager
Definition WoWModel.h:180
std::vector< ModelVertex > origVertices
Definition WoWModel.h:132
GameFile * gamefile
Definition WoWModel.h:112
@ ANIMATION_MOUNT
std::string wstringToString(const std::wstring &ws)
Convert a wide string to narrow ASCII (lossy, for display only).
void initAnimationControl(WoWModel *model, AppState &app)
Populate app.animEntries / app.skinEntries from the model.
void tryToEquipItem(WoWModel *model, int id, AppState &app, const ItemDatabase &db)
void rebuildItemBrowseFilter(AppState &app, const ItemDatabase &db)
void rebuildMountFilter(AppState &app)
void applySkin(WoWModel *model, int skinIndex, AppState &app)
Apply a creature / item skin variant to the model.
void mountCharacter(int displayID, GameFile *creatureFile, AppState &app, float fov)
void rebuildNpcFilter(AppState &app, const std::vector< NPCRecord > &npcList)
void loadModel(GameFile *file, AppState &app, float fov)
Load a .m2 GameFile, create a WoWModel, attach to root, init controls.
void buildStartOutfits(WoWModel *model, AppState &app)
void initModelControl(WoWModel *model, AppState &app)
Populate app.geosetGroups and app.pcrState from the model.
static bool correctType(int type, int slot)
void clearModel(AppState &app)
Tear down the current model and reset related state.
void applyStartOutfit(WoWModel *model, int outfitId, AppState &app, const ItemDatabase &db)
void rebuildEquipFilteredItems(AppState &app, const ItemDatabase &db)
void rebuildStartOutfitFilter(AppState &app)
void initCharacterControl(WoWModel *model, AppState &app)
Populate app.customizationOptions from the model's ChrCustomization DB.
void dismountCharacter(AppState &app, float fov)
void loadNPC(unsigned int creatureID, AppState &app, float fov)
void loadItemModel(unsigned int itemId, AppState &app, float fov)
void applyItemSet(WoWModel *model, int setId, AppState &app, const ItemDatabase &db)
void rebuildItemSetFilter(AppState &app)
void resetCameraToModel(OrbitCamera &camera, const WoWModel *model, float fov)
Reset the orbit camera to frame the given model.
void buildMountList(AppState &app)
WoWModel * getLoadedModel(AppState &app)
Return the currently loaded WoWModel (first child of root), or nullptr.
void buildItemSets(AppState &app)
bool containsIgnoreCase(const std::string &s, const std::string &substr)
bool startsWithIgnoreCase(const std::string &s, const std::string &prefix)
std::string toLower(const std::string &s)
Reusable animation entry — matches CharacterViewerPanel::AnimEntry.
std::set< int > creatureGeosetData
std::vector< SkinEntry > skinEntries
Definition AppState.h:164
int selectedSecondaryAnim
Definition AppState.h:159
int selectedMouthAnim
Definition AppState.h:160
int blpSkin[3]
Definition AppState.h:166
float mouthSpeed
Definition AppState.h:161
float animSpeed
Definition AppState.h:157
int selectedAnimCombo
Definition AppState.h:156
std::vector< AnimEntry > animEntries
Definition AppState.h:155
Top-level aggregate of all mutable application state.
Definition AppState.h:261
LoadingState loading
Definition AppState.h:263
SceneState scene
Definition AppState.h:262
AnimationState anim
Definition AppState.h:265
ExportState exporting
Definition AppState.h:268
CharacterState character
Definition AppState.h:266
BrowserState browsers
Definition AppState.h:267
std::string npcSearchBuf
Definition AppState.h:202
std::vector< size_t > itemBrowseFiltered
Definition AppState.h:208
bool itemBrowseFilterDirty
Definition AppState.h:209
std::vector< MountEntry > mountList
Definition AppState.h:212
std::string itemBrowseSearchBuf
Definition AppState.h:207
std::vector< std::string > creatureModelNames
Definition AppState.h:214
std::vector< StartOutfitEntry > startOutfits
Definition AppState.h:195
bool mountFilterDirty
Definition AppState.h:219
std::string itemSetSearchBuf
Definition AppState.h:190
std::vector< size_t > mountFiltered
Definition AppState.h:218
std::vector< size_t > npcFiltered
Definition AppState.h:203
ParticleColorState pcrState
Definition AppState.h:223
bool npcFilterDirty
Definition AppState.h:204
std::string mountSearchBuf
Definition AppState.h:216
std::vector< size_t > startOutfitFiltered
Definition AppState.h:198
bool mountListBuilt
Definition AppState.h:215
std::vector< GeosetGroupEntry > geosetGroups
Definition AppState.h:222
std::string startOutfitSearchBuf
Definition AppState.h:197
std::vector< GameFile * > creatureModels
Definition AppState.h:213
std::vector< ItemSetEntry > itemSets
Definition AppState.h:188
bool itemSetFilterDirty
Definition AppState.h:192
bool startOutfitsBuilt
Definition AppState.h:196
bool startOutfitFilterDirty
Definition AppState.h:199
std::vector< size_t > itemSetFiltered
Definition AppState.h:191
bool itemSetsBuilt
Definition AppState.h:189
std::vector< CustomizationOption > customizationOptions
Definition AppState.h:174
std::string equipSearchBuf
Definition AppState.h:175
int equipSlotLevels[NUM_CHAR_SLOTS]
Definition AppState.h:179
int equipSlotToEdit
Definition AppState.h:176
std::vector< size_t > equipFilteredItems
Definition AppState.h:178
std::string exportStatus
Definition AppState.h:234
std::vector< char > exportAnimChecked
Definition AppState.h:235
A single equipment item record from the item database.
Definition database.h:26
int slot()
Definition database.cpp:37
std::string name
Display name of the item.
Definition database.h:27
An item set from the ItemSet DB2 table.
std::string name
Display name.
A starter outfit from the CharStartOutfit DB2 table.
std::string name
Display name.
bool isWoWLoaded
Definition AppState.h:91
std::atomic< bool > initDB
Definition AppState.h:92
A mount entry from the CreatureDisplayInfo DB2 table.
Definition MountsPanel.h:16
std::string name
Display name.
Definition MountsPanel.h:18
int displayID
Creature display ID.
Definition MountsPanel.h:17
bool isMounted
Definition AppState.h:83
bool isModel
Definition AppState.h:81
bool isChar
Definition AppState.h:82
OrbitCamera camera
Definition AppState.h:70
std::unique_ptr< Attachment > root
Definition AppState.h:71
WoWModel * selModel
Definition AppState.h:72
Single geoset entry referencing a model geoset by index and id.
std::string label
Human-readable label for the geoset.
size_t index
Index into model->geosets[].
A named group of geosets sharing the same mesh id.
std::string name
Display name for the group.
size_t meshId
Shared mesh id for geosets in this group.
bool hasSet[3]
Which colour IDs (11, 12, 13) are present in the model.
unsigned int uint
Definition types.h:36
@ IT_THROWN
Definition wow_enums.h:257
@ IT_BOOTS
Definition wow_enums.h:240
@ IT_LEFTHANDED
Definition wow_enums.h:254
@ IT_SHIELD
Definition wow_enums.h:246
@ IT_OFFHAND
Definition wow_enums.h:255
@ IT_CAPE
Definition wow_enums.h:248
@ IT_TABARD
Definition wow_enums.h:251
@ IT_BOW
Definition wow_enums.h:247
@ IT_ROBE
Definition wow_enums.h:252
@ IT_PANTS
Definition wow_enums.h:239
@ IT_RIGHTHANDED
Definition wow_enums.h:253
@ IT_BRACERS
Definition wow_enums.h:241
@ IT_CHEST
Definition wow_enums.h:237
@ IT_SHIRT
Definition wow_enums.h:236
@ IT_ALL
Definition wow_enums.h:232
@ IT_GUN
Definition wow_enums.h:258
@ IT_QUIVER
Definition wow_enums.h:250
@ IT_BELT
Definition wow_enums.h:238
@ IT_GLOVES
Definition wow_enums.h:242
@ IT_SHOULDER
Definition wow_enums.h:235
@ IT_2HANDED
Definition wow_enums.h:249
@ IT_HEAD
Definition wow_enums.h:233
@ IT_DAGGER
Definition wow_enums.h:245
CharGeosets
Character geoset group identifiers (mesh IDs for body/armour regions).
Definition wow_enums.h:26
@ MT_CHAR
Definition wow_enums.h:214
@ MT_NORMAL
Definition wow_enums.h:213
CharSlots
Character equipment slot indices.
Definition wow_enums.h:5
@ CS_BRACERS
Definition wow_enums.h:13
@ CS_QUIVER
Definition wow_enums.h:19
@ CS_HAND_LEFT
Definition wow_enums.h:16
@ CS_BELT
Definition wow_enums.h:9
@ CS_CAPE
Definition wow_enums.h:17
@ CS_SHOULDER
Definition wow_enums.h:7
@ CS_PANTS
Definition wow_enums.h:11
@ CS_TABARD
Definition wow_enums.h:18
@ CS_CHEST
Definition wow_enums.h:12
@ CS_HAND_RIGHT
Definition wow_enums.h:15
@ CS_HEAD
Definition wow_enums.h:6
@ CS_BOOTS
Definition wow_enums.h:8
@ CS_SHIRT
Definition wow_enums.h:10
@ CS_GLOVES
Definition wow_enums.h:14
@ EGT_DEFAULT
Definition wow_enums.h:335
@ TEXTURE_GAMEOBJECT1
Definition wow_enums.h:321
@ TEXTURE_OBJECT_SKIN
Definition wow_enums.h:312