mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-04-03 20:03:50 +00:00
chore(application): extract entity spawner + composer, apply app and UI updates
- add include/core/appearance_composer.hpp + src/core/appearance_composer.cpp - update include/core/application.hpp + src/core/application.cpp - update src/ui/game_screen.cpp - adjust CMakeLists.txt and README.md for new composer module
This commit is contained in:
parent
b10a2c28d6
commit
afeaa13562
7 changed files with 544 additions and 375 deletions
|
|
@ -487,6 +487,7 @@ set(WOWEE_SOURCES
|
||||||
# Core
|
# Core
|
||||||
src/core/application.cpp
|
src/core/application.cpp
|
||||||
src/core/entity_spawner.cpp
|
src/core/entity_spawner.cpp
|
||||||
|
src/core/appearance_composer.cpp
|
||||||
src/core/window.cpp
|
src/core/window.cpp
|
||||||
src/core/input.cpp
|
src/core/input.cpp
|
||||||
src/core/logger.cpp
|
src/core/logger.cpp
|
||||||
|
|
|
||||||
10
README.md
10
README.md
|
|
@ -347,3 +347,13 @@ This project does not include any Blizzard Entertainment proprietary data, asset
|
||||||
## Known Issues
|
## Known Issues
|
||||||
|
|
||||||
MANY issues this is actively under development
|
MANY issues this is actively under development
|
||||||
|
|
||||||
|
## Star History
|
||||||
|
|
||||||
|
<a href="https://www.star-history.com/?repos=Kelsidavis%2FWoWee&type=date&legend=top-left">
|
||||||
|
<picture>
|
||||||
|
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/image?repos=Kelsidavis/WoWee&type=date&theme=dark&legend=top-left" />
|
||||||
|
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/image?repos=Kelsidavis/WoWee&type=date&legend=top-left" />
|
||||||
|
<img alt="Star History Chart" src="https://api.star-history.com/image?repos=Kelsidavis/WoWee&type=date&legend=top-left" />
|
||||||
|
</picture>
|
||||||
|
</a>
|
||||||
|
|
|
||||||
101
include/core/appearance_composer.hpp
Normal file
101
include/core/appearance_composer.hpp
Normal file
|
|
@ -0,0 +1,101 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "game/character.hpp"
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
#include <unordered_set>
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
namespace wowee {
|
||||||
|
|
||||||
|
namespace rendering { class Renderer; }
|
||||||
|
namespace pipeline { class AssetManager; class DBCLayout; struct M2Model; }
|
||||||
|
namespace game { class GameHandler; }
|
||||||
|
|
||||||
|
namespace core {
|
||||||
|
|
||||||
|
class EntitySpawner;
|
||||||
|
|
||||||
|
// Default (bare) geoset IDs per equipment group.
|
||||||
|
// Each group's base is groupNumber * 100; variant 01 is typically bare/default.
|
||||||
|
constexpr uint16_t kGeosetDefaultConnector = 101; // Group 1: default hair connector
|
||||||
|
constexpr uint16_t kGeosetBareForearms = 401; // Group 4: no gloves
|
||||||
|
constexpr uint16_t kGeosetBareShins = 503; // Group 5: no boots
|
||||||
|
constexpr uint16_t kGeosetDefaultEars = 702; // Group 7: ears
|
||||||
|
constexpr uint16_t kGeosetBareSleeves = 801; // Group 8: no chest armor sleeves
|
||||||
|
constexpr uint16_t kGeosetDefaultKneepads = 902; // Group 9: kneepads
|
||||||
|
constexpr uint16_t kGeosetDefaultTabard = 1201; // Group 12: tabard base
|
||||||
|
constexpr uint16_t kGeosetBarePants = 1301; // Group 13: no leggings
|
||||||
|
constexpr uint16_t kGeosetNoCape = 1501; // Group 15: no cape
|
||||||
|
constexpr uint16_t kGeosetWithCape = 1502; // Group 15: with cape
|
||||||
|
constexpr uint16_t kGeosetBareFeet = 2002; // Group 20: bare feet
|
||||||
|
|
||||||
|
/// Resolved texture paths from CharSections.dbc for player character compositing.
|
||||||
|
struct PlayerTextureInfo {
|
||||||
|
std::string bodySkinPath;
|
||||||
|
std::string faceLowerPath;
|
||||||
|
std::string faceUpperPath;
|
||||||
|
std::string hairTexturePath;
|
||||||
|
std::vector<std::string> underwearPaths;
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Handles player character visual appearance: skin compositing, geoset selection,
|
||||||
|
/// texture path lookups, and equipment weapon rendering.
|
||||||
|
class AppearanceComposer {
|
||||||
|
public:
|
||||||
|
AppearanceComposer(rendering::Renderer* renderer,
|
||||||
|
pipeline::AssetManager* assetManager,
|
||||||
|
game::GameHandler* gameHandler,
|
||||||
|
pipeline::DBCLayout* dbcLayout,
|
||||||
|
EntitySpawner* entitySpawner);
|
||||||
|
|
||||||
|
// Player model path resolution
|
||||||
|
std::string getPlayerModelPath(game::Race race, game::Gender gender) const;
|
||||||
|
|
||||||
|
// Phase 1: Resolve texture paths from CharSections.dbc and fill model texture slots.
|
||||||
|
// Call BEFORE charRenderer->loadModel().
|
||||||
|
PlayerTextureInfo resolvePlayerTextures(pipeline::M2Model& model,
|
||||||
|
game::Race race, game::Gender gender,
|
||||||
|
uint32_t appearanceBytes);
|
||||||
|
|
||||||
|
// Phase 2: Apply composited textures to loaded model instance.
|
||||||
|
// Call AFTER charRenderer->loadModel(). Saves skin state for re-compositing.
|
||||||
|
void compositePlayerSkin(uint32_t modelSlotId, const PlayerTextureInfo& texInfo);
|
||||||
|
|
||||||
|
// Build default active geosets for player character
|
||||||
|
std::unordered_set<uint16_t> buildDefaultPlayerGeosets(uint8_t hairStyleId, uint8_t facialId);
|
||||||
|
|
||||||
|
// Equipment weapon loading (reads inventory, attaches weapon M2 models)
|
||||||
|
void loadEquippedWeapons();
|
||||||
|
|
||||||
|
// Weapon sheathe state
|
||||||
|
void setWeaponsSheathed(bool sheathed) { weaponsSheathed_ = sheathed; }
|
||||||
|
bool isWeaponsSheathed() const { return weaponsSheathed_; }
|
||||||
|
void toggleWeaponsSheathed() { weaponsSheathed_ = !weaponsSheathed_; }
|
||||||
|
|
||||||
|
// Saved skin state accessors (used by game_screen.cpp for equipment re-compositing)
|
||||||
|
const std::string& getBodySkinPath() const { return bodySkinPath_; }
|
||||||
|
const std::vector<std::string>& getUnderwearPaths() const { return underwearPaths_; }
|
||||||
|
uint32_t getSkinTextureSlotIndex() const { return skinTextureSlotIndex_; }
|
||||||
|
uint32_t getCloakTextureSlotIndex() const { return cloakTextureSlotIndex_; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool loadWeaponM2(const std::string& m2Path, pipeline::M2Model& outModel);
|
||||||
|
|
||||||
|
rendering::Renderer* renderer_;
|
||||||
|
pipeline::AssetManager* assetManager_;
|
||||||
|
game::GameHandler* gameHandler_;
|
||||||
|
pipeline::DBCLayout* dbcLayout_;
|
||||||
|
EntitySpawner* entitySpawner_;
|
||||||
|
|
||||||
|
// Saved at spawn for skin re-compositing on equipment changes
|
||||||
|
std::string bodySkinPath_;
|
||||||
|
std::vector<std::string> underwearPaths_;
|
||||||
|
uint32_t skinTextureSlotIndex_ = 0;
|
||||||
|
uint32_t cloakTextureSlotIndex_ = 0;
|
||||||
|
|
||||||
|
bool weaponsSheathed_ = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace core
|
||||||
|
} // namespace wowee
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
#include "core/window.hpp"
|
#include "core/window.hpp"
|
||||||
#include "core/input.hpp"
|
#include "core/input.hpp"
|
||||||
#include "core/entity_spawner.hpp"
|
#include "core/entity_spawner.hpp"
|
||||||
|
#include "core/appearance_composer.hpp"
|
||||||
#include "game/character.hpp"
|
#include "game/character.hpp"
|
||||||
#include "game/game_services.hpp"
|
#include "game/game_services.hpp"
|
||||||
#include "pipeline/blp_loader.hpp"
|
#include "pipeline/blp_loader.hpp"
|
||||||
|
|
@ -73,9 +74,7 @@ public:
|
||||||
// Singleton access
|
// Singleton access
|
||||||
static Application& getInstance() { return *instance; }
|
static Application& getInstance() { return *instance; }
|
||||||
|
|
||||||
// Weapon loading (called at spawn and on equipment change)
|
|
||||||
void loadEquippedWeapons();
|
|
||||||
bool loadWeaponM2(const std::string& m2Path, pipeline::M2Model& outModel);
|
|
||||||
|
|
||||||
// Logout to login screen
|
// Logout to login screen
|
||||||
void logoutToLogin();
|
void logoutToLogin();
|
||||||
|
|
@ -85,23 +84,25 @@ public:
|
||||||
bool getRenderFootZForGuid(uint64_t guid, float& outFootZ) const;
|
bool getRenderFootZForGuid(uint64_t guid, float& outFootZ) const;
|
||||||
bool getRenderPositionForGuid(uint64_t guid, glm::vec3& outPos) const;
|
bool getRenderPositionForGuid(uint64_t guid, glm::vec3& outPos) const;
|
||||||
|
|
||||||
// Character skin composite state (saved at spawn for re-compositing on equipment change)
|
// Character skin composite state — delegated to AppearanceComposer
|
||||||
const std::string& getBodySkinPath() const { return bodySkinPath_; }
|
const std::string& getBodySkinPath() const { return appearanceComposer_ ? appearanceComposer_->getBodySkinPath() : emptyString_; }
|
||||||
const std::vector<std::string>& getUnderwearPaths() const { return underwearPaths_; }
|
const std::vector<std::string>& getUnderwearPaths() const { return appearanceComposer_ ? appearanceComposer_->getUnderwearPaths() : emptyStringVec_; }
|
||||||
uint32_t getSkinTextureSlotIndex() const { return skinTextureSlotIndex_; }
|
uint32_t getSkinTextureSlotIndex() const { return appearanceComposer_ ? appearanceComposer_->getSkinTextureSlotIndex() : 0; }
|
||||||
uint32_t getCloakTextureSlotIndex() const { return cloakTextureSlotIndex_; }
|
uint32_t getCloakTextureSlotIndex() const { return appearanceComposer_ ? appearanceComposer_->getCloakTextureSlotIndex() : 0; }
|
||||||
uint32_t getGryphonDisplayId() const { return entitySpawner_ ? entitySpawner_->getGryphonDisplayId() : 0; }
|
uint32_t getGryphonDisplayId() const { return entitySpawner_ ? entitySpawner_->getGryphonDisplayId() : 0; }
|
||||||
uint32_t getWyvernDisplayId() const { return entitySpawner_ ? entitySpawner_->getWyvernDisplayId() : 0; }
|
uint32_t getWyvernDisplayId() const { return entitySpawner_ ? entitySpawner_->getWyvernDisplayId() : 0; }
|
||||||
|
|
||||||
// Entity spawner access
|
// Entity spawner access
|
||||||
EntitySpawner* getEntitySpawner() { return entitySpawner_.get(); }
|
EntitySpawner* getEntitySpawner() { return entitySpawner_.get(); }
|
||||||
|
|
||||||
|
// Appearance composer access
|
||||||
|
AppearanceComposer* getAppearanceComposer() { return appearanceComposer_.get(); }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void update(float deltaTime);
|
void update(float deltaTime);
|
||||||
void render();
|
void render();
|
||||||
void setupUICallbacks();
|
void setupUICallbacks();
|
||||||
void spawnPlayerCharacter();
|
void spawnPlayerCharacter();
|
||||||
std::string getPlayerModelPath() const;
|
|
||||||
static const char* mapIdToName(uint32_t mapId);
|
static const char* mapIdToName(uint32_t mapId);
|
||||||
static const char* mapDisplayName(uint32_t mapId);
|
static const char* mapDisplayName(uint32_t mapId);
|
||||||
void loadOnlineWorldTerrain(uint32_t mapId, float x, float y, float z);
|
void loadOnlineWorldTerrain(uint32_t mapId, float x, float y, float z);
|
||||||
|
|
@ -123,6 +124,7 @@ private:
|
||||||
std::unique_ptr<game::ExpansionRegistry> expansionRegistry_;
|
std::unique_ptr<game::ExpansionRegistry> expansionRegistry_;
|
||||||
std::unique_ptr<pipeline::DBCLayout> dbcLayout_;
|
std::unique_ptr<pipeline::DBCLayout> dbcLayout_;
|
||||||
std::unique_ptr<EntitySpawner> entitySpawner_;
|
std::unique_ptr<EntitySpawner> entitySpawner_;
|
||||||
|
std::unique_ptr<AppearanceComposer> appearanceComposer_;
|
||||||
|
|
||||||
AppState state = AppState::AUTHENTICATION;
|
AppState state = AppState::AUTHENTICATION;
|
||||||
bool running = false;
|
bool running = false;
|
||||||
|
|
@ -140,11 +142,9 @@ private:
|
||||||
uint32_t spawnedAppearanceBytes_ = 0;
|
uint32_t spawnedAppearanceBytes_ = 0;
|
||||||
uint8_t spawnedFacialFeatures_ = 0;
|
uint8_t spawnedFacialFeatures_ = 0;
|
||||||
|
|
||||||
// Saved at spawn for skin re-compositing
|
// Static empty values for null-safe delegation
|
||||||
std::string bodySkinPath_;
|
static inline const std::string emptyString_;
|
||||||
std::vector<std::string> underwearPaths_;
|
static inline const std::vector<std::string> emptyStringVec_;
|
||||||
uint32_t skinTextureSlotIndex_ = 0;
|
|
||||||
uint32_t cloakTextureSlotIndex_ = 0;
|
|
||||||
|
|
||||||
bool lastTaxiFlight_ = false;
|
bool lastTaxiFlight_ = false;
|
||||||
uint32_t loadedMapId_ = 0xFFFFFFFF; // Map ID of currently loaded terrain (0xFFFFFFFF = none)
|
uint32_t loadedMapId_ = 0xFFFFFFFF; // Map ID of currently loaded terrain (0xFFFFFFFF = none)
|
||||||
|
|
@ -174,7 +174,6 @@ private:
|
||||||
glm::vec3 chargeEndPos_{0.0f}; // Render coordinates
|
glm::vec3 chargeEndPos_{0.0f}; // Render coordinates
|
||||||
uint64_t chargeTargetGuid_ = 0;
|
uint64_t chargeTargetGuid_ = 0;
|
||||||
|
|
||||||
bool weaponsSheathed_ = false;
|
|
||||||
bool wasAutoAttacking_ = false;
|
bool wasAutoAttacking_ = false;
|
||||||
bool mapNameCacheLoaded_ = false;
|
bool mapNameCacheLoaded_ = false;
|
||||||
std::unordered_map<uint32_t, std::string> mapNameById_;
|
std::unordered_map<uint32_t, std::string> mapNameById_;
|
||||||
|
|
|
||||||
383
src/core/appearance_composer.cpp
Normal file
383
src/core/appearance_composer.cpp
Normal file
|
|
@ -0,0 +1,383 @@
|
||||||
|
#include "core/appearance_composer.hpp"
|
||||||
|
#include "core/entity_spawner.hpp"
|
||||||
|
#include "core/logger.hpp"
|
||||||
|
#include "rendering/renderer.hpp"
|
||||||
|
#include "rendering/character_renderer.hpp"
|
||||||
|
#include "pipeline/asset_manager.hpp"
|
||||||
|
#include "pipeline/m2_loader.hpp"
|
||||||
|
#include "pipeline/dbc_loader.hpp"
|
||||||
|
#include "pipeline/dbc_layout.hpp"
|
||||||
|
#include "game/game_handler.hpp"
|
||||||
|
|
||||||
|
namespace wowee {
|
||||||
|
namespace core {
|
||||||
|
|
||||||
|
AppearanceComposer::AppearanceComposer(rendering::Renderer* renderer,
|
||||||
|
pipeline::AssetManager* assetManager,
|
||||||
|
game::GameHandler* gameHandler,
|
||||||
|
pipeline::DBCLayout* dbcLayout,
|
||||||
|
EntitySpawner* entitySpawner)
|
||||||
|
: renderer_(renderer)
|
||||||
|
, assetManager_(assetManager)
|
||||||
|
, gameHandler_(gameHandler)
|
||||||
|
, dbcLayout_(dbcLayout)
|
||||||
|
, entitySpawner_(entitySpawner)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string AppearanceComposer::getPlayerModelPath(game::Race race, game::Gender gender) const {
|
||||||
|
return game::getPlayerModelPath(race, gender);
|
||||||
|
}
|
||||||
|
|
||||||
|
PlayerTextureInfo AppearanceComposer::resolvePlayerTextures(pipeline::M2Model& model,
|
||||||
|
game::Race race, game::Gender gender,
|
||||||
|
uint32_t appearanceBytes) {
|
||||||
|
PlayerTextureInfo result;
|
||||||
|
|
||||||
|
uint32_t targetRaceId = static_cast<uint32_t>(race);
|
||||||
|
uint32_t targetSexId = (gender == game::Gender::FEMALE) ? 1u : 0u;
|
||||||
|
|
||||||
|
// Race name for fallback texture paths
|
||||||
|
const char* raceFolderName = "Human";
|
||||||
|
switch (race) {
|
||||||
|
case game::Race::HUMAN: raceFolderName = "Human"; break;
|
||||||
|
case game::Race::ORC: raceFolderName = "Orc"; break;
|
||||||
|
case game::Race::DWARF: raceFolderName = "Dwarf"; break;
|
||||||
|
case game::Race::NIGHT_ELF: raceFolderName = "NightElf"; break;
|
||||||
|
case game::Race::UNDEAD: raceFolderName = "Scourge"; break;
|
||||||
|
case game::Race::TAUREN: raceFolderName = "Tauren"; break;
|
||||||
|
case game::Race::GNOME: raceFolderName = "Gnome"; break;
|
||||||
|
case game::Race::TROLL: raceFolderName = "Troll"; break;
|
||||||
|
case game::Race::BLOOD_ELF: raceFolderName = "BloodElf"; break;
|
||||||
|
case game::Race::DRAENEI: raceFolderName = "Draenei"; break;
|
||||||
|
default: break;
|
||||||
|
}
|
||||||
|
const char* genderFolder = (gender == game::Gender::FEMALE) ? "Female" : "Male";
|
||||||
|
std::string raceGender = std::string(raceFolderName) + genderFolder;
|
||||||
|
result.bodySkinPath = std::string("Character\\") + raceFolderName + "\\" + genderFolder + "\\" + raceGender + "Skin00_00.blp";
|
||||||
|
std::string pelvisPath = std::string("Character\\") + raceFolderName + "\\" + genderFolder + "\\" + raceGender + "NakedPelvisSkin00_00.blp";
|
||||||
|
|
||||||
|
// Extract appearance bytes for texture lookups
|
||||||
|
uint8_t charSkinId = appearanceBytes & 0xFF;
|
||||||
|
uint8_t charFaceId = (appearanceBytes >> 8) & 0xFF;
|
||||||
|
uint8_t charHairStyleId = (appearanceBytes >> 16) & 0xFF;
|
||||||
|
uint8_t charHairColorId = (appearanceBytes >> 24) & 0xFF;
|
||||||
|
LOG_INFO("Appearance: skin=", static_cast<int>(charSkinId), " face=", static_cast<int>(charFaceId),
|
||||||
|
" hairStyle=", static_cast<int>(charHairStyleId), " hairColor=", static_cast<int>(charHairColorId));
|
||||||
|
|
||||||
|
// Parse CharSections.dbc for skin/face/hair/underwear texture paths
|
||||||
|
auto charSectionsDbc = assetManager_->loadDBC("CharSections.dbc");
|
||||||
|
if (charSectionsDbc) {
|
||||||
|
LOG_INFO("CharSections.dbc loaded: ", charSectionsDbc->getRecordCount(), " records");
|
||||||
|
const auto* csL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("CharSections") : nullptr;
|
||||||
|
auto csF = pipeline::detectCharSectionsFields(charSectionsDbc.get(), csL);
|
||||||
|
bool foundSkin = false;
|
||||||
|
bool foundUnderwear = false;
|
||||||
|
bool foundFaceLower = false;
|
||||||
|
bool foundHair = false;
|
||||||
|
for (uint32_t r = 0; r < charSectionsDbc->getRecordCount(); r++) {
|
||||||
|
uint32_t raceId = charSectionsDbc->getUInt32(r, csF.raceId);
|
||||||
|
uint32_t sexId = charSectionsDbc->getUInt32(r, csF.sexId);
|
||||||
|
uint32_t baseSection = charSectionsDbc->getUInt32(r, csF.baseSection);
|
||||||
|
uint32_t variationIndex = charSectionsDbc->getUInt32(r, csF.variationIndex);
|
||||||
|
uint32_t colorIndex = charSectionsDbc->getUInt32(r, csF.colorIndex);
|
||||||
|
|
||||||
|
if (raceId != targetRaceId || sexId != targetSexId) continue;
|
||||||
|
|
||||||
|
// Section 0 = skin: match by colorIndex = skin byte
|
||||||
|
if (baseSection == 0 && !foundSkin && colorIndex == charSkinId) {
|
||||||
|
std::string tex1 = charSectionsDbc->getString(r, csF.texture1);
|
||||||
|
if (!tex1.empty()) {
|
||||||
|
result.bodySkinPath = tex1;
|
||||||
|
foundSkin = true;
|
||||||
|
LOG_INFO(" DBC body skin: ", result.bodySkinPath, " (skin=", static_cast<int>(charSkinId), ")");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Section 3 = hair: match variation=hairStyle, color=hairColor
|
||||||
|
else if (baseSection == 3 && !foundHair &&
|
||||||
|
variationIndex == charHairStyleId && colorIndex == charHairColorId) {
|
||||||
|
result.hairTexturePath = charSectionsDbc->getString(r, csF.texture1);
|
||||||
|
if (!result.hairTexturePath.empty()) {
|
||||||
|
foundHair = true;
|
||||||
|
LOG_INFO(" DBC hair texture: ", result.hairTexturePath,
|
||||||
|
" (style=", static_cast<int>(charHairStyleId), " color=", static_cast<int>(charHairColorId), ")");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Section 1 = face: match variation=faceId, colorIndex=skinId
|
||||||
|
// Texture1 = face lower, Texture2 = face upper
|
||||||
|
else if (baseSection == 1 && !foundFaceLower &&
|
||||||
|
variationIndex == charFaceId && colorIndex == charSkinId) {
|
||||||
|
std::string tex1 = charSectionsDbc->getString(r, csF.texture1);
|
||||||
|
std::string tex2 = charSectionsDbc->getString(r, csF.texture2);
|
||||||
|
if (!tex1.empty()) {
|
||||||
|
result.faceLowerPath = tex1;
|
||||||
|
LOG_INFO(" DBC face lower: ", result.faceLowerPath);
|
||||||
|
}
|
||||||
|
if (!tex2.empty()) {
|
||||||
|
result.faceUpperPath = tex2;
|
||||||
|
LOG_INFO(" DBC face upper: ", result.faceUpperPath);
|
||||||
|
}
|
||||||
|
foundFaceLower = true;
|
||||||
|
}
|
||||||
|
// Section 4 = underwear
|
||||||
|
else if (baseSection == 4 && !foundUnderwear && colorIndex == charSkinId) {
|
||||||
|
for (uint32_t f = csF.texture1; f <= csF.texture1 + 2; f++) {
|
||||||
|
std::string tex = charSectionsDbc->getString(r, f);
|
||||||
|
if (!tex.empty()) {
|
||||||
|
result.underwearPaths.push_back(tex);
|
||||||
|
LOG_INFO(" DBC underwear texture: ", tex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
foundUnderwear = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (foundSkin && foundHair && foundFaceLower && foundUnderwear) break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!foundHair) {
|
||||||
|
LOG_WARNING("No DBC hair match for style=", static_cast<int>(charHairStyleId),
|
||||||
|
" color=", static_cast<int>(charHairColorId),
|
||||||
|
" race=", targetRaceId, " sex=", targetSexId);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LOG_WARNING("Failed to load CharSections.dbc, using hardcoded textures");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fill model texture slots with resolved paths
|
||||||
|
for (auto& tex : model.textures) {
|
||||||
|
if (tex.type == 1 && tex.filename.empty()) {
|
||||||
|
tex.filename = result.bodySkinPath;
|
||||||
|
} else if (tex.type == 6) {
|
||||||
|
if (!result.hairTexturePath.empty()) {
|
||||||
|
tex.filename = result.hairTexturePath;
|
||||||
|
} else if (tex.filename.empty()) {
|
||||||
|
tex.filename = std::string("Character\\") + raceFolderName + "\\Hair00_00.blp";
|
||||||
|
}
|
||||||
|
} else if (tex.type == 8 && tex.filename.empty()) {
|
||||||
|
if (!result.underwearPaths.empty()) {
|
||||||
|
tex.filename = result.underwearPaths[0];
|
||||||
|
} else {
|
||||||
|
tex.filename = pelvisPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
void AppearanceComposer::compositePlayerSkin(uint32_t modelSlotId, const PlayerTextureInfo& texInfo) {
|
||||||
|
if (!renderer_) return;
|
||||||
|
auto* charRenderer = renderer_->getCharacterRenderer();
|
||||||
|
if (!charRenderer) return;
|
||||||
|
|
||||||
|
// Save skin composite state for re-compositing on equipment changes
|
||||||
|
// Include face textures so compositeWithRegions can rebuild the full base
|
||||||
|
bodySkinPath_ = texInfo.bodySkinPath;
|
||||||
|
underwearPaths_.clear();
|
||||||
|
if (!texInfo.faceLowerPath.empty()) underwearPaths_.push_back(texInfo.faceLowerPath);
|
||||||
|
if (!texInfo.faceUpperPath.empty()) underwearPaths_.push_back(texInfo.faceUpperPath);
|
||||||
|
for (const auto& up : texInfo.underwearPaths) underwearPaths_.push_back(up);
|
||||||
|
|
||||||
|
// Composite body skin + face + underwear overlays
|
||||||
|
{
|
||||||
|
std::vector<std::string> layers;
|
||||||
|
layers.push_back(texInfo.bodySkinPath);
|
||||||
|
if (!texInfo.faceLowerPath.empty()) layers.push_back(texInfo.faceLowerPath);
|
||||||
|
if (!texInfo.faceUpperPath.empty()) layers.push_back(texInfo.faceUpperPath);
|
||||||
|
for (const auto& up : texInfo.underwearPaths) {
|
||||||
|
layers.push_back(up);
|
||||||
|
}
|
||||||
|
if (layers.size() > 1) {
|
||||||
|
rendering::VkTexture* compositeTex = charRenderer->compositeTextures(layers);
|
||||||
|
if (compositeTex != 0) {
|
||||||
|
// Find type-1 (skin) texture slot and replace with composite
|
||||||
|
// We need model texture info — walk slots via charRenderer
|
||||||
|
// Use the model slot ID to find the right texture index
|
||||||
|
auto* modelData = charRenderer->getModelData(modelSlotId);
|
||||||
|
if (modelData) {
|
||||||
|
for (size_t ti = 0; ti < modelData->textures.size(); ti++) {
|
||||||
|
if (modelData->textures[ti].type == 1) {
|
||||||
|
charRenderer->setModelTexture(modelSlotId, static_cast<uint32_t>(ti), compositeTex);
|
||||||
|
skinTextureSlotIndex_ = static_cast<uint32_t>(ti);
|
||||||
|
LOG_INFO("Replaced type-1 texture slot ", ti, " with composited body+face+underwear");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Override hair texture on GPU (type-6 slot) after model load
|
||||||
|
if (!texInfo.hairTexturePath.empty()) {
|
||||||
|
rendering::VkTexture* hairTex = charRenderer->loadTexture(texInfo.hairTexturePath);
|
||||||
|
if (hairTex) {
|
||||||
|
auto* modelData = charRenderer->getModelData(modelSlotId);
|
||||||
|
if (modelData) {
|
||||||
|
for (size_t ti = 0; ti < modelData->textures.size(); ti++) {
|
||||||
|
if (modelData->textures[ti].type == 6) {
|
||||||
|
charRenderer->setModelTexture(modelSlotId, static_cast<uint32_t>(ti), hairTex);
|
||||||
|
LOG_INFO("Applied DBC hair texture to slot ", ti, ": ", texInfo.hairTexturePath);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find cloak (type-2, Object Skin) texture slot index
|
||||||
|
{
|
||||||
|
auto* modelData = charRenderer->getModelData(modelSlotId);
|
||||||
|
if (modelData) {
|
||||||
|
for (size_t ti = 0; ti < modelData->textures.size(); ti++) {
|
||||||
|
if (modelData->textures[ti].type == 2) {
|
||||||
|
cloakTextureSlotIndex_ = static_cast<uint32_t>(ti);
|
||||||
|
LOG_INFO("Cloak texture slot: ", ti);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::unordered_set<uint16_t> AppearanceComposer::buildDefaultPlayerGeosets(uint8_t hairStyleId, uint8_t facialId) {
|
||||||
|
std::unordered_set<uint16_t> activeGeosets;
|
||||||
|
// Body parts (group 0: IDs 0-99, some models use up to 27)
|
||||||
|
for (uint16_t i = 0; i <= 99; i++) activeGeosets.insert(i);
|
||||||
|
|
||||||
|
// Hair style geoset: group 1 = 100 + variation + 1
|
||||||
|
activeGeosets.insert(static_cast<uint16_t>(100 + hairStyleId + 1));
|
||||||
|
// Facial hair geoset: group 2 = 200 + variation + 1
|
||||||
|
activeGeosets.insert(static_cast<uint16_t>(200 + facialId + 1));
|
||||||
|
activeGeosets.insert(kGeosetBareForearms);
|
||||||
|
activeGeosets.insert(kGeosetBareShins);
|
||||||
|
activeGeosets.insert(kGeosetDefaultEars);
|
||||||
|
activeGeosets.insert(kGeosetBareSleeves);
|
||||||
|
activeGeosets.insert(kGeosetDefaultKneepads);
|
||||||
|
activeGeosets.insert(kGeosetBarePants);
|
||||||
|
activeGeosets.insert(kGeosetWithCape);
|
||||||
|
activeGeosets.insert(kGeosetBareFeet);
|
||||||
|
// 1703 = DK eye glow mesh — skip for normal characters
|
||||||
|
// Normal eyes are part of the face texture on the body mesh
|
||||||
|
return activeGeosets;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AppearanceComposer::loadWeaponM2(const std::string& m2Path, pipeline::M2Model& outModel) {
|
||||||
|
auto m2Data = assetManager_->readFile(m2Path);
|
||||||
|
if (m2Data.empty()) return false;
|
||||||
|
outModel = pipeline::M2Loader::load(m2Data);
|
||||||
|
// Load skin (WotLK+ M2 format): strip .m2, append 00.skin
|
||||||
|
std::string skinPath = m2Path;
|
||||||
|
size_t dotPos = skinPath.rfind('.');
|
||||||
|
if (dotPos != std::string::npos) skinPath = skinPath.substr(0, dotPos);
|
||||||
|
skinPath += "00.skin";
|
||||||
|
auto skinData = assetManager_->readFile(skinPath);
|
||||||
|
if (!skinData.empty() && outModel.version >= 264)
|
||||||
|
pipeline::M2Loader::loadSkin(skinData, outModel);
|
||||||
|
return outModel.isValid();
|
||||||
|
}
|
||||||
|
|
||||||
|
void AppearanceComposer::loadEquippedWeapons() {
|
||||||
|
if (!renderer_ || !renderer_->getCharacterRenderer() || !assetManager_ || !assetManager_->isInitialized())
|
||||||
|
return;
|
||||||
|
if (!gameHandler_) return;
|
||||||
|
|
||||||
|
auto* charRenderer = renderer_->getCharacterRenderer();
|
||||||
|
uint32_t charInstanceId = renderer_->getCharacterInstanceId();
|
||||||
|
if (charInstanceId == 0) return;
|
||||||
|
|
||||||
|
auto& inventory = gameHandler_->getInventory();
|
||||||
|
|
||||||
|
// Load ItemDisplayInfo.dbc
|
||||||
|
auto displayInfoDbc = assetManager_->loadDBC("ItemDisplayInfo.dbc");
|
||||||
|
if (!displayInfoDbc) {
|
||||||
|
LOG_WARNING("loadEquippedWeapons: failed to load ItemDisplayInfo.dbc");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Mapping: EquipSlot → attachment ID (1=RightHand, 2=LeftHand)
|
||||||
|
struct WeaponSlot {
|
||||||
|
game::EquipSlot slot;
|
||||||
|
uint32_t attachmentId;
|
||||||
|
};
|
||||||
|
WeaponSlot weaponSlots[] = {
|
||||||
|
{ game::EquipSlot::MAIN_HAND, 1 },
|
||||||
|
{ game::EquipSlot::OFF_HAND, 2 },
|
||||||
|
};
|
||||||
|
|
||||||
|
if (weaponsSheathed_) {
|
||||||
|
for (const auto& ws : weaponSlots) {
|
||||||
|
charRenderer->detachWeapon(charInstanceId, ws.attachmentId);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const auto& ws : weaponSlots) {
|
||||||
|
const auto& equipSlot = inventory.getEquipSlot(ws.slot);
|
||||||
|
|
||||||
|
// If slot is empty or has no displayInfoId, detach any existing weapon
|
||||||
|
if (equipSlot.empty() || equipSlot.item.displayInfoId == 0) {
|
||||||
|
charRenderer->detachWeapon(charInstanceId, ws.attachmentId);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t displayInfoId = equipSlot.item.displayInfoId;
|
||||||
|
int32_t recIdx = displayInfoDbc->findRecordById(displayInfoId);
|
||||||
|
if (recIdx < 0) {
|
||||||
|
LOG_WARNING("loadEquippedWeapons: displayInfoId ", displayInfoId, " not found in DBC");
|
||||||
|
charRenderer->detachWeapon(charInstanceId, ws.attachmentId);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto* idiL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("ItemDisplayInfo") : nullptr;
|
||||||
|
std::string modelName = displayInfoDbc->getString(static_cast<uint32_t>(recIdx), idiL ? (*idiL)["LeftModel"] : 1);
|
||||||
|
std::string textureName = displayInfoDbc->getString(static_cast<uint32_t>(recIdx), idiL ? (*idiL)["LeftModelTexture"] : 3);
|
||||||
|
|
||||||
|
if (modelName.empty()) {
|
||||||
|
LOG_WARNING("loadEquippedWeapons: empty model name for displayInfoId ", displayInfoId);
|
||||||
|
charRenderer->detachWeapon(charInstanceId, ws.attachmentId);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert .mdx → .m2
|
||||||
|
std::string modelFile = modelName;
|
||||||
|
{
|
||||||
|
size_t dotPos = modelFile.rfind('.');
|
||||||
|
if (dotPos != std::string::npos) {
|
||||||
|
modelFile = modelFile.substr(0, dotPos) + ".m2";
|
||||||
|
} else {
|
||||||
|
modelFile += ".m2";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try Weapon directory first, then Shield
|
||||||
|
std::string m2Path = "Item\\ObjectComponents\\Weapon\\" + modelFile;
|
||||||
|
pipeline::M2Model weaponModel;
|
||||||
|
if (!loadWeaponM2(m2Path, weaponModel)) {
|
||||||
|
m2Path = "Item\\ObjectComponents\\Shield\\" + modelFile;
|
||||||
|
if (!loadWeaponM2(m2Path, weaponModel)) {
|
||||||
|
LOG_WARNING("loadEquippedWeapons: failed to load ", modelFile);
|
||||||
|
charRenderer->detachWeapon(charInstanceId, ws.attachmentId);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build texture path
|
||||||
|
std::string texturePath;
|
||||||
|
if (!textureName.empty()) {
|
||||||
|
texturePath = "Item\\ObjectComponents\\Weapon\\" + textureName + ".blp";
|
||||||
|
if (!assetManager_->fileExists(texturePath)) {
|
||||||
|
texturePath = "Item\\ObjectComponents\\Shield\\" + textureName + ".blp";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t weaponModelId = entitySpawner_->allocateWeaponModelId();
|
||||||
|
bool ok = charRenderer->attachWeapon(charInstanceId, ws.attachmentId,
|
||||||
|
weaponModel, weaponModelId, texturePath);
|
||||||
|
if (ok) {
|
||||||
|
LOG_INFO("Equipped weapon: ", m2Path, " at attachment ", ws.attachmentId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace core
|
||||||
|
} // namespace wowee
|
||||||
|
|
@ -84,19 +84,6 @@ bool envFlagEnabled(const char* key, bool defaultValue = false) {
|
||||||
raw[0] == 'n' || raw[0] == 'N');
|
raw[0] == 'n' || raw[0] == 'N');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default (bare) geoset IDs per equipment group.
|
|
||||||
// Each group's base is groupNumber * 100; variant 01 is typically bare/default.
|
|
||||||
constexpr uint16_t kGeosetDefaultConnector = 101; // Group 1: default hair connector
|
|
||||||
constexpr uint16_t kGeosetBareForearms = 401; // Group 4: no gloves
|
|
||||||
constexpr uint16_t kGeosetBareShins = 503; // Group 5: no boots
|
|
||||||
constexpr uint16_t kGeosetDefaultEars = 702; // Group 7: ears
|
|
||||||
constexpr uint16_t kGeosetBareSleeves = 801; // Group 8: no chest armor sleeves
|
|
||||||
constexpr uint16_t kGeosetDefaultKneepads = 902; // Group 9: kneepads
|
|
||||||
constexpr uint16_t kGeosetDefaultTabard = 1201; // Group 12: tabard base
|
|
||||||
constexpr uint16_t kGeosetBarePants = 1301; // Group 13: no leggings
|
|
||||||
constexpr uint16_t kGeosetNoCape = 1501; // Group 15: no cape
|
|
||||||
constexpr uint16_t kGeosetWithCape = 1502; // Group 15: with cape
|
|
||||||
constexpr uint16_t kGeosetBareFeet = 2002; // Group 20: bare feet
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -204,10 +191,6 @@ const char* Application::mapIdToName(uint32_t mapId) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string Application::getPlayerModelPath() const {
|
|
||||||
return game::getPlayerModelPath(playerRace_, playerGender_);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
Application* Application::instance = nullptr;
|
Application* Application::instance = nullptr;
|
||||||
|
|
||||||
|
|
@ -342,6 +325,10 @@ bool Application::initialize() {
|
||||||
dbcLayout_.get(), &gameServices_);
|
dbcLayout_.get(), &gameServices_);
|
||||||
entitySpawner_->initialize();
|
entitySpawner_->initialize();
|
||||||
|
|
||||||
|
appearanceComposer_ = std::make_unique<AppearanceComposer>(
|
||||||
|
renderer.get(), assetManager.get(), gameHandler.get(),
|
||||||
|
dbcLayout_.get(), entitySpawner_.get());
|
||||||
|
|
||||||
// Ensure the main in-world CharacterRenderer can load textures immediately.
|
// Ensure the main in-world CharacterRenderer can load textures immediately.
|
||||||
// Previously this was only wired during terrain initialization, which meant early spawns
|
// Previously this was only wired during terrain initialization, which meant early spawns
|
||||||
// (before terrain load) would render with white fallback textures (notably hair).
|
// (before terrain load) would render with white fallback textures (notably hair).
|
||||||
|
|
@ -970,7 +957,7 @@ void Application::setState(AppState newState) {
|
||||||
npcsSpawned = false;
|
npcsSpawned = false;
|
||||||
playerCharacterSpawned = false;
|
playerCharacterSpawned = false;
|
||||||
addonsLoaded_ = false;
|
addonsLoaded_ = false;
|
||||||
weaponsSheathed_ = false;
|
if (appearanceComposer_) appearanceComposer_->setWeaponsSheathed(false);
|
||||||
wasAutoAttacking_ = false;
|
wasAutoAttacking_ = false;
|
||||||
loadedMapId_ = 0xFFFFFFFF;
|
loadedMapId_ = 0xFFFFFFFF;
|
||||||
spawnedPlayerGuid_ = 0;
|
spawnedPlayerGuid_ = 0;
|
||||||
|
|
@ -1105,7 +1092,7 @@ void Application::logoutToLogin() {
|
||||||
// --- Per-session flags ---
|
// --- Per-session flags ---
|
||||||
npcsSpawned = false;
|
npcsSpawned = false;
|
||||||
playerCharacterSpawned = false;
|
playerCharacterSpawned = false;
|
||||||
weaponsSheathed_ = false;
|
if (appearanceComposer_) appearanceComposer_->setWeaponsSheathed(false);
|
||||||
wasAutoAttacking_ = false;
|
wasAutoAttacking_ = false;
|
||||||
loadedMapId_ = 0xFFFFFFFF;
|
loadedMapId_ = 0xFFFFFFFF;
|
||||||
lastTaxiFlight_ = false;
|
lastTaxiFlight_ = false;
|
||||||
|
|
@ -1241,9 +1228,9 @@ void Application::update(float deltaTime) {
|
||||||
updateCheckpoint = "in_game: auto-unsheathe";
|
updateCheckpoint = "in_game: auto-unsheathe";
|
||||||
if (gameHandler) {
|
if (gameHandler) {
|
||||||
const bool autoAttacking = gameHandler->isAutoAttacking();
|
const bool autoAttacking = gameHandler->isAutoAttacking();
|
||||||
if (autoAttacking && !wasAutoAttacking_ && weaponsSheathed_) {
|
if (autoAttacking && !wasAutoAttacking_ && appearanceComposer_ && appearanceComposer_->isWeaponsSheathed()) {
|
||||||
weaponsSheathed_ = false;
|
appearanceComposer_->setWeaponsSheathed(false);
|
||||||
loadEquippedWeapons();
|
appearanceComposer_->loadEquippedWeapons();
|
||||||
}
|
}
|
||||||
wasAutoAttacking_ = autoAttacking;
|
wasAutoAttacking_ = autoAttacking;
|
||||||
}
|
}
|
||||||
|
|
@ -1254,9 +1241,9 @@ void Application::update(float deltaTime) {
|
||||||
{
|
{
|
||||||
const bool uiWantsKeyboard = ImGui::GetIO().WantCaptureKeyboard;
|
const bool uiWantsKeyboard = ImGui::GetIO().WantCaptureKeyboard;
|
||||||
auto& input = Input::getInstance();
|
auto& input = Input::getInstance();
|
||||||
if (!uiWantsKeyboard && input.isKeyJustPressed(SDL_SCANCODE_Z)) {
|
if (!uiWantsKeyboard && input.isKeyJustPressed(SDL_SCANCODE_Z) && appearanceComposer_) {
|
||||||
weaponsSheathed_ = !weaponsSheathed_;
|
appearanceComposer_->toggleWeaponsSheathed();
|
||||||
loadEquippedWeapons();
|
appearanceComposer_->loadEquippedWeapons();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -3606,7 +3593,7 @@ void Application::spawnPlayerCharacter() {
|
||||||
auto* charRenderer = renderer->getCharacterRenderer();
|
auto* charRenderer = renderer->getCharacterRenderer();
|
||||||
auto* camera = renderer->getCamera();
|
auto* camera = renderer->getCamera();
|
||||||
bool loaded = false;
|
bool loaded = false;
|
||||||
std::string m2Path = getPlayerModelPath();
|
std::string m2Path = appearanceComposer_->getPlayerModelPath(playerRace_, playerGender_);
|
||||||
std::string modelDir;
|
std::string modelDir;
|
||||||
std::string baseName;
|
std::string baseName;
|
||||||
{
|
{
|
||||||
|
|
@ -3643,144 +3630,18 @@ void Application::spawnPlayerCharacter() {
|
||||||
LOG_INFO(" Texture ", ti, ": type=", tex.type, " name='", tex.filename, "'");
|
LOG_INFO(" Texture ", ti, ": type=", tex.type, " name='", tex.filename, "'");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Look up textures from CharSections.dbc for all races
|
// Resolve textures from CharSections.dbc via AppearanceComposer
|
||||||
|
PlayerTextureInfo texInfo;
|
||||||
bool useCharSections = true;
|
bool useCharSections = true;
|
||||||
uint32_t targetRaceId = static_cast<uint32_t>(playerRace_);
|
if (appearanceComposer_) {
|
||||||
uint32_t targetSexId = (playerGender_ == game::Gender::FEMALE) ? 1u : 0u;
|
uint32_t appearanceBytes = 0;
|
||||||
|
|
||||||
// Race name for fallback texture paths
|
|
||||||
const char* raceFolderName = "Human";
|
|
||||||
switch (playerRace_) {
|
|
||||||
case game::Race::HUMAN: raceFolderName = "Human"; break;
|
|
||||||
case game::Race::ORC: raceFolderName = "Orc"; break;
|
|
||||||
case game::Race::DWARF: raceFolderName = "Dwarf"; break;
|
|
||||||
case game::Race::NIGHT_ELF: raceFolderName = "NightElf"; break;
|
|
||||||
case game::Race::UNDEAD: raceFolderName = "Scourge"; break;
|
|
||||||
case game::Race::TAUREN: raceFolderName = "Tauren"; break;
|
|
||||||
case game::Race::GNOME: raceFolderName = "Gnome"; break;
|
|
||||||
case game::Race::TROLL: raceFolderName = "Troll"; break;
|
|
||||||
case game::Race::BLOOD_ELF: raceFolderName = "BloodElf"; break;
|
|
||||||
case game::Race::DRAENEI: raceFolderName = "Draenei"; break;
|
|
||||||
default: break;
|
|
||||||
}
|
|
||||||
const char* genderFolder = (playerGender_ == game::Gender::FEMALE) ? "Female" : "Male";
|
|
||||||
std::string raceGender = std::string(raceFolderName) + genderFolder;
|
|
||||||
std::string bodySkinPath = std::string("Character\\") + raceFolderName + "\\" + genderFolder + "\\" + raceGender + "Skin00_00.blp";
|
|
||||||
std::string pelvisPath = std::string("Character\\") + raceFolderName + "\\" + genderFolder + "\\" + raceGender + "NakedPelvisSkin00_00.blp";
|
|
||||||
std::string faceLowerTexturePath;
|
|
||||||
std::string faceUpperTexturePath;
|
|
||||||
std::vector<std::string> underwearPaths;
|
|
||||||
|
|
||||||
// Extract appearance bytes for texture lookups
|
|
||||||
uint8_t charSkinId = 0, charFaceId = 0, charHairStyleId = 0, charHairColorId = 0;
|
|
||||||
if (gameHandler) {
|
if (gameHandler) {
|
||||||
const game::Character* activeChar = gameHandler->getActiveCharacter();
|
const game::Character* activeChar = gameHandler->getActiveCharacter();
|
||||||
if (activeChar) {
|
if (activeChar) {
|
||||||
charSkinId = activeChar->appearanceBytes & 0xFF;
|
appearanceBytes = activeChar->appearanceBytes;
|
||||||
charFaceId = (activeChar->appearanceBytes >> 8) & 0xFF;
|
|
||||||
charHairStyleId = (activeChar->appearanceBytes >> 16) & 0xFF;
|
|
||||||
charHairColorId = (activeChar->appearanceBytes >> 24) & 0xFF;
|
|
||||||
LOG_INFO("Appearance: skin=", static_cast<int>(charSkinId), " face=", static_cast<int>(charFaceId),
|
|
||||||
" hairStyle=", static_cast<int>(charHairStyleId), " hairColor=", static_cast<int>(charHairColorId));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string hairTexturePath;
|
|
||||||
if (useCharSections) {
|
|
||||||
auto charSectionsDbc = assetManager->loadDBC("CharSections.dbc");
|
|
||||||
if (charSectionsDbc) {
|
|
||||||
LOG_INFO("CharSections.dbc loaded: ", charSectionsDbc->getRecordCount(), " records");
|
|
||||||
const auto* csL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("CharSections") : nullptr;
|
|
||||||
auto csF = pipeline::detectCharSectionsFields(charSectionsDbc.get(), csL);
|
|
||||||
bool foundSkin = false;
|
|
||||||
bool foundUnderwear = false;
|
|
||||||
bool foundFaceLower = false;
|
|
||||||
bool foundHair = false;
|
|
||||||
for (uint32_t r = 0; r < charSectionsDbc->getRecordCount(); r++) {
|
|
||||||
uint32_t raceId = charSectionsDbc->getUInt32(r, csF.raceId);
|
|
||||||
uint32_t sexId = charSectionsDbc->getUInt32(r, csF.sexId);
|
|
||||||
uint32_t baseSection = charSectionsDbc->getUInt32(r, csF.baseSection);
|
|
||||||
uint32_t variationIndex = charSectionsDbc->getUInt32(r, csF.variationIndex);
|
|
||||||
uint32_t colorIndex = charSectionsDbc->getUInt32(r, csF.colorIndex);
|
|
||||||
|
|
||||||
if (raceId != targetRaceId || sexId != targetSexId) continue;
|
|
||||||
|
|
||||||
// Section 0 = skin: match by colorIndex = skin byte
|
|
||||||
if (baseSection == 0 && !foundSkin && colorIndex == charSkinId) {
|
|
||||||
std::string tex1 = charSectionsDbc->getString(r, csF.texture1);
|
|
||||||
if (!tex1.empty()) {
|
|
||||||
bodySkinPath = tex1;
|
|
||||||
foundSkin = true;
|
|
||||||
LOG_INFO(" DBC body skin: ", bodySkinPath, " (skin=", static_cast<int>(charSkinId), ")");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Section 3 = hair: match variation=hairStyle, color=hairColor
|
|
||||||
else if (baseSection == 3 && !foundHair &&
|
|
||||||
variationIndex == charHairStyleId && colorIndex == charHairColorId) {
|
|
||||||
hairTexturePath = charSectionsDbc->getString(r, csF.texture1);
|
|
||||||
if (!hairTexturePath.empty()) {
|
|
||||||
foundHair = true;
|
|
||||||
LOG_INFO(" DBC hair texture: ", hairTexturePath,
|
|
||||||
" (style=", static_cast<int>(charHairStyleId), " color=", static_cast<int>(charHairColorId), ")");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Section 1 = face: match variation=faceId, colorIndex=skinId
|
|
||||||
// Texture1 = face lower, Texture2 = face upper
|
|
||||||
else if (baseSection == 1 && !foundFaceLower &&
|
|
||||||
variationIndex == charFaceId && colorIndex == charSkinId) {
|
|
||||||
std::string tex1 = charSectionsDbc->getString(r, csF.texture1);
|
|
||||||
std::string tex2 = charSectionsDbc->getString(r, csF.texture2);
|
|
||||||
if (!tex1.empty()) {
|
|
||||||
faceLowerTexturePath = tex1;
|
|
||||||
LOG_INFO(" DBC face lower: ", faceLowerTexturePath);
|
|
||||||
}
|
|
||||||
if (!tex2.empty()) {
|
|
||||||
faceUpperTexturePath = tex2;
|
|
||||||
LOG_INFO(" DBC face upper: ", faceUpperTexturePath);
|
|
||||||
}
|
|
||||||
foundFaceLower = true;
|
|
||||||
}
|
|
||||||
// Section 4 = underwear
|
|
||||||
else if (baseSection == 4 && !foundUnderwear && colorIndex == charSkinId) {
|
|
||||||
for (uint32_t f = csF.texture1; f <= csF.texture1 + 2; f++) {
|
|
||||||
std::string tex = charSectionsDbc->getString(r, f);
|
|
||||||
if (!tex.empty()) {
|
|
||||||
underwearPaths.push_back(tex);
|
|
||||||
LOG_INFO(" DBC underwear texture: ", tex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
foundUnderwear = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (foundSkin && foundHair && foundFaceLower && foundUnderwear) break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!foundHair) {
|
|
||||||
LOG_WARNING("No DBC hair match for style=", static_cast<int>(charHairStyleId),
|
|
||||||
" color=", static_cast<int>(charHairColorId),
|
|
||||||
" race=", targetRaceId, " sex=", targetSexId);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
LOG_WARNING("Failed to load CharSections.dbc, using hardcoded textures");
|
|
||||||
}
|
|
||||||
|
|
||||||
for (auto& tex : model.textures) {
|
|
||||||
if (tex.type == 1 && tex.filename.empty()) {
|
|
||||||
tex.filename = bodySkinPath;
|
|
||||||
} else if (tex.type == 6) {
|
|
||||||
if (!hairTexturePath.empty()) {
|
|
||||||
tex.filename = hairTexturePath;
|
|
||||||
} else if (tex.filename.empty()) {
|
|
||||||
tex.filename = std::string("Character\\") + raceFolderName + "\\Hair00_00.blp";
|
|
||||||
}
|
|
||||||
} else if (tex.type == 8 && tex.filename.empty()) {
|
|
||||||
if (!underwearPaths.empty()) {
|
|
||||||
tex.filename = underwearPaths[0];
|
|
||||||
} else {
|
|
||||||
tex.filename = pelvisPath;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
texInfo = appearanceComposer_->resolvePlayerTextures(model, playerRace_, playerGender_, appearanceBytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load external .anim files for sequences with external data.
|
// Load external .anim files for sequences with external data.
|
||||||
|
|
@ -3806,62 +3667,9 @@ void Application::spawnPlayerCharacter() {
|
||||||
|
|
||||||
charRenderer->loadModel(model, 1);
|
charRenderer->loadModel(model, 1);
|
||||||
|
|
||||||
if (useCharSections) {
|
// Apply composited textures via AppearanceComposer (saves skin state for re-compositing)
|
||||||
// Save skin composite state for re-compositing on equipment changes
|
if (useCharSections && appearanceComposer_) {
|
||||||
// Include face textures so compositeWithRegions can rebuild the full base
|
appearanceComposer_->compositePlayerSkin(1, texInfo);
|
||||||
bodySkinPath_ = bodySkinPath;
|
|
||||||
underwearPaths_.clear();
|
|
||||||
if (!faceLowerTexturePath.empty()) underwearPaths_.push_back(faceLowerTexturePath);
|
|
||||||
if (!faceUpperTexturePath.empty()) underwearPaths_.push_back(faceUpperTexturePath);
|
|
||||||
for (const auto& up : underwearPaths) underwearPaths_.push_back(up);
|
|
||||||
|
|
||||||
// Composite body skin + face + underwear overlays
|
|
||||||
{
|
|
||||||
std::vector<std::string> layers;
|
|
||||||
layers.push_back(bodySkinPath);
|
|
||||||
if (!faceLowerTexturePath.empty()) layers.push_back(faceLowerTexturePath);
|
|
||||||
if (!faceUpperTexturePath.empty()) layers.push_back(faceUpperTexturePath);
|
|
||||||
for (const auto& up : underwearPaths) {
|
|
||||||
layers.push_back(up);
|
|
||||||
}
|
|
||||||
if (layers.size() > 1) {
|
|
||||||
rendering::VkTexture* compositeTex = charRenderer->compositeTextures(layers);
|
|
||||||
if (compositeTex != 0) {
|
|
||||||
for (size_t ti = 0; ti < model.textures.size(); ti++) {
|
|
||||||
if (model.textures[ti].type == 1) {
|
|
||||||
charRenderer->setModelTexture(1, static_cast<uint32_t>(ti), compositeTex);
|
|
||||||
skinTextureSlotIndex_ = static_cast<uint32_t>(ti);
|
|
||||||
LOG_INFO("Replaced type-1 texture slot ", ti, " with composited body+face+underwear");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Override hair texture on GPU (type-6 slot) after model load
|
|
||||||
if (!hairTexturePath.empty()) {
|
|
||||||
rendering::VkTexture* hairTex = charRenderer->loadTexture(hairTexturePath);
|
|
||||||
if (hairTex) {
|
|
||||||
for (size_t ti = 0; ti < model.textures.size(); ti++) {
|
|
||||||
if (model.textures[ti].type == 6) {
|
|
||||||
charRenderer->setModelTexture(1, static_cast<uint32_t>(ti), hairTex);
|
|
||||||
LOG_INFO("Applied DBC hair texture to slot ", ti, ": ", hairTexturePath);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
bodySkinPath_.clear();
|
|
||||||
underwearPaths_.clear();
|
|
||||||
}
|
|
||||||
// Find cloak (type-2, Object Skin) texture slot index
|
|
||||||
for (size_t ti = 0; ti < model.textures.size(); ti++) {
|
|
||||||
if (model.textures[ti].type == 2) {
|
|
||||||
cloakTextureSlotIndex_ = static_cast<uint32_t>(ti);
|
|
||||||
LOG_INFO("Cloak texture slot: ", ti);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
loaded = true;
|
loaded = true;
|
||||||
|
|
@ -3942,12 +3750,7 @@ void Application::spawnPlayerCharacter() {
|
||||||
renderer->getCharacterPosition() = spawnPos;
|
renderer->getCharacterPosition() = spawnPos;
|
||||||
renderer->setCharacterFollow(instanceId);
|
renderer->setCharacterFollow(instanceId);
|
||||||
|
|
||||||
// Default geosets for the active character (match CharacterPreview logic).
|
// Build default geosets for the active character via AppearanceComposer
|
||||||
// Previous hardcoded values (notably always inserting 101) caused wrong hair meshes in-world.
|
|
||||||
std::unordered_set<uint16_t> activeGeosets;
|
|
||||||
// Body parts (group 0: IDs 0-99, some models use up to 27)
|
|
||||||
for (uint16_t i = 0; i <= 99; i++) activeGeosets.insert(i);
|
|
||||||
|
|
||||||
uint8_t hairStyleId = 0;
|
uint8_t hairStyleId = 0;
|
||||||
uint8_t facialId = 0;
|
uint8_t facialId = 0;
|
||||||
if (gameHandler) {
|
if (gameHandler) {
|
||||||
|
|
@ -3956,20 +3759,9 @@ void Application::spawnPlayerCharacter() {
|
||||||
facialId = ch->facialFeatures;
|
facialId = ch->facialFeatures;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Hair style geoset: group 1 = 100 + variation + 1
|
auto activeGeosets = appearanceComposer_
|
||||||
activeGeosets.insert(static_cast<uint16_t>(100 + hairStyleId + 1));
|
? appearanceComposer_->buildDefaultPlayerGeosets(hairStyleId, facialId)
|
||||||
// Facial hair geoset: group 2 = 200 + variation + 1
|
: std::unordered_set<uint16_t>{};
|
||||||
activeGeosets.insert(static_cast<uint16_t>(200 + facialId + 1));
|
|
||||||
activeGeosets.insert(kGeosetBareForearms);
|
|
||||||
activeGeosets.insert(kGeosetBareShins);
|
|
||||||
activeGeosets.insert(kGeosetDefaultEars);
|
|
||||||
activeGeosets.insert(kGeosetBareSleeves);
|
|
||||||
activeGeosets.insert(kGeosetDefaultKneepads);
|
|
||||||
activeGeosets.insert(kGeosetBarePants);
|
|
||||||
activeGeosets.insert(kGeosetWithCape);
|
|
||||||
activeGeosets.insert(kGeosetBareFeet);
|
|
||||||
// 1703 = DK eye glow mesh — skip for normal characters
|
|
||||||
// Normal eyes are part of the face texture on the body mesh
|
|
||||||
charRenderer->setActiveGeosets(instanceId, activeGeosets);
|
charRenderer->setActiveGeosets(instanceId, activeGeosets);
|
||||||
|
|
||||||
// Play idle animation (Stand = animation ID 0)
|
// Play idle animation (Stand = animation ID 0)
|
||||||
|
|
@ -4024,124 +3816,7 @@ void Application::spawnPlayerCharacter() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load equipped weapons (sword + shield)
|
// Load equipped weapons (sword + shield)
|
||||||
loadEquippedWeapons();
|
if (appearanceComposer_) appearanceComposer_->loadEquippedWeapons();
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool Application::loadWeaponM2(const std::string& m2Path, pipeline::M2Model& outModel) {
|
|
||||||
auto m2Data = assetManager->readFile(m2Path);
|
|
||||||
if (m2Data.empty()) return false;
|
|
||||||
outModel = pipeline::M2Loader::load(m2Data);
|
|
||||||
// Load skin (WotLK+ M2 format): strip .m2, append 00.skin
|
|
||||||
std::string skinPath = m2Path;
|
|
||||||
size_t dotPos = skinPath.rfind('.');
|
|
||||||
if (dotPos != std::string::npos) skinPath = skinPath.substr(0, dotPos);
|
|
||||||
skinPath += "00.skin";
|
|
||||||
auto skinData = assetManager->readFile(skinPath);
|
|
||||||
if (!skinData.empty() && outModel.version >= 264)
|
|
||||||
pipeline::M2Loader::loadSkin(skinData, outModel);
|
|
||||||
return outModel.isValid();
|
|
||||||
}
|
|
||||||
|
|
||||||
void Application::loadEquippedWeapons() {
|
|
||||||
if (!renderer || !renderer->getCharacterRenderer() || !assetManager || !assetManager->isInitialized())
|
|
||||||
return;
|
|
||||||
if (!gameHandler) return;
|
|
||||||
|
|
||||||
auto* charRenderer = renderer->getCharacterRenderer();
|
|
||||||
uint32_t charInstanceId = renderer->getCharacterInstanceId();
|
|
||||||
if (charInstanceId == 0) return;
|
|
||||||
|
|
||||||
auto& inventory = gameHandler->getInventory();
|
|
||||||
|
|
||||||
// Load ItemDisplayInfo.dbc
|
|
||||||
auto displayInfoDbc = assetManager->loadDBC("ItemDisplayInfo.dbc");
|
|
||||||
if (!displayInfoDbc) {
|
|
||||||
LOG_WARNING("loadEquippedWeapons: failed to load ItemDisplayInfo.dbc");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Mapping: EquipSlot → attachment ID (1=RightHand, 2=LeftHand)
|
|
||||||
struct WeaponSlot {
|
|
||||||
game::EquipSlot slot;
|
|
||||||
uint32_t attachmentId;
|
|
||||||
};
|
|
||||||
WeaponSlot weaponSlots[] = {
|
|
||||||
{ game::EquipSlot::MAIN_HAND, 1 },
|
|
||||||
{ game::EquipSlot::OFF_HAND, 2 },
|
|
||||||
};
|
|
||||||
|
|
||||||
if (weaponsSheathed_) {
|
|
||||||
for (const auto& ws : weaponSlots) {
|
|
||||||
charRenderer->detachWeapon(charInstanceId, ws.attachmentId);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const auto& ws : weaponSlots) {
|
|
||||||
const auto& equipSlot = inventory.getEquipSlot(ws.slot);
|
|
||||||
|
|
||||||
// If slot is empty or has no displayInfoId, detach any existing weapon
|
|
||||||
if (equipSlot.empty() || equipSlot.item.displayInfoId == 0) {
|
|
||||||
charRenderer->detachWeapon(charInstanceId, ws.attachmentId);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
uint32_t displayInfoId = equipSlot.item.displayInfoId;
|
|
||||||
int32_t recIdx = displayInfoDbc->findRecordById(displayInfoId);
|
|
||||||
if (recIdx < 0) {
|
|
||||||
LOG_WARNING("loadEquippedWeapons: displayInfoId ", displayInfoId, " not found in DBC");
|
|
||||||
charRenderer->detachWeapon(charInstanceId, ws.attachmentId);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const auto* idiL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("ItemDisplayInfo") : nullptr;
|
|
||||||
std::string modelName = displayInfoDbc->getString(static_cast<uint32_t>(recIdx), idiL ? (*idiL)["LeftModel"] : 1);
|
|
||||||
std::string textureName = displayInfoDbc->getString(static_cast<uint32_t>(recIdx), idiL ? (*idiL)["LeftModelTexture"] : 3);
|
|
||||||
|
|
||||||
if (modelName.empty()) {
|
|
||||||
LOG_WARNING("loadEquippedWeapons: empty model name for displayInfoId ", displayInfoId);
|
|
||||||
charRenderer->detachWeapon(charInstanceId, ws.attachmentId);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert .mdx → .m2
|
|
||||||
std::string modelFile = modelName;
|
|
||||||
{
|
|
||||||
size_t dotPos = modelFile.rfind('.');
|
|
||||||
if (dotPos != std::string::npos) {
|
|
||||||
modelFile = modelFile.substr(0, dotPos) + ".m2";
|
|
||||||
} else {
|
|
||||||
modelFile += ".m2";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try Weapon directory first, then Shield
|
|
||||||
std::string m2Path = "Item\\ObjectComponents\\Weapon\\" + modelFile;
|
|
||||||
pipeline::M2Model weaponModel;
|
|
||||||
if (!loadWeaponM2(m2Path, weaponModel)) {
|
|
||||||
m2Path = "Item\\ObjectComponents\\Shield\\" + modelFile;
|
|
||||||
if (!loadWeaponM2(m2Path, weaponModel)) {
|
|
||||||
LOG_WARNING("loadEquippedWeapons: failed to load ", modelFile);
|
|
||||||
charRenderer->detachWeapon(charInstanceId, ws.attachmentId);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build texture path
|
|
||||||
std::string texturePath;
|
|
||||||
if (!textureName.empty()) {
|
|
||||||
texturePath = "Item\\ObjectComponents\\Weapon\\" + textureName + ".blp";
|
|
||||||
if (!assetManager->fileExists(texturePath)) {
|
|
||||||
texturePath = "Item\\ObjectComponents\\Shield\\" + textureName + ".blp";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
uint32_t weaponModelId = entitySpawner_->allocateWeaponModelId();
|
|
||||||
bool ok = charRenderer->attachWeapon(charInstanceId, ws.attachmentId,
|
|
||||||
weaponModel, weaponModelId, texturePath);
|
|
||||||
if (ok) {
|
|
||||||
LOG_INFO("Equipped weapon: ", m2Path, " at attachment ", ws.attachmentId);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -4514,8 +4189,8 @@ void Application::loadOnlineWorldTerrain(uint32_t mapId, float x, float y, float
|
||||||
playerGender_ = activeChar->gender;
|
playerGender_ = activeChar->gender;
|
||||||
playerClass_ = activeChar->characterClass;
|
playerClass_ = activeChar->characterClass;
|
||||||
spawnSnapToGround = false;
|
spawnSnapToGround = false;
|
||||||
weaponsSheathed_ = false;
|
if (appearanceComposer_) appearanceComposer_->setWeaponsSheathed(false);
|
||||||
loadEquippedWeapons(); // will no-op until instance exists
|
if (appearanceComposer_) appearanceComposer_->loadEquippedWeapons(); // will no-op until instance exists
|
||||||
spawnPlayerCharacter();
|
spawnPlayerCharacter();
|
||||||
}
|
}
|
||||||
renderer->getCharacterPosition() = spawnRender;
|
renderer->getCharacterPosition() = spawnRender;
|
||||||
|
|
@ -4568,7 +4243,7 @@ void Application::loadOnlineWorldTerrain(uint32_t mapId, float x, float y, float
|
||||||
// Spawn player character now that renderers are initialized
|
// Spawn player character now that renderers are initialized
|
||||||
if (!playerCharacterSpawned) {
|
if (!playerCharacterSpawned) {
|
||||||
spawnPlayerCharacter();
|
spawnPlayerCharacter();
|
||||||
loadEquippedWeapons();
|
if (appearanceComposer_) appearanceComposer_->loadEquippedWeapons();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load the root WMO
|
// Load the root WMO
|
||||||
|
|
@ -4810,7 +4485,7 @@ void Application::loadOnlineWorldTerrain(uint32_t mapId, float x, float y, float
|
||||||
// player model now that the renderer actually exists.
|
// player model now that the renderer actually exists.
|
||||||
if (!playerCharacterSpawned) {
|
if (!playerCharacterSpawned) {
|
||||||
spawnPlayerCharacter();
|
spawnPlayerCharacter();
|
||||||
loadEquippedWeapons();
|
if (appearanceComposer_) appearanceComposer_->loadEquippedWeapons();
|
||||||
}
|
}
|
||||||
|
|
||||||
showProgress("Streaming terrain tiles...", 0.35f);
|
showProgress("Streaming terrain tiles...", 0.35f);
|
||||||
|
|
|
||||||
|
|
@ -631,7 +631,7 @@ void GameScreen::render(game::GameHandler& gameHandler) {
|
||||||
if (inventoryScreen.consumeEquipmentDirty() || gameHandler.consumeOnlineEquipmentDirty()) {
|
if (inventoryScreen.consumeEquipmentDirty() || gameHandler.consumeOnlineEquipmentDirty()) {
|
||||||
updateCharacterGeosets(gameHandler.getInventory());
|
updateCharacterGeosets(gameHandler.getInventory());
|
||||||
updateCharacterTextures(gameHandler.getInventory());
|
updateCharacterTextures(gameHandler.getInventory());
|
||||||
core::Application::getInstance().loadEquippedWeapons();
|
if (auto* ac = core::Application::getInstance().getAppearanceComposer()) ac->loadEquippedWeapons();
|
||||||
inventoryScreen.markPreviewDirty();
|
inventoryScreen.markPreviewDirty();
|
||||||
// Update renderer weapon type for animation selection
|
// Update renderer weapon type for animation selection
|
||||||
auto* r = core::Application::getInstance().getRenderer();
|
auto* r = core::Application::getInstance().getRenderer();
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue