mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-03-22 23:30:14 +00:00
Add transport support, gameobject queries, and fix item use
- Add setInstancePosition() to M2Renderer and WMORenderer for moving transport instances at runtime - Detect UPDATEFLAG_TRANSPORT on gameobjects and track transport GUIDs - Parse player-on-transport state from movement blocks - Wire transport move callback in Application to update render positions - Implement CMSG_GAMEOBJECT_QUERY / SMSG_GAMEOBJECT_QUERY_RESPONSE so gameobjects display proper names instead of "Unknown" - Add name/entry fields to GameObject entity class - Fix CMSG_USE_ITEM packet: remove extra uint8 that shifted the item GUID by one byte, breaking hearthstone and all item usage - Remove redundant CMSG_LOOT after CMSG_GAMEOBJECT_USE for chests - Show PvP enabled/disabled state in toggle message - Relax WMO ramp wall-collision step-up check to allow walking on gentle ramps where floor rise per step is under 0.1 units - Add M2 fallback when WMO group files fail to load for gameobjects - Handle re-creation of existing gameobject render instances by updating position instead of silently ignoring
This commit is contained in:
parent
5610faa958
commit
0ce38cfb99
12 changed files with 391 additions and 65 deletions
|
|
@ -256,10 +256,18 @@ public:
|
|||
GameObject() { type = ObjectType::GAMEOBJECT; }
|
||||
explicit GameObject(uint64_t guid) : Entity(guid) { type = ObjectType::GAMEOBJECT; }
|
||||
|
||||
const std::string& getName() const { return name; }
|
||||
void setName(const std::string& n) { name = n; }
|
||||
|
||||
uint32_t getEntry() const { return entry; }
|
||||
void setEntry(uint32_t e) { entry = e; }
|
||||
|
||||
uint32_t getDisplayId() const { return displayId; }
|
||||
void setDisplayId(uint32_t id) { displayId = id; }
|
||||
|
||||
protected:
|
||||
std::string name;
|
||||
uint32_t entry = 0;
|
||||
uint32_t displayId = 0;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
#include "game/inventory.hpp"
|
||||
#include "game/spell_defines.hpp"
|
||||
#include "game/group_defines.hpp"
|
||||
#include <glm/glm.hpp>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
|
@ -297,6 +298,7 @@ public:
|
|||
// ---- Phase 1: Name queries ----
|
||||
void queryPlayerName(uint64_t guid);
|
||||
void queryCreatureInfo(uint32_t entry, uint64_t guid);
|
||||
void queryGameObjectInfo(uint32_t entry, uint64_t guid);
|
||||
std::string getCachedPlayerName(uint64_t guid) const;
|
||||
std::string getCachedCreatureName(uint32_t entry) const;
|
||||
|
||||
|
|
@ -395,6 +397,16 @@ public:
|
|||
using CreatureMoveCallback = std::function<void(uint64_t guid, float x, float y, float z, uint32_t durationMs)>;
|
||||
void setCreatureMoveCallback(CreatureMoveCallback cb) { creatureMoveCallback_ = std::move(cb); }
|
||||
|
||||
// Transport move callback (online mode - triggered when transport position updates)
|
||||
// Parameters: guid, x, y, z (canonical), orientation
|
||||
using TransportMoveCallback = std::function<void(uint64_t guid, float x, float y, float z, float orientation)>;
|
||||
void setTransportMoveCallback(TransportMoveCallback cb) { transportMoveCallback_ = std::move(cb); }
|
||||
|
||||
// Transport state for player-on-transport
|
||||
bool isOnTransport() const { return playerTransportGuid_ != 0; }
|
||||
uint64_t getPlayerTransportGuid() const { return playerTransportGuid_; }
|
||||
glm::vec3 getPlayerTransportOffset() const { return playerTransportOffset_; }
|
||||
|
||||
// Cooldowns
|
||||
float getSpellCooldown(uint32_t spellId) const;
|
||||
|
||||
|
|
@ -600,6 +612,7 @@ private:
|
|||
// ---- Phase 1 handlers ----
|
||||
void handleNameQueryResponse(network::Packet& packet);
|
||||
void handleCreatureQueryResponse(network::Packet& packet);
|
||||
void handleGameObjectQueryResponse(network::Packet& packet);
|
||||
void handleItemQueryResponse(network::Packet& packet);
|
||||
void queryItemInfo(uint32_t entry, uint64_t guid);
|
||||
void rebuildOnlineInventory();
|
||||
|
|
@ -766,6 +779,8 @@ private:
|
|||
std::unordered_set<uint64_t> pendingNameQueries;
|
||||
std::unordered_map<uint32_t, CreatureQueryResponseData> creatureInfoCache;
|
||||
std::unordered_set<uint32_t> pendingCreatureQueries;
|
||||
std::unordered_map<uint32_t, GameObjectQueryResponseData> gameObjectInfoCache_;
|
||||
std::unordered_set<uint32_t> pendingGameObjectQueries_;
|
||||
|
||||
// ---- Friend list cache ----
|
||||
std::unordered_map<std::string, uint64_t> friendsCache; // name -> guid
|
||||
|
|
@ -818,8 +833,14 @@ private:
|
|||
CreatureSpawnCallback creatureSpawnCallback_;
|
||||
CreatureDespawnCallback creatureDespawnCallback_;
|
||||
CreatureMoveCallback creatureMoveCallback_;
|
||||
TransportMoveCallback transportMoveCallback_;
|
||||
GameObjectSpawnCallback gameObjectSpawnCallback_;
|
||||
GameObjectDespawnCallback gameObjectDespawnCallback_;
|
||||
|
||||
// Transport tracking
|
||||
std::unordered_set<uint64_t> transportGuids_; // GUIDs of known transport GameObjects
|
||||
uint64_t playerTransportGuid_ = 0; // Transport the player is riding (0 = none)
|
||||
glm::vec3 playerTransportOffset_ = glm::vec3(0.0f); // Player offset on transport
|
||||
std::vector<uint32_t> knownSpells;
|
||||
std::unordered_map<uint32_t, float> spellCooldowns; // spellId -> remaining seconds
|
||||
uint8_t castCount = 0;
|
||||
|
|
|
|||
|
|
@ -451,6 +451,14 @@ struct UpdateBlock {
|
|||
float x = 0.0f, y = 0.0f, z = 0.0f, orientation = 0.0f;
|
||||
float runSpeed = 0.0f;
|
||||
|
||||
// Update flags from movement block (for detecting transports, etc.)
|
||||
uint16_t updateFlags = 0;
|
||||
|
||||
// Transport data from LIVING movement block (MOVEMENTFLAG_ONTRANSPORT)
|
||||
bool onTransport = false;
|
||||
uint64_t transportGuid = 0;
|
||||
float transportX = 0.0f, transportY = 0.0f, transportZ = 0.0f, transportO = 0.0f;
|
||||
|
||||
// Field data (for VALUES and CREATE updates)
|
||||
std::map<uint16_t, uint32_t> fields;
|
||||
};
|
||||
|
|
@ -1050,6 +1058,31 @@ public:
|
|||
static bool parse(network::Packet& packet, CreatureQueryResponseData& data);
|
||||
};
|
||||
|
||||
// ============================================================
|
||||
// GameObject Query
|
||||
// ============================================================
|
||||
|
||||
/** CMSG_GAMEOBJECT_QUERY packet builder */
|
||||
class GameObjectQueryPacket {
|
||||
public:
|
||||
static network::Packet build(uint32_t entry, uint64_t guid);
|
||||
};
|
||||
|
||||
/** SMSG_GAMEOBJECT_QUERY_RESPONSE data */
|
||||
struct GameObjectQueryResponseData {
|
||||
uint32_t entry = 0;
|
||||
std::string name;
|
||||
uint32_t type = 0; // GameObjectType (e.g. 3=chest, 2=questgiver)
|
||||
|
||||
bool isValid() const { return entry != 0 && !name.empty(); }
|
||||
};
|
||||
|
||||
/** SMSG_GAMEOBJECT_QUERY_RESPONSE parser */
|
||||
class GameObjectQueryResponseParser {
|
||||
public:
|
||||
static bool parse(network::Packet& packet, GameObjectQueryResponseData& data);
|
||||
};
|
||||
|
||||
// ============================================================
|
||||
// Item Query
|
||||
// ============================================================
|
||||
|
|
|
|||
|
|
@ -208,6 +208,13 @@ public:
|
|||
*/
|
||||
void renderM2Particles(const glm::mat4& view, const glm::mat4& proj);
|
||||
|
||||
/**
|
||||
* Update the world position of an existing instance (e.g., for transports)
|
||||
* @param instanceId Instance ID returned by createInstance()
|
||||
* @param position New world position
|
||||
*/
|
||||
void setInstancePosition(uint32_t instanceId, const glm::vec3& position);
|
||||
|
||||
/**
|
||||
* Remove a specific instance by ID
|
||||
* @param instanceId Instance ID returned by createInstance()
|
||||
|
|
|
|||
|
|
@ -75,6 +75,13 @@ public:
|
|||
const glm::vec3& rotation = glm::vec3(0.0f),
|
||||
float scale = 1.0f);
|
||||
|
||||
/**
|
||||
* Update the world position of an existing instance (e.g., for transports)
|
||||
* @param instanceId Instance to update
|
||||
* @param position New world position
|
||||
*/
|
||||
void setInstancePosition(uint32_t instanceId, const glm::vec3& position);
|
||||
|
||||
/**
|
||||
* Remove WMO instance
|
||||
* @param instanceId Instance to remove
|
||||
|
|
|
|||
|
|
@ -606,6 +606,39 @@ void Application::setupUICallbacks() {
|
|||
}
|
||||
});
|
||||
|
||||
// Transport move callback (online mode) - update transport gameobject positions
|
||||
gameHandler->setTransportMoveCallback([this](uint64_t guid, float x, float y, float z, float /*orientation*/) {
|
||||
auto it = gameObjectInstances_.find(guid);
|
||||
if (it == gameObjectInstances_.end()) return;
|
||||
glm::vec3 renderPos = core::coords::canonicalToRender(glm::vec3(x, y, z));
|
||||
if (renderer) {
|
||||
if (it->second.isWmo) {
|
||||
if (auto* wmoRenderer = renderer->getWMORenderer()) {
|
||||
wmoRenderer->setInstancePosition(it->second.instanceId, renderPos);
|
||||
}
|
||||
} else {
|
||||
if (auto* m2Renderer = renderer->getM2Renderer()) {
|
||||
m2Renderer->setInstancePosition(it->second.instanceId, renderPos);
|
||||
}
|
||||
}
|
||||
|
||||
// Move player with transport if riding it
|
||||
if (gameHandler && gameHandler->isOnTransport() && gameHandler->getPlayerTransportGuid() == guid) {
|
||||
auto* cc = renderer->getCameraController();
|
||||
if (cc) {
|
||||
glm::vec3* ft = cc->getFollowTargetMutable();
|
||||
if (ft) {
|
||||
// Transport offset is in server/canonical coords — convert to render
|
||||
glm::vec3 offset = gameHandler->getPlayerTransportOffset();
|
||||
glm::vec3 canonicalPlayerPos = glm::vec3(x + offset.x, y + offset.y, z + offset.z);
|
||||
glm::vec3 playerRenderPos = core::coords::canonicalToRender(canonicalPlayerPos);
|
||||
*ft = playerRenderPos;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// NPC death callback (online mode) - play death animation
|
||||
gameHandler->setNpcDeathCallback([this](uint64_t guid) {
|
||||
auto it = creatureInstances_.find(guid);
|
||||
|
|
@ -2202,7 +2235,21 @@ void Application::spawnOnlineGameObject(uint64_t guid, uint32_t displayId, float
|
|||
}
|
||||
if (!gameObjectLookupsBuilt_) return;
|
||||
|
||||
if (gameObjectInstances_.count(guid)) return;
|
||||
if (gameObjectInstances_.count(guid)) {
|
||||
// Already have a render instance — update its position (e.g. transport re-creation)
|
||||
auto& info = gameObjectInstances_[guid];
|
||||
glm::vec3 renderPos = core::coords::canonicalToRender(glm::vec3(x, y, z));
|
||||
if (renderer) {
|
||||
if (info.isWmo) {
|
||||
if (auto* wr = renderer->getWMORenderer())
|
||||
wr->setInstancePosition(info.instanceId, renderPos);
|
||||
} else {
|
||||
if (auto* mr = renderer->getM2Renderer())
|
||||
mr->setInstancePosition(info.instanceId, renderPos);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
std::string modelPath = getGameObjectModelPathForDisplayId(displayId);
|
||||
if (modelPath.empty()) {
|
||||
|
|
@ -2218,6 +2265,7 @@ void Application::spawnOnlineGameObject(uint64_t guid, uint32_t displayId, float
|
|||
glm::vec3 renderPos = core::coords::canonicalToRender(glm::vec3(x, y, z));
|
||||
float renderYaw = orientation + glm::radians(90.0f);
|
||||
|
||||
bool loadedAsWmo = false;
|
||||
if (isWmo) {
|
||||
auto* wmoRenderer = renderer->getWMORenderer();
|
||||
if (!wmoRenderer) return;
|
||||
|
|
@ -2226,62 +2274,84 @@ void Application::spawnOnlineGameObject(uint64_t guid, uint32_t displayId, float
|
|||
auto itCache = gameObjectDisplayIdWmoCache_.find(displayId);
|
||||
if (itCache != gameObjectDisplayIdWmoCache_.end()) {
|
||||
modelId = itCache->second;
|
||||
loadedAsWmo = true;
|
||||
} else {
|
||||
modelId = nextGameObjectWmoModelId_++;
|
||||
auto wmoData = assetManager->readFile(modelPath);
|
||||
if (wmoData.empty()) {
|
||||
LOG_WARNING("Failed to read gameobject WMO: ", modelPath);
|
||||
return;
|
||||
}
|
||||
if (!wmoData.empty()) {
|
||||
pipeline::WMOModel wmoModel = pipeline::WMOLoader::load(wmoData);
|
||||
LOG_INFO("Gameobject WMO root loaded: ", modelPath, " nGroups=", wmoModel.nGroups);
|
||||
int loadedGroups = 0;
|
||||
if (wmoModel.nGroups > 0) {
|
||||
std::string basePath = modelPath;
|
||||
std::string extension;
|
||||
if (basePath.size() > 4) {
|
||||
extension = basePath.substr(basePath.size() - 4);
|
||||
std::string extLower = extension;
|
||||
for (char& c : extLower) c = static_cast<char>(std::tolower(c));
|
||||
if (extLower == ".wmo") {
|
||||
basePath = basePath.substr(0, basePath.size() - 4);
|
||||
}
|
||||
}
|
||||
|
||||
pipeline::WMOModel wmoModel = pipeline::WMOLoader::load(wmoData);
|
||||
if (wmoModel.nGroups > 0) {
|
||||
std::string basePath = modelPath;
|
||||
std::string extension;
|
||||
if (basePath.size() > 4) {
|
||||
extension = basePath.substr(basePath.size() - 4);
|
||||
std::string extLower = extension;
|
||||
for (char& c : extLower) c = static_cast<char>(std::tolower(c));
|
||||
if (extLower == ".wmo") {
|
||||
basePath = basePath.substr(0, basePath.size() - 4);
|
||||
for (uint32_t gi = 0; gi < wmoModel.nGroups; gi++) {
|
||||
char groupSuffix[16];
|
||||
snprintf(groupSuffix, sizeof(groupSuffix), "_%03u%s", gi, extension.c_str());
|
||||
std::string groupPath = basePath + groupSuffix;
|
||||
std::vector<uint8_t> groupData = assetManager->readFile(groupPath);
|
||||
if (groupData.empty()) {
|
||||
snprintf(groupSuffix, sizeof(groupSuffix), "_%03u.wmo", gi);
|
||||
groupData = assetManager->readFile(basePath + groupSuffix);
|
||||
}
|
||||
if (groupData.empty()) {
|
||||
snprintf(groupSuffix, sizeof(groupSuffix), "_%03u.WMO", gi);
|
||||
groupData = assetManager->readFile(basePath + groupSuffix);
|
||||
}
|
||||
if (!groupData.empty()) {
|
||||
pipeline::WMOLoader::loadGroup(groupData, wmoModel, gi);
|
||||
loadedGroups++;
|
||||
} else {
|
||||
LOG_WARNING(" Failed to load WMO group ", gi, " for: ", basePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (uint32_t gi = 0; gi < wmoModel.nGroups; gi++) {
|
||||
char groupSuffix[16];
|
||||
snprintf(groupSuffix, sizeof(groupSuffix), "_%03u%s", gi, extension.c_str());
|
||||
std::string groupPath = basePath + groupSuffix;
|
||||
std::vector<uint8_t> groupData = assetManager->readFile(groupPath);
|
||||
if (groupData.empty()) {
|
||||
snprintf(groupSuffix, sizeof(groupSuffix), "_%03u.wmo", gi);
|
||||
groupData = assetManager->readFile(basePath + groupSuffix);
|
||||
}
|
||||
if (groupData.empty()) {
|
||||
snprintf(groupSuffix, sizeof(groupSuffix), "_%03u.WMO", gi);
|
||||
groupData = assetManager->readFile(basePath + groupSuffix);
|
||||
}
|
||||
if (!groupData.empty()) {
|
||||
pipeline::WMOLoader::loadGroup(groupData, wmoModel, gi);
|
||||
if (loadedGroups > 0 || wmoModel.nGroups == 0) {
|
||||
modelId = nextGameObjectWmoModelId_++;
|
||||
if (wmoRenderer->loadModel(wmoModel, modelId)) {
|
||||
gameObjectDisplayIdWmoCache_[displayId] = modelId;
|
||||
loadedAsWmo = true;
|
||||
} else {
|
||||
LOG_WARNING("Failed to load gameobject WMO model: ", modelPath);
|
||||
}
|
||||
} else {
|
||||
LOG_WARNING("No WMO groups loaded for gameobject: ", modelPath,
|
||||
" — falling back to M2");
|
||||
}
|
||||
} else {
|
||||
LOG_WARNING("Failed to read gameobject WMO: ", modelPath, " — falling back to M2");
|
||||
}
|
||||
|
||||
if (!wmoRenderer->loadModel(wmoModel, modelId)) {
|
||||
LOG_WARNING("Failed to load gameobject WMO: ", modelPath);
|
||||
return;
|
||||
}
|
||||
gameObjectDisplayIdWmoCache_[displayId] = modelId;
|
||||
}
|
||||
|
||||
uint32_t instanceId = wmoRenderer->createInstance(modelId, renderPos,
|
||||
glm::vec3(0.0f, 0.0f, renderYaw), 1.0f);
|
||||
if (instanceId == 0) {
|
||||
LOG_WARNING("Failed to create gameobject WMO instance for guid 0x", std::hex, guid, std::dec);
|
||||
if (loadedAsWmo) {
|
||||
uint32_t instanceId = wmoRenderer->createInstance(modelId, renderPos,
|
||||
glm::vec3(0.0f, 0.0f, renderYaw), 1.0f);
|
||||
if (instanceId == 0) {
|
||||
LOG_WARNING("Failed to create gameobject WMO instance for guid 0x", std::hex, guid, std::dec);
|
||||
return;
|
||||
}
|
||||
|
||||
gameObjectInstances_[guid] = {modelId, instanceId, true};
|
||||
LOG_INFO("Spawned gameobject WMO: guid=0x", std::hex, guid, std::dec,
|
||||
" displayId=", displayId, " at (", x, ", ", y, ", ", z, ")");
|
||||
return;
|
||||
}
|
||||
|
||||
gameObjectInstances_[guid] = {modelId, instanceId, true};
|
||||
} else {
|
||||
// WMO failed — fall through to try as M2
|
||||
// Convert .wmo path to .m2 for fallback
|
||||
modelPath = modelPath.substr(0, modelPath.size() - 4) + ".m2";
|
||||
}
|
||||
|
||||
{
|
||||
auto* m2Renderer = renderer->getM2Renderer();
|
||||
if (!m2Renderer) return;
|
||||
|
||||
|
|
|
|||
|
|
@ -662,9 +662,11 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
break;
|
||||
}
|
||||
case Opcode::SMSG_BUY_FAILED:
|
||||
case Opcode::SMSG_GAMEOBJECT_QUERY_RESPONSE:
|
||||
case Opcode::MSG_RAID_TARGET_UPDATE:
|
||||
break;
|
||||
case Opcode::SMSG_GAMEOBJECT_QUERY_RESPONSE:
|
||||
handleGameObjectQueryResponse(packet);
|
||||
break;
|
||||
case Opcode::SMSG_QUESTGIVER_STATUS: {
|
||||
// uint64 npcGuid + uint8 status
|
||||
if (packet.getSize() - packet.getReadPos() >= 9) {
|
||||
|
|
@ -731,9 +733,18 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
case Opcode::MSG_MOVE_TELEPORT_ACK:
|
||||
handleTeleportAck(packet);
|
||||
break;
|
||||
case Opcode::SMSG_TRANSFER_PENDING:
|
||||
LOG_INFO("SMSG_TRANSFER_PENDING received - map transfer incoming");
|
||||
case Opcode::SMSG_TRANSFER_PENDING: {
|
||||
// SMSG_TRANSFER_PENDING: uint32 mapId, then optional transport data
|
||||
uint32_t pendingMapId = packet.readUInt32();
|
||||
LOG_INFO("SMSG_TRANSFER_PENDING: mapId=", pendingMapId);
|
||||
// Optional: if remaining data, there's a transport entry + mapId
|
||||
if (packet.getReadPos() + 8 <= packet.getSize()) {
|
||||
uint32_t transportEntry = packet.readUInt32();
|
||||
uint32_t transportMapId = packet.readUInt32();
|
||||
LOG_INFO(" Transport entry=", transportEntry, " transportMapId=", transportMapId);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// ---- Taxi / Flight Paths ----
|
||||
case Opcode::SMSG_SHOWTAXINODES:
|
||||
|
|
@ -1356,6 +1367,11 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
|
|||
gameObjectDespawnCallback_(guid);
|
||||
}
|
||||
}
|
||||
transportGuids_.erase(guid);
|
||||
if (playerTransportGuid_ == guid) {
|
||||
playerTransportGuid_ = 0;
|
||||
playerTransportOffset_ = glm::vec3(0.0f);
|
||||
}
|
||||
entityManager.removeEntity(guid);
|
||||
}
|
||||
}
|
||||
|
|
@ -1400,6 +1416,21 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
|
|||
if (block.guid == playerGuid && block.runSpeed > 0.1f && block.runSpeed < 100.0f) {
|
||||
serverRunSpeed_ = block.runSpeed;
|
||||
}
|
||||
// Track player-on-transport state
|
||||
if (block.guid == playerGuid) {
|
||||
if (block.onTransport) {
|
||||
playerTransportGuid_ = block.transportGuid;
|
||||
playerTransportOffset_ = glm::vec3(block.transportX, block.transportY, block.transportZ);
|
||||
LOG_INFO("Player on transport: 0x", std::hex, playerTransportGuid_, std::dec,
|
||||
" offset=(", playerTransportOffset_.x, ", ", playerTransportOffset_.y, ", ", playerTransportOffset_.z, ")");
|
||||
} else {
|
||||
if (playerTransportGuid_ != 0) {
|
||||
LOG_INFO("Player left transport");
|
||||
}
|
||||
playerTransportGuid_ = 0;
|
||||
playerTransportOffset_ = glm::vec3(0.0f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Set fields
|
||||
|
|
@ -1501,17 +1532,39 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
|
|||
}
|
||||
}
|
||||
}
|
||||
// Extract displayId for gameobjects (3.3.5a: GAMEOBJECT_DISPLAYID = field 8)
|
||||
// Extract displayId and entry for gameobjects (3.3.5a: GAMEOBJECT_DISPLAYID = field 8)
|
||||
if (block.objectType == ObjectType::GAMEOBJECT) {
|
||||
auto go = std::static_pointer_cast<GameObject>(entity);
|
||||
auto itDisp = block.fields.find(8);
|
||||
if (itDisp != block.fields.end()) {
|
||||
go->setDisplayId(itDisp->second);
|
||||
}
|
||||
// Extract entry and query name (OBJECT_FIELD_ENTRY = index 3)
|
||||
auto itEntry = block.fields.find(3);
|
||||
if (itEntry != block.fields.end() && itEntry->second != 0) {
|
||||
go->setEntry(itEntry->second);
|
||||
auto cacheIt = gameObjectInfoCache_.find(itEntry->second);
|
||||
if (cacheIt != gameObjectInfoCache_.end()) {
|
||||
go->setName(cacheIt->second.name);
|
||||
}
|
||||
queryGameObjectInfo(itEntry->second, block.guid);
|
||||
}
|
||||
// Detect transport GameObjects via UPDATEFLAG_TRANSPORT (0x0002)
|
||||
if (block.updateFlags & 0x0002) {
|
||||
transportGuids_.insert(block.guid);
|
||||
LOG_INFO("Detected transport GameObject: 0x", std::hex, block.guid, std::dec,
|
||||
" displayId=", go->getDisplayId(),
|
||||
" pos=(", go->getX(), ", ", go->getY(), ", ", go->getZ(), ")");
|
||||
}
|
||||
if (go->getDisplayId() != 0 && gameObjectSpawnCallback_) {
|
||||
gameObjectSpawnCallback_(block.guid, go->getDisplayId(),
|
||||
go->getX(), go->getY(), go->getZ(), go->getOrientation());
|
||||
}
|
||||
// Fire transport move callback for transports (position update on re-creation)
|
||||
if (transportGuids_.count(block.guid) && transportMoveCallback_) {
|
||||
transportMoveCallback_(block.guid,
|
||||
go->getX(), go->getY(), go->getZ(), go->getOrientation());
|
||||
}
|
||||
}
|
||||
// Track online item objects
|
||||
if (block.objectType == ObjectType::ITEM) {
|
||||
|
|
@ -1724,6 +1777,11 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
|
|||
glm::vec3 pos = core::coords::serverToCanonical(glm::vec3(block.x, block.y, block.z));
|
||||
entity->setPosition(pos.x, pos.y, pos.z, block.orientation);
|
||||
LOG_DEBUG("Updated entity position: 0x", std::hex, block.guid, std::dec);
|
||||
|
||||
// Fire transport move callback if this is a known transport
|
||||
if (transportGuids_.count(block.guid) && transportMoveCallback_) {
|
||||
transportMoveCallback_(block.guid, pos.x, pos.y, pos.z, block.orientation);
|
||||
}
|
||||
} else {
|
||||
LOG_WARNING("MOVEMENT update for unknown entity: 0x", std::hex, block.guid, std::dec);
|
||||
}
|
||||
|
|
@ -2454,7 +2512,19 @@ void GameHandler::togglePvp() {
|
|||
|
||||
auto packet = TogglePvpPacket::build();
|
||||
socket->send(packet);
|
||||
addSystemChatMessage("PvP flag toggled.");
|
||||
// Check current PVP state from player's UNIT_FIELD_FLAGS (index 59)
|
||||
// UNIT_FLAG_PVP = 0x00001000
|
||||
auto entity = entityManager.getEntity(playerGuid);
|
||||
bool currentlyPvp = false;
|
||||
if (entity) {
|
||||
currentlyPvp = (entity->getField(59) & 0x00001000) != 0;
|
||||
}
|
||||
// We're toggling, so report the NEW state
|
||||
if (currentlyPvp) {
|
||||
addSystemChatMessage("PvP flag disabled.");
|
||||
} else {
|
||||
addSystemChatMessage("PvP flag enabled.");
|
||||
}
|
||||
LOG_INFO("Toggled PvP flag");
|
||||
}
|
||||
|
||||
|
|
@ -2939,6 +3009,15 @@ void GameHandler::queryCreatureInfo(uint32_t entry, uint64_t guid) {
|
|||
socket->send(packet);
|
||||
}
|
||||
|
||||
void GameHandler::queryGameObjectInfo(uint32_t entry, uint64_t guid) {
|
||||
if (gameObjectInfoCache_.count(entry) || pendingGameObjectQueries_.count(entry)) return;
|
||||
if (state != WorldState::IN_WORLD || !socket) return;
|
||||
|
||||
pendingGameObjectQueries_.insert(entry);
|
||||
auto packet = GameObjectQueryPacket::build(entry, guid);
|
||||
socket->send(packet);
|
||||
}
|
||||
|
||||
std::string GameHandler::getCachedPlayerName(uint64_t guid) const {
|
||||
auto it = playerNameCache.find(guid);
|
||||
return (it != playerNameCache.end()) ? it->second : "";
|
||||
|
|
@ -2986,6 +3065,30 @@ void GameHandler::handleCreatureQueryResponse(network::Packet& packet) {
|
|||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// GameObject Query
|
||||
// ============================================================
|
||||
|
||||
void GameHandler::handleGameObjectQueryResponse(network::Packet& packet) {
|
||||
GameObjectQueryResponseData data;
|
||||
if (!GameObjectQueryResponseParser::parse(packet, data)) return;
|
||||
|
||||
pendingGameObjectQueries_.erase(data.entry);
|
||||
|
||||
if (data.isValid()) {
|
||||
gameObjectInfoCache_[data.entry] = data;
|
||||
// Update all gameobject entities with this entry
|
||||
for (auto& [guid, entity] : entityManager.getEntities()) {
|
||||
if (entity->getType() == ObjectType::GAMEOBJECT) {
|
||||
auto go = std::static_pointer_cast<GameObject>(entity);
|
||||
if (go->getEntry() == data.entry) {
|
||||
go->setName(data.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Item Query
|
||||
// ============================================================
|
||||
|
|
@ -3959,9 +4062,6 @@ void GameHandler::interactWithGameObject(uint64_t guid) {
|
|||
if (state != WorldState::IN_WORLD || !socket) return;
|
||||
auto packet = GameObjectUsePacket::build(guid);
|
||||
socket->send(packet);
|
||||
// Many lootable chests require a loot request after use.
|
||||
auto loot = LootPacket::build(guid);
|
||||
socket->send(loot);
|
||||
}
|
||||
|
||||
void GameHandler::selectGossipOption(uint32_t optionId) {
|
||||
|
|
|
|||
|
|
@ -624,6 +624,7 @@ bool UpdateObjectParser::parseMovementBlock(network::Packet& packet, UpdateBlock
|
|||
|
||||
// Update flags (3.3.5a uses 2 bytes for flags)
|
||||
uint16_t updateFlags = packet.readUInt16();
|
||||
block.updateFlags = updateFlags;
|
||||
|
||||
LOG_DEBUG(" UpdateFlags: 0x", std::hex, updateFlags, std::dec);
|
||||
|
||||
|
|
@ -667,14 +668,18 @@ bool UpdateObjectParser::parseMovementBlock(network::Packet& packet, UpdateBlock
|
|||
|
||||
// Transport data (if on transport)
|
||||
if (moveFlags & 0x00000200) { // MOVEMENTFLAG_ONTRANSPORT
|
||||
/*uint64_t transportGuid =*/ readPackedGuid(packet);
|
||||
/*float tX =*/ packet.readFloat();
|
||||
/*float tY =*/ packet.readFloat();
|
||||
/*float tZ =*/ packet.readFloat();
|
||||
/*float tO =*/ packet.readFloat();
|
||||
block.onTransport = true;
|
||||
block.transportGuid = readPackedGuid(packet);
|
||||
block.transportX = packet.readFloat();
|
||||
block.transportY = packet.readFloat();
|
||||
block.transportZ = packet.readFloat();
|
||||
block.transportO = packet.readFloat();
|
||||
/*uint32_t tTime =*/ packet.readUInt32();
|
||||
/*int8_t tSeat =*/ packet.readUInt8();
|
||||
|
||||
LOG_DEBUG(" OnTransport: guid=0x", std::hex, block.transportGuid, std::dec,
|
||||
" offset=(", block.transportX, ", ", block.transportY, ", ", block.transportZ, ")");
|
||||
|
||||
if (moveFlags2 & 0x0200) { // MOVEMENTFLAG2_INTERPOLATED_MOVEMENT
|
||||
/*uint32_t tTime2 =*/ packet.readUInt32();
|
||||
}
|
||||
|
|
@ -1584,6 +1589,40 @@ bool CreatureQueryResponseParser::parse(network::Packet& packet, CreatureQueryRe
|
|||
return true;
|
||||
}
|
||||
|
||||
// ---- GameObject Query ----
|
||||
|
||||
network::Packet GameObjectQueryPacket::build(uint32_t entry, uint64_t guid) {
|
||||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_GAMEOBJECT_QUERY));
|
||||
packet.writeUInt32(entry);
|
||||
packet.writeUInt64(guid);
|
||||
LOG_DEBUG("Built CMSG_GAMEOBJECT_QUERY: entry=", entry, " guid=0x", std::hex, guid, std::dec);
|
||||
return packet;
|
||||
}
|
||||
|
||||
bool GameObjectQueryResponseParser::parse(network::Packet& packet, GameObjectQueryResponseData& data) {
|
||||
data.entry = packet.readUInt32();
|
||||
|
||||
// High bit set means gameobject not found
|
||||
if (data.entry & 0x80000000) {
|
||||
data.entry &= ~0x80000000;
|
||||
LOG_DEBUG("GameObject query: entry ", data.entry, " not found");
|
||||
data.name = "";
|
||||
return true;
|
||||
}
|
||||
|
||||
data.type = packet.readUInt32(); // GameObjectType
|
||||
/*uint32_t displayId =*/ packet.readUInt32();
|
||||
// 4 name strings (only first is usually populated)
|
||||
data.name = packet.readString();
|
||||
// name2, name3, name4
|
||||
packet.readString();
|
||||
packet.readString();
|
||||
packet.readString();
|
||||
|
||||
LOG_INFO("GameObject query response: ", data.name, " (type=", data.type, " entry=", data.entry, ")");
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---- Item Query ----
|
||||
|
||||
network::Packet ItemQueryPacket::build(uint32_t entry, uint64_t guid) {
|
||||
|
|
@ -2229,9 +2268,8 @@ network::Packet UseItemPacket::build(uint8_t bagIndex, uint8_t slotIndex, uint64
|
|||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_USE_ITEM));
|
||||
packet.writeUInt8(bagIndex);
|
||||
packet.writeUInt8(slotIndex);
|
||||
packet.writeUInt8(0); // spell index
|
||||
packet.writeUInt8(0); // cast count
|
||||
packet.writeUInt32(0); // spell id (unused)
|
||||
packet.writeUInt8(0); // cast count
|
||||
packet.writeUInt32(0); // spell id
|
||||
packet.writeUInt64(itemGuid);
|
||||
packet.writeUInt32(0); // glyph index
|
||||
packet.writeUInt8(0); // cast flags
|
||||
|
|
|
|||
|
|
@ -453,16 +453,16 @@ void CameraController::update(float deltaTime) {
|
|||
if (wmoRenderer) {
|
||||
glm::vec3 adjusted;
|
||||
if (wmoRenderer->checkWallCollision(stepPos, candidate, adjusted)) {
|
||||
// Before blocking, check if there's a floor at the
|
||||
// destination above current feet (stair step-up).
|
||||
// Before blocking, check if there's a walkable floor at the
|
||||
// destination (stair step-up or ramp continuation).
|
||||
float feetZ = stepPos.z;
|
||||
float probeZ = feetZ + 2.5f;
|
||||
auto floorH = wmoRenderer->getFloorHeight(
|
||||
candidate.x, candidate.y, probeZ);
|
||||
bool isStair = floorH &&
|
||||
*floorH > feetZ + 0.1f &&
|
||||
*floorH <= feetZ + 1.6f;
|
||||
if (!isStair) {
|
||||
bool walkable = floorH &&
|
||||
*floorH >= feetZ - 0.5f &&
|
||||
*floorH <= feetZ + 1.6f;
|
||||
if (!walkable) {
|
||||
candidate.x = adjusted.x;
|
||||
candidate.y = adjusted.y;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2148,6 +2148,21 @@ void M2Renderer::renderSmokeParticles(const Camera& /*camera*/, const glm::mat4&
|
|||
glEnable(GL_CULL_FACE);
|
||||
}
|
||||
|
||||
void M2Renderer::setInstancePosition(uint32_t instanceId, const glm::vec3& position) {
|
||||
auto idxIt = instanceIndexById.find(instanceId);
|
||||
if (idxIt == instanceIndexById.end()) return;
|
||||
auto& inst = instances[idxIt->second];
|
||||
inst.position = position;
|
||||
inst.updateModelMatrix();
|
||||
auto modelIt = models.find(inst.modelId);
|
||||
if (modelIt != models.end()) {
|
||||
glm::vec3 localMin, localMax;
|
||||
getTightCollisionBounds(modelIt->second, localMin, localMax);
|
||||
transformAABB(inst.modelMatrix, localMin, localMax, inst.worldBoundsMin, inst.worldBoundsMax);
|
||||
}
|
||||
rebuildSpatialIndex();
|
||||
}
|
||||
|
||||
void M2Renderer::removeInstance(uint32_t instanceId) {
|
||||
for (auto it = instances.begin(); it != instances.end(); ++it) {
|
||||
if (it->id == instanceId) {
|
||||
|
|
|
|||
|
|
@ -528,6 +528,30 @@ uint32_t WMORenderer::createInstance(uint32_t modelId, const glm::vec3& position
|
|||
return instance.id;
|
||||
}
|
||||
|
||||
void WMORenderer::setInstancePosition(uint32_t instanceId, const glm::vec3& position) {
|
||||
auto idxIt = instanceIndexById.find(instanceId);
|
||||
if (idxIt == instanceIndexById.end()) return;
|
||||
auto& inst = instances[idxIt->second];
|
||||
inst.position = position;
|
||||
inst.updateModelMatrix();
|
||||
auto modelIt = loadedModels.find(inst.modelId);
|
||||
if (modelIt != loadedModels.end()) {
|
||||
const ModelData& model = modelIt->second;
|
||||
transformAABB(inst.modelMatrix, model.boundingBoxMin, model.boundingBoxMax,
|
||||
inst.worldBoundsMin, inst.worldBoundsMax);
|
||||
inst.worldGroupBounds.clear();
|
||||
inst.worldGroupBounds.reserve(model.groups.size());
|
||||
for (const auto& group : model.groups) {
|
||||
glm::vec3 gMin, gMax;
|
||||
transformAABB(inst.modelMatrix, group.boundingBoxMin, group.boundingBoxMax, gMin, gMax);
|
||||
gMin -= glm::vec3(0.5f);
|
||||
gMax += glm::vec3(0.5f);
|
||||
inst.worldGroupBounds.emplace_back(gMin, gMax);
|
||||
}
|
||||
}
|
||||
rebuildSpatialIndex();
|
||||
}
|
||||
|
||||
void WMORenderer::removeInstance(uint32_t instanceId) {
|
||||
auto it = std::find_if(instances.begin(), instances.end(),
|
||||
[instanceId](const WMOInstance& inst) { return inst.id == instanceId; });
|
||||
|
|
|
|||
|
|
@ -46,6 +46,9 @@ namespace {
|
|||
} else if (entity->getType() == wowee::game::ObjectType::UNIT) {
|
||||
auto unit = std::static_pointer_cast<wowee::game::Unit>(entity);
|
||||
if (!unit->getName().empty()) return unit->getName();
|
||||
} else if (entity->getType() == wowee::game::ObjectType::GAMEOBJECT) {
|
||||
auto go = std::static_pointer_cast<wowee::game::GameObject>(entity);
|
||||
if (!go->getName().empty()) return go->getName();
|
||||
}
|
||||
return "Unknown";
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue