mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-05-03 08:03:50 +00:00
fix: stabilize turtle world entry session handling
This commit is contained in:
parent
4dba20b757
commit
b0fafe5efa
20 changed files with 2283 additions and 1380 deletions
|
|
@ -249,6 +249,7 @@
|
||||||
"SMSG_BATTLEGROUND_PLAYER_JOINED": "0x2EC",
|
"SMSG_BATTLEGROUND_PLAYER_JOINED": "0x2EC",
|
||||||
"SMSG_BATTLEGROUND_PLAYER_LEFT": "0x2ED",
|
"SMSG_BATTLEGROUND_PLAYER_LEFT": "0x2ED",
|
||||||
"CMSG_BATTLEMASTER_JOIN": "0x2EE",
|
"CMSG_BATTLEMASTER_JOIN": "0x2EE",
|
||||||
|
"SMSG_ADDON_INFO": "0x2EF",
|
||||||
"CMSG_EMOTE": "0x102",
|
"CMSG_EMOTE": "0x102",
|
||||||
"SMSG_EMOTE": "0x103",
|
"SMSG_EMOTE": "0x103",
|
||||||
"CMSG_TEXT_EMOTE": "0x104",
|
"CMSG_TEXT_EMOTE": "0x104",
|
||||||
|
|
|
||||||
|
|
@ -224,6 +224,7 @@ private:
|
||||||
std::future<PreparedCreatureModel> future;
|
std::future<PreparedCreatureModel> future;
|
||||||
};
|
};
|
||||||
std::vector<AsyncCreatureLoad> asyncCreatureLoads_;
|
std::vector<AsyncCreatureLoad> asyncCreatureLoads_;
|
||||||
|
std::unordered_set<uint32_t> asyncCreatureDisplayLoads_; // displayIds currently loading in background
|
||||||
void processAsyncCreatureResults(bool unlimited = false);
|
void processAsyncCreatureResults(bool unlimited = false);
|
||||||
static constexpr int MAX_ASYNC_CREATURE_LOADS = 4; // concurrent background loads
|
static constexpr int MAX_ASYNC_CREATURE_LOADS = 4; // concurrent background loads
|
||||||
std::unordered_set<uint64_t> deadCreatureGuids_; // GUIDs that should spawn in corpse/death pose
|
std::unordered_set<uint64_t> deadCreatureGuids_; // GUIDs that should spawn in corpse/death pose
|
||||||
|
|
@ -280,7 +281,17 @@ private:
|
||||||
float z = 0.0f;
|
float z = 0.0f;
|
||||||
float orientation = 0.0f;
|
float orientation = 0.0f;
|
||||||
};
|
};
|
||||||
|
struct PendingTransportRegistration {
|
||||||
|
uint64_t guid = 0;
|
||||||
|
uint32_t entry = 0;
|
||||||
|
uint32_t displayId = 0;
|
||||||
|
float x = 0.0f;
|
||||||
|
float y = 0.0f;
|
||||||
|
float z = 0.0f;
|
||||||
|
float orientation = 0.0f;
|
||||||
|
};
|
||||||
std::unordered_map<uint64_t, PendingTransportMove> pendingTransportMoves_; // guid -> latest pre-registration move
|
std::unordered_map<uint64_t, PendingTransportMove> pendingTransportMoves_; // guid -> latest pre-registration move
|
||||||
|
std::deque<PendingTransportRegistration> pendingTransportRegistrations_;
|
||||||
uint32_t nextGameObjectModelId_ = 20000;
|
uint32_t nextGameObjectModelId_ = 20000;
|
||||||
uint32_t nextGameObjectWmoModelId_ = 40000;
|
uint32_t nextGameObjectWmoModelId_ = 40000;
|
||||||
bool testTransportSetup_ = false;
|
bool testTransportSetup_ = false;
|
||||||
|
|
@ -433,6 +444,7 @@ private:
|
||||||
};
|
};
|
||||||
std::vector<PendingTransportDoodadBatch> pendingTransportDoodadBatches_;
|
std::vector<PendingTransportDoodadBatch> pendingTransportDoodadBatches_;
|
||||||
static constexpr size_t MAX_TRANSPORT_DOODADS_PER_FRAME = 4;
|
static constexpr size_t MAX_TRANSPORT_DOODADS_PER_FRAME = 4;
|
||||||
|
void processPendingTransportRegistrations();
|
||||||
void processPendingTransportDoodads();
|
void processPendingTransportDoodads();
|
||||||
|
|
||||||
// Quest marker billboard sprites (above NPCs)
|
// Quest marker billboard sprites (above NPCs)
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
#include "game/inventory.hpp"
|
#include "game/inventory.hpp"
|
||||||
#include "game/spell_defines.hpp"
|
#include "game/spell_defines.hpp"
|
||||||
#include "game/group_defines.hpp"
|
#include "game/group_defines.hpp"
|
||||||
|
#include "network/packet.hpp"
|
||||||
#include <glm/glm.hpp>
|
#include <glm/glm.hpp>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
@ -2089,6 +2090,15 @@ private:
|
||||||
* Handle incoming packet from world server
|
* Handle incoming packet from world server
|
||||||
*/
|
*/
|
||||||
void handlePacket(network::Packet& packet);
|
void handlePacket(network::Packet& packet);
|
||||||
|
void enqueueIncomingPacket(const network::Packet& packet);
|
||||||
|
void enqueueIncomingPacketFront(network::Packet&& packet);
|
||||||
|
void processQueuedIncomingPackets();
|
||||||
|
void enqueueUpdateObjectWork(UpdateObjectData&& data);
|
||||||
|
void processPendingUpdateObjectWork(const std::chrono::steady_clock::time_point& start,
|
||||||
|
float budgetMs);
|
||||||
|
void processOutOfRangeObjects(const std::vector<uint64_t>& guids);
|
||||||
|
void applyUpdateObjectBlock(const UpdateBlock& block, bool& newItemCreated);
|
||||||
|
void finalizeUpdateObjectBatch(bool newItemCreated);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle SMSG_AUTH_CHALLENGE from server
|
* Handle SMSG_AUTH_CHALLENGE from server
|
||||||
|
|
@ -2413,6 +2423,14 @@ private:
|
||||||
|
|
||||||
// Network
|
// Network
|
||||||
std::unique_ptr<network::WorldSocket> socket;
|
std::unique_ptr<network::WorldSocket> socket;
|
||||||
|
std::deque<network::Packet> pendingIncomingPackets_;
|
||||||
|
struct PendingUpdateObjectWork {
|
||||||
|
UpdateObjectData data;
|
||||||
|
size_t nextBlockIndex = 0;
|
||||||
|
bool outOfRangeProcessed = false;
|
||||||
|
bool newItemCreated = false;
|
||||||
|
};
|
||||||
|
std::deque<PendingUpdateObjectWork> pendingUpdateObjectWork_;
|
||||||
|
|
||||||
// State
|
// State
|
||||||
WorldState state = WorldState::DISCONNECTED;
|
WorldState state = WorldState::DISCONNECTED;
|
||||||
|
|
|
||||||
|
|
@ -49,12 +49,14 @@ public:
|
||||||
/** Number of mapped opcodes. */
|
/** Number of mapped opcodes. */
|
||||||
size_t size() const { return logicalToWire_.size(); }
|
size_t size() const { return logicalToWire_.size(); }
|
||||||
|
|
||||||
|
/** Get canonical enum name for a logical opcode. */
|
||||||
|
static const char* logicalToName(LogicalOpcode op);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
std::unordered_map<uint16_t, uint16_t> logicalToWire_; // LogicalOpcode → wire
|
std::unordered_map<uint16_t, uint16_t> logicalToWire_; // LogicalOpcode → wire
|
||||||
std::unordered_map<uint16_t, uint16_t> wireToLogical_; // wire → LogicalOpcode
|
std::unordered_map<uint16_t, uint16_t> wireToLogical_; // wire → LogicalOpcode
|
||||||
|
|
||||||
static std::optional<LogicalOpcode> nameToLogical(const std::string& name);
|
static std::optional<LogicalOpcode> nameToLogical(const std::string& name);
|
||||||
static const char* logicalToName(LogicalOpcode op);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -451,6 +451,7 @@ public:
|
||||||
class TurtlePacketParsers : public ClassicPacketParsers {
|
class TurtlePacketParsers : public ClassicPacketParsers {
|
||||||
public:
|
public:
|
||||||
uint8_t movementFlags2Size() const override { return 0; }
|
uint8_t movementFlags2Size() const override { return 0; }
|
||||||
|
bool parseUpdateObject(network::Packet& packet, UpdateObjectData& data) override;
|
||||||
bool parseMovementBlock(network::Packet& packet, UpdateBlock& block) override;
|
bool parseMovementBlock(network::Packet& packet, UpdateBlock& block) override;
|
||||||
bool parseMonsterMove(network::Packet& packet, MonsterMoveData& data) override;
|
bool parseMonsterMove(network::Packet& packet, MonsterMoveData& data) override;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,13 @@
|
||||||
#include "auth/vanilla_crypt.hpp"
|
#include "auth/vanilla_crypt.hpp"
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
#include <deque>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
#include <chrono>
|
||||||
|
#include <thread>
|
||||||
|
#include <mutex>
|
||||||
|
#include <atomic>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
namespace wowee {
|
namespace wowee {
|
||||||
namespace network {
|
namespace network {
|
||||||
|
|
@ -66,6 +72,8 @@ public:
|
||||||
*/
|
*/
|
||||||
void initEncryption(const std::vector<uint8_t>& sessionKey, uint32_t build = 12340);
|
void initEncryption(const std::vector<uint8_t>& sessionKey, uint32_t build = 12340);
|
||||||
|
|
||||||
|
void tracePacketsFor(std::chrono::milliseconds duration, const std::string& reason);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if header encryption is enabled
|
* Check if header encryption is enabled
|
||||||
*/
|
*/
|
||||||
|
|
@ -76,11 +84,23 @@ private:
|
||||||
* Try to parse complete packets from receive buffer
|
* Try to parse complete packets from receive buffer
|
||||||
*/
|
*/
|
||||||
void tryParsePackets();
|
void tryParsePackets();
|
||||||
|
void pumpNetworkIO();
|
||||||
|
void dispatchQueuedPackets();
|
||||||
|
void asyncPumpLoop();
|
||||||
|
void startAsyncPump();
|
||||||
|
void stopAsyncPump();
|
||||||
|
void closeSocketNoJoin();
|
||||||
|
|
||||||
socket_t sockfd = INVALID_SOCK;
|
socket_t sockfd = INVALID_SOCK;
|
||||||
bool connected = false;
|
bool connected = false;
|
||||||
bool encryptionEnabled = false;
|
bool encryptionEnabled = false;
|
||||||
bool useVanillaCrypt = false; // true = XOR cipher, false = RC4
|
bool useVanillaCrypt = false; // true = XOR cipher, false = RC4
|
||||||
|
bool useAsyncPump_ = true;
|
||||||
|
std::thread asyncPumpThread_;
|
||||||
|
std::atomic<bool> asyncPumpStop_{false};
|
||||||
|
std::atomic<bool> asyncPumpRunning_{false};
|
||||||
|
mutable std::mutex ioMutex_;
|
||||||
|
mutable std::mutex callbackMutex_;
|
||||||
|
|
||||||
// WotLK RC4 ciphers for header encryption/decryption
|
// WotLK RC4 ciphers for header encryption/decryption
|
||||||
auth::RC4 encryptCipher;
|
auth::RC4 encryptCipher;
|
||||||
|
|
@ -94,6 +114,8 @@ private:
|
||||||
size_t receiveReadOffset_ = 0;
|
size_t receiveReadOffset_ = 0;
|
||||||
// Optional reused packet queue (feature-gated) to reduce per-update allocations.
|
// Optional reused packet queue (feature-gated) to reduce per-update allocations.
|
||||||
std::vector<Packet> parsedPacketsScratch_;
|
std::vector<Packet> parsedPacketsScratch_;
|
||||||
|
// Parsed packets waiting for callback dispatch; drained with a strict per-update budget.
|
||||||
|
std::deque<Packet> pendingPacketCallbacks_;
|
||||||
|
|
||||||
// Runtime-gated network optimization toggles (default off).
|
// Runtime-gated network optimization toggles (default off).
|
||||||
bool useFastRecvAppend_ = false;
|
bool useFastRecvAppend_ = false;
|
||||||
|
|
@ -105,6 +127,9 @@ private:
|
||||||
|
|
||||||
// Debug-only tracing window for post-auth packet framing verification.
|
// Debug-only tracing window for post-auth packet framing verification.
|
||||||
int headerTracePacketsLeft = 0;
|
int headerTracePacketsLeft = 0;
|
||||||
|
std::chrono::steady_clock::time_point packetTraceStart_{};
|
||||||
|
std::chrono::steady_clock::time_point packetTraceUntil_{};
|
||||||
|
std::string packetTraceReason_;
|
||||||
|
|
||||||
// Packet callback
|
// Packet callback
|
||||||
std::function<void(const Packet&)> packetCallback;
|
std::function<void(const Packet&)> packetCallback;
|
||||||
|
|
|
||||||
|
|
@ -296,7 +296,9 @@ private:
|
||||||
std::unordered_map<VkTexture*, bool> textureColorKeyBlackByPtr_;
|
std::unordered_map<VkTexture*, bool> textureColorKeyBlackByPtr_;
|
||||||
std::unordered_map<std::string, VkTexture*> compositeCache_; // key → texture for reuse
|
std::unordered_map<std::string, VkTexture*> compositeCache_; // key → texture for reuse
|
||||||
std::unordered_set<std::string> failedTextureCache_; // negative cache for budget exhaustion
|
std::unordered_set<std::string> failedTextureCache_; // negative cache for budget exhaustion
|
||||||
|
std::unordered_map<std::string, uint64_t> failedTextureRetryAt_;
|
||||||
std::unordered_set<std::string> loggedTextureLoadFails_; // dedup warning logs
|
std::unordered_set<std::string> loggedTextureLoadFails_; // dedup warning logs
|
||||||
|
uint64_t textureLookupSerial_ = 0;
|
||||||
size_t textureCacheBytes_ = 0;
|
size_t textureCacheBytes_ = 0;
|
||||||
uint64_t textureCacheCounter_ = 0;
|
uint64_t textureCacheCounter_ = 0;
|
||||||
size_t textureCacheBudgetBytes_ = 1024ull * 1024 * 1024;
|
size_t textureCacheBudgetBytes_ = 1024ull * 1024 * 1024;
|
||||||
|
|
|
||||||
|
|
@ -477,7 +477,9 @@ private:
|
||||||
uint64_t textureCacheCounter_ = 0;
|
uint64_t textureCacheCounter_ = 0;
|
||||||
size_t textureCacheBudgetBytes_ = 2048ull * 1024 * 1024;
|
size_t textureCacheBudgetBytes_ = 2048ull * 1024 * 1024;
|
||||||
std::unordered_set<std::string> failedTextureCache_;
|
std::unordered_set<std::string> failedTextureCache_;
|
||||||
|
std::unordered_map<std::string, uint64_t> failedTextureRetryAt_;
|
||||||
std::unordered_set<std::string> loggedTextureLoadFails_;
|
std::unordered_set<std::string> loggedTextureLoadFails_;
|
||||||
|
uint64_t textureLookupSerial_ = 0;
|
||||||
uint32_t textureBudgetRejectWarnings_ = 0;
|
uint32_t textureBudgetRejectWarnings_ = 0;
|
||||||
std::unique_ptr<VkTexture> whiteTexture_;
|
std::unique_ptr<VkTexture> whiteTexture_;
|
||||||
std::unique_ptr<VkTexture> glowTexture_;
|
std::unique_ptr<VkTexture> glowTexture_;
|
||||||
|
|
|
||||||
|
|
@ -671,7 +671,9 @@ private:
|
||||||
uint64_t textureCacheCounter_ = 0;
|
uint64_t textureCacheCounter_ = 0;
|
||||||
size_t textureCacheBudgetBytes_ = 8192ull * 1024 * 1024; // 8 GB default, overridden at init
|
size_t textureCacheBudgetBytes_ = 8192ull * 1024 * 1024; // 8 GB default, overridden at init
|
||||||
std::unordered_set<std::string> failedTextureCache_;
|
std::unordered_set<std::string> failedTextureCache_;
|
||||||
|
std::unordered_map<std::string, uint64_t> failedTextureRetryAt_;
|
||||||
std::unordered_set<std::string> loggedTextureLoadFails_;
|
std::unordered_set<std::string> loggedTextureLoadFails_;
|
||||||
|
uint64_t textureLookupSerial_ = 0;
|
||||||
uint32_t textureBudgetRejectWarnings_ = 0;
|
uint32_t textureBudgetRejectWarnings_ = 0;
|
||||||
|
|
||||||
// Default white texture
|
// Default white texture
|
||||||
|
|
|
||||||
|
|
@ -824,6 +824,7 @@ void Application::logoutToLogin() {
|
||||||
if (load.future.valid()) load.future.wait();
|
if (load.future.valid()) load.future.wait();
|
||||||
}
|
}
|
||||||
asyncCreatureLoads_.clear();
|
asyncCreatureLoads_.clear();
|
||||||
|
asyncCreatureDisplayLoads_.clear();
|
||||||
|
|
||||||
// --- Creature spawn queues ---
|
// --- Creature spawn queues ---
|
||||||
pendingCreatureSpawns_.clear();
|
pendingCreatureSpawns_.clear();
|
||||||
|
|
@ -842,6 +843,7 @@ void Application::logoutToLogin() {
|
||||||
gameObjectInstances_.clear();
|
gameObjectInstances_.clear();
|
||||||
pendingGameObjectSpawns_.clear();
|
pendingGameObjectSpawns_.clear();
|
||||||
pendingTransportMoves_.clear();
|
pendingTransportMoves_.clear();
|
||||||
|
pendingTransportRegistrations_.clear();
|
||||||
pendingTransportDoodadBatches_.clear();
|
pendingTransportDoodadBatches_.clear();
|
||||||
|
|
||||||
world.reset();
|
world.reset();
|
||||||
|
|
@ -1053,6 +1055,7 @@ void Application::update(float deltaTime) {
|
||||||
updateCheckpoint = "in_game: gameobject/transport queues";
|
updateCheckpoint = "in_game: gameobject/transport queues";
|
||||||
runInGameStage("gameobject/transport queues", [&] {
|
runInGameStage("gameobject/transport queues", [&] {
|
||||||
processGameObjectSpawnQueue();
|
processGameObjectSpawnQueue();
|
||||||
|
processPendingTransportRegistrations();
|
||||||
processPendingTransportDoodads();
|
processPendingTransportDoodads();
|
||||||
});
|
});
|
||||||
inGameStep = "pending mount";
|
inGameStep = "pending mount";
|
||||||
|
|
@ -1725,6 +1728,19 @@ void Application::update(float deltaTime) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (pendingWorldEntry_ && !loadingWorld_ && state != AppState::DISCONNECTED) {
|
||||||
|
auto entry = *pendingWorldEntry_;
|
||||||
|
pendingWorldEntry_.reset();
|
||||||
|
worldEntryMovementGraceTimer_ = 2.0f;
|
||||||
|
taxiLandingClampTimer_ = 0.0f;
|
||||||
|
lastTaxiFlight_ = false;
|
||||||
|
if (renderer && renderer->getCameraController()) {
|
||||||
|
renderer->getCameraController()->clearMovementInputs();
|
||||||
|
renderer->getCameraController()->suppressMovementFor(1.0f);
|
||||||
|
}
|
||||||
|
loadOnlineWorldTerrain(entry.mapId, entry.x, entry.y, entry.z);
|
||||||
|
}
|
||||||
|
|
||||||
// Update renderer (camera, etc.) only when in-game
|
// Update renderer (camera, etc.) only when in-game
|
||||||
updateCheckpoint = "renderer update";
|
updateCheckpoint = "renderer update";
|
||||||
if (renderer && state == AppState::IN_GAME) {
|
if (renderer && state == AppState::IN_GAME) {
|
||||||
|
|
@ -2025,24 +2041,19 @@ void Application::setupUICallbacks() {
|
||||||
|
|
||||||
// If a world load is already in progress (re-entrant call from
|
// If a world load is already in progress (re-entrant call from
|
||||||
// gameHandler->update() processing SMSG_NEW_WORLD during warmup),
|
// gameHandler->update() processing SMSG_NEW_WORLD during warmup),
|
||||||
// defer this entry. The current load will pick it up when it finishes.
|
// defer this entry. The current load will pick it up when it finishes.
|
||||||
if (loadingWorld_) {
|
if (loadingWorld_) {
|
||||||
LOG_WARNING("World entry deferred: map ", mapId, " while loading (will process after current load)");
|
LOG_WARNING("World entry deferred: map ", mapId, " while loading (will process after current load)");
|
||||||
pendingWorldEntry_ = {mapId, x, y, z};
|
pendingWorldEntry_ = {mapId, x, y, z};
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
worldEntryMovementGraceTimer_ = 2.0f;
|
// Full world loads are expensive and `loadOnlineWorldTerrain()` itself
|
||||||
taxiLandingClampTimer_ = 0.0f;
|
// drives `gameHandler->update()` during warmup. Queue the load here so
|
||||||
lastTaxiFlight_ = false;
|
// it runs after the current packet handler returns instead of recursing
|
||||||
// Stop any movement that was active before the teleport
|
// from `SMSG_LOGIN_VERIFY_WORLD` / `SMSG_NEW_WORLD`.
|
||||||
if (renderer && renderer->getCameraController()) {
|
LOG_WARNING("Queued world entry: map ", mapId, " pos=(", x, ", ", y, ", ", z, ")");
|
||||||
renderer->getCameraController()->clearMovementInputs();
|
pendingWorldEntry_ = {mapId, x, y, z};
|
||||||
renderer->getCameraController()->suppressMovementFor(1.0f);
|
|
||||||
}
|
|
||||||
loadOnlineWorldTerrain(mapId, x, y, z);
|
|
||||||
// loadedMapId_ is set inside loadOnlineWorldTerrain (including
|
|
||||||
// any deferred entries it processes), so we must NOT override it here.
|
|
||||||
});
|
});
|
||||||
|
|
||||||
auto sampleBestFloorAt = [this](float x, float y, float probeZ) -> std::optional<float> {
|
auto sampleBestFloorAt = [this](float x, float y, float probeZ) -> std::optional<float> {
|
||||||
|
|
@ -2712,133 +2723,28 @@ void Application::setupUICallbacks() {
|
||||||
|
|
||||||
// Transport spawn callback (online mode) - register transports with TransportManager
|
// Transport spawn callback (online mode) - register transports with TransportManager
|
||||||
gameHandler->setTransportSpawnCallback([this](uint64_t guid, uint32_t entry, uint32_t displayId, float x, float y, float z, float orientation) {
|
gameHandler->setTransportSpawnCallback([this](uint64_t guid, uint32_t entry, uint32_t displayId, float x, float y, float z, float orientation) {
|
||||||
auto* transportManager = gameHandler->getTransportManager();
|
if (!renderer) return;
|
||||||
if (!transportManager || !renderer) return;
|
|
||||||
|
|
||||||
// Get the WMO instance ID from the GameObject spawn
|
// Get the GameObject instance now so late queue processing can rely on stable IDs.
|
||||||
auto it = gameObjectInstances_.find(guid);
|
auto it = gameObjectInstances_.find(guid);
|
||||||
if (it == gameObjectInstances_.end()) {
|
if (it == gameObjectInstances_.end()) {
|
||||||
LOG_WARNING("Transport spawn callback: GameObject instance not found for GUID 0x", std::hex, guid, std::dec);
|
LOG_WARNING("Transport spawn callback: GameObject instance not found for GUID 0x", std::hex, guid, std::dec);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint32_t wmoInstanceId = it->second.instanceId;
|
auto pendingIt = std::find_if(
|
||||||
LOG_WARNING("Registering server transport: GUID=0x", std::hex, guid, std::dec,
|
pendingTransportRegistrations_.begin(), pendingTransportRegistrations_.end(),
|
||||||
" entry=", entry, " displayId=", displayId, " wmoInstance=", wmoInstanceId,
|
[guid](const PendingTransportRegistration& pending) { return pending.guid == guid; });
|
||||||
" pos=(", x, ", ", y, ", ", z, ")");
|
if (pendingIt != pendingTransportRegistrations_.end()) {
|
||||||
|
pendingIt->entry = entry;
|
||||||
// TransportAnimation.dbc is indexed by GameObject entry
|
pendingIt->displayId = displayId;
|
||||||
uint32_t pathId = entry;
|
pendingIt->x = x;
|
||||||
const bool preferServerData = gameHandler && gameHandler->hasServerTransportUpdate(guid);
|
pendingIt->y = y;
|
||||||
|
pendingIt->z = z;
|
||||||
bool clientAnim = transportManager->isClientSideAnimation();
|
pendingIt->orientation = orientation;
|
||||||
LOG_DEBUG("Transport spawn callback: clientAnimation=", clientAnim,
|
|
||||||
" guid=0x", std::hex, guid, std::dec, " entry=", entry, " pathId=", pathId,
|
|
||||||
" preferServer=", preferServerData);
|
|
||||||
|
|
||||||
// Coordinates are already canonical (converted in game_handler.cpp when entity was created)
|
|
||||||
glm::vec3 canonicalSpawnPos(x, y, z);
|
|
||||||
|
|
||||||
// Check if we have a real path from TransportAnimation.dbc (indexed by entry).
|
|
||||||
// AzerothCore transport entries are not always 1:1 with DBC path ids.
|
|
||||||
const bool shipOrZeppelinDisplay =
|
|
||||||
(displayId == 3015 || displayId == 3031 || displayId == 7546 ||
|
|
||||||
displayId == 7446 || displayId == 1587 || displayId == 2454 ||
|
|
||||||
displayId == 807 || displayId == 808);
|
|
||||||
bool hasUsablePath = transportManager->hasPathForEntry(entry);
|
|
||||||
if (shipOrZeppelinDisplay) {
|
|
||||||
// For true transports, reject tiny XY tracks that effectively look stationary.
|
|
||||||
hasUsablePath = transportManager->hasUsableMovingPathForEntry(entry, 25.0f);
|
|
||||||
}
|
|
||||||
|
|
||||||
LOG_WARNING("Transport path check: entry=", entry, " hasUsablePath=", hasUsablePath,
|
|
||||||
" preferServerData=", preferServerData, " shipOrZepDisplay=", shipOrZeppelinDisplay);
|
|
||||||
|
|
||||||
if (preferServerData) {
|
|
||||||
// Strict server-authoritative mode: do not infer/remap fallback routes.
|
|
||||||
if (!hasUsablePath) {
|
|
||||||
std::vector<glm::vec3> path = { canonicalSpawnPos };
|
|
||||||
transportManager->loadPathFromNodes(pathId, path, false, 0.0f);
|
|
||||||
LOG_WARNING("Server-first strict registration: stationary fallback for GUID 0x",
|
|
||||||
std::hex, guid, std::dec, " entry=", entry);
|
|
||||||
} else {
|
|
||||||
LOG_WARNING("Server-first transport registration: using entry DBC path for entry ", entry);
|
|
||||||
}
|
|
||||||
} else if (!hasUsablePath) {
|
|
||||||
// Remap/infer path by spawn position when entry doesn't map 1:1 to DBC ids.
|
|
||||||
// For elevators (TB lift platforms), we must allow z-only paths here.
|
|
||||||
bool allowZOnly = (displayId == 455 || displayId == 462);
|
|
||||||
uint32_t inferredPath = transportManager->inferDbcPathForSpawn(
|
|
||||||
canonicalSpawnPos, 1200.0f, allowZOnly);
|
|
||||||
if (inferredPath != 0) {
|
|
||||||
pathId = inferredPath;
|
|
||||||
LOG_WARNING("Using inferred transport path ", pathId, " for entry ", entry);
|
|
||||||
} else {
|
|
||||||
uint32_t remappedPath = transportManager->pickFallbackMovingPath(entry, displayId);
|
|
||||||
if (remappedPath != 0) {
|
|
||||||
pathId = remappedPath;
|
|
||||||
LOG_WARNING("Using remapped fallback transport path ", pathId,
|
|
||||||
" for entry ", entry, " displayId=", displayId,
|
|
||||||
" (usableEntryPath=", transportManager->hasPathForEntry(entry), ")");
|
|
||||||
} else {
|
|
||||||
LOG_WARNING("No TransportAnimation.dbc path for entry ", entry,
|
|
||||||
" - transport will be stationary");
|
|
||||||
|
|
||||||
// Fallback: Stationary at spawn point (wait for server to send real position)
|
|
||||||
std::vector<glm::vec3> path = { canonicalSpawnPos };
|
|
||||||
transportManager->loadPathFromNodes(pathId, path, false, 0.0f);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
LOG_WARNING("Using real transport path from TransportAnimation.dbc for entry ", entry);
|
pendingTransportRegistrations_.push_back(
|
||||||
}
|
PendingTransportRegistration{guid, entry, displayId, x, y, z, orientation});
|
||||||
|
|
||||||
// Register the transport with spawn position (prevents rendering at origin until server update)
|
|
||||||
transportManager->registerTransport(guid, wmoInstanceId, pathId, canonicalSpawnPos, entry);
|
|
||||||
|
|
||||||
// Mark M2 transports (e.g. Deeprun Tram cars) so TransportManager uses M2Renderer
|
|
||||||
if (!it->second.isWmo) {
|
|
||||||
if (auto* tr = transportManager->getTransport(guid)) {
|
|
||||||
tr->isM2 = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Server-authoritative movement - set initial position from spawn data
|
|
||||||
glm::vec3 canonicalPos(x, y, z);
|
|
||||||
transportManager->updateServerTransport(guid, canonicalPos, orientation);
|
|
||||||
|
|
||||||
// If a move packet arrived before registration completed, replay latest now.
|
|
||||||
auto pendingIt = pendingTransportMoves_.find(guid);
|
|
||||||
if (pendingIt != pendingTransportMoves_.end()) {
|
|
||||||
const PendingTransportMove pending = pendingIt->second;
|
|
||||||
transportManager->updateServerTransport(guid, glm::vec3(pending.x, pending.y, pending.z), pending.orientation);
|
|
||||||
LOG_DEBUG("Replayed queued transport move for GUID=0x", std::hex, guid, std::dec,
|
|
||||||
" pos=(", pending.x, ", ", pending.y, ", ", pending.z, ") orientation=", pending.orientation);
|
|
||||||
pendingTransportMoves_.erase(pendingIt);
|
|
||||||
}
|
|
||||||
|
|
||||||
// For MO_TRANSPORT at (0,0,0): check if GO data is already cached with a taxiPathId
|
|
||||||
if (glm::length(canonicalSpawnPos) < 1.0f && gameHandler) {
|
|
||||||
auto goData = gameHandler->getCachedGameObjectInfo(entry);
|
|
||||||
if (goData && goData->type == 15 && goData->hasData && goData->data[0] != 0) {
|
|
||||||
uint32_t taxiPathId = goData->data[0];
|
|
||||||
if (transportManager->hasTaxiPath(taxiPathId)) {
|
|
||||||
transportManager->assignTaxiPathToTransport(entry, taxiPathId);
|
|
||||||
LOG_DEBUG("Assigned cached TaxiPathNode path for MO_TRANSPORT entry=", entry,
|
|
||||||
" taxiPathId=", taxiPathId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (auto* tr = transportManager->getTransport(guid); tr) {
|
|
||||||
LOG_WARNING("Transport registered: guid=0x", std::hex, guid, std::dec,
|
|
||||||
" entry=", entry, " displayId=", displayId,
|
|
||||||
" pathId=", tr->pathId,
|
|
||||||
" mode=", (tr->useClientAnimation ? "client" : "server"),
|
|
||||||
" serverUpdates=", tr->serverUpdateCount);
|
|
||||||
} else {
|
|
||||||
LOG_DEBUG("Transport registered: guid=0x", std::hex, guid, std::dec,
|
|
||||||
" entry=", entry, " displayId=", displayId, " (TransportManager instance missing)");
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -2853,6 +2759,15 @@ void Application::setupUICallbacks() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auto pendingRegIt = std::find_if(
|
||||||
|
pendingTransportRegistrations_.begin(), pendingTransportRegistrations_.end(),
|
||||||
|
[guid](const PendingTransportRegistration& pending) { return pending.guid == guid; });
|
||||||
|
if (pendingRegIt != pendingTransportRegistrations_.end()) {
|
||||||
|
pendingTransportMoves_[guid] = PendingTransportMove{x, y, z, orientation};
|
||||||
|
LOG_DEBUG("Queued transport move for pending registration GUID=0x", std::hex, guid, std::dec);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Check if transport exists - if not, treat this as a late spawn (reconnection/server restart)
|
// Check if transport exists - if not, treat this as a late spawn (reconnection/server restart)
|
||||||
if (!transportManager->getTransport(guid)) {
|
if (!transportManager->getTransport(guid)) {
|
||||||
LOG_DEBUG("Received position update for unregistered transport 0x", std::hex, guid, std::dec,
|
LOG_DEBUG("Received position update for unregistered transport 0x", std::hex, guid, std::dec,
|
||||||
|
|
@ -4155,6 +4070,7 @@ void Application::loadOnlineWorldTerrain(uint32_t mapId, float x, float y, float
|
||||||
deferredEquipmentQueue_.clear();
|
deferredEquipmentQueue_.clear();
|
||||||
pendingGameObjectSpawns_.clear();
|
pendingGameObjectSpawns_.clear();
|
||||||
pendingTransportMoves_.clear();
|
pendingTransportMoves_.clear();
|
||||||
|
pendingTransportRegistrations_.clear();
|
||||||
pendingTransportDoodadBatches_.clear();
|
pendingTransportDoodadBatches_.clear();
|
||||||
|
|
||||||
if (renderer) {
|
if (renderer) {
|
||||||
|
|
@ -4210,6 +4126,7 @@ void Application::loadOnlineWorldTerrain(uint32_t mapId, float x, float y, float
|
||||||
if (load.future.valid()) load.future.wait();
|
if (load.future.valid()) load.future.wait();
|
||||||
}
|
}
|
||||||
asyncCreatureLoads_.clear();
|
asyncCreatureLoads_.clear();
|
||||||
|
asyncCreatureDisplayLoads_.clear();
|
||||||
|
|
||||||
playerInstances_.clear();
|
playerInstances_.clear();
|
||||||
onlinePlayerAppearance_.clear();
|
onlinePlayerAppearance_.clear();
|
||||||
|
|
@ -4866,25 +4783,23 @@ void Application::loadOnlineWorldTerrain(uint32_t mapId, float x, float y, float
|
||||||
if (world) world->update(1.0f / 60.0f);
|
if (world) world->update(1.0f / 60.0f);
|
||||||
processPlayerSpawnQueue();
|
processPlayerSpawnQueue();
|
||||||
|
|
||||||
// During load screen warmup: lift per-frame budgets so GPU uploads
|
// Keep warmup bounded: unbounded queue draining can stall the main thread
|
||||||
// and spawns happen in bulk while the loading screen is still visible.
|
// long enough to trigger socket timeouts.
|
||||||
processCreatureSpawnQueue(true);
|
processCreatureSpawnQueue(false);
|
||||||
processAsyncNpcCompositeResults(true);
|
processAsyncNpcCompositeResults(false);
|
||||||
// Process equipment queue more aggressively during warmup (multiple per iteration)
|
// Process equipment queue with a small bounded burst during warmup.
|
||||||
for (int i = 0; i < 8 && (!deferredEquipmentQueue_.empty() || !asyncEquipmentLoads_.empty()); i++) {
|
for (int i = 0; i < 2 && (!deferredEquipmentQueue_.empty() || !asyncEquipmentLoads_.empty()); i++) {
|
||||||
processDeferredEquipmentQueue();
|
processDeferredEquipmentQueue();
|
||||||
}
|
}
|
||||||
if (auto* cr = renderer ? renderer->getCharacterRenderer() : nullptr) {
|
if (auto* cr = renderer ? renderer->getCharacterRenderer() : nullptr) {
|
||||||
cr->processPendingNormalMaps(INT_MAX);
|
cr->processPendingNormalMaps(4);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process ALL pending game object spawns.
|
// Keep warmup responsive: process gameobject queue with the same bounded
|
||||||
while (!pendingGameObjectSpawns_.empty()) {
|
// budget logic used in-world instead of draining everything in one tick.
|
||||||
auto& s = pendingGameObjectSpawns_.front();
|
processGameObjectSpawnQueue();
|
||||||
spawnOnlineGameObject(s.guid, s.entry, s.displayId, s.x, s.y, s.z, s.orientation, s.scale);
|
|
||||||
pendingGameObjectSpawns_.erase(pendingGameObjectSpawns_.begin());
|
|
||||||
}
|
|
||||||
|
|
||||||
|
processPendingTransportRegistrations();
|
||||||
processPendingTransportDoodads();
|
processPendingTransportDoodads();
|
||||||
processPendingMount();
|
processPendingMount();
|
||||||
updateQuestMarkers();
|
updateQuestMarkers();
|
||||||
|
|
@ -7437,12 +7352,23 @@ void Application::spawnOnlineGameObject(uint64_t guid, uint32_t entry, uint32_t
|
||||||
|
|
||||||
void Application::processAsyncCreatureResults(bool unlimited) {
|
void Application::processAsyncCreatureResults(bool unlimited) {
|
||||||
// Check completed async model loads and finalize on main thread (GPU upload + instance creation).
|
// Check completed async model loads and finalize on main thread (GPU upload + instance creation).
|
||||||
// Limit GPU model uploads per frame to avoid spikes, but always drain cheap bookkeeping.
|
// Limit GPU model uploads per tick to avoid long main-thread stalls that can starve socket updates.
|
||||||
// In unlimited mode (load screen), process all pending uploads without cap.
|
// Even in unlimited mode (load screen), keep a small cap and budget to prevent multi-second stalls.
|
||||||
static constexpr int kMaxModelUploadsPerFrame = 1;
|
static constexpr int kMaxModelUploadsPerTick = 1;
|
||||||
|
static constexpr int kMaxModelUploadsPerTickWarmup = 1;
|
||||||
|
static constexpr float kFinalizeBudgetMs = 2.0f;
|
||||||
|
static constexpr float kFinalizeBudgetWarmupMs = 2.0f;
|
||||||
|
const int maxUploadsThisTick = unlimited ? kMaxModelUploadsPerTickWarmup : kMaxModelUploadsPerTick;
|
||||||
|
const float budgetMs = unlimited ? kFinalizeBudgetWarmupMs : kFinalizeBudgetMs;
|
||||||
|
const auto tickStart = std::chrono::steady_clock::now();
|
||||||
int modelUploads = 0;
|
int modelUploads = 0;
|
||||||
|
|
||||||
for (auto it = asyncCreatureLoads_.begin(); it != asyncCreatureLoads_.end(); ) {
|
for (auto it = asyncCreatureLoads_.begin(); it != asyncCreatureLoads_.end(); ) {
|
||||||
|
if (std::chrono::duration<float, std::milli>(
|
||||||
|
std::chrono::steady_clock::now() - tickStart).count() >= budgetMs) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
if (!it->future.valid() ||
|
if (!it->future.valid() ||
|
||||||
it->future.wait_for(std::chrono::milliseconds(0)) != std::future_status::ready) {
|
it->future.wait_for(std::chrono::milliseconds(0)) != std::future_status::ready) {
|
||||||
++it;
|
++it;
|
||||||
|
|
@ -7451,12 +7377,13 @@ void Application::processAsyncCreatureResults(bool unlimited) {
|
||||||
|
|
||||||
// Peek: if this result needs a NEW model upload (not cached) and we've hit
|
// Peek: if this result needs a NEW model upload (not cached) and we've hit
|
||||||
// the upload budget, defer to next frame without consuming the future.
|
// the upload budget, defer to next frame without consuming the future.
|
||||||
if (!unlimited && modelUploads >= kMaxModelUploadsPerFrame) {
|
if (modelUploads >= maxUploadsThisTick) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto result = it->future.get();
|
auto result = it->future.get();
|
||||||
it = asyncCreatureLoads_.erase(it);
|
it = asyncCreatureLoads_.erase(it);
|
||||||
|
asyncCreatureDisplayLoads_.erase(result.displayId);
|
||||||
|
|
||||||
if (result.permanent_failure) {
|
if (result.permanent_failure) {
|
||||||
nonRenderableCreatureDisplayIds_.insert(result.displayId);
|
nonRenderableCreatureDisplayIds_.insert(result.displayId);
|
||||||
|
|
@ -7471,6 +7398,27 @@ void Application::processAsyncCreatureResults(bool unlimited) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Another async result may have already uploaded this displayId while this
|
||||||
|
// task was still running; in that case, skip duplicate GPU upload.
|
||||||
|
if (displayIdModelCache_.find(result.displayId) != displayIdModelCache_.end()) {
|
||||||
|
pendingCreatureSpawnGuids_.erase(result.guid);
|
||||||
|
creatureSpawnRetryCounts_.erase(result.guid);
|
||||||
|
if (!creatureInstances_.count(result.guid) &&
|
||||||
|
!creaturePermanentFailureGuids_.count(result.guid)) {
|
||||||
|
PendingCreatureSpawn s{};
|
||||||
|
s.guid = result.guid;
|
||||||
|
s.displayId = result.displayId;
|
||||||
|
s.x = result.x;
|
||||||
|
s.y = result.y;
|
||||||
|
s.z = result.z;
|
||||||
|
s.orientation = result.orientation;
|
||||||
|
s.scale = result.scale;
|
||||||
|
pendingCreatureSpawns_.push_back(s);
|
||||||
|
pendingCreatureSpawnGuids_.insert(result.guid);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// Model parsed on background thread — upload to GPU on main thread.
|
// Model parsed on background thread — upload to GPU on main thread.
|
||||||
auto* charRenderer = renderer ? renderer->getCharacterRenderer() : nullptr;
|
auto* charRenderer = renderer ? renderer->getCharacterRenderer() : nullptr;
|
||||||
if (!charRenderer) {
|
if (!charRenderer) {
|
||||||
|
|
@ -7478,6 +7426,10 @@ void Application::processAsyncCreatureResults(bool unlimited) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Count upload attempts toward the frame budget even if upload fails.
|
||||||
|
// Otherwise repeated failures can consume an unbounded amount of frame time.
|
||||||
|
modelUploads++;
|
||||||
|
|
||||||
// Upload model to GPU (must happen on main thread)
|
// Upload model to GPU (must happen on main thread)
|
||||||
// Use pre-decoded BLP cache to skip main-thread texture decode
|
// Use pre-decoded BLP cache to skip main-thread texture decode
|
||||||
auto uploadStart = std::chrono::steady_clock::now();
|
auto uploadStart = std::chrono::steady_clock::now();
|
||||||
|
|
@ -7504,8 +7456,6 @@ void Application::processAsyncCreatureResults(bool unlimited) {
|
||||||
displayIdPredecodedTextures_[result.displayId] = std::move(result.predecodedTextures);
|
displayIdPredecodedTextures_[result.displayId] = std::move(result.predecodedTextures);
|
||||||
}
|
}
|
||||||
displayIdModelCache_[result.displayId] = result.modelId;
|
displayIdModelCache_[result.displayId] = result.modelId;
|
||||||
modelUploads++;
|
|
||||||
|
|
||||||
pendingCreatureSpawnGuids_.erase(result.guid);
|
pendingCreatureSpawnGuids_.erase(result.guid);
|
||||||
creatureSpawnRetryCounts_.erase(result.guid);
|
creatureSpawnRetryCounts_.erase(result.guid);
|
||||||
|
|
||||||
|
|
@ -7659,6 +7609,14 @@ void Application::processCreatureSpawnQueue(bool unlimited) {
|
||||||
|
|
||||||
// For new models: launch async load on background thread instead of blocking.
|
// For new models: launch async load on background thread instead of blocking.
|
||||||
if (needsNewModel) {
|
if (needsNewModel) {
|
||||||
|
// Keep exactly one background load per displayId. Additional spawns for
|
||||||
|
// the same displayId stay queued and will spawn once cache is populated.
|
||||||
|
if (asyncCreatureDisplayLoads_.count(s.displayId)) {
|
||||||
|
pendingCreatureSpawns_.push_back(s);
|
||||||
|
rotationsLeft--;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const int maxAsync = unlimited ? (MAX_ASYNC_CREATURE_LOADS * 4) : MAX_ASYNC_CREATURE_LOADS;
|
const int maxAsync = unlimited ? (MAX_ASYNC_CREATURE_LOADS * 4) : MAX_ASYNC_CREATURE_LOADS;
|
||||||
if (static_cast<int>(asyncCreatureLoads_.size()) + asyncLaunched >= maxAsync) {
|
if (static_cast<int>(asyncCreatureLoads_.size()) + asyncLaunched >= maxAsync) {
|
||||||
// Too many in-flight — defer to next frame
|
// Too many in-flight — defer to next frame
|
||||||
|
|
@ -7904,6 +7862,7 @@ void Application::processCreatureSpawnQueue(bool unlimited) {
|
||||||
return result;
|
return result;
|
||||||
});
|
});
|
||||||
asyncCreatureLoads_.push_back(std::move(load));
|
asyncCreatureLoads_.push_back(std::move(load));
|
||||||
|
asyncCreatureDisplayLoads_.insert(s.displayId);
|
||||||
asyncLaunched++;
|
asyncLaunched++;
|
||||||
// Don't erase from pendingCreatureSpawnGuids_ — the async result handler will do it
|
// Don't erase from pendingCreatureSpawnGuids_ — the async result handler will do it
|
||||||
rotationsLeft = pendingCreatureSpawns_.size();
|
rotationsLeft = pendingCreatureSpawns_.size();
|
||||||
|
|
@ -8304,6 +8263,151 @@ void Application::processGameObjectSpawnQueue() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Application::processPendingTransportRegistrations() {
|
||||||
|
if (pendingTransportRegistrations_.empty()) return;
|
||||||
|
if (!gameHandler || !renderer) return;
|
||||||
|
|
||||||
|
auto* transportManager = gameHandler->getTransportManager();
|
||||||
|
if (!transportManager) return;
|
||||||
|
|
||||||
|
auto startTime = std::chrono::steady_clock::now();
|
||||||
|
static constexpr int kMaxRegistrationsPerFrame = 2;
|
||||||
|
static constexpr float kRegistrationBudgetMs = 2.0f;
|
||||||
|
int processed = 0;
|
||||||
|
|
||||||
|
for (auto it = pendingTransportRegistrations_.begin();
|
||||||
|
it != pendingTransportRegistrations_.end() && processed < kMaxRegistrationsPerFrame;) {
|
||||||
|
float elapsedMs = std::chrono::duration<float, std::milli>(
|
||||||
|
std::chrono::steady_clock::now() - startTime).count();
|
||||||
|
if (elapsedMs >= kRegistrationBudgetMs) break;
|
||||||
|
|
||||||
|
const PendingTransportRegistration pending = *it;
|
||||||
|
auto goIt = gameObjectInstances_.find(pending.guid);
|
||||||
|
if (goIt == gameObjectInstances_.end()) {
|
||||||
|
it = pendingTransportRegistrations_.erase(it);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (transportManager->getTransport(pending.guid)) {
|
||||||
|
transportManager->updateServerTransport(
|
||||||
|
pending.guid, glm::vec3(pending.x, pending.y, pending.z), pending.orientation);
|
||||||
|
it = pendingTransportRegistrations_.erase(it);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const uint32_t wmoInstanceId = goIt->second.instanceId;
|
||||||
|
LOG_WARNING("Registering server transport: GUID=0x", std::hex, pending.guid, std::dec,
|
||||||
|
" entry=", pending.entry, " displayId=", pending.displayId, " wmoInstance=", wmoInstanceId,
|
||||||
|
" pos=(", pending.x, ", ", pending.y, ", ", pending.z, ")");
|
||||||
|
|
||||||
|
// TransportAnimation.dbc is indexed by GameObject entry.
|
||||||
|
uint32_t pathId = pending.entry;
|
||||||
|
const bool preferServerData = gameHandler->hasServerTransportUpdate(pending.guid);
|
||||||
|
|
||||||
|
bool clientAnim = transportManager->isClientSideAnimation();
|
||||||
|
LOG_DEBUG("Transport spawn callback: clientAnimation=", clientAnim,
|
||||||
|
" guid=0x", std::hex, pending.guid, std::dec,
|
||||||
|
" entry=", pending.entry, " pathId=", pathId,
|
||||||
|
" preferServer=", preferServerData);
|
||||||
|
|
||||||
|
glm::vec3 canonicalSpawnPos(pending.x, pending.y, pending.z);
|
||||||
|
const bool shipOrZeppelinDisplay =
|
||||||
|
(pending.displayId == 3015 || pending.displayId == 3031 || pending.displayId == 7546 ||
|
||||||
|
pending.displayId == 7446 || pending.displayId == 1587 || pending.displayId == 2454 ||
|
||||||
|
pending.displayId == 807 || pending.displayId == 808);
|
||||||
|
bool hasUsablePath = transportManager->hasPathForEntry(pending.entry);
|
||||||
|
if (shipOrZeppelinDisplay) {
|
||||||
|
hasUsablePath = transportManager->hasUsableMovingPathForEntry(pending.entry, 25.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_WARNING("Transport path check: entry=", pending.entry, " hasUsablePath=", hasUsablePath,
|
||||||
|
" preferServerData=", preferServerData, " shipOrZepDisplay=", shipOrZeppelinDisplay);
|
||||||
|
|
||||||
|
if (preferServerData) {
|
||||||
|
if (!hasUsablePath) {
|
||||||
|
std::vector<glm::vec3> path = { canonicalSpawnPos };
|
||||||
|
transportManager->loadPathFromNodes(pathId, path, false, 0.0f);
|
||||||
|
LOG_WARNING("Server-first strict registration: stationary fallback for GUID 0x",
|
||||||
|
std::hex, pending.guid, std::dec, " entry=", pending.entry);
|
||||||
|
} else {
|
||||||
|
LOG_WARNING("Server-first transport registration: using entry DBC path for entry ", pending.entry);
|
||||||
|
}
|
||||||
|
} else if (!hasUsablePath) {
|
||||||
|
bool allowZOnly = (pending.displayId == 455 || pending.displayId == 462);
|
||||||
|
uint32_t inferredPath = transportManager->inferDbcPathForSpawn(
|
||||||
|
canonicalSpawnPos, 1200.0f, allowZOnly);
|
||||||
|
if (inferredPath != 0) {
|
||||||
|
pathId = inferredPath;
|
||||||
|
LOG_WARNING("Using inferred transport path ", pathId, " for entry ", pending.entry);
|
||||||
|
} else {
|
||||||
|
uint32_t remappedPath = transportManager->pickFallbackMovingPath(pending.entry, pending.displayId);
|
||||||
|
if (remappedPath != 0) {
|
||||||
|
pathId = remappedPath;
|
||||||
|
LOG_WARNING("Using remapped fallback transport path ", pathId,
|
||||||
|
" for entry ", pending.entry, " displayId=", pending.displayId,
|
||||||
|
" (usableEntryPath=", transportManager->hasPathForEntry(pending.entry), ")");
|
||||||
|
} else {
|
||||||
|
LOG_WARNING("No TransportAnimation.dbc path for entry ", pending.entry,
|
||||||
|
" - transport will be stationary");
|
||||||
|
std::vector<glm::vec3> path = { canonicalSpawnPos };
|
||||||
|
transportManager->loadPathFromNodes(pathId, path, false, 0.0f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LOG_WARNING("Using real transport path from TransportAnimation.dbc for entry ", pending.entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
transportManager->registerTransport(pending.guid, wmoInstanceId, pathId, canonicalSpawnPos, pending.entry);
|
||||||
|
|
||||||
|
if (!goIt->second.isWmo) {
|
||||||
|
if (auto* tr = transportManager->getTransport(pending.guid)) {
|
||||||
|
tr->isM2 = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
transportManager->updateServerTransport(
|
||||||
|
pending.guid, glm::vec3(pending.x, pending.y, pending.z), pending.orientation);
|
||||||
|
|
||||||
|
auto moveIt = pendingTransportMoves_.find(pending.guid);
|
||||||
|
if (moveIt != pendingTransportMoves_.end()) {
|
||||||
|
const PendingTransportMove latestMove = moveIt->second;
|
||||||
|
transportManager->updateServerTransport(
|
||||||
|
pending.guid, glm::vec3(latestMove.x, latestMove.y, latestMove.z), latestMove.orientation);
|
||||||
|
LOG_DEBUG("Replayed queued transport move for GUID=0x", std::hex, pending.guid, std::dec,
|
||||||
|
" pos=(", latestMove.x, ", ", latestMove.y, ", ", latestMove.z,
|
||||||
|
") orientation=", latestMove.orientation);
|
||||||
|
pendingTransportMoves_.erase(moveIt);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (glm::length(canonicalSpawnPos) < 1.0f) {
|
||||||
|
auto goData = gameHandler->getCachedGameObjectInfo(pending.entry);
|
||||||
|
if (goData && goData->type == 15 && goData->hasData && goData->data[0] != 0) {
|
||||||
|
uint32_t taxiPathId = goData->data[0];
|
||||||
|
if (transportManager->hasTaxiPath(taxiPathId)) {
|
||||||
|
transportManager->assignTaxiPathToTransport(pending.entry, taxiPathId);
|
||||||
|
LOG_DEBUG("Assigned cached TaxiPathNode path for MO_TRANSPORT entry=", pending.entry,
|
||||||
|
" taxiPathId=", taxiPathId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (auto* tr = transportManager->getTransport(pending.guid); tr) {
|
||||||
|
LOG_WARNING("Transport registered: guid=0x", std::hex, pending.guid, std::dec,
|
||||||
|
" entry=", pending.entry, " displayId=", pending.displayId,
|
||||||
|
" pathId=", tr->pathId,
|
||||||
|
" mode=", (tr->useClientAnimation ? "client" : "server"),
|
||||||
|
" serverUpdates=", tr->serverUpdateCount);
|
||||||
|
} else {
|
||||||
|
LOG_DEBUG("Transport registered: guid=0x", std::hex, pending.guid, std::dec,
|
||||||
|
" entry=", pending.entry, " displayId=", pending.displayId,
|
||||||
|
" (TransportManager instance missing)");
|
||||||
|
}
|
||||||
|
|
||||||
|
++processed;
|
||||||
|
it = pendingTransportRegistrations_.erase(it);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void Application::processPendingTransportDoodads() {
|
void Application::processPendingTransportDoodads() {
|
||||||
if (pendingTransportDoodadBatches_.empty()) return;
|
if (pendingTransportDoodadBatches_.empty()) return;
|
||||||
if (!renderer || !assetManager) return;
|
if (!renderer || !assetManager) return;
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -441,6 +441,18 @@ bool ClassicPacketParsers::parseSpellGo(network::Packet& packet, SpellGoData& da
|
||||||
if (rawHitCount > 128) {
|
if (rawHitCount > 128) {
|
||||||
LOG_WARNING("[Classic] Spell go: hitCount capped (requested=", (int)rawHitCount, ")");
|
LOG_WARNING("[Classic] Spell go: hitCount capped (requested=", (int)rawHitCount, ")");
|
||||||
}
|
}
|
||||||
|
// Packed GUIDs are variable length, but each target needs at least 1 byte (mask).
|
||||||
|
// Require the minimum bytes before entering per-target parsing loops.
|
||||||
|
if (rem() < static_cast<size_t>(rawHitCount) + 1u) { // +1 for mandatory missCount byte
|
||||||
|
static uint32_t badHitCountTrunc = 0;
|
||||||
|
++badHitCountTrunc;
|
||||||
|
if (badHitCountTrunc <= 10 || (badHitCountTrunc % 100) == 0) {
|
||||||
|
LOG_WARNING("[Classic] Spell go: invalid hitCount/remaining (hits=", (int)rawHitCount,
|
||||||
|
" remaining=", rem(), " occurrence=", badHitCountTrunc, ")");
|
||||||
|
}
|
||||||
|
packet.setReadPos(startPos);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
const uint8_t storedHitLimit = std::min<uint8_t>(rawHitCount, 128);
|
const uint8_t storedHitLimit = std::min<uint8_t>(rawHitCount, 128);
|
||||||
data.hitTargets.reserve(storedHitLimit);
|
data.hitTargets.reserve(storedHitLimit);
|
||||||
bool truncatedTargets = false;
|
bool truncatedTargets = false;
|
||||||
|
|
@ -472,6 +484,17 @@ bool ClassicPacketParsers::parseSpellGo(network::Packet& packet, SpellGoData& da
|
||||||
if (rawMissCount > 128) {
|
if (rawMissCount > 128) {
|
||||||
LOG_WARNING("[Classic] Spell go: missCount capped (requested=", (int)rawMissCount, ")");
|
LOG_WARNING("[Classic] Spell go: missCount capped (requested=", (int)rawMissCount, ")");
|
||||||
}
|
}
|
||||||
|
// Each miss entry needs at least packed-guid mask (1) + missType (1).
|
||||||
|
if (rem() < static_cast<size_t>(rawMissCount) * 2u) {
|
||||||
|
static uint32_t badMissCountTrunc = 0;
|
||||||
|
++badMissCountTrunc;
|
||||||
|
if (badMissCountTrunc <= 10 || (badMissCountTrunc % 100) == 0) {
|
||||||
|
LOG_WARNING("[Classic] Spell go: invalid missCount/remaining (misses=", (int)rawMissCount,
|
||||||
|
" remaining=", rem(), " occurrence=", badMissCountTrunc, ")");
|
||||||
|
}
|
||||||
|
packet.setReadPos(startPos);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
const uint8_t storedMissLimit = std::min<uint8_t>(rawMissCount, 128);
|
const uint8_t storedMissLimit = std::min<uint8_t>(rawMissCount, 128);
|
||||||
data.missTargets.reserve(storedMissLimit);
|
data.missTargets.reserve(storedMissLimit);
|
||||||
for (uint16_t i = 0; i < rawMissCount; ++i) {
|
for (uint16_t i = 0; i < rawMissCount; ++i) {
|
||||||
|
|
@ -1810,6 +1833,173 @@ bool TurtlePacketParsers::parseMovementBlock(network::Packet& packet, UpdateBloc
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool TurtlePacketParsers::parseUpdateObject(network::Packet& packet, UpdateObjectData& data) {
|
||||||
|
constexpr uint32_t kMaxReasonableUpdateBlocks = 4096;
|
||||||
|
|
||||||
|
auto parseWithLayout = [&](bool withHasTransportByte, UpdateObjectData& out) -> bool {
|
||||||
|
out = UpdateObjectData{};
|
||||||
|
const size_t start = packet.getReadPos();
|
||||||
|
if (packet.getSize() - start < 4) return false;
|
||||||
|
|
||||||
|
out.blockCount = packet.readUInt32();
|
||||||
|
if (out.blockCount > kMaxReasonableUpdateBlocks) {
|
||||||
|
packet.setReadPos(start);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (withHasTransportByte) {
|
||||||
|
if (packet.getReadPos() >= packet.getSize()) {
|
||||||
|
packet.setReadPos(start);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
/*uint8_t hasTransport =*/ packet.readUInt8();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (packet.getReadPos() + 1 <= packet.getSize()) {
|
||||||
|
uint8_t firstByte = packet.readUInt8();
|
||||||
|
if (firstByte == static_cast<uint8_t>(UpdateType::OUT_OF_RANGE_OBJECTS)) {
|
||||||
|
if (packet.getReadPos() + 4 > packet.getSize()) {
|
||||||
|
packet.setReadPos(start);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
uint32_t count = packet.readUInt32();
|
||||||
|
if (count > kMaxReasonableUpdateBlocks) {
|
||||||
|
packet.setReadPos(start);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (uint32_t i = 0; i < count; ++i) {
|
||||||
|
if (packet.getReadPos() >= packet.getSize()) {
|
||||||
|
packet.setReadPos(start);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
out.outOfRangeGuids.push_back(UpdateObjectParser::readPackedGuid(packet));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
packet.setReadPos(packet.getReadPos() - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
out.blocks.reserve(out.blockCount);
|
||||||
|
for (uint32_t i = 0; i < out.blockCount; ++i) {
|
||||||
|
if (packet.getReadPos() >= packet.getSize()) {
|
||||||
|
packet.setReadPos(start);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const size_t blockStart = packet.getReadPos();
|
||||||
|
uint8_t updateTypeVal = packet.readUInt8();
|
||||||
|
if (updateTypeVal > static_cast<uint8_t>(UpdateType::NEAR_OBJECTS)) {
|
||||||
|
packet.setReadPos(start);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const UpdateType updateType = static_cast<UpdateType>(updateTypeVal);
|
||||||
|
UpdateBlock block;
|
||||||
|
block.updateType = updateType;
|
||||||
|
bool ok = false;
|
||||||
|
|
||||||
|
auto parseMovementVariant = [&](auto&& movementParser, const char* layoutName) -> bool {
|
||||||
|
packet.setReadPos(blockStart + 1);
|
||||||
|
block = UpdateBlock{};
|
||||||
|
block.updateType = updateType;
|
||||||
|
|
||||||
|
switch (updateType) {
|
||||||
|
case UpdateType::MOVEMENT:
|
||||||
|
block.guid = UpdateObjectParser::readPackedGuid(packet);
|
||||||
|
if (!movementParser(packet, block)) return false;
|
||||||
|
LOG_DEBUG("[Turtle] Parsed MOVEMENT block via ", layoutName, " layout");
|
||||||
|
return true;
|
||||||
|
case UpdateType::CREATE_OBJECT:
|
||||||
|
case UpdateType::CREATE_OBJECT2:
|
||||||
|
block.guid = UpdateObjectParser::readPackedGuid(packet);
|
||||||
|
if (packet.getReadPos() >= packet.getSize()) return false;
|
||||||
|
block.objectType = static_cast<ObjectType>(packet.readUInt8());
|
||||||
|
if (!movementParser(packet, block)) return false;
|
||||||
|
if (!UpdateObjectParser::parseUpdateFields(packet, block)) return false;
|
||||||
|
LOG_DEBUG("[Turtle] Parsed CREATE block via ", layoutName, " layout");
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
switch (updateType) {
|
||||||
|
case UpdateType::VALUES:
|
||||||
|
block.guid = UpdateObjectParser::readPackedGuid(packet);
|
||||||
|
ok = UpdateObjectParser::parseUpdateFields(packet, block);
|
||||||
|
break;
|
||||||
|
case UpdateType::MOVEMENT:
|
||||||
|
case UpdateType::CREATE_OBJECT:
|
||||||
|
case UpdateType::CREATE_OBJECT2:
|
||||||
|
ok = parseMovementVariant(
|
||||||
|
[this](network::Packet& p, UpdateBlock& b) {
|
||||||
|
return this->TurtlePacketParsers::parseMovementBlock(p, b);
|
||||||
|
}, "turtle");
|
||||||
|
if (!ok) {
|
||||||
|
ok = parseMovementVariant(
|
||||||
|
[this](network::Packet& p, UpdateBlock& b) {
|
||||||
|
return this->ClassicPacketParsers::parseMovementBlock(p, b);
|
||||||
|
}, "classic");
|
||||||
|
}
|
||||||
|
if (!ok) {
|
||||||
|
ok = parseMovementVariant(
|
||||||
|
[this](network::Packet& p, UpdateBlock& b) {
|
||||||
|
return this->TbcPacketParsers::parseMovementBlock(p, b);
|
||||||
|
}, "tbc");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case UpdateType::OUT_OF_RANGE_OBJECTS:
|
||||||
|
case UpdateType::NEAR_OBJECTS:
|
||||||
|
ok = true;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
ok = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ok) {
|
||||||
|
packet.setReadPos(start);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
out.blocks.push_back(std::move(block));
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const size_t startPos = packet.getReadPos();
|
||||||
|
UpdateObjectData parsed;
|
||||||
|
if (parseWithLayout(true, parsed)) {
|
||||||
|
data = std::move(parsed);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
packet.setReadPos(startPos);
|
||||||
|
if (parseWithLayout(false, parsed)) {
|
||||||
|
LOG_DEBUG("[Turtle] SMSG_UPDATE_OBJECT parsed without has_transport byte fallback");
|
||||||
|
data = std::move(parsed);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
packet.setReadPos(startPos);
|
||||||
|
if (ClassicPacketParsers::parseUpdateObject(packet, parsed)) {
|
||||||
|
LOG_DEBUG("[Turtle] SMSG_UPDATE_OBJECT parsed via full classic fallback");
|
||||||
|
data = std::move(parsed);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
packet.setReadPos(startPos);
|
||||||
|
if (TbcPacketParsers::parseUpdateObject(packet, parsed)) {
|
||||||
|
LOG_DEBUG("[Turtle] SMSG_UPDATE_OBJECT parsed via full TBC fallback");
|
||||||
|
data = std::move(parsed);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
packet.setReadPos(startPos);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
bool TurtlePacketParsers::parseMonsterMove(network::Packet& packet, MonsterMoveData& data) {
|
bool TurtlePacketParsers::parseMonsterMove(network::Packet& packet, MonsterMoveData& data) {
|
||||||
// Turtle realms can emit both vanilla-like and WotLK-like monster move bodies.
|
// Turtle realms can emit both vanilla-like and WotLK-like monster move bodies.
|
||||||
// Try the canonical Turtle/vanilla parser first, then fall back to WotLK layout.
|
// Try the canonical Turtle/vanilla parser first, then fall back to WotLK layout.
|
||||||
|
|
|
||||||
|
|
@ -272,9 +272,28 @@ bool WardenMemory::readMemory(uint32_t va, uint8_t length, uint8_t* outBuf) cons
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// PE image range
|
if (!loaded_) return false;
|
||||||
if (!loaded_ || va < imageBase_) return false;
|
|
||||||
uint32_t offset = va - imageBase_;
|
// Warden MEM_CHECK offsets are seen in multiple forms:
|
||||||
|
// 1) Absolute VA (e.g. 0x00401337)
|
||||||
|
// 2) RVA (e.g. 0x000139A9)
|
||||||
|
// 3) Tiny module-relative offsets (e.g. 0x00000229, 0x00000008)
|
||||||
|
// Accept all three to avoid fallback-to-zeros on Classic/Turtle.
|
||||||
|
uint32_t offset = 0;
|
||||||
|
if (va >= imageBase_) {
|
||||||
|
// Absolute VA.
|
||||||
|
offset = va - imageBase_;
|
||||||
|
} else if (va < imageSize_) {
|
||||||
|
// RVA into WoW.exe image.
|
||||||
|
offset = va;
|
||||||
|
} else {
|
||||||
|
// Tiny relative offsets frequently target fake Warden runtime globals.
|
||||||
|
constexpr uint32_t kFakeWardenBase = 0xCE8000;
|
||||||
|
const uint32_t remappedVa = kFakeWardenBase + va;
|
||||||
|
if (remappedVa < imageBase_) return false;
|
||||||
|
offset = remappedVa - imageBase_;
|
||||||
|
}
|
||||||
|
|
||||||
if (static_cast<uint64_t>(offset) + length > imageSize_) return false;
|
if (static_cast<uint64_t>(offset) + length > imageSize_) return false;
|
||||||
|
|
||||||
std::memcpy(outBuf, image_.data() + offset, length);
|
std::memcpy(outBuf, image_.data() + offset, length);
|
||||||
|
|
|
||||||
|
|
@ -59,15 +59,14 @@ bool WardenModule::load(const std::vector<uint8_t>& moduleData,
|
||||||
|
|
||||||
// Step 1: Verify MD5 hash
|
// Step 1: Verify MD5 hash
|
||||||
if (!verifyMD5(moduleData, md5Hash)) {
|
if (!verifyMD5(moduleData, md5Hash)) {
|
||||||
std::cerr << "[WardenModule] MD5 verification failed!" << '\n';
|
std::cerr << "[WardenModule] MD5 verification failed; continuing in compatibility mode" << '\n';
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
std::cout << "[WardenModule] ✓ MD5 verified" << '\n';
|
std::cout << "[WardenModule] ✓ MD5 verified" << '\n';
|
||||||
|
|
||||||
// Step 2: RC4 decrypt (Warden protocol-required legacy RC4; server-mandated, cannot be changed)
|
// Step 2: RC4 decrypt (Warden protocol-required legacy RC4; server-mandated, cannot be changed)
|
||||||
if (!decryptRC4(moduleData, rc4Key, decryptedData_)) { // codeql[cpp/weak-cryptographic-algorithm]
|
if (!decryptRC4(moduleData, rc4Key, decryptedData_)) { // codeql[cpp/weak-cryptographic-algorithm]
|
||||||
std::cerr << "[WardenModule] RC4 decryption failed!" << '\n';
|
std::cerr << "[WardenModule] RC4 decryption failed; using raw module bytes fallback" << '\n';
|
||||||
return false;
|
decryptedData_ = moduleData;
|
||||||
}
|
}
|
||||||
std::cout << "[WardenModule] ✓ RC4 decrypted (" << decryptedData_.size() << " bytes)" << '\n';
|
std::cout << "[WardenModule] ✓ RC4 decrypted (" << decryptedData_.size() << " bytes)" << '\n';
|
||||||
|
|
||||||
|
|
@ -85,20 +84,18 @@ bool WardenModule::load(const std::vector<uint8_t>& moduleData,
|
||||||
dataWithoutSig = decryptedData_;
|
dataWithoutSig = decryptedData_;
|
||||||
}
|
}
|
||||||
if (!decompressZlib(dataWithoutSig, decompressedData_)) {
|
if (!decompressZlib(dataWithoutSig, decompressedData_)) {
|
||||||
std::cerr << "[WardenModule] zlib decompression failed!" << '\n';
|
std::cerr << "[WardenModule] zlib decompression failed; using decrypted bytes fallback" << '\n';
|
||||||
return false;
|
decompressedData_ = decryptedData_;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 5: Parse custom executable format
|
// Step 5: Parse custom executable format
|
||||||
if (!parseExecutableFormat(decompressedData_)) {
|
if (!parseExecutableFormat(decompressedData_)) {
|
||||||
std::cerr << "[WardenModule] Executable format parsing failed!" << '\n';
|
std::cerr << "[WardenModule] Executable format parsing failed; continuing with minimal module image" << '\n';
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 6: Apply relocations
|
// Step 6: Apply relocations
|
||||||
if (!applyRelocations()) {
|
if (!applyRelocations()) {
|
||||||
std::cerr << "[WardenModule] Address relocations failed!" << '\n';
|
std::cerr << "[WardenModule] Address relocations failed; continuing with unrelocated image" << '\n';
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 7: Bind APIs
|
// Step 7: Bind APIs
|
||||||
|
|
@ -109,8 +106,7 @@ bool WardenModule::load(const std::vector<uint8_t>& moduleData,
|
||||||
|
|
||||||
// Step 8: Initialize module
|
// Step 8: Initialize module
|
||||||
if (!initializeModule()) {
|
if (!initializeModule()) {
|
||||||
std::cerr << "[WardenModule] Module initialization failed!" << '\n';
|
std::cerr << "[WardenModule] Module initialization failed; continuing with stub callbacks" << '\n';
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Module loading pipeline complete!
|
// Module loading pipeline complete!
|
||||||
|
|
|
||||||
|
|
@ -1344,8 +1344,10 @@ bool UpdateObjectParser::parseUpdateBlock(network::Packet& packet, UpdateBlock&
|
||||||
}
|
}
|
||||||
|
|
||||||
bool UpdateObjectParser::parse(network::Packet& packet, UpdateObjectData& data) {
|
bool UpdateObjectParser::parse(network::Packet& packet, UpdateObjectData& data) {
|
||||||
constexpr uint32_t kMaxReasonableUpdateBlocks = 4096;
|
// Keep worst-case packet parsing bounded. Extremely large counts are typically
|
||||||
constexpr uint32_t kMaxReasonableOutOfRangeGuids = 16384;
|
// malformed/desynced and can stall a frame long enough to trigger disconnects.
|
||||||
|
constexpr uint32_t kMaxReasonableUpdateBlocks = 1024;
|
||||||
|
constexpr uint32_t kMaxReasonableOutOfRangeGuids = 4096;
|
||||||
|
|
||||||
// Read block count
|
// Read block count
|
||||||
data.blockCount = packet.readUInt32();
|
data.blockCount = packet.readUInt32();
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
#include "network/world_socket.hpp"
|
#include "network/world_socket.hpp"
|
||||||
#include "network/packet.hpp"
|
#include "network/packet.hpp"
|
||||||
#include "network/net_platform.hpp"
|
#include "network/net_platform.hpp"
|
||||||
|
#include "game/opcode_table.hpp"
|
||||||
#include "auth/crypto.hpp"
|
#include "auth/crypto.hpp"
|
||||||
#include "core/logger.hpp"
|
#include "core/logger.hpp"
|
||||||
#include <iomanip>
|
#include <iomanip>
|
||||||
|
|
@ -9,10 +10,49 @@
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
|
#include <chrono>
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
constexpr size_t kMaxReceiveBufferBytes = 8 * 1024 * 1024;
|
constexpr size_t kMaxReceiveBufferBytes = 8 * 1024 * 1024;
|
||||||
constexpr int kMaxParsedPacketsPerUpdate = 220;
|
constexpr int kDefaultMaxParsedPacketsPerUpdate = 16;
|
||||||
|
constexpr int kAbsoluteMaxParsedPacketsPerUpdate = 220;
|
||||||
|
constexpr int kMinParsedPacketsPerUpdate = 8;
|
||||||
|
constexpr int kDefaultMaxPacketCallbacksPerUpdate = 6;
|
||||||
|
constexpr int kAbsoluteMaxPacketCallbacksPerUpdate = 64;
|
||||||
|
constexpr int kMinPacketCallbacksPerUpdate = 1;
|
||||||
|
constexpr int kMaxRecvCallsPerUpdate = 64;
|
||||||
|
constexpr size_t kMaxRecvBytesPerUpdate = 512 * 1024;
|
||||||
|
constexpr size_t kMaxQueuedPacketCallbacks = 4096;
|
||||||
|
constexpr int kAsyncPumpSleepMs = 2;
|
||||||
|
|
||||||
|
inline int parsedPacketsBudgetPerUpdate() {
|
||||||
|
static int budget = []() {
|
||||||
|
const char* raw = std::getenv("WOWEE_NET_MAX_PARSED_PACKETS");
|
||||||
|
if (!raw || !*raw) return kDefaultMaxParsedPacketsPerUpdate;
|
||||||
|
char* end = nullptr;
|
||||||
|
long parsed = std::strtol(raw, &end, 10);
|
||||||
|
if (end == raw) return kDefaultMaxParsedPacketsPerUpdate;
|
||||||
|
if (parsed < kMinParsedPacketsPerUpdate) return kMinParsedPacketsPerUpdate;
|
||||||
|
if (parsed > kAbsoluteMaxParsedPacketsPerUpdate) return kAbsoluteMaxParsedPacketsPerUpdate;
|
||||||
|
return static_cast<int>(parsed);
|
||||||
|
}();
|
||||||
|
return budget;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline int packetCallbacksBudgetPerUpdate() {
|
||||||
|
static int budget = []() {
|
||||||
|
const char* raw = std::getenv("WOWEE_NET_MAX_PACKET_CALLBACKS");
|
||||||
|
if (!raw || !*raw) return kDefaultMaxPacketCallbacksPerUpdate;
|
||||||
|
char* end = nullptr;
|
||||||
|
long parsed = std::strtol(raw, &end, 10);
|
||||||
|
if (end == raw) return kDefaultMaxPacketCallbacksPerUpdate;
|
||||||
|
if (parsed < kMinPacketCallbacksPerUpdate) return kMinPacketCallbacksPerUpdate;
|
||||||
|
if (parsed > kAbsoluteMaxPacketCallbacksPerUpdate) return kAbsoluteMaxPacketCallbacksPerUpdate;
|
||||||
|
return static_cast<int>(parsed);
|
||||||
|
}();
|
||||||
|
return budget;
|
||||||
|
}
|
||||||
|
|
||||||
inline bool isLoginPipelineSmsg(uint16_t opcode) {
|
inline bool isLoginPipelineSmsg(uint16_t opcode) {
|
||||||
switch (opcode) {
|
switch (opcode) {
|
||||||
|
|
@ -49,6 +89,14 @@ inline bool envFlagEnabled(const char* key, bool defaultValue = false) {
|
||||||
return !(raw[0] == '0' || raw[0] == 'f' || raw[0] == 'F' ||
|
return !(raw[0] == '0' || raw[0] == 'f' || raw[0] == 'F' ||
|
||||||
raw[0] == 'n' || raw[0] == 'N');
|
raw[0] == 'n' || raw[0] == 'N');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const char* opcodeNameForTrace(uint16_t wireOpcode) {
|
||||||
|
const auto* table = wowee::game::getActiveOpcodeTable();
|
||||||
|
if (!table) return "UNKNOWN";
|
||||||
|
auto logical = table->fromWire(wireOpcode);
|
||||||
|
if (!logical) return "UNKNOWN";
|
||||||
|
return wowee::game::OpcodeTable::logicalToName(*logical);
|
||||||
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
namespace wowee {
|
namespace wowee {
|
||||||
|
|
@ -71,6 +119,7 @@ WorldSocket::WorldSocket() {
|
||||||
receiveBuffer.reserve(64 * 1024);
|
receiveBuffer.reserve(64 * 1024);
|
||||||
useFastRecvAppend_ = envFlagEnabled("WOWEE_NET_FAST_RECV_APPEND", true);
|
useFastRecvAppend_ = envFlagEnabled("WOWEE_NET_FAST_RECV_APPEND", true);
|
||||||
useParseScratchQueue_ = envFlagEnabled("WOWEE_NET_PARSE_SCRATCH", false);
|
useParseScratchQueue_ = envFlagEnabled("WOWEE_NET_PARSE_SCRATCH", false);
|
||||||
|
useAsyncPump_ = envFlagEnabled("WOWEE_NET_ASYNC_PUMP", true);
|
||||||
if (useParseScratchQueue_) {
|
if (useParseScratchQueue_) {
|
||||||
LOG_WARNING("WOWEE_NET_PARSE_SCRATCH is temporarily disabled (known unstable); forcing off");
|
LOG_WARNING("WOWEE_NET_PARSE_SCRATCH is temporarily disabled (known unstable); forcing off");
|
||||||
useParseScratchQueue_ = false;
|
useParseScratchQueue_ = false;
|
||||||
|
|
@ -79,7 +128,10 @@ WorldSocket::WorldSocket() {
|
||||||
parsedPacketsScratch_.reserve(64);
|
parsedPacketsScratch_.reserve(64);
|
||||||
}
|
}
|
||||||
LOG_INFO("WorldSocket net opts: fast_recv_append=", useFastRecvAppend_ ? "on" : "off",
|
LOG_INFO("WorldSocket net opts: fast_recv_append=", useFastRecvAppend_ ? "on" : "off",
|
||||||
" parse_scratch=", useParseScratchQueue_ ? "on" : "off");
|
" async_pump=", useAsyncPump_ ? "on" : "off",
|
||||||
|
" parse_scratch=", useParseScratchQueue_ ? "on" : "off",
|
||||||
|
" max_parsed_packets=", parsedPacketsBudgetPerUpdate(),
|
||||||
|
" max_packet_callbacks=", packetCallbacksBudgetPerUpdate());
|
||||||
}
|
}
|
||||||
|
|
||||||
WorldSocket::~WorldSocket() {
|
WorldSocket::~WorldSocket() {
|
||||||
|
|
@ -89,6 +141,8 @@ WorldSocket::~WorldSocket() {
|
||||||
bool WorldSocket::connect(const std::string& host, uint16_t port) {
|
bool WorldSocket::connect(const std::string& host, uint16_t port) {
|
||||||
LOG_INFO("Connecting to world server: ", host, ":", port);
|
LOG_INFO("Connecting to world server: ", host, ":", port);
|
||||||
|
|
||||||
|
stopAsyncPump();
|
||||||
|
|
||||||
// Create socket
|
// Create socket
|
||||||
sockfd = socket(AF_INET, SOCK_STREAM, 0);
|
sockfd = socket(AF_INET, SOCK_STREAM, 0);
|
||||||
if (sockfd == INVALID_SOCK) {
|
if (sockfd == INVALID_SOCK) {
|
||||||
|
|
@ -165,32 +219,59 @@ bool WorldSocket::connect(const std::string& host, uint16_t port) {
|
||||||
|
|
||||||
connected = true;
|
connected = true;
|
||||||
LOG_INFO("Connected to world server: ", host, ":", port);
|
LOG_INFO("Connected to world server: ", host, ":", port);
|
||||||
|
startAsyncPump();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void WorldSocket::disconnect() {
|
void WorldSocket::disconnect() {
|
||||||
|
stopAsyncPump();
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(ioMutex_);
|
||||||
|
closeSocketNoJoin();
|
||||||
|
encryptionEnabled = false;
|
||||||
|
useVanillaCrypt = false;
|
||||||
|
receiveBuffer.clear();
|
||||||
|
receiveReadOffset_ = 0;
|
||||||
|
parsedPacketsScratch_.clear();
|
||||||
|
headerBytesDecrypted = 0;
|
||||||
|
packetTraceStart_ = {};
|
||||||
|
packetTraceUntil_ = {};
|
||||||
|
packetTraceReason_.clear();
|
||||||
|
}
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(callbackMutex_);
|
||||||
|
pendingPacketCallbacks_.clear();
|
||||||
|
}
|
||||||
|
LOG_INFO("Disconnected from world server");
|
||||||
|
}
|
||||||
|
|
||||||
|
void WorldSocket::tracePacketsFor(std::chrono::milliseconds duration, const std::string& reason) {
|
||||||
|
std::lock_guard<std::mutex> lock(ioMutex_);
|
||||||
|
packetTraceStart_ = std::chrono::steady_clock::now();
|
||||||
|
packetTraceUntil_ = packetTraceStart_ + duration;
|
||||||
|
packetTraceReason_ = reason;
|
||||||
|
LOG_WARNING("WS TRACE enabled: reason='", packetTraceReason_,
|
||||||
|
"' durationMs=", duration.count());
|
||||||
|
}
|
||||||
|
|
||||||
|
bool WorldSocket::isConnected() const {
|
||||||
|
std::lock_guard<std::mutex> lock(ioMutex_);
|
||||||
|
return connected;
|
||||||
|
}
|
||||||
|
|
||||||
|
void WorldSocket::closeSocketNoJoin() {
|
||||||
if (sockfd != INVALID_SOCK) {
|
if (sockfd != INVALID_SOCK) {
|
||||||
net::closeSocket(sockfd);
|
net::closeSocket(sockfd);
|
||||||
sockfd = INVALID_SOCK;
|
sockfd = INVALID_SOCK;
|
||||||
}
|
}
|
||||||
connected = false;
|
connected = false;
|
||||||
encryptionEnabled = false;
|
|
||||||
useVanillaCrypt = false;
|
|
||||||
receiveBuffer.clear();
|
|
||||||
receiveReadOffset_ = 0;
|
|
||||||
parsedPacketsScratch_.clear();
|
|
||||||
headerBytesDecrypted = 0;
|
|
||||||
LOG_INFO("Disconnected from world server");
|
|
||||||
}
|
|
||||||
|
|
||||||
bool WorldSocket::isConnected() const {
|
|
||||||
return connected;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void WorldSocket::send(const Packet& packet) {
|
void WorldSocket::send(const Packet& packet) {
|
||||||
if (!connected) return;
|
|
||||||
static const bool kLogCharCreatePayload = envFlagEnabled("WOWEE_NET_LOG_CHAR_CREATE", false);
|
static const bool kLogCharCreatePayload = envFlagEnabled("WOWEE_NET_LOG_CHAR_CREATE", false);
|
||||||
static const bool kLogSwapItemPackets = envFlagEnabled("WOWEE_NET_LOG_SWAP_ITEM", false);
|
static const bool kLogSwapItemPackets = envFlagEnabled("WOWEE_NET_LOG_SWAP_ITEM", false);
|
||||||
|
std::lock_guard<std::mutex> lock(ioMutex_);
|
||||||
|
if (!connected || sockfd == INVALID_SOCK) return;
|
||||||
|
|
||||||
const auto& data = packet.getData();
|
const auto& data = packet.getData();
|
||||||
uint16_t opcode = packet.getOpcode();
|
uint16_t opcode = packet.getOpcode();
|
||||||
|
|
@ -254,6 +335,17 @@ void WorldSocket::send(const Packet& packet) {
|
||||||
LOG_INFO("WS TX opcode=0x", std::hex, opcode, std::dec, " payloadLen=", payloadLen, " data=[", hex, "]");
|
LOG_INFO("WS TX opcode=0x", std::hex, opcode, std::dec, " payloadLen=", payloadLen, " data=[", hex, "]");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const auto traceNow = std::chrono::steady_clock::now();
|
||||||
|
if (packetTraceUntil_ > traceNow) {
|
||||||
|
const auto elapsedMs = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||||
|
traceNow - packetTraceStart_).count();
|
||||||
|
LOG_WARNING("WS TRACE TX +", elapsedMs, "ms opcode=0x",
|
||||||
|
std::hex, opcode, std::dec,
|
||||||
|
" logical=", opcodeNameForTrace(opcode),
|
||||||
|
" payload=", payloadLen,
|
||||||
|
" reason='", packetTraceReason_, "'");
|
||||||
|
}
|
||||||
|
|
||||||
// WotLK 3.3.5 CMSG header (6 bytes total):
|
// WotLK 3.3.5 CMSG header (6 bytes total):
|
||||||
// - size (2 bytes, big-endian) = payloadLen + 4 (opcode is 4 bytes for CMSG)
|
// - size (2 bytes, big-endian) = payloadLen + 4 (opcode is 4 bytes for CMSG)
|
||||||
// - opcode (4 bytes, little-endian)
|
// - opcode (4 bytes, little-endian)
|
||||||
|
|
@ -317,7 +409,46 @@ void WorldSocket::send(const Packet& packet) {
|
||||||
}
|
}
|
||||||
|
|
||||||
void WorldSocket::update() {
|
void WorldSocket::update() {
|
||||||
if (!connected) return;
|
if (!useAsyncPump_) {
|
||||||
|
pumpNetworkIO();
|
||||||
|
}
|
||||||
|
dispatchQueuedPackets();
|
||||||
|
}
|
||||||
|
|
||||||
|
void WorldSocket::startAsyncPump() {
|
||||||
|
if (!useAsyncPump_ || asyncPumpRunning_.load(std::memory_order_acquire)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
asyncPumpStop_.store(false, std::memory_order_release);
|
||||||
|
asyncPumpThread_ = std::thread(&WorldSocket::asyncPumpLoop, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
void WorldSocket::stopAsyncPump() {
|
||||||
|
asyncPumpStop_.store(true, std::memory_order_release);
|
||||||
|
if (asyncPumpThread_.joinable()) {
|
||||||
|
asyncPumpThread_.join();
|
||||||
|
}
|
||||||
|
asyncPumpRunning_.store(false, std::memory_order_release);
|
||||||
|
}
|
||||||
|
|
||||||
|
void WorldSocket::asyncPumpLoop() {
|
||||||
|
asyncPumpRunning_.store(true, std::memory_order_release);
|
||||||
|
while (!asyncPumpStop_.load(std::memory_order_acquire)) {
|
||||||
|
pumpNetworkIO();
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(ioMutex_);
|
||||||
|
if (!connected) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(kAsyncPumpSleepMs));
|
||||||
|
}
|
||||||
|
asyncPumpRunning_.store(false, std::memory_order_release);
|
||||||
|
}
|
||||||
|
|
||||||
|
void WorldSocket::pumpNetworkIO() {
|
||||||
|
std::lock_guard<std::mutex> lock(ioMutex_);
|
||||||
|
if (!connected || sockfd == INVALID_SOCK) return;
|
||||||
auto bufferedBytes = [&]() -> size_t {
|
auto bufferedBytes = [&]() -> size_t {
|
||||||
return (receiveBuffer.size() >= receiveReadOffset_)
|
return (receiveBuffer.size() >= receiveReadOffset_)
|
||||||
? (receiveBuffer.size() - receiveReadOffset_)
|
? (receiveBuffer.size() - receiveReadOffset_)
|
||||||
|
|
@ -343,7 +474,8 @@ void WorldSocket::update() {
|
||||||
bool receivedAny = false;
|
bool receivedAny = false;
|
||||||
size_t bytesReadThisTick = 0;
|
size_t bytesReadThisTick = 0;
|
||||||
int readOps = 0;
|
int readOps = 0;
|
||||||
while (connected) {
|
while (connected && readOps < kMaxRecvCallsPerUpdate &&
|
||||||
|
bytesReadThisTick < kMaxRecvBytesPerUpdate) {
|
||||||
uint8_t buffer[4096];
|
uint8_t buffer[4096];
|
||||||
ssize_t received = net::portableRecv(sockfd, buffer, sizeof(buffer));
|
ssize_t received = net::portableRecv(sockfd, buffer, sizeof(buffer));
|
||||||
|
|
||||||
|
|
@ -362,7 +494,7 @@ void WorldSocket::update() {
|
||||||
LOG_ERROR("World socket receive buffer would overflow (buffered=", liveBytes,
|
LOG_ERROR("World socket receive buffer would overflow (buffered=", liveBytes,
|
||||||
" incoming=", receivedSize, " max=", kMaxReceiveBufferBytes,
|
" incoming=", receivedSize, " max=", kMaxReceiveBufferBytes,
|
||||||
"). Disconnecting to recover framing.");
|
"). Disconnecting to recover framing.");
|
||||||
disconnect();
|
closeSocketNoJoin();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const size_t oldSize = receiveBuffer.size();
|
const size_t oldSize = receiveBuffer.size();
|
||||||
|
|
@ -375,7 +507,7 @@ void WorldSocket::update() {
|
||||||
if (newCap < needed) {
|
if (newCap < needed) {
|
||||||
LOG_ERROR("World socket receive buffer capacity growth failed (needed=", needed,
|
LOG_ERROR("World socket receive buffer capacity growth failed (needed=", needed,
|
||||||
" max=", kMaxReceiveBufferBytes, "). Disconnecting to recover framing.");
|
" max=", kMaxReceiveBufferBytes, "). Disconnecting to recover framing.");
|
||||||
disconnect();
|
closeSocketNoJoin();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
receiveBuffer.reserve(newCap);
|
receiveBuffer.reserve(newCap);
|
||||||
|
|
@ -387,7 +519,7 @@ void WorldSocket::update() {
|
||||||
if (bufferedBytes() > kMaxReceiveBufferBytes) {
|
if (bufferedBytes() > kMaxReceiveBufferBytes) {
|
||||||
LOG_ERROR("World socket receive buffer overflow (", bufferedBytes(),
|
LOG_ERROR("World socket receive buffer overflow (", bufferedBytes(),
|
||||||
" bytes). Disconnecting to recover framing.");
|
" bytes). Disconnecting to recover framing.");
|
||||||
disconnect();
|
closeSocketNoJoin();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
|
|
@ -409,7 +541,7 @@ void WorldSocket::update() {
|
||||||
}
|
}
|
||||||
|
|
||||||
LOG_ERROR("Receive failed: ", net::errorString(err));
|
LOG_ERROR("Receive failed: ", net::errorString(err));
|
||||||
disconnect();
|
closeSocketNoJoin();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -434,10 +566,15 @@ void WorldSocket::update() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (connected && (readOps >= kMaxRecvCallsPerUpdate || bytesReadThisTick >= kMaxRecvBytesPerUpdate)) {
|
||||||
|
LOG_DEBUG("World socket recv budget reached (calls=", readOps,
|
||||||
|
", bytes=", bytesReadThisTick, "), deferring remaining socket drain");
|
||||||
|
}
|
||||||
|
|
||||||
if (sawClose) {
|
if (sawClose) {
|
||||||
LOG_INFO("World server connection closed (receivedAny=", receivedAny,
|
LOG_INFO("World server connection closed (receivedAny=", receivedAny,
|
||||||
" buffered=", bufferedBytes(), ")");
|
" buffered=", bufferedBytes(), ")");
|
||||||
disconnect();
|
closeSocketNoJoin();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -462,7 +599,8 @@ void WorldSocket::tryParsePackets() {
|
||||||
} else {
|
} else {
|
||||||
parsedPacketsLocal.reserve(32);
|
parsedPacketsLocal.reserve(32);
|
||||||
}
|
}
|
||||||
while ((receiveBuffer.size() - parseOffset) >= 4 && parsedThisTick < kMaxParsedPacketsPerUpdate) {
|
const int maxParsedThisTick = parsedPacketsBudgetPerUpdate();
|
||||||
|
while ((receiveBuffer.size() - parseOffset) >= 4 && parsedThisTick < maxParsedThisTick) {
|
||||||
uint8_t rawHeader[4] = {0, 0, 0, 0};
|
uint8_t rawHeader[4] = {0, 0, 0, 0};
|
||||||
std::memcpy(rawHeader, receiveBuffer.data() + parseOffset, 4);
|
std::memcpy(rawHeader, receiveBuffer.data() + parseOffset, 4);
|
||||||
|
|
||||||
|
|
@ -491,7 +629,7 @@ void WorldSocket::tryParsePackets() {
|
||||||
static_cast<int>(rawHeader[2]), " ",
|
static_cast<int>(rawHeader[2]), " ",
|
||||||
static_cast<int>(rawHeader[3]), std::dec,
|
static_cast<int>(rawHeader[3]), std::dec,
|
||||||
" enc=", encryptionEnabled, ". Disconnecting to recover stream.");
|
" enc=", encryptionEnabled, ". Disconnecting to recover stream.");
|
||||||
disconnect();
|
closeSocketNoJoin();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
constexpr uint16_t kMaxWorldPacketSize = 0x4000;
|
constexpr uint16_t kMaxWorldPacketSize = 0x4000;
|
||||||
|
|
@ -503,7 +641,7 @@ void WorldSocket::tryParsePackets() {
|
||||||
static_cast<int>(rawHeader[2]), " ",
|
static_cast<int>(rawHeader[2]), " ",
|
||||||
static_cast<int>(rawHeader[3]), std::dec,
|
static_cast<int>(rawHeader[3]), std::dec,
|
||||||
" enc=", encryptionEnabled, ". Disconnecting to recover stream.");
|
" enc=", encryptionEnabled, ". Disconnecting to recover stream.");
|
||||||
disconnect();
|
closeSocketNoJoin();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -535,6 +673,16 @@ void WorldSocket::tryParsePackets() {
|
||||||
" buffered=", (receiveBuffer.size() - parseOffset),
|
" buffered=", (receiveBuffer.size() - parseOffset),
|
||||||
" enc=", encryptionEnabled ? "yes" : "no");
|
" enc=", encryptionEnabled ? "yes" : "no");
|
||||||
}
|
}
|
||||||
|
const auto traceNow = std::chrono::steady_clock::now();
|
||||||
|
if (packetTraceUntil_ > traceNow) {
|
||||||
|
const auto elapsedMs = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||||
|
traceNow - packetTraceStart_).count();
|
||||||
|
LOG_WARNING("WS TRACE RX +", elapsedMs, "ms opcode=0x",
|
||||||
|
std::hex, opcode, std::dec,
|
||||||
|
" logical=", opcodeNameForTrace(opcode),
|
||||||
|
" payload=", payloadLen,
|
||||||
|
" reason='", packetTraceReason_, "'");
|
||||||
|
}
|
||||||
|
|
||||||
if ((receiveBuffer.size() - parseOffset) < totalSize) {
|
if ((receiveBuffer.size() - parseOffset) < totalSize) {
|
||||||
// Not enough data yet - header stays decrypted in buffer
|
// Not enough data yet - header stays decrypted in buffer
|
||||||
|
|
@ -555,7 +703,7 @@ void WorldSocket::tryParsePackets() {
|
||||||
" payload=", payloadLen, " buffered=", receiveBuffer.size(),
|
" payload=", payloadLen, " buffered=", receiveBuffer.size(),
|
||||||
" parseOffset=", parseOffset, " what=", e.what(),
|
" parseOffset=", parseOffset, " what=", e.what(),
|
||||||
". Disconnecting to recover.");
|
". Disconnecting to recover.");
|
||||||
disconnect();
|
closeSocketNoJoin();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
parseOffset += totalSize;
|
parseOffset += totalSize;
|
||||||
|
|
@ -578,23 +726,57 @@ void WorldSocket::tryParsePackets() {
|
||||||
}
|
}
|
||||||
headerBytesDecrypted = localHeaderBytesDecrypted;
|
headerBytesDecrypted = localHeaderBytesDecrypted;
|
||||||
|
|
||||||
if (packetCallback) {
|
// Queue parsed packets for main-thread dispatch.
|
||||||
for (const auto& packet : *parsedPackets) {
|
if (!parsedPackets->empty()) {
|
||||||
if (!connected) break;
|
std::lock_guard<std::mutex> callbackLock(callbackMutex_);
|
||||||
packetCallback(packet);
|
for (auto& packet : *parsedPackets) {
|
||||||
|
pendingPacketCallbacks_.push_back(std::move(packet));
|
||||||
|
}
|
||||||
|
if (pendingPacketCallbacks_.size() > kMaxQueuedPacketCallbacks) {
|
||||||
|
LOG_ERROR("World socket callback queue overflow (", pendingPacketCallbacks_.size(),
|
||||||
|
" packets). Disconnecting to recover.");
|
||||||
|
pendingPacketCallbacks_.clear();
|
||||||
|
closeSocketNoJoin();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const size_t buffered = (receiveBuffer.size() >= receiveReadOffset_)
|
const size_t buffered = (receiveBuffer.size() >= receiveReadOffset_)
|
||||||
? (receiveBuffer.size() - receiveReadOffset_)
|
? (receiveBuffer.size() - receiveReadOffset_)
|
||||||
: 0;
|
: 0;
|
||||||
if (parsedThisTick >= kMaxParsedPacketsPerUpdate && buffered >= 4) {
|
if (parsedThisTick >= maxParsedThisTick && buffered >= 4) {
|
||||||
LOG_DEBUG("World socket parse budget reached (", parsedThisTick,
|
LOG_DEBUG("World socket parse budget reached (", parsedThisTick,
|
||||||
" packets); deferring remaining buffered data=", buffered, " bytes");
|
" packets); deferring remaining buffered data=", buffered, " bytes");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void WorldSocket::dispatchQueuedPackets() {
|
||||||
|
std::deque<Packet> localPackets;
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(callbackMutex_);
|
||||||
|
if (!packetCallback || pendingPacketCallbacks_.empty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const int maxCallbacksThisTick = packetCallbacksBudgetPerUpdate();
|
||||||
|
for (int i = 0; i < maxCallbacksThisTick && !pendingPacketCallbacks_.empty(); ++i) {
|
||||||
|
localPackets.push_back(std::move(pendingPacketCallbacks_.front()));
|
||||||
|
pendingPacketCallbacks_.pop_front();
|
||||||
|
}
|
||||||
|
if (!pendingPacketCallbacks_.empty()) {
|
||||||
|
LOG_DEBUG("World socket callback budget reached (", localPackets.size(),
|
||||||
|
" callbacks); deferring ", pendingPacketCallbacks_.size(),
|
||||||
|
" queued packet callbacks");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
while (!localPackets.empty()) {
|
||||||
|
packetCallback(localPackets.front());
|
||||||
|
localPackets.pop_front();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void WorldSocket::initEncryption(const std::vector<uint8_t>& sessionKey, uint32_t build) {
|
void WorldSocket::initEncryption(const std::vector<uint8_t>& sessionKey, uint32_t build) {
|
||||||
|
std::lock_guard<std::mutex> lock(ioMutex_);
|
||||||
if (sessionKey.size() != 40) {
|
if (sessionKey.size() != 40) {
|
||||||
LOG_ERROR("Invalid session key size: ", sessionKey.size(), " (expected 40)");
|
LOG_ERROR("Invalid session key size: ", sessionKey.size(), " (expected 40)");
|
||||||
return;
|
return;
|
||||||
|
|
|
||||||
|
|
@ -343,6 +343,8 @@ void CharacterRenderer::shutdown() {
|
||||||
// Clean up composite cache
|
// Clean up composite cache
|
||||||
compositeCache_.clear();
|
compositeCache_.clear();
|
||||||
failedTextureCache_.clear();
|
failedTextureCache_.clear();
|
||||||
|
failedTextureRetryAt_.clear();
|
||||||
|
textureLookupSerial_ = 0;
|
||||||
|
|
||||||
whiteTexture_.reset();
|
whiteTexture_.reset();
|
||||||
transparentTexture_.reset();
|
transparentTexture_.reset();
|
||||||
|
|
@ -430,6 +432,8 @@ void CharacterRenderer::clear() {
|
||||||
textureCacheBytes_ = 0;
|
textureCacheBytes_ = 0;
|
||||||
textureCacheCounter_ = 0;
|
textureCacheCounter_ = 0;
|
||||||
loggedTextureLoadFails_.clear();
|
loggedTextureLoadFails_.clear();
|
||||||
|
failedTextureRetryAt_.clear();
|
||||||
|
textureLookupSerial_ = 0;
|
||||||
|
|
||||||
// Clear composite and failed caches
|
// Clear composite and failed caches
|
||||||
compositeCache_.clear();
|
compositeCache_.clear();
|
||||||
|
|
@ -604,6 +608,7 @@ CharacterRenderer::NormalMapResult CharacterRenderer::generateNormalHeightMapCPU
|
||||||
}
|
}
|
||||||
|
|
||||||
VkTexture* CharacterRenderer::loadTexture(const std::string& path) {
|
VkTexture* CharacterRenderer::loadTexture(const std::string& path) {
|
||||||
|
constexpr uint64_t kFailedTextureRetryLookups = 512;
|
||||||
// Skip empty or whitespace-only paths (type-0 textures have no filename)
|
// Skip empty or whitespace-only paths (type-0 textures have no filename)
|
||||||
if (path.empty()) return whiteTexture_.get();
|
if (path.empty()) return whiteTexture_.get();
|
||||||
bool allWhitespace = true;
|
bool allWhitespace = true;
|
||||||
|
|
@ -619,6 +624,7 @@ VkTexture* CharacterRenderer::loadTexture(const std::string& path) {
|
||||||
return key;
|
return key;
|
||||||
};
|
};
|
||||||
std::string key = normalizeKey(path);
|
std::string key = normalizeKey(path);
|
||||||
|
const uint64_t lookupSerial = ++textureLookupSerial_;
|
||||||
auto containsToken = [](const std::string& haystack, const char* token) {
|
auto containsToken = [](const std::string& haystack, const char* token) {
|
||||||
return haystack.find(token) != std::string::npos;
|
return haystack.find(token) != std::string::npos;
|
||||||
};
|
};
|
||||||
|
|
@ -634,6 +640,10 @@ VkTexture* CharacterRenderer::loadTexture(const std::string& path) {
|
||||||
it->second.lastUse = ++textureCacheCounter_;
|
it->second.lastUse = ++textureCacheCounter_;
|
||||||
return it->second.texture.get();
|
return it->second.texture.get();
|
||||||
}
|
}
|
||||||
|
auto failIt = failedTextureRetryAt_.find(key);
|
||||||
|
if (failIt != failedTextureRetryAt_.end() && lookupSerial < failIt->second) {
|
||||||
|
return whiteTexture_.get();
|
||||||
|
}
|
||||||
|
|
||||||
if (!assetManager || !assetManager->isInitialized()) {
|
if (!assetManager || !assetManager->isInitialized()) {
|
||||||
return whiteTexture_.get();
|
return whiteTexture_.get();
|
||||||
|
|
@ -652,8 +662,9 @@ VkTexture* CharacterRenderer::loadTexture(const std::string& path) {
|
||||||
blpImage = assetManager->loadTexture(key);
|
blpImage = assetManager->loadTexture(key);
|
||||||
}
|
}
|
||||||
if (!blpImage.isValid()) {
|
if (!blpImage.isValid()) {
|
||||||
// Return white fallback but don't cache the failure — allow retry
|
// Cache misses briefly to avoid repeated expensive MPQ/disk probes.
|
||||||
// on next character load in case the asset becomes available.
|
failedTextureCache_.insert(key);
|
||||||
|
failedTextureRetryAt_[key] = lookupSerial + kFailedTextureRetryLookups;
|
||||||
if (loggedTextureLoadFails_.insert(key).second) {
|
if (loggedTextureLoadFails_.insert(key).second) {
|
||||||
core::Logger::getInstance().warning("Failed to load texture: ", path);
|
core::Logger::getInstance().warning("Failed to load texture: ", path);
|
||||||
}
|
}
|
||||||
|
|
@ -666,6 +677,7 @@ VkTexture* CharacterRenderer::loadTexture(const std::string& path) {
|
||||||
if (failedTextureCache_.size() < kMaxFailedTextureCache) {
|
if (failedTextureCache_.size() < kMaxFailedTextureCache) {
|
||||||
// Budget is saturated; avoid repeatedly decoding/uploading this texture.
|
// Budget is saturated; avoid repeatedly decoding/uploading this texture.
|
||||||
failedTextureCache_.insert(key);
|
failedTextureCache_.insert(key);
|
||||||
|
failedTextureRetryAt_[key] = lookupSerial + kFailedTextureRetryLookups;
|
||||||
}
|
}
|
||||||
if (textureBudgetRejectWarnings_ < 3) {
|
if (textureBudgetRejectWarnings_ < 3) {
|
||||||
core::Logger::getInstance().warning(
|
core::Logger::getInstance().warning(
|
||||||
|
|
@ -724,6 +736,8 @@ VkTexture* CharacterRenderer::loadTexture(const std::string& path) {
|
||||||
textureHasAlphaByPtr_[texPtr] = hasAlpha;
|
textureHasAlphaByPtr_[texPtr] = hasAlpha;
|
||||||
textureColorKeyBlackByPtr_[texPtr] = colorKeyBlackHint;
|
textureColorKeyBlackByPtr_[texPtr] = colorKeyBlackHint;
|
||||||
textureCache[key] = std::move(e);
|
textureCache[key] = std::move(e);
|
||||||
|
failedTextureCache_.erase(key);
|
||||||
|
failedTextureRetryAt_.erase(key);
|
||||||
|
|
||||||
core::Logger::getInstance().debug("Loaded character texture: ", path, " (", blpImage.width, "x", blpImage.height, ")");
|
core::Logger::getInstance().debug("Loaded character texture: ", path, " (", blpImage.width, "x", blpImage.height, ")");
|
||||||
return texPtr;
|
return texPtr;
|
||||||
|
|
|
||||||
|
|
@ -714,7 +714,9 @@ void M2Renderer::shutdown() {
|
||||||
textureHasAlphaByPtr_.clear();
|
textureHasAlphaByPtr_.clear();
|
||||||
textureColorKeyBlackByPtr_.clear();
|
textureColorKeyBlackByPtr_.clear();
|
||||||
failedTextureCache_.clear();
|
failedTextureCache_.clear();
|
||||||
|
failedTextureRetryAt_.clear();
|
||||||
loggedTextureLoadFails_.clear();
|
loggedTextureLoadFails_.clear();
|
||||||
|
textureLookupSerial_ = 0;
|
||||||
textureBudgetRejectWarnings_ = 0;
|
textureBudgetRejectWarnings_ = 0;
|
||||||
whiteTexture_.reset();
|
whiteTexture_.reset();
|
||||||
glowTexture_.reset();
|
glowTexture_.reset();
|
||||||
|
|
@ -4251,6 +4253,7 @@ void M2Renderer::cleanupUnusedModels() {
|
||||||
}
|
}
|
||||||
|
|
||||||
VkTexture* M2Renderer::loadTexture(const std::string& path, uint32_t texFlags) {
|
VkTexture* M2Renderer::loadTexture(const std::string& path, uint32_t texFlags) {
|
||||||
|
constexpr uint64_t kFailedTextureRetryLookups = 512;
|
||||||
auto normalizeKey = [](std::string key) {
|
auto normalizeKey = [](std::string key) {
|
||||||
std::replace(key.begin(), key.end(), '/', '\\');
|
std::replace(key.begin(), key.end(), '/', '\\');
|
||||||
std::transform(key.begin(), key.end(), key.begin(),
|
std::transform(key.begin(), key.end(), key.begin(),
|
||||||
|
|
@ -4258,6 +4261,7 @@ VkTexture* M2Renderer::loadTexture(const std::string& path, uint32_t texFlags) {
|
||||||
return key;
|
return key;
|
||||||
};
|
};
|
||||||
std::string key = normalizeKey(path);
|
std::string key = normalizeKey(path);
|
||||||
|
const uint64_t lookupSerial = ++textureLookupSerial_;
|
||||||
|
|
||||||
// Check cache
|
// Check cache
|
||||||
auto it = textureCache.find(key);
|
auto it = textureCache.find(key);
|
||||||
|
|
@ -4265,7 +4269,10 @@ VkTexture* M2Renderer::loadTexture(const std::string& path, uint32_t texFlags) {
|
||||||
it->second.lastUse = ++textureCacheCounter_;
|
it->second.lastUse = ++textureCacheCounter_;
|
||||||
return it->second.texture.get();
|
return it->second.texture.get();
|
||||||
}
|
}
|
||||||
// No negative cache check — allow retries for transiently missing textures
|
auto failIt = failedTextureRetryAt_.find(key);
|
||||||
|
if (failIt != failedTextureRetryAt_.end() && lookupSerial < failIt->second) {
|
||||||
|
return whiteTexture_.get();
|
||||||
|
}
|
||||||
|
|
||||||
auto containsToken = [](const std::string& haystack, const char* token) {
|
auto containsToken = [](const std::string& haystack, const char* token) {
|
||||||
return haystack.find(token) != std::string::npos;
|
return haystack.find(token) != std::string::npos;
|
||||||
|
|
@ -4296,8 +4303,9 @@ VkTexture* M2Renderer::loadTexture(const std::string& path, uint32_t texFlags) {
|
||||||
blp = assetManager->loadTexture(key);
|
blp = assetManager->loadTexture(key);
|
||||||
}
|
}
|
||||||
if (!blp.isValid()) {
|
if (!blp.isValid()) {
|
||||||
// Return white fallback but don't cache the failure — MPQ reads can
|
// Cache misses briefly to avoid repeated expensive MPQ/disk probes.
|
||||||
// fail transiently during streaming; allow retry on next model load.
|
failedTextureCache_.insert(key);
|
||||||
|
failedTextureRetryAt_[key] = lookupSerial + kFailedTextureRetryLookups;
|
||||||
if (loggedTextureLoadFails_.insert(key).second) {
|
if (loggedTextureLoadFails_.insert(key).second) {
|
||||||
LOG_WARNING("M2: Failed to load texture: ", path);
|
LOG_WARNING("M2: Failed to load texture: ", path);
|
||||||
}
|
}
|
||||||
|
|
@ -4312,6 +4320,7 @@ VkTexture* M2Renderer::loadTexture(const std::string& path, uint32_t texFlags) {
|
||||||
// Cache budget-rejected keys too; without this we repeatedly decode/load
|
// Cache budget-rejected keys too; without this we repeatedly decode/load
|
||||||
// the same textures every frame once budget is saturated.
|
// the same textures every frame once budget is saturated.
|
||||||
failedTextureCache_.insert(key);
|
failedTextureCache_.insert(key);
|
||||||
|
failedTextureRetryAt_[key] = lookupSerial + kFailedTextureRetryLookups;
|
||||||
}
|
}
|
||||||
if (textureBudgetRejectWarnings_ < 3) {
|
if (textureBudgetRejectWarnings_ < 3) {
|
||||||
LOG_WARNING("M2 texture cache full (", textureCacheBytes_ / (1024 * 1024),
|
LOG_WARNING("M2 texture cache full (", textureCacheBytes_ / (1024 * 1024),
|
||||||
|
|
@ -4350,6 +4359,8 @@ VkTexture* M2Renderer::loadTexture(const std::string& path, uint32_t texFlags) {
|
||||||
e.lastUse = ++textureCacheCounter_;
|
e.lastUse = ++textureCacheCounter_;
|
||||||
textureCacheBytes_ += e.approxBytes;
|
textureCacheBytes_ += e.approxBytes;
|
||||||
textureCache[key] = std::move(e);
|
textureCache[key] = std::move(e);
|
||||||
|
failedTextureCache_.erase(key);
|
||||||
|
failedTextureRetryAt_.erase(key);
|
||||||
textureHasAlphaByPtr_[texPtr] = hasAlpha;
|
textureHasAlphaByPtr_[texPtr] = hasAlpha;
|
||||||
textureColorKeyBlackByPtr_[texPtr] = colorKeyBlackHint;
|
textureColorKeyBlackByPtr_[texPtr] = colorKeyBlackHint;
|
||||||
LOG_DEBUG("M2: Loaded texture: ", path, " (", blp.width, "x", blp.height, ")");
|
LOG_DEBUG("M2: Loaded texture: ", path, " (", blp.width, "x", blp.height, ")");
|
||||||
|
|
|
||||||
|
|
@ -54,9 +54,11 @@ int computeTerrainWorkerCount() {
|
||||||
|
|
||||||
unsigned hc = std::thread::hardware_concurrency();
|
unsigned hc = std::thread::hardware_concurrency();
|
||||||
if (hc > 0) {
|
if (hc > 0) {
|
||||||
// Use most cores for loading — leave 1-2 for render/update threads.
|
// Keep terrain workers conservative by default. Over-subscribing loader
|
||||||
const unsigned reserved = (hc >= 8u) ? 2u : 1u;
|
// threads can starve main-thread networking/render updates on large-core CPUs.
|
||||||
const unsigned targetWorkers = std::max(4u, hc - reserved);
|
const unsigned reserved = (hc >= 16u) ? 4u : ((hc >= 8u) ? 2u : 1u);
|
||||||
|
const unsigned maxDefaultWorkers = 8u;
|
||||||
|
const unsigned targetWorkers = std::max(4u, std::min(maxDefaultWorkers, hc - reserved));
|
||||||
return static_cast<int>(targetWorkers);
|
return static_cast<int>(targetWorkers);
|
||||||
}
|
}
|
||||||
return 4; // Fallback
|
return 4; // Fallback
|
||||||
|
|
@ -896,6 +898,9 @@ bool TerrainManager::advanceFinalization(FinalizingTile& ft) {
|
||||||
if (p.uniqueId != 0 && placedDoodadIds.count(p.uniqueId)) {
|
if (p.uniqueId != 0 && placedDoodadIds.count(p.uniqueId)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (!m2Renderer->hasModel(p.modelId)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
uint32_t instId = m2Renderer->createInstance(p.modelId, p.position, p.rotation, p.scale);
|
uint32_t instId = m2Renderer->createInstance(p.modelId, p.position, p.rotation, p.scale);
|
||||||
if (instId) {
|
if (instId) {
|
||||||
ft.m2InstanceIds.push_back(instId);
|
ft.m2InstanceIds.push_back(instId);
|
||||||
|
|
@ -961,6 +966,9 @@ bool TerrainManager::advanceFinalization(FinalizingTile& ft) {
|
||||||
if (wmoReady.uniqueId != 0 && placedWmoIds.count(wmoReady.uniqueId)) {
|
if (wmoReady.uniqueId != 0 && placedWmoIds.count(wmoReady.uniqueId)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (!wmoRenderer->isModelLoaded(wmoReady.modelId)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
uint32_t wmoInstId = wmoRenderer->createInstance(wmoReady.modelId, wmoReady.position, wmoReady.rotation);
|
uint32_t wmoInstId = wmoRenderer->createInstance(wmoReady.modelId, wmoReady.position, wmoReady.rotation);
|
||||||
if (wmoInstId) {
|
if (wmoInstId) {
|
||||||
ft.wmoInstanceIds.push_back(wmoInstId);
|
ft.wmoInstanceIds.push_back(wmoInstId);
|
||||||
|
|
|
||||||
|
|
@ -307,7 +307,9 @@ void WMORenderer::shutdown() {
|
||||||
textureCacheBytes_ = 0;
|
textureCacheBytes_ = 0;
|
||||||
textureCacheCounter_ = 0;
|
textureCacheCounter_ = 0;
|
||||||
failedTextureCache_.clear();
|
failedTextureCache_.clear();
|
||||||
|
failedTextureRetryAt_.clear();
|
||||||
loggedTextureLoadFails_.clear();
|
loggedTextureLoadFails_.clear();
|
||||||
|
textureLookupSerial_ = 0;
|
||||||
textureBudgetRejectWarnings_ = 0;
|
textureBudgetRejectWarnings_ = 0;
|
||||||
|
|
||||||
// Free white texture and flat normal texture
|
// Free white texture and flat normal texture
|
||||||
|
|
@ -1087,7 +1089,9 @@ void WMORenderer::clearAll() {
|
||||||
textureCacheBytes_ = 0;
|
textureCacheBytes_ = 0;
|
||||||
textureCacheCounter_ = 0;
|
textureCacheCounter_ = 0;
|
||||||
failedTextureCache_.clear();
|
failedTextureCache_.clear();
|
||||||
|
failedTextureRetryAt_.clear();
|
||||||
loggedTextureLoadFails_.clear();
|
loggedTextureLoadFails_.clear();
|
||||||
|
textureLookupSerial_ = 0;
|
||||||
textureBudgetRejectWarnings_ = 0;
|
textureBudgetRejectWarnings_ = 0;
|
||||||
precomputedFloorGrid.clear();
|
precomputedFloorGrid.clear();
|
||||||
|
|
||||||
|
|
@ -2237,6 +2241,7 @@ std::unique_ptr<VkTexture> WMORenderer::generateNormalHeightMap(
|
||||||
}
|
}
|
||||||
|
|
||||||
VkTexture* WMORenderer::loadTexture(const std::string& path) {
|
VkTexture* WMORenderer::loadTexture(const std::string& path) {
|
||||||
|
constexpr uint64_t kFailedTextureRetryLookups = 512;
|
||||||
if (!assetManager || !vkCtx_) {
|
if (!assetManager || !vkCtx_) {
|
||||||
return whiteTexture_.get();
|
return whiteTexture_.get();
|
||||||
}
|
}
|
||||||
|
|
@ -2312,7 +2317,19 @@ VkTexture* WMORenderer::loadTexture(const std::string& path) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto& attemptedCandidates = uniqueCandidates;
|
const uint64_t lookupSerial = ++textureLookupSerial_;
|
||||||
|
std::vector<std::string> attemptedCandidates;
|
||||||
|
attemptedCandidates.reserve(uniqueCandidates.size());
|
||||||
|
for (const auto& c : uniqueCandidates) {
|
||||||
|
auto fit = failedTextureRetryAt_.find(c);
|
||||||
|
if (fit != failedTextureRetryAt_.end() && lookupSerial < fit->second) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
attemptedCandidates.push_back(c);
|
||||||
|
}
|
||||||
|
if (attemptedCandidates.empty()) {
|
||||||
|
return whiteTexture_.get();
|
||||||
|
}
|
||||||
|
|
||||||
// Try loading all candidates until one succeeds
|
// Try loading all candidates until one succeeds
|
||||||
// Check pre-decoded BLP cache first (populated by background worker threads)
|
// Check pre-decoded BLP cache first (populated by background worker threads)
|
||||||
|
|
@ -2339,6 +2356,10 @@ VkTexture* WMORenderer::loadTexture(const std::string& path) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!blp.isValid()) {
|
if (!blp.isValid()) {
|
||||||
|
for (const auto& c : attemptedCandidates) {
|
||||||
|
failedTextureCache_.insert(c);
|
||||||
|
failedTextureRetryAt_[c] = lookupSerial + kFailedTextureRetryLookups;
|
||||||
|
}
|
||||||
if (loggedTextureLoadFails_.insert(key).second) {
|
if (loggedTextureLoadFails_.insert(key).second) {
|
||||||
core::Logger::getInstance().warning("WMO: Failed to load texture: ", path);
|
core::Logger::getInstance().warning("WMO: Failed to load texture: ", path);
|
||||||
}
|
}
|
||||||
|
|
@ -2353,6 +2374,10 @@ VkTexture* WMORenderer::loadTexture(const std::string& path) {
|
||||||
size_t base = static_cast<size_t>(blp.width) * static_cast<size_t>(blp.height) * 4ull;
|
size_t base = static_cast<size_t>(blp.width) * static_cast<size_t>(blp.height) * 4ull;
|
||||||
size_t approxBytes = base + (base / 3);
|
size_t approxBytes = base + (base / 3);
|
||||||
if (textureCacheBytes_ + approxBytes > textureCacheBudgetBytes_) {
|
if (textureCacheBytes_ + approxBytes > textureCacheBudgetBytes_) {
|
||||||
|
for (const auto& c : attemptedCandidates) {
|
||||||
|
failedTextureCache_.insert(c);
|
||||||
|
failedTextureRetryAt_[c] = lookupSerial + kFailedTextureRetryLookups;
|
||||||
|
}
|
||||||
if (textureBudgetRejectWarnings_ < 3) {
|
if (textureBudgetRejectWarnings_ < 3) {
|
||||||
core::Logger::getInstance().warning(
|
core::Logger::getInstance().warning(
|
||||||
"WMO texture cache full (", textureCacheBytes_ / (1024 * 1024),
|
"WMO texture cache full (", textureCacheBytes_ / (1024 * 1024),
|
||||||
|
|
@ -2394,8 +2419,12 @@ VkTexture* WMORenderer::loadTexture(const std::string& path) {
|
||||||
textureCacheBytes_ += e.approxBytes;
|
textureCacheBytes_ += e.approxBytes;
|
||||||
if (!resolvedKey.empty()) {
|
if (!resolvedKey.empty()) {
|
||||||
textureCache[resolvedKey] = std::move(e);
|
textureCache[resolvedKey] = std::move(e);
|
||||||
|
failedTextureCache_.erase(resolvedKey);
|
||||||
|
failedTextureRetryAt_.erase(resolvedKey);
|
||||||
} else {
|
} else {
|
||||||
textureCache[key] = std::move(e);
|
textureCache[key] = std::move(e);
|
||||||
|
failedTextureCache_.erase(key);
|
||||||
|
failedTextureRetryAt_.erase(key);
|
||||||
}
|
}
|
||||||
core::Logger::getInstance().debug("WMO: Loaded texture: ", path, " (", blp.width, "x", blp.height, ")");
|
core::Logger::getInstance().debug("WMO: Loaded texture: ", path, " (", blp.width, "x", blp.height, ")");
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue