WoW Model Viewer
Your premiere tool for viewing, equipping and animating World of Warcraft models.
Loading...
Searching...
No Matches
CharDetails.cpp
Go to the documentation of this file.
1#include "CharDetails.h"
2
3#include "CharDetailsEvent.h"
4#include "DB2Table.h"
5#include "Game.h"
6#include "WoWModel.h"
7#include "Logger.h"
8#include "string_utils.h"
9
10#include <set>
11
13 eyeGlowType(EGT_NONE), showUnderwear(true), showEars(true), showHair(true),
14 showFacialHair(true), showFeet(true), autoHideGeosetsForHeadItems(true),
15 isNPC(true), model_(nullptr), isDemonHunter_(false)
16{
18}
19
20void CharDetails::save(pugi::xml_node& parentNode)
21{
22 pugi::xml_node node = parentNode.append_child("CharDetails");
23
24 for (const auto& opt : currentCustomization_)
25 {
26 pugi::xml_node child = node.append_child("customization");
27 child.append_attribute("id") = opt.first;
28 child.append_attribute("value") = opt.second;
29 }
30
31 node.append_child("eyeGlowType").append_attribute("value") = static_cast<int>(eyeGlowType);
32 node.append_child("showUnderwear").append_attribute("value") = showUnderwear ? 1 : 0;
33 node.append_child("showEars").append_attribute("value") = showEars ? 1 : 0;
34 node.append_child("showHair").append_attribute("value") = showHair ? 1 : 0;
35 node.append_child("showFacialHair").append_attribute("value") = showFacialHair ? 1 : 0;
36 node.append_child("showFeet").append_attribute("value") = showFeet ? 1 : 0;
37 node.append_child("isDemonHunter").append_attribute("value") = isDemonHunter_ ? 1 : 0;
38}
39
40void CharDetails::load(const std::string& f)
41{
42 pugi::xml_document doc;
43 pugi::xml_parse_result result = doc.load_file(f.c_str());
44 if (!result)
45 {
46 LOG_ERROR << "Fail to open" << f.c_str();
47 return;
48 }
49
50 pugi::xml_node charNode = doc.document_element();
51 // Navigate to CharDetails node if needed
52 if (std::string(charNode.name()) != "CharDetails")
53 charNode = charNode.child("CharDetails");
54
55 if (!charNode)
56 charNode = doc.child("model").child("CharDetails");
57
58 if (!charNode)
59 {
60 LOG_ERROR << "CharDetails node not found in" << f.c_str();
61 return;
62 }
63
64 for (pugi::xml_node child = charNode.first_child(); child; child = child.next_sibling())
65 {
66 const std::string name = child.name();
67
68 if (name == "customization")
69 set(child.attribute("id").as_uint(), child.attribute("value").as_uint());
70 else if (name == "eyeGlowType")
71 eyeGlowType = static_cast<EyeGlowTypes>(child.attribute("value").as_uint());
72 else if (name == "showUnderwear")
73 showUnderwear = child.attribute("value").as_uint();
74 else if (name == "showEars")
75 showEars = child.attribute("value").as_uint();
76 else if (name == "showHair")
77 showHair = child.attribute("value").as_uint();
78 else if (name == "showFacialHair")
79 showFacialHair = child.attribute("value").as_uint();
80 else if (name == "showFeet")
81 showFeet = child.attribute("value").as_uint();
82 else if (name == "isDemonHunter")
83 {
84 LOG_INFO << __FILE__ << __LINE__ << "reading demonHunter mode value";
85 setDemonHunterMode(child.attribute("value").as_uint());
86 }
87 }
88}
89
91{
92 if ((model != nullptr) & (model != model_))
93 {
94 model_ = model;
96 }
97
99
100 showUnderwear = true;
101 showHair = true;
102 showFacialHair = true;
103 showEars = true;
104 showFeet = false;
105
106 isNPC = false;
107
108 // Auto-enable demon hunter mode for Night Elf and Blood Elf races
110
111 // Set default options to their first available choice (only options without Flags & 0x20)
112 for (const auto& c : choicesPerOptionMap_)
113 {
114 if (!c.second.empty() && defaultOptionIds_.count(c.first))
115 currentCustomization_[c.first] = c.second[0];
116 }
117
118 // Single rebuild of all customization elements
120
123
124 // Notify observers for all options
125 for (const auto& c : choicesPerOptionMap_)
126 {
127 if (!c.second.empty())
128 {
130 event.setCustomizationOptionId(c.first);
131 notify(event);
132 }
133 }
134
135 if (model_)
136 model_->refresh();
137}
138
140{
141 reset();
142}
143
145{
146 if (!model_)
147 return;
148
149 // clear any previous value found
150 choicesPerOptionMap_.clear();
151 defaultOptionIds_.clear();
152
153 const auto infos = model_->infos;
154 if (infos.raceID == -1)
155 return;
156
157 const auto options = WOWDB.getTable("ChrCustomizationOption");
158
159 if (options)
160 {
161 // Collect matching options and sort by OrderIndex
162 struct OptionEntry { uint id; uint orderIndex; uint flags; };
163 std::vector<OptionEntry> matchingOptions;
164 for (const auto& row : *options)
165 {
166 if (row.getUInt("ChrModelID") == static_cast<uint32_t>(infos.ChrModelID[0]))
167 {
168 matchingOptions.push_back({static_cast<uint>(row.recordID()), row.getUInt("OrderIndex"), row.getUInt("Flags")});
169 }
170 }
171 std::sort(matchingOptions.begin(), matchingOptions.end(),
172 [](const OptionEntry& a, const OptionEntry& b) { return a.orderIndex < b.orderIndex; });
173
174 for (const auto& opt : matchingOptions)
175 {
176 choicesPerOptionMap_[opt.id] = {};
177
178 // Options without flag 0x20 get a default choice on reset (matches wow.export)
179 if (!(opt.flags & 0x20))
180 defaultOptionIds_.insert(opt.id);
181 }
182 }
183
184 for (const auto& option : choicesPerOptionMap_)
186}
187
189{
191 const auto originalVals = std::move(vals);
192 vals.clear();
193
194 // 1. fill direct values
195 const DB2Table* choicesTbl = WOWDB.getTable("ChrCustomizationChoice");
196 if (choicesTbl)
197 {
198 struct ChoiceEntry { uint id; uint orderIndex; };
199 std::vector<ChoiceEntry> matchingChoices;
200 for (const auto& row : *choicesTbl)
201 {
202 if (row.getUInt("ChrCustomizationOptionID") == chrCustomizationOption)
203 matchingChoices.push_back({static_cast<uint>(row.recordID()), row.getUInt("OrderIndex")});
204 }
205 std::sort(matchingChoices.begin(), matchingChoices.end(),
206 [](const ChoiceEntry& a, const ChoiceEntry& b) { return a.orderIndex < b.orderIndex; });
207
208 LOG_INFO << __FUNCTION__ << "DIRECT values" << matchingChoices.size();
209 for (const auto& c : matchingChoices)
210 vals.push_back(c.id);
211 }
212
213 if (vals != originalVals)
214 {
215 LOG_INFO << __FUNCTION__ << chrCustomizationOption;
216 std::string info;
217 for (const auto& v : vals)
218 info += std::to_string(v) + " ";
219 LOG_INFO << info;
220
222 event.setCustomizationOptionId(chrCustomizationOption);
223 notify(event);
224 }
225}
226
227void CharDetails::set(uint chrCustomizationOptionID, uint chrCustomizationChoiceID) // wow version >= 9.x
228{
229 if (!model_)
230 return;
231
232 const auto infos = model_->infos;
233 if (infos.raceID == -1)
234 return;
235
237
239
240 // Full rebuild of all customization elements using the active choice set
242
244 event.setCustomizationOptionId(chrCustomizationOptionID);
245 notify(event);
246
247 model_->refresh();
248 // TEXTUREMANAGER.dump();
249}
250
251std::vector<uint> CharDetails::getCustomizationChoices(const uint chrCustomizationOptionID)
252{
255
257 if (it != choicesPerOptionMap_.end())
258 return it->second;
259 return {};
260}
261
262uint CharDetails::get(uint chrCustomizationOptionID) const
263{
265 if (it != currentCustomization_.end())
266 return it->second;
267 return 0;
268}
269
271{
273
274 const DB2Table* elementsTbl = WOWDB.getTable("ChrCustomizationElement");
275 if (!elementsTbl)
276 return;
277
278 // Build set of active choice IDs for fast lookup
279 std::set<uint> activeChoiceIds;
280 for (const auto& [optionId, choiceId] : currentCustomization_)
282
283 const DB2Table* geosetTbl = WOWDB.getTable("ChrCustomizationGeoset");
284 const DB2Table* skinnedTbl = WOWDB.getTable("ChrCustomizationSkinnedModel");
285 const DB2Table* matTbl = WOWDB.getTable("ChrCustomizationMaterial");
286 const DB2Table* texFileTbl = WOWDB.getTable("TextureFileData");
287 const DB2Table* layerTbl = WOWDB.getTable("ChrModelTextureLayer");
288
289 // For each active choice, find and apply matching elements
290 for (const auto& [optionId, choiceId] : currentCustomization_)
291 {
292 for (const auto& row : *elementsTbl)
293 {
294 if (row.getUInt("ChrCustomizationChoiceID") != choiceId)
295 continue;
296
297 const uint eltId = static_cast<uint>(row.recordID());
298 const uint geosetID = row.getUInt("ChrCustomizationGeosetID");
299 const uint skinnedModelID = row.getUInt("ChrCustomizationSkinnedModelID");
300 const uint materialID = row.getUInt("ChrCustomizationMaterialID");
301
302 // Geosets: apply regardless of RelatedChrCustomizationChoiceID (matches wow.export)
303 if (geosetID != 0 && geosetTbl)
304 {
305 LOG_INFO << "ChrCustomizationGeosetID based customization for" << eltId << "/" << geosetID;
306 DB2Row geoRow = geosetTbl->getRow(geosetID);
307 if (geoRow)
308 {
309 customizationElementsPerOption_[optionId].geosets.emplace_back(
310 geoRow.getUInt("GeosetType"), geoRow.getUInt("GeosetID"));
311 }
312 }
313
314 // Skinned models: apply regardless of RelatedChrCustomizationChoiceID
315 if (skinnedModelID != 0 && skinnedTbl)
316 {
317 LOG_INFO << "ChrCustomizationSkinnedModelID based customization for" << eltId << "/" << skinnedModelID;
319 if (skinRow)
320 {
321 customizationElementsPerOption_[optionId].models.emplace_back(
322 skinRow.getUInt("CollectionsFileDataID"),
323 std::make_pair(skinRow.getUInt("GeosetType"), skinRow.getUInt("GeosetID")));
324 }
325 }
326
327 // Materials: only apply if RelatedChrCustomizationChoiceID is 0 or in the active set
328 if (materialID != 0 && matTbl && texFileTbl && layerTbl)
329 {
330 const uint relatedChoiceId = row.getUInt("RelatedChrCustomizationChoiceID");
332 continue;
333
334 LOG_INFO << "ChrCustomizationMaterialID based customization for" << eltId << "/" << materialID;
335
336 DB2Row matRow = matTbl->getRow(materialID);
337 if (matRow)
338 {
339 const uint32_t materialResID = matRow.getUInt("MaterialResourcesID");
340 const uint32_t textureTargetID = matRow.getUInt("ChrModelTextureTargetID");
341
342 // Find FileDataID from TextureFileData
343 uint fileDataID = 0;
344 for (const auto& tfdRow : *texFileTbl)
345 {
346 if (tfdRow.getUInt("MaterialResourcesID") == materialResID)
347 {
348 fileDataID = tfdRow.getUInt("FileDataID");
349 break;
350 }
351 }
352
353 // Find ChrModelTextureLayer matching target and layout
354 uint layer = 0;
355 int bitmask = -1;
356 uint textureType = 0;
357 uint blendMode = 0;
358 for (const auto& layerRow : *layerTbl)
359 {
360 if (layerRow.getUInt("ChrModelTextureTargetID1") == textureTargetID &&
361 static_cast<int>(layerRow.getUInt("CharComponentTextureLayoutsID")) == model_->infos.textureLayoutID)
362 {
363 layer = layerRow.getUInt("Layer");
364 bitmask = layerRow.getInt("TextureSectionTypeBitMask");
365 textureType = layerRow.getUInt("TextureType");
366 blendMode = layerRow.getUInt("BlendMode");
367 break;
368 }
369 }
370
372 t.layer = layer;
374 t.type = textureType;
375 t.blendMode = blendMode;
376 t.fileId = fileDataID;
377
378 LOG_INFO << "texture ->" << "layer" << t.layer << "region" << t.region << "type" << t.type <<
379 "blendMode" << t.blendMode << "fileId" << t.fileId;
380
381 customizationElementsPerOption_[optionId].textures.push_back(t);
382 }
383 }
384 }
385 }
386}
387
389{
390 if (mask == -1)
391 return -1;
392
393 if (mask == 0)
394 return 0;
395
396 auto val = 1;
397
398 while (((mask = mask >> 1) & 0x01) == 0)
399 val++;
400
401 return val;
402}
403
410
411
413{
414 geosets.clear();
415
416 for (auto i = 0; i < NUM_GEOSETS; i++)
417 geosets[i] = 1;
418
419 // Eyeglow and Earrings/Piercings should be hidden by default (matches wow.export)
420 geosets[CG_EYEGLOW] = 0;
421 geosets[CG_EARRINGS] = 0;
422
423 if (showEars)
424 geosets[CG_EARS] = 2;
425 else
426 geosets[CG_EARS] = 0;
427
428 // apply customization elements
429 for (const auto& elt : customizationElementsPerOption_)
430 {
431 for (auto geo : elt.second.geosets)
432 {
433 // don't display ears if option is unchecked
434 if (geo.first == CG_EARS && !showEars)
435 continue;
436
437 // don't display hair if option is unchecked
438 if (geo.first == CG_SKIN_OR_HAIR && !showHair)
439 continue;
440
441 // don't display facial hairs if option is unchecked
442 if ((geo.first == CG_FACE_1 || geo.first == CG_FACE_2 || geo.first == CG_FACE_3) && !showFacialHair)
443 continue;
444
445 geosets[geo.first] = geo.second;
446 }
447 }
448
449 if (model_)
450 {
451 // only show underwear bottoms if the character isn't wearing pants or chest
453 {
454 // demon hunters and female pandaren use the TABARD2 geoset for part of their underwear:
457 }
458 else // hide underwear
459 {
460 // demon hunters and female pandaren - need to hide the TABARD2 geoset when no underwear:
463 }
464 }
465}
466
468{
469 textures.clear();
470
471 // apply customization elements
472 for (const auto& elt : customizationElementsPerOption_)
473 {
474 for (auto t : elt.second.textures)
475 {
476 if (model_ != nullptr)
477 {
478 // don't apply underwear tops/bras if show underwear is off or if the character is wearing a shirt or chest
479 if (t.region == CR_TORSO_UPPER &&
480 (!showUnderwear ||
482 continue;
483
484 // don't apply underwear bottoms if show underwear is off or if the character is wearing pants
485 if (t.region == CR_LEG_UPPER &&
486 (!showUnderwear ||
488 continue;
489 }
490
491 textures.push_back(t);
492 }
493 }
494}
495
497{
498 // first clean any previous merging
499 for (const auto& m : models_)
500 model_->unmergeModel(m.first);
501
502 models_.clear();
503
504 for (const auto& elt : customizationElementsPerOption_)
505 {
506 for (const auto m : elt.second.models)
507 {
508 auto* model = model_->mergeModel(m.first);
509 model->setGeosetGroupDisplay(static_cast<CharGeosets>(m.second.first), m.second.second);
510 models_.emplace_back(m.first, m.second);
511 }
512 }
513}
#define LOG_ERROR
Definition Logger.h:11
#define LOG_INFO
Definition Logger.h:10
#define WOWDB
Definition WoWDatabase.h:65
Event fired when character detail customisation options change.
@ CHOICE_LIST_CHANGED
A customisation choice list was modified.
void rebuildAllCustomizationElements()
EyeGlowTypes eyeGlowType
Definition CharDetails.h:91
std::map< uint, CustomizationElements > customizationElementsPerOption_
void setDemonHunterMode(bool val)
void save(pugi::xml_node &parentNode)
WoWModel * model_
void set(uint chrCustomizationOptionID, uint chrCustomizationChoiceID)
static int bitMaskToSectionType(int mask)
void reset(WoWModel *m=nullptr)
void refreshGeosets()
std::map< uint, std::vector< uint > > choicesPerOptionMap_
bool showFacialHair
Definition CharDetails.h:93
std::set< uint > defaultOptionIds_
bool showUnderwear
Definition CharDetails.h:93
void refreshSkinnedModels()
void fillCustomizationMap()
std::map< uint, uint > currentCustomization_
void refreshTextures()
std::map< uint, uint > geosets
Definition CharDetails.h:97
bool isDemonHunter_
void fillCustomizationMapForOption(uint chrCustomizationOption)
uint get(uint chrCustomizationOptionID) const
std::vector< uint > getCustomizationChoices(const uint chrCustomizationOptionID)
std::vector< TextureCustomization > textures
Definition CharDetails.h:98
std::vector< std::pair< uint, std::pair< uint, uint > > > models_
void load(const std::string &filepath)
Lightweight handle to a single row in a DB2Table.
Definition DB2Table.h:27
uint32_t getUInt(const std::string &field, unsigned int arrayIndex=0) const
Definition DB2Table.cpp:17
Provides typed, field-name-based access to records in a WDC DB2 file.
Definition DB2Table.h:50
void notify(Event &)
Broadcast an event to all attached observers.
int sexID
0 = male, 1 = female.
Definition RaceInfos.h:14
int textureLayoutID
Texture layout ID for compositing.
Definition RaceInfos.h:15
int raceID
Race ID (-1 = invalid).
Definition RaceInfos.h:13
Core WoW .m2 model: geometry, animation, textures, and character data.
Definition WoWModel.h:50
WoWModel * mergeModel(std::string name, int type=1, bool noRefresh=false)
RaceInfos infos
Definition WoWModel.h:215
int getItemId(CharSlots slot)
bool isWearingARobe()
void unmergeModel(std::string name)
void refresh()
void setGeosetGroupDisplay(CharGeosets group, int val)
unsigned int uint
Definition types.h:36
@ GENDER_FEMALE
Definition wow_enums.h:342
@ CR_LEG_UPPER
Definition wow_enums.h:148
@ CR_TORSO_UPPER
Definition wow_enums.h:146
CharGeosets
Character geoset group identifiers (mesh IDs for body/armour regions).
Definition wow_enums.h:26
@ CG_FACE_1
Definition wow_enums.h:28
@ CG_EARS
Definition wow_enums.h:34
@ CG_SKIN_OR_HAIR
Definition wow_enums.h:27
@ CG_EYEGLOW
Definition wow_enums.h:43
@ NUM_GEOSETS
Definition wow_enums.h:70
@ CG_FACE_3
Definition wow_enums.h:30
@ CG_FACE_2
Definition wow_enums.h:29
@ CG_DH_LOINCLOTH
Definition wow_enums.h:41
@ CG_EARRINGS
Definition wow_enums.h:61
@ CS_PANTS
Definition wow_enums.h:11
@ CS_CHEST
Definition wow_enums.h:12
@ CS_SHIRT
Definition wow_enums.h:10
EyeGlowTypes
Definition wow_enums.h:333
@ EGT_NONE
Definition wow_enums.h:334
@ RACE_NIGHTELF
Definition wow_enums.h:354
@ RACE_BLOODELF
Definition wow_enums.h:360
@ RACE_PANDAREN
Definition wow_enums.h:374