Compare commits

...

10 commits

Author SHA1 Message Date
Kelsi
05d1c874d9 Fix auth background ImTextureID cast for OpenGL build 2026-02-22 20:55:24 -08:00
Kelsi
a1c41422cc Port Vulkan sign-in and loading screen visuals to OpenGL 2026-02-22 20:49:25 -08:00
Kelsi
a81a418dd2 Port terrain chunk coordinate fix from vulkan 2026-02-22 20:46:34 -08:00
Kelsi
d87b27a24a Port TBC/Turtle packet parsing fallbacks from vulkan 2026-02-22 20:44:50 -08:00
Kelsi
33c1a33059 Port runtime logging and parser hardening from vulkan 2026-02-22 20:40:21 -08:00
Kelsi
d2ec0af6d9 Reduce update-object and inventory update overhead
(cherry picked from commit 0631b9f5dc)
2026-02-22 20:37:19 -08:00
Kelsi
2820463c7a Optimize update-object field mask parsing
(cherry picked from commit 37888c666d)
2026-02-22 20:37:19 -08:00
Kelsi
e20341202a Fix update-object spline parsing regression
(cherry picked from commit 3a9bd0d4e5)
2026-02-22 20:37:19 -08:00
Kelsi
cce0b7e42b Optimize world socket buffer handling and logging
(cherry picked from commit 17a2a1f7ef)
2026-02-22 20:37:19 -08:00
Kelsi
250fcd4f2e Add Discord badge to README 2026-02-21 22:10:33 -08:00
21 changed files with 824 additions and 373 deletions

View file

