mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-03-22 23:30:14 +00:00
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
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:
parent
a559d5944b
commit
d0e8b44866
7 changed files with 750 additions and 109 deletions
|
|
@ -284,6 +284,7 @@ set(WOWEE_SOURCES
|
||||||
src/pipeline/m2_loader.cpp
|
src/pipeline/m2_loader.cpp
|
||||||
src/pipeline/wmo_loader.cpp
|
src/pipeline/wmo_loader.cpp
|
||||||
src/pipeline/adt_loader.cpp
|
src/pipeline/adt_loader.cpp
|
||||||
|
src/pipeline/wdt_loader.cpp
|
||||||
src/pipeline/dbc_layout.cpp
|
src/pipeline/dbc_layout.cpp
|
||||||
|
|
||||||
src/pipeline/terrain_mesh.cpp
|
src/pipeline/terrain_mesh.cpp
|
||||||
|
|
@ -401,6 +402,7 @@ set(WOWEE_HEADERS
|
||||||
include/pipeline/m2_loader.hpp
|
include/pipeline/m2_loader.hpp
|
||||||
include/pipeline/wmo_loader.hpp
|
include/pipeline/wmo_loader.hpp
|
||||||
include/pipeline/adt_loader.hpp
|
include/pipeline/adt_loader.hpp
|
||||||
|
include/pipeline/wdt_loader.hpp
|
||||||
include/pipeline/dbc_loader.hpp
|
include/pipeline/dbc_loader.hpp
|
||||||
include/pipeline/terrain_mesh.hpp
|
include/pipeline/terrain_mesh.hpp
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -321,6 +321,10 @@ public:
|
||||||
// Random roll
|
// Random roll
|
||||||
void randomRoll(uint32_t minRoll = 1, uint32_t maxRoll = 100);
|
void randomRoll(uint32_t minRoll = 1, uint32_t maxRoll = 100);
|
||||||
|
|
||||||
|
// Battleground
|
||||||
|
bool hasPendingBgInvite() const;
|
||||||
|
void acceptBattlefield(uint32_t queueSlot = 0xFFFFFFFF);
|
||||||
|
|
||||||
// Logout commands
|
// Logout commands
|
||||||
void requestLogout();
|
void requestLogout();
|
||||||
void cancelLogout();
|
void cancelLogout();
|
||||||
|
|
@ -1189,8 +1193,13 @@ private:
|
||||||
void handleForceMoveFlagChange(network::Packet& packet, const char* name, Opcode ackOpcode, uint32_t flag, bool set);
|
void handleForceMoveFlagChange(network::Packet& packet, const char* name, Opcode ackOpcode, uint32_t flag, bool set);
|
||||||
void handleMoveKnockBack(network::Packet& packet);
|
void handleMoveKnockBack(network::Packet& packet);
|
||||||
|
|
||||||
|
// ---- Area trigger detection ----
|
||||||
|
void loadAreaTriggerDbc();
|
||||||
|
void checkAreaTriggers();
|
||||||
|
|
||||||
// ---- Arena / Battleground handlers ----
|
// ---- Arena / Battleground handlers ----
|
||||||
void handleBattlefieldStatus(network::Packet& packet);
|
void handleBattlefieldStatus(network::Packet& packet);
|
||||||
|
void handleInstanceDifficulty(network::Packet& packet);
|
||||||
void handleArenaTeamCommandResult(network::Packet& packet);
|
void handleArenaTeamCommandResult(network::Packet& packet);
|
||||||
void handleArenaTeamQueryResponse(network::Packet& packet);
|
void handleArenaTeamQueryResponse(network::Packet& packet);
|
||||||
void handleArenaTeamInvite(network::Packet& packet);
|
void handleArenaTeamInvite(network::Packet& packet);
|
||||||
|
|
@ -1477,12 +1486,40 @@ private:
|
||||||
std::unordered_map<uint32_t, TalentEntry> talentCache_; // talentId -> entry
|
std::unordered_map<uint32_t, TalentEntry> talentCache_; // talentId -> entry
|
||||||
std::unordered_map<uint32_t, TalentTabEntry> talentTabCache_; // tabId -> entry
|
std::unordered_map<uint32_t, TalentTabEntry> talentTabCache_; // tabId -> entry
|
||||||
bool talentDbcLoaded_ = false;
|
bool talentDbcLoaded_ = false;
|
||||||
|
|
||||||
|
// ---- Area trigger detection ----
|
||||||
|
struct AreaTriggerEntry {
|
||||||
|
uint32_t id = 0;
|
||||||
|
uint32_t mapId = 0;
|
||||||
|
float x = 0, y = 0, z = 0; // canonical WoW coords (converted from DBC)
|
||||||
|
float radius = 0;
|
||||||
|
float boxLength = 0, boxWidth = 0, boxHeight = 0;
|
||||||
|
float boxYaw = 0;
|
||||||
|
};
|
||||||
|
bool areaTriggerDbcLoaded_ = false;
|
||||||
|
std::vector<AreaTriggerEntry> areaTriggers_;
|
||||||
|
std::unordered_set<uint32_t> activeAreaTriggers_; // triggers player is currently inside
|
||||||
|
float areaTriggerCheckTimer_ = 0.0f;
|
||||||
|
|
||||||
float castTimeTotal = 0.0f;
|
float castTimeTotal = 0.0f;
|
||||||
std::array<ActionBarSlot, 12> actionBar{};
|
std::array<ActionBarSlot, 12> actionBar{};
|
||||||
std::vector<AuraSlot> playerAuras;
|
std::vector<AuraSlot> playerAuras;
|
||||||
std::vector<AuraSlot> targetAuras;
|
std::vector<AuraSlot> targetAuras;
|
||||||
uint64_t petGuid_ = 0;
|
uint64_t petGuid_ = 0;
|
||||||
|
|
||||||
|
// ---- Battleground queue state ----
|
||||||
|
struct BgQueueSlot {
|
||||||
|
uint32_t queueSlot = 0;
|
||||||
|
uint32_t bgTypeId = 0;
|
||||||
|
uint8_t arenaType = 0;
|
||||||
|
uint32_t statusId = 0; // 0=none, 1=wait_queue, 2=wait_join, 3=in_progress
|
||||||
|
};
|
||||||
|
std::array<BgQueueSlot, 3> bgQueues_{};
|
||||||
|
|
||||||
|
// Instance difficulty
|
||||||
|
uint32_t instanceDifficulty_ = 0;
|
||||||
|
bool instanceIsHeroic_ = false;
|
||||||
|
|
||||||
// ---- Phase 4: Group ----
|
// ---- Phase 4: Group ----
|
||||||
GroupListData partyData;
|
GroupListData partyData;
|
||||||
bool pendingGroupInvite = false;
|
bool pendingGroupInvite = false;
|
||||||
|
|
|
||||||
26
include/pipeline/wdt_loader.hpp
Normal file
26
include/pipeline/wdt_loader.hpp
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace wowee {
|
||||||
|
namespace pipeline {
|
||||||
|
|
||||||
|
struct WDTInfo {
|
||||||
|
uint32_t mphdFlags = 0;
|
||||||
|
bool isWMOOnly() const { return mphdFlags & 0x01; } // WDTF_GLOBAL_WMO
|
||||||
|
|
||||||
|
std::string rootWMOPath; // from MWMO chunk (null-terminated string)
|
||||||
|
|
||||||
|
// MODF placement (only valid for WMO-only maps):
|
||||||
|
float position[3] = {}; // ADT placement space coords
|
||||||
|
float rotation[3] = {}; // degrees
|
||||||
|
uint16_t flags = 0;
|
||||||
|
uint16_t doodadSet = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
WDTInfo parseWDT(const std::vector<uint8_t>& data);
|
||||||
|
|
||||||
|
} // namespace pipeline
|
||||||
|
} // namespace wowee
|
||||||
|
|
@ -32,6 +32,7 @@
|
||||||
#include <imgui.h>
|
#include <imgui.h>
|
||||||
#include "pipeline/m2_loader.hpp"
|
#include "pipeline/m2_loader.hpp"
|
||||||
#include "pipeline/wmo_loader.hpp"
|
#include "pipeline/wmo_loader.hpp"
|
||||||
|
#include "pipeline/wdt_loader.hpp"
|
||||||
#include "pipeline/dbc_loader.hpp"
|
#include "pipeline/dbc_loader.hpp"
|
||||||
#include "ui/ui_manager.hpp"
|
#include "ui/ui_manager.hpp"
|
||||||
#include "auth/auth_handler.hpp"
|
#include "auth/auth_handler.hpp"
|
||||||
|
|
@ -3185,6 +3186,59 @@ void Application::loadOnlineWorldTerrain(uint32_t mapId, float x, float y, float
|
||||||
|
|
||||||
showProgress("Entering world...", 0.0f);
|
showProgress("Entering world...", 0.0f);
|
||||||
|
|
||||||
|
// --- Clean up previous map's state on map change ---
|
||||||
|
// (Same cleanup as logout, but preserves player identity and renderer objects.)
|
||||||
|
if (loadedMapId_ != 0xFFFFFFFF) {
|
||||||
|
LOG_INFO("Map change: cleaning up old map ", loadedMapId_, " before loading map ", mapId);
|
||||||
|
|
||||||
|
// Clear entity instances from old map
|
||||||
|
creatureInstances_.clear();
|
||||||
|
creatureModelIds_.clear();
|
||||||
|
creatureRenderPosCache_.clear();
|
||||||
|
creatureWeaponsAttached_.clear();
|
||||||
|
creatureWeaponAttachAttempts_.clear();
|
||||||
|
deadCreatureGuids_.clear();
|
||||||
|
nonRenderableCreatureDisplayIds_.clear();
|
||||||
|
creaturePermanentFailureGuids_.clear();
|
||||||
|
|
||||||
|
pendingCreatureSpawns_.clear();
|
||||||
|
pendingCreatureSpawnGuids_.clear();
|
||||||
|
creatureSpawnRetryCounts_.clear();
|
||||||
|
|
||||||
|
playerInstances_.clear();
|
||||||
|
onlinePlayerAppearance_.clear();
|
||||||
|
pendingOnlinePlayerEquipment_.clear();
|
||||||
|
deferredEquipmentQueue_.clear();
|
||||||
|
pendingPlayerSpawns_.clear();
|
||||||
|
pendingPlayerSpawnGuids_.clear();
|
||||||
|
|
||||||
|
gameObjectInstances_.clear();
|
||||||
|
pendingGameObjectSpawns_.clear();
|
||||||
|
pendingTransportMoves_.clear();
|
||||||
|
pendingTransportDoodadBatches_.clear();
|
||||||
|
|
||||||
|
if (renderer) {
|
||||||
|
// Clear all world geometry from old map
|
||||||
|
if (auto* wmo = renderer->getWMORenderer()) {
|
||||||
|
wmo->clearInstances();
|
||||||
|
}
|
||||||
|
if (auto* m2 = renderer->getM2Renderer()) {
|
||||||
|
m2->clear();
|
||||||
|
}
|
||||||
|
if (auto* terrain = renderer->getTerrainManager()) {
|
||||||
|
terrain->softReset();
|
||||||
|
terrain->setStreamingEnabled(true); // Re-enable in case previous map disabled it
|
||||||
|
}
|
||||||
|
if (auto* questMarkers = renderer->getQuestMarkerRenderer()) {
|
||||||
|
questMarkers->clear();
|
||||||
|
}
|
||||||
|
renderer->clearMount();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Force player character re-spawn on new map
|
||||||
|
playerCharacterSpawned = false;
|
||||||
|
}
|
||||||
|
|
||||||
// Resolve map folder name from Map.dbc (authoritative for world/instance maps).
|
// Resolve map folder name from Map.dbc (authoritative for world/instance maps).
|
||||||
// This is required for instances like DeeprunTram (map 369) that are not Azeroth/Kalimdor.
|
// This is required for instances like DeeprunTram (map 369) that are not Azeroth/Kalimdor.
|
||||||
if (!mapNameCacheLoaded_ && assetManager) {
|
if (!mapNameCacheLoaded_ && assetManager) {
|
||||||
|
|
@ -3310,6 +3364,211 @@ void Application::loadOnlineWorldTerrain(uint32_t mapId, float x, float y, float
|
||||||
|
|
||||||
showProgress("Loading terrain...", 0.20f);
|
showProgress("Loading terrain...", 0.20f);
|
||||||
|
|
||||||
|
// Check WDT to detect WMO-only maps (dungeons, raids, BGs)
|
||||||
|
bool isWMOOnlyMap = false;
|
||||||
|
pipeline::WDTInfo wdtInfo;
|
||||||
|
{
|
||||||
|
std::string wdtPath = "World\\Maps\\" + mapName + "\\" + mapName + ".wdt";
|
||||||
|
LOG_WARNING("Reading WDT: ", wdtPath);
|
||||||
|
std::vector<uint8_t> wdtData = assetManager->readFile(wdtPath);
|
||||||
|
if (!wdtData.empty()) {
|
||||||
|
wdtInfo = pipeline::parseWDT(wdtData);
|
||||||
|
isWMOOnlyMap = wdtInfo.isWMOOnly() && !wdtInfo.rootWMOPath.empty();
|
||||||
|
LOG_WARNING("WDT result: isWMOOnly=", isWMOOnlyMap, " rootWMO='", wdtInfo.rootWMOPath, "'");
|
||||||
|
} else {
|
||||||
|
LOG_WARNING("No WDT file found at ", wdtPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool terrainOk = false;
|
||||||
|
|
||||||
|
if (isWMOOnlyMap) {
|
||||||
|
// ---- WMO-only map (dungeon/raid/BG): load root WMO directly ----
|
||||||
|
LOG_INFO("WMO-only map detected — loading root WMO: ", wdtInfo.rootWMOPath);
|
||||||
|
showProgress("Loading instance geometry...", 0.25f);
|
||||||
|
|
||||||
|
// Still call loadTestTerrain with a dummy path to initialize all renderers
|
||||||
|
// (terrain, WMO, M2, character). The terrain load will fail gracefully.
|
||||||
|
auto [tileX, tileY] = core::coords::canonicalToTile(spawnCanonical.x, spawnCanonical.y);
|
||||||
|
std::string dummyAdtPath = "World\\Maps\\" + mapName + "\\" + mapName + "_" +
|
||||||
|
std::to_string(tileX) + "_" + std::to_string(tileY) + ".adt";
|
||||||
|
renderer->loadTestTerrain(assetManager.get(), dummyAdtPath);
|
||||||
|
|
||||||
|
// Disable terrain streaming — no ADT tiles for WMO-only maps
|
||||||
|
if (renderer->getTerrainManager()) {
|
||||||
|
renderer->getTerrainManager()->setStreamingEnabled(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Spawn player character now that renderers are initialized
|
||||||
|
if (!playerCharacterSpawned) {
|
||||||
|
spawnPlayerCharacter();
|
||||||
|
loadEquippedWeapons();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load the root WMO
|
||||||
|
auto* wmoRenderer = renderer->getWMORenderer();
|
||||||
|
if (wmoRenderer) {
|
||||||
|
std::vector<uint8_t> wmoData = assetManager->readFile(wdtInfo.rootWMOPath);
|
||||||
|
if (!wmoData.empty()) {
|
||||||
|
pipeline::WMOModel wmoModel = pipeline::WMOLoader::load(wmoData);
|
||||||
|
|
||||||
|
if (wmoModel.nGroups > 0) {
|
||||||
|
showProgress("Loading instance groups...", 0.35f);
|
||||||
|
std::string basePath = wdtInfo.rootWMOPath;
|
||||||
|
std::string extension;
|
||||||
|
if (basePath.size() > 4) {
|
||||||
|
extension = basePath.substr(basePath.size() - 4);
|
||||||
|
std::string extLower = extension;
|
||||||
|
for (char& c : extLower) c = std::tolower(c);
|
||||||
|
if (extLower == ".wmo") {
|
||||||
|
basePath = basePath.substr(0, basePath.size() - 4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t loadedGroups = 0;
|
||||||
|
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++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update loading progress
|
||||||
|
if (wmoModel.nGroups > 1) {
|
||||||
|
float groupProgress = 0.35f + 0.30f * static_cast<float>(gi + 1) / wmoModel.nGroups;
|
||||||
|
char buf[128];
|
||||||
|
snprintf(buf, sizeof(buf), "Loading instance groups... %u / %u", gi + 1, wmoModel.nGroups);
|
||||||
|
showProgress(buf, groupProgress);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_INFO("Loaded ", loadedGroups, " / ", wmoModel.nGroups, " WMO groups for instance");
|
||||||
|
}
|
||||||
|
|
||||||
|
// WMO-only maps: MODF position is at world origin (always 0,0,0 in practice).
|
||||||
|
// Unlike ADT MODF which uses placement space, WMO-only maps place the WMO
|
||||||
|
// directly in render coordinates with no offset or yaw bias.
|
||||||
|
glm::vec3 wmoPos(0.0f);
|
||||||
|
glm::vec3 wmoRot(0.0f);
|
||||||
|
if (wdtInfo.position[0] != 0.0f || wdtInfo.position[1] != 0.0f || wdtInfo.position[2] != 0.0f) {
|
||||||
|
// Non-zero placement — convert from ADT space (rare/never happens, but be safe)
|
||||||
|
wmoPos = core::coords::adtToWorld(
|
||||||
|
wdtInfo.position[0], wdtInfo.position[1], wdtInfo.position[2]);
|
||||||
|
wmoRot = glm::vec3(
|
||||||
|
-wdtInfo.rotation[2] * 3.14159f / 180.0f,
|
||||||
|
-wdtInfo.rotation[0] * 3.14159f / 180.0f,
|
||||||
|
(wdtInfo.rotation[1] + 180.0f) * 3.14159f / 180.0f
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
showProgress("Uploading instance geometry...", 0.70f);
|
||||||
|
uint32_t wmoModelId = 900000 + mapId; // Unique ID range for instance WMOs
|
||||||
|
if (wmoRenderer->loadModel(wmoModel, wmoModelId)) {
|
||||||
|
uint32_t instanceId = wmoRenderer->createInstance(wmoModelId, wmoPos, wmoRot, 1.0f);
|
||||||
|
if (instanceId > 0) {
|
||||||
|
LOG_INFO("Instance WMO loaded: modelId=", wmoModelId,
|
||||||
|
" instanceId=", instanceId,
|
||||||
|
" pos=(", wmoPos.x, ", ", wmoPos.y, ", ", wmoPos.z, ")");
|
||||||
|
|
||||||
|
// Load doodads from the specified doodad set
|
||||||
|
auto* m2Renderer = renderer->getM2Renderer();
|
||||||
|
if (m2Renderer && !wmoModel.doodadSets.empty() && !wmoModel.doodads.empty()) {
|
||||||
|
uint32_t setIdx = std::min(static_cast<uint32_t>(wdtInfo.doodadSet),
|
||||||
|
static_cast<uint32_t>(wmoModel.doodadSets.size() - 1));
|
||||||
|
const auto& doodadSet = wmoModel.doodadSets[setIdx];
|
||||||
|
|
||||||
|
showProgress("Loading instance doodads...", 0.75f);
|
||||||
|
glm::mat4 wmoMatrix(1.0f);
|
||||||
|
wmoMatrix = glm::translate(wmoMatrix, wmoPos);
|
||||||
|
wmoMatrix = glm::rotate(wmoMatrix, wmoRot.z, glm::vec3(0, 0, 1));
|
||||||
|
wmoMatrix = glm::rotate(wmoMatrix, wmoRot.y, glm::vec3(0, 1, 0));
|
||||||
|
wmoMatrix = glm::rotate(wmoMatrix, wmoRot.x, glm::vec3(1, 0, 0));
|
||||||
|
|
||||||
|
uint32_t loadedDoodads = 0;
|
||||||
|
for (uint32_t di = 0; di < doodadSet.count; di++) {
|
||||||
|
uint32_t doodadIdx = doodadSet.startIndex + di;
|
||||||
|
if (doodadIdx >= wmoModel.doodads.size()) break;
|
||||||
|
|
||||||
|
const auto& doodad = wmoModel.doodads[doodadIdx];
|
||||||
|
auto nameIt = wmoModel.doodadNames.find(doodad.nameIndex);
|
||||||
|
if (nameIt == wmoModel.doodadNames.end()) continue;
|
||||||
|
|
||||||
|
std::string m2Path = nameIt->second;
|
||||||
|
if (m2Path.empty()) continue;
|
||||||
|
|
||||||
|
if (m2Path.size() > 4) {
|
||||||
|
std::string ext = m2Path.substr(m2Path.size() - 4);
|
||||||
|
for (char& c : ext) c = std::tolower(c);
|
||||||
|
if (ext == ".mdx" || ext == ".mdl") {
|
||||||
|
m2Path = m2Path.substr(0, m2Path.size() - 4) + ".m2";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<uint8_t> m2Data = assetManager->readFile(m2Path);
|
||||||
|
if (m2Data.empty()) continue;
|
||||||
|
|
||||||
|
pipeline::M2Model m2Model = pipeline::M2Loader::load(m2Data);
|
||||||
|
if (m2Model.name.empty()) m2Model.name = m2Path;
|
||||||
|
|
||||||
|
std::string skinPath = m2Path.substr(0, m2Path.size() - 3) + "00.skin";
|
||||||
|
std::vector<uint8_t> skinData = assetManager->readFile(skinPath);
|
||||||
|
if (!skinData.empty() && m2Model.version >= 264) {
|
||||||
|
pipeline::M2Loader::loadSkin(skinData, m2Model);
|
||||||
|
}
|
||||||
|
if (!m2Model.isValid()) continue;
|
||||||
|
|
||||||
|
glm::quat fixedRotation(doodad.rotation.w, doodad.rotation.y,
|
||||||
|
doodad.rotation.x, doodad.rotation.z);
|
||||||
|
glm::mat4 doodadLocal(1.0f);
|
||||||
|
doodadLocal = glm::translate(doodadLocal, doodad.position);
|
||||||
|
doodadLocal *= glm::mat4_cast(fixedRotation);
|
||||||
|
doodadLocal = glm::scale(doodadLocal, glm::vec3(doodad.scale));
|
||||||
|
|
||||||
|
glm::mat4 worldMatrix = wmoMatrix * doodadLocal;
|
||||||
|
glm::vec3 worldPos = glm::vec3(worldMatrix[3]);
|
||||||
|
|
||||||
|
uint32_t doodadModelId = static_cast<uint32_t>(std::hash<std::string>{}(m2Path));
|
||||||
|
m2Renderer->loadModel(m2Model, doodadModelId);
|
||||||
|
m2Renderer->createInstance(doodadModelId, worldPos, glm::vec3(0.0f), doodad.scale);
|
||||||
|
loadedDoodads++;
|
||||||
|
}
|
||||||
|
LOG_INFO("Loaded ", loadedDoodads, " instance WMO doodads");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LOG_WARNING("Failed to create instance WMO instance");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LOG_WARNING("Failed to load instance WMO model");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LOG_WARNING("Failed to read root WMO file: ", wdtInfo.rootWMOPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build collision cache for the instance WMO
|
||||||
|
showProgress("Building collision cache...", 0.88f);
|
||||||
|
if (loadingScreenOk) { loadingScreen.render(); window->swapBuffers(); }
|
||||||
|
wmoRenderer->loadFloorCache();
|
||||||
|
if (wmoRenderer->getFloorCacheSize() == 0) {
|
||||||
|
showProgress("Computing walkable surfaces...", 0.90f);
|
||||||
|
if (loadingScreenOk) { loadingScreen.render(); window->swapBuffers(); }
|
||||||
|
wmoRenderer->precomputeFloorCache();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
terrainOk = true; // Mark as OK so post-load setup runs
|
||||||
|
} else {
|
||||||
|
// ---- Normal ADT-based map ----
|
||||||
// Compute ADT tile from canonical coordinates
|
// Compute ADT tile from canonical coordinates
|
||||||
auto [tileX, tileY] = core::coords::canonicalToTile(spawnCanonical.x, spawnCanonical.y);
|
auto [tileX, tileY] = core::coords::canonicalToTile(spawnCanonical.x, spawnCanonical.y);
|
||||||
std::string adtPath = "World\\Maps\\" + mapName + "\\" + mapName + "_" +
|
std::string adtPath = "World\\Maps\\" + mapName + "\\" + mapName + "_" +
|
||||||
|
|
@ -3318,7 +3577,7 @@ void Application::loadOnlineWorldTerrain(uint32_t mapId, float x, float y, float
|
||||||
spawnCanonical.x, ", ", spawnCanonical.y, ", ", spawnCanonical.z, ")");
|
spawnCanonical.x, ", ", spawnCanonical.y, ", ", spawnCanonical.z, ")");
|
||||||
|
|
||||||
// Load the initial terrain tile
|
// Load the initial terrain tile
|
||||||
bool terrainOk = renderer->loadTestTerrain(assetManager.get(), adtPath);
|
terrainOk = renderer->loadTestTerrain(assetManager.get(), adtPath);
|
||||||
if (!terrainOk) {
|
if (!terrainOk) {
|
||||||
LOG_WARNING("Could not load terrain for online world - atmospheric rendering only");
|
LOG_WARNING("Could not load terrain for online world - atmospheric rendering only");
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -3442,6 +3701,7 @@ void Application::loadOnlineWorldTerrain(uint32_t mapId, float x, float y, float
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Snap player to loaded terrain so they don't spawn underground
|
// Snap player to loaded terrain so they don't spawn underground
|
||||||
if (renderer->getCameraController()) {
|
if (renderer->getCameraController()) {
|
||||||
|
|
|
||||||
|
|
@ -513,6 +513,9 @@ void GameHandler::resetDbcCaches() {
|
||||||
taxiNodes_.clear();
|
taxiNodes_.clear();
|
||||||
taxiPathEdges_.clear();
|
taxiPathEdges_.clear();
|
||||||
taxiPathNodes_.clear();
|
taxiPathNodes_.clear();
|
||||||
|
areaTriggerDbcLoaded_ = false;
|
||||||
|
areaTriggers_.clear();
|
||||||
|
activeAreaTriggers_.clear();
|
||||||
talentDbcLoaded_ = false;
|
talentDbcLoaded_ = false;
|
||||||
talentCache_.clear();
|
talentCache_.clear();
|
||||||
talentTabCache_.clear();
|
talentTabCache_.clear();
|
||||||
|
|
@ -720,6 +723,13 @@ void GameHandler::update(float deltaTime) {
|
||||||
timeSinceLastMoveHeartbeat_ = 0.0f;
|
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)
|
// Update cast timer (Phase 3)
|
||||||
if (pendingGameObjectInteractGuid_ != 0 &&
|
if (pendingGameObjectInteractGuid_ != 0 &&
|
||||||
(autoAttacking || autoAttackRequested_)) {
|
(autoAttacking || autoAttackRequested_)) {
|
||||||
|
|
@ -2683,7 +2693,7 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
||||||
case Opcode::SMSG_TRANSFER_PENDING: {
|
case Opcode::SMSG_TRANSFER_PENDING: {
|
||||||
// SMSG_TRANSFER_PENDING: uint32 mapId, then optional transport data
|
// SMSG_TRANSFER_PENDING: uint32 mapId, then optional transport data
|
||||||
uint32_t pendingMapId = packet.readUInt32();
|
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
|
// Optional: if remaining data, there's a transport entry + mapId
|
||||||
if (packet.getReadPos() + 8 <= packet.getSize()) {
|
if (packet.getReadPos() + 8 <= packet.getSize()) {
|
||||||
uint32_t transportEntry = packet.readUInt32();
|
uint32_t transportEntry = packet.readUInt32();
|
||||||
|
|
@ -2750,6 +2760,9 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
||||||
case Opcode::SMSG_BATTLEGROUND_PLAYER_LEFT:
|
case Opcode::SMSG_BATTLEGROUND_PLAYER_LEFT:
|
||||||
LOG_INFO("Battleground player left");
|
LOG_INFO("Battleground player left");
|
||||||
break;
|
break;
|
||||||
|
case Opcode::SMSG_INSTANCE_DIFFICULTY:
|
||||||
|
handleInstanceDifficulty(packet);
|
||||||
|
break;
|
||||||
case Opcode::SMSG_ARENA_TEAM_COMMAND_RESULT:
|
case Opcode::SMSG_ARENA_TEAM_COMMAND_RESULT:
|
||||||
handleArenaTeamCommandResult(packet);
|
handleArenaTeamCommandResult(packet);
|
||||||
break;
|
break;
|
||||||
|
|
@ -8655,6 +8668,14 @@ void GameHandler::handleBattlefieldStatus(network::Packet& packet) {
|
||||||
bgName = std::to_string(arenaType) + "v" + std::to_string(arenaType) + " Arena";
|
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) {
|
switch (statusId) {
|
||||||
case 0: // STATUS_NONE
|
case 0: // STATUS_NONE
|
||||||
LOG_INFO("Battlefield status: NONE for ", bgName);
|
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) {
|
void GameHandler::handleArenaTeamCommandResult(network::Packet& packet) {
|
||||||
if (packet.getSize() - packet.getReadPos() < 8) return;
|
if (packet.getSize() - packet.getReadPos() < 8) return;
|
||||||
uint32_t command = packet.readUInt32();
|
uint32_t command = packet.readUInt32();
|
||||||
|
|
@ -11960,7 +12158,7 @@ void GameHandler::handleNewWorld(network::Packet& packet) {
|
||||||
float serverZ = packet.readFloat();
|
float serverZ = packet.readFloat();
|
||||||
float orientation = packet.readFloat();
|
float orientation = packet.readFloat();
|
||||||
|
|
||||||
LOG_INFO("SMSG_NEW_WORLD: mapId=", mapId,
|
LOG_WARNING("SMSG_NEW_WORLD: mapId=", mapId,
|
||||||
" pos=(", serverX, ", ", serverY, ", ", serverZ, ")",
|
" pos=(", serverX, ", ", serverY, ", ", serverZ, ")",
|
||||||
" orient=", orientation);
|
" orient=", orientation);
|
||||||
|
|
||||||
|
|
@ -12032,6 +12230,8 @@ void GameHandler::handleNewWorld(network::Packet& packet) {
|
||||||
worldStates_.clear();
|
worldStates_.clear();
|
||||||
worldStateMapId_ = mapId;
|
worldStateMapId_ = mapId;
|
||||||
worldStateZoneId_ = 0;
|
worldStateZoneId_ = 0;
|
||||||
|
activeAreaTriggers_.clear();
|
||||||
|
areaTriggerCheckTimer_ = -5.0f; // 5-second cooldown after map transfer
|
||||||
stopAutoAttack();
|
stopAutoAttack();
|
||||||
casting = false;
|
casting = false;
|
||||||
currentCastSpellId = 0;
|
currentCastSpellId = 0;
|
||||||
|
|
|
||||||
110
src/pipeline/wdt_loader.cpp
Normal file
110
src/pipeline/wdt_loader.cpp
Normal file
|
|
@ -0,0 +1,110 @@
|
||||||
|
#include "pipeline/wdt_loader.hpp"
|
||||||
|
#include "core/logger.hpp"
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
namespace wowee {
|
||||||
|
namespace pipeline {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
uint32_t readU32(const uint8_t* data, size_t offset) {
|
||||||
|
uint32_t v;
|
||||||
|
std::memcpy(&v, data + offset, 4);
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t readU16(const uint8_t* data, size_t offset) {
|
||||||
|
uint16_t v;
|
||||||
|
std::memcpy(&v, data + offset, 2);
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
float readF32(const uint8_t* data, size_t offset) {
|
||||||
|
float v;
|
||||||
|
std::memcpy(&v, data + offset, 4);
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Chunk magic constants (little-endian)
|
||||||
|
constexpr uint32_t MVER = 0x5245564D; // "REVM"
|
||||||
|
constexpr uint32_t MPHD = 0x4448504D; // "DHPM"
|
||||||
|
constexpr uint32_t MAIN = 0x4E49414D; // "NIAM"
|
||||||
|
constexpr uint32_t MWMO = 0x4F4D574D; // "OMWM"
|
||||||
|
constexpr uint32_t MODF = 0x46444F4D; // "FDOM"
|
||||||
|
|
||||||
|
} // anonymous namespace
|
||||||
|
|
||||||
|
WDTInfo parseWDT(const std::vector<uint8_t>& data) {
|
||||||
|
WDTInfo info;
|
||||||
|
|
||||||
|
if (data.size() < 8) {
|
||||||
|
LOG_WARNING("WDT data too small (", data.size(), " bytes)");
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t offset = 0;
|
||||||
|
|
||||||
|
while (offset + 8 <= data.size()) {
|
||||||
|
uint32_t magic = readU32(data.data(), offset);
|
||||||
|
uint32_t chunkSize = readU32(data.data(), offset + 4);
|
||||||
|
|
||||||
|
if (offset + 8 + chunkSize > data.size()) {
|
||||||
|
LOG_WARNING("WDT chunk extends beyond file at offset ", offset);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const uint8_t* chunkData = data.data() + offset + 8;
|
||||||
|
|
||||||
|
if (magic == MVER) {
|
||||||
|
if (chunkSize >= 4) {
|
||||||
|
uint32_t version = readU32(chunkData, 0);
|
||||||
|
LOG_DEBUG("WDT version: ", version);
|
||||||
|
}
|
||||||
|
} else if (magic == MPHD) {
|
||||||
|
if (chunkSize >= 4) {
|
||||||
|
info.mphdFlags = readU32(chunkData, 0);
|
||||||
|
LOG_DEBUG("WDT MPHD flags: 0x", std::hex, info.mphdFlags, std::dec);
|
||||||
|
}
|
||||||
|
} else if (magic == MWMO) {
|
||||||
|
// Null-terminated WMO path string(s)
|
||||||
|
if (chunkSize > 0) {
|
||||||
|
const char* str = reinterpret_cast<const char*>(chunkData);
|
||||||
|
size_t len = std::strlen(str);
|
||||||
|
if (len > 0) {
|
||||||
|
info.rootWMOPath = std::string(str, len);
|
||||||
|
LOG_DEBUG("WDT root WMO: ", info.rootWMOPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (magic == MODF) {
|
||||||
|
// MODF entry is 64 bytes (same layout as ADT MODF)
|
||||||
|
if (chunkSize >= 64) {
|
||||||
|
// nameId at offset 0 (unused for WDT — path comes from MWMO)
|
||||||
|
// uniqueId at offset 4
|
||||||
|
info.position[0] = readF32(chunkData, 8);
|
||||||
|
info.position[1] = readF32(chunkData, 12);
|
||||||
|
info.position[2] = readF32(chunkData, 16);
|
||||||
|
info.rotation[0] = readF32(chunkData, 20);
|
||||||
|
info.rotation[1] = readF32(chunkData, 24);
|
||||||
|
info.rotation[2] = readF32(chunkData, 28);
|
||||||
|
// extents at 32-55
|
||||||
|
info.flags = readU16(chunkData, 56);
|
||||||
|
info.doodadSet = readU16(chunkData, 58);
|
||||||
|
LOG_DEBUG("WDT MODF placement: pos=(", info.position[0], ", ",
|
||||||
|
info.position[1], ", ", info.position[2], ") rot=(",
|
||||||
|
info.rotation[0], ", ", info.rotation[1], ", ",
|
||||||
|
info.rotation[2], ") doodadSet=", info.doodadSet);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
offset += 8 + chunkSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_WARNING("WDT parse result: mphdFlags=0x", std::hex, info.mphdFlags, std::dec,
|
||||||
|
" isWMOOnly=", info.isWMOOnly(),
|
||||||
|
" rootWMO='", info.rootWMOPath, "'");
|
||||||
|
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace pipeline
|
||||||
|
} // namespace wowee
|
||||||
|
|
@ -2765,6 +2765,12 @@ void GameScreen::sendChatMessage(game::GameHandler& gameHandler) {
|
||||||
isChannelCommand = true;
|
isChannelCommand = true;
|
||||||
switchChatType = 9;
|
switchChatType = 9;
|
||||||
} else if (cmdLower == "join") {
|
} else if (cmdLower == "join") {
|
||||||
|
// /join with no args: accept pending BG invite if any
|
||||||
|
if (spacePos == std::string::npos && gameHandler.hasPendingBgInvite()) {
|
||||||
|
gameHandler.acceptBattlefield();
|
||||||
|
chatInputBuffer[0] = '\0';
|
||||||
|
return;
|
||||||
|
}
|
||||||
// /join ChannelName [password]
|
// /join ChannelName [password]
|
||||||
if (spacePos != std::string::npos) {
|
if (spacePos != std::string::npos) {
|
||||||
std::string rest = command.substr(spacePos + 1);
|
std::string rest = command.substr(spacePos + 1);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue