mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-03-22 23:30:14 +00:00
Fix Turtle/Classic parsing and online player textures
This commit is contained in:
parent
d2ff21a95f
commit
5afd1b65a8
13 changed files with 518 additions and 27 deletions
|
|
@ -2,6 +2,7 @@
|
|||
"OBJECT_FIELD_ENTRY": 3,
|
||||
"UNIT_FIELD_TARGET_LO": 16,
|
||||
"UNIT_FIELD_TARGET_HI": 17,
|
||||
"UNIT_FIELD_BYTES_0": 36,
|
||||
"UNIT_FIELD_HEALTH": 22,
|
||||
"UNIT_FIELD_POWER1": 23,
|
||||
"UNIT_FIELD_MAXHEALTH": 28,
|
||||
|
|
@ -15,6 +16,8 @@
|
|||
"UNIT_DYNAMIC_FLAGS": 143,
|
||||
"UNIT_END": 188,
|
||||
"PLAYER_FLAGS": 190,
|
||||
"PLAYER_BYTES": 191,
|
||||
"PLAYER_BYTES_2": 192,
|
||||
"PLAYER_XP": 716,
|
||||
"PLAYER_NEXT_LEVEL_XP": 717,
|
||||
"PLAYER_FIELD_COINAGE": 1176,
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
"OBJECT_FIELD_ENTRY": 3,
|
||||
"UNIT_FIELD_TARGET_LO": 16,
|
||||
"UNIT_FIELD_TARGET_HI": 17,
|
||||
"UNIT_FIELD_BYTES_0": 36,
|
||||
"UNIT_FIELD_HEALTH": 22,
|
||||
"UNIT_FIELD_POWER1": 23,
|
||||
"UNIT_FIELD_MAXHEALTH": 28,
|
||||
|
|
@ -16,6 +17,8 @@
|
|||
"UNIT_DYNAMIC_FLAGS": 164,
|
||||
"UNIT_END": 234,
|
||||
"PLAYER_FLAGS": 236,
|
||||
"PLAYER_BYTES": 237,
|
||||
"PLAYER_BYTES_2": 238,
|
||||
"PLAYER_XP": 926,
|
||||
"PLAYER_NEXT_LEVEL_XP": 927,
|
||||
"PLAYER_FIELD_COINAGE": 1441,
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
"OBJECT_FIELD_ENTRY": 3,
|
||||
"UNIT_FIELD_TARGET_LO": 16,
|
||||
"UNIT_FIELD_TARGET_HI": 17,
|
||||
"UNIT_FIELD_BYTES_0": 36,
|
||||
"UNIT_FIELD_HEALTH": 22,
|
||||
"UNIT_FIELD_POWER1": 23,
|
||||
"UNIT_FIELD_MAXHEALTH": 28,
|
||||
|
|
@ -15,6 +16,8 @@
|
|||
"UNIT_DYNAMIC_FLAGS": 143,
|
||||
"UNIT_END": 188,
|
||||
"PLAYER_FLAGS": 190,
|
||||
"PLAYER_BYTES": 191,
|
||||
"PLAYER_BYTES_2": 192,
|
||||
"PLAYER_XP": 716,
|
||||
"PLAYER_NEXT_LEVEL_XP": 717,
|
||||
"PLAYER_FIELD_COINAGE": 1176,
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
"OBJECT_FIELD_ENTRY": 3,
|
||||
"UNIT_FIELD_TARGET_LO": 6,
|
||||
"UNIT_FIELD_TARGET_HI": 7,
|
||||
"UNIT_FIELD_BYTES_0": 56,
|
||||
"UNIT_FIELD_HEALTH": 24,
|
||||
"UNIT_FIELD_POWER1": 25,
|
||||
"UNIT_FIELD_MAXHEALTH": 32,
|
||||
|
|
@ -16,6 +17,8 @@
|
|||
"UNIT_DYNAMIC_FLAGS": 147,
|
||||
"UNIT_END": 148,
|
||||
"PLAYER_FLAGS": 150,
|
||||
"PLAYER_BYTES": 151,
|
||||
"PLAYER_BYTES_2": 152,
|
||||
"PLAYER_XP": 634,
|
||||
"PLAYER_NEXT_LEVEL_XP": 635,
|
||||
"PLAYER_FIELD_COINAGE": 1170,
|
||||
|
|
|
|||
|
|
@ -90,6 +90,13 @@ private:
|
|||
void buildFactionHostilityMap(uint8_t playerRace);
|
||||
void spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x, float y, float z, float orientation);
|
||||
void despawnOnlineCreature(uint64_t guid);
|
||||
void spawnOnlinePlayer(uint64_t guid,
|
||||
uint8_t raceId,
|
||||
uint8_t genderId,
|
||||
uint32_t appearanceBytes,
|
||||
uint8_t facialFeatures,
|
||||
float x, float y, float z, float orientation);
|
||||
void despawnOnlinePlayer(uint64_t guid);
|
||||
void buildCreatureDisplayLookups();
|
||||
std::string getModelPathForDisplayId(uint32_t displayId) const;
|
||||
void spawnOnlineGameObject(uint64_t guid, uint32_t entry, uint32_t displayId, float x, float y, float z, float orientation);
|
||||
|
|
@ -218,6 +225,25 @@ private:
|
|||
std::unordered_set<uint64_t> pendingCreatureSpawnGuids_;
|
||||
std::unordered_map<uint64_t, uint16_t> creatureSpawnRetryCounts_;
|
||||
std::unordered_set<uint32_t> nonRenderableCreatureDisplayIds_;
|
||||
|
||||
// Online player instances (separate from creatures so we can apply per-player skin/hair textures).
|
||||
std::unordered_map<uint64_t, uint32_t> playerInstances_; // guid → render instanceId
|
||||
// Cache base player model geometry by (raceId, genderId)
|
||||
std::unordered_map<uint32_t, uint32_t> playerModelCache_; // key=(race<<8)|gender → modelId
|
||||
struct PlayerTextureSlots { int skin = -1; int hair = -1; int underwear = -1; };
|
||||
std::unordered_map<uint32_t, PlayerTextureSlots> playerTextureSlotsByModelId_;
|
||||
uint32_t nextPlayerModelId_ = 60000;
|
||||
struct PendingPlayerSpawn {
|
||||
uint64_t guid;
|
||||
uint8_t raceId;
|
||||
uint8_t genderId;
|
||||
uint32_t appearanceBytes;
|
||||
uint8_t facialFeatures;
|
||||
float x, y, z, orientation;
|
||||
};
|
||||
std::vector<PendingPlayerSpawn> pendingPlayerSpawns_;
|
||||
std::unordered_set<uint64_t> pendingPlayerSpawnGuids_;
|
||||
void processPlayerSpawnQueue();
|
||||
std::unordered_set<uint64_t> creaturePermanentFailureGuids_;
|
||||
void processCreatureSpawnQueue();
|
||||
|
||||
|
|
|
|||
|
|
@ -480,6 +480,20 @@ public:
|
|||
using CreatureDespawnCallback = std::function<void(uint64_t guid)>;
|
||||
void setCreatureDespawnCallback(CreatureDespawnCallback cb) { creatureDespawnCallback_ = std::move(cb); }
|
||||
|
||||
// Player spawn callback (online mode - triggered when a player enters view).
|
||||
// Players need appearance data so the renderer can build the right body/hair textures.
|
||||
using PlayerSpawnCallback = std::function<void(uint64_t guid,
|
||||
uint32_t displayId,
|
||||
uint8_t raceId,
|
||||
uint8_t genderId,
|
||||
uint32_t appearanceBytes,
|
||||
uint8_t facialFeatures,
|
||||
float x, float y, float z, float orientation)>;
|
||||
void setPlayerSpawnCallback(PlayerSpawnCallback cb) { playerSpawnCallback_ = std::move(cb); }
|
||||
|
||||
using PlayerDespawnCallback = std::function<void(uint64_t guid)>;
|
||||
void setPlayerDespawnCallback(PlayerDespawnCallback cb) { playerDespawnCallback_ = std::move(cb); }
|
||||
|
||||
// GameObject spawn callback (online mode - triggered when gameobject enters view)
|
||||
// Parameters: guid, entry, displayId, x, y, z (canonical), orientation
|
||||
using GameObjectSpawnCallback = std::function<void(uint64_t guid, uint32_t entry, uint32_t displayId, float x, float y, float z, float orientation)>;
|
||||
|
|
@ -1065,6 +1079,8 @@ private:
|
|||
BindPointCallback bindPointCallback_;
|
||||
CreatureSpawnCallback creatureSpawnCallback_;
|
||||
CreatureDespawnCallback creatureDespawnCallback_;
|
||||
PlayerSpawnCallback playerSpawnCallback_;
|
||||
PlayerDespawnCallback playerDespawnCallback_;
|
||||
CreatureMoveCallback creatureMoveCallback_;
|
||||
TransportMoveCallback transportMoveCallback_;
|
||||
TransportSpawnCallback transportSpawnCallback_;
|
||||
|
|
|
|||
|
|
@ -22,6 +22,10 @@ class PacketParsers {
|
|||
public:
|
||||
virtual ~PacketParsers() = default;
|
||||
|
||||
// Size of MovementInfo.flags2 in bytes for MSG_MOVE_* payloads.
|
||||
// Classic: none, TBC: u8, WotLK: u16.
|
||||
virtual uint8_t movementFlags2Size() const { return 2; }
|
||||
|
||||
// --- Movement ---
|
||||
|
||||
/** Parse movement block from SMSG_UPDATE_OBJECT */
|
||||
|
|
@ -145,6 +149,7 @@ class WotlkPacketParsers : public PacketParsers {
|
|||
*/
|
||||
class TbcPacketParsers : public PacketParsers {
|
||||
public:
|
||||
uint8_t movementFlags2Size() const override { return 1; }
|
||||
bool parseMovementBlock(network::Packet& packet, UpdateBlock& block) override;
|
||||
void writeMovementPayload(network::Packet& packet, const MovementInfo& info) override;
|
||||
network::Packet buildMovementPacket(LogicalOpcode opcode,
|
||||
|
|
@ -171,6 +176,7 @@ public:
|
|||
*/
|
||||
class ClassicPacketParsers : public TbcPacketParsers {
|
||||
public:
|
||||
uint8_t movementFlags2Size() const override { return 0; }
|
||||
bool parseCharEnum(network::Packet& packet, CharEnumResponse& response) override;
|
||||
bool parseMovementBlock(network::Packet& packet, UpdateBlock& block) override;
|
||||
void writeMovementPayload(network::Packet& packet, const MovementInfo& info) override;
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ enum class UF : uint16_t {
|
|||
// Unit fields
|
||||
UNIT_FIELD_TARGET_LO,
|
||||
UNIT_FIELD_TARGET_HI,
|
||||
UNIT_FIELD_BYTES_0,
|
||||
UNIT_FIELD_HEALTH,
|
||||
UNIT_FIELD_POWER1,
|
||||
UNIT_FIELD_MAXHEALTH,
|
||||
|
|
@ -34,6 +35,8 @@ enum class UF : uint16_t {
|
|||
|
||||
// Player fields
|
||||
PLAYER_FLAGS,
|
||||
PLAYER_BYTES,
|
||||
PLAYER_BYTES_2,
|
||||
PLAYER_XP,
|
||||
PLAYER_NEXT_LEVEL_XP,
|
||||
PLAYER_FIELD_COINAGE,
|
||||
|
|
|
|||
|
|
@ -66,6 +66,8 @@ public:
|
|||
const pipeline::M2Model* getModelData(uint32_t modelId) const;
|
||||
void setActiveGeosets(uint32_t instanceId, const std::unordered_set<uint16_t>& geosets);
|
||||
void setGroupTextureOverride(uint32_t instanceId, uint16_t geosetGroup, GLuint textureId);
|
||||
void setTextureSlotOverride(uint32_t instanceId, uint16_t textureSlot, GLuint textureId);
|
||||
void clearTextureSlotOverride(uint32_t instanceId, uint16_t textureSlot);
|
||||
void setInstanceVisible(uint32_t instanceId, bool visible);
|
||||
void removeInstance(uint32_t instanceId);
|
||||
bool getAnimationState(uint32_t instanceId, uint32_t& animationId, float& animationTimeMs, float& animationDurationMs) const;
|
||||
|
|
@ -151,6 +153,9 @@ private:
|
|||
// Per-geoset-group texture overrides (group → GL texture ID)
|
||||
std::unordered_map<uint16_t, GLuint> groupTextureOverrides;
|
||||
|
||||
// Per-texture-slot overrides (slot → GL texture ID)
|
||||
std::unordered_map<uint16_t, GLuint> textureSlotOverrides;
|
||||
|
||||
// Weapon attachments (weapons parented to this instance's bones)
|
||||
std::vector<WeaponAttachment> weaponAttachments;
|
||||
|
||||
|
|
|
|||
|
|
@ -590,6 +590,7 @@ void Application::update(float deltaTime) {
|
|||
worldTime += std::chrono::duration<float, std::milli>(w2 - w1).count();
|
||||
|
||||
auto cq1 = std::chrono::high_resolution_clock::now();
|
||||
processPlayerSpawnQueue();
|
||||
// Process deferred online creature spawns (throttled)
|
||||
processCreatureSpawnQueue();
|
||||
auto cq2 = std::chrono::high_resolution_clock::now();
|
||||
|
|
@ -1199,11 +1200,29 @@ void Application::setupUICallbacks() {
|
|||
pendingCreatureSpawnGuids_.insert(guid);
|
||||
});
|
||||
|
||||
// Player spawn callback (online mode) - spawn player models with correct textures
|
||||
gameHandler->setPlayerSpawnCallback([this](uint64_t guid,
|
||||
uint32_t /*displayId*/,
|
||||
uint8_t raceId,
|
||||
uint8_t genderId,
|
||||
uint32_t appearanceBytes,
|
||||
uint8_t facialFeatures,
|
||||
float x, float y, float z, float orientation) {
|
||||
if (playerInstances_.count(guid)) return;
|
||||
if (pendingPlayerSpawnGuids_.count(guid)) return;
|
||||
pendingPlayerSpawns_.push_back({guid, raceId, genderId, appearanceBytes, facialFeatures, x, y, z, orientation});
|
||||
pendingPlayerSpawnGuids_.insert(guid);
|
||||
});
|
||||
|
||||
// Creature despawn callback (online mode) - remove creature models
|
||||
gameHandler->setCreatureDespawnCallback([this](uint64_t guid) {
|
||||
despawnOnlineCreature(guid);
|
||||
});
|
||||
|
||||
gameHandler->setPlayerDespawnCallback([this](uint64_t guid) {
|
||||
despawnOnlinePlayer(guid);
|
||||
});
|
||||
|
||||
// GameObject spawn callback (online mode) - spawn static models (mailboxes, etc.)
|
||||
gameHandler->setGameObjectSpawnCallback([this](uint64_t guid, uint32_t entry, uint32_t displayId, float x, float y, float z, float orientation) {
|
||||
pendingGameObjectSpawns_.push_back({guid, entry, displayId, x, y, z, orientation});
|
||||
|
|
@ -1309,11 +1328,18 @@ void Application::setupUICallbacks() {
|
|||
|
||||
// Creature move callback (online mode) - update creature positions
|
||||
gameHandler->setCreatureMoveCallback([this](uint64_t guid, float x, float y, float z, uint32_t durationMs) {
|
||||
auto it = creatureInstances_.find(guid);
|
||||
if (it != creatureInstances_.end() && renderer && renderer->getCharacterRenderer()) {
|
||||
if (!renderer || !renderer->getCharacterRenderer()) return;
|
||||
uint32_t instanceId = 0;
|
||||
auto pit = playerInstances_.find(guid);
|
||||
if (pit != playerInstances_.end()) instanceId = pit->second;
|
||||
else {
|
||||
auto it = creatureInstances_.find(guid);
|
||||
if (it != creatureInstances_.end()) instanceId = it->second;
|
||||
}
|
||||
if (instanceId != 0) {
|
||||
glm::vec3 renderPos = core::coords::canonicalToRender(glm::vec3(x, y, z));
|
||||
float durationSec = static_cast<float>(durationMs) / 1000.0f;
|
||||
renderer->getCharacterRenderer()->moveInstanceTo(it->second, renderPos, durationSec);
|
||||
renderer->getCharacterRenderer()->moveInstanceTo(instanceId, renderPos, durationSec);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -2915,6 +2941,10 @@ bool Application::getRenderBoundsForGuid(uint64_t guid, glm::vec3& outCenter, fl
|
|||
if (gameHandler && guid == gameHandler->getPlayerGuid()) {
|
||||
instanceId = renderer->getCharacterInstanceId();
|
||||
}
|
||||
if (instanceId == 0) {
|
||||
auto pit = playerInstances_.find(guid);
|
||||
if (pit != playerInstances_.end()) instanceId = pit->second;
|
||||
}
|
||||
if (instanceId == 0) {
|
||||
auto it = creatureInstances_.find(guid);
|
||||
if (it != creatureInstances_.end()) instanceId = it->second;
|
||||
|
|
@ -3488,6 +3518,240 @@ void Application::spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x
|
|||
" displayId=", displayId, " at (", x, ", ", y, ", ", z, ")");
|
||||
}
|
||||
|
||||
void Application::spawnOnlinePlayer(uint64_t guid,
|
||||
uint8_t raceId,
|
||||
uint8_t genderId,
|
||||
uint32_t appearanceBytes,
|
||||
uint8_t facialFeatures,
|
||||
float x, float y, float z, float orientation) {
|
||||
if (!renderer || !renderer->getCharacterRenderer() || !assetManager || !assetManager->isInitialized()) return;
|
||||
if (playerInstances_.count(guid)) return;
|
||||
|
||||
auto* charRenderer = renderer->getCharacterRenderer();
|
||||
|
||||
// Base geometry model: cache by (race, gender)
|
||||
uint32_t cacheKey = (static_cast<uint32_t>(raceId) << 8) | static_cast<uint32_t>(genderId & 0xFF);
|
||||
uint32_t modelId = 0;
|
||||
auto itCache = playerModelCache_.find(cacheKey);
|
||||
if (itCache != playerModelCache_.end()) {
|
||||
modelId = itCache->second;
|
||||
} else {
|
||||
game::Race race = static_cast<game::Race>(raceId);
|
||||
game::Gender gender = (genderId == 1) ? game::Gender::FEMALE : game::Gender::MALE;
|
||||
std::string m2Path = game::getPlayerModelPath(race, gender);
|
||||
if (m2Path.empty()) {
|
||||
LOG_WARNING("spawnOnlinePlayer: unknown race/gender for guid 0x", std::hex, guid, std::dec,
|
||||
" race=", (int)raceId, " gender=", (int)genderId);
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse modelDir/baseName for skin/anim loading
|
||||
std::string modelDir;
|
||||
std::string baseName;
|
||||
{
|
||||
size_t slash = m2Path.rfind('\\');
|
||||
if (slash != std::string::npos) {
|
||||
modelDir = m2Path.substr(0, slash + 1);
|
||||
baseName = m2Path.substr(slash + 1);
|
||||
} else {
|
||||
baseName = m2Path;
|
||||
}
|
||||
size_t dot = baseName.rfind('.');
|
||||
if (dot != std::string::npos) baseName = baseName.substr(0, dot);
|
||||
}
|
||||
|
||||
auto m2Data = assetManager->readFile(m2Path);
|
||||
if (m2Data.empty()) {
|
||||
LOG_WARNING("spawnOnlinePlayer: failed to read M2: ", m2Path);
|
||||
return;
|
||||
}
|
||||
|
||||
pipeline::M2Model model = pipeline::M2Loader::load(m2Data);
|
||||
if (!model.isValid() || model.vertices.empty()) {
|
||||
LOG_WARNING("spawnOnlinePlayer: failed to parse M2: ", m2Path);
|
||||
return;
|
||||
}
|
||||
|
||||
// Skin file
|
||||
std::string skinPath = modelDir + baseName + "00.skin";
|
||||
auto skinData = assetManager->readFile(skinPath);
|
||||
if (!skinData.empty()) {
|
||||
pipeline::M2Loader::loadSkin(skinData, model);
|
||||
}
|
||||
|
||||
// Load only core external animations (stand/walk/run) to avoid stalls
|
||||
for (uint32_t si = 0; si < model.sequences.size(); si++) {
|
||||
if (!(model.sequences[si].flags & 0x20)) {
|
||||
uint32_t animId = model.sequences[si].id;
|
||||
if (animId != 0 && animId != 4 && animId != 5) continue;
|
||||
char animFileName[256];
|
||||
snprintf(animFileName, sizeof(animFileName),
|
||||
"%s%s%04u-%02u.anim",
|
||||
modelDir.c_str(),
|
||||
baseName.c_str(),
|
||||
animId,
|
||||
model.sequences[si].variationIndex);
|
||||
auto animData = assetManager->readFileOptional(animFileName);
|
||||
if (!animData.empty()) {
|
||||
pipeline::M2Loader::loadAnimFile(m2Data, animData, si, model);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
modelId = nextPlayerModelId_++;
|
||||
if (!charRenderer->loadModel(model, modelId)) {
|
||||
LOG_WARNING("spawnOnlinePlayer: failed to load model to GPU: ", m2Path);
|
||||
return;
|
||||
}
|
||||
|
||||
playerModelCache_[cacheKey] = modelId;
|
||||
}
|
||||
|
||||
// Determine texture slots once per model
|
||||
if (!playerTextureSlotsByModelId_.count(modelId)) {
|
||||
PlayerTextureSlots slots;
|
||||
if (const auto* md = charRenderer->getModelData(modelId)) {
|
||||
for (size_t ti = 0; ti < md->textures.size(); ti++) {
|
||||
uint32_t t = md->textures[ti].type;
|
||||
if (t == 1 && slots.skin < 0) slots.skin = (int)ti;
|
||||
else if (t == 6 && slots.hair < 0) slots.hair = (int)ti;
|
||||
else if (t == 8 && slots.underwear < 0) slots.underwear = (int)ti;
|
||||
}
|
||||
}
|
||||
playerTextureSlotsByModelId_[modelId] = slots;
|
||||
}
|
||||
|
||||
// Create instance at server position
|
||||
glm::vec3 renderPos = core::coords::canonicalToRender(glm::vec3(x, y, z));
|
||||
float renderYaw = orientation + glm::radians(90.0f);
|
||||
uint32_t instanceId = charRenderer->createInstance(modelId, renderPos, glm::vec3(0.0f, 0.0f, renderYaw), 1.0f);
|
||||
if (instanceId == 0) return;
|
||||
|
||||
// Resolve skin/hair texture paths via CharSections, then apply as per-instance overrides
|
||||
const char* raceFolderName = "Human";
|
||||
switch (static_cast<game::Race>(raceId)) {
|
||||
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 = (genderId == 1) ? "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::vector<std::string> underwearPaths;
|
||||
std::string hairTexturePath;
|
||||
|
||||
uint8_t skinId = appearanceBytes & 0xFF;
|
||||
uint8_t faceId = (appearanceBytes >> 8) & 0xFF;
|
||||
uint8_t hairStyleId = (appearanceBytes >> 16) & 0xFF;
|
||||
uint8_t hairColorId = (appearanceBytes >> 24) & 0xFF;
|
||||
|
||||
if (auto charSectionsDbc = assetManager->loadDBC("CharSections.dbc"); charSectionsDbc && charSectionsDbc->isLoaded()) {
|
||||
const auto* csL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("CharSections") : nullptr;
|
||||
uint32_t targetRaceId = raceId;
|
||||
uint32_t targetSexId = genderId;
|
||||
const uint32_t csTex1 = csL ? (*csL)["Texture1"] : 4;
|
||||
|
||||
bool foundSkin = false;
|
||||
bool foundUnderwear = false;
|
||||
bool foundHair = false;
|
||||
bool foundFaceLower = false;
|
||||
(void)faceId; // face lower not yet applied as separate layer
|
||||
|
||||
for (uint32_t r = 0; r < charSectionsDbc->getRecordCount(); r++) {
|
||||
uint32_t rRace = charSectionsDbc->getUInt32(r, csL ? (*csL)["RaceID"] : 1);
|
||||
uint32_t rSex = charSectionsDbc->getUInt32(r, csL ? (*csL)["SexID"] : 2);
|
||||
uint32_t baseSection = charSectionsDbc->getUInt32(r, csL ? (*csL)["BaseSection"] : 3);
|
||||
uint32_t variationIndex = charSectionsDbc->getUInt32(r, csL ? (*csL)["VariationIndex"] : 8);
|
||||
uint32_t colorIndex = charSectionsDbc->getUInt32(r, csL ? (*csL)["ColorIndex"] : 9);
|
||||
|
||||
if (rRace != targetRaceId || rSex != targetSexId) continue;
|
||||
|
||||
if (baseSection == 0 && !foundSkin && colorIndex == skinId) {
|
||||
std::string tex1 = charSectionsDbc->getString(r, csTex1);
|
||||
if (!tex1.empty()) { bodySkinPath = tex1; foundSkin = true; }
|
||||
} else if (baseSection == 3 && !foundHair &&
|
||||
variationIndex == hairStyleId && colorIndex == hairColorId) {
|
||||
hairTexturePath = charSectionsDbc->getString(r, csTex1);
|
||||
if (!hairTexturePath.empty()) foundHair = true;
|
||||
} else if (baseSection == 4 && !foundUnderwear && colorIndex == skinId) {
|
||||
for (uint32_t f = csTex1; f <= csTex1 + 2; f++) {
|
||||
std::string tex = charSectionsDbc->getString(r, f);
|
||||
if (!tex.empty()) underwearPaths.push_back(tex);
|
||||
}
|
||||
foundUnderwear = true;
|
||||
} else if (baseSection == 1 && !foundFaceLower &&
|
||||
variationIndex == faceId && colorIndex == skinId) {
|
||||
foundFaceLower = true;
|
||||
}
|
||||
|
||||
if (foundSkin && foundUnderwear && foundHair && foundFaceLower) break;
|
||||
}
|
||||
}
|
||||
|
||||
// Composite base skin + underwear overlays (same as local character logic)
|
||||
GLuint compositeTex = 0;
|
||||
if (!underwearPaths.empty()) {
|
||||
std::vector<std::string> layers;
|
||||
layers.push_back(bodySkinPath);
|
||||
for (const auto& up : underwearPaths) layers.push_back(up);
|
||||
compositeTex = charRenderer->compositeTextures(layers);
|
||||
} else {
|
||||
compositeTex = charRenderer->loadTexture(bodySkinPath);
|
||||
}
|
||||
|
||||
GLuint hairTex = 0;
|
||||
if (!hairTexturePath.empty()) {
|
||||
hairTex = charRenderer->loadTexture(hairTexturePath);
|
||||
}
|
||||
GLuint underwearTex = 0;
|
||||
if (!underwearPaths.empty()) underwearTex = charRenderer->loadTexture(underwearPaths[0]);
|
||||
else underwearTex = charRenderer->loadTexture(pelvisPath);
|
||||
|
||||
const PlayerTextureSlots& slots = playerTextureSlotsByModelId_[modelId];
|
||||
if (slots.skin >= 0 && compositeTex != 0) {
|
||||
charRenderer->setTextureSlotOverride(instanceId, static_cast<uint16_t>(slots.skin), compositeTex);
|
||||
}
|
||||
if (slots.hair >= 0 && hairTex != 0) {
|
||||
charRenderer->setTextureSlotOverride(instanceId, static_cast<uint16_t>(slots.hair), hairTex);
|
||||
}
|
||||
if (slots.underwear >= 0 && underwearTex != 0) {
|
||||
charRenderer->setTextureSlotOverride(instanceId, static_cast<uint16_t>(slots.underwear), underwearTex);
|
||||
}
|
||||
|
||||
// Geosets: body + hair/facial hair selections
|
||||
std::unordered_set<uint16_t> activeGeosets;
|
||||
for (uint16_t i = 0; i <= 18; i++) activeGeosets.insert(i);
|
||||
activeGeosets.insert(static_cast<uint16_t>(100 + hairStyleId + 1));
|
||||
activeGeosets.insert(static_cast<uint16_t>(200 + facialFeatures + 1));
|
||||
activeGeosets.insert(301);
|
||||
activeGeosets.insert(401);
|
||||
activeGeosets.insert(501);
|
||||
activeGeosets.insert(701);
|
||||
activeGeosets.insert(1301);
|
||||
activeGeosets.insert(1501);
|
||||
charRenderer->setActiveGeosets(instanceId, activeGeosets);
|
||||
|
||||
charRenderer->playAnimation(instanceId, 0, true);
|
||||
playerInstances_[guid] = instanceId;
|
||||
}
|
||||
|
||||
void Application::despawnOnlinePlayer(uint64_t guid) {
|
||||
if (!renderer || !renderer->getCharacterRenderer()) return;
|
||||
auto it = playerInstances_.find(guid);
|
||||
if (it == playerInstances_.end()) return;
|
||||
renderer->getCharacterRenderer()->removeInstance(it->second);
|
||||
playerInstances_.erase(it);
|
||||
}
|
||||
|
||||
void Application::spawnOnlineGameObject(uint64_t guid, uint32_t entry, uint32_t displayId, float x, float y, float z, float orientation) {
|
||||
if (!renderer || !assetManager) return;
|
||||
|
||||
|
|
@ -3818,6 +4082,27 @@ void Application::processCreatureSpawnQueue() {
|
|||
}
|
||||
}
|
||||
|
||||
void Application::processPlayerSpawnQueue() {
|
||||
if (pendingPlayerSpawns_.empty()) return;
|
||||
if (!assetManager || !assetManager->isInitialized()) return;
|
||||
|
||||
int processed = 0;
|
||||
while (!pendingPlayerSpawns_.empty() && processed < MAX_SPAWNS_PER_FRAME) {
|
||||
PendingPlayerSpawn s = pendingPlayerSpawns_.front();
|
||||
pendingPlayerSpawns_.erase(pendingPlayerSpawns_.begin());
|
||||
pendingPlayerSpawnGuids_.erase(s.guid);
|
||||
|
||||
// Skip if already spawned (could have been spawned by a previous update this frame)
|
||||
if (playerInstances_.count(s.guid)) {
|
||||
processed++;
|
||||
continue;
|
||||
}
|
||||
|
||||
spawnOnlinePlayer(s.guid, s.raceId, s.genderId, s.appearanceBytes, s.facialFeatures, s.x, s.y, s.z, s.orientation);
|
||||
processed++;
|
||||
}
|
||||
}
|
||||
|
||||
void Application::processGameObjectSpawnQueue() {
|
||||
if (pendingGameObjectSpawns_.empty()) return;
|
||||
|
||||
|
|
@ -4083,6 +4368,13 @@ void Application::processPendingMount() {
|
|||
}
|
||||
|
||||
void Application::despawnOnlineCreature(uint64_t guid) {
|
||||
// If this guid is a PLAYER, it will be tracked in playerInstances_.
|
||||
// Route to the correct despawn path so we don't leak instances.
|
||||
if (playerInstances_.count(guid)) {
|
||||
despawnOnlinePlayer(guid);
|
||||
return;
|
||||
}
|
||||
|
||||
pendingCreatureSpawnGuids_.erase(guid);
|
||||
creatureSpawnRetryCounts_.erase(guid);
|
||||
creaturePermanentFailureGuids_.erase(guid);
|
||||
|
|
|
|||
|
|
@ -1558,14 +1558,10 @@ void GameHandler::handleCharEnum(network::Packet& packet) {
|
|||
LOG_INFO("Handling SMSG_CHAR_ENUM");
|
||||
|
||||
CharEnumResponse response;
|
||||
bool parsed = false;
|
||||
if (build <= 6005) {
|
||||
// Vanilla 1.12.x format (different equipment layout, no customization flag)
|
||||
ClassicPacketParsers classicParser;
|
||||
parsed = classicParser.parseCharEnum(packet, response);
|
||||
} else {
|
||||
parsed = CharEnumParser::parse(packet, response);
|
||||
}
|
||||
// IMPORTANT: Do not infer packet formats from numeric build alone.
|
||||
// Turtle WoW uses a "high" build but classic-era world packet formats.
|
||||
bool parsed = packetParsers_ ? packetParsers_->parseCharEnum(packet, response)
|
||||
: CharEnumParser::parse(packet, response);
|
||||
if (!parsed) {
|
||||
fail("Failed to parse SMSG_CHAR_ENUM");
|
||||
return;
|
||||
|
|
@ -2688,6 +2684,87 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
|
|||
return;
|
||||
}
|
||||
|
||||
auto extractPlayerAppearance = [&](const std::map<uint16_t, uint32_t>& fields,
|
||||
uint8_t& outRace,
|
||||
uint8_t& outGender,
|
||||
uint32_t& outAppearanceBytes,
|
||||
uint8_t& outFacial) -> bool {
|
||||
outRace = 0;
|
||||
outGender = 0;
|
||||
outAppearanceBytes = 0;
|
||||
outFacial = 0;
|
||||
|
||||
auto readField = [&](uint16_t idx, uint32_t& out) -> bool {
|
||||
if (idx == 0xFFFF) return false;
|
||||
auto it = fields.find(idx);
|
||||
if (it == fields.end()) return false;
|
||||
out = it->second;
|
||||
return true;
|
||||
};
|
||||
|
||||
uint32_t bytes0 = 0;
|
||||
uint32_t pbytes = 0;
|
||||
uint32_t pbytes2 = 0;
|
||||
|
||||
const uint16_t ufBytes0 = fieldIndex(UF::UNIT_FIELD_BYTES_0);
|
||||
const uint16_t ufPbytes = fieldIndex(UF::PLAYER_BYTES);
|
||||
const uint16_t ufPbytes2 = fieldIndex(UF::PLAYER_BYTES_2);
|
||||
|
||||
bool haveBytes0 = readField(ufBytes0, bytes0);
|
||||
bool havePbytes = readField(ufPbytes, pbytes);
|
||||
bool havePbytes2 = readField(ufPbytes2, pbytes2);
|
||||
|
||||
// Heuristic fallback: Turtle can run with unusual build numbers; if the JSON table is missing,
|
||||
// try to locate plausible packed fields by scanning.
|
||||
if (!haveBytes0) {
|
||||
for (const auto& [idx, v] : fields) {
|
||||
uint8_t race = static_cast<uint8_t>(v & 0xFF);
|
||||
uint8_t cls = static_cast<uint8_t>((v >> 8) & 0xFF);
|
||||
uint8_t gender = static_cast<uint8_t>((v >> 16) & 0xFF);
|
||||
uint8_t power = static_cast<uint8_t>((v >> 24) & 0xFF);
|
||||
if (race >= 1 && race <= 20 &&
|
||||
cls >= 1 && cls <= 20 &&
|
||||
gender <= 1 &&
|
||||
power <= 10) {
|
||||
bytes0 = v;
|
||||
haveBytes0 = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!havePbytes) {
|
||||
for (const auto& [idx, v] : fields) {
|
||||
uint8_t skin = static_cast<uint8_t>(v & 0xFF);
|
||||
uint8_t face = static_cast<uint8_t>((v >> 8) & 0xFF);
|
||||
uint8_t hair = static_cast<uint8_t>((v >> 16) & 0xFF);
|
||||
uint8_t color = static_cast<uint8_t>((v >> 24) & 0xFF);
|
||||
if (skin <= 50 && face <= 50 && hair <= 100 && color <= 50) {
|
||||
pbytes = v;
|
||||
havePbytes = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!havePbytes2) {
|
||||
for (const auto& [idx, v] : fields) {
|
||||
uint8_t facial = static_cast<uint8_t>(v & 0xFF);
|
||||
if (facial <= 100) {
|
||||
pbytes2 = v;
|
||||
havePbytes2 = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!haveBytes0 || !havePbytes) return false;
|
||||
|
||||
outRace = static_cast<uint8_t>(bytes0 & 0xFF);
|
||||
outGender = static_cast<uint8_t>((bytes0 >> 16) & 0xFF);
|
||||
outAppearanceBytes = pbytes;
|
||||
outFacial = havePbytes2 ? static_cast<uint8_t>(pbytes2 & 0xFF) : 0;
|
||||
return true;
|
||||
};
|
||||
|
||||
// Process out-of-range objects first
|
||||
for (uint64_t guid : data.outOfRangeGuids) {
|
||||
if (entityManager.hasEntity(guid)) {
|
||||
|
|
@ -2713,6 +2790,8 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
|
|||
if (entity) {
|
||||
if (entity->getType() == ObjectType::UNIT && creatureDespawnCallback_) {
|
||||
creatureDespawnCallback_(guid);
|
||||
} else if (entity->getType() == ObjectType::PLAYER && playerDespawnCallback_) {
|
||||
playerDespawnCallback_(guid);
|
||||
} else if (entity->getType() == ObjectType::GAMEOBJECT && gameObjectDespawnCallback_) {
|
||||
gameObjectDespawnCallback_(guid);
|
||||
}
|
||||
|
|
@ -2903,9 +2982,20 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
|
|||
if (unit->getFactionTemplate() != 0) {
|
||||
unit->setHostile(isHostileFaction(unit->getFactionTemplate()));
|
||||
}
|
||||
// Trigger creature spawn callback for units/players with displayId
|
||||
// Trigger creature spawn callback for units/players with displayId
|
||||
if ((block.objectType == ObjectType::UNIT || block.objectType == ObjectType::PLAYER) && unit->getDisplayId() != 0) {
|
||||
if (creatureSpawnCallback_) {
|
||||
if (block.objectType == ObjectType::PLAYER && block.guid != playerGuid) {
|
||||
if (playerSpawnCallback_) {
|
||||
uint8_t race = 0, gender = 0, facial = 0;
|
||||
uint32_t appearanceBytes = 0;
|
||||
// Use the entity's accumulated field state, not just this block's changed fields.
|
||||
if (extractPlayerAppearance(entity->getFields(), race, gender, appearanceBytes, facial)) {
|
||||
playerSpawnCallback_(block.guid, unit->getDisplayId(), race, gender,
|
||||
appearanceBytes, facial,
|
||||
unit->getX(), unit->getY(), unit->getZ(), unit->getOrientation());
|
||||
}
|
||||
}
|
||||
} else if (creatureSpawnCallback_) {
|
||||
creatureSpawnCallback_(block.guid, unit->getDisplayId(),
|
||||
unit->getX(), unit->getY(), unit->getZ(), unit->getOrientation());
|
||||
}
|
||||
|
|
@ -3238,7 +3328,18 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
|
|||
displayIdChanged &&
|
||||
unit->getDisplayId() != 0 &&
|
||||
unit->getDisplayId() != oldDisplayId) {
|
||||
if (creatureSpawnCallback_) {
|
||||
if (entity->getType() == ObjectType::PLAYER && block.guid != playerGuid) {
|
||||
if (playerSpawnCallback_) {
|
||||
uint8_t race = 0, gender = 0, facial = 0;
|
||||
uint32_t appearanceBytes = 0;
|
||||
// Use the entity's accumulated field state, not just this block's changed fields.
|
||||
if (extractPlayerAppearance(entity->getFields(), race, gender, appearanceBytes, facial)) {
|
||||
playerSpawnCallback_(block.guid, unit->getDisplayId(), race, gender,
|
||||
appearanceBytes, facial,
|
||||
unit->getX(), unit->getY(), unit->getZ(), unit->getOrientation());
|
||||
}
|
||||
}
|
||||
} else if (creatureSpawnCallback_) {
|
||||
creatureSpawnCallback_(block.guid, unit->getDisplayId(),
|
||||
unit->getX(), unit->getY(), unit->getZ(), unit->getOrientation());
|
||||
}
|
||||
|
|
@ -5332,14 +5433,11 @@ void GameHandler::handleOtherPlayerMovement(network::Packet& packet) {
|
|||
// For classic: moveFlags(u32) + time(u32) + pos(4xf32) + [transport] + [pitch] + fallTime(u32) + [jump] + [splineElev]
|
||||
MovementInfo info = {};
|
||||
info.flags = packet.readUInt32();
|
||||
// WotLK has uint16 flags2, classic/TBC don't
|
||||
if (build >= 8606) { // TBC+
|
||||
if (build >= 12340) {
|
||||
info.flags2 = packet.readUInt16();
|
||||
} else {
|
||||
info.flags2 = packet.readUInt8();
|
||||
}
|
||||
}
|
||||
// WotLK has u16 flags2, TBC has u8, Classic has none.
|
||||
// Do NOT use build-number thresholds here (Turtle uses classic formats with a high build).
|
||||
uint8_t flags2Size = packetParsers_ ? packetParsers_->movementFlags2Size() : 2;
|
||||
if (flags2Size == 2) info.flags2 = packet.readUInt16();
|
||||
else if (flags2Size == 1) info.flags2 = packet.readUInt8();
|
||||
info.time = packet.readUInt32();
|
||||
info.x = packet.readFloat();
|
||||
info.y = packet.readFloat();
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ static const UFNameEntry kUFNames[] = {
|
|||
{"OBJECT_FIELD_ENTRY", UF::OBJECT_FIELD_ENTRY},
|
||||
{"UNIT_FIELD_TARGET_LO", UF::UNIT_FIELD_TARGET_LO},
|
||||
{"UNIT_FIELD_TARGET_HI", UF::UNIT_FIELD_TARGET_HI},
|
||||
{"UNIT_FIELD_BYTES_0", UF::UNIT_FIELD_BYTES_0},
|
||||
{"UNIT_FIELD_HEALTH", UF::UNIT_FIELD_HEALTH},
|
||||
{"UNIT_FIELD_POWER1", UF::UNIT_FIELD_POWER1},
|
||||
{"UNIT_FIELD_MAXHEALTH", UF::UNIT_FIELD_MAXHEALTH},
|
||||
|
|
@ -35,6 +36,8 @@ static const UFNameEntry kUFNames[] = {
|
|||
{"UNIT_DYNAMIC_FLAGS", UF::UNIT_DYNAMIC_FLAGS},
|
||||
{"UNIT_END", UF::UNIT_END},
|
||||
{"PLAYER_FLAGS", UF::PLAYER_FLAGS},
|
||||
{"PLAYER_BYTES", UF::PLAYER_BYTES},
|
||||
{"PLAYER_BYTES_2", UF::PLAYER_BYTES_2},
|
||||
{"PLAYER_XP", UF::PLAYER_XP},
|
||||
{"PLAYER_NEXT_LEVEL_XP", UF::PLAYER_NEXT_LEVEL_XP},
|
||||
{"PLAYER_FIELD_COINAGE", UF::PLAYER_FIELD_COINAGE},
|
||||
|
|
@ -55,6 +58,7 @@ void UpdateFieldTable::loadWotlkDefaults() {
|
|||
{UF::OBJECT_FIELD_ENTRY, 3},
|
||||
{UF::UNIT_FIELD_TARGET_LO, 6},
|
||||
{UF::UNIT_FIELD_TARGET_HI, 7},
|
||||
{UF::UNIT_FIELD_BYTES_0, 56},
|
||||
{UF::UNIT_FIELD_HEALTH, 24},
|
||||
{UF::UNIT_FIELD_POWER1, 25},
|
||||
{UF::UNIT_FIELD_MAXHEALTH, 32},
|
||||
|
|
@ -69,6 +73,8 @@ void UpdateFieldTable::loadWotlkDefaults() {
|
|||
{UF::UNIT_DYNAMIC_FLAGS, 147},
|
||||
{UF::UNIT_END, 148},
|
||||
{UF::PLAYER_FLAGS, 150},
|
||||
{UF::PLAYER_BYTES, 151},
|
||||
{UF::PLAYER_BYTES_2, 152},
|
||||
{UF::PLAYER_XP, 634},
|
||||
{UF::PLAYER_NEXT_LEVEL_XP, 635},
|
||||
{UF::PLAYER_FIELD_COINAGE, 1170},
|
||||
|
|
|
|||
|
|
@ -1291,7 +1291,7 @@ void CharacterRenderer::render(const Camera& camera, const glm::mat4& view, cons
|
|||
}
|
||||
}
|
||||
|
||||
auto resolveBatchTexture = [&](const M2ModelGPU& gm, const pipeline::M2Batch& b) -> GLuint {
|
||||
auto resolveBatchTexture = [&](const CharacterInstance& inst, const M2ModelGPU& gm, const pipeline::M2Batch& b) -> GLuint {
|
||||
// A skin batch can reference multiple textures (b.textureCount) starting at b.textureIndex.
|
||||
// We currently bind only a single texture, so pick the most appropriate one.
|
||||
//
|
||||
|
|
@ -1316,6 +1316,10 @@ void CharacterRenderer::render(const Camera& camera, const glm::mat4& view, cons
|
|||
if (texSlot >= gm.textureIds.size()) continue;
|
||||
|
||||
GLuint texId = gm.textureIds[texSlot];
|
||||
auto itO = inst.textureSlotOverrides.find(texSlot);
|
||||
if (itO != inst.textureSlotOverrides.end() && itO->second != 0) {
|
||||
texId = itO->second;
|
||||
}
|
||||
uint32_t texType = (texSlot < gm.data.textures.size()) ? gm.data.textures[texSlot].type : 0;
|
||||
|
||||
if (!hasFirst) {
|
||||
|
|
@ -1353,7 +1357,7 @@ void CharacterRenderer::render(const Camera& camera, const glm::mat4& view, cons
|
|||
(b.submeshId / 100 != 0) &&
|
||||
instance.activeGeosets.find(b.submeshId) == instance.activeGeosets.end();
|
||||
|
||||
GLuint resolvedTex = resolveBatchTexture(gpuModel, b);
|
||||
GLuint resolvedTex = resolveBatchTexture(instance, gpuModel, b);
|
||||
std::string texInfo = "GL" + std::to_string(resolvedTex);
|
||||
|
||||
if (filtered) skipped++; else rendered++;
|
||||
|
|
@ -1383,7 +1387,7 @@ void CharacterRenderer::render(const Camera& camera, const glm::mat4& view, cons
|
|||
}
|
||||
|
||||
// Resolve texture for this batch (prefer hair textures for hair geosets).
|
||||
GLuint texId = resolveBatchTexture(gpuModel, batch);
|
||||
GLuint texId = resolveBatchTexture(instance, gpuModel, batch);
|
||||
|
||||
// For body parts with white/fallback texture, use skin (type 1) texture
|
||||
// This handles humanoid models where some body parts use different texture slots
|
||||
|
|
@ -1404,11 +1408,16 @@ void CharacterRenderer::render(const Camera& camera, const glm::mat4& view, cons
|
|||
// Do NOT apply skin composite to hair (type 6) batches
|
||||
if (texType != 6) {
|
||||
for (size_t ti = 0; ti < gpuModel.textureIds.size(); ti++) {
|
||||
if (gpuModel.textureIds[ti] != whiteTexture && gpuModel.textureIds[ti] != 0) {
|
||||
GLuint candidate = gpuModel.textureIds[ti];
|
||||
auto itO = instance.textureSlotOverrides.find(static_cast<uint16_t>(ti));
|
||||
if (itO != instance.textureSlotOverrides.end() && itO->second != 0) {
|
||||
candidate = itO->second;
|
||||
}
|
||||
if (candidate != whiteTexture && candidate != 0) {
|
||||
// Only use type 1 (skin) textures as fallback
|
||||
if (ti < gpuModel.data.textures.size() &&
|
||||
(gpuModel.data.textures[ti].type == 1 || gpuModel.data.textures[ti].type == 11)) {
|
||||
texId = gpuModel.textureIds[ti];
|
||||
texId = candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
@ -1489,6 +1498,10 @@ void CharacterRenderer::renderShadow(const glm::mat4& lightSpaceMatrix) {
|
|||
uint16_t lookupIdx = gpuModel.data.textureLookup[batch.textureIndex];
|
||||
if (lookupIdx < gpuModel.textureIds.size()) {
|
||||
texId = gpuModel.textureIds[lookupIdx];
|
||||
auto itO = instance.textureSlotOverrides.find(lookupIdx);
|
||||
if (itO != instance.textureSlotOverrides.end() && itO->second != 0) {
|
||||
texId = itO->second;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1613,6 +1626,20 @@ void CharacterRenderer::setGroupTextureOverride(uint32_t instanceId, uint16_t ge
|
|||
}
|
||||
}
|
||||
|
||||
void CharacterRenderer::setTextureSlotOverride(uint32_t instanceId, uint16_t textureSlot, GLuint textureId) {
|
||||
auto it = instances.find(instanceId);
|
||||
if (it != instances.end()) {
|
||||
it->second.textureSlotOverrides[textureSlot] = textureId;
|
||||
}
|
||||
}
|
||||
|
||||
void CharacterRenderer::clearTextureSlotOverride(uint32_t instanceId, uint16_t textureSlot) {
|
||||
auto it = instances.find(instanceId);
|
||||
if (it != instances.end()) {
|
||||
it->second.textureSlotOverrides.erase(textureSlot);
|
||||
}
|
||||
}
|
||||
|
||||
void CharacterRenderer::setInstanceVisible(uint32_t instanceId, bool visible) {
|
||||
auto it = instances.find(instanceId);
|
||||
if (it != instances.end()) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue