mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-05-03 08:03:50 +00:00
Fix Turtle/Classic parsing and online player textures
This commit is contained in:
parent
010243bbd9
commit
bcfc075e1e
13 changed files with 518 additions and 27 deletions
|
|
@ -2,6 +2,7 @@
|
||||||
"OBJECT_FIELD_ENTRY": 3,
|
"OBJECT_FIELD_ENTRY": 3,
|
||||||
"UNIT_FIELD_TARGET_LO": 16,
|
"UNIT_FIELD_TARGET_LO": 16,
|
||||||
"UNIT_FIELD_TARGET_HI": 17,
|
"UNIT_FIELD_TARGET_HI": 17,
|
||||||
|
"UNIT_FIELD_BYTES_0": 36,
|
||||||
"UNIT_FIELD_HEALTH": 22,
|
"UNIT_FIELD_HEALTH": 22,
|
||||||
"UNIT_FIELD_POWER1": 23,
|
"UNIT_FIELD_POWER1": 23,
|
||||||
"UNIT_FIELD_MAXHEALTH": 28,
|
"UNIT_FIELD_MAXHEALTH": 28,
|
||||||
|
|
@ -15,6 +16,8 @@
|
||||||
"UNIT_DYNAMIC_FLAGS": 143,
|
"UNIT_DYNAMIC_FLAGS": 143,
|
||||||
"UNIT_END": 188,
|
"UNIT_END": 188,
|
||||||
"PLAYER_FLAGS": 190,
|
"PLAYER_FLAGS": 190,
|
||||||
|
"PLAYER_BYTES": 191,
|
||||||
|
"PLAYER_BYTES_2": 192,
|
||||||
"PLAYER_XP": 716,
|
"PLAYER_XP": 716,
|
||||||
"PLAYER_NEXT_LEVEL_XP": 717,
|
"PLAYER_NEXT_LEVEL_XP": 717,
|
||||||
"PLAYER_FIELD_COINAGE": 1176,
|
"PLAYER_FIELD_COINAGE": 1176,
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
"OBJECT_FIELD_ENTRY": 3,
|
"OBJECT_FIELD_ENTRY": 3,
|
||||||
"UNIT_FIELD_TARGET_LO": 16,
|
"UNIT_FIELD_TARGET_LO": 16,
|
||||||
"UNIT_FIELD_TARGET_HI": 17,
|
"UNIT_FIELD_TARGET_HI": 17,
|
||||||
|
"UNIT_FIELD_BYTES_0": 36,
|
||||||
"UNIT_FIELD_HEALTH": 22,
|
"UNIT_FIELD_HEALTH": 22,
|
||||||
"UNIT_FIELD_POWER1": 23,
|
"UNIT_FIELD_POWER1": 23,
|
||||||
"UNIT_FIELD_MAXHEALTH": 28,
|
"UNIT_FIELD_MAXHEALTH": 28,
|
||||||
|
|
@ -16,6 +17,8 @@
|
||||||
"UNIT_DYNAMIC_FLAGS": 164,
|
"UNIT_DYNAMIC_FLAGS": 164,
|
||||||
"UNIT_END": 234,
|
"UNIT_END": 234,
|
||||||
"PLAYER_FLAGS": 236,
|
"PLAYER_FLAGS": 236,
|
||||||
|
"PLAYER_BYTES": 237,
|
||||||
|
"PLAYER_BYTES_2": 238,
|
||||||
"PLAYER_XP": 926,
|
"PLAYER_XP": 926,
|
||||||
"PLAYER_NEXT_LEVEL_XP": 927,
|
"PLAYER_NEXT_LEVEL_XP": 927,
|
||||||
"PLAYER_FIELD_COINAGE": 1441,
|
"PLAYER_FIELD_COINAGE": 1441,
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
"OBJECT_FIELD_ENTRY": 3,
|
"OBJECT_FIELD_ENTRY": 3,
|
||||||
"UNIT_FIELD_TARGET_LO": 16,
|
"UNIT_FIELD_TARGET_LO": 16,
|
||||||
"UNIT_FIELD_TARGET_HI": 17,
|
"UNIT_FIELD_TARGET_HI": 17,
|
||||||
|
"UNIT_FIELD_BYTES_0": 36,
|
||||||
"UNIT_FIELD_HEALTH": 22,
|
"UNIT_FIELD_HEALTH": 22,
|
||||||
"UNIT_FIELD_POWER1": 23,
|
"UNIT_FIELD_POWER1": 23,
|
||||||
"UNIT_FIELD_MAXHEALTH": 28,
|
"UNIT_FIELD_MAXHEALTH": 28,
|
||||||
|
|
@ -15,6 +16,8 @@
|
||||||
"UNIT_DYNAMIC_FLAGS": 143,
|
"UNIT_DYNAMIC_FLAGS": 143,
|
||||||
"UNIT_END": 188,
|
"UNIT_END": 188,
|
||||||
"PLAYER_FLAGS": 190,
|
"PLAYER_FLAGS": 190,
|
||||||
|
"PLAYER_BYTES": 191,
|
||||||
|
"PLAYER_BYTES_2": 192,
|
||||||
"PLAYER_XP": 716,
|
"PLAYER_XP": 716,
|
||||||
"PLAYER_NEXT_LEVEL_XP": 717,
|
"PLAYER_NEXT_LEVEL_XP": 717,
|
||||||
"PLAYER_FIELD_COINAGE": 1176,
|
"PLAYER_FIELD_COINAGE": 1176,
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
"OBJECT_FIELD_ENTRY": 3,
|
"OBJECT_FIELD_ENTRY": 3,
|
||||||
"UNIT_FIELD_TARGET_LO": 6,
|
"UNIT_FIELD_TARGET_LO": 6,
|
||||||
"UNIT_FIELD_TARGET_HI": 7,
|
"UNIT_FIELD_TARGET_HI": 7,
|
||||||
|
"UNIT_FIELD_BYTES_0": 56,
|
||||||
"UNIT_FIELD_HEALTH": 24,
|
"UNIT_FIELD_HEALTH": 24,
|
||||||
"UNIT_FIELD_POWER1": 25,
|
"UNIT_FIELD_POWER1": 25,
|
||||||
"UNIT_FIELD_MAXHEALTH": 32,
|
"UNIT_FIELD_MAXHEALTH": 32,
|
||||||
|
|
@ -16,6 +17,8 @@
|
||||||
"UNIT_DYNAMIC_FLAGS": 147,
|
"UNIT_DYNAMIC_FLAGS": 147,
|
||||||
"UNIT_END": 148,
|
"UNIT_END": 148,
|
||||||
"PLAYER_FLAGS": 150,
|
"PLAYER_FLAGS": 150,
|
||||||
|
"PLAYER_BYTES": 151,
|
||||||
|
"PLAYER_BYTES_2": 152,
|
||||||
"PLAYER_XP": 634,
|
"PLAYER_XP": 634,
|
||||||
"PLAYER_NEXT_LEVEL_XP": 635,
|
"PLAYER_NEXT_LEVEL_XP": 635,
|
||||||
"PLAYER_FIELD_COINAGE": 1170,
|
"PLAYER_FIELD_COINAGE": 1170,
|
||||||
|
|
|
||||||
|
|
@ -90,6 +90,13 @@ private:
|
||||||
void buildFactionHostilityMap(uint8_t playerRace);
|
void buildFactionHostilityMap(uint8_t playerRace);
|
||||||
void spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x, float y, float z, float orientation);
|
void spawnOnlineCreature(uint64_t guid, uint32_t displayId, float x, float y, float z, float orientation);
|
||||||
void despawnOnlineCreature(uint64_t guid);
|
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();
|
void buildCreatureDisplayLookups();
|
||||||
std::string getModelPathForDisplayId(uint32_t displayId) const;
|
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);
|
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_set<uint64_t> pendingCreatureSpawnGuids_;
|
||||||
std::unordered_map<uint64_t, uint16_t> creatureSpawnRetryCounts_;
|
std::unordered_map<uint64_t, uint16_t> creatureSpawnRetryCounts_;
|
||||||
std::unordered_set<uint32_t> nonRenderableCreatureDisplayIds_;
|
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_;
|
std::unordered_set<uint64_t> creaturePermanentFailureGuids_;
|
||||||
void processCreatureSpawnQueue();
|
void processCreatureSpawnQueue();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -480,6 +480,20 @@ public:
|
||||||
using CreatureDespawnCallback = std::function<void(uint64_t guid)>;
|
using CreatureDespawnCallback = std::function<void(uint64_t guid)>;
|
||||||
void setCreatureDespawnCallback(CreatureDespawnCallback cb) { creatureDespawnCallback_ = std::move(cb); }
|
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)
|
// GameObject spawn callback (online mode - triggered when gameobject enters view)
|
||||||
// Parameters: guid, entry, displayId, x, y, z (canonical), orientation
|
// 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)>;
|
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_;
|
BindPointCallback bindPointCallback_;
|
||||||
CreatureSpawnCallback creatureSpawnCallback_;
|
CreatureSpawnCallback creatureSpawnCallback_;
|
||||||
CreatureDespawnCallback creatureDespawnCallback_;
|
CreatureDespawnCallback creatureDespawnCallback_;
|
||||||
|
PlayerSpawnCallback playerSpawnCallback_;
|
||||||
|
PlayerDespawnCallback playerDespawnCallback_;
|
||||||
CreatureMoveCallback creatureMoveCallback_;
|
CreatureMoveCallback creatureMoveCallback_;
|
||||||
TransportMoveCallback transportMoveCallback_;
|
TransportMoveCallback transportMoveCallback_;
|
||||||
TransportSpawnCallback transportSpawnCallback_;
|
TransportSpawnCallback transportSpawnCallback_;
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,10 @@ class PacketParsers {
|
||||||
public:
|
public:
|
||||||
virtual ~PacketParsers() = default;
|
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 ---
|
// --- Movement ---
|
||||||
|
|
||||||
/** Parse movement block from SMSG_UPDATE_OBJECT */
|
/** Parse movement block from SMSG_UPDATE_OBJECT */
|
||||||
|
|
@ -145,6 +149,7 @@ class WotlkPacketParsers : public PacketParsers {
|
||||||
*/
|
*/
|
||||||
class TbcPacketParsers : public PacketParsers {
|
class TbcPacketParsers : public PacketParsers {
|
||||||
public:
|
public:
|
||||||
|
uint8_t movementFlags2Size() const override { return 1; }
|
||||||
bool parseMovementBlock(network::Packet& packet, UpdateBlock& block) override;
|
bool parseMovementBlock(network::Packet& packet, UpdateBlock& block) override;
|
||||||
void writeMovementPayload(network::Packet& packet, const MovementInfo& info) override;
|
void writeMovementPayload(network::Packet& packet, const MovementInfo& info) override;
|
||||||
network::Packet buildMovementPacket(LogicalOpcode opcode,
|
network::Packet buildMovementPacket(LogicalOpcode opcode,
|
||||||
|
|
@ -171,6 +176,7 @@ public:
|
||||||
*/
|
*/
|
||||||
class ClassicPacketParsers : public TbcPacketParsers {
|
class ClassicPacketParsers : public TbcPacketParsers {
|
||||||
public:
|
public:
|
||||||
|
uint8_t movementFlags2Size() const override { return 0; }
|
||||||
bool parseCharEnum(network::Packet& packet, CharEnumResponse& response) override;
|
bool parseCharEnum(network::Packet& packet, CharEnumResponse& response) override;
|
||||||
bool parseMovementBlock(network::Packet& packet, UpdateBlock& block) override;
|
bool parseMovementBlock(network::Packet& packet, UpdateBlock& block) override;
|
||||||
void writeMovementPayload(network::Packet& packet, const MovementInfo& info) override;
|
void writeMovementPayload(network::Packet& packet, const MovementInfo& info) override;
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ enum class UF : uint16_t {
|
||||||
// Unit fields
|
// Unit fields
|
||||||
UNIT_FIELD_TARGET_LO,
|
UNIT_FIELD_TARGET_LO,
|
||||||
UNIT_FIELD_TARGET_HI,
|
UNIT_FIELD_TARGET_HI,
|
||||||
|
UNIT_FIELD_BYTES_0,
|
||||||
UNIT_FIELD_HEALTH,
|
UNIT_FIELD_HEALTH,
|
||||||
UNIT_FIELD_POWER1,
|
UNIT_FIELD_POWER1,
|
||||||
UNIT_FIELD_MAXHEALTH,
|
UNIT_FIELD_MAXHEALTH,
|
||||||
|
|
@ -34,6 +35,8 @@ enum class UF : uint16_t {
|
||||||
|
|
||||||
// Player fields
|
// Player fields
|
||||||
PLAYER_FLAGS,
|
PLAYER_FLAGS,
|
||||||
|
PLAYER_BYTES,
|
||||||
|
PLAYER_BYTES_2,
|
||||||
PLAYER_XP,
|
PLAYER_XP,
|
||||||
PLAYER_NEXT_LEVEL_XP,
|
PLAYER_NEXT_LEVEL_XP,
|
||||||
PLAYER_FIELD_COINAGE,
|
PLAYER_FIELD_COINAGE,
|
||||||
|
|
|
||||||
|
|
@ -66,6 +66,8 @@ public:
|
||||||
const pipeline::M2Model* getModelData(uint32_t modelId) const;
|
const pipeline::M2Model* getModelData(uint32_t modelId) const;
|
||||||
void setActiveGeosets(uint32_t instanceId, const std::unordered_set<uint16_t>& geosets);
|
void setActiveGeosets(uint32_t instanceId, const std::unordered_set<uint16_t>& geosets);
|
||||||
void setGroupTextureOverride(uint32_t instanceId, uint16_t geosetGroup, GLuint textureId);
|
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 setInstanceVisible(uint32_t instanceId, bool visible);
|
||||||
void removeInstance(uint32_t instanceId);
|
void removeInstance(uint32_t instanceId);
|
||||||
bool getAnimationState(uint32_t instanceId, uint32_t& animationId, float& animationTimeMs, float& animationDurationMs) const;
|
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)
|
// Per-geoset-group texture overrides (group → GL texture ID)
|
||||||
std::unordered_map<uint16_t, GLuint> groupTextureOverrides;
|
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)
|
// Weapon attachments (weapons parented to this instance's bones)
|
||||||
std::vector<WeaponAttachment> weaponAttachments;
|
std::vector<WeaponAttachment> weaponAttachments;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -590,6 +590,7 @@ void Application::update(float deltaTime) {
|
||||||
worldTime += std::chrono::duration<float, std::milli>(w2 - w1).count();
|
worldTime += std::chrono::duration<float, std::milli>(w2 - w1).count();
|
||||||
|
|
||||||
auto cq1 = std::chrono::high_resolution_clock::now();
|
auto cq1 = std::chrono::high_resolution_clock::now();
|
||||||
|
processPlayerSpawnQueue();
|
||||||
// Process deferred online creature spawns (throttled)
|
// Process deferred online creature spawns (throttled)
|
||||||
processCreatureSpawnQueue();
|
processCreatureSpawnQueue();
|
||||||
auto cq2 = std::chrono::high_resolution_clock::now();
|
auto cq2 = std::chrono::high_resolution_clock::now();
|
||||||
|
|
@ -1199,11 +1200,29 @@ void Application::setupUICallbacks() {
|
||||||
pendingCreatureSpawnGuids_.insert(guid);
|
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
|
// Creature despawn callback (online mode) - remove creature models
|
||||||
gameHandler->setCreatureDespawnCallback([this](uint64_t guid) {
|
gameHandler->setCreatureDespawnCallback([this](uint64_t guid) {
|
||||||
despawnOnlineCreature(guid);
|
despawnOnlineCreature(guid);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
gameHandler->setPlayerDespawnCallback([this](uint64_t guid) {
|
||||||
|
despawnOnlinePlayer(guid);
|
||||||
|
});
|
||||||
|
|
||||||
// GameObject spawn callback (online mode) - spawn static models (mailboxes, etc.)
|
// 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) {
|
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});
|
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
|
// Creature move callback (online mode) - update creature positions
|
||||||
gameHandler->setCreatureMoveCallback([this](uint64_t guid, float x, float y, float z, uint32_t durationMs) {
|
gameHandler->setCreatureMoveCallback([this](uint64_t guid, float x, float y, float z, uint32_t durationMs) {
|
||||||
|
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);
|
auto it = creatureInstances_.find(guid);
|
||||||
if (it != creatureInstances_.end() && renderer && renderer->getCharacterRenderer()) {
|
if (it != creatureInstances_.end()) instanceId = it->second;
|
||||||
|
}
|
||||||
|
if (instanceId != 0) {
|
||||||
glm::vec3 renderPos = core::coords::canonicalToRender(glm::vec3(x, y, z));
|
glm::vec3 renderPos = core::coords::canonicalToRender(glm::vec3(x, y, z));
|
||||||
float durationSec = static_cast<float>(durationMs) / 1000.0f;
|
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()) {
|
if (gameHandler && guid == gameHandler->getPlayerGuid()) {
|
||||||
instanceId = renderer->getCharacterInstanceId();
|
instanceId = renderer->getCharacterInstanceId();
|
||||||
}
|
}
|
||||||
|
if (instanceId == 0) {
|
||||||
|
auto pit = playerInstances_.find(guid);
|
||||||
|
if (pit != playerInstances_.end()) instanceId = pit->second;
|
||||||
|
}
|
||||||
if (instanceId == 0) {
|
if (instanceId == 0) {
|
||||||
auto it = creatureInstances_.find(guid);
|
auto it = creatureInstances_.find(guid);
|
||||||
if (it != creatureInstances_.end()) instanceId = it->second;
|
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, ")");
|
" 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) {
|
void Application::spawnOnlineGameObject(uint64_t guid, uint32_t entry, uint32_t displayId, float x, float y, float z, float orientation) {
|
||||||
if (!renderer || !assetManager) return;
|
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() {
|
void Application::processGameObjectSpawnQueue() {
|
||||||
if (pendingGameObjectSpawns_.empty()) return;
|
if (pendingGameObjectSpawns_.empty()) return;
|
||||||
|
|
||||||
|
|
@ -4083,6 +4368,13 @@ void Application::processPendingMount() {
|
||||||
}
|
}
|
||||||
|
|
||||||
void Application::despawnOnlineCreature(uint64_t guid) {
|
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);
|
pendingCreatureSpawnGuids_.erase(guid);
|
||||||
creatureSpawnRetryCounts_.erase(guid);
|
creatureSpawnRetryCounts_.erase(guid);
|
||||||
creaturePermanentFailureGuids_.erase(guid);
|
creaturePermanentFailureGuids_.erase(guid);
|
||||||
|
|
|
||||||
|
|
@ -1558,14 +1558,10 @@ void GameHandler::handleCharEnum(network::Packet& packet) {
|
||||||
LOG_INFO("Handling SMSG_CHAR_ENUM");
|
LOG_INFO("Handling SMSG_CHAR_ENUM");
|
||||||
|
|
||||||
CharEnumResponse response;
|
CharEnumResponse response;
|
||||||
bool parsed = false;
|
// IMPORTANT: Do not infer packet formats from numeric build alone.
|
||||||
if (build <= 6005) {
|
// Turtle WoW uses a "high" build but classic-era world packet formats.
|
||||||
// Vanilla 1.12.x format (different equipment layout, no customization flag)
|
bool parsed = packetParsers_ ? packetParsers_->parseCharEnum(packet, response)
|
||||||
ClassicPacketParsers classicParser;
|
: CharEnumParser::parse(packet, response);
|
||||||
parsed = classicParser.parseCharEnum(packet, response);
|
|
||||||
} else {
|
|
||||||
parsed = CharEnumParser::parse(packet, response);
|
|
||||||
}
|
|
||||||
if (!parsed) {
|
if (!parsed) {
|
||||||
fail("Failed to parse SMSG_CHAR_ENUM");
|
fail("Failed to parse SMSG_CHAR_ENUM");
|
||||||
return;
|
return;
|
||||||
|
|
@ -2688,6 +2684,87 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
|
||||||
return;
|
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
|
// Process out-of-range objects first
|
||||||
for (uint64_t guid : data.outOfRangeGuids) {
|
for (uint64_t guid : data.outOfRangeGuids) {
|
||||||
if (entityManager.hasEntity(guid)) {
|
if (entityManager.hasEntity(guid)) {
|
||||||
|
|
@ -2713,6 +2790,8 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
|
||||||
if (entity) {
|
if (entity) {
|
||||||
if (entity->getType() == ObjectType::UNIT && creatureDespawnCallback_) {
|
if (entity->getType() == ObjectType::UNIT && creatureDespawnCallback_) {
|
||||||
creatureDespawnCallback_(guid);
|
creatureDespawnCallback_(guid);
|
||||||
|
} else if (entity->getType() == ObjectType::PLAYER && playerDespawnCallback_) {
|
||||||
|
playerDespawnCallback_(guid);
|
||||||
} else if (entity->getType() == ObjectType::GAMEOBJECT && gameObjectDespawnCallback_) {
|
} else if (entity->getType() == ObjectType::GAMEOBJECT && gameObjectDespawnCallback_) {
|
||||||
gameObjectDespawnCallback_(guid);
|
gameObjectDespawnCallback_(guid);
|
||||||
}
|
}
|
||||||
|
|
@ -2905,7 +2984,18 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
|
||||||
}
|
}
|
||||||
// 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 ((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(),
|
creatureSpawnCallback_(block.guid, unit->getDisplayId(),
|
||||||
unit->getX(), unit->getY(), unit->getZ(), unit->getOrientation());
|
unit->getX(), unit->getY(), unit->getZ(), unit->getOrientation());
|
||||||
}
|
}
|
||||||
|
|
@ -3238,7 +3328,18 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
|
||||||
displayIdChanged &&
|
displayIdChanged &&
|
||||||
unit->getDisplayId() != 0 &&
|
unit->getDisplayId() != 0 &&
|
||||||
unit->getDisplayId() != oldDisplayId) {
|
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(),
|
creatureSpawnCallback_(block.guid, unit->getDisplayId(),
|
||||||
unit->getX(), unit->getY(), unit->getZ(), unit->getOrientation());
|
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]
|
// For classic: moveFlags(u32) + time(u32) + pos(4xf32) + [transport] + [pitch] + fallTime(u32) + [jump] + [splineElev]
|
||||||
MovementInfo info = {};
|
MovementInfo info = {};
|
||||||
info.flags = packet.readUInt32();
|
info.flags = packet.readUInt32();
|
||||||
// WotLK has uint16 flags2, classic/TBC don't
|
// WotLK has u16 flags2, TBC has u8, Classic has none.
|
||||||
if (build >= 8606) { // TBC+
|
// Do NOT use build-number thresholds here (Turtle uses classic formats with a high build).
|
||||||
if (build >= 12340) {
|
uint8_t flags2Size = packetParsers_ ? packetParsers_->movementFlags2Size() : 2;
|
||||||
info.flags2 = packet.readUInt16();
|
if (flags2Size == 2) info.flags2 = packet.readUInt16();
|
||||||
} else {
|
else if (flags2Size == 1) info.flags2 = packet.readUInt8();
|
||||||
info.flags2 = packet.readUInt8();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
info.time = packet.readUInt32();
|
info.time = packet.readUInt32();
|
||||||
info.x = packet.readFloat();
|
info.x = packet.readFloat();
|
||||||
info.y = packet.readFloat();
|
info.y = packet.readFloat();
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ static const UFNameEntry kUFNames[] = {
|
||||||
{"OBJECT_FIELD_ENTRY", UF::OBJECT_FIELD_ENTRY},
|
{"OBJECT_FIELD_ENTRY", UF::OBJECT_FIELD_ENTRY},
|
||||||
{"UNIT_FIELD_TARGET_LO", UF::UNIT_FIELD_TARGET_LO},
|
{"UNIT_FIELD_TARGET_LO", UF::UNIT_FIELD_TARGET_LO},
|
||||||
{"UNIT_FIELD_TARGET_HI", UF::UNIT_FIELD_TARGET_HI},
|
{"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_HEALTH", UF::UNIT_FIELD_HEALTH},
|
||||||
{"UNIT_FIELD_POWER1", UF::UNIT_FIELD_POWER1},
|
{"UNIT_FIELD_POWER1", UF::UNIT_FIELD_POWER1},
|
||||||
{"UNIT_FIELD_MAXHEALTH", UF::UNIT_FIELD_MAXHEALTH},
|
{"UNIT_FIELD_MAXHEALTH", UF::UNIT_FIELD_MAXHEALTH},
|
||||||
|
|
@ -35,6 +36,8 @@ static const UFNameEntry kUFNames[] = {
|
||||||
{"UNIT_DYNAMIC_FLAGS", UF::UNIT_DYNAMIC_FLAGS},
|
{"UNIT_DYNAMIC_FLAGS", UF::UNIT_DYNAMIC_FLAGS},
|
||||||
{"UNIT_END", UF::UNIT_END},
|
{"UNIT_END", UF::UNIT_END},
|
||||||
{"PLAYER_FLAGS", UF::PLAYER_FLAGS},
|
{"PLAYER_FLAGS", UF::PLAYER_FLAGS},
|
||||||
|
{"PLAYER_BYTES", UF::PLAYER_BYTES},
|
||||||
|
{"PLAYER_BYTES_2", UF::PLAYER_BYTES_2},
|
||||||
{"PLAYER_XP", UF::PLAYER_XP},
|
{"PLAYER_XP", UF::PLAYER_XP},
|
||||||
{"PLAYER_NEXT_LEVEL_XP", UF::PLAYER_NEXT_LEVEL_XP},
|
{"PLAYER_NEXT_LEVEL_XP", UF::PLAYER_NEXT_LEVEL_XP},
|
||||||
{"PLAYER_FIELD_COINAGE", UF::PLAYER_FIELD_COINAGE},
|
{"PLAYER_FIELD_COINAGE", UF::PLAYER_FIELD_COINAGE},
|
||||||
|
|
@ -55,6 +58,7 @@ void UpdateFieldTable::loadWotlkDefaults() {
|
||||||
{UF::OBJECT_FIELD_ENTRY, 3},
|
{UF::OBJECT_FIELD_ENTRY, 3},
|
||||||
{UF::UNIT_FIELD_TARGET_LO, 6},
|
{UF::UNIT_FIELD_TARGET_LO, 6},
|
||||||
{UF::UNIT_FIELD_TARGET_HI, 7},
|
{UF::UNIT_FIELD_TARGET_HI, 7},
|
||||||
|
{UF::UNIT_FIELD_BYTES_0, 56},
|
||||||
{UF::UNIT_FIELD_HEALTH, 24},
|
{UF::UNIT_FIELD_HEALTH, 24},
|
||||||
{UF::UNIT_FIELD_POWER1, 25},
|
{UF::UNIT_FIELD_POWER1, 25},
|
||||||
{UF::UNIT_FIELD_MAXHEALTH, 32},
|
{UF::UNIT_FIELD_MAXHEALTH, 32},
|
||||||
|
|
@ -69,6 +73,8 @@ void UpdateFieldTable::loadWotlkDefaults() {
|
||||||
{UF::UNIT_DYNAMIC_FLAGS, 147},
|
{UF::UNIT_DYNAMIC_FLAGS, 147},
|
||||||
{UF::UNIT_END, 148},
|
{UF::UNIT_END, 148},
|
||||||
{UF::PLAYER_FLAGS, 150},
|
{UF::PLAYER_FLAGS, 150},
|
||||||
|
{UF::PLAYER_BYTES, 151},
|
||||||
|
{UF::PLAYER_BYTES_2, 152},
|
||||||
{UF::PLAYER_XP, 634},
|
{UF::PLAYER_XP, 634},
|
||||||
{UF::PLAYER_NEXT_LEVEL_XP, 635},
|
{UF::PLAYER_NEXT_LEVEL_XP, 635},
|
||||||
{UF::PLAYER_FIELD_COINAGE, 1170},
|
{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.
|
// 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.
|
// 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;
|
if (texSlot >= gm.textureIds.size()) continue;
|
||||||
|
|
||||||
GLuint texId = gm.textureIds[texSlot];
|
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;
|
uint32_t texType = (texSlot < gm.data.textures.size()) ? gm.data.textures[texSlot].type : 0;
|
||||||
|
|
||||||
if (!hasFirst) {
|
if (!hasFirst) {
|
||||||
|
|
@ -1353,7 +1357,7 @@ void CharacterRenderer::render(const Camera& camera, const glm::mat4& view, cons
|
||||||
(b.submeshId / 100 != 0) &&
|
(b.submeshId / 100 != 0) &&
|
||||||
instance.activeGeosets.find(b.submeshId) == instance.activeGeosets.end();
|
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);
|
std::string texInfo = "GL" + std::to_string(resolvedTex);
|
||||||
|
|
||||||
if (filtered) skipped++; else rendered++;
|
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).
|
// 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
|
// For body parts with white/fallback texture, use skin (type 1) texture
|
||||||
// This handles humanoid models where some body parts use different texture slots
|
// 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
|
// Do NOT apply skin composite to hair (type 6) batches
|
||||||
if (texType != 6) {
|
if (texType != 6) {
|
||||||
for (size_t ti = 0; ti < gpuModel.textureIds.size(); ti++) {
|
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
|
// Only use type 1 (skin) textures as fallback
|
||||||
if (ti < gpuModel.data.textures.size() &&
|
if (ti < gpuModel.data.textures.size() &&
|
||||||
(gpuModel.data.textures[ti].type == 1 || gpuModel.data.textures[ti].type == 11)) {
|
(gpuModel.data.textures[ti].type == 1 || gpuModel.data.textures[ti].type == 11)) {
|
||||||
texId = gpuModel.textureIds[ti];
|
texId = candidate;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1489,6 +1498,10 @@ void CharacterRenderer::renderShadow(const glm::mat4& lightSpaceMatrix) {
|
||||||
uint16_t lookupIdx = gpuModel.data.textureLookup[batch.textureIndex];
|
uint16_t lookupIdx = gpuModel.data.textureLookup[batch.textureIndex];
|
||||||
if (lookupIdx < gpuModel.textureIds.size()) {
|
if (lookupIdx < gpuModel.textureIds.size()) {
|
||||||
texId = gpuModel.textureIds[lookupIdx];
|
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) {
|
void CharacterRenderer::setInstanceVisible(uint32_t instanceId, bool visible) {
|
||||||
auto it = instances.find(instanceId);
|
auto it = instances.find(instanceId);
|
||||||
if (it != instances.end()) {
|
if (it != instances.end()) {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue