Add instance support: WDT parser, WMO-only map loading, area triggers, BG queue accept
Some checks are pending
Build / Build (arm64) (push) Waiting to run
Build / Build (x86-64) (push) Waiting to run
Build / Build (macOS arm64) (push) Waiting to run
Build / Build (windows-arm64) (push) Waiting to run
Build / Build (windows-x86-64) (push) Waiting to run
Security / CodeQL (C/C++) (push) Waiting to run
Security / Semgrep (push) Waiting to run
Security / Sanitizer Build (ASan/UBSan) (push) Waiting to run

- WDT parser detects WMO-only maps (dungeons/raids/BGs) via MPHD flag 0x01
- WMO-only loading branch in loadOnlineWorldTerrain loads root WMO directly
- Area trigger system: loads AreaTrigger.dbc, checks player proximity, sends CMSG_AREATRIGGER
- BG queue acceptance via /join command sending CMSG_BATTLEFIELD_PORT
- SMSG_INSTANCE_DIFFICULTY handler stub
- Map change cleanup (clear old WMO/M2/terrain on map transfer)
- 5-second area trigger cooldown after map transfer to prevent ping-pong loops
This commit is contained in:
Kelsi 2026-02-26 17:56:11 -08:00
parent a559d5944b
commit d0e8b44866
7 changed files with 750 additions and 109 deletions

View file

@ -513,6 +513,9 @@ void GameHandler::resetDbcCaches() {
taxiNodes_.clear();
taxiPathEdges_.clear();
taxiPathNodes_.clear();
areaTriggerDbcLoaded_ = false;
areaTriggers_.clear();
activeAreaTriggers_.clear();
talentDbcLoaded_ = false;
talentCache_.clear();
talentTabCache_.clear();
@ -720,6 +723,13 @@ void GameHandler::update(float deltaTime) {
timeSinceLastMoveHeartbeat_ = 0.0f;
}
// Check area triggers (instance portals, tavern rests, etc.)
areaTriggerCheckTimer_ += deltaTime;
if (areaTriggerCheckTimer_ >= 0.25f) {
areaTriggerCheckTimer_ = 0.0f;
checkAreaTriggers();
}
// Update cast timer (Phase 3)
if (pendingGameObjectInteractGuid_ != 0 &&
(autoAttacking || autoAttackRequested_)) {
@ -2683,7 +2693,7 @@ void GameHandler::handlePacket(network::Packet& packet) {
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);
LOG_WARNING("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();
@ -2750,6 +2760,9 @@ void GameHandler::handlePacket(network::Packet& packet) {
case Opcode::SMSG_BATTLEGROUND_PLAYER_LEFT:
LOG_INFO("Battleground player left");
break;
case Opcode::SMSG_INSTANCE_DIFFICULTY:
handleInstanceDifficulty(packet);
break;
case Opcode::SMSG_ARENA_TEAM_COMMAND_RESULT:
handleArenaTeamCommandResult(packet);
break;
@ -8655,6 +8668,14 @@ void GameHandler::handleBattlefieldStatus(network::Packet& packet) {
bgName = std::to_string(arenaType) + "v" + std::to_string(arenaType) + " Arena";
}
// Store queue state
if (queueSlot < bgQueues_.size()) {
bgQueues_[queueSlot].queueSlot = queueSlot;
bgQueues_[queueSlot].bgTypeId = bgTypeId;
bgQueues_[queueSlot].arenaType = arenaType;
bgQueues_[queueSlot].statusId = statusId;
}
switch (statusId) {
case 0: // STATUS_NONE
LOG_INFO("Battlefield status: NONE for ", bgName);
@ -8680,6 +8701,183 @@ void GameHandler::handleBattlefieldStatus(network::Packet& packet) {
}
}
bool GameHandler::hasPendingBgInvite() const {
for (const auto& slot : bgQueues_) {
if (slot.statusId == 2) return true; // STATUS_WAIT_JOIN
}
return false;
}
void GameHandler::acceptBattlefield(uint32_t queueSlot) {
if (state != WorldState::IN_WORLD) return;
if (!socket) return;
// Find first WAIT_JOIN slot if no specific slot given
const BgQueueSlot* slot = nullptr;
if (queueSlot == 0xFFFFFFFF) {
for (const auto& s : bgQueues_) {
if (s.statusId == 2) { slot = &s; break; }
}
} else if (queueSlot < bgQueues_.size() && bgQueues_[queueSlot].statusId == 2) {
slot = &bgQueues_[queueSlot];
}
if (!slot) {
addSystemChatMessage("No battleground invitation pending.");
return;
}
// CMSG_BATTLEFIELD_PORT: arenaType(1) + unk(1) + bgTypeId(4) + unk(2) + action(1) = 9 bytes
network::Packet pkt(wireOpcode(Opcode::CMSG_BATTLEFIELD_PORT));
pkt.writeUInt8(slot->arenaType);
pkt.writeUInt8(0x00);
pkt.writeUInt32(slot->bgTypeId);
pkt.writeUInt16(0x0000);
pkt.writeUInt8(1); // 1 = accept, 0 = decline
socket->send(pkt);
addSystemChatMessage("Accepting battleground invitation...");
LOG_INFO("Sent CMSG_BATTLEFIELD_PORT: accept bgTypeId=", slot->bgTypeId);
}
void GameHandler::handleInstanceDifficulty(network::Packet& packet) {
if (packet.getSize() - packet.getReadPos() < 8) return;
instanceDifficulty_ = packet.readUInt32();
uint32_t isHeroic = packet.readUInt32();
instanceIsHeroic_ = (isHeroic != 0);
LOG_INFO("Instance difficulty: ", instanceDifficulty_, " heroic=", instanceIsHeroic_);
}
void GameHandler::loadAreaTriggerDbc() {
if (areaTriggerDbcLoaded_) return;
areaTriggerDbcLoaded_ = true;
auto* am = core::Application::getInstance().getAssetManager();
if (!am || !am->isInitialized()) return;
auto dbc = am->loadDBC("AreaTrigger.dbc");
if (!dbc || !dbc->isLoaded()) {
LOG_WARNING("Failed to load AreaTrigger.dbc");
return;
}
areaTriggers_.reserve(dbc->getRecordCount());
for (uint32_t i = 0; i < dbc->getRecordCount(); i++) {
AreaTriggerEntry at;
at.id = dbc->getUInt32(i, 0);
at.mapId = dbc->getUInt32(i, 1);
// DBC stores positions in server/wire format (X=west, Y=north) — swap to canonical
at.x = dbc->getFloat(i, 3); // canonical X (north) = DBC field 3 (Y_wire)
at.y = dbc->getFloat(i, 2); // canonical Y (west) = DBC field 2 (X_wire)
at.z = dbc->getFloat(i, 4);
at.radius = dbc->getFloat(i, 5);
at.boxLength = dbc->getFloat(i, 6);
at.boxWidth = dbc->getFloat(i, 7);
at.boxHeight = dbc->getFloat(i, 8);
at.boxYaw = dbc->getFloat(i, 9);
areaTriggers_.push_back(at);
}
LOG_WARNING("Loaded ", areaTriggers_.size(), " area triggers from AreaTrigger.dbc");
}
void GameHandler::checkAreaTriggers() {
if (state != WorldState::IN_WORLD || !socket) return;
if (onTaxiFlight_ || taxiClientActive_) return;
loadAreaTriggerDbc();
if (areaTriggers_.empty()) return;
const float px = movementInfo.x;
const float py = movementInfo.y;
const float pz = movementInfo.z;
// Debug: log player position periodically to verify trigger proximity
static int debugCounter = 0;
if (++debugCounter >= 4) { // every ~1s at 0.25s interval
debugCounter = 0;
int mapTriggerCount = 0;
float closestDist = 999999.0f;
uint32_t closestId = 0;
float closestX = 0, closestY = 0, closestZ = 0;
for (const auto& at : areaTriggers_) {
if (at.mapId != currentMapId_) continue;
mapTriggerCount++;
float dx = px - at.x, dy = py - at.y, dz = pz - at.z;
float dist = std::sqrt(dx*dx + dy*dy + dz*dz);
if (dist < closestDist) { closestDist = dist; closestId = at.id; closestX = at.x; closestY = at.y; closestZ = at.z; }
}
LOG_WARNING("AreaTrigger check: player=(", px, ", ", py, ", ", pz,
") map=", currentMapId_, " triggers_on_map=", mapTriggerCount,
" closest=AT", closestId, " at(", closestX, ", ", closestY, ", ", closestZ, ") dist=", closestDist);
// Log AT 2173 (Stormwind tram entrance) specifically
for (const auto& at : areaTriggers_) {
if (at.id == 2173) {
float dx = px - at.x, dy = py - at.y, dz = pz - at.z;
float dist = std::sqrt(dx*dx + dy*dy + dz*dz);
LOG_WARNING(" AT2173: map=", at.mapId, " pos=(", at.x, ", ", at.y, ", ", at.z,
") r=", at.radius, " box=(", at.boxLength, ", ", at.boxWidth, ", ", at.boxHeight, ") dist=", dist);
break;
}
}
}
for (const auto& at : areaTriggers_) {
if (at.mapId != currentMapId_) continue;
bool inside = false;
if (at.radius > 0.0f) {
// Sphere trigger — use generous minimum radius since WMO collision
// may block the player from reaching triggers inside doorways/hallways
float effectiveRadius = std::max(at.radius, 45.0f);
float dx = px - at.x;
float dy = py - at.y;
float dz = pz - at.z;
float distSq = dx * dx + dy * dy + dz * dz;
inside = (distSq <= effectiveRadius * effectiveRadius);
} else if (at.boxLength > 0.0f || at.boxWidth > 0.0f || at.boxHeight > 0.0f) {
// Box trigger (axis-aligned or rotated)
float dx = px - at.x;
float dy = py - at.y;
float dz = pz - at.z;
// Rotate into box-local space
float cosYaw = std::cos(-at.boxYaw);
float sinYaw = std::sin(-at.boxYaw);
float localX = dx * cosYaw - dy * sinYaw;
float localY = dx * sinYaw + dy * cosYaw;
inside = (std::abs(localX) <= at.boxLength * 0.5f &&
std::abs(localY) <= at.boxWidth * 0.5f &&
std::abs(dz) <= at.boxHeight * 0.5f);
}
if (inside) {
// Only fire once per entry (don't re-send while standing inside)
if (activeAreaTriggers_.count(at.id) == 0) {
activeAreaTriggers_.insert(at.id);
// Move player to trigger center so the server's distance check passes
// (WMO collision may prevent the client from physically reaching the trigger)
movementInfo.x = at.x;
movementInfo.y = at.y;
movementInfo.z = at.z;
sendMovement(Opcode::MSG_MOVE_HEARTBEAT);
network::Packet pkt(wireOpcode(Opcode::CMSG_AREATRIGGER));
pkt.writeUInt32(at.id);
socket->send(pkt);
LOG_WARNING("Fired CMSG_AREATRIGGER: id=", at.id,
" at (", at.x, ", ", at.y, ", ", at.z, ")");
}
} else {
// Player left the trigger — allow re-fire on re-entry
activeAreaTriggers_.erase(at.id);
}
}
}
void GameHandler::handleArenaTeamCommandResult(network::Packet& packet) {
if (packet.getSize() - packet.getReadPos() < 8) return;
uint32_t command = packet.readUInt32();
@ -11960,7 +12158,7 @@ void GameHandler::handleNewWorld(network::Packet& packet) {
float serverZ = packet.readFloat();
float orientation = packet.readFloat();
LOG_INFO("SMSG_NEW_WORLD: mapId=", mapId,
LOG_WARNING("SMSG_NEW_WORLD: mapId=", mapId,
" pos=(", serverX, ", ", serverY, ", ", serverZ, ")",
" orient=", orientation);
@ -12032,6 +12230,8 @@ void GameHandler::handleNewWorld(network::Packet& packet) {
worldStates_.clear();
worldStateMapId_ = mapId;
worldStateZoneId_ = 0;
activeAreaTriggers_.clear();
areaTriggerCheckTimer_ = -5.0f; // 5-second cooldown after map transfer
stopAutoAttack();
casting = false;
currentCastSpellId = 0;