@ -7,6 +7,7 @@
A native C++ World of Warcraft client with a custom OpenGL renderer.
[![Sponsor](https://img.shields.io/github/sponsors/Kelsidavis?label=Sponsor&logo=GitHub)](https://github.com/sponsors/Kelsidavis)
[![Discord](https://img.shields.io/discord/1?label=Discord&logo=discord)](https://discord.gg/SDqjA79B)
[![Watch the video](https://img.youtube.com/vi/Pd9JuYYxu0o/maxresdefault.jpg)](https://youtu.be/Pd9JuYYxu0o)

BIN
assets/krayonload.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

BIN
assets/krayonsignin.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

View file

@ -5,6 +5,8 @@
#include <sstream>
#include <mutex>
#include <fstream>
#include <atomic>
#include <chrono>
namespace wowee {
namespace core {
@ -30,29 +32,35 @@ public:
void log(LogLevel level, const std::string& message);
void setLogLevel(LogLevel level);
bool shouldLog(LogLevel level) const;
template<typename... Args>
void debug(Args&&... args) {
if (!shouldLog(LogLevel::DEBUG)) return;
log(LogLevel::DEBUG, format(std::forward<Args>(args)...));
}
template<typename... Args>
void info(Args&&... args) {
if (!shouldLog(LogLevel::INFO)) return;
log(LogLevel::INFO, format(std::forward<Args>(args)...));
}
template<typename... Args>
void warning(Args&&... args) {
if (!shouldLog(LogLevel::WARNING)) return;
log(LogLevel::WARNING, format(std::forward<Args>(args)...));
}
template<typename... Args>
void error(Args&&... args) {
if (!shouldLog(LogLevel::ERROR)) return;
log(LogLevel::ERROR, format(std::forward<Args>(args)...));
}
template<typename... Args>
void fatal(Args&&... args) {
if (!shouldLog(LogLevel::FATAL)) return;
log(LogLevel::FATAL, format(std::forward<Args>(args)...));
}
@ -69,10 +77,13 @@ private:
return oss.str();
}
LogLevel minLevel = LogLevel::INFO; // Changed from DEBUG to reduce log spam
std::atomic<int> minLevel_{static_cast<int>(LogLevel::INFO)};
std::mutex mutex;
std::ofstream fileStream;
bool fileReady = false;
bool echoToStdout_ = true;
std::chrono::steady_clock::time_point lastFlushTime_{};
uint32_t flushIntervalMs_ = 250;
void ensureFile();
};

View file

@ -353,6 +353,7 @@ class TurtlePacketParsers : public ClassicPacketParsers {
public:
uint8_t movementFlags2Size() const override { return 0; }
bool parseMovementBlock(network::Packet& packet, UpdateBlock& block) override;
bool parseMonsterMove(network::Packet& packet, MonsterMoveData& data) override;
};
/**

View file

@ -12,6 +12,7 @@ public:
Packet() = default;
explicit Packet(uint16_t opcode);
Packet(uint16_t opcode, const std::vector<uint8_t>& data);
Packet(uint16_t opcode, std::vector<uint8_t>&& data);
void writeUInt8(uint8_t value);
void writeUInt16(uint16_t value);

View file

@ -91,6 +91,13 @@ private:
// Receive buffer
std::vector<uint8_t> receiveBuffer;
size_t receiveReadOffset_ = 0;
// Optional reused packet queue (feature-gated) to reduce per-update allocations.
std::vector<Packet> parsedPacketsScratch_;
// Runtime-gated network optimization toggles (default off).
bool useFastRecvAppend_ = false;
bool useParseScratchQueue_ = false;
// Track how many header bytes have been decrypted (0-4)
// This prevents re-decrypting the same header when waiting for more data

View file

@ -1,7 +1,7 @@
#pragma once
#include "auth/auth_handler.hpp"
#include "rendering/video_player.hpp"
#include <cstdint>
#include <string>
#include <vector>
#include <functional>
@ -16,6 +16,7 @@ namespace wowee { namespace ui {
class AuthScreen {
public:
AuthScreen();
~AuthScreen();
/**
* Render the UI
@ -103,9 +104,13 @@ private:
void upsertCurrentServerProfile(bool includePasswordHash);
std::string currentExpansionId() const;
// Background video
bool videoInitAttempted = false;
rendering::VideoPlayer backgroundVideo;
// Background image (OpenGL texture)
bool bgInitAttempted = false;
bool loadBackgroundImage();
void destroyBackgroundImage();
uint32_t bgTextureId_ = 0;
int bgWidth_ = 0;
int bgHeight_ = 0;
bool musicInitAttempted = false;
bool musicPlaying = false;

View file

@ -58,6 +58,14 @@
namespace wowee {
namespace core {
namespace {
bool envFlagEnabled(const char* key, bool defaultValue = false) {
const char* raw = std::getenv(key);
if (!raw || !*raw) return defaultValue;
return !(raw[0] == '0' || raw[0] == 'f' || raw[0] == 'F' ||
raw[0] == 'n' || raw[0] == 'N');
}
} // namespace
const char* Application::mapIdToName(uint32_t mapId) {
switch (mapId) {
@ -221,6 +229,10 @@ bool Application::initialize() {
void Application::run() {
LOG_INFO("Starting main loop");
const bool frameProfileEnabled = envFlagEnabled("WOWEE_FRAME_PROFILE", false);
if (frameProfileEnabled) {
LOG_INFO("Frame timing profile enabled (WOWEE_FRAME_PROFILE=1)");
}
auto lastTime = std::chrono::high_resolution_clock::now();
@ -336,8 +348,11 @@ void Application::run() {
totalSwapMs += std::chrono::duration<double, std::milli>(t4 - t3).count();
if (++frameCount >= 60) {
printf("[Frame] Update: %.1f ms, Render: %.1f ms, Swap: %.1f ms\n",
totalUpdateMs / 60.0, totalRenderMs / 60.0, totalSwapMs / 60.0);
if (frameProfileEnabled && core::Logger::getInstance().shouldLog(core::LogLevel::DEBUG)) {
LOG_DEBUG("[Frame] Update: ", totalUpdateMs / 60.0,
"ms Render: ", totalRenderMs / 60.0,
"ms Swap: ", totalSwapMs / 60.0, "ms");
}
frameCount = 0;
totalUpdateMs = totalRenderMs = totalSwapMs = 0;
}

View file

@ -3,6 +3,7 @@
#include <iomanip>
#include <ctime>
#include <filesystem>
#include <cstdlib>
namespace wowee {
namespace core {
@ -15,13 +16,26 @@ Logger& Logger::getInstance() {
void Logger::ensureFile() {
if (fileReady) return;
fileReady = true;
if (const char* logStdout = std::getenv("WOWEE_LOG_STDOUT")) {
if (logStdout[0] == '0') {
echoToStdout_ = false;
}
}
if (const char* flushMs = std::getenv("WOWEE_LOG_FLUSH_MS")) {
char* end = nullptr;
unsigned long parsed = std::strtoul(flushMs, &end, 10);
if (end != flushMs && parsed <= 10000ul) {
flushIntervalMs_ = static_cast<uint32_t>(parsed);
}
}
std::error_code ec;
std::filesystem::create_directories("logs", ec);
fileStream.open("logs/wowee.log", std::ios::out | std::ios::trunc);
lastFlushTime_ = std::chrono::steady_clock::now();
}
void Logger::log(LogLevel level, const std::string& message) {
if (level < minLevel) {
if (!shouldLog(level)) {
return;
}
@ -58,15 +72,32 @@ void Logger::log(LogLevel level, const std::string& message) {
line << "] " << message;
std::cout << line.str() << '\n';
if (echoToStdout_) {
std::cout << line.str() << '\n';
}
if (fileStream.is_open()) {
fileStream << line.str() << '\n';
fileStream.flush();
bool shouldFlush = (level >= LogLevel::WARNING);
if (!shouldFlush) {
auto nowSteady = std::chrono::steady_clock::now();
auto elapsedMs = std::chrono::duration_cast<std::chrono::milliseconds>(nowSteady - lastFlushTime_).count();
shouldFlush = (elapsedMs >= static_cast<long long>(flushIntervalMs_));
if (shouldFlush) {
lastFlushTime_ = nowSteady;
}
}
if (shouldFlush) {
fileStream.flush();
}
}
}
void Logger::setLogLevel(LogLevel level) {
minLevel = level;
minLevel_.store(static_cast<int>(level), std::memory_order_relaxed);
}
bool Logger::shouldLog(LogLevel level) const {
return static_cast<int>(level) >= minLevel_.load(std::memory_order_relaxed);
}
} // namespace core

View file

@ -38,7 +38,9 @@
#include <array>
#include <cstdlib>
#include <cstring>
#include <exception>
#include <limits>
#include <new>
#include <openssl/sha.h>
#include <openssl/hmac.h>
@ -93,6 +95,13 @@ bool isClassicLikeExpansion() {
return isActiveExpansion("classic") || isActiveExpansion("turtle");
}
bool envFlagEnabled(const char* key, bool defaultValue = false) {
const char* raw = std::getenv(key);
if (!raw || !*raw) return defaultValue;
return !(raw[0] == '0' || raw[0] == 'f' || raw[0] == 'F' ||
raw[0] == 'n' || raw[0] == 'N');
}
std::string formatCopperAmount(uint32_t amount) {
uint32_t gold = amount / 10000;
uint32_t silver = (amount / 100) % 100;
@ -1134,6 +1143,8 @@ void GameHandler::handlePacket(network::Packet& packet) {
}
uint16_t opcode = packet.getOpcode();
try {
const bool allowVanillaAliases = isClassicLikeExpansion() || isActiveExpansion("tbc");
// Vanilla compatibility aliases:
// - 0x006B: can be SMSG_COMPRESSED_MOVES on some vanilla-family servers
@ -1141,7 +1152,7 @@ void GameHandler::handlePacket(network::Packet& packet) {
// - 0x0103: SMSG_PLAY_MUSIC (some vanilla-family servers)
//
// We gate these by payload shape so expansion-native mappings remain intact.
if (opcode == 0x006B) {
if (allowVanillaAliases && opcode == 0x006B) {
// Try compressed movement batch first:
// [u8 subSize][u16 subOpcode][subPayload...] ...
// where subOpcode is typically SMSG_MONSTER_MOVE / SMSG_MONSTER_MOVE_TRANSPORT.
@ -1189,7 +1200,7 @@ void GameHandler::handlePacket(network::Packet& packet) {
// Not weather-shaped: rewind and fall through to normal opcode table handling.
packet.setReadPos(0);
}
} else if (opcode == 0x0103) {
} else if (allowVanillaAliases && opcode == 0x0103) {
// Expected play-music payload: uint32 sound/music id
if (packet.getSize() - packet.getReadPos() == 4) {
uint32_t soundId = packet.readUInt32();
@ -1953,11 +1964,22 @@ void GameHandler::handlePacket(network::Packet& packet) {
worldStateZoneId_ = packet.readUInt32();
uint16_t count = packet.readUInt16();
size_t needed = static_cast<size_t>(count) * 8;
if (packet.getSize() - packet.getReadPos() < needed) {
LOG_WARNING("SMSG_INIT_WORLD_STATES truncated: expected ", needed,
" bytes of state pairs, got ", packet.getSize() - packet.getReadPos());
packet.setReadPos(packet.getSize());
break;
size_t available = packet.getSize() - packet.getReadPos();
if (available < needed) {
// Be tolerant across expansion/private-core variants: if packet shape
// still looks like N*(key,val) dwords, parse what is present.
if ((available % 8) == 0) {
uint16_t adjustedCount = static_cast<uint16_t>(available / 8);
LOG_WARNING("SMSG_INIT_WORLD_STATES count mismatch: header=", count,
" adjusted=", adjustedCount, " (available=", available, ")");
count = adjustedCount;
needed = available;
} else {
LOG_WARNING("SMSG_INIT_WORLD_STATES truncated: expected ", needed,
" bytes of state pairs, got ", available);
packet.setReadPos(packet.getSize());
break;
}
}
worldStates_.clear();
worldStates_.reserve(count);
@ -2849,6 +2871,23 @@ void GameHandler::handlePacket(network::Packet& packet) {
}
break;
}
} catch (const std::bad_alloc& e) {
LOG_ERROR("OOM while handling world opcode=0x", std::hex, opcode, std::dec,
" state=", worldStateName(state),
" size=", packet.getSize(),
" readPos=", packet.getReadPos(),
" what=", e.what());
if (socket && state == WorldState::IN_WORLD) {
disconnect();
fail("Out of memory while parsing world packet");
}
} catch (const std::exception& e) {
LOG_ERROR("Exception while handling world opcode=0x", std::hex, opcode, std::dec,
" state=", worldStateName(state),
" size=", packet.getSize(),
" readPos=", packet.getReadPos(),
" what=", e.what());
}
}
void GameHandler::handleAuthChallenge(network::Packet& packet) {
@ -4581,6 +4620,7 @@ void GameHandler::setOrientation(float orientation) {
}
void GameHandler::handleUpdateObject(network::Packet& packet) {
static const bool kVerboseUpdateObject = envFlagEnabled("WOWEE_LOG_UPDATE_OBJECT_VERBOSE", false);
UpdateObjectData data;
if (!packetParsers_->parseUpdateObject(packet, data)) {
LOG_WARNING("Failed to parse SMSG_UPDATE_OBJECT");
@ -4710,48 +4750,46 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
// Process out-of-range objects first
for (uint64_t guid : data.outOfRangeGuids) {
if (entityManager.hasEntity(guid)) {
const bool isKnownTransport = transportGuids_.count(guid) > 0;
if (isKnownTransport) {
// Keep transports alive across out-of-range flapping.
// Boats/zeppelins are global movers and removing them here can make
// them disappear until a later movement snapshot happens to recreate them.
const bool playerAboardNow = (playerTransportGuid_ == guid);
const bool stickyAboard = (playerTransportStickyGuid_ == guid && playerTransportStickyTimer_ > 0.0f);
const bool movementSaysAboard = (movementInfo.transportGuid == guid);
LOG_INFO("Preserving transport on out-of-range: 0x",
std::hex, guid, std::dec,
" now=", playerAboardNow,
" sticky=", stickyAboard,
" movement=", movementSaysAboard);
continue;
}
auto entity = entityManager.getEntity(guid);
if (!entity) continue;
LOG_DEBUG("Entity went out of range: 0x", std::hex, guid, std::dec);
// Trigger despawn callbacks before removing entity
auto entity = entityManager.getEntity(guid);
if (entity) {
if (entity->getType() == ObjectType::UNIT && creatureDespawnCallback_) {
creatureDespawnCallback_(guid);
} else if (entity->getType() == ObjectType::PLAYER && playerDespawnCallback_) {
playerDespawnCallback_(guid);
otherPlayerVisibleItemEntries_.erase(guid);
otherPlayerVisibleDirty_.erase(guid);
otherPlayerMoveTimeMs_.erase(guid);
inspectedPlayerItemEntries_.erase(guid);
pendingAutoInspect_.erase(guid);
} else if (entity->getType() == ObjectType::GAMEOBJECT && gameObjectDespawnCallback_) {
gameObjectDespawnCallback_(guid);
}
}
transportGuids_.erase(guid);
serverUpdatedTransportGuids_.erase(guid);
clearTransportAttachment(guid);
if (playerTransportGuid_ == guid) {
clearPlayerTransport();
}
entityManager.removeEntity(guid);
const bool isKnownTransport = transportGuids_.count(guid) > 0;
if (isKnownTransport) {
// Keep transports alive across out-of-range flapping.
// Boats/zeppelins are global movers and removing them here can make
// them disappear until a later movement snapshot happens to recreate them.
const bool playerAboardNow = (playerTransportGuid_ == guid);
const bool stickyAboard = (playerTransportStickyGuid_ == guid && playerTransportStickyTimer_ > 0.0f);
const bool movementSaysAboard = (movementInfo.transportGuid == guid);
LOG_INFO("Preserving transport on out-of-range: 0x",
std::hex, guid, std::dec,
" now=", playerAboardNow,
" sticky=", stickyAboard,
" movement=", movementSaysAboard);
continue;
}
LOG_DEBUG("Entity went out of range: 0x", std::hex, guid, std::dec);
// Trigger despawn callbacks before removing entity
if (entity->getType() == ObjectType::UNIT && creatureDespawnCallback_) {
creatureDespawnCallback_(guid);
} else if (entity->getType() == ObjectType::PLAYER && playerDespawnCallback_) {
playerDespawnCallback_(guid);
otherPlayerVisibleItemEntries_.erase(guid);
otherPlayerVisibleDirty_.erase(guid);
otherPlayerMoveTimeMs_.erase(guid);
inspectedPlayerItemEntries_.erase(guid);
pendingAutoInspect_.erase(guid);
} else if (entity->getType() == ObjectType::GAMEOBJECT && gameObjectDespawnCallback_) {
gameObjectDespawnCallback_(guid);
}
transportGuids_.erase(guid);
serverUpdatedTransportGuids_.erase(guid);
clearTransportAttachment(guid);
if (playerTransportGuid_ == guid) {
clearPlayerTransport();
}
entityManager.removeEntity(guid);
}
// Process update blocks
@ -5081,92 +5119,21 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
// Extract XP / inventory slot / skill fields for player entity
if (block.guid == playerGuid && block.objectType == ObjectType::PLAYER) {
// Store baseline snapshot on first update
static bool baselineStored = false;
static std::map<uint16_t, uint32_t> baselineFields;
if (!baselineStored) {
baselineFields = block.fields;
baselineStored = true;
LOG_INFO("===== BASELINE PLAYER FIELDS STORED =====");
LOG_INFO(" Total fields: ", block.fields.size());
}
// Diff against baseline to find changes
std::vector<uint16_t> changedIndices;
std::vector<uint16_t> newIndices;
std::vector<uint16_t> removedIndices;
for (const auto& [idx, val] : block.fields) {
auto it = baselineFields.find(idx);
if (it == baselineFields.end()) {
newIndices.push_back(idx);
} else if (it->second != val) {
changedIndices.push_back(idx);
}
}
for (const auto& [idx, val] : baselineFields) {
if (block.fields.find(idx) == block.fields.end()) {
removedIndices.push_back(idx);
}
}
// Auto-detect coinage index using the previous snapshot vs this full snapshot.
maybeDetectCoinageIndex(lastPlayerFields_, block.fields);
lastPlayerFields_ = block.fields;
detectInventorySlotBases(block.fields);
// Debug: Show field changes
LOG_INFO("Player update with ", block.fields.size(), " fields");
if (!changedIndices.empty() || !newIndices.empty() || !removedIndices.empty()) {
LOG_INFO(" ===== FIELD CHANGES DETECTED =====");
if (!changedIndices.empty()) {
LOG_INFO(" Changed fields (", changedIndices.size(), "):");
std::sort(changedIndices.begin(), changedIndices.end());
for (size_t i = 0; i < std::min(size_t(30), changedIndices.size()); ++i) {
uint16_t idx = changedIndices[i];
uint32_t oldVal = baselineFields[idx];
uint32_t newVal = block.fields.at(idx);
LOG_INFO(" [", idx, "]: ", oldVal, " -> ", newVal,
" (0x", std::hex, oldVal, " -> 0x", newVal, std::dec, ")");
}
if (changedIndices.size() > 30) {
LOG_INFO(" ... (", changedIndices.size() - 30, " more)");
}
}
if (!newIndices.empty()) {
LOG_INFO(" New fields (", newIndices.size(), "):");
std::sort(newIndices.begin(), newIndices.end());
for (size_t i = 0; i < std::min(size_t(20), newIndices.size()); ++i) {
uint16_t idx = newIndices[i];
uint32_t val = block.fields.at(idx);
LOG_INFO(" [", idx, "]: ", val, " (0x", std::hex, val, std::dec, ")");
}
if (newIndices.size() > 20) {
LOG_INFO(" ... (", newIndices.size() - 20, " more)");
}
}
if (!removedIndices.empty()) {
LOG_INFO(" Removed fields (", removedIndices.size(), "):");
std::sort(removedIndices.begin(), removedIndices.end());
for (size_t i = 0; i < std::min(size_t(20), removedIndices.size()); ++i) {
uint16_t idx = removedIndices[i];
uint32_t val = baselineFields.at(idx);
LOG_INFO(" [", idx, "]: was ", val, " (0x", std::hex, val, std::dec, ")");
}
if (kVerboseUpdateObject) {
uint16_t maxField = 0;
for (const auto& [key, _val] : block.fields) {
if (key > maxField) maxField = key;
}
LOG_INFO("Player update with ", block.fields.size(),
" fields (max index=", maxField, ")");
}
uint16_t maxField = 0;
for (const auto& [key, val] : block.fields) {
if (key > maxField) maxField = key;
}
LOG_INFO(" Highest field index: ", maxField);
bool slotsChanged = false;
const uint16_t ufPlayerXp = fieldIndex(UF::PLAYER_XP);
const uint16_t ufPlayerNextXp = fieldIndex(UF::PLAYER_NEXT_LEVEL_XP);
@ -5184,11 +5151,11 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
}
else if (key == ufCoinage) {
playerMoneyCopper_ = val;
LOG_INFO("Money set from update fields: ", val, " copper");
LOG_DEBUG("Money set from update fields: ", val, " copper");
}
else if (ufArmor != 0xFFFF && key == ufArmor) {
playerArmorRating_ = static_cast<int32_t>(val);
LOG_INFO("Armor rating from update fields: ", playerArmorRating_);
LOG_DEBUG("Armor rating from update fields: ", playerArmorRating_);
}
// Do not synthesize quest-log entries from raw update-field slots.
// Slot layouts differ on some classic-family realms and can produce
@ -5426,7 +5393,12 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
}
// Update XP / inventory slot / skill fields for player entity
if (block.guid == playerGuid) {
std::map<uint16_t, uint32_t> oldFieldsSnapshot = lastPlayerFields_;
const bool needCoinageDetectSnapshot =
(pendingMoneyDelta_ != 0 && pendingMoneyDeltaTimer_ > 0.0f);
std::map<uint16_t, uint32_t> oldFieldsSnapshot;
if (needCoinageDetectSnapshot) {
oldFieldsSnapshot = lastPlayerFields_;
}
if (block.hasMovement && block.runSpeed > 0.1f && block.runSpeed < 100.0f) {
serverRunSpeed_ = block.runSpeed;
// Some server dismount paths update run speed without updating mount display field.
@ -5440,10 +5412,13 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
}
}
}
auto mergeHint = lastPlayerFields_.end();
for (const auto& [key, val] : block.fields) {
lastPlayerFields_[key] = val;
mergeHint = lastPlayerFields_.insert_or_assign(mergeHint, key, val);
}
if (needCoinageDetectSnapshot) {
maybeDetectCoinageIndex(oldFieldsSnapshot, lastPlayerFields_);
}
maybeDetectCoinageIndex(oldFieldsSnapshot, lastPlayerFields_);
maybeDetectVisibleItemLayout();
detectInventorySlotBases(block.fields);
bool slotsChanged = false;
@ -5456,15 +5431,15 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
for (const auto& [key, val] : block.fields) {
if (key == ufPlayerXp) {
playerXp_ = val;
LOG_INFO("XP updated: ", val);
LOG_DEBUG("XP updated: ", val);
}
else if (key == ufPlayerNextXp) {
playerNextLevelXp_ = val;
LOG_INFO("Next level XP updated: ", val);
LOG_DEBUG("Next level XP updated: ", val);
}
else if (key == ufPlayerLevel) {
serverPlayerLevel_ = val;
LOG_INFO("Level updated: ", val);
LOG_DEBUG("Level updated: ", val);
for (auto& ch : characters) {
if (ch.guid == playerGuid) {
ch.level = val;
@ -5474,7 +5449,7 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
}
else if (key == ufCoinage) {
playerMoneyCopper_ = val;
LOG_INFO("Money updated via VALUES: ", val, " copper");
LOG_DEBUG("Money updated via VALUES: ", val, " copper");
}
else if (ufArmor != 0xFFFF && key == ufArmor) {
playerArmorRating_ = static_cast<int32_t>(val);
@ -5505,17 +5480,33 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
// Update item stack count for online items
if (entity->getType() == ObjectType::ITEM || entity->getType() == ObjectType::CONTAINER) {
bool inventoryChanged = false;
const uint16_t itemStackField = fieldIndex(UF::ITEM_FIELD_STACK_COUNT);
const uint16_t containerNumSlotsField = fieldIndex(UF::CONTAINER_FIELD_NUM_SLOTS);
const uint16_t containerSlot1Field = fieldIndex(UF::CONTAINER_FIELD_SLOT_1);
for (const auto& [key, val] : block.fields) {
if (key == fieldIndex(UF::ITEM_FIELD_STACK_COUNT)) {
if (key == itemStackField) {
auto it = onlineItems_.find(block.guid);
if (it != onlineItems_.end()) it->second.stackCount = val;
if (it != onlineItems_.end() && it->second.stackCount != val) {
it->second.stackCount = val;
inventoryChanged = true;
}
}
}
// Update container slot GUIDs on bag content changes
if (entity->getType() == ObjectType::CONTAINER) {
for (const auto& [key, _] : block.fields) {
if ((containerNumSlotsField != 0xFFFF && key == containerNumSlotsField) ||
(containerSlot1Field != 0xFFFF && key >= containerSlot1Field && key < containerSlot1Field + 72)) {
inventoryChanged = true;
break;
}
}
extractContainerFields(block.guid, block.fields);
}
rebuildOnlineInventory();
if (inventoryChanged) {
rebuildOnlineInventory();
}
}
if (block.hasMovement && entity->getType() == ObjectType::GAMEOBJECT) {
if (transportGuids_.count(block.guid) && transportMoveCallback_) {
@ -8650,10 +8641,30 @@ void GameHandler::handleCompressedMoves(network::Packet& packet) {
void GameHandler::handleMonsterMove(network::Packet& packet) {
MonsterMoveData data;
auto logMonsterMoveParseFailure = [&](const std::string& msg) {
static uint32_t failCount = 0;
++failCount;
if (failCount <= 10 || (failCount % 100) == 0) {
LOG_WARNING(msg, " (occurrence=", failCount, ")");
}
};
auto stripWrappedSubpacket = [&](const std::vector<uint8_t>& bytes, std::vector<uint8_t>& stripped) -> bool {
if (bytes.size() < 3) return false;
uint8_t subSize = bytes[0];
if (subSize < 2) return false;
size_t wrappedLen = static_cast<size_t>(subSize) + 1; // size byte + body
if (wrappedLen != bytes.size()) return false;
size_t payloadLen = static_cast<size_t>(subSize) - 2; // opcode(2) stripped
if (3 + payloadLen > bytes.size()) return false;
stripped.assign(bytes.begin() + 3, bytes.begin() + 3 + payloadLen);
return true;
};
// Turtle WoW (1.17+) compresses each SMSG_MONSTER_MOVE individually:
// format: uint32 decompressedSize + zlib data (zlib magic = 0x78 ??)
const auto& rawData = packet.getData();
bool isCompressed = rawData.size() >= 6 &&
const bool allowTurtleMoveCompression = isActiveExpansion("turtle");
bool isCompressed = allowTurtleMoveCompression &&
rawData.size() >= 6 &&
rawData[4] == 0x78 &&
(rawData[5] == 0x01 || rawData[5] == 0x9C ||
rawData[5] == 0xDA || rawData[5] == 0x5E);
@ -8685,36 +8696,42 @@ void GameHandler::handleMonsterMove(network::Packet& packet) {
}
LOG_INFO("MonsterMove decomp[", destLen, "]: ", hex);
}
// Some Turtle WoW compressed move payloads include an inner
// sub-packet wrapper: uint8 size + uint16 opcode + payload.
// Do not key this on expansion opcode mappings; strip by structure.
std::vector<uint8_t> parseBytes = decompressed;
if (destLen >= 3) {
uint8_t subSize = decompressed[0];
size_t wrappedLen = static_cast<size_t>(subSize) + 1; // size byte + subSize bytes
uint16_t innerOpcode = static_cast<uint16_t>(decompressed[1]) |
(static_cast<uint16_t>(decompressed[2]) << 8);
uint16_t monsterMoveWire = wireOpcode(Opcode::SMSG_MONSTER_MOVE);
bool looksLikeMonsterMoveWrapper =
(innerOpcode == 0x00DD) || (innerOpcode == monsterMoveWire);
// Strict case: one exact wrapped sub-packet in this decompressed blob.
if (subSize >= 2 && wrappedLen == destLen && looksLikeMonsterMoveWrapper) {
size_t payloadStart = 3;
size_t payloadLen = static_cast<size_t>(subSize) - 2;
parseBytes.assign(decompressed.begin() + payloadStart,
decompressed.begin() + payloadStart + payloadLen);
}
}
std::vector<uint8_t> stripped;
bool hasWrappedForm = stripWrappedSubpacket(decompressed, stripped);
network::Packet decompPacket(packet.getOpcode(), parseBytes);
// Try unwrapped payload first (common form), then wrapped-subpacket fallback.
network::Packet decompPacket(packet.getOpcode(), decompressed);
if (!packetParsers_->parseMonsterMove(decompPacket, data)) {
LOG_WARNING("Failed to parse vanilla SMSG_MONSTER_MOVE (decompressed ",
destLen, " bytes, parseBytes ", parseBytes.size(), " bytes)");
return;
if (!hasWrappedForm) {
logMonsterMoveParseFailure("Failed to parse SMSG_MONSTER_MOVE (decompressed " +
std::to_string(destLen) + " bytes)");
return;
}
network::Packet wrappedPacket(packet.getOpcode(), stripped);
if (!packetParsers_->parseMonsterMove(wrappedPacket, data)) {
logMonsterMoveParseFailure("Failed to parse SMSG_MONSTER_MOVE (decompressed " +
std::to_string(destLen) + " bytes, wrapped payload " +
std::to_string(stripped.size()) + " bytes)");
return;
}
LOG_WARNING("SMSG_MONSTER_MOVE parsed via wrapped-subpacket fallback");
}
} else if (!packetParsers_->parseMonsterMove(packet, data)) {
LOG_WARNING("Failed to parse SMSG_MONSTER_MOVE");
return;
// Some realms occasionally embed an extra [size|opcode] wrapper even when the
// outer packet wasn't zlib-compressed. Retry with wrapper stripped by structure.
std::vector<uint8_t> stripped;
if (stripWrappedSubpacket(rawData, stripped)) {
network::Packet wrappedPacket(packet.getOpcode(), stripped);
if (packetParsers_->parseMonsterMove(wrappedPacket, data)) {
LOG_WARNING("SMSG_MONSTER_MOVE parsed via uncompressed wrapped-subpacket fallback");
} else {
logMonsterMoveParseFailure("Failed to parse SMSG_MONSTER_MOVE");
return;
}
} else {
logMonsterMoveParseFailure("Failed to parse SMSG_MONSTER_MOVE");
return;
}
}
// Update entity position in entity manager

View file

@ -1192,6 +1192,24 @@ bool TurtlePacketParsers::parseMovementBlock(network::Packet& packet, UpdateBloc
return true;
}
bool TurtlePacketParsers::parseMonsterMove(network::Packet& packet, MonsterMoveData& data) {
// 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.
size_t start = packet.getReadPos();
if (MonsterMoveParser::parseVanilla(packet, data)) {
return true;
}
packet.setReadPos(start);
if (MonsterMoveParser::parse(packet, data)) {
LOG_WARNING("[Turtle] SMSG_MONSTER_MOVE parsed via WotLK fallback layout");
return true;
}
packet.setReadPos(start);
return false;
}
// ============================================================================
// Classic/Vanilla quest giver status
//

View file

@ -1,5 +1,6 @@
#include "game/packet_parsers.hpp"
#include "core/logger.hpp"
#include <utility>
namespace wowee {
namespace game {
@ -376,83 +377,125 @@ bool TbcPacketParsers::parseCharEnum(network::Packet& packet, CharEnumResponse&
// (WotLK removed this field)
// ============================================================================
bool TbcPacketParsers::parseUpdateObject(network::Packet& packet, UpdateObjectData& data) {
// Read block count
data.blockCount = packet.readUInt32();
constexpr uint32_t kMaxReasonableUpdateBlocks = 4096;
auto parseWithLayout = [&](bool withHasTransportByte, UpdateObjectData& out) -> bool {
out = UpdateObjectData{};
size_t start = packet.getReadPos();
if (packet.getSize() - start < 4) return false;
// TBC/Classic: has_transport byte (WotLK removed this)
/*uint8_t hasTransport =*/ packet.readUInt8();
LOG_DEBUG("[TBC] SMSG_UPDATE_OBJECT: objectCount=", data.blockCount);
// Check for out-of-range objects first
if (packet.getReadPos() + 1 <= packet.getSize()) {
uint8_t firstByte = packet.readUInt8();
if (firstByte == static_cast<uint8_t>(UpdateType::OUT_OF_RANGE_OBJECTS)) {
uint32_t count = packet.readUInt32();
for (uint32_t i = 0; i < count; ++i) {
uint64_t guid = UpdateObjectParser::readPackedGuid(packet);
data.outOfRangeGuids.push_back(guid);
LOG_DEBUG(" Out of range: 0x", std::hex, guid, std::dec);
}
} else {
packet.setReadPos(packet.getReadPos() - 1);
out.blockCount = packet.readUInt32();
if (out.blockCount > kMaxReasonableUpdateBlocks) {
packet.setReadPos(start);
return false;
}
}
// Parse update blocks — dispatching movement via virtual parseMovementBlock()
data.blocks.reserve(data.blockCount);
for (uint32_t i = 0; i < data.blockCount; ++i) {
LOG_DEBUG("Parsing block ", i + 1, " / ", data.blockCount);
UpdateBlock block;
// Read update type
uint8_t updateTypeVal = packet.readUInt8();
block.updateType = static_cast<UpdateType>(updateTypeVal);
LOG_DEBUG("Update block: type=", (int)updateTypeVal);
bool ok = false;
switch (block.updateType) {
case UpdateType::VALUES: {
block.guid = UpdateObjectParser::readPackedGuid(packet);
ok = UpdateObjectParser::parseUpdateFields(packet, block);
break;
if (withHasTransportByte) {
if (packet.getReadPos() >= packet.getSize()) {
packet.setReadPos(start);
return false;
}
case UpdateType::MOVEMENT: {
block.guid = UpdateObjectParser::readPackedGuid(packet);
ok = this->parseMovementBlock(packet, block);
break;
}
case UpdateType::CREATE_OBJECT:
case UpdateType::CREATE_OBJECT2: {
block.guid = UpdateObjectParser::readPackedGuid(packet);
uint8_t objectTypeVal = packet.readUInt8();
block.objectType = static_cast<ObjectType>(objectTypeVal);
ok = this->parseMovementBlock(packet, block);
if (ok) {
ok = UpdateObjectParser::parseUpdateFields(packet, block);
/*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;
}
break;
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;
}
uint64_t guid = UpdateObjectParser::readPackedGuid(packet);
out.outOfRangeGuids.push_back(guid);
}
} else {
packet.setReadPos(packet.getReadPos() - 1);
}
case UpdateType::OUT_OF_RANGE_OBJECTS:
case UpdateType::NEAR_OBJECTS:
ok = true;
break;
default:
LOG_WARNING("Unknown update type: ", (int)updateTypeVal);
ok = false;
break;
}
if (!ok) {
LOG_WARNING("Failed to parse update block ", i + 1, " of ", data.blockCount,
" — keeping ", data.blocks.size(), " parsed blocks");
break;
out.blocks.reserve(out.blockCount);
for (uint32_t i = 0; i < out.blockCount; ++i) {
if (packet.getReadPos() >= packet.getSize()) {
packet.setReadPos(start);
return false;
}
UpdateBlock block;
uint8_t updateTypeVal = packet.readUInt8();
if (updateTypeVal > static_cast<uint8_t>(UpdateType::NEAR_OBJECTS)) {
packet.setReadPos(start);
return false;
}
block.updateType = static_cast<UpdateType>(updateTypeVal);
bool ok = false;
switch (block.updateType) {
case UpdateType::VALUES: {
block.guid = UpdateObjectParser::readPackedGuid(packet);
ok = UpdateObjectParser::parseUpdateFields(packet, block);
break;
}
case UpdateType::MOVEMENT: {
block.guid = UpdateObjectParser::readPackedGuid(packet);
ok = this->parseMovementBlock(packet, block);
break;
}
case UpdateType::CREATE_OBJECT:
case UpdateType::CREATE_OBJECT2: {
block.guid = UpdateObjectParser::readPackedGuid(packet);
if (packet.getReadPos() >= packet.getSize()) {
ok = false;
break;
}
uint8_t objectTypeVal = packet.readUInt8();
block.objectType = static_cast<ObjectType>(objectTypeVal);
ok = this->parseMovementBlock(packet, block);
if (ok) ok = UpdateObjectParser::parseUpdateFields(packet, block);
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(block);
}
data.blocks.push_back(block);
return true;
};
size_t startPos = packet.getReadPos();
UpdateObjectData parsed;
if (parseWithLayout(true, parsed)) {
data = std::move(parsed);
return true;
}
return true;
packet.setReadPos(startPos);
if (parseWithLayout(false, parsed)) {
LOG_WARNING("[TBC] SMSG_UPDATE_OBJECT parsed without has_transport byte fallback");
data = std::move(parsed);
return true;
}
packet.setReadPos(startPos);
return false;
}
network::Packet TbcPacketParsers::buildAcceptQuestPacket(uint64_t npcGuid, uint32_t questId) {

View file

@ -532,29 +532,45 @@ bool LoginVerifyWorldParser::parse(network::Packet& packet, LoginVerifyWorldData
}
bool AccountDataTimesParser::parse(network::Packet& packet, AccountDataTimesData& data) {
// SMSG_ACCOUNT_DATA_TIMES format (WoW 3.3.5a):
// uint32 serverTime (Unix timestamp)
// uint8 unknown (always 1?)
// uint32[8] accountDataTimes (timestamps for each data slot)
if (packet.getSize() < 37) {
LOG_ERROR("SMSG_ACCOUNT_DATA_TIMES packet too small: ", packet.getSize(), " bytes");
// Common layouts seen in the wild:
// - WotLK-like: uint32 serverTime, uint8 unk, uint32 mask, uint32[up to 8] slotTimes
// - Older/variant: uint32 serverTime, uint8 unk, uint32[up to 8] slotTimes
// Some servers only send a subset of slots.
if (packet.getSize() < 5) {
LOG_ERROR("SMSG_ACCOUNT_DATA_TIMES packet too small: ", packet.getSize(),
" bytes (need at least 5)");
return false;
}
for (uint32_t& t : data.accountDataTimes) {
t = 0;
}
data.serverTime = packet.readUInt32();
data.unknown = packet.readUInt8();
size_t remaining = packet.getSize() - packet.getReadPos();
uint32_t mask = 0xFF;
if (remaining >= 4 && ((remaining - 4) % 4) == 0) {
// Treat first dword as slot mask when payload shape matches.
mask = packet.readUInt32();
}
remaining = packet.getSize() - packet.getReadPos();
size_t slotWords = std::min<size_t>(8, remaining / 4);
LOG_DEBUG("Parsed SMSG_ACCOUNT_DATA_TIMES:");
LOG_DEBUG(" Server time: ", data.serverTime);
LOG_DEBUG(" Unknown: ", (int)data.unknown);
LOG_DEBUG(" Mask: 0x", std::hex, mask, std::dec, " slotsInPacket=", slotWords);
for (int i = 0; i < 8; ++i) {
for (size_t i = 0; i < slotWords; ++i) {
data.accountDataTimes[i] = packet.readUInt32();
if (data.accountDataTimes[i] != 0) {
if (data.accountDataTimes[i] != 0 || ((mask & (1u << i)) != 0)) {
LOG_DEBUG(" Data slot ", i, ": ", data.accountDataTimes[i]);
}
}
if (packet.getReadPos() != packet.getSize()) {
LOG_DEBUG(" AccountDataTimes trailing bytes: ", packet.getSize() - packet.getReadPos());
packet.setReadPos(packet.getSize());
}
return true;
}
@ -886,53 +902,99 @@ bool UpdateObjectParser::parseMovementBlock(network::Packet& packet, UpdateBlock
// Spline data
if (moveFlags & 0x08000000) { // MOVEMENTFLAG_SPLINE_ENABLED
auto bytesAvailable = [&](size_t n) -> bool { return packet.getReadPos() + n <= packet.getSize(); };
if (!bytesAvailable(4)) return false;
uint32_t splineFlags = packet.readUInt32();
LOG_DEBUG(" Spline: flags=0x", std::hex, splineFlags, std::dec);
if (splineFlags & 0x00010000) { // SPLINEFLAG_FINAL_POINT
if (!bytesAvailable(12)) return false;
/*float finalX =*/ packet.readFloat();
/*float finalY =*/ packet.readFloat();
/*float finalZ =*/ packet.readFloat();
} else if (splineFlags & 0x00020000) { // SPLINEFLAG_FINAL_TARGET
if (!bytesAvailable(8)) return false;
/*uint64_t finalTarget =*/ packet.readUInt64();
} else if (splineFlags & 0x00040000) { // SPLINEFLAG_FINAL_ANGLE
if (!bytesAvailable(4)) return false;
/*float finalAngle =*/ packet.readFloat();
}
// Legacy UPDATE_OBJECT spline layout used by many servers:
// timePassed, duration, splineId, durationMod, durationModNext,
// verticalAccel, effectStartTime, pointCount, points, splineMode, endPoint.
const size_t legacyStart = packet.getReadPos();
if (!bytesAvailable(12 + 8 + 8 + 4)) return false;
/*uint32_t timePassed =*/ packet.readUInt32();
/*uint32_t duration =*/ packet.readUInt32();
/*uint32_t splineId =*/ packet.readUInt32();
/*float durationMod =*/ packet.readFloat();
/*float durationModNext =*/ packet.readFloat();
/*float verticalAccel =*/ packet.readFloat();
/*uint32_t effectStartTime =*/ packet.readUInt32();
uint32_t pointCount = packet.readUInt32();
if (pointCount > 256) {
const size_t remainingAfterCount = packet.getSize() - packet.getReadPos();
const bool legacyCountLooksValid = (pointCount <= 256);
const size_t legacyPointsBytes = static_cast<size_t>(pointCount) * 12ull;
const bool legacyPayloadFits = (legacyPointsBytes + 13ull) <= remainingAfterCount;
if (legacyCountLooksValid && legacyPayloadFits) {
for (uint32_t i = 0; i < pointCount; i++) {
/*float px =*/ packet.readFloat();
/*float py =*/ packet.readFloat();
/*float pz =*/ packet.readFloat();
}
/*uint8_t splineMode =*/ packet.readUInt8();
/*float endPointX =*/ packet.readFloat();
/*float endPointY =*/ packet.readFloat();
/*float endPointZ =*/ packet.readFloat();
LOG_DEBUG(" Spline pointCount=", pointCount);
}
// Legacy pointCount looks invalid; try compact WotLK layout as recovery.
// This keeps malformed/variant packets from desyncing the whole update block.
packet.setReadPos(legacyStart);
const size_t afterFinalFacingPos = packet.getReadPos();
if (splineFlags & 0x00400000) { // Animation
if (!bytesAvailable(5)) return false;
/*uint8_t animType =*/ packet.readUInt8();
/*uint32_t animStart =*/ packet.readUInt32();
}
if (!bytesAvailable(4)) return false;
/*uint32_t duration =*/ packet.readUInt32();
if (splineFlags & 0x00000800) { // Parabolic
if (!bytesAvailable(8)) return false;
/*float verticalAccel =*/ packet.readFloat();
/*uint32_t effectStartTime =*/ packet.readUInt32();
}
if (!bytesAvailable(4)) return false;
const uint32_t compactPointCount = packet.readUInt32();
if (compactPointCount > 16384) {
static uint32_t badSplineCount = 0;
++badSplineCount;
if (badSplineCount <= 5 || (badSplineCount % 100) == 0) {
LOG_WARNING(" Spline pointCount=", pointCount,
" exceeds maximum, capping at 0 (readPos=",
packet.getReadPos(), "/", packet.getSize(),
", occurrence=", badSplineCount, ")");
" invalid (legacy+compact) at readPos=",
afterFinalFacingPos, "/", packet.getSize(),
", occurrence=", badSplineCount);
}
pointCount = 0;
} else {
LOG_DEBUG(" Spline pointCount=", pointCount);
return false;
}
for (uint32_t i = 0; i < pointCount; i++) {
/*float px =*/ packet.readFloat();
/*float py =*/ packet.readFloat();
/*float pz =*/ packet.readFloat();
const bool uncompressed = (splineFlags & (0x00080000 | 0x00002000)) != 0;
size_t compactPayloadBytes = 0;
if (compactPointCount > 0) {
if (uncompressed) {
compactPayloadBytes = static_cast<size_t>(compactPointCount) * 12ull;
} else {
compactPayloadBytes = 12ull;
if (compactPointCount > 1) {
compactPayloadBytes += static_cast<size_t>(compactPointCount - 1) * 4ull;
}
}
if (!bytesAvailable(compactPayloadBytes)) return false;
packet.setReadPos(packet.getReadPos() + compactPayloadBytes);
}
/*uint8_t splineMode =*/ packet.readUInt8();
/*float endPointX =*/ packet.readFloat();
/*float endPointY =*/ packet.readFloat();
/*float endPointZ =*/ packet.readFloat();
}
}
else if (updateFlags & UPDATEFLAG_POSITION) {
@ -1025,8 +1087,9 @@ bool UpdateObjectParser::parseUpdateFields(network::Packet& packet, UpdateBlock&
LOG_DEBUG(" maskBlockCount = ", (int)blockCount);
LOG_DEBUG(" fieldsCapacity (blocks * 32) = ", fieldsCapacity);
// Read update mask
std::vector<uint32_t> updateMask(blockCount);
// Read update mask into a reused scratch buffer to avoid per-block allocations.
static thread_local std::vector<uint32_t> updateMask;
updateMask.resize(blockCount);
for (int i = 0; i < blockCount; ++i) {
updateMask[i] = packet.readUInt32();
}
@ -1035,22 +1098,30 @@ bool UpdateObjectParser::parseUpdateFields(network::Packet& packet, UpdateBlock&
uint16_t highestSetBit = 0;
uint32_t valuesReadCount = 0;
// Read field values for each bit set in mask
// Read only set bits in each mask block (faster than scanning all 32 bits).
for (int blockIdx = 0; blockIdx < blockCount; ++blockIdx) {
uint32_t mask = updateMask[blockIdx];
for (int bit = 0; bit < 32; ++bit) {
if (mask & (1 << bit)) {
uint16_t fieldIndex = blockIdx * 32 + bit;
if (fieldIndex > highestSetBit) {
highestSetBit = fieldIndex;
}
uint32_t value = packet.readUInt32();
block.fields[fieldIndex] = value;
valuesReadCount++;
LOG_DEBUG(" Field[", fieldIndex, "] = 0x", std::hex, value, std::dec);
while (mask != 0) {
const uint16_t fieldIndex =
#if defined(__GNUC__) || defined(__clang__)
static_cast<uint16_t>(blockIdx * 32 + __builtin_ctz(mask));
#else
static_cast<uint16_t>(blockIdx * 32 + [] (uint32_t v) -> uint16_t {
uint16_t b = 0;
while ((v & 1u) == 0u) { v >>= 1u; ++b; }
return b;
}(mask));
#endif
if (fieldIndex > highestSetBit) {
highestSetBit = fieldIndex;
}
uint32_t value = packet.readUInt32();
// fieldIndex is monotonically increasing here, so end() is a good insertion hint.
block.fields.emplace_hint(block.fields.end(), fieldIndex, value);
valuesReadCount++;
LOG_DEBUG(" Field[", fieldIndex, "] = 0x", std::hex, value, std::dec);
mask &= (mask - 1u);
}
}
@ -1131,9 +1202,16 @@ bool UpdateObjectParser::parseUpdateBlock(network::Packet& packet, UpdateBlock&
}
bool UpdateObjectParser::parse(network::Packet& packet, UpdateObjectData& data) {
constexpr uint32_t kMaxReasonableUpdateBlocks = 4096;
constexpr uint32_t kMaxReasonableOutOfRangeGuids = 16384;
// Read block count
data.blockCount = packet.readUInt32();
if (data.blockCount > kMaxReasonableUpdateBlocks) {
LOG_ERROR("SMSG_UPDATE_OBJECT rejected: unreasonable blockCount=", data.blockCount,
" packetSize=", packet.getSize());
return false;
}
LOG_DEBUG("SMSG_UPDATE_OBJECT:");
LOG_DEBUG(" objectCount = ", data.blockCount);
@ -1146,6 +1224,11 @@ bool UpdateObjectParser::parse(network::Packet& packet, UpdateObjectData& data)
if (firstByte == static_cast<uint8_t>(UpdateType::OUT_OF_RANGE_OBJECTS)) {
// Read out-of-range GUID count
uint32_t count = packet.readUInt32();
if (count > kMaxReasonableOutOfRangeGuids) {
LOG_ERROR("SMSG_UPDATE_OBJECT rejected: unreasonable outOfRange count=", count,
" packetSize=", packet.getSize());
return false;
}
for (uint32_t i = 0; i < count; ++i) {
uint64_t guid = readPackedGuid(packet);
@ -1173,7 +1256,7 @@ bool UpdateObjectParser::parse(network::Packet& packet, UpdateObjectData& data)
return false;
}
data.blocks.push_back(block);
data.blocks.emplace_back(std::move(block));
}

View file

@ -2,6 +2,9 @@
#include "core/logger.hpp"
#include <exception>
#include <csignal>
#include <cstdlib>
#include <cctype>
#include <string>
#include <SDL2/SDL.h>
#ifdef __linux__
#include <X11/Xlib.h>
@ -27,6 +30,19 @@ static void crashHandler(int sig) {
std::raise(sig);
}
static wowee::core::LogLevel readLogLevelFromEnv() {
const char* raw = std::getenv("WOWEE_LOG_LEVEL");
if (!raw || !*raw) return wowee::core::LogLevel::WARNING;
std::string level(raw);
for (char& c : level) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
if (level == "debug") return wowee::core::LogLevel::DEBUG;
if (level == "info") return wowee::core::LogLevel::INFO;
if (level == "warn" || level == "warning") return wowee::core::LogLevel::WARNING;
if (level == "error") return wowee::core::LogLevel::ERROR;
if (level == "fatal") return wowee::core::LogLevel::FATAL;
return wowee::core::LogLevel::WARNING;
}
int main([[maybe_unused]] int argc, [[maybe_unused]] char* argv[]) {
#ifdef __linux__
g_emergencyDisplay = XOpenDisplay(nullptr);
@ -37,7 +53,7 @@ int main([[maybe_unused]] int argc, [[maybe_unused]] char* argv[]) {
std::signal(SIGTERM, crashHandler);
std::signal(SIGINT, crashHandler);
try {
wowee::core::Logger::getInstance().setLogLevel(wowee::core::LogLevel::INFO);
wowee::core::Logger::getInstance().setLogLevel(readLogLevelFromEnv());
LOG_INFO("=== Wowee Native Client ===");
LOG_INFO("Starting application...");

View file

@ -1,5 +1,6 @@
#include "network/packet.hpp"
#include <cstring>
#include <utility>
namespace wowee {
namespace network {
@ -9,6 +10,9 @@ Packet::Packet(uint16_t opcode) : opcode(opcode) {}
Packet::Packet(uint16_t opcode, const std::vector<uint8_t>& data)
: opcode(opcode), data(data), readPos(0) {}
Packet::Packet(uint16_t opcode, std::vector<uint8_t>&& data)
: opcode(opcode), data(std::move(data)), readPos(0) {}
void Packet::writeUInt8(uint8_t value) {
data.push_back(value);
}

View file

@ -7,6 +7,8 @@
#include <sstream>
#include <cstdio>
#include <fstream>
#include <cstdlib>
#include <cstring>
namespace {
constexpr size_t kMaxReceiveBufferBytes = 8 * 1024 * 1024;
@ -40,6 +42,13 @@ inline bool isLoginPipelineCmsg(uint16_t opcode) {
return false;
}
}
inline bool envFlagEnabled(const char* key, bool defaultValue = false) {
const char* raw = std::getenv(key);
if (!raw || !*raw) return defaultValue;
return !(raw[0] == '0' || raw[0] == 'f' || raw[0] == 'F' ||
raw[0] == 'n' || raw[0] == 'N');
}
} // namespace
namespace wowee {
@ -58,6 +67,19 @@ static const uint8_t DECRYPT_KEY[] = {
WorldSocket::WorldSocket() {
net::ensureInit();
// Always reserve baseline receive capacity (safe, behavior-preserving).
receiveBuffer.reserve(64 * 1024);
useFastRecvAppend_ = envFlagEnabled("WOWEE_NET_FAST_RECV_APPEND", true);
useParseScratchQueue_ = envFlagEnabled("WOWEE_NET_PARSE_SCRATCH", false);
if (useParseScratchQueue_) {
LOG_WARNING("WOWEE_NET_PARSE_SCRATCH is temporarily disabled (known unstable); forcing off");
useParseScratchQueue_ = false;
}
if (useParseScratchQueue_) {
parsedPacketsScratch_.reserve(64);
}
LOG_INFO("WorldSocket net opts: fast_recv_append=", useFastRecvAppend_ ? "on" : "off",
" parse_scratch=", useParseScratchQueue_ ? "on" : "off");
}
WorldSocket::~WorldSocket() {
@ -118,6 +140,8 @@ void WorldSocket::disconnect() {
encryptionEnabled = false;
useVanillaCrypt = false;
receiveBuffer.clear();
receiveReadOffset_ = 0;
parsedPacketsScratch_.clear();
headerBytesDecrypted = 0;
LOG_INFO("Disconnected from world server");
}
@ -128,13 +152,15 @@ bool WorldSocket::isConnected() const {
void WorldSocket::send(const Packet& packet) {
if (!connected) return;
static const bool kLogCharCreatePayload = envFlagEnabled("WOWEE_NET_LOG_CHAR_CREATE", false);
static const bool kLogSwapItemPackets = envFlagEnabled("WOWEE_NET_LOG_SWAP_ITEM", false);
const auto& data = packet.getData();
uint16_t opcode = packet.getOpcode();
uint16_t payloadLen = static_cast<uint16_t>(data.size());
// Debug: parse and log character-create payload fields (helps diagnose appearance issues).
if (opcode == 0x036) { // CMSG_CHAR_CREATE
if (kLogCharCreatePayload && opcode == 0x036) { // CMSG_CHAR_CREATE
size_t pos = 0;
std::string name;
while (pos < data.size()) {
@ -181,7 +207,7 @@ void WorldSocket::send(const Packet& packet) {
}
}
if (opcode == 0x10C || opcode == 0x10D) { // CMSG_SWAP_ITEM / CMSG_SWAP_INV_ITEM
if (kLogSwapItemPackets && (opcode == 0x10C || opcode == 0x10D)) { // CMSG_SWAP_ITEM / CMSG_SWAP_INV_ITEM
std::string hex;
for (size_t i = 0; i < data.size(); i++) {
char buf[4];
@ -255,6 +281,23 @@ void WorldSocket::send(const Packet& packet) {
void WorldSocket::update() {
if (!connected) return;
auto bufferedBytes = [&]() -> size_t {
return (receiveBuffer.size() >= receiveReadOffset_)
? (receiveBuffer.size() - receiveReadOffset_)
: 0;
};
auto compactReceiveBuffer = [&]() {
if (receiveReadOffset_ == 0) return;
if (receiveReadOffset_ >= receiveBuffer.size()) {
receiveBuffer.clear();
receiveReadOffset_ = 0;
return;
}
const size_t remaining = receiveBuffer.size() - receiveReadOffset_;
std::memmove(receiveBuffer.data(), receiveBuffer.data() + receiveReadOffset_, remaining);
receiveBuffer.resize(remaining);
receiveReadOffset_ = 0;
};
// Drain the socket. Some servers send an auth response and immediately close; a single recv()
// may read the response, and a subsequent recv() can return 0 (FIN). If we disconnect right
@ -270,10 +313,42 @@ void WorldSocket::update() {
if (received > 0) {
receivedAny = true;
++readOps;
bytesReadThisTick += static_cast<size_t>(received);
receiveBuffer.insert(receiveBuffer.end(), buffer, buffer + received);
if (receiveBuffer.size() > kMaxReceiveBufferBytes) {
LOG_ERROR("World socket receive buffer overflow (", receiveBuffer.size(),
size_t receivedSize = static_cast<size_t>(received);
bytesReadThisTick += receivedSize;
if (useFastRecvAppend_) {
size_t liveBytes = bufferedBytes();
if (liveBytes > kMaxReceiveBufferBytes || receivedSize > (kMaxReceiveBufferBytes - liveBytes)) {
compactReceiveBuffer();
liveBytes = bufferedBytes();
}
if (liveBytes > kMaxReceiveBufferBytes || receivedSize > (kMaxReceiveBufferBytes - liveBytes)) {
LOG_ERROR("World socket receive buffer would overflow (buffered=", liveBytes,
" incoming=", receivedSize, " max=", kMaxReceiveBufferBytes,
"). Disconnecting to recover framing.");
disconnect();
return;
}
const size_t oldSize = receiveBuffer.size();
const size_t needed = oldSize + receivedSize;
if (receiveBuffer.capacity() < needed) {
size_t newCap = receiveBuffer.capacity() ? receiveBuffer.capacity() : 64 * 1024;
while (newCap < needed && newCap < kMaxReceiveBufferBytes) {
newCap = std::min(kMaxReceiveBufferBytes, newCap * 2);
}
if (newCap < needed) {
LOG_ERROR("World socket receive buffer capacity growth failed (needed=", needed,
" max=", kMaxReceiveBufferBytes, "). Disconnecting to recover framing.");
disconnect();
return;
}
receiveBuffer.reserve(newCap);
}
receiveBuffer.insert(receiveBuffer.end(), buffer, buffer + receivedSize);
} else {
receiveBuffer.insert(receiveBuffer.end(), buffer, buffer + received);
}
if (bufferedBytes() > kMaxReceiveBufferBytes) {
LOG_ERROR("World socket receive buffer overflow (", bufferedBytes(),
" bytes). Disconnecting to recover framing.");
disconnect();
return;
@ -297,26 +372,29 @@ void WorldSocket::update() {
}
if (receivedAny) {
LOG_DEBUG("World socket read ", bytesReadThisTick, " bytes in ", readOps,
" recv call(s), buffered=", receiveBuffer.size());
// Hex dump received bytes for auth debugging
if (bytesReadThisTick <= 128) {
const bool debugLog = core::Logger::getInstance().shouldLog(core::LogLevel::DEBUG);
if (debugLog) {
LOG_DEBUG("World socket read ", bytesReadThisTick, " bytes in ", readOps,
" recv call(s), buffered=", bufferedBytes());
}
// Hex dump received bytes for auth debugging (debug-only to avoid per-frame string work)
if (debugLog && bytesReadThisTick <= 128) {
std::string hex;
for (size_t i = 0; i < receiveBuffer.size(); ++i) {
for (size_t i = receiveReadOffset_; i < receiveBuffer.size(); ++i) {
char buf[4]; snprintf(buf, sizeof(buf), "%02x ", receiveBuffer[i]); hex += buf;
}
LOG_DEBUG("World socket raw bytes: ", hex);
}
tryParsePackets();
if (connected && !receiveBuffer.empty()) {
LOG_DEBUG("World socket parse left ", receiveBuffer.size(),
if (debugLog && connected && bufferedBytes() > 0) {
LOG_DEBUG("World socket parse left ", bufferedBytes(),
" bytes buffered (awaiting complete packet)");
}
}
if (sawClose) {
LOG_INFO("World server connection closed (receivedAny=", receivedAny,
" buffered=", receiveBuffer.size(), ")");
" buffered=", bufferedBytes(), ")");
disconnect();
return;
}
@ -325,27 +403,44 @@ void WorldSocket::update() {
void WorldSocket::tryParsePackets() {
// World server packets have 4-byte incoming header: size(2) + opcode(2)
int parsedThisTick = 0;
while (receiveBuffer.size() >= 4 && parsedThisTick < kMaxParsedPacketsPerUpdate) {
size_t parseOffset = receiveReadOffset_;
size_t localHeaderBytesDecrypted = headerBytesDecrypted;
std::vector<Packet> parsedPacketsLocal;
std::vector<Packet>* parsedPackets = &parsedPacketsLocal;
if (useParseScratchQueue_) {
parsedPacketsScratch_.clear();
// Keep a warm queue to reduce steady-state allocations, but avoid
// retaining pathological capacity after burst/misaligned streams.
if (parsedPacketsScratch_.capacity() > 1024) {
std::vector<Packet>().swap(parsedPacketsScratch_);
} else if (parsedPacketsScratch_.capacity() < 64) {
parsedPacketsScratch_.reserve(64);
}
parsedPackets = &parsedPacketsScratch_;
} else {
parsedPacketsLocal.reserve(32);
}
while ((receiveBuffer.size() - parseOffset) >= 4 && parsedThisTick < kMaxParsedPacketsPerUpdate) {
uint8_t rawHeader[4] = {0, 0, 0, 0};
std::memcpy(rawHeader, receiveBuffer.data(), 4);
std::memcpy(rawHeader, receiveBuffer.data() + parseOffset, 4);
// Decrypt header bytes in-place if encryption is enabled
// Only decrypt bytes we haven't already decrypted
if (encryptionEnabled && headerBytesDecrypted < 4) {
size_t toDecrypt = 4 - headerBytesDecrypted;
if (encryptionEnabled && localHeaderBytesDecrypted < 4) {
size_t toDecrypt = 4 - localHeaderBytesDecrypted;
if (useVanillaCrypt) {
vanillaCrypt.decrypt(receiveBuffer.data() + headerBytesDecrypted, toDecrypt);
vanillaCrypt.decrypt(receiveBuffer.data() + parseOffset + localHeaderBytesDecrypted, toDecrypt);
} else {
decryptCipher.process(receiveBuffer.data() + headerBytesDecrypted, toDecrypt);
decryptCipher.process(receiveBuffer.data() + parseOffset + localHeaderBytesDecrypted, toDecrypt);
}
headerBytesDecrypted = 4;
localHeaderBytesDecrypted = 4;
}
// Parse header (now decrypted in-place).
// Size: 2 bytes big-endian. For world packets, this includes opcode bytes.
uint16_t size = (receiveBuffer[0] << 8) | receiveBuffer[1];
uint16_t size = (receiveBuffer[parseOffset + 0] << 8) | receiveBuffer[parseOffset + 1];
// Opcode: 2 bytes little-endian.
uint16_t opcode = receiveBuffer[2] | (receiveBuffer[3] << 8);
uint16_t opcode = receiveBuffer[parseOffset + 2] | (receiveBuffer[parseOffset + 3] << 8);
if (size < 2) {
LOG_ERROR("World packet framing desync: invalid size=", size,
" rawHdr=", std::hex,
@ -381,50 +476,79 @@ void WorldSocket::tryParsePackets() {
static_cast<int>(rawHeader[2]), " ",
static_cast<int>(rawHeader[3]),
" dec=",
static_cast<int>(receiveBuffer[0]), " ",
static_cast<int>(receiveBuffer[1]), " ",
static_cast<int>(receiveBuffer[2]), " ",
static_cast<int>(receiveBuffer[3]),
static_cast<int>(receiveBuffer[parseOffset + 0]), " ",
static_cast<int>(receiveBuffer[parseOffset + 1]), " ",
static_cast<int>(receiveBuffer[parseOffset + 2]), " ",
static_cast<int>(receiveBuffer[parseOffset + 3]),
std::dec,
" size=", size,
" payload=", payloadLen,
" opcode=0x", std::hex, opcode, std::dec,
" buffered=", receiveBuffer.size());
" buffered=", (receiveBuffer.size() - parseOffset));
--headerTracePacketsLeft;
}
if (isLoginPipelineSmsg(opcode)) {
LOG_INFO("WS RX LOGIN opcode=0x", std::hex, opcode, std::dec,
" size=", size, " payload=", payloadLen,
" buffered=", receiveBuffer.size(),
" buffered=", (receiveBuffer.size() - parseOffset),
" enc=", encryptionEnabled ? "yes" : "no");
}
if (receiveBuffer.size() < totalSize) {
if ((receiveBuffer.size() - parseOffset) < totalSize) {
// Not enough data yet - header stays decrypted in buffer
break;
}
// Extract payload (skip header)
std::vector<uint8_t> packetData(receiveBuffer.begin() + 4,
receiveBuffer.begin() + totalSize);
// Create packet with opcode and payload
Packet packet(opcode, packetData);
// Remove parsed data from buffer and reset header decryption counter
receiveBuffer.erase(receiveBuffer.begin(), receiveBuffer.begin() + totalSize);
headerBytesDecrypted = 0;
// Call callback if set
if (packetCallback) {
packetCallback(packet);
// Extract payload (skip header). Guard allocation failures so malformed
// streams cannot unwind into application-level OOM crashes.
try {
std::vector<uint8_t> packetData(payloadLen);
if (payloadLen > 0) {
std::memcpy(packetData.data(), receiveBuffer.data() + parseOffset + 4, payloadLen);
}
// Queue packet; callbacks run after buffer state is finalized.
parsedPackets->emplace_back(opcode, std::move(packetData));
} catch (const std::bad_alloc& e) {
LOG_ERROR("OOM while queuing world packet opcode=0x", std::hex, opcode, std::dec,
" payload=", payloadLen, " buffered=", receiveBuffer.size(),
" parseOffset=", parseOffset, " what=", e.what(),
". Disconnecting to recover.");
disconnect();
return;
}
parseOffset += totalSize;
localHeaderBytesDecrypted = 0;
++parsedThisTick;
}
if (parsedThisTick >= kMaxParsedPacketsPerUpdate && receiveBuffer.size() >= 4) {
if (parseOffset > receiveReadOffset_) {
receiveReadOffset_ = parseOffset;
// Compact lazily to avoid front-erase memmove every update.
if (receiveReadOffset_ >= receiveBuffer.size()) {
receiveBuffer.clear();
receiveReadOffset_ = 0;
} else if (receiveReadOffset_ >= 64 * 1024 || receiveReadOffset_ * 2 >= receiveBuffer.size()) {
const size_t remaining = receiveBuffer.size() - receiveReadOffset_;
std::memmove(receiveBuffer.data(), receiveBuffer.data() + receiveReadOffset_, remaining);
receiveBuffer.resize(remaining);
receiveReadOffset_ = 0;
}
}
headerBytesDecrypted = localHeaderBytesDecrypted;
if (packetCallback) {
for (const auto& packet : *parsedPackets) {
if (!connected) break;
packetCallback(packet);
}
}
const size_t buffered = (receiveBuffer.size() >= receiveReadOffset_)
? (receiveBuffer.size() - receiveReadOffset_)
: 0;
if (parsedThisTick >= kMaxParsedPacketsPerUpdate && buffered >= 4) {
LOG_DEBUG("World socket parse budget reached (", parsedThisTick,
" packets); deferring remaining buffered data=", receiveBuffer.size(), " bytes");
" packets); deferring remaining buffered data=", buffered, " bytes");
}
}

View file

@ -147,8 +147,15 @@ BLPImage AssetManager::loadTexture(const std::string& path) {
std::vector<uint8_t> blpData = readFile(normalizedPath);
if (blpData.empty()) {
static std::unordered_set<std::string> loggedMissingTextures;
if (loggedMissingTextures.insert(normalizedPath).second) {
static bool missingTextureLogSuppressed = false;
static constexpr size_t kMaxMissingTextureLogKeys = 20000;
if (loggedMissingTextures.size() < kMaxMissingTextureLogKeys &&
loggedMissingTextures.insert(normalizedPath).second) {
LOG_WARNING("Texture not found: ", normalizedPath);
} else if (!missingTextureLogSuppressed && loggedMissingTextures.size() >= kMaxMissingTextureLogKeys) {
LOG_WARNING("Texture-not-found warning key cache reached ", kMaxMissingTextureLogKeys,
" entries; suppressing new unique texture-miss logs");
missingTextureLogSuppressed = true;
}
return BLPImage();
}
@ -156,8 +163,15 @@ BLPImage AssetManager::loadTexture(const std::string& path) {
BLPImage image = BLPLoader::load(blpData);
if (!image.isValid()) {
static std::unordered_set<std::string> loggedDecodeFails;
if (loggedDecodeFails.insert(normalizedPath).second) {
static bool decodeFailLogSuppressed = false;
static constexpr size_t kMaxDecodeFailLogKeys = 8000;
if (loggedDecodeFails.size() < kMaxDecodeFailLogKeys &&
loggedDecodeFails.insert(normalizedPath).second) {
LOG_ERROR("Failed to load texture: ", normalizedPath);
} else if (!decodeFailLogSuppressed && loggedDecodeFails.size() >= kMaxDecodeFailLogKeys) {
LOG_WARNING("Texture-decode warning key cache reached ", kMaxDecodeFailLogKeys,
" entries; suppressing new unique decode-failure logs");
decodeFailLogSuppressed = true;
}
return BLPImage();
}

View file

@ -1,4 +1,5 @@
#include "pipeline/terrain_mesh.hpp"
#include "core/coordinates.hpp"
#include "core/logger.hpp"
#include <cmath>
@ -49,10 +50,14 @@ ChunkMesh TerrainMeshGenerator::generateChunkMesh(const MapChunk& chunk, int chu
mesh.chunkX = chunkX;
mesh.chunkY = chunkY;
// World position from chunk data
mesh.worldX = chunk.position[0];
mesh.worldY = chunk.position[1];
mesh.worldZ = chunk.position[2];
// Compute render-space XY from tile/chunk indices.
// MCNK XY fields can vary across sources; deriving from tile/chunk indices
// keeps chunk placement stable and consistent with ADT naming.
const float tileNW_renderX = (32.0f - static_cast<float>(tileY)) * core::coords::TILE_SIZE;
const float tileNW_renderY = (32.0f - static_cast<float>(tileX)) * core::coords::TILE_SIZE;
mesh.worldX = tileNW_renderX - static_cast<float>(chunkY) * CHUNK_SIZE;
mesh.worldY = tileNW_renderY - static_cast<float>(chunkX) * CHUNK_SIZE;
mesh.worldZ = chunk.position[2]; // height base from MCNK
// Generate vertices from heightmap (pass chunk grid indices and tile coords)
mesh.vertices = generateVertices(chunk, chunkX, chunkY, tileX, tileY);
@ -167,7 +172,7 @@ ChunkMesh TerrainMeshGenerator::generateChunkMesh(const MapChunk& chunk, int chu
return mesh;
}
std::vector<TerrainVertex> TerrainMeshGenerator::generateVertices(const MapChunk& chunk, [[maybe_unused]] int chunkX, [[maybe_unused]] int chunkY, [[maybe_unused]] int tileX, [[maybe_unused]] int tileY) {
std::vector<TerrainVertex> TerrainMeshGenerator::generateVertices(const MapChunk& chunk, int chunkX, int chunkY, int tileX, int tileY) {
std::vector<TerrainVertex> vertices;
vertices.reserve(145); // 145 vertices total
@ -176,10 +181,12 @@ std::vector<TerrainVertex> TerrainMeshGenerator::generateVertices(const MapChunk
// WoW terrain uses 145 heights stored in a 9x17 row-major grid layout
const float unitSize = CHUNK_SIZE / 8.0f; // 66.67 units per vertex step
// chunk.position contains world coordinates for this chunk's origin
// Both X and Y are at world scale (no scaling needed)
float chunkBaseX = chunk.position[0];
float chunkBaseY = chunk.position[1];
// Compute render-space base from tile/chunk indices.
const float tileNW_renderX = (32.0f - static_cast<float>(tileY)) * core::coords::TILE_SIZE;
const float tileNW_renderY = (32.0f - static_cast<float>(tileX)) * core::coords::TILE_SIZE;
float chunkBaseX = tileNW_renderX - static_cast<float>(chunkY) * CHUNK_SIZE;
float chunkBaseY = tileNW_renderY - static_cast<float>(chunkX) * CHUNK_SIZE;
float chunkBaseZ = chunk.position[2];
for (int index = 0; index < 145; index++) {
int y = index / 17; // Row (0-8)
@ -196,11 +203,10 @@ std::vector<TerrainVertex> TerrainMeshGenerator::generateVertices(const MapChunk
TerrainVertex vertex;
// Position - match wowee.js coordinate layout (swap X/Y and negate)
// wowee.js: X = -(y * unitSize), Y = -(x * unitSize)
// Position in render space.
vertex.position[0] = chunkBaseX - (offsetY * unitSize);
vertex.position[1] = chunkBaseY - (offsetX * unitSize);
vertex.position[2] = chunk.position[2] + heightMap.heights[index];
vertex.position[2] = chunkBaseZ + heightMap.heights[index];
// Normal
if (index * 3 + 2 < static_cast<int>(chunk.normals.size())) {

View file

@ -4,9 +4,8 @@
#include <imgui_internal.h>
#include <imgui_impl_opengl3.h>
#include <imgui_impl_sdl2.h>
#include <random>
#include <chrono>
#include <cstdio>
#include <filesystem>
#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"
@ -15,6 +14,9 @@ namespace wowee {
namespace rendering {
LoadingScreen::LoadingScreen() {
imagePaths.push_back("assets/krayonload.png");
imagePaths.push_back((std::filesystem::current_path() / "assets/krayonload.png").string());
// Fallbacks for environments that don't have the newer image yet.
imagePaths.push_back("assets/loading1.jpeg");
imagePaths.push_back("assets/loading2.jpeg");
}
@ -163,15 +165,15 @@ void LoadingScreen::shutdown() {
void LoadingScreen::selectRandomImage() {
if (imagePaths.empty()) return;
unsigned seed = static_cast<unsigned>(
std::chrono::system_clock::now().time_since_epoch().count());
std::default_random_engine generator(seed);
std::uniform_int_distribution<int> distribution(0, imagePaths.size() - 1);
currentImageIndex = distribution(generator);
// Vulkan branch uses a single curated loading image; keep deterministic
// selection with fallback to legacy assets if needed.
for (size_t i = 0; i < imagePaths.size(); ++i) {
if (std::filesystem::exists(imagePaths[i])) {
currentImageIndex = static_cast<int>(i);
break;
}
}
LOG_INFO("Selected loading screen: ", imagePaths[currentImageIndex]);
loadImage(imagePaths[currentImageIndex]);
}

View file

@ -6,7 +6,9 @@
#include "pipeline/asset_manager.hpp"
#include "audio/music_manager.hpp"
#include "game/expansion_profile.hpp"
#include <GL/glew.h>
#include <imgui.h>
#include "stb_image.h"
#include <filesystem>
#include <sstream>
#include <fstream>
@ -50,6 +52,10 @@ static std::vector<uint8_t> hexDecode(const std::string& hex) {
AuthScreen::AuthScreen() {
}
AuthScreen::~AuthScreen() {
destroyBackgroundImage();
}
std::string AuthScreen::makeServerKey(const std::string& host, int port) {
std::ostringstream ss;
ss << host << ":" << port;
@ -159,39 +165,34 @@ void AuthScreen::render(auth::AuthHandler& authHandler) {
loginInfoLoaded = true;
}
if (!videoInitAttempted) {
videoInitAttempted = true;
std::string videoPath = "assets/startscreen.mp4";
if (!std::filesystem::exists(videoPath)) {
videoPath = (std::filesystem::current_path() / "assets/startscreen.mp4").string();
}
backgroundVideo.open(videoPath);
if (!bgInitAttempted) {
bgInitAttempted = true;
loadBackgroundImage();
}
backgroundVideo.update(ImGui::GetIO().DeltaTime);
if (backgroundVideo.isReady()) {
if (bgTextureId_ != 0) {
ImVec2 screen = ImGui::GetIO().DisplaySize;
float screenW = screen.x;
float screenH = screen.y;
float videoW = static_cast<float>(backgroundVideo.getWidth());
float videoH = static_cast<float>(backgroundVideo.getHeight());
if (videoW > 0.0f && videoH > 0.0f) {
float imgW = static_cast<float>(bgWidth_);
float imgH = static_cast<float>(bgHeight_);
if (imgW > 0.0f && imgH > 0.0f) {
float screenAspect = screenW / screenH;
float videoAspect = videoW / videoH;
float imgAspect = imgW / imgH;
ImVec2 uv0(0.0f, 0.0f);
ImVec2 uv1(1.0f, 1.0f);
if (videoAspect > screenAspect) {
float scale = screenAspect / videoAspect;
if (imgAspect > screenAspect) {
float scale = screenAspect / imgAspect;
float crop = (1.0f - scale) * 0.5f;
uv0.x = crop;
uv1.x = 1.0f - crop;
} else if (videoAspect < screenAspect) {
float scale = videoAspect / screenAspect;
} else if (imgAspect < screenAspect) {
float scale = imgAspect / screenAspect;
float crop = (1.0f - scale) * 0.5f;
uv0.y = crop;
uv1.y = 1.0f - crop;
}
ImDrawList* bg = ImGui::GetBackgroundDrawList();
bg->AddImage(static_cast<ImTextureID>(static_cast<uintptr_t>(backgroundVideo.getTextureId())),
bg->AddImage(static_cast<ImTextureID>(bgTextureId_),
ImVec2(0, 0), ImVec2(screenW, screenH), uv0, uv1);
}
}
@ -484,6 +485,57 @@ void AuthScreen::render(auth::AuthHandler& authHandler) {
ImGui::End();
}
bool AuthScreen::loadBackgroundImage() {
destroyBackgroundImage();
const std::array<std::string, 2> candidates = {
"assets/krayonsignin.png",
(std::filesystem::current_path() / "assets/krayonsignin.png").string()
};
int channels = 0;
unsigned char* data = nullptr;
std::string loadedPath;
for (const auto& p : candidates) {
stbi_set_flip_vertically_on_load(false);
data = stbi_load(p.c_str(), &bgWidth_, &bgHeight_, &channels, 4);
if (data) {
loadedPath = p;
break;
}
}
if (!data) {
LOG_WARNING("AuthScreen: failed to load background image assets/krayonsignin.png");
bgWidth_ = 0;
bgHeight_ = 0;
return false;
}
glGenTextures(1, &bgTextureId_);
glBindTexture(GL_TEXTURE_2D, bgTextureId_);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, bgWidth_, bgHeight_, 0, GL_RGBA, GL_UNSIGNED_BYTE, data);
glBindTexture(GL_TEXTURE_2D, 0);
stbi_image_free(data);
LOG_INFO("AuthScreen: loaded sign-in background: ", loadedPath, " (", bgWidth_, "x", bgHeight_, ")");
return true;
}
void AuthScreen::destroyBackgroundImage() {
if (bgTextureId_ != 0) {
GLuint tex = static_cast<GLuint>(bgTextureId_);
glDeleteTextures(1, &tex);
bgTextureId_ = 0;
}
bgWidth_ = 0;
bgHeight_ = 0;
}
void AuthScreen::stopLoginMusic() {
auto& app = core::Application::getInstance();
auto* renderer = app.getRenderer();