34 s.push_back(
static_cast<char>(c & 0x7F));
42 return att ?
dynamic_cast<WoWModel*
>(att->model()) :
nullptr;
57 if (v.pos.z < zMin) zMin = v.pos.z;
58 if (v.pos.z > zMax) zMax = v.pos.z;
65 if (!model || skinIndex < 0 || skinIndex >=
static_cast<int>(app.
anim.
skinEntries.size()))
70 for (
size_t i = 0; i < skin.count; ++i)
100 for (
size_t i = 0; i < model->
anims.size(); ++i)
103 auto it = animsMap.find(model->
anims[i].animID);
104 if (it != animsMap.end())
107 e.
label =
"Anim " + std::to_string(model->
anims[i].animID) +
" [" + std::to_string(i) +
"]";
111 if (model->
anims[i].animID == 0 && standIndex < 0)
118 int useAnim = (standIndex >= 0) ? app.
anim.
animEntries[standIndex].animIndex : 0;
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"));
131 const auto* cdiTable =
WOWDB.getTable(
"CreatureDisplayInfo");
132 const auto* cmdTable =
WOWDB.getTable(
"CreatureModelData");
133 const auto* geoTable =
WOWDB.getTable(
"CreatureDisplayInfoGeosetData");
136 if (cdiTable && cmdTable && geoTable)
137 for (
const auto& cdiRow : *cdiTable)
139 auto cmdRow = cmdTable->getRow(cdiRow.getUInt(
"ModelID"));
140 if (!cmdRow ||
static_cast<int>(cmdRow.getUInt(
"FileDataID")) != targetFDID)
145 uint32_t texFDIDs[3] = {
146 cdiRow.getUInt(
"TextureVariationFileDataID1"),
147 cdiRow.getUInt(
"TextureVariationFileDataID2"),
148 cdiRow.getUInt(
"TextureVariationFileDataID3")
150 for (
size_t s = 0; s < 3; ++s)
152 if (texFDIDs[s] != 0)
155 if (se.
tex[s]) ++cnt;
158 if (cnt == 0)
continue;
162 uint32_t cdi = cdiRow.recordID();
163 for (
const auto& geoRow : *geoTable)
165 if (geoRow.getUInt(
"CreatureDisplayInfoID") != cdi)
167 int geoType = 100 * (
static_cast<int>(geoRow.getUInt(
"GeosetIndex")) + 1);
168 int geoId =
static_cast<int>(geoRow.getUInt(
"GeosetValue"));
178 const auto* idiTable =
WOWDB.getTable(
"ItemDisplayInfo");
179 const auto* texFDTable =
WOWDB.getTable(
"TextureFileData");
180 const auto* modFDTable =
WOWDB.getTable(
"ModelFileData");
183 std::set<uint32_t> matchingModelResIDs;
184 if (idiTable && texFDTable && modFDTable)
186 for (
const auto& mfdRow : *modFDTable)
188 if (
static_cast<int>(mfdRow.getUInt(
"FileDataID")) == targetFDID)
189 matchingModelResIDs.insert(mfdRow.getUInt(
"ModelResourcesID"));
192 for (
const auto& idiRow : *idiTable)
194 if (matchingModelResIDs.find(idiRow.getUInt(
"ModelResourcesID1")) == matchingModelResIDs.end())
197 uint32_t matResID = idiRow.getUInt(
"ModelMaterialResourcesID1");
198 for (
const auto& tfdRow : *texFDTable)
200 if (tfdRow.getUInt(
"MaterialResourcesID") != matResID)
202 uint32_t fdid = tfdRow.getUInt(
"FileDataID");
203 if (fdid == 0)
continue;
206 if (!se.
tex[0])
continue;
227 auto& cd = model->
cd;
231 cd.showFacialHair =
true;
233 cd.autoHideGeosetsForHeadItems =
true;
238 const auto& infos = model->
infos;
239 if (infos.raceID == -1 || infos.ChrModelID.empty())
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]);
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)
252 if (row.getUInt(
"ChrModelID") == targetModelID)
253 matchingOptions.push_back({ row.recordID(), row.getUInt(
"OrderIndex"), row.getUInt(
"Flags"),
254 row.getUInt(
"ChrCustomizationCategoryID") });
256 std::sort(matchingOptions.begin(), matchingOptions.end(),
257 [](
const OptionEntry& a,
const OptionEntry& b) { return a.orderIndex < b.orderIndex; });
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)
264 uint32_t optID = row.getUInt(
"ChrCustomizationOptionID");
265 std::string cname = row.getString(
"Name_Lang");
266 uint32_t orderIdx = row.getUInt(
"OrderIndex");
268 cname =
"Choice " + std::to_string(orderIdx);
269 choicesByOption[optID].push_back({ row.recordID(), orderIdx, std::move(cname) });
271 for (
auto& [optID, choices] : choicesByOption)
273 std::sort(choices.begin(), choices.end(),
274 [](
const ChoiceData& a,
const ChoiceData& b) { return a.orderIndex < b.orderIndex; });
277 for (
const auto& optEntry : matchingOptions)
279 unsigned int optionID = optEntry.id;
286 auto optRow = optTable->getRow(optionID);
289 std::string n = optRow.getString(
"Name_Lang");
290 opt.
name = n.empty() ? (
"Option " + std::to_string(optEntry.orderIndex)) : n;
294 opt.
name =
"Option " + std::to_string(optEntry.orderIndex);
298 auto it = choicesByOption.find(optionID);
299 if (it == choicesByOption.end() || it->second.empty())
302 for (
const auto& choice : it->second)
309 unsigned int cur = cd.get(optionID);
311 for (
size_t c = 0; c < opt.
choiceIDs.size(); ++c)
320 app.character.customizationOptions.push_back(std::move(opt));
334 std::map<size_t, size_t> meshToGroupIdx;
335 for (
size_t i = 0; i < model->
geosets.size(); ++i)
337 size_t mesh = model->
geosets[i]->id / 100;
338 if (meshToGroupIdx.find(mesh) == meshToGroupIdx.end())
343 group.
name = groupName.empty() ? std::to_string(mesh) : groupName;
351 ge.
label = std::format(
"{} [{}, {}, {}]", i, mesh,
388 default:
return false;
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);
403 for (
size_t i = 0; i < db.
items.size(); ++i)
405 const auto& item = db.
items[i];
418 if (
id == 0 || !model)
424 LOG_ERROR <<
"Cannot retrieve item from database (id " <<
id <<
")";
428 int itemSlot = itemr.
slot();
431 LOG_ERROR <<
"Cannot determine slot for object " << itemr.
name;
452 const auto* itemSetTable =
WOWDB.getTable(
"ItemSet");
453 if (!itemSetTable)
return;
454 for (
const auto& row : *itemSetTable)
457 e.
id =
static_cast<int>(row.recordID());
458 e.
name = row.getString(
"Name_Lang");
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);
492 if (!model || setId <= 0)
495 const auto* itemSetTable =
WOWDB.getTable(
"ItemSet");
496 if (!itemSetTable)
return;
497 auto setRow = itemSetTable->getRow(
static_cast<uint32_t
>(setId));
500 LOG_ERROR <<
"Item set query failed for ID " << setId;
504 for (
const auto it : *model)
508 for (
unsigned i = 0; i < 8; ++i)
510 std::string fieldName =
"ItemID" + std::to_string(i + 1);
511 uint32_t itemID = setRow.getUInt(fieldName);
518 catch (
const std::exception& e)
520 LOG_ERROR <<
"Failed to equip item set entry " << i
521 <<
" (id=" << itemID <<
"): " << e.what();
526 LOG_INFO <<
"Applied item set ID " << setId;
539 const auto& infos = model->
infos;
540 if (infos.raceID == -1)
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);
549 for (
const auto& row : *csoTable)
551 if (row.getUInt(
"raceID") != targetRace || row.getUInt(
"sexID") != targetSex)
554 auto classRow = chrClassTable->getRow(row.getUInt(
"classID"));
555 std::string className = classRow ? classRow.getString(
"Filename") :
"";
559 e.
id =
static_cast<int>(row.recordID());
560 if (!e.
name.empty() && e.
id > 0)
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);
593 if (!model || outfitId <= 0)
596 const auto* csoTable =
WOWDB.getTable(
"CharStartOutfit");
597 if (!csoTable)
return;
598 auto csoRow = csoTable->getRow(
static_cast<uint32_t
>(outfitId));
601 LOG_ERROR <<
"Start outfit query failed for ID " << outfitId;
605 for (
const auto it : *model)
609 for (
unsigned i = 0; i < 24; ++i)
611 std::string fieldName =
"iitem" + std::to_string(i + 1);
612 uint32_t itemID = csoRow.getUInt(fieldName);
619 catch (
const std::exception& ex)
621 LOG_ERROR <<
"Failed to equip start outfit entry " << i
622 <<
" (id=" << itemID <<
"): " << ex.what();
627 LOG_INFO <<
"Applied start outfit ID " << outfitId;
677 auto* model =
new WoWModel(file,
true);
687 const std::string fn = file->
fullname();
709 model->charModelDetails.isChar =
true;
728 LOG_INFO <<
"Model loaded: " << model->name();
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);
742 for (
size_t i = 0; i < npcList.size(); ++i)
744 const auto& npc = npcList[i];
745 if (npc.model == 0)
continue;
756 const auto* creatureTable =
WOWDB.getTable(
"Creature");
757 if (!creatureTable)
return;
758 auto creatureRow = creatureTable->getRow(creatureID);
761 LOG_ERROR <<
"NPC query failed for ID " << creatureID;
765 uint32_t displayInfoID = creatureRow.getUInt(
"DisplayID1");
766 const auto* cdiTable =
WOWDB.getTable(
"CreatureDisplayInfo");
767 if (!cdiTable)
return;
768 auto cdiRow = cdiTable->getRow(displayInfoID);
771 LOG_ERROR <<
"NPC query failed for ID " << creatureID <<
" (no CreatureDisplayInfo for DisplayID1=" << displayInfoID <<
")";
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");
789 uint32_t texFDIDs[3] = {
790 cdiRow.getUInt(
"TextureVariationFileDataID1"),
791 cdiRow.getUInt(
"TextureVariationFileDataID2"),
792 cdiRow.getUInt(
"TextureVariationFileDataID3")
797 for (
size_t t = 0; t < 3 && match; ++t)
802 if (texFDIDs[t] != 0)
803 match = (fdid ==
static_cast<int>(texFDIDs[t]));
825 const auto* cdieTable =
WOWDB.getTable(
"CreatureDisplayInfoExtra");
826 auto cdieRow = cdieTable ? cdieTable->getRow(extraId) :
DB2Row();
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},
842 for (
const auto& npcRow : *npcSlotTable)
844 if (npcRow.getUInt(
"NpcModelID") != extraId)
846 auto it = ItemTypeToInternal.find(
static_cast<int>(npcRow.getUInt(
"ItemSlot")));
847 if (it != ItemTypeToInternal.end())
851 item->
setDisplayId(
static_cast<int>(npcRow.getUInt(
"ItemDisplayInfoID")));
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);
871 for (
size_t i = 0; i < db.
items.size(); ++i)
873 const auto& item = db.
items[i];
874 if (item.id == 0)
continue;
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;
894 uint32_t itemAppearanceID = 0;
895 for (
const auto& row : *imaTable)
897 if (row.getUInt(
"ItemID") == itemId)
899 itemAppearanceID = row.getUInt(
"ItemAppearanceID");
903 if (itemAppearanceID == 0)
return;
905 auto iaRow = iaTable->getRow(itemAppearanceID);
908 uint32_t displayInfoID = iaRow.getUInt(
"ItemDisplayInfoID");
909 auto idiRow = idiTable->getRow(displayInfoID);
912 uint32_t modelResID = idiRow.getUInt(
"ModelResourcesID1");
913 uint32_t modelFDID = 0;
914 for (
const auto& mfdRow : *modFDTable)
916 if (mfdRow.getUInt(
"ModelResourcesID") == modelResID)
918 modelFDID = mfdRow.getUInt(
"FileDataID");
922 if (modelFDID == 0)
return;
932 uint32_t matResID = idiRow.getUInt(
"ModelMaterialResourcesID1");
933 for (
const auto& tfdRow : *texFDTable)
935 if (tfdRow.getUInt(
"MaterialResourcesID") == matResID)
937 uint32_t texFDID = tfdRow.getUInt(
"FileDataID");
951 LOG_ERROR <<
"Exception loading item model for ID " << itemId;
966 const auto* mountTable =
WOWDB.getTable(
"Mount");
967 const auto* mxdTable =
WOWDB.getTable(
"MountXDisplay");
968 if (mountTable && mxdTable)
969 for (
const auto& mxdRow : *mxdTable)
971 uint32_t mountID = mxdRow.getUInt(
"MountID");
972 auto mountRow = mountTable->getRow(mountID);
974 me.
displayID =
static_cast<int>(mxdRow.getUInt(
"CreatureDisplayInfoID"));
975 me.
name = mountRow ? mountRow.getString(
"Name_Lang") :
"";
982 std::vector<GameFile*> files;
983 GAMEDIRECTORY.getFilesForFolder(files, std::string(
"creature/"), std::string(
"m2"));
984 for (
auto* gf : files)
987 std::string n = gf->fullname();
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]; });
1000 for (
size_t i = 0; i < indices.size(); ++i)
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);
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));
1063 LOG_ERROR <<
"Mount display query failed for displayID " << displayID;
1066 auto cmdRow = cmdTable->getRow(cdiRow.getUInt(
"ModelID"));
1067 uint32_t mountFDID = cmdRow ? cmdRow.getUInt(
"FileDataID") : 0;
1070 LOG_ERROR <<
"Mount display query failed for displayID " << displayID;
1075 else if (creatureFile)
1077 modelFile = creatureFile;
1087 auto* mountModel =
new WoWModel(modelFile,
false);
1088 if (!mountModel->ok)
1090 LOG_ERROR <<
"Mount model failed to load.";
1094 mountModel->isMount =
true;
1101 const auto* cdiTexTable =
WOWDB.getTable(
"CreatureDisplayInfo");
1102 if (!cdiTexTable)
return;
1103 auto cdiTexRow = cdiTexTable->getRow(
static_cast<uint32_t
>(morphID));
1106 static const char* texFields[] = {
1107 "TextureVariationFileDataID1",
1108 "TextureVariationFileDataID2",
1109 "TextureVariationFileDataID3"
1111 for (
size_t t = 0; t < 3; ++t)
1113 uint32_t texFDID = cdiTexRow.getUInt(texFields[t]);
1135 charModel->
rot_ = charModel->
pos_ = glm::vec3(0.0f);
1136 charModel->
scale_ = 1.0f;
1137 mountModel->rot_.x = 0.0f;
1168 charModel->
scale_ = 1.0f;
1169 charModel->
rot_ = charModel->
pos_ = glm::vec3(0.0f);
1177 LOG_INFO <<
"Character dismounted.";
TextureManager TEXTUREMANAGER
void SetAnim(short index, unsigned int id, short loop)
void SetSpeed(float speed)
Scene-graph node that attaches a Displayable to a parent bone slot.
Displayable * model() const
void set(uint chrCustomizationOptionID, uint chrCustomizationChoiceID)
@ FACIAL_CUSTOMIZATION_STYLE
@ ADDITIONAL_FACIAL_CUSTOMIZATION
@ FACIAL_CUSTOMIZATION_COLOR
Lightweight handle to a single row in a DB2Table.
Abstract base class representing a file within the game data archive.
const std::string & fullname() const
In-memory item database loaded from the CSV item list.
std::vector< ItemRecord > items
const ItemRecord & getById(int id) const
const std::string & itemName() const
Orbit camera that revolves around a target point.
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.
void setDisplayId(int id)
Core WoW .m2 model: geometry, animation, textures, and character data.
std::vector< ModelAnimation > anims
WoWItem * getItem(CharSlots slot)
std::map< int, std::wstring > getAnimsMap()
void updateTextureList(GameFile *tex, int special)
std::vector< uint > replacableParticleColorIDs
void setCreatureGeosetData(std::set< GeosetNum > cgd)
std::vector< int16 > animLookups
static std::string getCGGroupName(CharGeosets cg)
std::vector< ModelGeosetHD * > geosets
AnimManager * animManager
std::vector< ModelVertex > origVertices
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
int selectedSecondaryAnim
std::vector< AnimEntry > animEntries
Top-level aggregate of all mutable application state.
std::vector< size_t > itemBrowseFiltered
bool itemBrowseFilterDirty
std::vector< MountEntry > mountList
std::string itemBrowseSearchBuf
std::vector< std::string > creatureModelNames
std::vector< StartOutfitEntry > startOutfits
std::string itemSetSearchBuf
std::vector< size_t > mountFiltered
std::vector< size_t > npcFiltered
ParticleColorState pcrState
std::string mountSearchBuf
std::vector< size_t > startOutfitFiltered
std::vector< GeosetGroupEntry > geosetGroups
std::string startOutfitSearchBuf
std::vector< GameFile * > creatureModels
std::vector< ItemSetEntry > itemSets
bool startOutfitFilterDirty
std::vector< size_t > itemSetFiltered
std::vector< CustomizationOption > customizationOptions
std::string equipSearchBuf
int equipSlotLevels[NUM_CHAR_SLOTS]
std::vector< size_t > equipFilteredItems
std::vector< std::string > choiceNames
std::vector< unsigned int > choiceIDs
std::vector< char > exportAnimChecked
A single equipment item record from the item database.
std::string name
Display name of the item.
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.
std::atomic< bool > initDB
A mount entry from the CreatureDisplayInfo DB2 table.
std::string name
Display name.
int displayID
Creature display ID.
std::unique_ptr< Attachment > root
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.
CharGeosets
Character geoset group identifiers (mesh IDs for body/armour regions).
CharSlots
Character equipment slot indices.