Initial commit: wowee native WoW 3.3.5a client

This commit is contained in:
Kelsi 2026-02-02 12:24:50 -08:00
commit ce6cb8f38e
147 changed files with 32347 additions and 0 deletions

142
src/audio/music_manager.cpp Normal file
View file

@ -0,0 +1,142 @@
#include "audio/music_manager.hpp"
#include "pipeline/asset_manager.hpp"
#include "core/logger.hpp"
#include <fstream>
#include <cstdlib>
#include <csignal>
#include <sys/wait.h>
#include <unistd.h>
namespace wowee {
namespace audio {
MusicManager::MusicManager() {
tempFilePath = "/tmp/wowee_music.mp3";
}
MusicManager::~MusicManager() {
shutdown();
}
bool MusicManager::initialize(pipeline::AssetManager* assets) {
assetManager = assets;
LOG_INFO("Music manager initialized");
return true;
}
void MusicManager::shutdown() {
stopCurrentProcess();
// Clean up temp file
std::remove(tempFilePath.c_str());
}
void MusicManager::playMusic(const std::string& mpqPath, bool loop) {
if (!assetManager) return;
if (mpqPath == currentTrack && playing) return;
// Read music file from MPQ
auto data = assetManager->readFile(mpqPath);
if (data.empty()) {
LOG_WARNING("Music: Could not read: ", mpqPath);
return;
}
// Stop current playback
stopCurrentProcess();
// Write to temp file
std::ofstream out(tempFilePath, std::ios::binary);
if (!out) {
LOG_ERROR("Music: Could not write temp file");
return;
}
out.write(reinterpret_cast<const char*>(data.data()), data.size());
out.close();
// Play with ffplay in background
pid_t pid = fork();
if (pid == 0) {
// Child process — create new process group so we can kill all children
setpgid(0, 0);
// Redirect output to /dev/null
freopen("/dev/null", "w", stdout);
freopen("/dev/null", "w", stderr);
if (loop) {
execlp("ffplay", "ffplay", "-nodisp", "-autoexit", "-loop", "0",
"-volume", "30", tempFilePath.c_str(), nullptr);
} else {
execlp("ffplay", "ffplay", "-nodisp", "-autoexit",
"-volume", "30", tempFilePath.c_str(), nullptr);
}
_exit(1); // exec failed
} else if (pid > 0) {
playerPid = pid;
playing = true;
currentTrack = mpqPath;
LOG_INFO("Music: Playing ", mpqPath);
} else {
LOG_ERROR("Music: fork() failed");
}
}
void MusicManager::stopMusic(float fadeMs) {
(void)fadeMs; // ffplay doesn't support fade easily
stopCurrentProcess();
playing = false;
currentTrack.clear();
}
void MusicManager::crossfadeTo(const std::string& mpqPath, float fadeMs) {
if (mpqPath == currentTrack && playing) return;
// Simple implementation: stop and start (no actual crossfade with subprocess)
if (fadeMs > 0 && playing) {
crossfading = true;
pendingTrack = mpqPath;
fadeTimer = 0.0f;
fadeDuration = fadeMs / 1000.0f;
stopCurrentProcess();
} else {
playMusic(mpqPath);
}
}
void MusicManager::update(float deltaTime) {
// Check if player process is still running
if (playerPid > 0) {
int status;
pid_t result = waitpid(playerPid, &status, WNOHANG);
if (result == playerPid) {
// Process ended
playerPid = -1;
playing = false;
}
}
// Handle crossfade
if (crossfading) {
fadeTimer += deltaTime;
if (fadeTimer >= fadeDuration * 0.3f) {
// Start new track after brief pause
crossfading = false;
playMusic(pendingTrack);
pendingTrack.clear();
}
}
}
void MusicManager::stopCurrentProcess() {
if (playerPid > 0) {
// Kill the entire process group (ffplay may spawn children)
kill(-playerPid, SIGTERM);
kill(playerPid, SIGTERM);
int status;
waitpid(playerPid, &status, 0);
playerPid = -1;
playing = false;
}
}
} // namespace audio
} // namespace wowee

289
src/auth/auth_handler.cpp Normal file
View file

@ -0,0 +1,289 @@
#include "auth/auth_handler.hpp"
#include "network/tcp_socket.hpp"
#include "network/packet.hpp"
#include "core/logger.hpp"
namespace wowee {
namespace auth {
AuthHandler::AuthHandler() {
LOG_DEBUG("AuthHandler created");
}
AuthHandler::~AuthHandler() {
disconnect();
}
bool AuthHandler::connect(const std::string& host, uint16_t port) {
LOG_INFO("Connecting to auth server: ", host, ":", port);
socket = std::make_unique<network::TCPSocket>();
// Set up packet callback
socket->setPacketCallback([this](const network::Packet& packet) {
// Create a mutable copy for handling
network::Packet mutablePacket = packet;
handlePacket(mutablePacket);
});
if (!socket->connect(host, port)) {
LOG_ERROR("Failed to connect to auth server");
setState(AuthState::FAILED);
return false;
}
setState(AuthState::CONNECTED);
LOG_INFO("Connected to auth server");
return true;
}
void AuthHandler::disconnect() {
if (socket) {
socket->disconnect();
socket.reset();
}
setState(AuthState::DISCONNECTED);
LOG_INFO("Disconnected from auth server");
}
bool AuthHandler::isConnected() const {
return socket && socket->isConnected();
}
void AuthHandler::requestRealmList() {
if (!isConnected()) {
LOG_ERROR("Cannot request realm list: not connected to auth server");
return;
}
if (state != AuthState::AUTHENTICATED) {
LOG_ERROR("Cannot request realm list: not authenticated (state: ", (int)state, ")");
return;
}
LOG_INFO("Requesting realm list");
sendRealmListRequest();
}
void AuthHandler::authenticate(const std::string& user, const std::string& pass) {
if (!isConnected()) {
LOG_ERROR("Cannot authenticate: not connected to auth server");
fail("Not connected");
return;
}
if (state != AuthState::CONNECTED) {
LOG_ERROR("Cannot authenticate: invalid state");
fail("Invalid state");
return;
}
LOG_INFO("Starting authentication for user: ", user);
username = user;
password = pass;
// Initialize SRP
srp = std::make_unique<SRP>();
srp->initialize(username, password);
// Send LOGON_CHALLENGE
sendLogonChallenge();
}
void AuthHandler::sendLogonChallenge() {
LOG_DEBUG("Sending LOGON_CHALLENGE");
auto packet = LogonChallengePacket::build(username, clientInfo);
socket->send(packet);
setState(AuthState::CHALLENGE_SENT);
}
void AuthHandler::handleLogonChallengeResponse(network::Packet& packet) {
LOG_DEBUG("Handling LOGON_CHALLENGE response");
LogonChallengeResponse response;
if (!LogonChallengeResponseParser::parse(packet, response)) {
fail("Failed to parse LOGON_CHALLENGE response");
return;
}
if (!response.isSuccess()) {
fail(std::string("LOGON_CHALLENGE failed: ") + getAuthResultString(response.result));
return;
}
// Feed SRP with server challenge data
srp->feed(response.B, response.g, response.N, response.salt);
setState(AuthState::CHALLENGE_RECEIVED);
// Send LOGON_PROOF immediately
sendLogonProof();
}
void AuthHandler::sendLogonProof() {
LOG_DEBUG("Sending LOGON_PROOF");
auto A = srp->getA();
auto M1 = srp->getM1();
auto packet = LogonProofPacket::build(A, M1);
socket->send(packet);
setState(AuthState::PROOF_SENT);
}
void AuthHandler::handleLogonProofResponse(network::Packet& packet) {
LOG_DEBUG("Handling LOGON_PROOF response");
LogonProofResponse response;
if (!LogonProofResponseParser::parse(packet, response)) {
fail("Failed to parse LOGON_PROOF response");
return;
}
if (!response.isSuccess()) {
fail("LOGON_PROOF failed: invalid proof");
return;
}
// Verify server proof
if (!srp->verifyServerProof(response.M2)) {
fail("Server proof verification failed");
return;
}
// Authentication successful!
sessionKey = srp->getSessionKey();
setState(AuthState::AUTHENTICATED);
LOG_INFO("========================================");
LOG_INFO(" AUTHENTICATION SUCCESSFUL!");
LOG_INFO("========================================");
LOG_INFO("User: ", username);
LOG_INFO("Session key size: ", sessionKey.size(), " bytes");
if (onSuccess) {
onSuccess(sessionKey);
}
}
void AuthHandler::sendRealmListRequest() {
LOG_DEBUG("Sending REALM_LIST request");
auto packet = RealmListPacket::build();
socket->send(packet);
setState(AuthState::REALM_LIST_REQUESTED);
}
void AuthHandler::handleRealmListResponse(network::Packet& packet) {
LOG_DEBUG("Handling REALM_LIST response");
RealmListResponse response;
if (!RealmListResponseParser::parse(packet, response)) {
LOG_ERROR("Failed to parse REALM_LIST response");
return;
}
realms = response.realms;
setState(AuthState::REALM_LIST_RECEIVED);
LOG_INFO("========================================");
LOG_INFO(" REALM LIST RECEIVED!");
LOG_INFO("========================================");
LOG_INFO("Total realms: ", realms.size());
for (size_t i = 0; i < realms.size(); ++i) {
const auto& realm = realms[i];
LOG_INFO("Realm ", (i + 1), ": ", realm.name);
LOG_INFO(" Address: ", realm.address);
LOG_INFO(" ID: ", (int)realm.id);
LOG_INFO(" Population: ", realm.population);
LOG_INFO(" Characters: ", (int)realm.characters);
if (realm.hasVersionInfo()) {
LOG_INFO(" Version: ", (int)realm.majorVersion, ".",
(int)realm.minorVersion, ".", (int)realm.patchVersion,
" (build ", realm.build, ")");
}
}
if (onRealmList) {
onRealmList(realms);
}
}
void AuthHandler::handlePacket(network::Packet& packet) {
if (packet.getSize() < 1) {
LOG_WARNING("Received empty packet");
return;
}
// Read opcode
uint8_t opcodeValue = packet.readUInt8();
// Note: packet now has read position advanced past opcode
AuthOpcode opcode = static_cast<AuthOpcode>(opcodeValue);
LOG_DEBUG("Received auth packet, opcode: 0x", std::hex, (int)opcodeValue, std::dec);
switch (opcode) {
case AuthOpcode::LOGON_CHALLENGE:
if (state == AuthState::CHALLENGE_SENT) {
handleLogonChallengeResponse(packet);
} else {
LOG_WARNING("Unexpected LOGON_CHALLENGE response in state: ", (int)state);
}
break;
case AuthOpcode::LOGON_PROOF:
if (state == AuthState::PROOF_SENT) {
handleLogonProofResponse(packet);
} else {
LOG_WARNING("Unexpected LOGON_PROOF response in state: ", (int)state);
}
break;
case AuthOpcode::REALM_LIST:
if (state == AuthState::REALM_LIST_REQUESTED) {
handleRealmListResponse(packet);
} else {
LOG_WARNING("Unexpected REALM_LIST response in state: ", (int)state);
}
break;
default:
LOG_WARNING("Unhandled auth opcode: 0x", std::hex, (int)opcodeValue, std::dec);
break;
}
}
void AuthHandler::update(float /*deltaTime*/) {
if (!socket) {
return;
}
// Update socket (processes incoming data and calls packet callback)
socket->update();
}
void AuthHandler::setState(AuthState newState) {
if (state != newState) {
LOG_DEBUG("Auth state: ", (int)state, " -> ", (int)newState);
state = newState;
}
}
void AuthHandler::fail(const std::string& reason) {
LOG_ERROR("Authentication failed: ", reason);
setState(AuthState::FAILED);
if (onFailure) {
onFailure(reason);
}
}
} // namespace auth
} // namespace wowee

32
src/auth/auth_opcodes.cpp Normal file
View file

@ -0,0 +1,32 @@
#include "auth/auth_opcodes.hpp"
namespace wowee {
namespace auth {
const char* getAuthResultString(AuthResult result) {
switch (result) {
case AuthResult::SUCCESS: return "Success";
case AuthResult::UNKNOWN0: return "Unknown Error 0";
case AuthResult::UNKNOWN1: return "Unknown Error 1";
case AuthResult::ACCOUNT_BANNED: return "Account Banned";
case AuthResult::ACCOUNT_INVALID: return "Account Invalid";
case AuthResult::PASSWORD_INVALID: return "Password Invalid";
case AuthResult::ALREADY_ONLINE: return "Already Online";
case AuthResult::OUT_OF_CREDIT: return "Out of Credit";
case AuthResult::BUSY: return "Server Busy";
case AuthResult::BUILD_INVALID: return "Build Invalid";
case AuthResult::BUILD_UPDATE: return "Build Update Required";
case AuthResult::INVALID_SERVER: return "Invalid Server";
case AuthResult::ACCOUNT_SUSPENDED: return "Account Suspended";
case AuthResult::ACCESS_DENIED: return "Access Denied";
case AuthResult::SURVEY: return "Survey Required";
case AuthResult::PARENTAL_CONTROL: return "Parental Control";
case AuthResult::LOCK_ENFORCED: return "Lock Enforced";
case AuthResult::TRIAL_EXPIRED: return "Trial Expired";
case AuthResult::BATTLE_NET: return "Battle.net Error";
default: return "Unknown";
}
}
} // namespace auth
} // namespace wowee

312
src/auth/auth_packets.cpp Normal file
View file

@ -0,0 +1,312 @@
#include "auth/auth_packets.hpp"
#include "core/logger.hpp"
#include <algorithm>
#include <cctype>
#include <cstring>
namespace wowee {
namespace auth {
network::Packet LogonChallengePacket::build(const std::string& account, const ClientInfo& info) {
// Convert account to uppercase
std::string upperAccount = account;
std::transform(upperAccount.begin(), upperAccount.end(), upperAccount.begin(), ::toupper);
// Calculate packet size
// Opcode(1) + unknown(1) + size(2) + game(4) + version(3) + build(2) +
// platform(4) + os(4) + locale(4) + timezone(4) + ip(4) + accountLen(1) + account(N)
uint16_t payloadSize = 30 + upperAccount.length();
network::Packet packet(static_cast<uint16_t>(AuthOpcode::LOGON_CHALLENGE));
// Unknown byte
packet.writeUInt8(0x00);
// Payload size
packet.writeUInt16(payloadSize);
// Game name (4 bytes, null-padded)
packet.writeBytes(reinterpret_cast<const uint8_t*>(info.game.c_str()),
std::min<size_t>(4, info.game.length()));
for (size_t i = info.game.length(); i < 4; ++i) {
packet.writeUInt8(0);
}
// Version (3 bytes)
packet.writeUInt8(info.majorVersion);
packet.writeUInt8(info.minorVersion);
packet.writeUInt8(info.patchVersion);
// Build (2 bytes)
packet.writeUInt16(info.build);
// Platform (4 bytes, null-padded)
packet.writeBytes(reinterpret_cast<const uint8_t*>(info.platform.c_str()),
std::min<size_t>(4, info.platform.length()));
for (size_t i = info.platform.length(); i < 4; ++i) {
packet.writeUInt8(0);
}
// OS (4 bytes, null-padded)
packet.writeBytes(reinterpret_cast<const uint8_t*>(info.os.c_str()),
std::min<size_t>(4, info.os.length()));
for (size_t i = info.os.length(); i < 4; ++i) {
packet.writeUInt8(0);
}
// Locale (4 bytes, null-padded)
packet.writeBytes(reinterpret_cast<const uint8_t*>(info.locale.c_str()),
std::min<size_t>(4, info.locale.length()));
for (size_t i = info.locale.length(); i < 4; ++i) {
packet.writeUInt8(0);
}
// Timezone
packet.writeUInt32(info.timezone);
// IP address (always 0)
packet.writeUInt32(0);
// Account length and name
packet.writeUInt8(static_cast<uint8_t>(upperAccount.length()));
packet.writeBytes(reinterpret_cast<const uint8_t*>(upperAccount.c_str()),
upperAccount.length());
LOG_DEBUG("Built LOGON_CHALLENGE packet for account: ", upperAccount);
LOG_DEBUG(" Payload size: ", payloadSize, " bytes");
LOG_DEBUG(" Total size: ", packet.getSize(), " bytes");
return packet;
}
bool LogonChallengeResponseParser::parse(network::Packet& packet, LogonChallengeResponse& response) {
// Read opcode (should be LOGON_CHALLENGE)
uint8_t opcode = packet.readUInt8();
if (opcode != static_cast<uint8_t>(AuthOpcode::LOGON_CHALLENGE)) {
LOG_ERROR("Invalid opcode in LOGON_CHALLENGE response: ", (int)opcode);
return false;
}
// Unknown byte
packet.readUInt8();
// Status
response.result = static_cast<AuthResult>(packet.readUInt8());
LOG_INFO("LOGON_CHALLENGE response: ", getAuthResultString(response.result));
if (response.result != AuthResult::SUCCESS) {
return true; // Valid packet, but authentication failed
}
// B (server public ephemeral) - 32 bytes
response.B.resize(32);
for (int i = 0; i < 32; ++i) {
response.B[i] = packet.readUInt8();
}
// g length and value
uint8_t gLen = packet.readUInt8();
response.g.resize(gLen);
for (uint8_t i = 0; i < gLen; ++i) {
response.g[i] = packet.readUInt8();
}
// N length and value
uint8_t nLen = packet.readUInt8();
response.N.resize(nLen);
for (uint8_t i = 0; i < nLen; ++i) {
response.N[i] = packet.readUInt8();
}
// Salt - 32 bytes
response.salt.resize(32);
for (int i = 0; i < 32; ++i) {
response.salt[i] = packet.readUInt8();
}
// Unknown/padding - 16 bytes
for (int i = 0; i < 16; ++i) {
packet.readUInt8();
}
// Security flags
response.securityFlags = packet.readUInt8();
LOG_DEBUG("Parsed LOGON_CHALLENGE response:");
LOG_DEBUG(" B size: ", response.B.size(), " bytes");
LOG_DEBUG(" g size: ", response.g.size(), " bytes");
LOG_DEBUG(" N size: ", response.N.size(), " bytes");
LOG_DEBUG(" salt size: ", response.salt.size(), " bytes");
LOG_DEBUG(" Security flags: ", (int)response.securityFlags);
return true;
}
network::Packet LogonProofPacket::build(const std::vector<uint8_t>& A,
const std::vector<uint8_t>& M1) {
if (A.size() != 32) {
LOG_ERROR("Invalid A size: ", A.size(), " (expected 32)");
}
if (M1.size() != 20) {
LOG_ERROR("Invalid M1 size: ", M1.size(), " (expected 20)");
}
network::Packet packet(static_cast<uint16_t>(AuthOpcode::LOGON_PROOF));
// A (client public ephemeral) - 32 bytes
packet.writeBytes(A.data(), A.size());
// M1 (client proof) - 20 bytes
packet.writeBytes(M1.data(), M1.size());
// CRC hash - 20 bytes (zeros)
for (int i = 0; i < 20; ++i) {
packet.writeUInt8(0);
}
// Number of keys
packet.writeUInt8(0);
// Security flags
packet.writeUInt8(0);
LOG_DEBUG("Built LOGON_PROOF packet:");
LOG_DEBUG(" A size: ", A.size(), " bytes");
LOG_DEBUG(" M1 size: ", M1.size(), " bytes");
LOG_DEBUG(" Total size: ", packet.getSize(), " bytes");
return packet;
}
bool LogonProofResponseParser::parse(network::Packet& packet, LogonProofResponse& response) {
// Read opcode (should be LOGON_PROOF)
uint8_t opcode = packet.readUInt8();
if (opcode != static_cast<uint8_t>(AuthOpcode::LOGON_PROOF)) {
LOG_ERROR("Invalid opcode in LOGON_PROOF response: ", (int)opcode);
return false;
}
// Status
response.status = packet.readUInt8();
LOG_INFO("LOGON_PROOF response status: ", (int)response.status);
if (response.status != 0) {
LOG_ERROR("LOGON_PROOF failed with status: ", (int)response.status);
return true; // Valid packet, but proof failed
}
// M2 (server proof) - 20 bytes
response.M2.resize(20);
for (int i = 0; i < 20; ++i) {
response.M2[i] = packet.readUInt8();
}
LOG_DEBUG("Parsed LOGON_PROOF response:");
LOG_DEBUG(" M2 size: ", response.M2.size(), " bytes");
return true;
}
network::Packet RealmListPacket::build() {
network::Packet packet(static_cast<uint16_t>(AuthOpcode::REALM_LIST));
// Unknown uint32 (per WoWDev documentation)
packet.writeUInt32(0x00);
LOG_DEBUG("Built REALM_LIST request packet");
LOG_DEBUG(" Total size: ", packet.getSize(), " bytes");
return packet;
}
bool RealmListResponseParser::parse(network::Packet& packet, RealmListResponse& response) {
// Read opcode (should be REALM_LIST)
uint8_t opcode = packet.readUInt8();
if (opcode != static_cast<uint8_t>(AuthOpcode::REALM_LIST)) {
LOG_ERROR("Invalid opcode in REALM_LIST response: ", (int)opcode);
return false;
}
// Packet size (2 bytes) - we already know the size, skip it
uint16_t packetSize = packet.readUInt16();
LOG_DEBUG("REALM_LIST response packet size: ", packetSize, " bytes");
// Unknown uint32
packet.readUInt32();
// Realm count
uint16_t realmCount = packet.readUInt16();
LOG_INFO("REALM_LIST response: ", realmCount, " realms");
response.realms.clear();
response.realms.reserve(realmCount);
for (uint16_t i = 0; i < realmCount; ++i) {
Realm realm;
// Icon
realm.icon = packet.readUInt8();
// Lock
realm.lock = packet.readUInt8();
// Flags
realm.flags = packet.readUInt8();
// Name (C-string)
realm.name = packet.readString();
// Address (C-string)
realm.address = packet.readString();
// Population (float)
// Read 4 bytes as little-endian float
uint32_t populationBits = packet.readUInt32();
std::memcpy(&realm.population, &populationBits, sizeof(float));
// Characters
realm.characters = packet.readUInt8();
// Timezone
realm.timezone = packet.readUInt8();
// ID
realm.id = packet.readUInt8();
// Version info (conditional - only if flags & 0x04)
if (realm.hasVersionInfo()) {
realm.majorVersion = packet.readUInt8();
realm.minorVersion = packet.readUInt8();
realm.patchVersion = packet.readUInt8();
realm.build = packet.readUInt16();
LOG_DEBUG(" Realm ", (int)i, " (", realm.name, ") version: ",
(int)realm.majorVersion, ".", (int)realm.minorVersion, ".",
(int)realm.patchVersion, " (", realm.build, ")");
} else {
LOG_DEBUG(" Realm ", (int)i, " (", realm.name, ") - no version info");
}
LOG_DEBUG(" Realm ", (int)i, " details:");
LOG_DEBUG(" Name: ", realm.name);
LOG_DEBUG(" Address: ", realm.address);
LOG_DEBUG(" ID: ", (int)realm.id);
LOG_DEBUG(" Icon: ", (int)realm.icon);
LOG_DEBUG(" Lock: ", (int)realm.lock);
LOG_DEBUG(" Flags: ", (int)realm.flags);
LOG_DEBUG(" Population: ", realm.population);
LOG_DEBUG(" Characters: ", (int)realm.characters);
LOG_DEBUG(" Timezone: ", (int)realm.timezone);
response.realms.push_back(realm);
}
LOG_INFO("Parsed ", response.realms.size(), " realms successfully");
return true;
}
} // namespace auth
} // namespace wowee

152
src/auth/big_num.cpp Normal file
View file

@ -0,0 +1,152 @@
#include "auth/big_num.hpp"
#include "core/logger.hpp"
#include <openssl/rand.h>
#include <algorithm>
namespace wowee {
namespace auth {
BigNum::BigNum() : bn(BN_new()) {
if (!bn) {
LOG_ERROR("Failed to create BIGNUM");
}
}
BigNum::BigNum(uint32_t value) : bn(BN_new()) {
BN_set_word(bn, value);
}
BigNum::BigNum(const std::vector<uint8_t>& bytes, bool littleEndian) : bn(BN_new()) {
if (littleEndian) {
// Convert little-endian to big-endian for OpenSSL
std::vector<uint8_t> reversed = bytes;
std::reverse(reversed.begin(), reversed.end());
BN_bin2bn(reversed.data(), reversed.size(), bn);
} else {
BN_bin2bn(bytes.data(), bytes.size(), bn);
}
}
BigNum::~BigNum() {
if (bn) {
BN_free(bn);
}
}
BigNum::BigNum(const BigNum& other) : bn(BN_dup(other.bn)) {}
BigNum& BigNum::operator=(const BigNum& other) {
if (this != &other) {
BN_free(bn);
bn = BN_dup(other.bn);
}
return *this;
}
BigNum::BigNum(BigNum&& other) noexcept : bn(other.bn) {
other.bn = nullptr;
}
BigNum& BigNum::operator=(BigNum&& other) noexcept {
if (this != &other) {
BN_free(bn);
bn = other.bn;
other.bn = nullptr;
}
return *this;
}
BigNum BigNum::fromRandom(int bytes) {
std::vector<uint8_t> randomBytes(bytes);
RAND_bytes(randomBytes.data(), bytes);
return BigNum(randomBytes, true);
}
BigNum BigNum::fromHex(const std::string& hex) {
BigNum result;
BN_hex2bn(&result.bn, hex.c_str());
return result;
}
BigNum BigNum::fromDecimal(const std::string& dec) {
BigNum result;
BN_dec2bn(&result.bn, dec.c_str());
return result;
}
BigNum BigNum::add(const BigNum& other) const {
BigNum result;
BN_add(result.bn, bn, other.bn);
return result;
}
BigNum BigNum::subtract(const BigNum& other) const {
BigNum result;
BN_sub(result.bn, bn, other.bn);
return result;
}
BigNum BigNum::multiply(const BigNum& other) const {
BigNum result;
BN_CTX* ctx = BN_CTX_new();
BN_mul(result.bn, bn, other.bn, ctx);
BN_CTX_free(ctx);
return result;
}
BigNum BigNum::mod(const BigNum& modulus) const {
BigNum result;
BN_CTX* ctx = BN_CTX_new();
BN_mod(result.bn, bn, modulus.bn, ctx);
BN_CTX_free(ctx);
return result;
}
BigNum BigNum::modPow(const BigNum& exponent, const BigNum& modulus) const {
BigNum result;
BN_CTX* ctx = BN_CTX_new();
BN_mod_exp(result.bn, bn, exponent.bn, modulus.bn, ctx);
BN_CTX_free(ctx);
return result;
}
bool BigNum::equals(const BigNum& other) const {
return BN_cmp(bn, other.bn) == 0;
}
bool BigNum::isZero() const {
return BN_is_zero(bn);
}
std::vector<uint8_t> BigNum::toArray(bool littleEndian, int minSize) const {
int size = BN_num_bytes(bn);
if (minSize > size) {
size = minSize;
}
std::vector<uint8_t> bytes(size, 0);
int actualSize = BN_bn2bin(bn, bytes.data() + (size - BN_num_bytes(bn)));
if (littleEndian) {
std::reverse(bytes.begin(), bytes.end());
}
return bytes;
}
std::string BigNum::toHex() const {
char* hex = BN_bn2hex(bn);
std::string result(hex);
OPENSSL_free(hex);
return result;
}
std::string BigNum::toDecimal() const {
char* dec = BN_bn2dec(bn);
std::string result(dec);
OPENSSL_free(dec);
return result;
}
} // namespace auth
} // namespace wowee

33
src/auth/crypto.cpp Normal file
View file

@ -0,0 +1,33 @@
#include "auth/crypto.hpp"
#include <openssl/sha.h>
#include <openssl/hmac.h>
namespace wowee {
namespace auth {
std::vector<uint8_t> Crypto::sha1(const std::vector<uint8_t>& data) {
std::vector<uint8_t> hash(SHA_DIGEST_LENGTH);
SHA1(data.data(), data.size(), hash.data());
return hash;
}
std::vector<uint8_t> Crypto::sha1(const std::string& data) {
std::vector<uint8_t> bytes(data.begin(), data.end());
return sha1(bytes);
}
std::vector<uint8_t> Crypto::hmacSHA1(const std::vector<uint8_t>& key,
const std::vector<uint8_t>& data) {
std::vector<uint8_t> hash(SHA_DIGEST_LENGTH);
unsigned int length = 0;
HMAC(EVP_sha1(),
key.data(), key.size(),
data.data(), data.size(),
hash.data(), &length);
return hash;
}
} // namespace auth
} // namespace wowee

75
src/auth/rc4.cpp Normal file
View file

@ -0,0 +1,75 @@
#include "auth/rc4.hpp"
#include "core/logger.hpp"
#include <cstring>
namespace wowee {
namespace auth {
RC4::RC4() : x(0), y(0) {
// Initialize state to identity
for (int i = 0; i < 256; ++i) {
state[i] = static_cast<uint8_t>(i);
}
}
void RC4::init(const std::vector<uint8_t>& key) {
if (key.empty()) {
LOG_ERROR("RC4: Cannot initialize with empty key");
return;
}
// Reset indices
x = 0;
y = 0;
// Initialize state
for (int i = 0; i < 256; ++i) {
state[i] = static_cast<uint8_t>(i);
}
// Key scheduling algorithm (KSA)
uint8_t j = 0;
for (int i = 0; i < 256; ++i) {
j = j + state[i] + key[i % key.size()];
// Swap state[i] and state[j]
uint8_t temp = state[i];
state[i] = state[j];
state[j] = temp;
}
LOG_DEBUG("RC4: Initialized with ", key.size(), "-byte key");
}
void RC4::process(uint8_t* data, size_t length) {
if (!data || length == 0) {
return;
}
// Pseudo-random generation algorithm (PRGA)
for (size_t n = 0; n < length; ++n) {
// Increment indices
x = x + 1;
y = y + state[x];
// Swap state[x] and state[y]
uint8_t temp = state[x];
state[x] = state[y];
state[y] = temp;
// Generate keystream byte and XOR with data
uint8_t keystreamByte = state[(state[x] + state[y]) & 0xFF];
data[n] ^= keystreamByte;
}
}
void RC4::drop(size_t count) {
// Drop keystream bytes by processing zeros
std::vector<uint8_t> dummy(count, 0);
process(dummy.data(), count);
LOG_DEBUG("RC4: Dropped ", count, " keystream bytes");
}
} // namespace auth
} // namespace wowee

269
src/auth/srp.cpp Normal file
View file

@ -0,0 +1,269 @@
#include "auth/srp.hpp"
#include "auth/crypto.hpp"
#include "core/logger.hpp"
#include <algorithm>
#include <cctype>
namespace wowee {
namespace auth {
SRP::SRP() : k(K_VALUE) {
LOG_DEBUG("SRP instance created");
}
void SRP::initialize(const std::string& username, const std::string& password) {
LOG_DEBUG("Initializing SRP with username: ", username);
// Store credentials for later use
stored_username = username;
stored_password = password;
initialized = true;
LOG_DEBUG("SRP initialized");
}
void SRP::feed(const std::vector<uint8_t>& B_bytes,
const std::vector<uint8_t>& g_bytes,
const std::vector<uint8_t>& N_bytes,
const std::vector<uint8_t>& salt_bytes) {
if (!initialized) {
LOG_ERROR("SRP not initialized! Call initialize() first.");
return;
}
LOG_DEBUG("Feeding SRP challenge data");
LOG_DEBUG(" B size: ", B_bytes.size(), " bytes");
LOG_DEBUG(" g size: ", g_bytes.size(), " bytes");
LOG_DEBUG(" N size: ", N_bytes.size(), " bytes");
LOG_DEBUG(" salt size: ", salt_bytes.size(), " bytes");
// Store server values (all little-endian)
this->B = BigNum(B_bytes, true);
this->g = BigNum(g_bytes, true);
this->N = BigNum(N_bytes, true);
this->s = BigNum(salt_bytes, true);
LOG_DEBUG("SRP challenge data loaded");
// Now compute everything in sequence
// 1. Compute auth hash: H(I:P)
std::vector<uint8_t> auth_hash = computeAuthHash(stored_username, stored_password);
// 2. Compute x = H(s | H(I:P))
std::vector<uint8_t> x_input;
x_input.insert(x_input.end(), salt_bytes.begin(), salt_bytes.end());
x_input.insert(x_input.end(), auth_hash.begin(), auth_hash.end());
std::vector<uint8_t> x_bytes = Crypto::sha1(x_input);
x = BigNum(x_bytes, true);
LOG_DEBUG("Computed x (salted password hash)");
// 3. Generate client ephemeral (a, A)
computeClientEphemeral();
// 4. Compute session key (S, K)
computeSessionKey();
// 5. Compute proofs (M1, M2)
computeProofs(stored_username);
LOG_INFO("SRP authentication data ready!");
}
std::vector<uint8_t> SRP::computeAuthHash(const std::string& username,
const std::string& password) const {
// Convert to uppercase (WoW requirement)
std::string upperUser = username;
std::string upperPass = password;
std::transform(upperUser.begin(), upperUser.end(), upperUser.begin(), ::toupper);
std::transform(upperPass.begin(), upperPass.end(), upperPass.begin(), ::toupper);
// H(I:P)
std::string combined = upperUser + ":" + upperPass;
return Crypto::sha1(combined);
}
void SRP::computeClientEphemeral() {
LOG_DEBUG("Computing client ephemeral");
// Generate random private ephemeral a (19 bytes = 152 bits)
// Keep trying until we get a valid A
int attempts = 0;
while (attempts < 100) {
a = BigNum::fromRandom(19);
// A = g^a mod N
A = g.modPow(a, N);
// Ensure A is not zero
if (!A.mod(N).isZero()) {
LOG_DEBUG("Generated valid client ephemeral after ", attempts + 1, " attempts");
break;
}
attempts++;
}
if (attempts >= 100) {
LOG_ERROR("Failed to generate valid client ephemeral after 100 attempts!");
}
}
void SRP::computeSessionKey() {
LOG_DEBUG("Computing session key");
// u = H(A | B) - scrambling parameter
std::vector<uint8_t> A_bytes = A.toArray(true, 32); // 32 bytes, little-endian
std::vector<uint8_t> B_bytes = B.toArray(true, 32); // 32 bytes, little-endian
std::vector<uint8_t> AB;
AB.insert(AB.end(), A_bytes.begin(), A_bytes.end());
AB.insert(AB.end(), B_bytes.begin(), B_bytes.end());
std::vector<uint8_t> u_bytes = Crypto::sha1(AB);
u = BigNum(u_bytes, true);
LOG_DEBUG("Scrambler u calculated");
// Compute session key: S = (B - kg^x)^(a + ux) mod N
// Step 1: kg^x
BigNum gx = g.modPow(x, N);
BigNum kgx = k.multiply(gx);
// Step 2: B - kg^x
BigNum B_minus_kgx = B.subtract(kgx);
// Step 3: ux
BigNum ux = u.multiply(x);
// Step 4: a + ux
BigNum aux = a.add(ux);
// Step 5: (B - kg^x)^(a + ux) mod N
S = B_minus_kgx.modPow(aux, N);
LOG_DEBUG("Session key S calculated");
// Interleave the session key to create K
// Split S into even and odd bytes, hash each half, then interleave
std::vector<uint8_t> S_bytes = S.toArray(true, 32); // 32 bytes for WoW
std::vector<uint8_t> S1, S2;
for (size_t i = 0; i < 16; ++i) {
S1.push_back(S_bytes[i * 2]); // Even indices
S2.push_back(S_bytes[i * 2 + 1]); // Odd indices
}
// Hash each half
std::vector<uint8_t> S1_hash = Crypto::sha1(S1); // 20 bytes
std::vector<uint8_t> S2_hash = Crypto::sha1(S2); // 20 bytes
// Interleave the hashes to create K (40 bytes total)
K.clear();
K.reserve(40);
for (size_t i = 0; i < 20; ++i) {
K.push_back(S1_hash[i]);
K.push_back(S2_hash[i]);
}
LOG_DEBUG("Interleaved session key K created (", K.size(), " bytes)");
}
void SRP::computeProofs(const std::string& username) {
LOG_DEBUG("Computing authentication proofs");
// Convert username to uppercase
std::string upperUser = username;
std::transform(upperUser.begin(), upperUser.end(), upperUser.begin(), ::toupper);
// Compute H(N) and H(g)
std::vector<uint8_t> N_bytes = N.toArray(true, 256); // Full 256 bytes
std::vector<uint8_t> g_bytes = g.toArray(true);
std::vector<uint8_t> N_hash = Crypto::sha1(N_bytes);
std::vector<uint8_t> g_hash = Crypto::sha1(g_bytes);
// XOR them: H(N) ^ H(g)
std::vector<uint8_t> Ng_xor(20);
for (size_t i = 0; i < 20; ++i) {
Ng_xor[i] = N_hash[i] ^ g_hash[i];
}
// Compute H(username)
std::vector<uint8_t> user_hash = Crypto::sha1(upperUser);
// Get A, B, and salt as byte arrays
std::vector<uint8_t> A_bytes = A.toArray(true, 32);
std::vector<uint8_t> B_bytes = B.toArray(true, 32);
std::vector<uint8_t> s_bytes = s.toArray(true, 32);
// M1 = H( H(N)^H(g) | H(I) | s | A | B | K )
std::vector<uint8_t> M1_input;
M1_input.insert(M1_input.end(), Ng_xor.begin(), Ng_xor.end()); // 20 bytes
M1_input.insert(M1_input.end(), user_hash.begin(), user_hash.end()); // 20 bytes
M1_input.insert(M1_input.end(), s_bytes.begin(), s_bytes.end()); // 32 bytes
M1_input.insert(M1_input.end(), A_bytes.begin(), A_bytes.end()); // 32 bytes
M1_input.insert(M1_input.end(), B_bytes.begin(), B_bytes.end()); // 32 bytes
M1_input.insert(M1_input.end(), K.begin(), K.end()); // 40 bytes
M1 = Crypto::sha1(M1_input); // 20 bytes
LOG_DEBUG("Client proof M1 calculated (", M1.size(), " bytes)");
// M2 = H( A | M1 | K )
std::vector<uint8_t> M2_input;
M2_input.insert(M2_input.end(), A_bytes.begin(), A_bytes.end()); // 32 bytes
M2_input.insert(M2_input.end(), M1.begin(), M1.end()); // 20 bytes
M2_input.insert(M2_input.end(), K.begin(), K.end()); // 40 bytes
M2 = Crypto::sha1(M2_input); // 20 bytes
LOG_DEBUG("Expected server proof M2 calculated (", M2.size(), " bytes)");
}
std::vector<uint8_t> SRP::getA() const {
if (A.isZero()) {
LOG_WARNING("Client ephemeral A not yet computed!");
}
return A.toArray(true, 32); // 32 bytes, little-endian
}
std::vector<uint8_t> SRP::getM1() const {
if (M1.empty()) {
LOG_WARNING("Client proof M1 not yet computed!");
}
return M1;
}
bool SRP::verifyServerProof(const std::vector<uint8_t>& serverM2) const {
if (M2.empty()) {
LOG_ERROR("Expected server proof M2 not computed!");
return false;
}
if (serverM2.size() != M2.size()) {
LOG_ERROR("Server proof size mismatch: ", serverM2.size(), " vs ", M2.size());
return false;
}
bool match = std::equal(M2.begin(), M2.end(), serverM2.begin());
if (match) {
LOG_INFO("Server proof verified successfully!");
} else {
LOG_ERROR("Server proof verification FAILED!");
}
return match;
}
std::vector<uint8_t> SRP::getSessionKey() const {
if (K.empty()) {
LOG_WARNING("Session key K not yet computed!");
}
return K;
}
} // namespace auth
} // namespace wowee

1403
src/core/application.cpp Normal file

File diff suppressed because it is too large Load diff

81
src/core/input.cpp Normal file
View file

@ -0,0 +1,81 @@
#include "core/input.hpp"
namespace wowee {
namespace core {
Input& Input::getInstance() {
static Input instance;
return instance;
}
void Input::update() {
// Copy current state to previous
previousKeyState = currentKeyState;
previousMouseState = currentMouseState;
previousMousePosition = mousePosition;
// Get current keyboard state
const Uint8* keyState = SDL_GetKeyboardState(nullptr);
for (int i = 0; i < NUM_KEYS; ++i) {
currentKeyState[i] = keyState[i];
}
// Get current mouse state
int mouseX, mouseY;
Uint32 mouseState = SDL_GetMouseState(&mouseX, &mouseY);
mousePosition = glm::vec2(static_cast<float>(mouseX), static_cast<float>(mouseY));
for (int i = 0; i < NUM_MOUSE_BUTTONS; ++i) {
currentMouseState[i] = (mouseState & SDL_BUTTON(i)) != 0;
}
// Calculate mouse delta
mouseDelta = mousePosition - previousMousePosition;
// Reset wheel delta (will be set by handleEvent)
mouseWheelDelta = 0.0f;
}
void Input::handleEvent(const SDL_Event& event) {
if (event.type == SDL_MOUSEWHEEL) {
mouseWheelDelta = static_cast<float>(event.wheel.y);
}
}
bool Input::isKeyPressed(SDL_Scancode key) const {
if (key < 0 || key >= NUM_KEYS) return false;
return currentKeyState[key];
}
bool Input::isKeyJustPressed(SDL_Scancode key) const {
if (key < 0 || key >= NUM_KEYS) return false;
return currentKeyState[key] && !previousKeyState[key];
}
bool Input::isKeyJustReleased(SDL_Scancode key) const {
if (key < 0 || key >= NUM_KEYS) return false;
return !currentKeyState[key] && previousKeyState[key];
}
bool Input::isMouseButtonPressed(int button) const {
if (button < 0 || button >= NUM_MOUSE_BUTTONS) return false;
return currentMouseState[button];
}
bool Input::isMouseButtonJustPressed(int button) const {
if (button < 0 || button >= NUM_MOUSE_BUTTONS) return false;
return currentMouseState[button] && !previousMouseState[button];
}
bool Input::isMouseButtonJustReleased(int button) const {
if (button < 0 || button >= NUM_MOUSE_BUTTONS) return false;
return !currentMouseState[button] && previousMouseState[button];
}
void Input::setMouseLocked(bool locked) {
mouseLocked = locked;
SDL_SetRelativeMouseMode(locked ? SDL_TRUE : SDL_FALSE);
}
} // namespace core
} // namespace wowee

52
src/core/logger.cpp Normal file
View file

@ -0,0 +1,52 @@
#include "core/logger.hpp"
#include <chrono>
#include <iomanip>
#include <ctime>
namespace wowee {
namespace core {
Logger& Logger::getInstance() {
static Logger instance;
return instance;
}
void Logger::log(LogLevel level, const std::string& message) {
if (level < minLevel) {
return;
}
std::lock_guard<std::mutex> lock(mutex);
// Get current time
auto now = std::chrono::system_clock::now();
auto time = std::chrono::system_clock::to_time_t(now);
auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(
now.time_since_epoch()) % 1000;
std::tm tm;
localtime_r(&time, &tm);
// Format: [YYYY-MM-DD HH:MM:SS.mmm] [LEVEL] message
std::cout << "["
<< std::put_time(&tm, "%Y-%m-%d %H:%M:%S")
<< "." << std::setfill('0') << std::setw(3) << ms.count()
<< "] [";
switch (level) {
case LogLevel::DEBUG: std::cout << "DEBUG"; break;
case LogLevel::INFO: std::cout << "INFO "; break;
case LogLevel::WARNING: std::cout << "WARN "; break;
case LogLevel::ERROR: std::cout << "ERROR"; break;
case LogLevel::FATAL: std::cout << "FATAL"; break;
}
std::cout << "] " << message << std::endl;
}
void Logger::setLogLevel(LogLevel level) {
minLevel = level;
}
} // namespace core
} // namespace wowee

134
src/core/window.cpp Normal file
View file

@ -0,0 +1,134 @@
#include "core/window.hpp"
#include "core/logger.hpp"
#include <GL/glew.h>
namespace wowee {
namespace core {
Window::Window(const WindowConfig& config)
: config(config)
, width(config.width)
, height(config.height) {
}
Window::~Window() {
shutdown();
}
bool Window::initialize() {
LOG_INFO("Initializing window: ", config.title);
// Initialize SDL
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS) != 0) {
LOG_ERROR("Failed to initialize SDL: ", SDL_GetError());
return false;
}
// Set OpenGL attributes
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 3);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE);
SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24);
SDL_GL_SetAttribute(SDL_GL_STENCIL_SIZE, 8);
// Create window
Uint32 flags = SDL_WINDOW_OPENGL | SDL_WINDOW_SHOWN;
if (config.fullscreen) {
flags |= SDL_WINDOW_FULLSCREEN_DESKTOP;
}
if (config.resizable) {
flags |= SDL_WINDOW_RESIZABLE;
}
window = SDL_CreateWindow(
config.title.c_str(),
SDL_WINDOWPOS_CENTERED,
SDL_WINDOWPOS_CENTERED,
width,
height,
flags
);
if (!window) {
LOG_ERROR("Failed to create window: ", SDL_GetError());
return false;
}
// Create OpenGL context
glContext = SDL_GL_CreateContext(window);
if (!glContext) {
LOG_ERROR("Failed to create OpenGL context: ", SDL_GetError());
return false;
}
// Set VSync
if (SDL_GL_SetSwapInterval(config.vsync ? 1 : 0) != 0) {
LOG_WARNING("Failed to set VSync: ", SDL_GetError());
}
// Initialize GLEW
glewExperimental = GL_TRUE;
GLenum glewError = glewInit();
if (glewError != GLEW_OK) {
LOG_ERROR("Failed to initialize GLEW: ", glewGetErrorString(glewError));
return false;
}
// Log OpenGL info
LOG_INFO("OpenGL Version: ", glGetString(GL_VERSION));
LOG_INFO("GLSL Version: ", glGetString(GL_SHADING_LANGUAGE_VERSION));
LOG_INFO("Renderer: ", glGetString(GL_RENDERER));
LOG_INFO("Vendor: ", glGetString(GL_VENDOR));
// Set up OpenGL defaults
glEnable(GL_DEPTH_TEST);
glDepthFunc(GL_LESS);
glEnable(GL_CULL_FACE);
glCullFace(GL_BACK);
glFrontFace(GL_CCW);
LOG_INFO("Window initialized successfully");
return true;
}
void Window::shutdown() {
if (glContext) {
SDL_GL_DeleteContext(glContext);
glContext = nullptr;
}
if (window) {
SDL_DestroyWindow(window);
window = nullptr;
}
SDL_Quit();
LOG_INFO("Window shutdown complete");
}
void Window::swapBuffers() {
SDL_GL_SwapWindow(window);
}
void Window::pollEvents() {
SDL_Event event;
while (SDL_PollEvent(&event)) {
// ImGui will handle events in UI manager
// For now, just handle quit
if (event.type == SDL_QUIT) {
shouldCloseFlag = true;
}
else if (event.type == SDL_WINDOWEVENT) {
if (event.window.event == SDL_WINDOWEVENT_RESIZED) {
width = event.window.data1;
height = event.window.data2;
glViewport(0, 0, width, height);
LOG_DEBUG("Window resized to ", width, "x", height);
}
}
}
}
} // namespace core
} // namespace wowee

48
src/game/character.cpp Normal file
View file

@ -0,0 +1,48 @@
#include "game/character.hpp"
namespace wowee {
namespace game {
const char* getRaceName(Race race) {
switch (race) {
case Race::HUMAN: return "Human";
case Race::ORC: return "Orc";
case Race::DWARF: return "Dwarf";
case Race::NIGHT_ELF: return "Night Elf";
case Race::UNDEAD: return "Undead";
case Race::TAUREN: return "Tauren";
case Race::GNOME: return "Gnome";
case Race::TROLL: return "Troll";
case Race::GOBLIN: return "Goblin";
case Race::BLOOD_ELF: return "Blood Elf";
case Race::DRAENEI: return "Draenei";
default: return "Unknown";
}
}
const char* getClassName(Class characterClass) {
switch (characterClass) {
case Class::WARRIOR: return "Warrior";
case Class::PALADIN: return "Paladin";
case Class::HUNTER: return "Hunter";
case Class::ROGUE: return "Rogue";
case Class::PRIEST: return "Priest";
case Class::DEATH_KNIGHT: return "Death Knight";
case Class::SHAMAN: return "Shaman";
case Class::MAGE: return "Mage";
case Class::WARLOCK: return "Warlock";
case Class::DRUID: return "Druid";
default: return "Unknown";
}
}
const char* getGenderName(Gender gender) {
switch (gender) {
case Gender::MALE: return "Male";
case Gender::FEMALE: return "Female";
default: return "Unknown";
}
}
} // namespace game
} // namespace wowee

37
src/game/entity.cpp Normal file
View file

@ -0,0 +1,37 @@
#include "game/entity.hpp"
#include "core/logger.hpp"
namespace wowee {
namespace game {
void EntityManager::addEntity(uint64_t guid, std::shared_ptr<Entity> entity) {
if (!entity) {
LOG_WARNING("Attempted to add null entity with GUID: 0x", std::hex, guid, std::dec);
return;
}
entities[guid] = entity;
LOG_DEBUG("Added entity: GUID=0x", std::hex, guid, std::dec,
", Type=", static_cast<int>(entity->getType()));
}
void EntityManager::removeEntity(uint64_t guid) {
auto it = entities.find(guid);
if (it != entities.end()) {
LOG_DEBUG("Removed entity: GUID=0x", std::hex, guid, std::dec);
entities.erase(it);
}
}
std::shared_ptr<Entity> EntityManager::getEntity(uint64_t guid) const {
auto it = entities.find(guid);
return (it != entities.end()) ? it->second : nullptr;
}
bool EntityManager::hasEntity(uint64_t guid) const {
return entities.find(guid) != entities.end();
}
} // namespace game
} // namespace wowee

843
src/game/game_handler.cpp Normal file
View file

@ -0,0 +1,843 @@
#include "game/game_handler.hpp"
#include "game/opcodes.hpp"
#include "network/world_socket.hpp"
#include "network/packet.hpp"
#include "core/logger.hpp"
#include <algorithm>
#include <cctype>
#include <random>
#include <chrono>
namespace wowee {
namespace game {
GameHandler::GameHandler() {
LOG_DEBUG("GameHandler created");
}
GameHandler::~GameHandler() {
disconnect();
}
bool GameHandler::connect(const std::string& host,
uint16_t port,
const std::vector<uint8_t>& sessionKey,
const std::string& accountName,
uint32_t build) {
if (sessionKey.size() != 40) {
LOG_ERROR("Invalid session key size: ", sessionKey.size(), " (expected 40)");
fail("Invalid session key");
return false;
}
LOG_INFO("========================================");
LOG_INFO(" CONNECTING TO WORLD SERVER");
LOG_INFO("========================================");
LOG_INFO("Host: ", host);
LOG_INFO("Port: ", port);
LOG_INFO("Account: ", accountName);
LOG_INFO("Build: ", build);
// Store authentication data
this->sessionKey = sessionKey;
this->accountName = accountName;
this->build = build;
// Generate random client seed
this->clientSeed = generateClientSeed();
LOG_DEBUG("Generated client seed: 0x", std::hex, clientSeed, std::dec);
// Create world socket
socket = std::make_unique<network::WorldSocket>();
// Set up packet callback
socket->setPacketCallback([this](const network::Packet& packet) {
network::Packet mutablePacket = packet;
handlePacket(mutablePacket);
});
// Connect to world server
setState(WorldState::CONNECTING);
if (!socket->connect(host, port)) {
LOG_ERROR("Failed to connect to world server");
fail("Connection failed");
return false;
}
setState(WorldState::CONNECTED);
LOG_INFO("Connected to world server, waiting for SMSG_AUTH_CHALLENGE...");
return true;
}
void GameHandler::disconnect() {
if (socket) {
socket->disconnect();
socket.reset();
}
setState(WorldState::DISCONNECTED);
LOG_INFO("Disconnected from world server");
}
bool GameHandler::isConnected() const {
return socket && socket->isConnected();
}
void GameHandler::update(float deltaTime) {
if (!socket) {
return;
}
// Update socket (processes incoming data and triggers callbacks)
socket->update();
// Validate target still exists
if (targetGuid != 0 && !entityManager.hasEntity(targetGuid)) {
clearTarget();
}
// Send periodic heartbeat if in world
if (state == WorldState::IN_WORLD) {
timeSinceLastPing += deltaTime;
if (timeSinceLastPing >= pingInterval) {
sendPing();
timeSinceLastPing = 0.0f;
}
}
}
void GameHandler::handlePacket(network::Packet& packet) {
if (packet.getSize() < 1) {
LOG_WARNING("Received empty packet");
return;
}
uint16_t opcode = packet.getOpcode();
LOG_DEBUG("Received world packet: opcode=0x", std::hex, opcode, std::dec,
" size=", packet.getSize(), " bytes");
// Route packet based on opcode
Opcode opcodeEnum = static_cast<Opcode>(opcode);
switch (opcodeEnum) {
case Opcode::SMSG_AUTH_CHALLENGE:
if (state == WorldState::CONNECTED) {
handleAuthChallenge(packet);
} else {
LOG_WARNING("Unexpected SMSG_AUTH_CHALLENGE in state: ", (int)state);
}
break;
case Opcode::SMSG_AUTH_RESPONSE:
if (state == WorldState::AUTH_SENT) {
handleAuthResponse(packet);
} else {
LOG_WARNING("Unexpected SMSG_AUTH_RESPONSE in state: ", (int)state);
}
break;
case Opcode::SMSG_CHAR_ENUM:
if (state == WorldState::CHAR_LIST_REQUESTED) {
handleCharEnum(packet);
} else {
LOG_WARNING("Unexpected SMSG_CHAR_ENUM in state: ", (int)state);
}
break;
case Opcode::SMSG_LOGIN_VERIFY_WORLD:
if (state == WorldState::ENTERING_WORLD) {
handleLoginVerifyWorld(packet);
} else {
LOG_WARNING("Unexpected SMSG_LOGIN_VERIFY_WORLD in state: ", (int)state);
}
break;
case Opcode::SMSG_ACCOUNT_DATA_TIMES:
// Can be received at any time after authentication
handleAccountDataTimes(packet);
break;
case Opcode::SMSG_MOTD:
// Can be received at any time after entering world
handleMotd(packet);
break;
case Opcode::SMSG_PONG:
// Can be received at any time after entering world
handlePong(packet);
break;
case Opcode::SMSG_UPDATE_OBJECT:
// Can be received after entering world
if (state == WorldState::IN_WORLD) {
handleUpdateObject(packet);
}
break;
case Opcode::SMSG_DESTROY_OBJECT:
// Can be received after entering world
if (state == WorldState::IN_WORLD) {
handleDestroyObject(packet);
}
break;
case Opcode::SMSG_MESSAGECHAT:
// Can be received after entering world
if (state == WorldState::IN_WORLD) {
handleMessageChat(packet);
}
break;
default:
LOG_WARNING("Unhandled world opcode: 0x", std::hex, opcode, std::dec);
break;
}
}
void GameHandler::handleAuthChallenge(network::Packet& packet) {
LOG_INFO("Handling SMSG_AUTH_CHALLENGE");
AuthChallengeData challenge;
if (!AuthChallengeParser::parse(packet, challenge)) {
fail("Failed to parse SMSG_AUTH_CHALLENGE");
return;
}
if (!challenge.isValid()) {
fail("Invalid auth challenge data");
return;
}
// Store server seed
serverSeed = challenge.serverSeed;
LOG_DEBUG("Server seed: 0x", std::hex, serverSeed, std::dec);
setState(WorldState::CHALLENGE_RECEIVED);
// Send authentication session
sendAuthSession();
}
void GameHandler::sendAuthSession() {
LOG_INFO("Sending CMSG_AUTH_SESSION");
// Build authentication packet
auto packet = AuthSessionPacket::build(
build,
accountName,
clientSeed,
sessionKey,
serverSeed
);
LOG_DEBUG("CMSG_AUTH_SESSION packet size: ", packet.getSize(), " bytes");
// Send packet (NOT encrypted yet)
socket->send(packet);
// CRITICAL: Initialize encryption AFTER sending AUTH_SESSION
// but BEFORE receiving AUTH_RESPONSE
LOG_INFO("Initializing RC4 header encryption...");
socket->initEncryption(sessionKey);
setState(WorldState::AUTH_SENT);
LOG_INFO("CMSG_AUTH_SESSION sent, encryption initialized, waiting for response...");
}
void GameHandler::handleAuthResponse(network::Packet& packet) {
LOG_INFO("Handling SMSG_AUTH_RESPONSE");
AuthResponseData response;
if (!AuthResponseParser::parse(packet, response)) {
fail("Failed to parse SMSG_AUTH_RESPONSE");
return;
}
if (!response.isSuccess()) {
std::string reason = std::string("Authentication failed: ") +
getAuthResultString(response.result);
fail(reason);
return;
}
// Authentication successful!
setState(WorldState::AUTHENTICATED);
LOG_INFO("========================================");
LOG_INFO(" WORLD AUTHENTICATION SUCCESSFUL!");
LOG_INFO("========================================");
LOG_INFO("Connected to world server");
LOG_INFO("Ready for character operations");
setState(WorldState::READY);
// Call success callback
if (onSuccess) {
onSuccess();
}
}
void GameHandler::requestCharacterList() {
if (state != WorldState::READY && state != WorldState::AUTHENTICATED) {
LOG_WARNING("Cannot request character list in state: ", (int)state);
return;
}
LOG_INFO("Requesting character list from server...");
// Build CMSG_CHAR_ENUM packet (no body, just opcode)
auto packet = CharEnumPacket::build();
// Send packet
socket->send(packet);
setState(WorldState::CHAR_LIST_REQUESTED);
LOG_INFO("CMSG_CHAR_ENUM sent, waiting for character list...");
}
void GameHandler::handleCharEnum(network::Packet& packet) {
LOG_INFO("Handling SMSG_CHAR_ENUM");
CharEnumResponse response;
if (!CharEnumParser::parse(packet, response)) {
fail("Failed to parse SMSG_CHAR_ENUM");
return;
}
// Store characters
characters = response.characters;
setState(WorldState::CHAR_LIST_RECEIVED);
LOG_INFO("========================================");
LOG_INFO(" CHARACTER LIST RECEIVED");
LOG_INFO("========================================");
LOG_INFO("Found ", characters.size(), " character(s)");
if (characters.empty()) {
LOG_INFO("No characters on this account");
} else {
LOG_INFO("Characters:");
for (size_t i = 0; i < characters.size(); ++i) {
const auto& character = characters[i];
LOG_INFO(" [", i + 1, "] ", character.name);
LOG_INFO(" GUID: 0x", std::hex, character.guid, std::dec);
LOG_INFO(" ", getRaceName(character.race), " ",
getClassName(character.characterClass));
LOG_INFO(" Level ", (int)character.level);
}
}
LOG_INFO("Ready to select character");
}
void GameHandler::selectCharacter(uint64_t characterGuid) {
if (state != WorldState::CHAR_LIST_RECEIVED) {
LOG_WARNING("Cannot select character in state: ", (int)state);
return;
}
LOG_INFO("========================================");
LOG_INFO(" ENTERING WORLD");
LOG_INFO("========================================");
LOG_INFO("Character GUID: 0x", std::hex, characterGuid, std::dec);
// Find character name for logging
for (const auto& character : characters) {
if (character.guid == characterGuid) {
LOG_INFO("Character: ", character.name);
LOG_INFO("Level ", (int)character.level, " ",
getRaceName(character.race), " ",
getClassName(character.characterClass));
break;
}
}
// Build CMSG_PLAYER_LOGIN packet
auto packet = PlayerLoginPacket::build(characterGuid);
// Send packet
socket->send(packet);
setState(WorldState::ENTERING_WORLD);
LOG_INFO("CMSG_PLAYER_LOGIN sent, entering world...");
}
void GameHandler::handleLoginVerifyWorld(network::Packet& packet) {
LOG_INFO("Handling SMSG_LOGIN_VERIFY_WORLD");
LoginVerifyWorldData data;
if (!LoginVerifyWorldParser::parse(packet, data)) {
fail("Failed to parse SMSG_LOGIN_VERIFY_WORLD");
return;
}
if (!data.isValid()) {
fail("Invalid world entry data");
return;
}
// Successfully entered the world!
setState(WorldState::IN_WORLD);
LOG_INFO("========================================");
LOG_INFO(" SUCCESSFULLY ENTERED WORLD!");
LOG_INFO("========================================");
LOG_INFO("Map ID: ", data.mapId);
LOG_INFO("Position: (", data.x, ", ", data.y, ", ", data.z, ")");
LOG_INFO("Orientation: ", data.orientation, " radians");
LOG_INFO("Player is now in the game world");
// Initialize movement info with world entry position
movementInfo.x = data.x;
movementInfo.y = data.y;
movementInfo.z = data.z;
movementInfo.orientation = data.orientation;
movementInfo.flags = 0;
movementInfo.flags2 = 0;
movementInfo.time = 0;
}
void GameHandler::handleAccountDataTimes(network::Packet& packet) {
LOG_DEBUG("Handling SMSG_ACCOUNT_DATA_TIMES");
AccountDataTimesData data;
if (!AccountDataTimesParser::parse(packet, data)) {
LOG_WARNING("Failed to parse SMSG_ACCOUNT_DATA_TIMES");
return;
}
LOG_DEBUG("Account data times received (server time: ", data.serverTime, ")");
}
void GameHandler::handleMotd(network::Packet& packet) {
LOG_INFO("Handling SMSG_MOTD");
MotdData data;
if (!MotdParser::parse(packet, data)) {
LOG_WARNING("Failed to parse SMSG_MOTD");
return;
}
if (!data.isEmpty()) {
LOG_INFO("========================================");
LOG_INFO(" MESSAGE OF THE DAY");
LOG_INFO("========================================");
for (const auto& line : data.lines) {
LOG_INFO(line);
}
LOG_INFO("========================================");
}
}
void GameHandler::sendPing() {
if (state != WorldState::IN_WORLD) {
return;
}
// Increment sequence number
pingSequence++;
LOG_DEBUG("Sending CMSG_PING (heartbeat)");
LOG_DEBUG(" Sequence: ", pingSequence);
// Build and send ping packet
auto packet = PingPacket::build(pingSequence, lastLatency);
socket->send(packet);
}
void GameHandler::handlePong(network::Packet& packet) {
LOG_DEBUG("Handling SMSG_PONG");
PongData data;
if (!PongParser::parse(packet, data)) {
LOG_WARNING("Failed to parse SMSG_PONG");
return;
}
// Verify sequence matches
if (data.sequence != pingSequence) {
LOG_WARNING("SMSG_PONG sequence mismatch: expected ", pingSequence,
", got ", data.sequence);
return;
}
LOG_DEBUG("Heartbeat acknowledged (sequence: ", data.sequence, ")");
}
void GameHandler::sendMovement(Opcode opcode) {
if (state != WorldState::IN_WORLD) {
LOG_WARNING("Cannot send movement in state: ", (int)state);
return;
}
// Update movement time
movementInfo.time = ++movementTime;
// Update movement flags based on opcode
switch (opcode) {
case Opcode::CMSG_MOVE_START_FORWARD:
movementInfo.flags |= static_cast<uint32_t>(MovementFlags::FORWARD);
break;
case Opcode::CMSG_MOVE_START_BACKWARD:
movementInfo.flags |= static_cast<uint32_t>(MovementFlags::BACKWARD);
break;
case Opcode::CMSG_MOVE_STOP:
movementInfo.flags &= ~(static_cast<uint32_t>(MovementFlags::FORWARD) |
static_cast<uint32_t>(MovementFlags::BACKWARD));
break;
case Opcode::CMSG_MOVE_START_STRAFE_LEFT:
movementInfo.flags |= static_cast<uint32_t>(MovementFlags::STRAFE_LEFT);
break;
case Opcode::CMSG_MOVE_START_STRAFE_RIGHT:
movementInfo.flags |= static_cast<uint32_t>(MovementFlags::STRAFE_RIGHT);
break;
case Opcode::CMSG_MOVE_STOP_STRAFE:
movementInfo.flags &= ~(static_cast<uint32_t>(MovementFlags::STRAFE_LEFT) |
static_cast<uint32_t>(MovementFlags::STRAFE_RIGHT));
break;
case Opcode::CMSG_MOVE_JUMP:
movementInfo.flags |= static_cast<uint32_t>(MovementFlags::FALLING);
break;
case Opcode::CMSG_MOVE_START_TURN_LEFT:
movementInfo.flags |= static_cast<uint32_t>(MovementFlags::TURN_LEFT);
break;
case Opcode::CMSG_MOVE_START_TURN_RIGHT:
movementInfo.flags |= static_cast<uint32_t>(MovementFlags::TURN_RIGHT);
break;
case Opcode::CMSG_MOVE_STOP_TURN:
movementInfo.flags &= ~(static_cast<uint32_t>(MovementFlags::TURN_LEFT) |
static_cast<uint32_t>(MovementFlags::TURN_RIGHT));
break;
case Opcode::CMSG_MOVE_FALL_LAND:
movementInfo.flags &= ~static_cast<uint32_t>(MovementFlags::FALLING);
break;
case Opcode::CMSG_MOVE_HEARTBEAT:
// No flag changes — just sends current position
break;
default:
break;
}
LOG_DEBUG("Sending movement packet: opcode=0x", std::hex,
static_cast<uint16_t>(opcode), std::dec);
// Build and send movement packet
auto packet = MovementPacket::build(opcode, movementInfo);
socket->send(packet);
}
void GameHandler::setPosition(float x, float y, float z) {
movementInfo.x = x;
movementInfo.y = y;
movementInfo.z = z;
}
void GameHandler::setOrientation(float orientation) {
movementInfo.orientation = orientation;
}
void GameHandler::handleUpdateObject(network::Packet& packet) {
LOG_INFO("Handling SMSG_UPDATE_OBJECT");
UpdateObjectData data;
if (!UpdateObjectParser::parse(packet, data)) {
LOG_WARNING("Failed to parse SMSG_UPDATE_OBJECT");
return;
}
// Process out-of-range objects first
for (uint64_t guid : data.outOfRangeGuids) {
if (entityManager.hasEntity(guid)) {
LOG_INFO("Entity went out of range: 0x", std::hex, guid, std::dec);
entityManager.removeEntity(guid);
}
}
// Process update blocks
for (const auto& block : data.blocks) {
switch (block.updateType) {
case UpdateType::CREATE_OBJECT:
case UpdateType::CREATE_OBJECT2: {
// Create new entity
std::shared_ptr<Entity> entity;
switch (block.objectType) {
case ObjectType::PLAYER:
entity = std::make_shared<Player>(block.guid);
LOG_INFO("Created player entity: 0x", std::hex, block.guid, std::dec);
break;
case ObjectType::UNIT:
entity = std::make_shared<Unit>(block.guid);
LOG_INFO("Created unit entity: 0x", std::hex, block.guid, std::dec);
break;
case ObjectType::GAMEOBJECT:
entity = std::make_shared<GameObject>(block.guid);
LOG_INFO("Created gameobject entity: 0x", std::hex, block.guid, std::dec);
break;
default:
entity = std::make_shared<Entity>(block.guid);
entity->setType(block.objectType);
LOG_INFO("Created generic entity: 0x", std::hex, block.guid, std::dec,
", type=", static_cast<int>(block.objectType));
break;
}
// Set position from movement block
if (block.hasMovement) {
entity->setPosition(block.x, block.y, block.z, block.orientation);
LOG_DEBUG(" Position: (", block.x, ", ", block.y, ", ", block.z, ")");
}
// Set fields
for (const auto& field : block.fields) {
entity->setField(field.first, field.second);
}
// Add to manager
entityManager.addEntity(block.guid, entity);
break;
}
case UpdateType::VALUES: {
// Update existing entity fields
auto entity = entityManager.getEntity(block.guid);
if (entity) {
for (const auto& field : block.fields) {
entity->setField(field.first, field.second);
}
LOG_DEBUG("Updated entity fields: 0x", std::hex, block.guid, std::dec);
} else {
LOG_WARNING("VALUES update for unknown entity: 0x", std::hex, block.guid, std::dec);
}
break;
}
case UpdateType::MOVEMENT: {
// Update entity position
auto entity = entityManager.getEntity(block.guid);
if (entity) {
entity->setPosition(block.x, block.y, block.z, block.orientation);
LOG_DEBUG("Updated entity position: 0x", std::hex, block.guid, std::dec);
} else {
LOG_WARNING("MOVEMENT update for unknown entity: 0x", std::hex, block.guid, std::dec);
}
break;
}
default:
break;
}
}
tabCycleStale = true;
LOG_INFO("Entity count: ", entityManager.getEntityCount());
}
void GameHandler::handleDestroyObject(network::Packet& packet) {
LOG_INFO("Handling SMSG_DESTROY_OBJECT");
DestroyObjectData data;
if (!DestroyObjectParser::parse(packet, data)) {
LOG_WARNING("Failed to parse SMSG_DESTROY_OBJECT");
return;
}
// Remove entity
if (entityManager.hasEntity(data.guid)) {
entityManager.removeEntity(data.guid);
LOG_INFO("Destroyed entity: 0x", std::hex, data.guid, std::dec,
" (", (data.isDeath ? "death" : "despawn"), ")");
} else {
LOG_WARNING("Destroy object for unknown entity: 0x", std::hex, data.guid, std::dec);
}
tabCycleStale = true;
LOG_INFO("Entity count: ", entityManager.getEntityCount());
}
void GameHandler::sendChatMessage(ChatType type, const std::string& message, const std::string& target) {
if (state != WorldState::IN_WORLD) {
LOG_WARNING("Cannot send chat in state: ", (int)state);
return;
}
if (message.empty()) {
LOG_WARNING("Cannot send empty chat message");
return;
}
LOG_INFO("Sending chat message: [", getChatTypeString(type), "] ", message);
// Determine language based on character (for now, use COMMON)
ChatLanguage language = ChatLanguage::COMMON;
// Build and send packet
auto packet = MessageChatPacket::build(type, language, message, target);
socket->send(packet);
}
void GameHandler::handleMessageChat(network::Packet& packet) {
LOG_DEBUG("Handling SMSG_MESSAGECHAT");
MessageChatData data;
if (!MessageChatParser::parse(packet, data)) {
LOG_WARNING("Failed to parse SMSG_MESSAGECHAT");
return;
}
// Add to chat history
chatHistory.push_back(data);
// Limit chat history size
if (chatHistory.size() > maxChatHistory) {
chatHistory.erase(chatHistory.begin());
}
// Log the message
std::string senderInfo;
if (!data.senderName.empty()) {
senderInfo = data.senderName;
} else if (data.senderGuid != 0) {
// Try to find entity name
auto entity = entityManager.getEntity(data.senderGuid);
if (entity && entity->getType() == ObjectType::PLAYER) {
auto player = std::dynamic_pointer_cast<Player>(entity);
if (player && !player->getName().empty()) {
senderInfo = player->getName();
} else {
senderInfo = "Player-" + std::to_string(data.senderGuid);
}
} else {
senderInfo = "Unknown-" + std::to_string(data.senderGuid);
}
} else {
senderInfo = "System";
}
std::string channelInfo;
if (!data.channelName.empty()) {
channelInfo = "[" + data.channelName + "] ";
}
LOG_INFO("========================================");
LOG_INFO(" CHAT [", getChatTypeString(data.type), "]");
LOG_INFO("========================================");
LOG_INFO(channelInfo, senderInfo, ": ", data.message);
LOG_INFO("========================================");
}
void GameHandler::setTarget(uint64_t guid) {
if (guid == targetGuid) return;
targetGuid = guid;
if (guid != 0) {
LOG_INFO("Target set: 0x", std::hex, guid, std::dec);
}
}
void GameHandler::clearTarget() {
if (targetGuid != 0) {
LOG_INFO("Target cleared");
}
targetGuid = 0;
tabCycleIndex = -1;
tabCycleStale = true;
}
std::shared_ptr<Entity> GameHandler::getTarget() const {
if (targetGuid == 0) return nullptr;
return entityManager.getEntity(targetGuid);
}
void GameHandler::tabTarget(float playerX, float playerY, float playerZ) {
// Rebuild cycle list if stale
if (tabCycleStale) {
tabCycleList.clear();
tabCycleIndex = -1;
struct EntityDist {
uint64_t guid;
float distance;
};
std::vector<EntityDist> sortable;
for (const auto& [guid, entity] : entityManager.getEntities()) {
auto t = entity->getType();
if (t != ObjectType::UNIT && t != ObjectType::PLAYER) continue;
float dx = entity->getX() - playerX;
float dy = entity->getY() - playerY;
float dz = entity->getZ() - playerZ;
float dist = std::sqrt(dx*dx + dy*dy + dz*dz);
sortable.push_back({guid, dist});
}
std::sort(sortable.begin(), sortable.end(),
[](const EntityDist& a, const EntityDist& b) { return a.distance < b.distance; });
for (const auto& ed : sortable) {
tabCycleList.push_back(ed.guid);
}
tabCycleStale = false;
}
if (tabCycleList.empty()) {
clearTarget();
return;
}
tabCycleIndex = (tabCycleIndex + 1) % static_cast<int>(tabCycleList.size());
setTarget(tabCycleList[tabCycleIndex]);
}
void GameHandler::addLocalChatMessage(const MessageChatData& msg) {
chatHistory.push_back(msg);
if (chatHistory.size() > maxChatHistory) {
chatHistory.erase(chatHistory.begin());
}
}
std::vector<MessageChatData> GameHandler::getChatHistory(size_t maxMessages) const {
if (maxMessages == 0 || maxMessages >= chatHistory.size()) {
return chatHistory;
}
// Return last N messages
return std::vector<MessageChatData>(
chatHistory.end() - maxMessages,
chatHistory.end()
);
}
uint32_t GameHandler::generateClientSeed() {
// Generate cryptographically random seed
std::random_device rd;
std::mt19937 gen(rd());
std::uniform_int_distribution<uint32_t> dis(1, 0xFFFFFFFF);
return dis(gen);
}
void GameHandler::setState(WorldState newState) {
if (state != newState) {
LOG_DEBUG("World state: ", (int)state, " -> ", (int)newState);
state = newState;
}
}
void GameHandler::fail(const std::string& reason) {
LOG_ERROR("World connection failed: ", reason);
setState(WorldState::FAILED);
if (onFailure) {
onFailure(reason);
}
}
} // namespace game
} // namespace wowee

274
src/game/inventory.cpp Normal file
View file

@ -0,0 +1,274 @@
#include "game/inventory.hpp"
#include "core/logger.hpp"
namespace wowee {
namespace game {
static const ItemSlot EMPTY_SLOT{};
Inventory::Inventory() = default;
const ItemSlot& Inventory::getBackpackSlot(int index) const {
if (index < 0 || index >= BACKPACK_SLOTS) return EMPTY_SLOT;
return backpack[index];
}
bool Inventory::setBackpackSlot(int index, const ItemDef& item) {
if (index < 0 || index >= BACKPACK_SLOTS) return false;
backpack[index].item = item;
return true;
}
bool Inventory::clearBackpackSlot(int index) {
if (index < 0 || index >= BACKPACK_SLOTS) return false;
backpack[index].item = ItemDef{};
return true;
}
const ItemSlot& Inventory::getEquipSlot(EquipSlot slot) const {
int idx = static_cast<int>(slot);
if (idx < 0 || idx >= NUM_EQUIP_SLOTS) return EMPTY_SLOT;
return equipment[idx];
}
bool Inventory::setEquipSlot(EquipSlot slot, const ItemDef& item) {
int idx = static_cast<int>(slot);
if (idx < 0 || idx >= NUM_EQUIP_SLOTS) return false;
equipment[idx].item = item;
return true;
}
bool Inventory::clearEquipSlot(EquipSlot slot) {
int idx = static_cast<int>(slot);
if (idx < 0 || idx >= NUM_EQUIP_SLOTS) return false;
equipment[idx].item = ItemDef{};
return true;
}
int Inventory::getBagSize(int bagIndex) const {
if (bagIndex < 0 || bagIndex >= NUM_BAG_SLOTS) return 0;
return bags[bagIndex].size;
}
const ItemSlot& Inventory::getBagSlot(int bagIndex, int slotIndex) const {
if (bagIndex < 0 || bagIndex >= NUM_BAG_SLOTS) return EMPTY_SLOT;
if (slotIndex < 0 || slotIndex >= bags[bagIndex].size) return EMPTY_SLOT;
return bags[bagIndex].slots[slotIndex];
}
bool Inventory::setBagSlot(int bagIndex, int slotIndex, const ItemDef& item) {
if (bagIndex < 0 || bagIndex >= NUM_BAG_SLOTS) return false;
if (slotIndex < 0 || slotIndex >= bags[bagIndex].size) return false;
bags[bagIndex].slots[slotIndex].item = item;
return true;
}
int Inventory::findFreeBackpackSlot() const {
for (int i = 0; i < BACKPACK_SLOTS; i++) {
if (backpack[i].empty()) return i;
}
return -1;
}
bool Inventory::addItem(const ItemDef& item) {
// Try stacking first
if (item.maxStack > 1) {
for (int i = 0; i < BACKPACK_SLOTS; i++) {
if (!backpack[i].empty() &&
backpack[i].item.itemId == item.itemId &&
backpack[i].item.stackCount < backpack[i].item.maxStack) {
uint32_t space = backpack[i].item.maxStack - backpack[i].item.stackCount;
uint32_t toAdd = std::min(space, item.stackCount);
backpack[i].item.stackCount += toAdd;
if (toAdd >= item.stackCount) return true;
// Remaining needs a new slot — fall through
}
}
}
int slot = findFreeBackpackSlot();
if (slot < 0) return false;
backpack[slot].item = item;
return true;
}
void Inventory::populateTestItems() {
// Equipment
{
ItemDef sword;
sword.itemId = 25;
sword.name = "Worn Shortsword";
sword.quality = ItemQuality::COMMON;
sword.inventoryType = 21; // Main Hand
sword.strength = 1;
sword.displayInfoId = 1542; // Sword_1H_Short_A_02.m2
sword.subclassName = "Sword";
setEquipSlot(EquipSlot::MAIN_HAND, sword);
}
{
ItemDef shield;
shield.itemId = 2129;
shield.name = "Large Round Shield";
shield.quality = ItemQuality::COMMON;
shield.inventoryType = 14; // Off Hand (Shield)
shield.armor = 18;
shield.stamina = 1;
shield.displayInfoId = 18662; // Shield_Round_A_01.m2
shield.subclassName = "Shield";
setEquipSlot(EquipSlot::OFF_HAND, shield);
}
// Shirt/pants/boots in backpack (character model renders bare geosets)
{
ItemDef shirt;
shirt.itemId = 38;
shirt.name = "Recruit's Shirt";
shirt.quality = ItemQuality::COMMON;
shirt.inventoryType = 4; // Shirt
shirt.displayInfoId = 2163;
addItem(shirt);
}
{
ItemDef pants;
pants.itemId = 39;
pants.name = "Recruit's Pants";
pants.quality = ItemQuality::COMMON;
pants.inventoryType = 7; // Legs
pants.armor = 4;
pants.displayInfoId = 1883;
addItem(pants);
}
{
ItemDef boots;
boots.itemId = 40;
boots.name = "Recruit's Boots";
boots.quality = ItemQuality::COMMON;
boots.inventoryType = 8; // Feet
boots.armor = 3;
boots.displayInfoId = 2166;
addItem(boots);
}
// Backpack items
{
ItemDef potion;
potion.itemId = 118;
potion.name = "Minor Healing Potion";
potion.quality = ItemQuality::COMMON;
potion.stackCount = 3;
potion.maxStack = 5;
addItem(potion);
}
{
ItemDef hearthstone;
hearthstone.itemId = 6948;
hearthstone.name = "Hearthstone";
hearthstone.quality = ItemQuality::COMMON;
addItem(hearthstone);
}
{
ItemDef leather;
leather.itemId = 2318;
leather.name = "Light Leather";
leather.quality = ItemQuality::COMMON;
leather.stackCount = 5;
leather.maxStack = 20;
addItem(leather);
}
{
ItemDef cloth;
cloth.itemId = 2589;
cloth.name = "Linen Cloth";
cloth.quality = ItemQuality::COMMON;
cloth.stackCount = 8;
cloth.maxStack = 20;
addItem(cloth);
}
{
ItemDef quest;
quest.itemId = 50000;
quest.name = "Kobold Candle";
quest.quality = ItemQuality::COMMON;
quest.stackCount = 4;
quest.maxStack = 10;
addItem(quest);
}
{
ItemDef ring;
ring.itemId = 11287;
ring.name = "Verdant Ring";
ring.quality = ItemQuality::UNCOMMON;
ring.inventoryType = 11; // Ring
ring.stamina = 3;
ring.spirit = 2;
addItem(ring);
}
{
ItemDef cloak;
cloak.itemId = 2570;
cloak.name = "Linen Cloak";
cloak.quality = ItemQuality::UNCOMMON;
cloak.inventoryType = 16; // Back
cloak.armor = 10;
cloak.agility = 1;
cloak.displayInfoId = 15055;
addItem(cloak);
}
{
ItemDef rareAxe;
rareAxe.itemId = 15268;
rareAxe.name = "Stoneslayer";
rareAxe.quality = ItemQuality::RARE;
rareAxe.inventoryType = 17; // Two-Hand
rareAxe.strength = 8;
rareAxe.stamina = 7;
rareAxe.subclassName = "Axe";
rareAxe.displayInfoId = 782; // Axe_2H_Battle_B_01.m2
addItem(rareAxe);
}
LOG_INFO("Inventory: populated test items (2 equipped, 11 backpack)");
}
const char* getQualityName(ItemQuality quality) {
switch (quality) {
case ItemQuality::POOR: return "Poor";
case ItemQuality::COMMON: return "Common";
case ItemQuality::UNCOMMON: return "Uncommon";
case ItemQuality::RARE: return "Rare";
case ItemQuality::EPIC: return "Epic";
case ItemQuality::LEGENDARY: return "Legendary";
default: return "Unknown";
}
}
const char* getEquipSlotName(EquipSlot slot) {
switch (slot) {
case EquipSlot::HEAD: return "Head";
case EquipSlot::NECK: return "Neck";
case EquipSlot::SHOULDERS: return "Shoulders";
case EquipSlot::SHIRT: return "Shirt";
case EquipSlot::CHEST: return "Chest";
case EquipSlot::WAIST: return "Waist";
case EquipSlot::LEGS: return "Legs";
case EquipSlot::FEET: return "Feet";
case EquipSlot::WRISTS: return "Wrists";
case EquipSlot::HANDS: return "Hands";
case EquipSlot::RING1: return "Ring 1";
case EquipSlot::RING2: return "Ring 2";
case EquipSlot::TRINKET1: return "Trinket 1";
case EquipSlot::TRINKET2: return "Trinket 2";
case EquipSlot::BACK: return "Back";
case EquipSlot::MAIN_HAND: return "Main Hand";
case EquipSlot::OFF_HAND: return "Off Hand";
case EquipSlot::RANGED: return "Ranged";
case EquipSlot::TABARD: return "Tabard";
case EquipSlot::BAG1: return "Bag 1";
case EquipSlot::BAG2: return "Bag 2";
case EquipSlot::BAG3: return "Bag 3";
case EquipSlot::BAG4: return "Bag 4";
default: return "Unknown";
}
}
} // namespace game
} // namespace wowee

374
src/game/npc_manager.cpp Normal file
View file

@ -0,0 +1,374 @@
#include "game/npc_manager.hpp"
#include "game/entity.hpp"
#include "pipeline/asset_manager.hpp"
#include "pipeline/m2_loader.hpp"
#include "pipeline/dbc_loader.hpp"
#include "rendering/character_renderer.hpp"
#include "core/logger.hpp"
#include <random>
#include <cmath>
#include <algorithm>
#include <cctype>
namespace wowee {
namespace game {
static constexpr float ZEROPOINT = 32.0f * 533.33333f;
// Random emote animation IDs (humanoid only)
static const uint32_t EMOTE_ANIMS[] = { 60, 66, 67, 70 }; // Talk, Bow, Wave, Laugh
static constexpr int NUM_EMOTE_ANIMS = 4;
static float randomFloat(float lo, float hi) {
static std::mt19937 rng(std::random_device{}());
std::uniform_real_distribution<float> dist(lo, hi);
return dist(rng);
}
static std::string toLowerStr(const std::string& s) {
std::string out = s;
for (char& c : out) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
return out;
}
// Look up texture variants for a creature M2 using CreatureDisplayInfo.dbc
// Returns up to 3 texture variant names (for type 1, 2, 3 texture slots)
static std::vector<std::string> lookupTextureVariants(
pipeline::AssetManager* am, const std::string& m2Path) {
std::vector<std::string> variants;
auto modelDataDbc = am->loadDBC("CreatureModelData.dbc");
auto displayInfoDbc = am->loadDBC("CreatureDisplayInfo.dbc");
if (!modelDataDbc || !displayInfoDbc) return variants;
// CreatureModelData stores .mdx paths; convert our .m2 path for matching
std::string mdxPath = m2Path;
if (mdxPath.size() > 3) {
mdxPath = mdxPath.substr(0, mdxPath.size() - 3) + ".mdx";
}
std::string mdxLower = toLowerStr(mdxPath);
// Find model ID from CreatureModelData (col 0 = ID, col 2 = modelName)
uint32_t creatureModelId = 0;
for (uint32_t r = 0; r < modelDataDbc->getRecordCount(); r++) {
std::string dbcModel = modelDataDbc->getString(r, 2);
if (toLowerStr(dbcModel) == mdxLower) {
creatureModelId = modelDataDbc->getUInt32(r, 0);
LOG_INFO("NpcManager: DBC match for '", m2Path,
"' -> CreatureModelData ID ", creatureModelId);
break;
}
}
if (creatureModelId == 0) return variants;
// Find first CreatureDisplayInfo entry for this model
// Col 0=ID, 1=ModelID, 6=TextureVariation_1, 7=TextureVariation_2, 8=TextureVariation_3
for (uint32_t r = 0; r < displayInfoDbc->getRecordCount(); r++) {
if (displayInfoDbc->getUInt32(r, 1) == creatureModelId) {
std::string v1 = displayInfoDbc->getString(r, 6);
std::string v2 = displayInfoDbc->getString(r, 7);
std::string v3 = displayInfoDbc->getString(r, 8);
if (!v1.empty()) variants.push_back(v1);
if (!v2.empty()) variants.push_back(v2);
if (!v3.empty()) variants.push_back(v3);
LOG_INFO("NpcManager: DisplayInfo textures: '", v1, "', '", v2, "', '", v3, "'");
break;
}
}
return variants;
}
void NpcManager::loadCreatureModel(pipeline::AssetManager* am,
rendering::CharacterRenderer* cr,
const std::string& m2Path,
uint32_t modelId) {
auto m2Data = am->readFile(m2Path);
if (m2Data.empty()) {
LOG_WARNING("NpcManager: failed to read M2 file: ", m2Path);
return;
}
auto model = pipeline::M2Loader::load(m2Data);
// Derive skin path: replace .m2 with 00.skin
std::string skinPath = m2Path;
if (skinPath.size() > 3) {
skinPath = skinPath.substr(0, skinPath.size() - 3) + "00.skin";
}
auto skinData = am->readFile(skinPath);
if (!skinData.empty()) {
pipeline::M2Loader::loadSkin(skinData, model);
}
if (!model.isValid()) {
LOG_WARNING("NpcManager: invalid model: ", m2Path);
return;
}
// Load external .anim files for sequences without flag 0x20
std::string basePath = m2Path.substr(0, m2Path.size() - 3); // remove ".m2"
for (uint32_t si = 0; si < model.sequences.size(); si++) {
if (!(model.sequences[si].flags & 0x20)) {
char animFileName[256];
snprintf(animFileName, sizeof(animFileName),
"%s%04u-%02u.anim",
basePath.c_str(),
model.sequences[si].id,
model.sequences[si].variationIndex);
auto animFileData = am->readFile(animFileName);
if (!animFileData.empty()) {
pipeline::M2Loader::loadAnimFile(m2Data, animFileData, si, model);
}
}
}
// --- Resolve creature skin textures ---
// Extract model directory: "Creature\Wolf\" from "Creature\Wolf\Wolf.m2"
size_t lastSlash = m2Path.find_last_of("\\/");
std::string modelDir = (lastSlash != std::string::npos)
? m2Path.substr(0, lastSlash + 1) : "";
// Extract model base name: "Wolf" from "Creature\Wolf\Wolf.m2"
std::string modelFileName = (lastSlash != std::string::npos)
? m2Path.substr(lastSlash + 1) : m2Path;
std::string modelBaseName = modelFileName.substr(0, modelFileName.size() - 3); // remove ".m2"
// Log existing texture info
for (size_t ti = 0; ti < model.textures.size(); ti++) {
LOG_INFO("NpcManager: ", m2Path, " tex[", ti, "] type=",
model.textures[ti].type, " file='", model.textures[ti].filename, "'");
}
// Check if any textures need resolution
// Type 11 = creature skin 1, type 12 = creature skin 2, type 13 = creature skin 3
// Type 1 = character body skin (also possible on some creature models)
auto needsResolve = [](uint32_t t) {
return t == 11 || t == 12 || t == 13 || t == 1 || t == 2 || t == 3;
};
bool needsVariants = false;
for (const auto& tex : model.textures) {
if (needsResolve(tex.type) && tex.filename.empty()) {
needsVariants = true;
break;
}
}
if (needsVariants) {
// Try DBC-based lookup first
auto variants = lookupTextureVariants(am, m2Path);
// Fill in unresolved textures from DBC variants
// Creature skin types map: type 11 -> variant[0], type 12 -> variant[1], type 13 -> variant[2]
// Also type 1 -> variant[0] as fallback
for (auto& tex : model.textures) {
if (!needsResolve(tex.type) || !tex.filename.empty()) continue;
// Determine which variant index this texture type maps to
size_t varIdx = 0;
if (tex.type == 11 || tex.type == 1) varIdx = 0;
else if (tex.type == 12 || tex.type == 2) varIdx = 1;
else if (tex.type == 13 || tex.type == 3) varIdx = 2;
std::string resolved;
if (varIdx < variants.size() && !variants[varIdx].empty()) {
// DBC variant: <ModelDir>\<Variant>.blp
resolved = modelDir + variants[varIdx] + ".blp";
if (!am->fileExists(resolved)) {
LOG_WARNING("NpcManager: DBC texture not found: ", resolved);
resolved.clear();
}
}
// Fallback heuristics if DBC didn't provide a texture
if (resolved.empty()) {
// Try <ModelDir>\<ModelName>Skin.blp
std::string skinTry = modelDir + modelBaseName + "Skin.blp";
if (am->fileExists(skinTry)) {
resolved = skinTry;
} else {
// Try <ModelDir>\<ModelName>.blp
std::string altTry = modelDir + modelBaseName + ".blp";
if (am->fileExists(altTry)) {
resolved = altTry;
}
}
}
if (!resolved.empty()) {
tex.filename = resolved;
LOG_INFO("NpcManager: resolved type-", tex.type,
" texture -> '", resolved, "'");
} else {
LOG_WARNING("NpcManager: could not resolve type-", tex.type,
" texture for ", m2Path);
}
}
}
cr->loadModel(model, modelId);
LOG_INFO("NpcManager: loaded model id=", modelId, " path=", m2Path,
" verts=", model.vertices.size(), " bones=", model.bones.size(),
" anims=", model.sequences.size(), " textures=", model.textures.size());
}
void NpcManager::initialize(pipeline::AssetManager* am,
rendering::CharacterRenderer* cr,
EntityManager& em,
const glm::vec3& playerSpawnGL) {
if (!am || !am->isInitialized() || !cr) {
LOG_WARNING("NpcManager: cannot initialize — missing AssetManager or CharacterRenderer");
return;
}
// Define spawn table: NPC positions are offsets from player spawn in GL coords
struct SpawnEntry {
const char* name;
const char* m2Path;
uint32_t level;
uint32_t health;
float offsetX; // GL X offset from player
float offsetY; // GL Y offset from player
float rotation;
float scale;
bool isCritter;
};
static const SpawnEntry spawnTable[] = {
// Guards
{ "Stormwind Guard", "Creature\\HumanMaleGuard\\HumanMaleGuard.m2",
60, 42000, -15.0f, 10.0f, 0.0f, 1.0f, false },
{ "Stormwind Guard", "Creature\\HumanMaleGuard\\HumanMaleGuard.m2",
60, 42000, 20.0f, -5.0f, 2.3f, 1.0f, false },
{ "Stormwind Guard", "Creature\\HumanMaleGuard\\HumanMaleGuard.m2",
60, 42000, -25.0f, -15.0f, 1.0f, 1.0f, false },
// Citizens
{ "Stormwind Citizen", "Creature\\HumanMalePeasant\\HumanMalePeasant.m2",
5, 1200, 12.0f, 18.0f, 3.5f, 1.0f, false },
{ "Stormwind Citizen", "Creature\\HumanMalePeasant\\HumanMalePeasant.m2",
5, 1200, -8.0f, -22.0f, 5.0f, 1.0f, false },
{ "Stormwind Citizen", "Creature\\HumanFemalePeasant\\HumanFemalePeasant.m2",
5, 1200, 30.0f, 8.0f, 1.8f, 1.0f, false },
{ "Stormwind Citizen", "Creature\\HumanFemalePeasant\\HumanFemalePeasant.m2",
5, 1200, -18.0f, 25.0f, 4.2f, 1.0f, false },
// Critters
{ "Wolf", "Creature\\Wolf\\Wolf.m2",
1, 42, 35.0f, -20.0f, 0.7f, 1.0f, true },
{ "Wolf", "Creature\\Wolf\\Wolf.m2",
1, 42, 40.0f, -15.0f, 1.2f, 1.0f, true },
{ "Chicken", "Creature\\Chicken\\Chicken.m2",
1, 10, -10.0f, 30.0f, 2.0f, 1.0f, true },
{ "Chicken", "Creature\\Chicken\\Chicken.m2",
1, 10, -12.0f, 33.0f, 3.8f, 1.0f, true },
{ "Cat", "Creature\\Cat\\Cat.m2",
1, 42, 5.0f, -35.0f, 4.5f, 1.0f, true },
{ "Deer", "Creature\\Deer\\Deer.m2",
1, 42, -35.0f, -30.0f, 0.3f, 1.0f, true },
};
constexpr size_t spawnCount = sizeof(spawnTable) / sizeof(spawnTable[0]);
// Load each unique M2 model once
for (size_t i = 0; i < spawnCount; i++) {
const std::string path(spawnTable[i].m2Path);
if (loadedModels.find(path) == loadedModels.end()) {
uint32_t mid = nextModelId++;
loadCreatureModel(am, cr, path, mid);
loadedModels[path] = mid;
}
}
// Spawn each NPC instance
for (size_t i = 0; i < spawnCount; i++) {
const auto& s = spawnTable[i];
const std::string path(s.m2Path);
auto it = loadedModels.find(path);
if (it == loadedModels.end()) continue; // model failed to load
uint32_t modelId = it->second;
// GL position: offset from player spawn
glm::vec3 glPos = playerSpawnGL + glm::vec3(s.offsetX, s.offsetY, 0.0f);
// Create render instance
uint32_t instanceId = cr->createInstance(modelId, glPos,
glm::vec3(0.0f, 0.0f, s.rotation), s.scale);
if (instanceId == 0) {
LOG_WARNING("NpcManager: failed to create instance for ", s.name);
continue;
}
// Play idle animation (anim ID 0)
cr->playAnimation(instanceId, 0, true);
// Assign unique GUID
uint64_t guid = nextGuid++;
// Create entity in EntityManager
auto unit = std::make_shared<Unit>(guid);
unit->setName(s.name);
unit->setLevel(s.level);
unit->setHealth(s.health);
unit->setMaxHealth(s.health);
// Convert GL position back to WoW coordinates for targeting system
float wowX = ZEROPOINT - glPos.y;
float wowY = glPos.z;
float wowZ = ZEROPOINT - glPos.x;
unit->setPosition(wowX, wowY, wowZ, s.rotation);
em.addEntity(guid, unit);
// Track NPC instance
NpcInstance npc{};
npc.guid = guid;
npc.renderInstanceId = instanceId;
npc.emoteTimer = randomFloat(5.0f, 15.0f);
npc.emoteEndTimer = 0.0f;
npc.isEmoting = false;
npc.isCritter = s.isCritter;
npcs.push_back(npc);
LOG_INFO("NpcManager: spawned '", s.name, "' guid=0x", std::hex, guid, std::dec,
" at GL(", glPos.x, ",", glPos.y, ",", glPos.z, ")");
}
LOG_INFO("NpcManager: initialized ", npcs.size(), " NPCs with ",
loadedModels.size(), " unique models");
}
void NpcManager::update(float deltaTime, rendering::CharacterRenderer* cr) {
if (!cr) return;
for (auto& npc : npcs) {
// Critters just idle — no emotes
if (npc.isCritter) continue;
if (npc.isEmoting) {
npc.emoteEndTimer -= deltaTime;
if (npc.emoteEndTimer <= 0.0f) {
// Return to idle
cr->playAnimation(npc.renderInstanceId, 0, true);
npc.isEmoting = false;
npc.emoteTimer = randomFloat(5.0f, 15.0f);
}
} else {
npc.emoteTimer -= deltaTime;
if (npc.emoteTimer <= 0.0f) {
// Play random emote
int idx = static_cast<int>(randomFloat(0.0f, static_cast<float>(NUM_EMOTE_ANIMS) - 0.01f));
uint32_t emoteAnim = EMOTE_ANIMS[idx];
cr->playAnimation(npc.renderInstanceId, emoteAnim, false);
npc.isEmoting = true;
npc.emoteEndTimer = randomFloat(2.0f, 4.0f);
}
}
}
}
} // namespace game
} // namespace wowee

7
src/game/opcodes.cpp Normal file
View file

@ -0,0 +1,7 @@
#include "game/opcodes.hpp"
namespace wowee {
namespace game {
// Opcodes are defined in header
} // namespace game
} // namespace wowee

7
src/game/player.cpp Normal file
View file

@ -0,0 +1,7 @@
#include "game/player.hpp"
namespace wowee {
namespace game {
// All methods are inline in header
} // namespace game
} // namespace wowee

15
src/game/world.cpp Normal file
View file

@ -0,0 +1,15 @@
#include "game/world.hpp"
namespace wowee {
namespace game {
void World::update(float deltaTime) {
// TODO: Update world state
}
void World::loadMap(uint32_t mapId) {
// TODO: Load map data
}
} // namespace game
} // namespace wowee

865
src/game/world_packets.cpp Normal file
View file

@ -0,0 +1,865 @@
#include "game/world_packets.hpp"
#include "game/opcodes.hpp"
#include "game/character.hpp"
#include "auth/crypto.hpp"
#include "core/logger.hpp"
#include <algorithm>
#include <cctype>
#include <cstring>
namespace wowee {
namespace game {
network::Packet AuthSessionPacket::build(uint32_t build,
const std::string& accountName,
uint32_t clientSeed,
const std::vector<uint8_t>& sessionKey,
uint32_t serverSeed) {
if (sessionKey.size() != 40) {
LOG_ERROR("Invalid session key size: ", sessionKey.size(), " (expected 40)");
}
// Convert account name to uppercase
std::string upperAccount = accountName;
std::transform(upperAccount.begin(), upperAccount.end(),
upperAccount.begin(), ::toupper);
LOG_INFO("Building CMSG_AUTH_SESSION for account: ", upperAccount);
// Compute authentication hash
auto authHash = computeAuthHash(upperAccount, clientSeed, serverSeed, sessionKey);
LOG_DEBUG(" Build: ", build);
LOG_DEBUG(" Client seed: 0x", std::hex, clientSeed, std::dec);
LOG_DEBUG(" Server seed: 0x", std::hex, serverSeed, std::dec);
LOG_DEBUG(" Auth hash: ", authHash.size(), " bytes");
// Create packet (opcode will be added by WorldSocket)
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_AUTH_SESSION));
// Build number (uint32, little-endian)
packet.writeUInt32(build);
// Unknown uint32 (always 0)
packet.writeUInt32(0);
// Account name (null-terminated string)
packet.writeString(upperAccount);
// Unknown uint32 (always 0)
packet.writeUInt32(0);
// Client seed (uint32, little-endian)
packet.writeUInt32(clientSeed);
// Unknown fields (5x uint32, all zeros)
for (int i = 0; i < 5; ++i) {
packet.writeUInt32(0);
}
// Authentication hash (20 bytes)
packet.writeBytes(authHash.data(), authHash.size());
// Addon CRC (uint32, can be 0)
packet.writeUInt32(0);
LOG_INFO("CMSG_AUTH_SESSION packet built: ", packet.getSize(), " bytes");
return packet;
}
std::vector<uint8_t> AuthSessionPacket::computeAuthHash(
const std::string& accountName,
uint32_t clientSeed,
uint32_t serverSeed,
const std::vector<uint8_t>& sessionKey) {
// Build hash input:
// account_name + [0,0,0,0] + client_seed + server_seed + session_key
std::vector<uint8_t> hashInput;
hashInput.reserve(accountName.size() + 4 + 4 + 4 + sessionKey.size());
// Account name (as bytes)
hashInput.insert(hashInput.end(), accountName.begin(), accountName.end());
// 4 null bytes
for (int i = 0; i < 4; ++i) {
hashInput.push_back(0);
}
// Client seed (little-endian)
hashInput.push_back(clientSeed & 0xFF);
hashInput.push_back((clientSeed >> 8) & 0xFF);
hashInput.push_back((clientSeed >> 16) & 0xFF);
hashInput.push_back((clientSeed >> 24) & 0xFF);
// Server seed (little-endian)
hashInput.push_back(serverSeed & 0xFF);
hashInput.push_back((serverSeed >> 8) & 0xFF);
hashInput.push_back((serverSeed >> 16) & 0xFF);
hashInput.push_back((serverSeed >> 24) & 0xFF);
// Session key (40 bytes)
hashInput.insert(hashInput.end(), sessionKey.begin(), sessionKey.end());
LOG_DEBUG("Auth hash input: ", hashInput.size(), " bytes");
// Compute SHA1 hash
return auth::Crypto::sha1(hashInput);
}
bool AuthChallengeParser::parse(network::Packet& packet, AuthChallengeData& data) {
// SMSG_AUTH_CHALLENGE format (WoW 3.3.5a):
// uint32 unknown1 (always 1?)
// uint32 serverSeed
if (packet.getSize() < 8) {
LOG_ERROR("SMSG_AUTH_CHALLENGE packet too small: ", packet.getSize(), " bytes");
return false;
}
data.unknown1 = packet.readUInt32();
data.serverSeed = packet.readUInt32();
LOG_INFO("Parsed SMSG_AUTH_CHALLENGE:");
LOG_INFO(" Unknown1: 0x", std::hex, data.unknown1, std::dec);
LOG_INFO(" Server seed: 0x", std::hex, data.serverSeed, std::dec);
// Note: 3.3.5a has additional data after this (seed2, etc.)
// but we only need the first seed for authentication
return true;
}
bool AuthResponseParser::parse(network::Packet& packet, AuthResponseData& response) {
// SMSG_AUTH_RESPONSE format:
// uint8 result
if (packet.getSize() < 1) {
LOG_ERROR("SMSG_AUTH_RESPONSE packet too small: ", packet.getSize(), " bytes");
return false;
}
uint8_t resultCode = packet.readUInt8();
response.result = static_cast<AuthResult>(resultCode);
LOG_INFO("Parsed SMSG_AUTH_RESPONSE: ", getAuthResultString(response.result));
return true;
}
const char* getAuthResultString(AuthResult result) {
switch (result) {
case AuthResult::OK:
return "OK - Authentication successful";
case AuthResult::FAILED:
return "FAILED - Authentication failed";
case AuthResult::REJECT:
return "REJECT - Connection rejected";
case AuthResult::BAD_SERVER_PROOF:
return "BAD_SERVER_PROOF - Invalid server proof";
case AuthResult::UNAVAILABLE:
return "UNAVAILABLE - Server unavailable";
case AuthResult::SYSTEM_ERROR:
return "SYSTEM_ERROR - System error occurred";
case AuthResult::BILLING_ERROR:
return "BILLING_ERROR - Billing error";
case AuthResult::BILLING_EXPIRED:
return "BILLING_EXPIRED - Subscription expired";
case AuthResult::VERSION_MISMATCH:
return "VERSION_MISMATCH - Client version mismatch";
case AuthResult::UNKNOWN_ACCOUNT:
return "UNKNOWN_ACCOUNT - Account not found";
case AuthResult::INCORRECT_PASSWORD:
return "INCORRECT_PASSWORD - Wrong password";
case AuthResult::SESSION_EXPIRED:
return "SESSION_EXPIRED - Session has expired";
case AuthResult::SERVER_SHUTTING_DOWN:
return "SERVER_SHUTTING_DOWN - Server is shutting down";
case AuthResult::ALREADY_LOGGING_IN:
return "ALREADY_LOGGING_IN - Already logging in";
case AuthResult::LOGIN_SERVER_NOT_FOUND:
return "LOGIN_SERVER_NOT_FOUND - Can't contact login server";
case AuthResult::WAIT_QUEUE:
return "WAIT_QUEUE - Waiting in queue";
case AuthResult::BANNED:
return "BANNED - Account is banned";
case AuthResult::ALREADY_ONLINE:
return "ALREADY_ONLINE - Character already logged in";
case AuthResult::NO_TIME:
return "NO_TIME - No game time remaining";
case AuthResult::DB_BUSY:
return "DB_BUSY - Database is busy";
case AuthResult::SUSPENDED:
return "SUSPENDED - Account is suspended";
case AuthResult::PARENTAL_CONTROL:
return "PARENTAL_CONTROL - Parental controls active";
case AuthResult::LOCKED_ENFORCED:
return "LOCKED_ENFORCED - Account is locked";
default:
return "UNKNOWN - Unknown result code";
}
}
network::Packet CharEnumPacket::build() {
// CMSG_CHAR_ENUM has no body - just the opcode
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_CHAR_ENUM));
LOG_DEBUG("Built CMSG_CHAR_ENUM packet (no body)");
return packet;
}
bool CharEnumParser::parse(network::Packet& packet, CharEnumResponse& response) {
// Read character count
uint8_t count = packet.readUInt8();
LOG_INFO("Parsing SMSG_CHAR_ENUM: ", (int)count, " characters");
response.characters.clear();
response.characters.reserve(count);
for (uint8_t i = 0; i < count; ++i) {
Character character;
// Read GUID (8 bytes, little-endian)
character.guid = packet.readUInt64();
// Read name (null-terminated string)
character.name = packet.readString();
// Read race, class, gender
character.race = static_cast<Race>(packet.readUInt8());
character.characterClass = static_cast<Class>(packet.readUInt8());
character.gender = static_cast<Gender>(packet.readUInt8());
// Read appearance data
character.appearanceBytes = packet.readUInt32();
character.facialFeatures = packet.readUInt8();
// Read level
character.level = packet.readUInt8();
// Read location
character.zoneId = packet.readUInt32();
character.mapId = packet.readUInt32();
character.x = packet.readFloat();
character.y = packet.readFloat();
character.z = packet.readFloat();
// Read affiliations
character.guildId = packet.readUInt32();
// Read flags
character.flags = packet.readUInt32();
// Skip customization flag (uint32) and unknown byte
packet.readUInt32(); // Customization
packet.readUInt8(); // Unknown
// Read pet data (always present, even if no pet)
character.pet.displayModel = packet.readUInt32();
character.pet.level = packet.readUInt32();
character.pet.family = packet.readUInt32();
// Read equipment (23 items)
character.equipment.reserve(23);
for (int j = 0; j < 23; ++j) {
EquipmentItem item;
item.displayModel = packet.readUInt32();
item.inventoryType = packet.readUInt8();
item.enchantment = packet.readUInt32();
character.equipment.push_back(item);
}
LOG_INFO(" Character ", (int)(i + 1), ": ", character.name);
LOG_INFO(" GUID: 0x", std::hex, character.guid, std::dec);
LOG_INFO(" ", getRaceName(character.race), " ",
getClassName(character.characterClass), " (",
getGenderName(character.gender), ")");
LOG_INFO(" Level: ", (int)character.level);
LOG_INFO(" Location: Zone ", character.zoneId, ", Map ", character.mapId);
LOG_INFO(" Position: (", character.x, ", ", character.y, ", ", character.z, ")");
if (character.hasGuild()) {
LOG_INFO(" Guild ID: ", character.guildId);
}
if (character.hasPet()) {
LOG_INFO(" Pet: Model ", character.pet.displayModel,
", Level ", character.pet.level);
}
response.characters.push_back(character);
}
LOG_INFO("Successfully parsed ", response.characters.size(), " characters");
return true;
}
network::Packet PlayerLoginPacket::build(uint64_t characterGuid) {
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_PLAYER_LOGIN));
// Write character GUID (8 bytes, little-endian)
packet.writeUInt64(characterGuid);
LOG_INFO("Built CMSG_PLAYER_LOGIN packet");
LOG_INFO(" Character GUID: 0x", std::hex, characterGuid, std::dec);
return packet;
}
bool LoginVerifyWorldParser::parse(network::Packet& packet, LoginVerifyWorldData& data) {
// SMSG_LOGIN_VERIFY_WORLD format (WoW 3.3.5a):
// uint32 mapId
// float x, y, z (position)
// float orientation
if (packet.getSize() < 20) {
LOG_ERROR("SMSG_LOGIN_VERIFY_WORLD packet too small: ", packet.getSize(), " bytes");
return false;
}
data.mapId = packet.readUInt32();
data.x = packet.readFloat();
data.y = packet.readFloat();
data.z = packet.readFloat();
data.orientation = packet.readFloat();
LOG_INFO("Parsed SMSG_LOGIN_VERIFY_WORLD:");
LOG_INFO(" Map ID: ", data.mapId);
LOG_INFO(" Position: (", data.x, ", ", data.y, ", ", data.z, ")");
LOG_INFO(" Orientation: ", data.orientation, " radians");
return true;
}
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");
return false;
}
data.serverTime = packet.readUInt32();
data.unknown = packet.readUInt8();
LOG_DEBUG("Parsed SMSG_ACCOUNT_DATA_TIMES:");
LOG_DEBUG(" Server time: ", data.serverTime);
LOG_DEBUG(" Unknown: ", (int)data.unknown);
for (int i = 0; i < 8; ++i) {
data.accountDataTimes[i] = packet.readUInt32();
if (data.accountDataTimes[i] != 0) {
LOG_DEBUG(" Data slot ", i, ": ", data.accountDataTimes[i]);
}
}
return true;
}
bool MotdParser::parse(network::Packet& packet, MotdData& data) {
// SMSG_MOTD format (WoW 3.3.5a):
// uint32 lineCount
// string[lineCount] lines (null-terminated strings)
if (packet.getSize() < 4) {
LOG_ERROR("SMSG_MOTD packet too small: ", packet.getSize(), " bytes");
return false;
}
uint32_t lineCount = packet.readUInt32();
LOG_INFO("Parsed SMSG_MOTD:");
LOG_INFO(" Line count: ", lineCount);
data.lines.clear();
data.lines.reserve(lineCount);
for (uint32_t i = 0; i < lineCount; ++i) {
std::string line = packet.readString();
data.lines.push_back(line);
LOG_INFO(" [", i + 1, "] ", line);
}
return true;
}
network::Packet PingPacket::build(uint32_t sequence, uint32_t latency) {
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_PING));
// Write sequence number (uint32, little-endian)
packet.writeUInt32(sequence);
// Write latency (uint32, little-endian, in milliseconds)
packet.writeUInt32(latency);
LOG_DEBUG("Built CMSG_PING packet");
LOG_DEBUG(" Sequence: ", sequence);
LOG_DEBUG(" Latency: ", latency, " ms");
return packet;
}
bool PongParser::parse(network::Packet& packet, PongData& data) {
// SMSG_PONG format (WoW 3.3.5a):
// uint32 sequence (echoed from CMSG_PING)
if (packet.getSize() < 4) {
LOG_ERROR("SMSG_PONG packet too small: ", packet.getSize(), " bytes");
return false;
}
data.sequence = packet.readUInt32();
LOG_DEBUG("Parsed SMSG_PONG:");
LOG_DEBUG(" Sequence: ", data.sequence);
return true;
}
network::Packet MovementPacket::build(Opcode opcode, const MovementInfo& info) {
network::Packet packet(static_cast<uint16_t>(opcode));
// Movement packet format (WoW 3.3.5a):
// uint32 flags
// uint16 flags2
// uint32 time
// float x, y, z
// float orientation
// Write movement flags
packet.writeUInt32(info.flags);
packet.writeUInt16(info.flags2);
// Write timestamp
packet.writeUInt32(info.time);
// Write position
packet.writeBytes(reinterpret_cast<const uint8_t*>(&info.x), sizeof(float));
packet.writeBytes(reinterpret_cast<const uint8_t*>(&info.y), sizeof(float));
packet.writeBytes(reinterpret_cast<const uint8_t*>(&info.z), sizeof(float));
// Write orientation
packet.writeBytes(reinterpret_cast<const uint8_t*>(&info.orientation), sizeof(float));
// Write pitch if swimming/flying
if (info.hasFlag(MovementFlags::SWIMMING) || info.hasFlag(MovementFlags::FLYING)) {
packet.writeBytes(reinterpret_cast<const uint8_t*>(&info.pitch), sizeof(float));
}
// Write fall time if falling
if (info.hasFlag(MovementFlags::FALLING)) {
packet.writeUInt32(info.fallTime);
packet.writeBytes(reinterpret_cast<const uint8_t*>(&info.jumpVelocity), sizeof(float));
// Extended fall data if far falling
if (info.hasFlag(MovementFlags::FALLINGFAR)) {
packet.writeBytes(reinterpret_cast<const uint8_t*>(&info.jumpSinAngle), sizeof(float));
packet.writeBytes(reinterpret_cast<const uint8_t*>(&info.jumpCosAngle), sizeof(float));
packet.writeBytes(reinterpret_cast<const uint8_t*>(&info.jumpXYSpeed), sizeof(float));
}
}
LOG_DEBUG("Built movement packet: opcode=0x", std::hex, static_cast<uint16_t>(opcode), std::dec);
LOG_DEBUG(" Flags: 0x", std::hex, info.flags, std::dec);
LOG_DEBUG(" Position: (", info.x, ", ", info.y, ", ", info.z, ")");
LOG_DEBUG(" Orientation: ", info.orientation);
return packet;
}
uint64_t UpdateObjectParser::readPackedGuid(network::Packet& packet) {
// Read packed GUID format:
// First byte is a mask indicating which bytes are present
uint8_t mask = packet.readUInt8();
if (mask == 0) {
return 0;
}
uint64_t guid = 0;
for (int i = 0; i < 8; ++i) {
if (mask & (1 << i)) {
uint8_t byte = packet.readUInt8();
guid |= (static_cast<uint64_t>(byte) << (i * 8));
}
}
return guid;
}
bool UpdateObjectParser::parseMovementBlock(network::Packet& packet, UpdateBlock& block) {
// Skip movement flags and other movement data for now
// This is a simplified implementation
// Read movement flags (not used yet)
/*uint32_t flags =*/ packet.readUInt32();
/*uint16_t flags2 =*/ packet.readUInt16();
// Read timestamp (not used yet)
/*uint32_t time =*/ packet.readUInt32();
// Read position
block.x = packet.readFloat();
block.y = packet.readFloat();
block.z = packet.readFloat();
block.orientation = packet.readFloat();
block.hasMovement = true;
LOG_DEBUG(" Movement: (", block.x, ", ", block.y, ", ", block.z, "), orientation=", block.orientation);
// TODO: Parse additional movement fields based on flags
// For now, we'll skip them to keep this simple
return true;
}
bool UpdateObjectParser::parseUpdateFields(network::Packet& packet, UpdateBlock& block) {
// Read number of blocks (each block is 32 fields = 32 bits)
uint8_t blockCount = packet.readUInt8();
if (blockCount == 0) {
return true; // No fields to update
}
LOG_DEBUG(" Parsing ", (int)blockCount, " field blocks");
// Read update mask
std::vector<uint32_t> updateMask(blockCount);
for (int i = 0; i < blockCount; ++i) {
updateMask[i] = packet.readUInt32();
}
// Read field values for each bit set in mask
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;
uint32_t value = packet.readUInt32();
block.fields[fieldIndex] = value;
LOG_DEBUG(" Field[", fieldIndex, "] = 0x", std::hex, value, std::dec);
}
}
}
LOG_DEBUG(" Parsed ", block.fields.size(), " fields");
return true;
}
bool UpdateObjectParser::parseUpdateBlock(network::Packet& packet, UpdateBlock& block) {
// Read update type
uint8_t updateTypeVal = packet.readUInt8();
block.updateType = static_cast<UpdateType>(updateTypeVal);
LOG_DEBUG("Update block: type=", (int)updateTypeVal);
switch (block.updateType) {
case UpdateType::VALUES: {
// Partial update - changed fields only
block.guid = readPackedGuid(packet);
LOG_DEBUG(" VALUES update for GUID: 0x", std::hex, block.guid, std::dec);
return parseUpdateFields(packet, block);
}
case UpdateType::MOVEMENT: {
// Movement update
block.guid = readPackedGuid(packet);
LOG_DEBUG(" MOVEMENT update for GUID: 0x", std::hex, block.guid, std::dec);
return parseMovementBlock(packet, block);
}
case UpdateType::CREATE_OBJECT:
case UpdateType::CREATE_OBJECT2: {
// Create new object with full data
block.guid = readPackedGuid(packet);
LOG_DEBUG(" CREATE_OBJECT for GUID: 0x", std::hex, block.guid, std::dec);
// Read object type
uint8_t objectTypeVal = packet.readUInt8();
block.objectType = static_cast<ObjectType>(objectTypeVal);
LOG_DEBUG(" Object type: ", (int)objectTypeVal);
// Parse movement if present
bool hasMovement = parseMovementBlock(packet, block);
if (!hasMovement) {
return false;
}
// Parse update fields
return parseUpdateFields(packet, block);
}
case UpdateType::OUT_OF_RANGE_OBJECTS: {
// Objects leaving view range - handled differently
LOG_DEBUG(" OUT_OF_RANGE_OBJECTS (skipping in block parser)");
return true;
}
case UpdateType::NEAR_OBJECTS: {
// Objects entering view range - handled differently
LOG_DEBUG(" NEAR_OBJECTS (skipping in block parser)");
return true;
}
default:
LOG_WARNING("Unknown update type: ", (int)updateTypeVal);
return false;
}
}
bool UpdateObjectParser::parse(network::Packet& packet, UpdateObjectData& data) {
LOG_INFO("Parsing SMSG_UPDATE_OBJECT");
// Read block count
data.blockCount = packet.readUInt32();
LOG_INFO(" Block count: ", 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)) {
// Read out-of-range GUID count
uint32_t count = packet.readUInt32();
LOG_INFO(" Out-of-range objects: ", count);
for (uint32_t i = 0; i < count; ++i) {
uint64_t guid = readPackedGuid(packet);
data.outOfRangeGuids.push_back(guid);
LOG_DEBUG(" Out of range: 0x", std::hex, guid, std::dec);
}
// Done - packet may have more blocks after this
// Reset read position to after the first byte if needed
} else {
// Not out-of-range, rewind
packet.setReadPos(packet.getReadPos() - 1);
}
}
// Parse update blocks
data.blocks.reserve(data.blockCount);
for (uint32_t i = 0; i < data.blockCount; ++i) {
LOG_DEBUG("Parsing block ", i + 1, " / ", data.blockCount);
UpdateBlock block;
if (!parseUpdateBlock(packet, block)) {
LOG_ERROR("Failed to parse update block ", i + 1);
return false;
}
data.blocks.push_back(block);
}
LOG_INFO("Successfully parsed ", data.blocks.size(), " update blocks");
return true;
}
bool DestroyObjectParser::parse(network::Packet& packet, DestroyObjectData& data) {
// SMSG_DESTROY_OBJECT format:
// uint64 guid
// uint8 isDeath (0 = despawn, 1 = death)
if (packet.getSize() < 9) {
LOG_ERROR("SMSG_DESTROY_OBJECT packet too small: ", packet.getSize(), " bytes");
return false;
}
data.guid = packet.readUInt64();
data.isDeath = (packet.readUInt8() != 0);
LOG_INFO("Parsed SMSG_DESTROY_OBJECT:");
LOG_INFO(" GUID: 0x", std::hex, data.guid, std::dec);
LOG_INFO(" Is death: ", data.isDeath ? "yes" : "no");
return true;
}
network::Packet MessageChatPacket::build(ChatType type,
ChatLanguage language,
const std::string& message,
const std::string& target) {
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_MESSAGECHAT));
// Write chat type
packet.writeUInt32(static_cast<uint32_t>(type));
// Write language
packet.writeUInt32(static_cast<uint32_t>(language));
// Write target (for whispers) or channel name
if (type == ChatType::WHISPER) {
packet.writeString(target);
} else if (type == ChatType::CHANNEL) {
packet.writeString(target); // Channel name
}
// Write message
packet.writeString(message);
LOG_DEBUG("Built CMSG_MESSAGECHAT packet");
LOG_DEBUG(" Type: ", static_cast<int>(type));
LOG_DEBUG(" Language: ", static_cast<int>(language));
LOG_DEBUG(" Message: ", message);
return packet;
}
bool MessageChatParser::parse(network::Packet& packet, MessageChatData& data) {
// SMSG_MESSAGECHAT format (WoW 3.3.5a):
// uint8 type
// uint32 language
// uint64 senderGuid
// uint32 unknown (always 0)
// [type-specific data]
// uint32 messageLength
// string message
// uint8 chatTag
if (packet.getSize() < 15) {
LOG_ERROR("SMSG_MESSAGECHAT packet too small: ", packet.getSize(), " bytes");
return false;
}
// Read chat type
uint8_t typeVal = packet.readUInt8();
data.type = static_cast<ChatType>(typeVal);
// Read language
uint32_t langVal = packet.readUInt32();
data.language = static_cast<ChatLanguage>(langVal);
// Read sender GUID
data.senderGuid = packet.readUInt64();
// Read unknown field
packet.readUInt32();
// Type-specific data
switch (data.type) {
case ChatType::MONSTER_SAY:
case ChatType::MONSTER_YELL:
case ChatType::MONSTER_EMOTE: {
// Read sender name length + name
uint32_t nameLen = packet.readUInt32();
if (nameLen > 0 && nameLen < 256) {
std::vector<char> nameBuffer(nameLen);
for (uint32_t i = 0; i < nameLen; ++i) {
nameBuffer[i] = static_cast<char>(packet.readUInt8());
}
data.senderName = std::string(nameBuffer.begin(), nameBuffer.end());
}
// Read receiver GUID (usually 0 for monsters)
data.receiverGuid = packet.readUInt64();
break;
}
case ChatType::WHISPER_INFORM: {
// Read receiver name
data.receiverName = packet.readString();
break;
}
case ChatType::CHANNEL: {
// Read channel name
data.channelName = packet.readString();
break;
}
case ChatType::ACHIEVEMENT:
case ChatType::GUILD_ACHIEVEMENT: {
// Read achievement ID
packet.readUInt32();
break;
}
default:
// No additional data for most types
break;
}
// Read message length
uint32_t messageLen = packet.readUInt32();
// Read message
if (messageLen > 0 && messageLen < 8192) {
std::vector<char> msgBuffer(messageLen);
for (uint32_t i = 0; i < messageLen; ++i) {
msgBuffer[i] = static_cast<char>(packet.readUInt8());
}
data.message = std::string(msgBuffer.begin(), msgBuffer.end());
}
// Read chat tag
data.chatTag = packet.readUInt8();
LOG_DEBUG("Parsed SMSG_MESSAGECHAT:");
LOG_DEBUG(" Type: ", getChatTypeString(data.type));
LOG_DEBUG(" Language: ", static_cast<int>(data.language));
LOG_DEBUG(" Sender GUID: 0x", std::hex, data.senderGuid, std::dec);
if (!data.senderName.empty()) {
LOG_DEBUG(" Sender name: ", data.senderName);
}
if (!data.channelName.empty()) {
LOG_DEBUG(" Channel: ", data.channelName);
}
LOG_DEBUG(" Message: ", data.message);
LOG_DEBUG(" Chat tag: 0x", std::hex, (int)data.chatTag, std::dec);
return true;
}
const char* getChatTypeString(ChatType type) {
switch (type) {
case ChatType::SAY: return "SAY";
case ChatType::PARTY: return "PARTY";
case ChatType::RAID: return "RAID";
case ChatType::GUILD: return "GUILD";
case ChatType::OFFICER: return "OFFICER";
case ChatType::YELL: return "YELL";
case ChatType::WHISPER: return "WHISPER";
case ChatType::WHISPER_INFORM: return "WHISPER_INFORM";
case ChatType::EMOTE: return "EMOTE";
case ChatType::TEXT_EMOTE: return "TEXT_EMOTE";
case ChatType::SYSTEM: return "SYSTEM";
case ChatType::MONSTER_SAY: return "MONSTER_SAY";
case ChatType::MONSTER_YELL: return "MONSTER_YELL";
case ChatType::MONSTER_EMOTE: return "MONSTER_EMOTE";
case ChatType::CHANNEL: return "CHANNEL";
case ChatType::CHANNEL_JOIN: return "CHANNEL_JOIN";
case ChatType::CHANNEL_LEAVE: return "CHANNEL_LEAVE";
case ChatType::CHANNEL_LIST: return "CHANNEL_LIST";
case ChatType::CHANNEL_NOTICE: return "CHANNEL_NOTICE";
case ChatType::CHANNEL_NOTICE_USER: return "CHANNEL_NOTICE_USER";
case ChatType::AFK: return "AFK";
case ChatType::DND: return "DND";
case ChatType::IGNORED: return "IGNORED";
case ChatType::SKILL: return "SKILL";
case ChatType::LOOT: return "LOOT";
case ChatType::BATTLEGROUND: return "BATTLEGROUND";
case ChatType::BATTLEGROUND_LEADER: return "BATTLEGROUND_LEADER";
case ChatType::RAID_LEADER: return "RAID_LEADER";
case ChatType::RAID_WARNING: return "RAID_WARNING";
case ChatType::ACHIEVEMENT: return "ACHIEVEMENT";
case ChatType::GUILD_ACHIEVEMENT: return "GUILD_ACHIEVEMENT";
default: return "UNKNOWN";
}
}
} // namespace game
} // namespace wowee

116
src/game/zone_manager.cpp Normal file
View file

@ -0,0 +1,116 @@
#include "game/zone_manager.hpp"
#include "core/logger.hpp"
#include <cstdlib>
#include <ctime>
namespace wowee {
namespace game {
void ZoneManager::initialize() {
// Elwynn Forest (zone 12)
ZoneInfo elwynn;
elwynn.id = 12;
elwynn.name = "Elwynn Forest";
elwynn.musicPaths = {
"Sound\\Music\\ZoneMusic\\Forest\\DayForest01.mp3",
"Sound\\Music\\ZoneMusic\\Forest\\DayForest02.mp3",
"Sound\\Music\\ZoneMusic\\Forest\\DayForest03.mp3",
};
zones[12] = elwynn;
// Stormwind City (zone 1519)
ZoneInfo stormwind;
stormwind.id = 1519;
stormwind.name = "Stormwind City";
stormwind.musicPaths = {
"Sound\\Music\\CityMusic\\Stormwind\\stormwind04-zone.mp3",
"Sound\\Music\\CityMusic\\Stormwind\\stormwind05-zone.mp3",
"Sound\\Music\\CityMusic\\Stormwind\\stormwind06-zone.mp3",
"Sound\\Music\\CityMusic\\Stormwind\\stormwind07-zone.mp3",
"Sound\\Music\\CityMusic\\Stormwind\\stormwind08-zone.mp3",
"Sound\\Music\\CityMusic\\Stormwind\\stormwind09-zone.mp3",
"Sound\\Music\\CityMusic\\Stormwind\\stormwind10-zone.mp3",
};
zones[1519] = stormwind;
// Dun Morogh (zone 1) - neighboring zone
ZoneInfo dunmorogh;
dunmorogh.id = 1;
dunmorogh.name = "Dun Morogh";
dunmorogh.musicPaths = {
"Sound\\Music\\ZoneMusic\\Mountain\\DayMountain01.mp3",
"Sound\\Music\\ZoneMusic\\Mountain\\DayMountain02.mp3",
"Sound\\Music\\ZoneMusic\\Mountain\\DayMountain03.mp3",
};
zones[1] = dunmorogh;
// Westfall (zone 40)
ZoneInfo westfall;
westfall.id = 40;
westfall.name = "Westfall";
westfall.musicPaths = {
"Sound\\Music\\ZoneMusic\\Plains\\DayPlains01.mp3",
"Sound\\Music\\ZoneMusic\\Plains\\DayPlains02.mp3",
"Sound\\Music\\ZoneMusic\\Plains\\DayPlains03.mp3",
};
zones[40] = westfall;
// Tile-to-zone mappings for Azeroth (Eastern Kingdoms)
// Elwynn Forest tiles
for (int tx = 31; tx <= 34; tx++) {
for (int ty = 48; ty <= 51; ty++) {
tileToZone[tx * 100 + ty] = 12; // Elwynn
}
}
// Stormwind City tiles (northern part of Elwynn area)
tileToZone[31 * 100 + 47] = 1519;
tileToZone[32 * 100 + 47] = 1519;
tileToZone[33 * 100 + 47] = 1519;
// Westfall tiles (west of Elwynn)
for (int ty = 48; ty <= 51; ty++) {
tileToZone[35 * 100 + ty] = 40;
tileToZone[36 * 100 + ty] = 40;
}
// Dun Morogh tiles (south/east of Elwynn)
for (int tx = 31; tx <= 34; tx++) {
tileToZone[tx * 100 + 52] = 1;
tileToZone[tx * 100 + 53] = 1;
}
std::srand(static_cast<unsigned>(std::time(nullptr)));
LOG_INFO("Zone manager initialized: ", zones.size(), " zones, ", tileToZone.size(), " tile mappings");
}
uint32_t ZoneManager::getZoneId(int tileX, int tileY) const {
int key = tileX * 100 + tileY;
auto it = tileToZone.find(key);
if (it != tileToZone.end()) {
return it->second;
}
return 0; // Unknown zone
}
const ZoneInfo* ZoneManager::getZoneInfo(uint32_t zoneId) const {
auto it = zones.find(zoneId);
if (it != zones.end()) {
return &it->second;
}
return nullptr;
}
std::string ZoneManager::getRandomMusic(uint32_t zoneId) const {
auto it = zones.find(zoneId);
if (it == zones.end() || it->second.musicPaths.empty()) {
return "";
}
const auto& paths = it->second.musicPaths;
return paths[std::rand() % paths.size()];
}
} // namespace game
} // namespace wowee

32
src/main.cpp Normal file
View file

@ -0,0 +1,32 @@
#include "core/application.hpp"
#include "core/logger.hpp"
#include <exception>
int main(int argc, char* argv[]) {
try {
wowee::core::Logger::getInstance().setLogLevel(wowee::core::LogLevel::DEBUG);
LOG_INFO("=== Wowser Native Client ===");
LOG_INFO("Starting application...");
wowee::core::Application app;
if (!app.initialize()) {
LOG_FATAL("Failed to initialize application");
return 1;
}
app.run();
app.shutdown();
LOG_INFO("Application exited successfully");
return 0;
}
catch (const std::exception& e) {
LOG_FATAL("Unhandled exception: ", e.what());
return 1;
}
catch (...) {
LOG_FATAL("Unknown exception occurred");
return 1;
}
}

90
src/network/packet.cpp Normal file
View file

@ -0,0 +1,90 @@
#include "network/packet.hpp"
#include <cstring>
namespace wowee {
namespace network {
Packet::Packet(uint16_t opcode) : opcode(opcode) {}
Packet::Packet(uint16_t opcode, const std::vector<uint8_t>& data)
: opcode(opcode), data(data), readPos(0) {}
void Packet::writeUInt8(uint8_t value) {
data.push_back(value);
}
void Packet::writeUInt16(uint16_t value) {
data.push_back(value & 0xFF);
data.push_back((value >> 8) & 0xFF);
}
void Packet::writeUInt32(uint32_t value) {
data.push_back(value & 0xFF);
data.push_back((value >> 8) & 0xFF);
data.push_back((value >> 16) & 0xFF);
data.push_back((value >> 24) & 0xFF);
}
void Packet::writeUInt64(uint64_t value) {
writeUInt32(value & 0xFFFFFFFF);
writeUInt32((value >> 32) & 0xFFFFFFFF);
}
void Packet::writeString(const std::string& value) {
for (char c : value) {
data.push_back(static_cast<uint8_t>(c));
}
data.push_back(0); // Null terminator
}
void Packet::writeBytes(const uint8_t* bytes, size_t length) {
data.insert(data.end(), bytes, bytes + length);
}
uint8_t Packet::readUInt8() {
if (readPos >= data.size()) return 0;
return data[readPos++];
}
uint16_t Packet::readUInt16() {
uint16_t value = 0;
value |= readUInt8();
value |= (readUInt8() << 8);
return value;
}
uint32_t Packet::readUInt32() {
uint32_t value = 0;
value |= readUInt8();
value |= (readUInt8() << 8);
value |= (readUInt8() << 16);
value |= (readUInt8() << 24);
return value;
}
uint64_t Packet::readUInt64() {
uint64_t value = readUInt32();
value |= (static_cast<uint64_t>(readUInt32()) << 32);
return value;
}
float Packet::readFloat() {
// Read as uint32 and reinterpret as float
uint32_t bits = readUInt32();
float value;
std::memcpy(&value, &bits, sizeof(float));
return value;
}
std::string Packet::readString() {
std::string result;
while (readPos < data.size()) {
uint8_t c = data[readPos++];
if (c == 0) break;
result += static_cast<char>(c);
}
return result;
}
} // namespace network
} // namespace wowee

9
src/network/socket.cpp Normal file
View file

@ -0,0 +1,9 @@
#include "network/socket.hpp"
namespace wowee {
namespace network {
// Base class implementation (empty - pure virtual methods in derived classes)
} // namespace network
} // namespace wowee

227
src/network/tcp_socket.cpp Normal file
View file

@ -0,0 +1,227 @@
#include "network/tcp_socket.hpp"
#include "network/packet.hpp"
#include "core/logger.hpp"
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <fcntl.h>
#include <netdb.h>
#include <cstring>
namespace wowee {
namespace network {
TCPSocket::TCPSocket() = default;
TCPSocket::~TCPSocket() {
disconnect();
}
bool TCPSocket::connect(const std::string& host, uint16_t port) {
LOG_INFO("Connecting to ", host, ":", port);
// Create socket
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0) {
LOG_ERROR("Failed to create socket");
return false;
}
// Set non-blocking
int flags = fcntl(sockfd, F_GETFL, 0);
fcntl(sockfd, F_SETFL, flags | O_NONBLOCK);
// Resolve host
struct hostent* server = gethostbyname(host.c_str());
if (server == nullptr) {
LOG_ERROR("Failed to resolve host: ", host);
close(sockfd);
sockfd = -1;
return false;
}
// Connect
struct sockaddr_in serverAddr;
memset(&serverAddr, 0, sizeof(serverAddr));
serverAddr.sin_family = AF_INET;
memcpy(&serverAddr.sin_addr.s_addr, server->h_addr, server->h_length);
serverAddr.sin_port = htons(port);
int result = ::connect(sockfd, (struct sockaddr*)&serverAddr, sizeof(serverAddr));
if (result < 0 && errno != EINPROGRESS) {
LOG_ERROR("Failed to connect: ", strerror(errno));
close(sockfd);
sockfd = -1;
return false;
}
connected = true;
LOG_INFO("Connected to ", host, ":", port);
return true;
}
void TCPSocket::disconnect() {
if (sockfd >= 0) {
close(sockfd);
sockfd = -1;
}
connected = false;
receiveBuffer.clear();
}
void TCPSocket::send(const Packet& packet) {
if (!connected) return;
// Build complete packet with opcode
std::vector<uint8_t> sendData;
// Add opcode (1 byte) - always little-endian, but it's just 1 byte so doesn't matter
sendData.push_back(static_cast<uint8_t>(packet.getOpcode() & 0xFF));
// Add packet data
const auto& data = packet.getData();
sendData.insert(sendData.end(), data.begin(), data.end());
LOG_DEBUG("Sending packet: opcode=0x", std::hex, packet.getOpcode(), std::dec,
" size=", sendData.size(), " bytes");
// Send complete packet
ssize_t sent = ::send(sockfd, sendData.data(), sendData.size(), 0);
if (sent < 0) {
LOG_ERROR("Send failed: ", strerror(errno));
} else if (static_cast<size_t>(sent) != sendData.size()) {
LOG_WARNING("Partial send: ", sent, " of ", sendData.size(), " bytes");
}
}
void TCPSocket::update() {
if (!connected) return;
// Receive data into buffer
uint8_t buffer[4096];
ssize_t received = recv(sockfd, buffer, sizeof(buffer), 0);
if (received > 0) {
LOG_DEBUG("Received ", received, " bytes from server");
receiveBuffer.insert(receiveBuffer.end(), buffer, buffer + received);
// Try to parse complete packets from buffer
tryParsePackets();
}
else if (received == 0) {
LOG_INFO("Connection closed by server");
disconnect();
}
else if (errno != EAGAIN && errno != EWOULDBLOCK) {
LOG_ERROR("Receive failed: ", strerror(errno));
disconnect();
}
}
void TCPSocket::tryParsePackets() {
// For auth packets, we need at least 1 byte (opcode)
while (receiveBuffer.size() >= 1) {
uint8_t opcode = receiveBuffer[0];
// Determine expected packet size based on opcode
// This is specific to authentication protocol
size_t expectedSize = getExpectedPacketSize(opcode);
if (expectedSize == 0) {
// Unknown opcode or need more data to determine size
LOG_WARNING("Unknown opcode or indeterminate size: 0x", std::hex, (int)opcode, std::dec);
break;
}
if (receiveBuffer.size() < expectedSize) {
// Not enough data yet
LOG_DEBUG("Waiting for more data: have ", receiveBuffer.size(),
" bytes, need ", expectedSize);
break;
}
// We have a complete packet!
LOG_DEBUG("Parsing packet: opcode=0x", std::hex, (int)opcode, std::dec,
" size=", expectedSize, " bytes");
// Create packet from buffer data
std::vector<uint8_t> packetData(receiveBuffer.begin(),
receiveBuffer.begin() + expectedSize);
Packet packet(opcode, packetData);
// Remove parsed data from buffer
receiveBuffer.erase(receiveBuffer.begin(), receiveBuffer.begin() + expectedSize);
// Call callback if set
if (packetCallback) {
packetCallback(packet);
}
}
}
size_t TCPSocket::getExpectedPacketSize(uint8_t opcode) {
// Authentication packet sizes (WoW 3.3.5a)
// Note: These are minimum sizes. Some packets are variable length.
switch (opcode) {
case 0x00: // LOGON_CHALLENGE response
// Need to read second byte to determine success/failure
if (receiveBuffer.size() >= 3) {
uint8_t status = receiveBuffer[2];
if (status == 0x00) {
// Success - need to calculate full size
// Minimum: opcode(1) + unknown(1) + status(1) + B(32) + glen(1) + g(1) + Nlen(1) + N(32) + salt(32) + unk(16) + flags(1)
// With typical values: 1 + 1 + 1 + 32 + 1 + 1 + 1 + 32 + 32 + 16 + 1 = 119 bytes minimum
// But N is usually 256 bytes, so more like: 1 + 1 + 1 + 32 + 1 + 1 + 1 + 256 + 32 + 16 + 1 = 343 bytes
// For safety, let's parse dynamically:
if (receiveBuffer.size() >= 36) { // enough to read g_len
uint8_t gLen = receiveBuffer[35];
size_t minSize = 36 + gLen + 1; // up to N_len
if (receiveBuffer.size() >= minSize) {
uint8_t nLen = receiveBuffer[36 + gLen];
size_t totalSize = 36 + gLen + 1 + nLen + 32 + 16 + 1;
return totalSize;
}
}
return 0; // Need more data
} else {
// Failure - just opcode + unknown + status
return 3;
}
}
return 0; // Need more data to determine
case 0x01: // LOGON_PROOF response
// opcode(1) + status(1) + M2(20) = 22 bytes on success
// opcode(1) + status(1) = 2 bytes on failure
if (receiveBuffer.size() >= 2) {
uint8_t status = receiveBuffer[1];
if (status == 0x00) {
return 22; // Success
} else {
return 2; // Failure
}
}
return 0; // Need more data
case 0x10: // REALM_LIST response
// Variable length - format: opcode(1) + size(2) + payload(size)
// Need to read size field (little-endian uint16 at offset 1-2)
if (receiveBuffer.size() >= 3) {
uint16_t size = receiveBuffer[1] | (receiveBuffer[2] << 8);
// Total packet size is: opcode(1) + size field(2) + payload(size)
return 1 + 2 + size;
}
return 0; // Need more data to read size field
default:
LOG_WARNING("Unknown auth packet opcode: 0x", std::hex, (int)opcode, std::dec);
return 0;
}
}
} // namespace network
} // namespace wowee

View file

@ -0,0 +1,236 @@
#include "network/world_socket.hpp"
#include "network/packet.hpp"
#include "auth/crypto.hpp"
#include "core/logger.hpp"
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <fcntl.h>
#include <netdb.h>
#include <cstring>
namespace wowee {
namespace network {
// WoW 3.3.5a RC4 encryption keys (hardcoded in client)
static const uint8_t ENCRYPT_KEY[] = {
0xC2, 0xB3, 0x72, 0x3C, 0xC6, 0xAE, 0xD9, 0xB5,
0x34, 0x3C, 0x53, 0xEE, 0x2F, 0x43, 0x67, 0xCE
};
static const uint8_t DECRYPT_KEY[] = {
0xCC, 0x98, 0xAE, 0x04, 0xE8, 0x97, 0xEA, 0xCA,
0x12, 0xDD, 0xC0, 0x93, 0x42, 0x91, 0x53, 0x57
};
WorldSocket::WorldSocket() = default;
WorldSocket::~WorldSocket() {
disconnect();
}
bool WorldSocket::connect(const std::string& host, uint16_t port) {
LOG_INFO("Connecting to world server: ", host, ":", port);
// Create socket
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0) {
LOG_ERROR("Failed to create socket");
return false;
}
// Set non-blocking
int flags = fcntl(sockfd, F_GETFL, 0);
fcntl(sockfd, F_SETFL, flags | O_NONBLOCK);
// Resolve host
struct hostent* server = gethostbyname(host.c_str());
if (server == nullptr) {
LOG_ERROR("Failed to resolve host: ", host);
close(sockfd);
sockfd = -1;
return false;
}
// Connect
struct sockaddr_in serverAddr;
memset(&serverAddr, 0, sizeof(serverAddr));
serverAddr.sin_family = AF_INET;
memcpy(&serverAddr.sin_addr.s_addr, server->h_addr, server->h_length);
serverAddr.sin_port = htons(port);
int result = ::connect(sockfd, (struct sockaddr*)&serverAddr, sizeof(serverAddr));
if (result < 0 && errno != EINPROGRESS) {
LOG_ERROR("Failed to connect: ", strerror(errno));
close(sockfd);
sockfd = -1;
return false;
}
connected = true;
LOG_INFO("Connected to world server: ", host, ":", port);
return true;
}
void WorldSocket::disconnect() {
if (sockfd >= 0) {
close(sockfd);
sockfd = -1;
}
connected = false;
encryptionEnabled = false;
receiveBuffer.clear();
LOG_INFO("Disconnected from world server");
}
bool WorldSocket::isConnected() const {
return connected;
}
void WorldSocket::send(const Packet& packet) {
if (!connected) return;
const auto& data = packet.getData();
uint16_t opcode = packet.getOpcode();
uint16_t size = static_cast<uint16_t>(data.size());
// Build header (6 bytes for outgoing): size(2) + opcode(4)
std::vector<uint8_t> sendData;
sendData.reserve(6 + size);
// Size (2 bytes, big-endian) - payload size only, does NOT include header
sendData.push_back((size >> 8) & 0xFF);
sendData.push_back(size & 0xFF);
// Opcode (4 bytes, big-endian)
sendData.push_back((opcode >> 24) & 0xFF);
sendData.push_back((opcode >> 16) & 0xFF);
sendData.push_back((opcode >> 8) & 0xFF);
sendData.push_back(opcode & 0xFF);
// Encrypt header if encryption is enabled
if (encryptionEnabled) {
encryptCipher.process(sendData.data(), 6);
LOG_DEBUG("Encrypted outgoing header: opcode=0x", std::hex, opcode, std::dec);
}
// Add payload (unencrypted)
sendData.insert(sendData.end(), data.begin(), data.end());
LOG_DEBUG("Sending world packet: opcode=0x", std::hex, opcode, std::dec,
" size=", size, " bytes (", sendData.size(), " total)");
// Send complete packet
ssize_t sent = ::send(sockfd, sendData.data(), sendData.size(), 0);
if (sent < 0) {
LOG_ERROR("Send failed: ", strerror(errno));
} else if (static_cast<size_t>(sent) != sendData.size()) {
LOG_WARNING("Partial send: ", sent, " of ", sendData.size(), " bytes");
}
}
void WorldSocket::update() {
if (!connected) return;
// Receive data into buffer
uint8_t buffer[4096];
ssize_t received = recv(sockfd, buffer, sizeof(buffer), 0);
if (received > 0) {
LOG_DEBUG("Received ", received, " bytes from world server");
receiveBuffer.insert(receiveBuffer.end(), buffer, buffer + received);
// Try to parse complete packets from buffer
tryParsePackets();
}
else if (received == 0) {
LOG_INFO("World server connection closed");
disconnect();
}
else if (errno != EAGAIN && errno != EWOULDBLOCK) {
LOG_ERROR("Receive failed: ", strerror(errno));
disconnect();
}
}
void WorldSocket::tryParsePackets() {
// World server packets have 4-byte incoming header: size(2) + opcode(2)
while (receiveBuffer.size() >= 4) {
// Copy header for decryption
uint8_t header[4];
memcpy(header, receiveBuffer.data(), 4);
// Decrypt header if encryption is enabled
if (encryptionEnabled) {
decryptCipher.process(header, 4);
}
// Parse header (big-endian)
uint16_t size = (header[0] << 8) | header[1];
uint16_t opcode = (header[2] << 8) | header[3];
// Total packet size: header(4) + payload(size)
size_t totalSize = 4 + size;
if (receiveBuffer.size() < totalSize) {
// Not enough data yet
LOG_DEBUG("Waiting for more data: have ", receiveBuffer.size(),
" bytes, need ", totalSize);
break;
}
// We have a complete packet!
LOG_DEBUG("Parsing world packet: opcode=0x", std::hex, opcode, std::dec,
" size=", size, " bytes");
// 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
receiveBuffer.erase(receiveBuffer.begin(), receiveBuffer.begin() + totalSize);
// Call callback if set
if (packetCallback) {
packetCallback(packet);
}
}
}
void WorldSocket::initEncryption(const std::vector<uint8_t>& sessionKey) {
if (sessionKey.size() != 40) {
LOG_ERROR("Invalid session key size: ", sessionKey.size(), " (expected 40)");
return;
}
LOG_INFO("Initializing world server header encryption");
// Convert hardcoded keys to vectors
std::vector<uint8_t> encryptKey(ENCRYPT_KEY, ENCRYPT_KEY + 16);
std::vector<uint8_t> decryptKey(DECRYPT_KEY, DECRYPT_KEY + 16);
// Compute HMAC-SHA1(key, sessionKey) for each cipher
std::vector<uint8_t> encryptHash = auth::Crypto::hmacSHA1(encryptKey, sessionKey);
std::vector<uint8_t> decryptHash = auth::Crypto::hmacSHA1(decryptKey, sessionKey);
LOG_DEBUG("Encrypt hash: ", encryptHash.size(), " bytes");
LOG_DEBUG("Decrypt hash: ", decryptHash.size(), " bytes");
// Initialize RC4 ciphers with HMAC results
encryptCipher.init(encryptHash);
decryptCipher.init(decryptHash);
// Drop first 1024 bytes of keystream (WoW protocol requirement)
encryptCipher.drop(1024);
decryptCipher.drop(1024);
encryptionEnabled = true;
LOG_INFO("World server encryption initialized successfully");
}
} // namespace network
} // namespace wowee

564
src/pipeline/adt_loader.cpp Normal file
View file

@ -0,0 +1,564 @@
#include "pipeline/adt_loader.hpp"
#include "core/logger.hpp"
#include <cstring>
#include <cmath>
namespace wowee {
namespace pipeline {
// HeightMap implementation
float HeightMap::getHeight(int x, int y) const {
if (x < 0 || x > 8 || y < 0 || y > 8) {
return 0.0f;
}
// WoW uses 9x9 outer + 8x8 inner vertex layout
// Outer vertices: 0-80 (9x9 grid)
// Inner vertices: 81-144 (8x8 grid between outer vertices)
// Calculate index based on vertex type
int index;
if (x < 9 && y < 9) {
// Outer vertex
index = y * 9 + x;
} else {
// Inner vertex (between outer vertices)
int innerX = x - 1;
int innerY = y - 1;
if (innerX >= 0 && innerX < 8 && innerY >= 0 && innerY < 8) {
index = 81 + innerY * 8 + innerX;
} else {
return 0.0f;
}
}
return heights[index];
}
// ADTLoader implementation
ADTTerrain ADTLoader::load(const std::vector<uint8_t>& adtData) {
ADTTerrain terrain;
if (adtData.empty()) {
LOG_ERROR("Empty ADT data");
return terrain;
}
LOG_INFO("Loading ADT terrain (", adtData.size(), " bytes)");
size_t offset = 0;
int chunkIndex = 0;
// Parse chunks
int totalChunks = 0;
while (offset < adtData.size()) {
ChunkHeader header;
if (!readChunkHeader(adtData.data(), offset, adtData.size(), header)) {
break;
}
const uint8_t* chunkData = adtData.data() + offset + 8;
size_t chunkSize = header.size;
totalChunks++;
if (totalChunks <= 5) {
// Log first few chunks for debugging
char magic[5] = {0};
std::memcpy(magic, &header.magic, 4);
LOG_INFO("Chunk #", totalChunks, ": magic=", magic,
" (0x", std::hex, header.magic, std::dec, "), size=", chunkSize);
}
// Parse based on chunk type
if (header.magic == MVER) {
parseMVER(chunkData, chunkSize, terrain);
}
else if (header.magic == MTEX) {
parseMTEX(chunkData, chunkSize, terrain);
}
else if (header.magic == MMDX) {
parseMMDX(chunkData, chunkSize, terrain);
}
else if (header.magic == MWMO) {
parseMWMO(chunkData, chunkSize, terrain);
}
else if (header.magic == MDDF) {
parseMDDF(chunkData, chunkSize, terrain);
}
else if (header.magic == MODF) {
parseMODF(chunkData, chunkSize, terrain);
}
else if (header.magic == MH2O) {
LOG_INFO("Found MH2O chunk (", chunkSize, " bytes)");
parseMH2O(chunkData, chunkSize, terrain);
}
else if (header.magic == MCNK) {
parseMCNK(chunkData, chunkSize, chunkIndex++, terrain);
}
// Move to next chunk
offset += 8 + chunkSize;
}
terrain.loaded = true;
LOG_INFO("ADT loaded: ", chunkIndex, " map chunks, ",
terrain.textures.size(), " textures, ",
terrain.doodadNames.size(), " doodads, ",
terrain.wmoNames.size(), " WMOs");
return terrain;
}
bool ADTLoader::readChunkHeader(const uint8_t* data, size_t offset, size_t dataSize, ChunkHeader& header) {
if (offset + 8 > dataSize) {
return false;
}
header.magic = readUInt32(data, offset);
header.size = readUInt32(data, offset + 4);
// Validate chunk size
if (offset + 8 + header.size > dataSize) {
LOG_WARNING("Chunk extends beyond file: magic=0x", std::hex, header.magic,
", size=", std::dec, header.size);
return false;
}
return true;
}
uint32_t ADTLoader::readUInt32(const uint8_t* data, size_t offset) {
uint32_t value;
std::memcpy(&value, data + offset, sizeof(uint32_t));
return value;
}
float ADTLoader::readFloat(const uint8_t* data, size_t offset) {
float value;
std::memcpy(&value, data + offset, sizeof(float));
return value;
}
uint16_t ADTLoader::readUInt16(const uint8_t* data, size_t offset) {
uint16_t value;
std::memcpy(&value, data + offset, sizeof(uint16_t));
return value;
}
void ADTLoader::parseMVER(const uint8_t* data, size_t size, ADTTerrain& terrain) {
if (size < 4) {
LOG_WARNING("MVER chunk too small");
return;
}
terrain.version = readUInt32(data, 0);
LOG_DEBUG("ADT version: ", terrain.version);
}
void ADTLoader::parseMTEX(const uint8_t* data, size_t size, ADTTerrain& terrain) {
// MTEX contains null-terminated texture filenames
size_t offset = 0;
while (offset < size) {
const char* textureName = reinterpret_cast<const char*>(data + offset);
size_t nameLen = std::strlen(textureName);
if (nameLen == 0) {
break;
}
terrain.textures.push_back(std::string(textureName, nameLen));
offset += nameLen + 1; // +1 for null terminator
}
LOG_DEBUG("Loaded ", terrain.textures.size(), " texture names");
}
void ADTLoader::parseMMDX(const uint8_t* data, size_t size, ADTTerrain& terrain) {
// MMDX contains null-terminated M2 model filenames
size_t offset = 0;
while (offset < size) {
const char* modelName = reinterpret_cast<const char*>(data + offset);
size_t nameLen = std::strlen(modelName);
if (nameLen == 0) {
break;
}
terrain.doodadNames.push_back(std::string(modelName, nameLen));
offset += nameLen + 1;
}
LOG_DEBUG("Loaded ", terrain.doodadNames.size(), " doodad names");
}
void ADTLoader::parseMWMO(const uint8_t* data, size_t size, ADTTerrain& terrain) {
// MWMO contains null-terminated WMO filenames
size_t offset = 0;
while (offset < size) {
const char* wmoName = reinterpret_cast<const char*>(data + offset);
size_t nameLen = std::strlen(wmoName);
if (nameLen == 0) {
break;
}
terrain.wmoNames.push_back(std::string(wmoName, nameLen));
offset += nameLen + 1;
}
LOG_DEBUG("Loaded ", terrain.wmoNames.size(), " WMO names");
for (size_t i = 0; i < terrain.wmoNames.size(); i++) {
LOG_INFO(" WMO[", i, "]: ", terrain.wmoNames[i]);
}
}
void ADTLoader::parseMDDF(const uint8_t* data, size_t size, ADTTerrain& terrain) {
// MDDF contains doodad placements (36 bytes each)
const size_t entrySize = 36;
size_t count = size / entrySize;
for (size_t i = 0; i < count; i++) {
size_t offset = i * entrySize;
ADTTerrain::DoodadPlacement placement;
placement.nameId = readUInt32(data, offset);
placement.uniqueId = readUInt32(data, offset + 4);
placement.position[0] = readFloat(data, offset + 8);
placement.position[1] = readFloat(data, offset + 12);
placement.position[2] = readFloat(data, offset + 16);
placement.rotation[0] = readFloat(data, offset + 20);
placement.rotation[1] = readFloat(data, offset + 24);
placement.rotation[2] = readFloat(data, offset + 28);
placement.scale = readUInt16(data, offset + 32);
placement.flags = readUInt16(data, offset + 34);
terrain.doodadPlacements.push_back(placement);
}
LOG_INFO("Loaded ", terrain.doodadPlacements.size(), " doodad placements");
}
void ADTLoader::parseMODF(const uint8_t* data, size_t size, ADTTerrain& terrain) {
// MODF contains WMO placements (64 bytes each)
const size_t entrySize = 64;
size_t count = size / entrySize;
for (size_t i = 0; i < count; i++) {
size_t offset = i * entrySize;
ADTTerrain::WMOPlacement placement;
placement.nameId = readUInt32(data, offset);
placement.uniqueId = readUInt32(data, offset + 4);
placement.position[0] = readFloat(data, offset + 8);
placement.position[1] = readFloat(data, offset + 12);
placement.position[2] = readFloat(data, offset + 16);
placement.rotation[0] = readFloat(data, offset + 20);
placement.rotation[1] = readFloat(data, offset + 24);
placement.rotation[2] = readFloat(data, offset + 28);
placement.extentLower[0] = readFloat(data, offset + 32);
placement.extentLower[1] = readFloat(data, offset + 36);
placement.extentLower[2] = readFloat(data, offset + 40);
placement.extentUpper[0] = readFloat(data, offset + 44);
placement.extentUpper[1] = readFloat(data, offset + 48);
placement.extentUpper[2] = readFloat(data, offset + 52);
placement.flags = readUInt16(data, offset + 56);
placement.doodadSet = readUInt16(data, offset + 58);
terrain.wmoPlacements.push_back(placement);
}
LOG_INFO("Loaded ", terrain.wmoPlacements.size(), " WMO placements");
}
void ADTLoader::parseMCNK(const uint8_t* data, size_t size, int chunkIndex, ADTTerrain& terrain) {
if (chunkIndex < 0 || chunkIndex >= 256) {
LOG_WARNING("Invalid chunk index: ", chunkIndex);
return;
}
MapChunk& chunk = terrain.chunks[chunkIndex];
// Read MCNK header (128 bytes)
if (size < 128) {
LOG_WARNING("MCNK chunk too small");
return;
}
chunk.flags = readUInt32(data, 0);
chunk.indexX = readUInt32(data, 4);
chunk.indexY = readUInt32(data, 8);
// Read holes mask (at offset 0x3C = 60 in MCNK header)
// Each bit represents a 2x2 block of the 8x8 quad grid
chunk.holes = readUInt16(data, 60);
// Read layer count and offsets from MCNK header
uint32_t nLayers = readUInt32(data, 12);
uint32_t ofsHeight = readUInt32(data, 20); // MCVT offset
uint32_t ofsNormal = readUInt32(data, 24); // MCNR offset
uint32_t ofsLayer = readUInt32(data, 28); // MCLY offset
uint32_t ofsAlpha = readUInt32(data, 36); // MCAL offset
uint32_t sizeAlpha = readUInt32(data, 40);
// Debug first chunk only
if (chunkIndex == 0) {
LOG_INFO("MCNK[0] offsets: nLayers=", nLayers,
" height=", ofsHeight, " normal=", ofsNormal,
" layer=", ofsLayer, " alpha=", ofsAlpha,
" sizeAlpha=", sizeAlpha, " size=", size,
" holes=0x", std::hex, chunk.holes, std::dec);
}
// Position (stored at offset 0x68 = 104 in MCNK header)
chunk.position[0] = readFloat(data, 104); // X
chunk.position[1] = readFloat(data, 108); // Y
chunk.position[2] = readFloat(data, 112); // Z
// Parse sub-chunks using offsets from MCNK header
// WoW ADT sub-chunks may have their own 8-byte headers (magic+size)
// Check by inspecting the first 4 bytes at the offset
// Height map (MCVT) - 145 floats = 580 bytes
if (ofsHeight > 0 && ofsHeight + 580 <= size) {
// Check if this points to a sub-chunk header (magic "MCVT" = 0x4D435654)
uint32_t possibleMagic = readUInt32(data, ofsHeight);
uint32_t headerSkip = 0;
if (possibleMagic == MCVT) {
headerSkip = 8; // Skip magic + size
if (chunkIndex == 0) {
LOG_INFO("MCNK sub-chunks have headers (MCVT magic found at offset ", ofsHeight, ")");
}
}
parseMCVT(data + ofsHeight + headerSkip, 580, chunk);
}
// Normals (MCNR) - 145 normals (3 bytes each) + 13 padding = 448 bytes
if (ofsNormal > 0 && ofsNormal + 448 <= size) {
uint32_t possibleMagic = readUInt32(data, ofsNormal);
uint32_t skip = (possibleMagic == MCNR) ? 8 : 0;
parseMCNR(data + ofsNormal + skip, 448, chunk);
}
// Texture layers (MCLY) - 16 bytes per layer
if (ofsLayer > 0 && nLayers > 0) {
size_t layerSize = nLayers * 16;
uint32_t possibleMagic = readUInt32(data, ofsLayer);
uint32_t skip = (possibleMagic == MCLY) ? 8 : 0;
if (ofsLayer + skip + layerSize <= size) {
parseMCLY(data + ofsLayer + skip, layerSize, chunk);
}
}
// Alpha maps (MCAL) - variable size from header
if (ofsAlpha > 0 && sizeAlpha > 0 && ofsAlpha + sizeAlpha <= size) {
uint32_t possibleMagic = readUInt32(data, ofsAlpha);
uint32_t skip = (possibleMagic == MCAL) ? 8 : 0;
parseMCAL(data + ofsAlpha + skip, sizeAlpha - skip, chunk);
}
}
void ADTLoader::parseMCVT(const uint8_t* data, size_t size, MapChunk& chunk) {
// MCVT contains 145 height values (floats)
if (size < 145 * sizeof(float)) {
LOG_WARNING("MCVT chunk too small: ", size, " bytes");
return;
}
float minHeight = 999999.0f;
float maxHeight = -999999.0f;
for (int i = 0; i < 145; i++) {
float height = readFloat(data, i * sizeof(float));
chunk.heightMap.heights[i] = height;
if (height < minHeight) minHeight = height;
if (height > maxHeight) maxHeight = height;
}
// Log height range for first chunk only
static bool logged = false;
if (!logged) {
LOG_DEBUG("MCVT height range: [", minHeight, ", ", maxHeight, "]");
logged = true;
}
}
void ADTLoader::parseMCNR(const uint8_t* data, size_t size, MapChunk& chunk) {
// MCNR contains 145 normals (3 bytes each, signed)
if (size < 145 * 3) {
LOG_WARNING("MCNR chunk too small: ", size, " bytes");
return;
}
for (int i = 0; i < 145 * 3; i++) {
chunk.normals[i] = static_cast<int8_t>(data[i]);
}
}
void ADTLoader::parseMCLY(const uint8_t* data, size_t size, MapChunk& chunk) {
// MCLY contains texture layer definitions (16 bytes each)
size_t layerCount = size / 16;
if (layerCount > 4) {
LOG_WARNING("More than 4 texture layers: ", layerCount);
layerCount = 4;
}
static int layerLogCount = 0;
for (size_t i = 0; i < layerCount; i++) {
TextureLayer layer;
layer.textureId = readUInt32(data, i * 16 + 0);
layer.flags = readUInt32(data, i * 16 + 4);
layer.offsetMCAL = readUInt32(data, i * 16 + 8);
layer.effectId = readUInt32(data, i * 16 + 12);
if (layerLogCount < 10) {
LOG_INFO(" MCLY[", i, "]: texId=", layer.textureId,
" flags=0x", std::hex, layer.flags, std::dec,
" alphaOfs=", layer.offsetMCAL,
" useAlpha=", layer.useAlpha(),
" compressed=", layer.compressedAlpha());
layerLogCount++;
}
chunk.layers.push_back(layer);
}
}
void ADTLoader::parseMCAL(const uint8_t* data, size_t size, MapChunk& chunk) {
// MCAL contains alpha maps for texture layers
// Store raw data; decompression happens per-layer during mesh generation
chunk.alphaMap.resize(size);
std::memcpy(chunk.alphaMap.data(), data, size);
}
void ADTLoader::parseMH2O(const uint8_t* data, size_t size, ADTTerrain& terrain) {
// MH2O contains water/liquid data for all 256 map chunks
// Structure: 256 SMLiquidChunk headers followed by instance data
// Each SMLiquidChunk header is 12 bytes (WotLK 3.3.5a):
// - uint32_t offsetInstances (offset from MH2O chunk start)
// - uint32_t layerCount
// - uint32_t offsetAttributes (offset from MH2O chunk start)
const size_t headerSize = 12; // SMLiquidChunk size for WotLK
const size_t totalHeaderSize = 256 * headerSize;
if (size < totalHeaderSize) {
LOG_WARNING("MH2O chunk too small for headers: ", size, " bytes");
return;
}
int totalLayers = 0;
for (int chunkIdx = 0; chunkIdx < 256; chunkIdx++) {
size_t headerOffset = chunkIdx * headerSize;
uint32_t offsetInstances = readUInt32(data, headerOffset);
uint32_t layerCount = readUInt32(data, headerOffset + 4);
// uint32_t offsetAttributes = readUInt32(data, headerOffset + 8); // Not used
if (layerCount == 0 || offsetInstances == 0) {
continue; // No water in this chunk
}
// Sanity checks
if (offsetInstances >= size) {
continue;
}
if (layerCount > 16) {
// Sanity check - max 16 layers per chunk is reasonable
LOG_WARNING("MH2O: Invalid layer count ", layerCount, " for chunk ", chunkIdx);
continue;
}
// Parse each liquid layer (SMLiquidInstance - 24 bytes)
for (uint32_t layerIdx = 0; layerIdx < layerCount; layerIdx++) {
size_t instanceOffset = offsetInstances + layerIdx * 24;
if (instanceOffset + 24 > size) {
break;
}
ADTTerrain::WaterLayer layer;
layer.liquidType = readUInt16(data, instanceOffset);
uint16_t liquidObject = readUInt16(data, instanceOffset + 2); // LVF format flags
layer.minHeight = readFloat(data, instanceOffset + 4);
layer.maxHeight = readFloat(data, instanceOffset + 8);
layer.x = data[instanceOffset + 12];
layer.y = data[instanceOffset + 13];
layer.width = data[instanceOffset + 14];
layer.height = data[instanceOffset + 15];
uint32_t offsetExistsBitmap = readUInt32(data, instanceOffset + 16);
uint32_t offsetVertexData = readUInt32(data, instanceOffset + 20);
// Skip invalid layers
if (layer.width == 0 || layer.height == 0) {
continue;
}
// Clamp dimensions to valid range
if (layer.width > 8) layer.width = 8;
if (layer.height > 8) layer.height = 8;
if (layer.x + layer.width > 8) layer.width = 8 - layer.x;
if (layer.y + layer.height > 8) layer.height = 8 - layer.y;
// Read exists bitmap (which tiles have water)
// The bitmap is (width * height) bits, packed into bytes
size_t numTiles = layer.width * layer.height;
size_t bitmapBytes = (numTiles + 7) / 8;
// Note: offsets in SMLiquidInstance are relative to MH2O chunk start
if (offsetExistsBitmap > 0) {
size_t bitmapOffset = offsetExistsBitmap;
if (bitmapOffset + bitmapBytes <= size) {
layer.mask.resize(bitmapBytes);
std::memcpy(layer.mask.data(), data + bitmapOffset, bitmapBytes);
}
} else {
// No bitmap means all tiles have water
layer.mask.resize(bitmapBytes, 0xFF);
}
// Read vertex heights
// Number of vertices is (width+1) * (height+1)
size_t numVertices = (layer.width + 1) * (layer.height + 1);
// Check liquid object flags (LVF) to determine vertex format
bool hasHeightData = (liquidObject != 2); // LVF_height_depth or LVF_height_texcoord
if (hasHeightData && offsetVertexData > 0) {
size_t vertexOffset = offsetVertexData;
size_t vertexDataSize = numVertices * sizeof(float);
if (vertexOffset + vertexDataSize <= size) {
layer.heights.resize(numVertices);
for (size_t i = 0; i < numVertices; i++) {
layer.heights[i] = readFloat(data, vertexOffset + i * sizeof(float));
}
} else {
// Offset out of bounds - use flat water
layer.heights.resize(numVertices, layer.minHeight);
}
} else {
// No height data - use flat surface at minHeight
layer.heights.resize(numVertices, layer.minHeight);
}
// Default flags
layer.flags = 0;
terrain.waterData[chunkIdx].layers.push_back(layer);
totalLayers++;
}
}
LOG_INFO("Loaded MH2O water data: ", totalLayers, " liquid layers across ", size, " bytes");
}
} // namespace pipeline
} // namespace wowee

View file

@ -0,0 +1,154 @@
#include "pipeline/asset_manager.hpp"
#include "core/logger.hpp"
#include <algorithm>
namespace wowee {
namespace pipeline {
AssetManager::AssetManager() = default;
AssetManager::~AssetManager() {
shutdown();
}
bool AssetManager::initialize(const std::string& dataPath_) {
if (initialized) {
LOG_WARNING("AssetManager already initialized");
return true;
}
dataPath = dataPath_;
LOG_INFO("Initializing asset manager with data path: ", dataPath);
// Initialize MPQ manager
if (!mpqManager.initialize(dataPath)) {
LOG_ERROR("Failed to initialize MPQ manager");
return false;
}
initialized = true;
LOG_INFO("Asset manager initialized successfully");
return true;
}
void AssetManager::shutdown() {
if (!initialized) {
return;
}
LOG_INFO("Shutting down asset manager");
clearCache();
mpqManager.shutdown();
initialized = false;
}
BLPImage AssetManager::loadTexture(const std::string& path) {
if (!initialized) {
LOG_ERROR("AssetManager not initialized");
return BLPImage();
}
// Normalize path
std::string normalizedPath = normalizePath(path);
LOG_DEBUG("Loading texture: ", normalizedPath);
// Read BLP file from MPQ
std::vector<uint8_t> blpData = mpqManager.readFile(normalizedPath);
if (blpData.empty()) {
LOG_WARNING("Texture not found: ", normalizedPath);
return BLPImage();
}
// Load BLP
BLPImage image = BLPLoader::load(blpData);
if (!image.isValid()) {
LOG_ERROR("Failed to load texture: ", normalizedPath);
return BLPImage();
}
LOG_INFO("Loaded texture: ", normalizedPath, " (", image.width, "x", image.height, ")");
return image;
}
std::shared_ptr<DBCFile> AssetManager::loadDBC(const std::string& name) {
if (!initialized) {
LOG_ERROR("AssetManager not initialized");
return nullptr;
}
// Check cache first
auto it = dbcCache.find(name);
if (it != dbcCache.end()) {
LOG_DEBUG("DBC already loaded (cached): ", name);
return it->second;
}
LOG_DEBUG("Loading DBC: ", name);
// Construct DBC path (DBFilesClient directory)
std::string dbcPath = "DBFilesClient\\" + name;
// Read DBC file from MPQ
std::vector<uint8_t> dbcData = mpqManager.readFile(dbcPath);
if (dbcData.empty()) {
LOG_WARNING("DBC not found: ", dbcPath);
return nullptr;
}
// Load DBC
auto dbc = std::make_shared<DBCFile>();
if (!dbc->load(dbcData)) {
LOG_ERROR("Failed to load DBC: ", dbcPath);
return nullptr;
}
// Cache the DBC
dbcCache[name] = dbc;
LOG_INFO("Loaded DBC: ", name, " (", dbc->getRecordCount(), " records)");
return dbc;
}
std::shared_ptr<DBCFile> AssetManager::getDBC(const std::string& name) const {
auto it = dbcCache.find(name);
if (it != dbcCache.end()) {
return it->second;
}
return nullptr;
}
bool AssetManager::fileExists(const std::string& path) const {
if (!initialized) {
return false;
}
return mpqManager.fileExists(normalizePath(path));
}
std::vector<uint8_t> AssetManager::readFile(const std::string& path) const {
if (!initialized) {
return std::vector<uint8_t>();
}
std::lock_guard<std::mutex> lock(readMutex);
return mpqManager.readFile(normalizePath(path));
}
void AssetManager::clearCache() {
dbcCache.clear();
LOG_INFO("Cleared asset cache");
}
std::string AssetManager::normalizePath(const std::string& path) const {
std::string normalized = path;
// Convert forward slashes to backslashes (WoW uses backslashes)
std::replace(normalized.begin(), normalized.end(), '/', '\\');
return normalized;
}
} // namespace pipeline
} // namespace wowee

437
src/pipeline/blp_loader.cpp Normal file
View file

@ -0,0 +1,437 @@
#include "pipeline/blp_loader.hpp"
#include "core/logger.hpp"
#include <cstring>
#include <algorithm>
namespace wowee {
namespace pipeline {
BLPImage BLPLoader::load(const std::vector<uint8_t>& blpData) {
if (blpData.size() < 8) { // Minimum: magic + first field
LOG_ERROR("BLP data too small");
return BLPImage();
}
const uint8_t* data = blpData.data();
const char* magic = reinterpret_cast<const char*>(data);
// Check magic number
if (std::memcmp(magic, "BLP1", 4) == 0) {
return loadBLP1(data, blpData.size());
} else if (std::memcmp(magic, "BLP2", 4) == 0) {
return loadBLP2(data, blpData.size());
} else if (std::memcmp(magic, "BLP0", 4) == 0) {
LOG_WARNING("BLP0 format not fully supported");
return BLPImage();
} else {
LOG_ERROR("Invalid BLP magic: ", std::string(magic, 4));
return BLPImage();
}
}
BLPImage BLPLoader::loadBLP1(const uint8_t* data, size_t size) {
// BLP1 header has all uint32 fields (different layout from BLP2)
const BLP1Header* header = reinterpret_cast<const BLP1Header*>(data);
BLPImage image;
image.format = BLPFormat::BLP1;
image.width = header->width;
image.height = header->height;
image.channels = 4;
image.mipLevels = header->hasMips ? 16 : 1;
// BLP1 compression: 0=JPEG (not used in WoW), 1=palette/indexed
// BLP1 does NOT support DXT — only palette with optional alpha
if (header->compression == 1) {
image.compression = BLPCompression::PALETTE;
} else if (header->compression == 0) {
LOG_WARNING("BLP1 JPEG compression not supported");
return BLPImage();
} else {
LOG_WARNING("BLP1 unknown compression: ", header->compression);
return BLPImage();
}
LOG_DEBUG("Loading BLP1: ", image.width, "x", image.height, " ",
getCompressionName(image.compression), " alpha=", header->alphaBits);
// Get first mipmap (full resolution)
uint32_t offset = header->mipOffsets[0];
uint32_t mipSize = header->mipSizes[0];
if (offset + mipSize > size) {
LOG_ERROR("BLP1 mipmap data out of bounds (offset=", offset, " size=", mipSize, " fileSize=", size, ")");
return BLPImage();
}
const uint8_t* mipData = data + offset;
// Allocate output buffer
int pixelCount = image.width * image.height;
image.data.resize(pixelCount * 4); // RGBA8
decompressPalette(mipData, image.data.data(), header->palette,
image.width, image.height, static_cast<uint8_t>(header->alphaBits));
return image;
}
BLPImage BLPLoader::loadBLP2(const uint8_t* data, size_t size) {
// BLP2 header has uint8 fields for compression/alpha/encoding
const BLP2Header* header = reinterpret_cast<const BLP2Header*>(data);
BLPImage image;
image.format = BLPFormat::BLP2;
image.width = header->width;
image.height = header->height;
image.channels = 4;
image.mipLevels = header->hasMips ? 16 : 1;
// BLP2 compression types:
// 1 = palette/uncompressed
// 2 = DXTC (DXT1/DXT3/DXT5 based on alphaDepth + alphaEncoding)
// 3 = plain A8R8G8B8
if (header->compression == 1) {
image.compression = BLPCompression::PALETTE;
} else if (header->compression == 2) {
// BLP2 DXTC format selection based on alphaDepth + alphaEncoding:
// alphaDepth=0 → DXT1 (no alpha)
// alphaDepth>0, alphaEncoding=0 → DXT1 (1-bit alpha)
// alphaDepth>0, alphaEncoding=1 → DXT3 (explicit 4-bit alpha)
// alphaDepth>0, alphaEncoding=7 → DXT5 (interpolated alpha)
if (header->alphaDepth == 0 || header->alphaEncoding == 0) {
image.compression = BLPCompression::DXT1;
} else if (header->alphaEncoding == 1) {
image.compression = BLPCompression::DXT3;
} else if (header->alphaEncoding == 7) {
image.compression = BLPCompression::DXT5;
} else {
image.compression = BLPCompression::DXT1;
}
} else if (header->compression == 3) {
image.compression = BLPCompression::ARGB8888;
} else {
image.compression = BLPCompression::ARGB8888;
}
LOG_DEBUG("Loading BLP2: ", image.width, "x", image.height, " ",
getCompressionName(image.compression),
" (comp=", (int)header->compression, " alphaDepth=", (int)header->alphaDepth,
" alphaEnc=", (int)header->alphaEncoding, " mipOfs=", header->mipOffsets[0],
" mipSize=", header->mipSizes[0], ")");
// Get first mipmap (full resolution)
uint32_t offset = header->mipOffsets[0];
uint32_t mipSize = header->mipSizes[0];
if (offset + mipSize > size) {
LOG_ERROR("BLP2 mipmap data out of bounds");
return BLPImage();
}
const uint8_t* mipData = data + offset;
// Allocate output buffer
int pixelCount = image.width * image.height;
image.data.resize(pixelCount * 4); // RGBA8
switch (image.compression) {
case BLPCompression::DXT1:
decompressDXT1(mipData, image.data.data(), image.width, image.height);
break;
case BLPCompression::DXT3:
decompressDXT3(mipData, image.data.data(), image.width, image.height);
break;
case BLPCompression::DXT5:
decompressDXT5(mipData, image.data.data(), image.width, image.height);
break;
case BLPCompression::PALETTE:
decompressPalette(mipData, image.data.data(), header->palette,
image.width, image.height, header->alphaDepth);
break;
case BLPCompression::ARGB8888:
for (int i = 0; i < pixelCount; i++) {
image.data[i * 4 + 0] = mipData[i * 4 + 2]; // R
image.data[i * 4 + 1] = mipData[i * 4 + 1]; // G
image.data[i * 4 + 2] = mipData[i * 4 + 0]; // B
image.data[i * 4 + 3] = mipData[i * 4 + 3]; // A
}
break;
default:
LOG_ERROR("Unsupported BLP2 compression type");
return BLPImage();
}
// DXT1 with alphaDepth=0 has no meaningful alpha channel, but the DXT1
// color-key mode can produce alpha=0 pixels. Force all alpha to 255.
if (header->alphaDepth == 0) {
for (int i = 0; i < pixelCount; i++) {
image.data[i * 4 + 3] = 255;
}
}
return image;
}
void BLPLoader::decompressDXT1(const uint8_t* src, uint8_t* dst, int width, int height) {
// DXT1 decompression (8 bytes per 4x4 block)
int blockWidth = (width + 3) / 4;
int blockHeight = (height + 3) / 4;
for (int by = 0; by < blockHeight; by++) {
for (int bx = 0; bx < blockWidth; bx++) {
const uint8_t* block = src + (by * blockWidth + bx) * 8;
// Read color endpoints (RGB565)
uint16_t c0 = block[0] | (block[1] << 8);
uint16_t c1 = block[2] | (block[3] << 8);
// Convert RGB565 to RGB888
uint8_t r0 = ((c0 >> 11) & 0x1F) * 255 / 31;
uint8_t g0 = ((c0 >> 5) & 0x3F) * 255 / 63;
uint8_t b0 = (c0 & 0x1F) * 255 / 31;
uint8_t r1 = ((c1 >> 11) & 0x1F) * 255 / 31;
uint8_t g1 = ((c1 >> 5) & 0x3F) * 255 / 63;
uint8_t b1 = (c1 & 0x1F) * 255 / 31;
// Read 4x4 color indices (2 bits per pixel)
uint32_t indices = block[4] | (block[5] << 8) | (block[6] << 16) | (block[7] << 24);
// Decompress 4x4 block
for (int py = 0; py < 4; py++) {
for (int px = 0; px < 4; px++) {
int x = bx * 4 + px;
int y = by * 4 + py;
if (x >= width || y >= height) continue;
int index = (indices >> ((py * 4 + px) * 2)) & 0x3;
uint8_t* pixel = dst + (y * width + x) * 4;
// Interpolate colors based on index
if (c0 > c1) {
switch (index) {
case 0: pixel[0] = r0; pixel[1] = g0; pixel[2] = b0; pixel[3] = 255; break;
case 1: pixel[0] = r1; pixel[1] = g1; pixel[2] = b1; pixel[3] = 255; break;
case 2: pixel[0] = (2*r0 + r1) / 3; pixel[1] = (2*g0 + g1) / 3; pixel[2] = (2*b0 + b1) / 3; pixel[3] = 255; break;
case 3: pixel[0] = (r0 + 2*r1) / 3; pixel[1] = (g0 + 2*g1) / 3; pixel[2] = (b0 + 2*b1) / 3; pixel[3] = 255; break;
}
} else {
switch (index) {
case 0: pixel[0] = r0; pixel[1] = g0; pixel[2] = b0; pixel[3] = 255; break;
case 1: pixel[0] = r1; pixel[1] = g1; pixel[2] = b1; pixel[3] = 255; break;
case 2: pixel[0] = (r0 + r1) / 2; pixel[1] = (g0 + g1) / 2; pixel[2] = (b0 + b1) / 2; pixel[3] = 255; break;
case 3: pixel[0] = 0; pixel[1] = 0; pixel[2] = 0; pixel[3] = 0; break; // Transparent
}
}
}
}
}
}
}
void BLPLoader::decompressDXT3(const uint8_t* src, uint8_t* dst, int width, int height) {
// DXT3 decompression (16 bytes per 4x4 block - 8 bytes alpha + 8 bytes color)
int blockWidth = (width + 3) / 4;
int blockHeight = (height + 3) / 4;
for (int by = 0; by < blockHeight; by++) {
for (int bx = 0; bx < blockWidth; bx++) {
const uint8_t* block = src + (by * blockWidth + bx) * 16;
// First 8 bytes: 4-bit alpha values
uint64_t alphaBlock = 0;
for (int i = 0; i < 8; i++) {
alphaBlock |= (uint64_t)block[i] << (i * 8);
}
// Color block (same as DXT1) starts at byte 8
const uint8_t* colorBlock = block + 8;
uint16_t c0 = colorBlock[0] | (colorBlock[1] << 8);
uint16_t c1 = colorBlock[2] | (colorBlock[3] << 8);
uint8_t r0 = ((c0 >> 11) & 0x1F) * 255 / 31;
uint8_t g0 = ((c0 >> 5) & 0x3F) * 255 / 63;
uint8_t b0 = (c0 & 0x1F) * 255 / 31;
uint8_t r1 = ((c1 >> 11) & 0x1F) * 255 / 31;
uint8_t g1 = ((c1 >> 5) & 0x3F) * 255 / 63;
uint8_t b1 = (c1 & 0x1F) * 255 / 31;
uint32_t indices = colorBlock[4] | (colorBlock[5] << 8) | (colorBlock[6] << 16) | (colorBlock[7] << 24);
for (int py = 0; py < 4; py++) {
for (int px = 0; px < 4; px++) {
int x = bx * 4 + px;
int y = by * 4 + py;
if (x >= width || y >= height) continue;
int index = (indices >> ((py * 4 + px) * 2)) & 0x3;
uint8_t* pixel = dst + (y * width + x) * 4;
// DXT3 always uses 4-color mode for the color portion
switch (index) {
case 0: pixel[0] = r0; pixel[1] = g0; pixel[2] = b0; break;
case 1: pixel[0] = r1; pixel[1] = g1; pixel[2] = b1; break;
case 2: pixel[0] = (2*r0 + r1) / 3; pixel[1] = (2*g0 + g1) / 3; pixel[2] = (2*b0 + b1) / 3; break;
case 3: pixel[0] = (r0 + 2*r1) / 3; pixel[1] = (g0 + 2*g1) / 3; pixel[2] = (b0 + 2*b1) / 3; break;
}
// Apply 4-bit alpha
int alphaIndex = py * 4 + px;
uint8_t alpha4 = (alphaBlock >> (alphaIndex * 4)) & 0xF;
pixel[3] = alpha4 * 255 / 15;
}
}
}
}
}
void BLPLoader::decompressDXT5(const uint8_t* src, uint8_t* dst, int width, int height) {
// DXT5 decompression (16 bytes per 4x4 block - interpolated alpha + color)
int blockWidth = (width + 3) / 4;
int blockHeight = (height + 3) / 4;
for (int by = 0; by < blockHeight; by++) {
for (int bx = 0; bx < blockWidth; bx++) {
const uint8_t* block = src + (by * blockWidth + bx) * 16;
// Alpha endpoints
uint8_t alpha0 = block[0];
uint8_t alpha1 = block[1];
// Build alpha lookup table
uint8_t alphas[8];
alphas[0] = alpha0;
alphas[1] = alpha1;
if (alpha0 > alpha1) {
alphas[2] = (6*alpha0 + 1*alpha1) / 7;
alphas[3] = (5*alpha0 + 2*alpha1) / 7;
alphas[4] = (4*alpha0 + 3*alpha1) / 7;
alphas[5] = (3*alpha0 + 4*alpha1) / 7;
alphas[6] = (2*alpha0 + 5*alpha1) / 7;
alphas[7] = (1*alpha0 + 6*alpha1) / 7;
} else {
alphas[2] = (4*alpha0 + 1*alpha1) / 5;
alphas[3] = (3*alpha0 + 2*alpha1) / 5;
alphas[4] = (2*alpha0 + 3*alpha1) / 5;
alphas[5] = (1*alpha0 + 4*alpha1) / 5;
alphas[6] = 0;
alphas[7] = 255;
}
// Alpha indices (48 bits for 16 pixels, 3 bits each)
uint64_t alphaIndices = 0;
for (int i = 2; i < 8; i++) {
alphaIndices |= (uint64_t)block[i] << ((i - 2) * 8);
}
// Color block (same as DXT1) starts at byte 8
const uint8_t* colorBlock = block + 8;
uint16_t c0 = colorBlock[0] | (colorBlock[1] << 8);
uint16_t c1 = colorBlock[2] | (colorBlock[3] << 8);
uint8_t r0 = ((c0 >> 11) & 0x1F) * 255 / 31;
uint8_t g0 = ((c0 >> 5) & 0x3F) * 255 / 63;
uint8_t b0 = (c0 & 0x1F) * 255 / 31;
uint8_t r1 = ((c1 >> 11) & 0x1F) * 255 / 31;
uint8_t g1 = ((c1 >> 5) & 0x3F) * 255 / 63;
uint8_t b1 = (c1 & 0x1F) * 255 / 31;
uint32_t indices = colorBlock[4] | (colorBlock[5] << 8) | (colorBlock[6] << 16) | (colorBlock[7] << 24);
for (int py = 0; py < 4; py++) {
for (int px = 0; px < 4; px++) {
int x = bx * 4 + px;
int y = by * 4 + py;
if (x >= width || y >= height) continue;
int index = (indices >> ((py * 4 + px) * 2)) & 0x3;
uint8_t* pixel = dst + (y * width + x) * 4;
// DXT5 always uses 4-color mode for the color portion
switch (index) {
case 0: pixel[0] = r0; pixel[1] = g0; pixel[2] = b0; break;
case 1: pixel[0] = r1; pixel[1] = g1; pixel[2] = b1; break;
case 2: pixel[0] = (2*r0 + r1) / 3; pixel[1] = (2*g0 + g1) / 3; pixel[2] = (2*b0 + b1) / 3; break;
case 3: pixel[0] = (r0 + 2*r1) / 3; pixel[1] = (g0 + 2*g1) / 3; pixel[2] = (b0 + 2*b1) / 3; break;
}
// Apply interpolated alpha
int alphaIdx = (alphaIndices >> ((py * 4 + px) * 3)) & 0x7;
pixel[3] = alphas[alphaIdx];
}
}
}
}
}
void BLPLoader::decompressPalette(const uint8_t* src, uint8_t* dst, const uint32_t* palette, int width, int height, uint8_t alphaDepth) {
int pixelCount = width * height;
// Palette indices are first (1 byte per pixel)
const uint8_t* indices = src;
// Alpha data follows the palette indices
const uint8_t* alphaData = src + pixelCount;
for (int i = 0; i < pixelCount; i++) {
uint8_t index = indices[i];
uint32_t color = palette[index];
// Palette stores BGR (the high byte is typically 0, not alpha)
dst[i * 4 + 0] = (color >> 16) & 0xFF; // R
dst[i * 4 + 1] = (color >> 8) & 0xFF; // G
dst[i * 4 + 2] = color & 0xFF; // B
// Alpha is stored separately after the index data
if (alphaDepth == 8) {
dst[i * 4 + 3] = alphaData[i];
} else if (alphaDepth == 4) {
// 4-bit alpha: 2 pixels per byte
uint8_t alphaByte = alphaData[i / 2];
dst[i * 4 + 3] = (i % 2 == 0) ? ((alphaByte & 0x0F) * 17) : ((alphaByte >> 4) * 17);
} else if (alphaDepth == 1) {
// 1-bit alpha: 8 pixels per byte
uint8_t alphaByte = alphaData[i / 8];
dst[i * 4 + 3] = ((alphaByte >> (i % 8)) & 1) ? 255 : 0;
} else {
// No alpha channel: fully opaque
dst[i * 4 + 3] = 255;
}
}
}
const char* BLPLoader::getFormatName(BLPFormat format) {
switch (format) {
case BLPFormat::BLP0: return "BLP0";
case BLPFormat::BLP1: return "BLP1";
case BLPFormat::BLP2: return "BLP2";
default: return "Unknown";
}
}
const char* BLPLoader::getCompressionName(BLPCompression compression) {
switch (compression) {
case BLPCompression::NONE: return "None";
case BLPCompression::PALETTE: return "Palette";
case BLPCompression::DXT1: return "DXT1";
case BLPCompression::DXT3: return "DXT3";
case BLPCompression::DXT5: return "DXT5";
case BLPCompression::ARGB8888: return "ARGB8888";
default: return "Unknown";
}
}
} // namespace pipeline
} // namespace wowee

162
src/pipeline/dbc_loader.cpp Normal file
View file

@ -0,0 +1,162 @@
#include "pipeline/dbc_loader.hpp"
#include "core/logger.hpp"
#include <cstring>
namespace wowee {
namespace pipeline {
DBCFile::DBCFile() = default;
DBCFile::~DBCFile() = default;
bool DBCFile::load(const std::vector<uint8_t>& dbcData) {
if (dbcData.size() < sizeof(DBCHeader)) {
LOG_ERROR("DBC data too small for header");
return false;
}
// Read header
const DBCHeader* header = reinterpret_cast<const DBCHeader*>(dbcData.data());
// Verify magic
if (std::memcmp(header->magic, "WDBC", 4) != 0) {
LOG_ERROR("Invalid DBC magic: ", std::string(header->magic, 4));
return false;
}
recordCount = header->recordCount;
fieldCount = header->fieldCount;
recordSize = header->recordSize;
stringBlockSize = header->stringBlockSize;
// Validate sizes
uint32_t expectedSize = sizeof(DBCHeader) + (recordCount * recordSize) + stringBlockSize;
if (dbcData.size() < expectedSize) {
LOG_ERROR("DBC file truncated: expected ", expectedSize, " bytes, got ", dbcData.size());
return false;
}
// Validate record size matches field count
if (recordSize != fieldCount * 4) {
LOG_WARNING("DBC record size mismatch: recordSize=", recordSize,
" but fieldCount*4=", fieldCount * 4);
}
LOG_DEBUG("Loading DBC: ", recordCount, " records, ",
fieldCount, " fields, ", recordSize, " bytes/record, ",
stringBlockSize, " string bytes");
// Copy record data
const uint8_t* recordStart = dbcData.data() + sizeof(DBCHeader);
uint32_t totalRecordSize = recordCount * recordSize;
recordData.resize(totalRecordSize);
std::memcpy(recordData.data(), recordStart, totalRecordSize);
// Copy string block
const uint8_t* stringStart = recordStart + totalRecordSize;
stringBlock.resize(stringBlockSize);
if (stringBlockSize > 0) {
std::memcpy(stringBlock.data(), stringStart, stringBlockSize);
}
loaded = true;
idCacheBuilt = false;
idToIndexCache.clear();
return true;
}
const uint8_t* DBCFile::getRecord(uint32_t index) const {
if (!loaded || index >= recordCount) {
return nullptr;
}
return recordData.data() + (index * recordSize);
}
uint32_t DBCFile::getUInt32(uint32_t recordIndex, uint32_t fieldIndex) const {
if (!loaded || recordIndex >= recordCount || fieldIndex >= fieldCount) {
return 0;
}
const uint8_t* record = getRecord(recordIndex);
if (!record) {
return 0;
}
const uint32_t* field = reinterpret_cast<const uint32_t*>(record + (fieldIndex * 4));
return *field;
}
int32_t DBCFile::getInt32(uint32_t recordIndex, uint32_t fieldIndex) const {
return static_cast<int32_t>(getUInt32(recordIndex, fieldIndex));
}
float DBCFile::getFloat(uint32_t recordIndex, uint32_t fieldIndex) const {
if (!loaded || recordIndex >= recordCount || fieldIndex >= fieldCount) {
return 0.0f;
}
const uint8_t* record = getRecord(recordIndex);
if (!record) {
return 0.0f;
}
const float* field = reinterpret_cast<const float*>(record + (fieldIndex * 4));
return *field;
}
std::string DBCFile::getString(uint32_t recordIndex, uint32_t fieldIndex) const {
uint32_t offset = getUInt32(recordIndex, fieldIndex);
return getStringByOffset(offset);
}
std::string DBCFile::getStringByOffset(uint32_t offset) const {
if (!loaded || offset >= stringBlockSize) {
return "";
}
// Find null terminator
const char* str = reinterpret_cast<const char*>(stringBlock.data() + offset);
const char* end = reinterpret_cast<const char*>(stringBlock.data() + stringBlockSize);
// Find string length (up to null terminator or end of block)
size_t length = 0;
while (str + length < end && str[length] != '\0') {
length++;
}
return std::string(str, length);
}
int32_t DBCFile::findRecordById(uint32_t id) const {
if (!loaded) {
return -1;
}
// Build ID cache if not already built
if (!idCacheBuilt) {
buildIdCache();
}
auto it = idToIndexCache.find(id);
if (it != idToIndexCache.end()) {
return static_cast<int32_t>(it->second);
}
return -1;
}
void DBCFile::buildIdCache() const {
idToIndexCache.clear();
for (uint32_t i = 0; i < recordCount; i++) {
uint32_t id = getUInt32(i, 0); // Assume first field is ID
idToIndexCache[id] = i;
}
idCacheBuilt = true;
LOG_DEBUG("Built DBC ID cache with ", idToIndexCache.size(), " entries");
}
} // namespace pipeline
} // namespace wowee

744
src/pipeline/m2_loader.cpp Normal file
View file

@ -0,0 +1,744 @@
/**
* M2 Model Loader Binary parser for WoW's M2 model format (WotLK 3.3.5a)
*
* M2 files contain skeletal-animated meshes used for characters, creatures,
* and doodads. The format stores geometry, bones with animation tracks,
* textures, and material batches. A companion .skin file holds the rendering
* batches and submesh definitions.
*
* Key format details:
* - On-disk bone struct is 88 bytes (includes 3 animation track headers).
* - Animation tracks use an "array-of-arrays" indirection: the header points
* to N sub-array headers, each being {uint32 count, uint32 offset}.
* - Rotation tracks store compressed quaternions as int16[4], decoded with
* an offset mapping (not simple division).
* - Skin file indices use two-level indirection: triangle vertex lookup
* table global vertex index.
* - Skin batch struct is 24 bytes on disk the geosetIndex field at offset 10
* is easily missed, causing a 2-byte alignment shift on all subsequent fields.
*
* Reference: https://wowdev.wiki/M2
*/
#include "pipeline/m2_loader.hpp"
#include "core/logger.hpp"
#include <cstring>
#include <algorithm>
namespace wowee {
namespace pipeline {
namespace {
// M2 file header structure (version 260+ for WotLK 3.3.5a)
struct M2Header {
char magic[4]; // 'MD20'
uint32_t version;
uint32_t nameLength;
uint32_t nameOffset;
uint32_t globalFlags;
uint32_t nGlobalSequences;
uint32_t ofsGlobalSequences;
uint32_t nAnimations;
uint32_t ofsAnimations;
uint32_t nAnimationLookup;
uint32_t ofsAnimationLookup;
uint32_t nBones;
uint32_t ofsBones;
uint32_t nKeyBoneLookup;
uint32_t ofsKeyBoneLookup;
uint32_t nVertices;
uint32_t ofsVertices;
uint32_t nViews; // Number of skin files
uint32_t nColors;
uint32_t ofsColors;
uint32_t nTextures;
uint32_t ofsTextures;
uint32_t nTransparency;
uint32_t ofsTransparency;
uint32_t nUVAnimation;
uint32_t ofsUVAnimation;
uint32_t nTexReplace;
uint32_t ofsTexReplace;
uint32_t nRenderFlags;
uint32_t ofsRenderFlags;
uint32_t nBoneLookupTable;
uint32_t ofsBoneLookupTable;
uint32_t nTexLookup;
uint32_t ofsTexLookup;
uint32_t nTexUnits;
uint32_t ofsTexUnits;
uint32_t nTransLookup;
uint32_t ofsTransLookup;
uint32_t nUVAnimLookup;
uint32_t ofsUVAnimLookup;
float vertexBox[6]; // Bounding box
float vertexRadius;
float boundingBox[6];
float boundingRadius;
uint32_t nBoundingTriangles;
uint32_t ofsBoundingTriangles;
uint32_t nBoundingVertices;
uint32_t ofsBoundingVertices;
uint32_t nBoundingNormals;
uint32_t ofsBoundingNormals;
uint32_t nAttachments;
uint32_t ofsAttachments;
uint32_t nAttachmentLookup;
uint32_t ofsAttachmentLookup;
};
// M2 vertex structure (on-disk format)
struct M2VertexDisk {
float pos[3];
uint8_t boneWeights[4];
uint8_t boneIndices[4];
float normal[3];
float texCoords[2][2];
};
// M2 animation track header (on-disk, 20 bytes)
struct M2TrackDisk {
uint16_t interpolationType;
int16_t globalSequence;
uint32_t nTimestamps;
uint32_t ofsTimestamps;
uint32_t nKeys;
uint32_t ofsKeys;
};
// Full M2 bone structure (on-disk, 88 bytes)
struct M2BoneDisk {
int32_t keyBoneId; // 4
uint32_t flags; // 4
int16_t parentBone; // 2
uint16_t submeshId; // 2
uint32_t boneNameCRC; // 4
M2TrackDisk translation; // 20
M2TrackDisk rotation; // 20
M2TrackDisk scale; // 20
float pivot[3]; // 12
}; // Total: 88
// M2 animation sequence structure
struct M2SequenceDisk {
uint16_t id;
uint16_t variationIndex;
uint32_t duration;
float movingSpeed;
uint32_t flags;
int16_t frequency;
uint16_t padding;
uint32_t replayMin;
uint32_t replayMax;
uint32_t blendTime;
float bounds[6];
float boundRadius;
int16_t nextAnimation;
uint16_t aliasNext;
};
// M2 texture definition
struct M2TextureDisk {
uint32_t type;
uint32_t flags;
uint32_t nameLength;
uint32_t nameOffset;
};
// Skin file header (contains rendering batches)
struct M2SkinHeader {
char magic[4]; // 'SKIN'
uint32_t nIndices;
uint32_t ofsIndices;
uint32_t nTriangles;
uint32_t ofsTriangles;
uint32_t nVertexProperties;
uint32_t ofsVertexProperties;
uint32_t nSubmeshes;
uint32_t ofsSubmeshes;
uint32_t nBatches;
uint32_t ofsBatches;
uint32_t nBones;
};
// Skin submesh structure (48 bytes for WotLK)
struct M2SkinSubmesh {
uint16_t id;
uint16_t level;
uint16_t vertexStart;
uint16_t vertexCount;
uint16_t indexStart;
uint16_t indexCount;
uint16_t boneCount;
uint16_t boneStart;
uint16_t boneInfluences;
uint16_t centerBoneIndex;
float centerPosition[3];
float sortCenterPosition[3];
float sortRadius;
};
// Skin batch structure (24 bytes on disk)
struct M2BatchDisk {
uint8_t flags;
int8_t priorityPlane;
uint16_t shader;
uint16_t skinSectionIndex;
uint16_t geosetIndex; // Geoset index (not same as submesh ID)
uint16_t colorIndex;
uint16_t materialIndex;
uint16_t materialLayer;
uint16_t textureCount;
uint16_t textureComboIndex; // Index into texture lookup table
uint16_t textureCoordIndex; // Texture coordinate combo index
uint16_t textureWeightIndex; // Transparency lookup index
uint16_t textureTransformIndex; // Texture animation lookup index
};
// Compressed quaternion (on-disk) for rotation tracks
struct CompressedQuat {
int16_t x, y, z, w;
};
// M2 attachment point (on-disk)
struct M2AttachmentDisk {
uint32_t id;
uint16_t bone;
uint16_t unknown;
float position[3];
uint8_t trackData[20]; // M2Track<uint8_t> — skip
};
template<typename T>
T readValue(const std::vector<uint8_t>& data, uint32_t offset) {
if (offset + sizeof(T) > data.size()) {
return T{};
}
T value;
std::memcpy(&value, &data[offset], sizeof(T));
return value;
}
template<typename T>
std::vector<T> readArray(const std::vector<uint8_t>& data, uint32_t offset, uint32_t count) {
std::vector<T> result;
if (count == 0 || offset + count * sizeof(T) > data.size()) {
return result;
}
result.resize(count);
std::memcpy(result.data(), &data[offset], count * sizeof(T));
return result;
}
std::string readString(const std::vector<uint8_t>& data, uint32_t offset, uint32_t length) {
if (offset + length > data.size()) {
return "";
}
// Strip trailing null bytes (M2 nameLength includes \0)
while (length > 0 && data[offset + length - 1] == 0) {
length--;
}
return std::string(reinterpret_cast<const char*>(&data[offset]), length);
}
enum class TrackType { VEC3, QUAT_COMPRESSED };
// Parse an M2 animation track from the binary data.
// The track uses an "array of arrays" layout: nTimestamps pairs of {count, offset}.
// sequenceFlags: per-sequence flags; sequences WITHOUT flag 0x20 store their keyframe
// data in external .anim files, so their sub-array offsets are .anim-relative and must
// be skipped when reading from the M2 file.
void parseAnimTrack(const std::vector<uint8_t>& data,
const M2TrackDisk& disk,
M2AnimationTrack& track,
TrackType type,
const std::vector<uint32_t>& sequenceFlags = {}) {
track.interpolationType = disk.interpolationType;
track.globalSequence = disk.globalSequence;
if (disk.nTimestamps == 0 || disk.nKeys == 0) return;
uint32_t numSubArrays = disk.nTimestamps;
track.sequences.resize(numSubArrays);
for (uint32_t i = 0; i < numSubArrays; i++) {
// Sequences without flag 0x20 have their animation data in external .anim files.
// Their sub-array offsets are .anim-file-relative, not M2-relative, so reading
// from the M2 file would produce garbage data.
if (i < sequenceFlags.size() && !(sequenceFlags[i] & 0x20)) continue;
// Each sub-array header is {uint32_t count, uint32_t offset} = 8 bytes
uint32_t tsHeaderOfs = disk.ofsTimestamps + i * 8;
uint32_t keyHeaderOfs = disk.ofsKeys + i * 8;
if (tsHeaderOfs + 8 > data.size() || keyHeaderOfs + 8 > data.size()) continue;
uint32_t tsCount = readValue<uint32_t>(data, tsHeaderOfs);
uint32_t tsOffset = readValue<uint32_t>(data, tsHeaderOfs + 4);
uint32_t keyCount = readValue<uint32_t>(data, keyHeaderOfs);
uint32_t keyOffset = readValue<uint32_t>(data, keyHeaderOfs + 4);
if (tsCount == 0 || keyCount == 0) continue;
// Validate offsets are within file data (external .anim files have out-of-range offsets)
if (tsOffset + tsCount * sizeof(uint32_t) > data.size()) continue;
// Read timestamps
auto timestamps = readArray<uint32_t>(data, tsOffset, tsCount);
track.sequences[i].timestamps = std::move(timestamps);
// Validate key data offset
size_t keyElementSize = (type == TrackType::VEC3) ? sizeof(float) * 3 : sizeof(int16_t) * 4;
if (keyOffset + keyCount * keyElementSize > data.size()) {
track.sequences[i].timestamps.clear();
continue;
}
// Read key values
if (type == TrackType::VEC3) {
// Translation/scale: float[3] per key
struct Vec3Disk { float x, y, z; };
auto values = readArray<Vec3Disk>(data, keyOffset, keyCount);
track.sequences[i].vec3Values.reserve(values.size());
for (const auto& v : values) {
track.sequences[i].vec3Values.emplace_back(v.x, v.y, v.z);
}
} else {
// Rotation: compressed quaternion int16[4] per key
auto compressed = readArray<CompressedQuat>(data, keyOffset, keyCount);
track.sequences[i].quatValues.reserve(compressed.size());
for (const auto& cq : compressed) {
// M2 compressed quaternion: offset mapping, NOT simple division
// int16 range [-32768..32767] maps to float [-1..1] with offset
float fx = (cq.x < 0) ? (cq.x + 32768) / 32767.0f : (cq.x - 32767) / 32767.0f;
float fy = (cq.y < 0) ? (cq.y + 32768) / 32767.0f : (cq.y - 32767) / 32767.0f;
float fz = (cq.z < 0) ? (cq.z + 32768) / 32767.0f : (cq.z - 32767) / 32767.0f;
float fw = (cq.w < 0) ? (cq.w + 32768) / 32767.0f : (cq.w - 32767) / 32767.0f;
// M2 on-disk: (x,y,z,w), GLM quat constructor: (w,x,y,z)
glm::quat q(fw, fx, fy, fz);
float len = glm::length(q);
if (len > 0.001f) {
q = q / len;
} else {
q = glm::quat(1.0f, 0.0f, 0.0f, 0.0f); // identity
}
track.sequences[i].quatValues.push_back(q);
}
}
}
}
} // anonymous namespace
M2Model M2Loader::load(const std::vector<uint8_t>& m2Data) {
M2Model model;
if (m2Data.size() < sizeof(M2Header)) {
core::Logger::getInstance().error("M2 data too small");
return model;
}
// Read header
M2Header header;
std::memcpy(&header, m2Data.data(), sizeof(M2Header));
// Verify magic
if (std::strncmp(header.magic, "MD20", 4) != 0) {
core::Logger::getInstance().error("Invalid M2 magic: expected MD20");
return model;
}
core::Logger::getInstance().debug("Loading M2 model (version ", header.version, ")");
// Read model name
if (header.nameLength > 0 && header.nameOffset > 0) {
model.name = readString(m2Data, header.nameOffset, header.nameLength);
}
model.version = header.version;
model.globalFlags = header.globalFlags;
// Bounding box
model.boundMin = glm::vec3(header.boundingBox[0], header.boundingBox[1], header.boundingBox[2]);
model.boundMax = glm::vec3(header.boundingBox[3], header.boundingBox[4], header.boundingBox[5]);
model.boundRadius = header.boundingRadius;
// Read vertices
if (header.nVertices > 0 && header.ofsVertices > 0) {
auto diskVerts = readArray<M2VertexDisk>(m2Data, header.ofsVertices, header.nVertices);
model.vertices.reserve(diskVerts.size());
for (const auto& dv : diskVerts) {
M2Vertex v;
v.position = glm::vec3(dv.pos[0], dv.pos[1], dv.pos[2]);
std::memcpy(v.boneWeights, dv.boneWeights, 4);
std::memcpy(v.boneIndices, dv.boneIndices, 4);
v.normal = glm::vec3(dv.normal[0], dv.normal[1], dv.normal[2]);
v.texCoords[0] = glm::vec2(dv.texCoords[0][0], dv.texCoords[0][1]);
v.texCoords[1] = glm::vec2(dv.texCoords[1][0], dv.texCoords[1][1]);
model.vertices.push_back(v);
}
core::Logger::getInstance().debug(" Vertices: ", model.vertices.size());
}
// Read animation sequences (needed before bones to know sequence count)
if (header.nAnimations > 0 && header.ofsAnimations > 0) {
auto diskSeqs = readArray<M2SequenceDisk>(m2Data, header.ofsAnimations, header.nAnimations);
model.sequences.reserve(diskSeqs.size());
for (const auto& ds : diskSeqs) {
M2Sequence seq;
seq.id = ds.id;
seq.variationIndex = ds.variationIndex;
seq.duration = ds.duration;
seq.movingSpeed = ds.movingSpeed;
seq.flags = ds.flags;
seq.frequency = ds.frequency;
seq.replayMin = ds.replayMin;
seq.replayMax = ds.replayMax;
seq.blendTime = ds.blendTime;
seq.boundMin = glm::vec3(ds.bounds[0], ds.bounds[1], ds.bounds[2]);
seq.boundMax = glm::vec3(ds.bounds[3], ds.bounds[4], ds.bounds[5]);
seq.boundRadius = ds.boundRadius;
seq.nextAnimation = ds.nextAnimation;
seq.aliasNext = ds.aliasNext;
model.sequences.push_back(seq);
}
core::Logger::getInstance().debug(" Animation sequences: ", model.sequences.size());
}
// Read bones with full animation track data
if (header.nBones > 0 && header.ofsBones > 0) {
// Verify we have enough data for the full bone structures
uint32_t expectedBoneSize = header.nBones * sizeof(M2BoneDisk);
if (header.ofsBones + expectedBoneSize > m2Data.size()) {
core::Logger::getInstance().warning("M2 bone data extends beyond file, loading with fallback");
}
model.bones.reserve(header.nBones);
int bonesWithKeyframes = 0;
// Build per-sequence flags to skip external-data sequences during M2 parse
std::vector<uint32_t> seqFlags;
seqFlags.reserve(model.sequences.size());
for (const auto& seq : model.sequences) {
seqFlags.push_back(seq.flags);
}
for (uint32_t boneIdx = 0; boneIdx < header.nBones; boneIdx++) {
uint32_t boneOffset = header.ofsBones + boneIdx * sizeof(M2BoneDisk);
if (boneOffset + sizeof(M2BoneDisk) > m2Data.size()) {
// Fallback: create identity bone
M2Bone bone;
bone.keyBoneId = -1;
bone.flags = 0;
bone.parentBone = -1;
bone.submeshId = 0;
bone.pivot = glm::vec3(0.0f);
model.bones.push_back(bone);
continue;
}
M2BoneDisk db = readValue<M2BoneDisk>(m2Data, boneOffset);
M2Bone bone;
bone.keyBoneId = db.keyBoneId;
bone.flags = db.flags;
bone.parentBone = db.parentBone;
bone.submeshId = db.submeshId;
bone.pivot = glm::vec3(db.pivot[0], db.pivot[1], db.pivot[2]);
// Parse animation tracks (skip sequences with external .anim data)
parseAnimTrack(m2Data, db.translation, bone.translation, TrackType::VEC3, seqFlags);
parseAnimTrack(m2Data, db.rotation, bone.rotation, TrackType::QUAT_COMPRESSED, seqFlags);
parseAnimTrack(m2Data, db.scale, bone.scale, TrackType::VEC3, seqFlags);
if (bone.translation.hasData() || bone.rotation.hasData() || bone.scale.hasData()) {
bonesWithKeyframes++;
}
model.bones.push_back(bone);
}
core::Logger::getInstance().debug(" Bones: ", model.bones.size(),
" (", bonesWithKeyframes, " with keyframes)");
}
// Read textures
if (header.nTextures > 0 && header.ofsTextures > 0) {
auto diskTextures = readArray<M2TextureDisk>(m2Data, header.ofsTextures, header.nTextures);
model.textures.reserve(diskTextures.size());
for (const auto& dt : diskTextures) {
M2Texture tex;
tex.type = dt.type;
tex.flags = dt.flags;
if (dt.nameLength > 0 && dt.nameOffset > 0) {
tex.filename = readString(m2Data, dt.nameOffset, dt.nameLength);
}
model.textures.push_back(tex);
}
core::Logger::getInstance().debug(" Textures: ", model.textures.size());
}
// Read texture lookup
if (header.nTexLookup > 0 && header.ofsTexLookup > 0) {
model.textureLookup = readArray<uint16_t>(m2Data, header.ofsTexLookup, header.nTexLookup);
}
// Read attachment points
if (header.nAttachments > 0 && header.ofsAttachments > 0) {
auto diskAttachments = readArray<M2AttachmentDisk>(m2Data, header.ofsAttachments, header.nAttachments);
model.attachments.reserve(diskAttachments.size());
for (const auto& da : diskAttachments) {
M2Attachment att;
att.id = da.id;
att.bone = da.bone;
att.position = glm::vec3(da.position[0], da.position[1], da.position[2]);
model.attachments.push_back(att);
}
core::Logger::getInstance().debug(" Attachments: ", model.attachments.size());
}
// Read attachment lookup
if (header.nAttachmentLookup > 0 && header.ofsAttachmentLookup > 0) {
model.attachmentLookup = readArray<uint16_t>(m2Data, header.ofsAttachmentLookup, header.nAttachmentLookup);
}
core::Logger::getInstance().debug("M2 model loaded: ", model.name);
return model;
}
bool M2Loader::loadSkin(const std::vector<uint8_t>& skinData, M2Model& model) {
if (skinData.size() < sizeof(M2SkinHeader)) {
core::Logger::getInstance().error("Skin data too small");
return false;
}
// Read skin header
M2SkinHeader header;
std::memcpy(&header, skinData.data(), sizeof(M2SkinHeader));
// Verify magic
if (std::strncmp(header.magic, "SKIN", 4) != 0) {
core::Logger::getInstance().error("Invalid skin magic: expected SKIN");
return false;
}
core::Logger::getInstance().debug("Loading M2 skin file");
// Read vertex lookup table (maps skin-local indices to global vertex indices)
std::vector<uint16_t> vertexLookup;
if (header.nIndices > 0 && header.ofsIndices > 0) {
vertexLookup = readArray<uint16_t>(skinData, header.ofsIndices, header.nIndices);
}
// Read triangle indices (indices into the vertex lookup table)
std::vector<uint16_t> triangles;
if (header.nTriangles > 0 && header.ofsTriangles > 0) {
triangles = readArray<uint16_t>(skinData, header.ofsTriangles, header.nTriangles);
}
// Resolve two-level indirection: triangle index -> lookup table -> global vertex
model.indices.clear();
model.indices.reserve(triangles.size());
uint32_t outOfBounds = 0;
for (uint16_t triIdx : triangles) {
if (triIdx < vertexLookup.size()) {
uint16_t globalIdx = vertexLookup[triIdx];
if (globalIdx < model.vertices.size()) {
model.indices.push_back(globalIdx);
} else {
model.indices.push_back(0);
outOfBounds++;
}
} else {
model.indices.push_back(0);
outOfBounds++;
}
}
core::Logger::getInstance().debug(" Resolved ", model.indices.size(), " final indices");
if (outOfBounds > 0) {
core::Logger::getInstance().warning(" ", outOfBounds, " out-of-bounds indices clamped to 0");
}
// Read submeshes (proper vertex/index ranges)
std::vector<M2SkinSubmesh> submeshes;
if (header.nSubmeshes > 0 && header.ofsSubmeshes > 0) {
submeshes = readArray<M2SkinSubmesh>(skinData, header.ofsSubmeshes, header.nSubmeshes);
core::Logger::getInstance().debug(" Submeshes: ", submeshes.size());
for (size_t i = 0; i < submeshes.size(); i++) {
const auto& sm = submeshes[i];
core::Logger::getInstance().info(" SkinSection[", i, "]: id=", sm.id,
" level=", sm.level,
" vtxStart=", sm.vertexStart, " vtxCount=", sm.vertexCount,
" idxStart=", sm.indexStart, " idxCount=", sm.indexCount,
" boneCount=", sm.boneCount, " boneStart=", sm.boneStart);
}
}
// Read batches with proper submesh references
if (header.nBatches > 0 && header.ofsBatches > 0) {
auto diskBatches = readArray<M2BatchDisk>(skinData, header.ofsBatches, header.nBatches);
model.batches.clear();
model.batches.reserve(diskBatches.size());
for (size_t i = 0; i < diskBatches.size(); i++) {
const auto& db = diskBatches[i];
M2Batch batch;
batch.flags = db.flags;
batch.priorityPlane = db.priorityPlane;
batch.shader = db.shader;
batch.skinSectionIndex = db.skinSectionIndex;
batch.colorIndex = db.colorIndex;
batch.materialIndex = db.materialIndex;
batch.materialLayer = db.materialLayer;
batch.textureCount = db.textureCount;
batch.textureIndex = db.textureComboIndex;
batch.textureUnit = db.textureCoordIndex;
batch.transparencyIndex = db.textureWeightIndex;
batch.textureAnimIndex = db.textureTransformIndex;
// Look up proper vertex/index ranges from submesh
if (db.skinSectionIndex < submeshes.size()) {
const auto& sm = submeshes[db.skinSectionIndex];
batch.indexStart = sm.indexStart;
batch.indexCount = sm.indexCount;
batch.vertexStart = sm.vertexStart;
batch.vertexCount = sm.vertexCount;
batch.submeshId = sm.id;
batch.submeshLevel = sm.level;
} else {
// Fallback: render entire model as one batch
batch.indexStart = 0;
batch.indexCount = model.indices.size();
batch.vertexStart = 0;
batch.vertexCount = model.vertices.size();
}
model.batches.push_back(batch);
}
core::Logger::getInstance().debug(" Batches: ", model.batches.size());
}
return true;
}
void M2Loader::loadAnimFile(const std::vector<uint8_t>& m2Data,
const std::vector<uint8_t>& animData,
uint32_t sequenceIndex,
M2Model& model) {
if (m2Data.size() < sizeof(M2Header) || animData.empty()) return;
M2Header header;
std::memcpy(&header, m2Data.data(), sizeof(M2Header));
if (header.nBones == 0 || header.ofsBones == 0) return;
if (sequenceIndex >= model.sequences.size()) return;
int patchedTracks = 0;
for (uint32_t boneIdx = 0; boneIdx < header.nBones && boneIdx < model.bones.size(); boneIdx++) {
uint32_t boneOffset = header.ofsBones + boneIdx * sizeof(M2BoneDisk);
if (boneOffset + sizeof(M2BoneDisk) > m2Data.size()) continue;
M2BoneDisk db = readValue<M2BoneDisk>(m2Data, boneOffset);
auto& bone = model.bones[boneIdx];
// Helper to patch one track for this sequence index
auto patchTrack = [&](const M2TrackDisk& disk, M2AnimationTrack& track, TrackType type) {
if (disk.nTimestamps == 0 || disk.nKeys == 0) return;
if (sequenceIndex >= disk.nTimestamps) return;
// Ensure track.sequences is large enough
if (track.sequences.size() <= sequenceIndex) {
track.sequences.resize(sequenceIndex + 1);
}
auto& seqKeys = track.sequences[sequenceIndex];
// Already has data (loaded from main M2 file)
if (!seqKeys.timestamps.empty()) return;
// Read sub-array header for this sequence from the M2 file
uint32_t tsHeaderOfs = disk.ofsTimestamps + sequenceIndex * 8;
uint32_t keyHeaderOfs = disk.ofsKeys + sequenceIndex * 8;
if (tsHeaderOfs + 8 > m2Data.size() || keyHeaderOfs + 8 > m2Data.size()) return;
uint32_t tsCount = readValue<uint32_t>(m2Data, tsHeaderOfs);
uint32_t tsOffset = readValue<uint32_t>(m2Data, tsHeaderOfs + 4);
uint32_t keyCount = readValue<uint32_t>(m2Data, keyHeaderOfs);
uint32_t keyOffset = readValue<uint32_t>(m2Data, keyHeaderOfs + 4);
if (tsCount == 0 || keyCount == 0) return;
// These offsets point into the .anim file data
if (tsOffset + tsCount * sizeof(uint32_t) > animData.size()) return;
size_t keyElementSize = (type == TrackType::VEC3) ? sizeof(float) * 3 : sizeof(int16_t) * 4;
if (keyOffset + keyCount * keyElementSize > animData.size()) return;
// Read timestamps from .anim data
auto timestamps = readArray<uint32_t>(animData, tsOffset, tsCount);
seqKeys.timestamps = std::move(timestamps);
// Read key values from .anim data
if (type == TrackType::VEC3) {
struct Vec3Disk { float x, y, z; };
auto values = readArray<Vec3Disk>(animData, keyOffset, keyCount);
seqKeys.vec3Values.reserve(values.size());
for (const auto& v : values) {
seqKeys.vec3Values.emplace_back(v.x, v.y, v.z);
}
} else {
auto compressed = readArray<CompressedQuat>(animData, keyOffset, keyCount);
seqKeys.quatValues.reserve(compressed.size());
for (const auto& cq : compressed) {
float fx = (cq.x < 0) ? (cq.x + 32768) / 32767.0f : (cq.x - 32767) / 32767.0f;
float fy = (cq.y < 0) ? (cq.y + 32768) / 32767.0f : (cq.y - 32767) / 32767.0f;
float fz = (cq.z < 0) ? (cq.z + 32768) / 32767.0f : (cq.z - 32767) / 32767.0f;
float fw = (cq.w < 0) ? (cq.w + 32768) / 32767.0f : (cq.w - 32767) / 32767.0f;
glm::quat q(fw, fx, fy, fz);
float len = glm::length(q);
if (len > 0.001f) {
q = q / len;
} else {
q = glm::quat(1.0f, 0.0f, 0.0f, 0.0f);
}
seqKeys.quatValues.push_back(q);
}
}
patchedTracks++;
};
patchTrack(db.translation, bone.translation, TrackType::VEC3);
patchTrack(db.rotation, bone.rotation, TrackType::QUAT_COMPRESSED);
patchTrack(db.scale, bone.scale, TrackType::VEC3);
}
core::Logger::getInstance().info("Loaded .anim for sequence ", sequenceIndex,
" (id=", model.sequences[sequenceIndex].id, "): patched ", patchedTracks, " bone tracks");
}
} // namespace pipeline
} // namespace wowee

View file

@ -0,0 +1,358 @@
#include "pipeline/mpq_manager.hpp"
#include "core/logger.hpp"
#include <algorithm>
#include <filesystem>
#include <fstream>
#include <sstream>
#ifdef HAVE_STORMLIB
#include <StormLib.h>
#endif
// Define HANDLE and INVALID_HANDLE_VALUE for both cases
#ifndef HAVE_STORMLIB
typedef void* HANDLE;
#endif
#ifndef INVALID_HANDLE_VALUE
#define INVALID_HANDLE_VALUE ((HANDLE)(long long)-1)
#endif
namespace wowee {
namespace pipeline {
MPQManager::MPQManager() = default;
MPQManager::~MPQManager() {
shutdown();
}
bool MPQManager::initialize(const std::string& dataPath_) {
if (initialized) {
LOG_WARNING("MPQManager already initialized");
return true;
}
dataPath = dataPath_;
LOG_INFO("Initializing MPQ manager with data path: ", dataPath);
// Check if data directory exists
if (!std::filesystem::exists(dataPath)) {
LOG_ERROR("Data directory does not exist: ", dataPath);
return false;
}
#ifdef HAVE_STORMLIB
// Load base archives (in order of priority)
std::vector<std::string> baseArchives = {
"common.MPQ",
"common-2.MPQ",
"expansion.MPQ",
"lichking.MPQ",
};
for (const auto& archive : baseArchives) {
std::string fullPath = dataPath + "/" + archive;
if (std::filesystem::exists(fullPath)) {
loadArchive(fullPath, 100); // Base archives have priority 100
} else {
LOG_DEBUG("Base archive not found (optional): ", archive);
}
}
// Load patch archives (highest priority)
loadPatchArchives();
// Load locale archives
loadLocaleArchives("enUS"); // TODO: Make configurable
if (archives.empty()) {
LOG_WARNING("No MPQ archives loaded - will use loose file fallback");
} else {
LOG_INFO("MPQ manager initialized with ", archives.size(), " archives");
}
#else
LOG_WARNING("StormLib not available - using loose file fallback only");
#endif
initialized = true;
return true;
}
void MPQManager::shutdown() {
if (!initialized) {
return;
}
#ifdef HAVE_STORMLIB
LOG_INFO("Shutting down MPQ manager");
for (auto& entry : archives) {
if (entry.handle != INVALID_HANDLE_VALUE) {
SFileCloseArchive(entry.handle);
}
}
#endif
archives.clear();
archiveNames.clear();
initialized = false;
}
bool MPQManager::loadArchive(const std::string& path, int priority) {
#ifndef HAVE_STORMLIB
LOG_ERROR("Cannot load archive - StormLib not available");
return false;
#endif
#ifdef HAVE_STORMLIB
// Check if file exists
if (!std::filesystem::exists(path)) {
LOG_ERROR("Archive file not found: ", path);
return false;
}
HANDLE handle = INVALID_HANDLE_VALUE;
if (!SFileOpenArchive(path.c_str(), 0, 0, &handle)) {
LOG_ERROR("Failed to open MPQ archive: ", path);
return false;
}
ArchiveEntry entry;
entry.handle = handle;
entry.path = path;
entry.priority = priority;
archives.push_back(entry);
archiveNames.push_back(path);
// Sort archives by priority (highest first)
std::sort(archives.begin(), archives.end(),
[](const ArchiveEntry& a, const ArchiveEntry& b) {
return a.priority > b.priority;
});
LOG_INFO("Loaded MPQ archive: ", path, " (priority ", priority, ")");
return true;
#endif
return false;
}
bool MPQManager::fileExists(const std::string& filename) const {
#ifdef HAVE_STORMLIB
// Check MPQ archives first if available
if (!archives.empty()) {
HANDLE archive = findFileArchive(filename);
if (archive != INVALID_HANDLE_VALUE) {
return true;
}
}
#endif
// Fall back to checking for loose file
std::string loosePath = filename;
std::replace(loosePath.begin(), loosePath.end(), '\\', '/');
std::string fullPath = dataPath + "/" + loosePath;
return std::filesystem::exists(fullPath);
}
std::vector<uint8_t> MPQManager::readFile(const std::string& filename) const {
#ifdef HAVE_STORMLIB
// Try MPQ archives first if available
if (!archives.empty()) {
HANDLE archive = findFileArchive(filename);
if (archive != INVALID_HANDLE_VALUE) {
// Open the file
HANDLE file = INVALID_HANDLE_VALUE;
if (SFileOpenFileEx(archive, filename.c_str(), 0, &file)) {
// Get file size
DWORD fileSize = SFileGetFileSize(file, nullptr);
if (fileSize > 0 && fileSize != SFILE_INVALID_SIZE) {
// Read file data
std::vector<uint8_t> data(fileSize);
DWORD bytesRead = 0;
if (SFileReadFile(file, data.data(), fileSize, &bytesRead, nullptr)) {
SFileCloseFile(file);
LOG_DEBUG("Read file from MPQ: ", filename, " (", bytesRead, " bytes)");
return data;
}
}
SFileCloseFile(file);
}
}
}
#endif
// Fall back to loose file loading
// Convert WoW path (backslashes) to filesystem path (forward slashes)
std::string loosePath = filename;
std::replace(loosePath.begin(), loosePath.end(), '\\', '/');
// Try with original case
std::string fullPath = dataPath + "/" + loosePath;
if (std::filesystem::exists(fullPath)) {
std::ifstream file(fullPath, std::ios::binary | std::ios::ate);
if (file.is_open()) {
size_t size = file.tellg();
file.seekg(0, std::ios::beg);
std::vector<uint8_t> data(size);
file.read(reinterpret_cast<char*>(data.data()), size);
LOG_DEBUG("Read loose file: ", loosePath, " (", size, " bytes)");
return data;
}
}
// Try case-insensitive search (common for Linux)
std::filesystem::path searchPath = dataPath;
std::vector<std::string> pathComponents;
std::istringstream iss(loosePath);
std::string component;
while (std::getline(iss, component, '/')) {
if (!component.empty()) {
pathComponents.push_back(component);
}
}
// Try to find file with case-insensitive matching
for (const auto& comp : pathComponents) {
bool found = false;
if (std::filesystem::exists(searchPath) && std::filesystem::is_directory(searchPath)) {
for (const auto& entry : std::filesystem::directory_iterator(searchPath)) {
std::string entryName = entry.path().filename().string();
// Case-insensitive comparison
if (std::equal(comp.begin(), comp.end(), entryName.begin(), entryName.end(),
[](char a, char b) { return std::tolower(a) == std::tolower(b); })) {
searchPath = entry.path();
found = true;
break;
}
}
}
if (!found) {
LOG_WARNING("File not found: ", filename);
return std::vector<uint8_t>();
}
}
// Try to read the found file
if (std::filesystem::exists(searchPath) && std::filesystem::is_regular_file(searchPath)) {
std::ifstream file(searchPath, std::ios::binary | std::ios::ate);
if (file.is_open()) {
size_t size = file.tellg();
file.seekg(0, std::ios::beg);
std::vector<uint8_t> data(size);
file.read(reinterpret_cast<char*>(data.data()), size);
LOG_DEBUG("Read loose file (case-insensitive): ", searchPath.string(), " (", size, " bytes)");
return data;
}
}
LOG_WARNING("File not found: ", filename);
return std::vector<uint8_t>();
}
uint32_t MPQManager::getFileSize(const std::string& filename) const {
#ifndef HAVE_STORMLIB
return 0;
#endif
#ifdef HAVE_STORMLIB
HANDLE archive = findFileArchive(filename);
if (archive == INVALID_HANDLE_VALUE) {
return 0;
}
HANDLE file = INVALID_HANDLE_VALUE;
if (!SFileOpenFileEx(archive, filename.c_str(), 0, &file)) {
return 0;
}
DWORD fileSize = SFileGetFileSize(file, nullptr);
SFileCloseFile(file);
return (fileSize == SFILE_INVALID_SIZE) ? 0 : fileSize;
#endif
return 0;
}
HANDLE MPQManager::findFileArchive(const std::string& filename) const {
#ifndef HAVE_STORMLIB
return INVALID_HANDLE_VALUE;
#endif
#ifdef HAVE_STORMLIB
// Search archives in priority order (already sorted)
for (const auto& entry : archives) {
if (SFileHasFile(entry.handle, filename.c_str())) {
return entry.handle;
}
}
#endif
return INVALID_HANDLE_VALUE;
}
bool MPQManager::loadPatchArchives() {
#ifndef HAVE_STORMLIB
return false;
#endif
// WoW 3.3.5a patch archives (in order of priority, highest first)
std::vector<std::pair<std::string, int>> patchArchives = {
{"patch-5.MPQ", 500},
{"patch-4.MPQ", 400},
{"patch-3.MPQ", 300},
{"patch-2.MPQ", 200},
{"patch.MPQ", 150},
};
int loadedPatches = 0;
for (const auto& [archive, priority] : patchArchives) {
std::string fullPath = dataPath + "/" + archive;
if (std::filesystem::exists(fullPath)) {
if (loadArchive(fullPath, priority)) {
loadedPatches++;
}
}
}
LOG_INFO("Loaded ", loadedPatches, " patch archives");
return loadedPatches > 0;
}
bool MPQManager::loadLocaleArchives(const std::string& locale) {
#ifndef HAVE_STORMLIB
return false;
#endif
std::string localePath = dataPath + "/" + locale;
if (!std::filesystem::exists(localePath)) {
LOG_WARNING("Locale directory not found: ", localePath);
return false;
}
// Locale-specific archives
std::vector<std::pair<std::string, int>> localeArchives = {
{"locale-" + locale + ".MPQ", 250},
{"patch-" + locale + ".MPQ", 450},
{"patch-" + locale + "-2.MPQ", 460},
{"patch-" + locale + "-3.MPQ", 470},
};
int loadedLocale = 0;
for (const auto& [archive, priority] : localeArchives) {
std::string fullPath = localePath + "/" + archive;
if (std::filesystem::exists(fullPath)) {
if (loadArchive(fullPath, priority)) {
loadedLocale++;
}
}
}
LOG_INFO("Loaded ", loadedLocale, " locale archives for ", locale);
return loadedLocale > 0;
}
} // namespace pipeline
} // namespace wowee

View file

@ -0,0 +1,345 @@
#include "pipeline/terrain_mesh.hpp"
#include "core/logger.hpp"
#include <cmath>
namespace wowee {
namespace pipeline {
TerrainMesh TerrainMeshGenerator::generate(const ADTTerrain& terrain) {
TerrainMesh mesh;
if (!terrain.isLoaded()) {
LOG_WARNING("Attempting to generate mesh from unloaded terrain");
return mesh;
}
LOG_INFO("Generating terrain mesh for ADT...");
// Copy texture list
mesh.textures = terrain.textures;
// Generate mesh for each chunk
int validCount = 0;
bool loggedFirstChunk = false;
for (int y = 0; y < 16; y++) {
for (int x = 0; x < 16; x++) {
const MapChunk& chunk = terrain.getChunk(x, y);
if (chunk.hasHeightMap()) {
mesh.getChunk(x, y) = generateChunkMesh(chunk, x, y, terrain.coord.x, terrain.coord.y);
validCount++;
// Debug: log first chunk world position
if (!loggedFirstChunk) {
loggedFirstChunk = true;
LOG_DEBUG("First terrain chunk world pos: (", chunk.position[0], ", ",
chunk.position[1], ", ", chunk.position[2], ")");
}
}
}
}
mesh.validChunkCount = validCount;
LOG_INFO("Generated ", validCount, " terrain chunk meshes");
return mesh;
}
ChunkMesh TerrainMeshGenerator::generateChunkMesh(const MapChunk& chunk, int chunkX, int chunkY, int tileX, int tileY) {
ChunkMesh mesh;
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];
// Generate vertices from heightmap (pass chunk grid indices and tile coords)
mesh.vertices = generateVertices(chunk, chunkX, chunkY, tileX, tileY);
// Generate triangle indices (checks for holes)
mesh.indices = generateIndices(chunk);
// Debug: verify mesh integrity (one-time)
static bool debugLogged = false;
if (!debugLogged && chunkX == 0 && chunkY == 0) {
debugLogged = true;
LOG_INFO("Terrain mesh debug: ", mesh.vertices.size(), " vertices, ",
mesh.indices.size(), " indices (", mesh.indices.size() / 3, " triangles)");
// Verify all indices are in bounds
int maxIndex = 0;
int minIndex = 9999;
for (auto idx : mesh.indices) {
if (static_cast<int>(idx) > maxIndex) maxIndex = idx;
if (static_cast<int>(idx) < minIndex) minIndex = idx;
}
LOG_INFO("Index range: [", minIndex, ", ", maxIndex, "] (expected [0, 144])");
if (maxIndex >= static_cast<int>(mesh.vertices.size())) {
LOG_ERROR("INDEX OUT OF BOUNDS! Max index ", maxIndex, " >= vertex count ", mesh.vertices.size());
}
// Check for invalid vertex positions
int invalidCount = 0;
for (size_t i = 0; i < mesh.vertices.size(); i++) {
const auto& v = mesh.vertices[i];
if (!std::isfinite(v.position[0]) || !std::isfinite(v.position[1]) || !std::isfinite(v.position[2])) {
invalidCount++;
}
}
if (invalidCount > 0) {
LOG_ERROR("Found ", invalidCount, " vertices with invalid positions!");
}
}
// Copy texture layers
for (size_t layerIdx = 0; layerIdx < chunk.layers.size(); layerIdx++) {
const auto& layer = chunk.layers[layerIdx];
ChunkMesh::LayerInfo layerInfo;
layerInfo.textureId = layer.textureId;
layerInfo.flags = layer.flags;
// Extract alpha data for this layer if it has alpha
if (layer.useAlpha() && layer.offsetMCAL < chunk.alphaMap.size()) {
size_t offset = layer.offsetMCAL;
// Compute actual per-layer size from next layer's offset (not total remaining)
size_t layerSize;
bool foundNext = false;
for (size_t j = layerIdx + 1; j < chunk.layers.size(); j++) {
if (chunk.layers[j].useAlpha()) {
layerSize = chunk.layers[j].offsetMCAL - offset;
foundNext = true;
break;
}
}
if (!foundNext) {
layerSize = chunk.alphaMap.size() - offset;
}
if (layer.compressedAlpha()) {
// Decompress RLE-compressed alpha map to 64x64 = 4096 bytes
layerInfo.alphaData.resize(4096, 0);
size_t readPos = offset;
size_t writePos = 0;
while (writePos < 4096 && readPos < chunk.alphaMap.size()) {
uint8_t cmd = chunk.alphaMap[readPos++];
bool fill = (cmd & 0x80) != 0;
int count = (cmd & 0x7F) + 1;
if (fill) {
if (readPos < chunk.alphaMap.size()) {
uint8_t val = chunk.alphaMap[readPos++];
for (int i = 0; i < count && writePos < 4096; i++) {
layerInfo.alphaData[writePos++] = val;
}
}
} else {
for (int i = 0; i < count && writePos < 4096 && readPos < chunk.alphaMap.size(); i++) {
layerInfo.alphaData[writePos++] = chunk.alphaMap[readPos++];
}
}
}
} else if (layerSize >= 4096) {
// Big alpha: 64x64 at 8-bit = 4096 bytes
layerInfo.alphaData.resize(4096);
std::copy(chunk.alphaMap.begin() + offset,
chunk.alphaMap.begin() + offset + 4096,
layerInfo.alphaData.begin());
} else if (layerSize >= 2048) {
// Non-big alpha: 2048 bytes = 4-bit per texel, 64x64
// Each byte: low nibble = first texel, high nibble = second texel
// Scale 0-15 to 0-255 (multiply by 17)
layerInfo.alphaData.resize(4096);
for (size_t i = 0; i < 2048; i++) {
uint8_t byte = chunk.alphaMap[offset + i];
layerInfo.alphaData[i * 2] = (byte & 0x0F) * 17;
layerInfo.alphaData[i * 2 + 1] = (byte >> 4) * 17;
}
}
}
mesh.layers.push_back(layerInfo);
}
return mesh;
}
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
const HeightMap& heightMap = chunk.heightMap;
// 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];
for (int index = 0; index < 145; index++) {
int y = index / 17; // Row (0-8)
int x = index % 17; // Column (0-16)
// Columns 9-16 are offset by 0.5 units (wowee exact logic)
float offsetX = static_cast<float>(x);
float offsetY = static_cast<float>(y);
if (x > 8) {
offsetY += 0.5f;
offsetX -= 8.5f;
}
TerrainVertex vertex;
// Position - match wowee.js coordinate layout (swap X/Y and negate)
// wowee.js: X = -(y * unitSize), Y = -(x * unitSize)
vertex.position[0] = chunkBaseX - (offsetY * unitSize);
vertex.position[1] = chunkBaseY - (offsetX * unitSize);
vertex.position[2] = chunk.position[2] + heightMap.heights[index];
// Normal
if (index * 3 + 2 < static_cast<int>(chunk.normals.size())) {
decompressNormal(&chunk.normals[index * 3], vertex.normal);
} else {
// Default up normal
vertex.normal[0] = 0.0f;
vertex.normal[1] = 0.0f;
vertex.normal[2] = 1.0f;
}
// Texture coordinates (0-1 per chunk, tiles with GL_REPEAT)
vertex.texCoord[0] = offsetX / 8.0f;
vertex.texCoord[1] = offsetY / 8.0f;
// Layer UV for alpha map sampling (0-1 range per chunk)
vertex.layerUV[0] = offsetX / 8.0f;
vertex.layerUV[1] = offsetY / 8.0f;
vertices.push_back(vertex);
}
return vertices;
}
std::vector<TerrainIndex> TerrainMeshGenerator::generateIndices(const MapChunk& chunk) {
std::vector<TerrainIndex> indices;
indices.reserve(768); // 8x8 quads * 4 triangles * 3 indices = 768
// Generate indices based on 9x17 grid layout (matching wowee.js)
// Each quad uses a center vertex with 4 surrounding vertices
// Index offsets from center: -9, -8, +9, +8
int holesSkipped = 0;
for (int y = 0; y < 8; y++) {
for (int x = 0; x < 8; x++) {
// Skip quads that are marked as holes (cave entrances, etc.)
if (chunk.isHole(y, x)) {
holesSkipped++;
continue;
}
// Center vertex index in the 9x17 grid
int center = 9 + y * 17 + x;
// Four triangles per quad
// Using CCW winding when viewed from +Z (top-down)
int tl = center - 9; // top-left outer
int tr = center - 8; // top-right outer
int bl = center + 8; // bottom-left outer
int br = center + 9; // bottom-right outer
// Triangle 1: top (center, tl, tr)
indices.push_back(center);
indices.push_back(tl);
indices.push_back(tr);
// Triangle 2: right (center, tr, br)
indices.push_back(center);
indices.push_back(tr);
indices.push_back(br);
// Triangle 3: bottom (center, br, bl)
indices.push_back(center);
indices.push_back(br);
indices.push_back(bl);
// Triangle 4: left (center, bl, tl)
indices.push_back(center);
indices.push_back(bl);
indices.push_back(tl);
}
}
// Debug: log if any holes were skipped (one-time per session)
static bool holesLogged = false;
if (!holesLogged && holesSkipped > 0) {
holesLogged = true;
LOG_INFO("Terrain holes: skipped ", holesSkipped, " quads due to hole mask (holes=0x",
std::hex, chunk.holes, std::dec, ")");
}
return indices;
}
void TerrainMeshGenerator::calculateTexCoords(TerrainVertex& vertex, int x, int y) {
// Base texture coordinates (0-1 range across chunk)
vertex.texCoord[0] = x / 16.0f;
vertex.texCoord[1] = y / 16.0f;
// Layer UVs (same as base for now)
vertex.layerUV[0] = vertex.texCoord[0];
vertex.layerUV[1] = vertex.texCoord[1];
}
void TerrainMeshGenerator::decompressNormal(const int8_t* compressedNormal, float* normal) {
// WoW stores normals as signed bytes (-127 to 127)
// Convert to float and normalize
float x = compressedNormal[0] / 127.0f;
float y = compressedNormal[1] / 127.0f;
float z = compressedNormal[2] / 127.0f;
// Normalize
float length = std::sqrt(x * x + y * y + z * z);
if (length > 0.0001f) {
normal[0] = x / length;
normal[1] = y / length;
normal[2] = z / length;
} else {
// Default up normal if degenerate
normal[0] = 0.0f;
normal[1] = 0.0f;
normal[2] = 1.0f;
}
}
int TerrainMeshGenerator::getVertexIndex(int x, int y) {
// Convert virtual grid position (0-16) to actual vertex index (0-144)
// Outer vertices (even positions): 0-80 (9x9 grid)
// Inner vertices (odd positions): 81-144 (8x8 grid)
bool isOuter = (y % 2 == 0) && (x % 2 == 0);
bool isInner = (y % 2 == 1) && (x % 2 == 1);
if (isOuter) {
int gridX = x / 2;
int gridY = y / 2;
return gridY * 9 + gridX; // 0-80
} else if (isInner) {
int gridX = (x - 1) / 2;
int gridY = (y - 1) / 2;
return 81 + gridY * 8 + gridX; // 81-144
}
return -1; // Invalid position
}
} // namespace pipeline
} // namespace wowee

556
src/pipeline/wmo_loader.cpp Normal file
View file

@ -0,0 +1,556 @@
#include "pipeline/wmo_loader.hpp"
#include "core/logger.hpp"
#include <cstring>
#include <glm/gtc/quaternion.hpp>
namespace wowee {
namespace pipeline {
namespace {
// WMO chunk identifiers
constexpr uint32_t MVER = 0x4D564552; // Version
constexpr uint32_t MOHD = 0x4D4F4844; // Header
constexpr uint32_t MOTX = 0x4D4F5458; // Textures
constexpr uint32_t MOMT = 0x4D4F4D54; // Materials
constexpr uint32_t MOGN = 0x4D4F474E; // Group names
constexpr uint32_t MOGI = 0x4D4F4749; // Group info
constexpr uint32_t MOLT = 0x4D4F4C54; // Lights
constexpr uint32_t MODN = 0x4D4F444E; // Doodad names
constexpr uint32_t MODD = 0x4D4F4444; // Doodad definitions
constexpr uint32_t MODS = 0x4D4F4453; // Doodad sets
constexpr uint32_t MOPV = 0x4D4F5056; // Portal vertices
constexpr uint32_t MOPT = 0x4D4F5054; // Portal info
constexpr uint32_t MOPR = 0x4D4F5052; // Portal references
constexpr uint32_t MFOG = 0x4D464F47; // Fog
// WMO group chunk identifiers
constexpr uint32_t MOGP = 0x4D4F4750; // Group header
constexpr uint32_t MOVV = 0x4D4F5656; // Vertices
constexpr uint32_t MOVI = 0x4D4F5649; // Indices
constexpr uint32_t MOBA = 0x4D4F4241; // Batches
constexpr uint32_t MOCV = 0x4D4F4356; // Vertex colors
constexpr uint32_t MONR = 0x4D4F4E52; // Normals
constexpr uint32_t MOTV = 0x4D4F5456; // Texture coords
// Read utilities
template<typename T>
T read(const std::vector<uint8_t>& data, uint32_t& offset) {
if (offset + sizeof(T) > data.size()) {
return T{};
}
T value;
std::memcpy(&value, &data[offset], sizeof(T));
offset += sizeof(T);
return value;
}
template<typename T>
std::vector<T> readArray(const std::vector<uint8_t>& data, uint32_t offset, uint32_t count) {
std::vector<T> result;
if (offset + count * sizeof(T) > data.size()) {
return result;
}
result.resize(count);
std::memcpy(result.data(), &data[offset], count * sizeof(T));
return result;
}
std::string readString(const std::vector<uint8_t>& data, uint32_t offset) {
std::string result;
while (offset < data.size() && data[offset] != 0) {
result += static_cast<char>(data[offset++]);
}
return result;
}
} // anonymous namespace
WMOModel WMOLoader::load(const std::vector<uint8_t>& wmoData) {
WMOModel model;
if (wmoData.size() < 8) {
core::Logger::getInstance().error("WMO data too small");
return model;
}
core::Logger::getInstance().info("Loading WMO model...");
uint32_t offset = 0;
// Parse chunks
while (offset + 8 <= wmoData.size()) {
uint32_t chunkId = read<uint32_t>(wmoData, offset);
uint32_t chunkSize = read<uint32_t>(wmoData, offset);
if (offset + chunkSize > wmoData.size()) {
core::Logger::getInstance().warning("Chunk extends beyond file");
break;
}
uint32_t chunkStart = offset;
uint32_t chunkEnd = offset + chunkSize;
switch (chunkId) {
case MVER: {
model.version = read<uint32_t>(wmoData, offset);
core::Logger::getInstance().info("WMO version: ", model.version);
break;
}
case MOHD: {
// Header
model.nGroups = read<uint32_t>(wmoData, offset);
model.nPortals = read<uint32_t>(wmoData, offset);
model.nLights = read<uint32_t>(wmoData, offset);
model.nDoodadNames = read<uint32_t>(wmoData, offset);
model.nDoodadDefs = read<uint32_t>(wmoData, offset);
model.nDoodadSets = read<uint32_t>(wmoData, offset);
[[maybe_unused]] uint32_t ambColor = read<uint32_t>(wmoData, offset); // Ambient color
[[maybe_unused]] uint32_t wmoID = read<uint32_t>(wmoData, offset);
model.boundingBoxMin.x = read<float>(wmoData, offset);
model.boundingBoxMin.y = read<float>(wmoData, offset);
model.boundingBoxMin.z = read<float>(wmoData, offset);
model.boundingBoxMax.x = read<float>(wmoData, offset);
model.boundingBoxMax.y = read<float>(wmoData, offset);
model.boundingBoxMax.z = read<float>(wmoData, offset);
core::Logger::getInstance().info("WMO groups: ", model.nGroups);
break;
}
case MOTX: {
// Textures - raw block of null-terminated strings
// Material texture1/texture2/texture3 are byte offsets into this chunk.
// We must map every offset to its texture index.
uint32_t texOffset = chunkStart;
uint32_t texIndex = 0;
core::Logger::getInstance().info("MOTX chunk: ", chunkSize, " bytes");
while (texOffset < chunkEnd) {
uint32_t relativeOffset = texOffset - chunkStart;
std::string texName = readString(wmoData, texOffset);
if (texName.empty()) {
// Skip null bytes (empty entries or padding)
texOffset++;
continue;
}
// Store mapping from byte offset to texture index
model.textureOffsetToIndex[relativeOffset] = texIndex;
model.textures.push_back(texName);
core::Logger::getInstance().info(" MOTX texture[", texIndex, "] at offset ", relativeOffset, ": ", texName);
texOffset += texName.length() + 1;
texIndex++;
}
core::Logger::getInstance().info("WMO textures: ", model.textures.size());
break;
}
case MOMT: {
// Materials - dump raw fields to find correct layout
uint32_t nMaterials = chunkSize / 64; // Each material is 64 bytes
for (uint32_t i = 0; i < nMaterials; i++) {
// Read all 16 uint32 fields (64 bytes)
uint32_t fields[16];
for (int j = 0; j < 16; j++) {
fields[j] = read<uint32_t>(wmoData, offset);
}
// SMOMaterial layout (wowdev.wiki):
// 0: flags, 1: shader, 2: blendMode
// 3: texture_1 (MOTX offset)
// 4: sidnColor (emissive), 5: frameSidnColor
// 6: texture_2 (MOTX offset)
// 7: diffColor, 8: ground_type
// 9: texture_3 (MOTX offset)
// 10: color_2, 11: flags2
// 12-15: runtime
WMOMaterial mat;
mat.flags = fields[0];
mat.shader = fields[1];
mat.blendMode = fields[2];
mat.texture1 = fields[3];
mat.color1 = fields[4];
mat.texture2 = fields[6]; // Skip frameSidnColor at [5]
mat.color2 = fields[7];
mat.texture3 = fields[9]; // Skip ground_type at [8]
mat.color3 = fields[10];
model.materials.push_back(mat);
}
core::Logger::getInstance().info("WMO materials: ", model.materials.size());
break;
}
case MOGN: {
// Group names
uint32_t nameOffset = chunkStart;
while (nameOffset < chunkEnd) {
std::string name = readString(wmoData, nameOffset);
if (name.empty()) break;
model.groupNames.push_back(name);
nameOffset += name.length() + 1;
}
core::Logger::getInstance().info("WMO group names: ", model.groupNames.size());
break;
}
case MOGI: {
// Group info
uint32_t nGroupInfo = chunkSize / 32; // Each group info is 32 bytes
for (uint32_t i = 0; i < nGroupInfo; i++) {
WMOGroupInfo info;
info.flags = read<uint32_t>(wmoData, offset);
info.boundingBoxMin.x = read<float>(wmoData, offset);
info.boundingBoxMin.y = read<float>(wmoData, offset);
info.boundingBoxMin.z = read<float>(wmoData, offset);
info.boundingBoxMax.x = read<float>(wmoData, offset);
info.boundingBoxMax.y = read<float>(wmoData, offset);
info.boundingBoxMax.z = read<float>(wmoData, offset);
info.nameOffset = read<int32_t>(wmoData, offset);
model.groupInfo.push_back(info);
}
core::Logger::getInstance().info("WMO group info: ", model.groupInfo.size());
break;
}
case MOLT: {
// Lights
uint32_t nLights = chunkSize / 48; // Approximate size
for (uint32_t i = 0; i < nLights && offset < chunkEnd; i++) {
WMOLight light;
light.type = read<uint32_t>(wmoData, offset);
light.useAttenuation = read<uint8_t>(wmoData, offset);
light.pad[0] = read<uint8_t>(wmoData, offset);
light.pad[1] = read<uint8_t>(wmoData, offset);
light.pad[2] = read<uint8_t>(wmoData, offset);
light.color.r = read<float>(wmoData, offset);
light.color.g = read<float>(wmoData, offset);
light.color.b = read<float>(wmoData, offset);
light.color.a = read<float>(wmoData, offset);
light.position.x = read<float>(wmoData, offset);
light.position.y = read<float>(wmoData, offset);
light.position.z = read<float>(wmoData, offset);
light.intensity = read<float>(wmoData, offset);
light.attenuationStart = read<float>(wmoData, offset);
light.attenuationEnd = read<float>(wmoData, offset);
for (int j = 0; j < 4; j++) {
light.unknown[j] = read<float>(wmoData, offset);
}
model.lights.push_back(light);
}
core::Logger::getInstance().info("WMO lights: ", model.lights.size());
break;
}
case MODN: {
// Doodad names — stored by byte offset into the MODN chunk
// (MODD nameIndex is a byte offset, not a vector index)
uint32_t nameOffset = 0; // Offset relative to chunk start
while (chunkStart + nameOffset < chunkEnd) {
std::string name = readString(wmoData, chunkStart + nameOffset);
if (!name.empty()) {
model.doodadNames[nameOffset] = name;
}
nameOffset += name.length() + 1;
}
core::Logger::getInstance().debug("Loaded ", model.doodadNames.size(), " doodad names");
break;
}
case MODD: {
// Doodad definitions
uint32_t nDoodads = chunkSize / 40; // Each doodad is 40 bytes
for (uint32_t i = 0; i < nDoodads; i++) {
WMODoodad doodad;
// Name index (3 bytes) + flags (1 byte)
uint32_t nameAndFlags = read<uint32_t>(wmoData, offset);
doodad.nameIndex = nameAndFlags & 0x00FFFFFF;
doodad.position.x = read<float>(wmoData, offset);
doodad.position.y = read<float>(wmoData, offset);
doodad.position.z = read<float>(wmoData, offset);
// C4Quaternion in file: x, y, z, w
doodad.rotation.x = read<float>(wmoData, offset);
doodad.rotation.y = read<float>(wmoData, offset);
doodad.rotation.z = read<float>(wmoData, offset);
doodad.rotation.w = read<float>(wmoData, offset);
doodad.scale = read<float>(wmoData, offset);
uint32_t color = read<uint32_t>(wmoData, offset);
doodad.color.b = ((color >> 0) & 0xFF) / 255.0f;
doodad.color.g = ((color >> 8) & 0xFF) / 255.0f;
doodad.color.r = ((color >> 16) & 0xFF) / 255.0f;
doodad.color.a = ((color >> 24) & 0xFF) / 255.0f;
model.doodads.push_back(doodad);
}
core::Logger::getInstance().info("WMO doodads: ", model.doodads.size());
break;
}
case MODS: {
// Doodad sets
uint32_t nSets = chunkSize / 32; // Each set is 32 bytes
for (uint32_t i = 0; i < nSets; i++) {
WMODoodadSet set;
std::memcpy(set.name, &wmoData[offset], 20);
offset += 20;
set.startIndex = read<uint32_t>(wmoData, offset);
set.count = read<uint32_t>(wmoData, offset);
set.padding = read<uint32_t>(wmoData, offset);
model.doodadSets.push_back(set);
}
core::Logger::getInstance().info("WMO doodad sets: ", model.doodadSets.size());
break;
}
case MOPV: {
// Portal vertices
uint32_t nVerts = chunkSize / 12; // Each vertex is 3 floats
for (uint32_t i = 0; i < nVerts; i++) {
glm::vec3 vert;
vert.x = read<float>(wmoData, offset);
vert.y = read<float>(wmoData, offset);
vert.z = read<float>(wmoData, offset);
model.portalVertices.push_back(vert);
}
break;
}
case MOPT: {
// Portal info
uint32_t nPortals = chunkSize / 20; // Each portal reference is 20 bytes
for (uint32_t i = 0; i < nPortals; i++) {
WMOPortal portal;
portal.startVertex = read<uint16_t>(wmoData, offset);
portal.vertexCount = read<uint16_t>(wmoData, offset);
portal.planeIndex = read<uint16_t>(wmoData, offset);
portal.padding = read<uint16_t>(wmoData, offset);
// Skip additional data (12 bytes)
offset += 12;
model.portals.push_back(portal);
}
core::Logger::getInstance().info("WMO portals: ", model.portals.size());
break;
}
default:
// Unknown chunk, skip it
break;
}
offset = chunkEnd;
}
// Initialize groups array
model.groups.resize(model.nGroups);
core::Logger::getInstance().info("WMO model loaded successfully");
return model;
}
bool WMOLoader::loadGroup(const std::vector<uint8_t>& groupData,
WMOModel& model,
uint32_t groupIndex) {
if (groupIndex >= model.groups.size()) {
core::Logger::getInstance().error("Invalid group index: ", groupIndex);
return false;
}
if (groupData.size() < 20) {
core::Logger::getInstance().error("WMO group file too small");
return false;
}
auto& group = model.groups[groupIndex];
group.groupId = groupIndex;
uint32_t offset = 0;
// Parse chunks in group file
while (offset + 8 < groupData.size()) {
uint32_t chunkId = read<uint32_t>(groupData, offset);
uint32_t chunkSize = read<uint32_t>(groupData, offset);
uint32_t chunkEnd = offset + chunkSize;
if (chunkEnd > groupData.size()) {
break;
}
if (chunkId == MVER) {
// Version - skip
}
else if (chunkId == MOGP) {
// Group header - parse sub-chunks
// MOGP header is 68 bytes, followed by sub-chunks
if (chunkSize < 68) {
offset = chunkEnd;
continue;
}
// Read MOGP header
uint32_t mogpOffset = offset;
group.flags = read<uint32_t>(groupData, mogpOffset);
group.boundingBoxMin.x = read<float>(groupData, mogpOffset);
group.boundingBoxMin.y = read<float>(groupData, mogpOffset);
group.boundingBoxMin.z = read<float>(groupData, mogpOffset);
group.boundingBoxMax.x = read<float>(groupData, mogpOffset);
group.boundingBoxMax.y = read<float>(groupData, mogpOffset);
group.boundingBoxMax.z = read<float>(groupData, mogpOffset);
mogpOffset += 4; // nameOffset
group.portalStart = read<uint16_t>(groupData, mogpOffset);
group.portalCount = read<uint16_t>(groupData, mogpOffset);
mogpOffset += 8; // transBatchCount, intBatchCount, extBatchCount, padding
group.fogIndices[0] = read<uint32_t>(groupData, mogpOffset);
group.fogIndices[1] = read<uint32_t>(groupData, mogpOffset);
group.fogIndices[2] = read<uint32_t>(groupData, mogpOffset);
group.fogIndices[3] = read<uint32_t>(groupData, mogpOffset);
group.liquidType = read<uint32_t>(groupData, mogpOffset);
// Skip to end of 68-byte header
mogpOffset = offset + 68;
// Parse sub-chunks within MOGP
while (mogpOffset + 8 < chunkEnd) {
uint32_t subChunkId = read<uint32_t>(groupData, mogpOffset);
uint32_t subChunkSize = read<uint32_t>(groupData, mogpOffset);
uint32_t subChunkEnd = mogpOffset + subChunkSize;
if (subChunkEnd > chunkEnd) {
break;
}
// Debug: log chunk magic as string
char magic[5] = {0};
magic[0] = (subChunkId >> 0) & 0xFF;
magic[1] = (subChunkId >> 8) & 0xFF;
magic[2] = (subChunkId >> 16) & 0xFF;
magic[3] = (subChunkId >> 24) & 0xFF;
static int logCount = 0;
if (logCount < 30) {
core::Logger::getInstance().debug(" WMO sub-chunk: ", magic, " (0x", std::hex, subChunkId, std::dec, ") size=", subChunkSize);
logCount++;
}
if (subChunkId == 0x4D4F5654) { // MOVT - Vertices
uint32_t vertexCount = subChunkSize / 12; // 3 floats per vertex
for (uint32_t i = 0; i < vertexCount; i++) {
WMOVertex vertex;
vertex.position.x = read<float>(groupData, mogpOffset);
vertex.position.y = read<float>(groupData, mogpOffset);
vertex.position.z = read<float>(groupData, mogpOffset);
vertex.normal = glm::vec3(0, 0, 1);
vertex.texCoord = glm::vec2(0, 0);
vertex.color = glm::vec4(1, 1, 1, 1);
group.vertices.push_back(vertex);
}
}
else if (subChunkId == 0x4D4F5649) { // MOVI - Indices
uint32_t indexCount = subChunkSize / 2; // uint16_t per index
for (uint32_t i = 0; i < indexCount; i++) {
group.indices.push_back(read<uint16_t>(groupData, mogpOffset));
}
}
else if (subChunkId == 0x4D4F4E52) { // MONR - Normals
uint32_t normalCount = subChunkSize / 12;
for (uint32_t i = 0; i < normalCount && i < group.vertices.size(); i++) {
group.vertices[i].normal.x = read<float>(groupData, mogpOffset);
group.vertices[i].normal.y = read<float>(groupData, mogpOffset);
group.vertices[i].normal.z = read<float>(groupData, mogpOffset);
}
}
else if (subChunkId == 0x4D4F5456) { // MOTV - Texture coords
// Update texture coords for existing vertices
uint32_t texCoordCount = subChunkSize / 8;
core::Logger::getInstance().info(" MOTV: ", texCoordCount, " tex coords for ", group.vertices.size(), " vertices");
for (uint32_t i = 0; i < texCoordCount && i < group.vertices.size(); i++) {
group.vertices[i].texCoord.x = read<float>(groupData, mogpOffset);
group.vertices[i].texCoord.y = read<float>(groupData, mogpOffset);
}
if (texCoordCount > 0 && !group.vertices.empty()) {
core::Logger::getInstance().debug(" First UV: (", group.vertices[0].texCoord.x, ", ", group.vertices[0].texCoord.y, ")");
}
}
else if (subChunkId == 0x4D4F4356) { // MOCV - Vertex colors
// Update vertex colors
uint32_t colorCount = subChunkSize / 4;
for (uint32_t i = 0; i < colorCount && i < group.vertices.size(); i++) {
uint8_t b = read<uint8_t>(groupData, mogpOffset);
uint8_t g = read<uint8_t>(groupData, mogpOffset);
uint8_t r = read<uint8_t>(groupData, mogpOffset);
uint8_t a = read<uint8_t>(groupData, mogpOffset);
group.vertices[i].color = glm::vec4(r/255.0f, g/255.0f, b/255.0f, a/255.0f);
}
}
else if (subChunkId == 0x4D4F4241) { // MOBA - Batches
// SMOBatch structure (24 bytes):
// - 6 x int16 bounding box (12 bytes)
// - uint32 startIndex (4 bytes)
// - uint16 count (2 bytes)
// - uint16 minIndex (2 bytes)
// - uint16 maxIndex (2 bytes)
// - uint8 flags (1 byte)
// - uint8 material_id (1 byte)
uint32_t batchCount = subChunkSize / 24;
for (uint32_t i = 0; i < batchCount; i++) {
WMOBatch batch;
mogpOffset += 12; // Skip bounding box (6 x int16 = 12 bytes)
batch.startIndex = read<uint32_t>(groupData, mogpOffset);
batch.indexCount = read<uint16_t>(groupData, mogpOffset);
batch.startVertex = read<uint16_t>(groupData, mogpOffset);
batch.lastVertex = read<uint16_t>(groupData, mogpOffset);
batch.flags = read<uint8_t>(groupData, mogpOffset);
batch.materialId = read<uint8_t>(groupData, mogpOffset);
group.batches.push_back(batch);
static int batchLogCount = 0;
if (batchLogCount < 15) {
core::Logger::getInstance().info(" Batch[", i, "]: start=", batch.startIndex,
" count=", batch.indexCount, " verts=[", batch.startVertex, "-",
batch.lastVertex, "] mat=", (int)batch.materialId, " flags=", (int)batch.flags);
batchLogCount++;
}
}
}
mogpOffset = subChunkEnd;
}
}
offset = chunkEnd;
}
// Create a default batch if none were loaded
if (group.batches.empty() && !group.indices.empty()) {
WMOBatch batch;
batch.startIndex = 0;
batch.indexCount = static_cast<uint16_t>(group.indices.size());
batch.materialId = 0;
group.batches.push_back(batch);
}
core::Logger::getInstance().info("WMO group ", groupIndex, " loaded: ",
group.vertices.size(), " vertices, ",
group.indices.size(), " indices, ",
group.batches.size(), " batches");
return !group.vertices.empty() && !group.indices.empty();
}
} // namespace pipeline
} // namespace wowee

56
src/rendering/camera.cpp Normal file
View file

@ -0,0 +1,56 @@
#include "rendering/camera.hpp"
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/constants.hpp>
namespace wowee {
namespace rendering {
Camera::Camera() {
updateViewMatrix();
updateProjectionMatrix();
}
void Camera::updateViewMatrix() {
glm::vec3 front = getForward();
// Use Z-up for WoW coordinate system
viewMatrix = glm::lookAt(position, position + front, glm::vec3(0.0f, 0.0f, 1.0f));
}
void Camera::updateProjectionMatrix() {
projectionMatrix = glm::perspective(glm::radians(fov), aspectRatio, nearPlane, farPlane);
}
glm::vec3 Camera::getForward() const {
// WoW coordinate system: X/Y horizontal, Z vertical
glm::vec3 front;
front.x = cos(glm::radians(yaw)) * cos(glm::radians(pitch));
front.y = sin(glm::radians(yaw)) * cos(glm::radians(pitch));
front.z = sin(glm::radians(pitch));
return glm::normalize(front);
}
glm::vec3 Camera::getRight() const {
// Use Z-up for WoW coordinate system
return glm::normalize(glm::cross(getForward(), glm::vec3(0.0f, 0.0f, 1.0f)));
}
glm::vec3 Camera::getUp() const {
return glm::normalize(glm::cross(getRight(), getForward()));
}
Ray Camera::screenToWorldRay(float screenX, float screenY, float screenW, float screenH) const {
float ndcX = (2.0f * screenX / screenW) - 1.0f;
float ndcY = 1.0f - (2.0f * screenY / screenH);
glm::mat4 invVP = glm::inverse(projectionMatrix * viewMatrix);
glm::vec4 nearPt = invVP * glm::vec4(ndcX, ndcY, -1.0f, 1.0f);
glm::vec4 farPt = invVP * glm::vec4(ndcX, ndcY, 1.0f, 1.0f);
nearPt /= nearPt.w;
farPt /= farPt.w;
return { glm::vec3(nearPt), glm::normalize(glm::vec3(farPt - nearPt)) };
}
} // namespace rendering
} // namespace wowee

View file

@ -0,0 +1,525 @@
#include "rendering/camera_controller.hpp"
#include "rendering/terrain_manager.hpp"
#include "rendering/wmo_renderer.hpp"
#include "rendering/water_renderer.hpp"
#include "game/opcodes.hpp"
#include "core/logger.hpp"
#include <glm/glm.hpp>
#include <imgui.h>
namespace wowee {
namespace rendering {
CameraController::CameraController(Camera* cam) : camera(cam) {
yaw = defaultYaw;
pitch = defaultPitch;
reset();
}
void CameraController::update(float deltaTime) {
if (!enabled || !camera) {
return;
}
auto& input = core::Input::getInstance();
// Don't process keyboard input when UI (e.g. chat box) has focus
bool uiWantsKeyboard = ImGui::GetIO().WantCaptureKeyboard;
// Determine current key states
bool nowForward = !uiWantsKeyboard && !sitting && input.isKeyPressed(SDL_SCANCODE_W);
bool nowBackward = !uiWantsKeyboard && !sitting && input.isKeyPressed(SDL_SCANCODE_S);
bool nowStrafeLeft = !uiWantsKeyboard && !sitting && input.isKeyPressed(SDL_SCANCODE_A);
bool nowStrafeRight = !uiWantsKeyboard && !sitting && input.isKeyPressed(SDL_SCANCODE_D);
bool nowJump = !uiWantsKeyboard && !sitting && input.isKeyPressed(SDL_SCANCODE_SPACE);
// Select physics constants based on mode
float gravity = useWoWSpeed ? WOW_GRAVITY : GRAVITY;
float jumpVel = useWoWSpeed ? WOW_JUMP_VELOCITY : JUMP_VELOCITY;
// Calculate movement speed based on direction and modifiers
float speed;
if (useWoWSpeed) {
// WoW-correct speeds
if (nowBackward && !nowForward) {
speed = WOW_BACK_SPEED;
} else if (!uiWantsKeyboard && (input.isKeyPressed(SDL_SCANCODE_LSHIFT) || input.isKeyPressed(SDL_SCANCODE_RSHIFT))) {
speed = WOW_WALK_SPEED; // Shift = walk in WoW mode
} else {
speed = WOW_RUN_SPEED;
}
} else {
// Exploration mode (original behavior)
speed = movementSpeed;
if (!uiWantsKeyboard && (input.isKeyPressed(SDL_SCANCODE_LSHIFT) || input.isKeyPressed(SDL_SCANCODE_RSHIFT))) {
speed *= sprintMultiplier;
}
if (!uiWantsKeyboard && (input.isKeyPressed(SDL_SCANCODE_LCTRL) || input.isKeyPressed(SDL_SCANCODE_RCTRL))) {
speed *= slowMultiplier;
}
}
// Get camera axes — project forward onto XY plane for walking
glm::vec3 forward3D = camera->getForward();
glm::vec3 forward = glm::normalize(glm::vec3(forward3D.x, forward3D.y, 0.0f));
glm::vec3 right = camera->getRight();
right.z = 0.0f;
if (glm::length(right) > 0.001f) {
right = glm::normalize(right);
}
// Toggle sit with X key (edge-triggered) — only when UI doesn't want keyboard
bool xDown = !uiWantsKeyboard && input.isKeyPressed(SDL_SCANCODE_X);
if (xDown && !xKeyWasDown) {
sitting = !sitting;
}
xKeyWasDown = xDown;
// Calculate horizontal movement vector
glm::vec3 movement(0.0f);
if (nowForward) movement += forward;
if (nowBackward) movement -= forward;
if (nowStrafeLeft) movement -= right;
if (nowStrafeRight) movement += right;
// Stand up if any movement key is pressed while sitting
if (!uiWantsKeyboard && sitting && (input.isKeyPressed(SDL_SCANCODE_W) || input.isKeyPressed(SDL_SCANCODE_S) ||
input.isKeyPressed(SDL_SCANCODE_A) || input.isKeyPressed(SDL_SCANCODE_D) ||
input.isKeyPressed(SDL_SCANCODE_SPACE))) {
sitting = false;
}
// Third-person orbit camera mode
if (thirdPerson && followTarget) {
// Move the follow target (character position) instead of the camera
glm::vec3 targetPos = *followTarget;
// Check for water at current position
std::optional<float> waterH;
if (waterRenderer) {
waterH = waterRenderer->getWaterHeightAt(targetPos.x, targetPos.y);
}
bool inWater = waterH && targetPos.z < *waterH;
if (inWater) {
swimming = true;
// Reduce horizontal speed while swimming
float swimSpeed = speed * SWIM_SPEED_FACTOR;
if (glm::length(movement) > 0.001f) {
movement = glm::normalize(movement);
targetPos += movement * swimSpeed * deltaTime;
}
// Spacebar = swim up (continuous, not a jump)
if (nowJump) {
verticalVelocity = SWIM_BUOYANCY;
} else {
// Gentle sink when not pressing space
verticalVelocity += SWIM_GRAVITY * deltaTime;
if (verticalVelocity < SWIM_SINK_SPEED) {
verticalVelocity = SWIM_SINK_SPEED;
}
}
targetPos.z += verticalVelocity * deltaTime;
// Don't rise above water surface
if (waterH && targetPos.z > *waterH - WATER_SURFACE_OFFSET) {
targetPos.z = *waterH - WATER_SURFACE_OFFSET;
if (verticalVelocity > 0.0f) verticalVelocity = 0.0f;
}
grounded = false;
} else {
swimming = false;
if (glm::length(movement) > 0.001f) {
movement = glm::normalize(movement);
targetPos += movement * speed * deltaTime;
}
// Jump
if (nowJump && grounded) {
verticalVelocity = jumpVel;
grounded = false;
}
// Apply gravity
verticalVelocity += gravity * deltaTime;
targetPos.z += verticalVelocity * deltaTime;
}
// Wall collision for character
if (wmoRenderer) {
glm::vec3 feetPos = targetPos;
glm::vec3 oldFeetPos = *followTarget;
glm::vec3 adjusted;
if (wmoRenderer->checkWallCollision(oldFeetPos, feetPos, adjusted)) {
targetPos.x = adjusted.x;
targetPos.y = adjusted.y;
targetPos.z = adjusted.z;
}
}
// Ground the character to terrain or WMO floor
{
std::optional<float> terrainH;
std::optional<float> wmoH;
if (terrainManager) {
terrainH = terrainManager->getHeightAt(targetPos.x, targetPos.y);
}
if (wmoRenderer) {
wmoH = wmoRenderer->getFloorHeight(targetPos.x, targetPos.y, targetPos.z + eyeHeight);
}
std::optional<float> groundH;
if (terrainH && wmoH) {
groundH = std::max(*terrainH, *wmoH);
} else if (terrainH) {
groundH = terrainH;
} else if (wmoH) {
groundH = wmoH;
}
if (groundH) {
lastGroundZ = *groundH;
if (targetPos.z <= *groundH) {
targetPos.z = *groundH;
verticalVelocity = 0.0f;
grounded = true;
swimming = false; // Touching ground = wading, not swimming
} else if (!swimming) {
grounded = false;
}
} else if (!swimming) {
// No terrain found — hold at last known ground
targetPos.z = lastGroundZ;
verticalVelocity = 0.0f;
grounded = true;
}
}
// Update follow target position
*followTarget = targetPos;
// Compute camera position orbiting behind the character
glm::vec3 lookAtPoint = targetPos + glm::vec3(0.0f, 0.0f, eyeHeight);
glm::vec3 camPos = lookAtPoint - forward3D * orbitDistance;
// Clamp camera above terrain/WMO floor
{
float minCamZ = camPos.z;
if (terrainManager) {
auto h = terrainManager->getHeightAt(camPos.x, camPos.y);
if (h) minCamZ = *h + 1.0f; // 1 unit above ground
}
if (wmoRenderer) {
auto wh = wmoRenderer->getFloorHeight(camPos.x, camPos.y, camPos.z + eyeHeight);
if (wh && (*wh + 1.0f) > minCamZ) minCamZ = *wh + 1.0f;
}
if (camPos.z < minCamZ) {
camPos.z = minCamZ;
}
}
camera->setPosition(camPos);
} else {
// Free-fly camera mode (original behavior)
glm::vec3 newPos = camera->getPosition();
float feetZ = newPos.z - eyeHeight;
// Check for water at feet position
std::optional<float> waterH;
if (waterRenderer) {
waterH = waterRenderer->getWaterHeightAt(newPos.x, newPos.y);
}
bool inWater = waterH && feetZ < *waterH;
if (inWater) {
swimming = true;
float swimSpeed = speed * SWIM_SPEED_FACTOR;
if (glm::length(movement) > 0.001f) {
movement = glm::normalize(movement);
newPos += movement * swimSpeed * deltaTime;
}
if (nowJump) {
verticalVelocity = SWIM_BUOYANCY;
} else {
verticalVelocity += SWIM_GRAVITY * deltaTime;
if (verticalVelocity < SWIM_SINK_SPEED) {
verticalVelocity = SWIM_SINK_SPEED;
}
}
newPos.z += verticalVelocity * deltaTime;
// Don't rise above water surface (feet at water level)
if (waterH && (newPos.z - eyeHeight) > *waterH - WATER_SURFACE_OFFSET) {
newPos.z = *waterH - WATER_SURFACE_OFFSET + eyeHeight;
if (verticalVelocity > 0.0f) verticalVelocity = 0.0f;
}
grounded = false;
} else {
swimming = false;
if (glm::length(movement) > 0.001f) {
movement = glm::normalize(movement);
newPos += movement * speed * deltaTime;
}
// Jump
if (nowJump && grounded) {
verticalVelocity = jumpVel;
grounded = false;
}
// Apply gravity
verticalVelocity += gravity * deltaTime;
newPos.z += verticalVelocity * deltaTime;
}
// Wall collision — push out of WMO walls before grounding
if (wmoRenderer) {
glm::vec3 feetPos = newPos - glm::vec3(0, 0, eyeHeight);
glm::vec3 oldFeetPos = camera->getPosition() - glm::vec3(0, 0, eyeHeight);
glm::vec3 adjusted;
if (wmoRenderer->checkWallCollision(oldFeetPos, feetPos, adjusted)) {
newPos.x = adjusted.x;
newPos.y = adjusted.y;
newPos.z = adjusted.z + eyeHeight;
}
}
// Ground to terrain or WMO floor
{
std::optional<float> terrainH;
std::optional<float> wmoH;
if (terrainManager) {
terrainH = terrainManager->getHeightAt(newPos.x, newPos.y);
}
if (wmoRenderer) {
wmoH = wmoRenderer->getFloorHeight(newPos.x, newPos.y, newPos.z);
}
std::optional<float> groundH;
if (terrainH && wmoH) {
groundH = std::max(*terrainH, *wmoH);
} else if (terrainH) {
groundH = terrainH;
} else if (wmoH) {
groundH = wmoH;
}
if (groundH) {
lastGroundZ = *groundH;
float groundZ = *groundH + eyeHeight;
if (newPos.z <= groundZ) {
newPos.z = groundZ;
verticalVelocity = 0.0f;
grounded = true;
swimming = false; // Touching ground = wading
} else if (!swimming) {
grounded = false;
}
} else if (!swimming) {
float groundZ = lastGroundZ + eyeHeight;
newPos.z = groundZ;
verticalVelocity = 0.0f;
grounded = true;
}
}
camera->setPosition(newPos);
}
// --- Edge-detection: send movement opcodes on state transitions ---
if (movementCallback) {
// Forward/backward
if (nowForward && !wasMovingForward) {
movementCallback(static_cast<uint32_t>(game::Opcode::CMSG_MOVE_START_FORWARD));
}
if (nowBackward && !wasMovingBackward) {
movementCallback(static_cast<uint32_t>(game::Opcode::CMSG_MOVE_START_BACKWARD));
}
if ((!nowForward && wasMovingForward) || (!nowBackward && wasMovingBackward)) {
if (!nowForward && !nowBackward) {
movementCallback(static_cast<uint32_t>(game::Opcode::CMSG_MOVE_STOP));
}
}
// Strafing
if (nowStrafeLeft && !wasStrafingLeft) {
movementCallback(static_cast<uint32_t>(game::Opcode::CMSG_MOVE_START_STRAFE_LEFT));
}
if (nowStrafeRight && !wasStrafingRight) {
movementCallback(static_cast<uint32_t>(game::Opcode::CMSG_MOVE_START_STRAFE_RIGHT));
}
if ((!nowStrafeLeft && wasStrafingLeft) || (!nowStrafeRight && wasStrafingRight)) {
if (!nowStrafeLeft && !nowStrafeRight) {
movementCallback(static_cast<uint32_t>(game::Opcode::CMSG_MOVE_STOP_STRAFE));
}
}
// Jump
if (nowJump && !wasJumping && grounded) {
movementCallback(static_cast<uint32_t>(game::Opcode::CMSG_MOVE_JUMP));
}
// Fall landing
if (wasFalling && grounded) {
movementCallback(static_cast<uint32_t>(game::Opcode::CMSG_MOVE_FALL_LAND));
}
}
// Swimming state transitions
if (movementCallback) {
if (swimming && !wasSwimming) {
movementCallback(static_cast<uint32_t>(game::Opcode::CMSG_MOVE_START_SWIM));
} else if (!swimming && wasSwimming) {
movementCallback(static_cast<uint32_t>(game::Opcode::CMSG_MOVE_STOP_SWIM));
}
}
// Update previous-frame state
wasSwimming = swimming;
wasMovingForward = nowForward;
wasMovingBackward = nowBackward;
wasStrafingLeft = nowStrafeLeft;
wasStrafingRight = nowStrafeRight;
wasJumping = nowJump;
wasFalling = !grounded && verticalVelocity <= 0.0f;
// Reset camera (R key)
if (!uiWantsKeyboard && input.isKeyPressed(SDL_SCANCODE_R)) {
reset();
}
}
void CameraController::processMouseMotion(const SDL_MouseMotionEvent& event) {
if (!enabled || !camera) {
return;
}
if (!mouseButtonDown) {
return;
}
// Directly update stored yaw/pitch (no lossy forward-vector derivation)
yaw -= event.xrel * mouseSensitivity;
pitch += event.yrel * mouseSensitivity;
pitch = glm::clamp(pitch, -89.0f, 89.0f);
camera->setRotation(yaw, pitch);
}
void CameraController::processMouseButton(const SDL_MouseButtonEvent& event) {
if (!enabled) {
return;
}
if (event.button == SDL_BUTTON_LEFT) {
leftMouseDown = (event.state == SDL_PRESSED);
}
if (event.button == SDL_BUTTON_RIGHT) {
rightMouseDown = (event.state == SDL_PRESSED);
}
bool anyDown = leftMouseDown || rightMouseDown;
if (anyDown && !mouseButtonDown) {
SDL_SetRelativeMouseMode(SDL_TRUE);
} else if (!anyDown && mouseButtonDown) {
SDL_SetRelativeMouseMode(SDL_FALSE);
}
mouseButtonDown = anyDown;
}
void CameraController::reset() {
if (!camera) {
return;
}
yaw = defaultYaw;
pitch = defaultPitch;
verticalVelocity = 0.0f;
grounded = false;
glm::vec3 spawnPos = defaultPosition;
// Snap spawn to terrain or WMO surface
std::optional<float> h;
if (terrainManager) {
h = terrainManager->getHeightAt(spawnPos.x, spawnPos.y);
}
if (wmoRenderer) {
auto wh = wmoRenderer->getFloorHeight(spawnPos.x, spawnPos.y, spawnPos.z);
if (wh && (!h || *wh > *h)) {
h = wh;
}
}
if (h) {
lastGroundZ = *h;
spawnPos.z = *h + eyeHeight;
}
camera->setPosition(spawnPos);
camera->setRotation(yaw, pitch);
LOG_INFO("Camera reset to default position");
}
void CameraController::processMouseWheel(float delta) {
orbitDistance -= delta * zoomSpeed;
orbitDistance = glm::clamp(orbitDistance, minOrbitDistance, maxOrbitDistance);
}
void CameraController::setFollowTarget(glm::vec3* target) {
followTarget = target;
if (target) {
thirdPerson = true;
LOG_INFO("Third-person camera enabled");
} else {
thirdPerson = false;
LOG_INFO("Free-fly camera enabled");
}
}
bool CameraController::isMoving() const {
if (!enabled || !camera) {
return false;
}
if (ImGui::GetIO().WantCaptureKeyboard) {
return false;
}
auto& input = core::Input::getInstance();
return input.isKeyPressed(SDL_SCANCODE_W) ||
input.isKeyPressed(SDL_SCANCODE_S) ||
input.isKeyPressed(SDL_SCANCODE_A) ||
input.isKeyPressed(SDL_SCANCODE_D);
}
bool CameraController::isSprinting() const {
if (!enabled || !camera) {
return false;
}
if (ImGui::GetIO().WantCaptureKeyboard) {
return false;
}
auto& input = core::Input::getInstance();
return isMoving() && (input.isKeyPressed(SDL_SCANCODE_LSHIFT) || input.isKeyPressed(SDL_SCANCODE_RSHIFT));
}
} // namespace rendering
} // namespace wowee

412
src/rendering/celestial.cpp Normal file
View file

@ -0,0 +1,412 @@
#include "rendering/celestial.hpp"
#include "rendering/shader.hpp"
#include "rendering/camera.hpp"
#include "core/logger.hpp"
#include <GL/glew.h>
#include <glm/gtc/matrix_transform.hpp>
#include <cmath>
namespace wowee {
namespace rendering {
Celestial::Celestial() = default;
Celestial::~Celestial() {
shutdown();
}
bool Celestial::initialize() {
LOG_INFO("Initializing celestial renderer");
// Create celestial shader
celestialShader = std::make_unique<Shader>();
// Vertex shader - billboard facing camera
const char* vertexShaderSource = R"(
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec2 aTexCoord;
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;
out vec2 TexCoord;
void main() {
TexCoord = aTexCoord;
// Billboard: remove rotation from view matrix, keep only translation
mat4 viewNoRotation = view;
viewNoRotation[0][0] = 1.0; viewNoRotation[0][1] = 0.0; viewNoRotation[0][2] = 0.0;
viewNoRotation[1][0] = 0.0; viewNoRotation[1][1] = 1.0; viewNoRotation[1][2] = 0.0;
viewNoRotation[2][0] = 0.0; viewNoRotation[2][1] = 0.0; viewNoRotation[2][2] = 1.0;
gl_Position = projection * viewNoRotation * model * vec4(aPos, 1.0);
}
)";
// Fragment shader - disc with glow and moon phase support
const char* fragmentShaderSource = R"(
#version 330 core
in vec2 TexCoord;
uniform vec3 celestialColor;
uniform float intensity;
uniform float moonPhase; // 0.0 = new moon, 0.5 = full moon, 1.0 = new moon
out vec4 FragColor;
void main() {
// Create circular disc
vec2 center = vec2(0.5, 0.5);
float dist = distance(TexCoord, center);
// Core disc
float disc = smoothstep(0.5, 0.4, dist);
// Glow around disc
float glow = smoothstep(0.7, 0.0, dist) * 0.3;
float alpha = (disc + glow) * intensity;
// Apply moon phase shadow (only for moon, indicated by low intensity)
if (intensity < 0.5) { // Moon has lower intensity than sun
// Calculate phase position (-1 to 1, where 0 is center)
float phasePos = (moonPhase - 0.5) * 2.0;
// Distance from phase terminator line
float x = (TexCoord.x - 0.5) * 2.0; // -1 to 1
// Create shadow using smoothstep
float shadow = 1.0;
if (moonPhase < 0.5) {
// Waning (right to left shadow)
shadow = smoothstep(phasePos - 0.1, phasePos + 0.1, x);
} else {
// Waxing (left to right shadow)
shadow = smoothstep(phasePos - 0.1, phasePos + 0.1, -x);
}
// Apply elliptical terminator for 3D effect
float y = (TexCoord.y - 0.5) * 2.0;
float ellipse = sqrt(1.0 - y * y);
float terminatorX = phasePos / ellipse;
if (moonPhase < 0.5) {
shadow = smoothstep(terminatorX - 0.15, terminatorX + 0.15, x);
} else {
shadow = smoothstep(terminatorX - 0.15, terminatorX + 0.15, -x);
}
// Darken shadowed area (not completely black, slight glow remains)
alpha *= mix(0.05, 1.0, shadow);
}
FragColor = vec4(celestialColor, alpha);
}
)";
if (!celestialShader->loadFromSource(vertexShaderSource, fragmentShaderSource)) {
LOG_ERROR("Failed to create celestial shader");
return false;
}
// Create billboard quad
createCelestialQuad();
LOG_INFO("Celestial renderer initialized");
return true;
}
void Celestial::shutdown() {
destroyCelestialQuad();
celestialShader.reset();
}
void Celestial::render(const Camera& camera, float timeOfDay) {
if (!renderingEnabled || vao == 0 || !celestialShader) {
return;
}
// Enable blending for celestial glow
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
// Disable depth writing (but keep depth testing)
glDepthMask(GL_FALSE);
// Render sun and moon
renderSun(camera, timeOfDay);
renderMoon(camera, timeOfDay);
// Restore state
glDepthMask(GL_TRUE);
glDisable(GL_BLEND);
}
void Celestial::renderSun(const Camera& camera, float timeOfDay) {
// Sun visible from 5:00 to 19:00
if (timeOfDay < 5.0f || timeOfDay >= 19.0f) {
return;
}
celestialShader->use();
// Get sun position
glm::vec3 sunPos = getSunPosition(timeOfDay);
// Create model matrix
glm::mat4 model = glm::mat4(1.0f);
model = glm::translate(model, sunPos);
model = glm::scale(model, glm::vec3(50.0f, 50.0f, 1.0f)); // 50 unit diameter
// Set uniforms
glm::mat4 view = camera.getViewMatrix();
glm::mat4 projection = camera.getProjectionMatrix();
celestialShader->setUniform("model", model);
celestialShader->setUniform("view", view);
celestialShader->setUniform("projection", projection);
// Sun color and intensity
glm::vec3 color = getSunColor(timeOfDay);
float intensity = getSunIntensity(timeOfDay);
celestialShader->setUniform("celestialColor", color);
celestialShader->setUniform("intensity", intensity);
celestialShader->setUniform("moonPhase", 0.5f); // Sun doesn't use this, but shader expects it
// Render quad
glBindVertexArray(vao);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, nullptr);
glBindVertexArray(0);
}
void Celestial::renderMoon(const Camera& camera, float timeOfDay) {
// Moon visible from 19:00 to 5:00 (night)
if (timeOfDay >= 5.0f && timeOfDay < 19.0f) {
return;
}
celestialShader->use();
// Get moon position
glm::vec3 moonPos = getMoonPosition(timeOfDay);
// Create model matrix
glm::mat4 model = glm::mat4(1.0f);
model = glm::translate(model, moonPos);
model = glm::scale(model, glm::vec3(40.0f, 40.0f, 1.0f)); // 40 unit diameter (smaller than sun)
// Set uniforms
glm::mat4 view = camera.getViewMatrix();
glm::mat4 projection = camera.getProjectionMatrix();
celestialShader->setUniform("model", model);
celestialShader->setUniform("view", view);
celestialShader->setUniform("projection", projection);
// Moon color (pale blue-white) and intensity
glm::vec3 color = glm::vec3(0.8f, 0.85f, 1.0f);
// Fade in/out at transitions
float intensity = 1.0f;
if (timeOfDay >= 19.0f && timeOfDay < 21.0f) {
// Fade in (19:00-21:00)
intensity = (timeOfDay - 19.0f) / 2.0f;
}
else if (timeOfDay >= 3.0f && timeOfDay < 5.0f) {
// Fade out (3:00-5:00)
intensity = 1.0f - (timeOfDay - 3.0f) / 2.0f;
}
celestialShader->setUniform("celestialColor", color);
celestialShader->setUniform("intensity", intensity);
celestialShader->setUniform("moonPhase", moonPhase);
// Render quad
glBindVertexArray(vao);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, nullptr);
glBindVertexArray(0);
}
glm::vec3 Celestial::getSunPosition(float timeOfDay) const {
// Sun rises at 6:00, peaks at 12:00, sets at 18:00
float angle = calculateCelestialAngle(timeOfDay, 6.0f, 18.0f);
const float radius = 800.0f; // Distance from origin
const float height = 600.0f; // Maximum height
// Arc across sky
float x = radius * std::sin(angle);
float z = height * std::cos(angle);
float y = 0.0f; // Y is horizontal in WoW coordinates
return glm::vec3(x, y, z);
}
glm::vec3 Celestial::getMoonPosition(float timeOfDay) const {
// Moon rises at 18:00, peaks at 0:00 (24:00), sets at 6:00
// Adjust time for moon (opposite to sun)
float moonTime = timeOfDay + 12.0f;
if (moonTime >= 24.0f) moonTime -= 24.0f;
float angle = calculateCelestialAngle(moonTime, 6.0f, 18.0f);
const float radius = 800.0f;
const float height = 600.0f;
float x = radius * std::sin(angle);
float z = height * std::cos(angle);
float y = 0.0f;
return glm::vec3(x, y, z);
}
glm::vec3 Celestial::getSunColor(float timeOfDay) const {
// Sunrise/sunset: orange/red
// Midday: bright yellow-white
if (timeOfDay >= 5.0f && timeOfDay < 7.0f) {
// Sunrise: orange
return glm::vec3(1.0f, 0.6f, 0.2f);
}
else if (timeOfDay >= 7.0f && timeOfDay < 9.0f) {
// Morning: blend to yellow
float t = (timeOfDay - 7.0f) / 2.0f;
glm::vec3 orange = glm::vec3(1.0f, 0.6f, 0.2f);
glm::vec3 yellow = glm::vec3(1.0f, 1.0f, 0.9f);
return glm::mix(orange, yellow, t);
}
else if (timeOfDay >= 9.0f && timeOfDay < 16.0f) {
// Day: bright yellow-white
return glm::vec3(1.0f, 1.0f, 0.9f);
}
else if (timeOfDay >= 16.0f && timeOfDay < 18.0f) {
// Evening: blend to orange
float t = (timeOfDay - 16.0f) / 2.0f;
glm::vec3 yellow = glm::vec3(1.0f, 1.0f, 0.9f);
glm::vec3 orange = glm::vec3(1.0f, 0.5f, 0.1f);
return glm::mix(yellow, orange, t);
}
else {
// Sunset: deep orange/red
return glm::vec3(1.0f, 0.4f, 0.1f);
}
}
float Celestial::getSunIntensity(float timeOfDay) const {
// Fade in at sunrise (5:00-6:00)
if (timeOfDay >= 5.0f && timeOfDay < 6.0f) {
return (timeOfDay - 5.0f); // 0 to 1
}
// Full intensity during day (6:00-18:00)
else if (timeOfDay >= 6.0f && timeOfDay < 18.0f) {
return 1.0f;
}
// Fade out at sunset (18:00-19:00)
else if (timeOfDay >= 18.0f && timeOfDay < 19.0f) {
return 1.0f - (timeOfDay - 18.0f); // 1 to 0
}
else {
return 0.0f;
}
}
float Celestial::calculateCelestialAngle(float timeOfDay, float riseTime, float setTime) const {
// Map time to angle (0 to PI)
// riseTime: 0 radians (horizon east)
// (riseTime + setTime) / 2: PI/2 radians (zenith)
// setTime: PI radians (horizon west)
float duration = setTime - riseTime;
float elapsed = timeOfDay - riseTime;
// Normalize to 0-1
float t = elapsed / duration;
// Map to 0 to PI (arc from east to west)
return t * M_PI;
}
void Celestial::createCelestialQuad() {
// Simple quad centered at origin
float vertices[] = {
// Position // TexCoord
-0.5f, 0.5f, 0.0f, 0.0f, 1.0f, // Top-left
0.5f, 0.5f, 0.0f, 1.0f, 1.0f, // Top-right
0.5f, -0.5f, 0.0f, 1.0f, 0.0f, // Bottom-right
-0.5f, -0.5f, 0.0f, 0.0f, 0.0f // Bottom-left
};
uint32_t indices[] = {
0, 1, 2, // First triangle
0, 2, 3 // Second triangle
};
// Create OpenGL buffers
glGenVertexArrays(1, &vao);
glGenBuffers(1, &vbo);
glGenBuffers(1, &ebo);
glBindVertexArray(vao);
// Upload vertex data
glBindBuffer(GL_ARRAY_BUFFER, vbo);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// Upload index data
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
// Set vertex attributes
// Position
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
// Texture coordinates
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(3 * sizeof(float)));
glEnableVertexAttribArray(1);
glBindVertexArray(0);
}
void Celestial::destroyCelestialQuad() {
if (vao != 0) {
glDeleteVertexArrays(1, &vao);
vao = 0;
}
if (vbo != 0) {
glDeleteBuffers(1, &vbo);
vbo = 0;
}
if (ebo != 0) {
glDeleteBuffers(1, &ebo);
ebo = 0;
}
}
void Celestial::update(float deltaTime) {
if (!moonPhaseCycling) {
return;
}
// Update moon phase timer
moonPhaseTimer += deltaTime;
// Moon completes full cycle in MOON_CYCLE_DURATION seconds
moonPhase = std::fmod(moonPhaseTimer / MOON_CYCLE_DURATION, 1.0f);
}
void Celestial::setMoonPhase(float phase) {
// Clamp phase to 0.0-1.0
moonPhase = glm::clamp(phase, 0.0f, 1.0f);
// Update timer to match phase
moonPhaseTimer = moonPhase * MOON_CYCLE_DURATION;
}
} // namespace rendering
} // namespace wowee

File diff suppressed because it is too large Load diff

312
src/rendering/clouds.cpp Normal file
View file

@ -0,0 +1,312 @@
#include "rendering/clouds.hpp"
#include "rendering/camera.hpp"
#include "rendering/shader.hpp"
#include "core/logger.hpp"
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
#include <cmath>
namespace wowee {
namespace rendering {
Clouds::Clouds() {
}
Clouds::~Clouds() {
cleanup();
}
bool Clouds::initialize() {
LOG_INFO("Initializing cloud system");
// Generate cloud dome mesh
generateMesh();
// Create VAO
glGenVertexArrays(1, &vao);
glGenBuffers(1, &vbo);
glGenBuffers(1, &ebo);
glBindVertexArray(vao);
// Upload vertex data
glBindBuffer(GL_ARRAY_BUFFER, vbo);
glBufferData(GL_ARRAY_BUFFER,
vertices.size() * sizeof(glm::vec3),
vertices.data(),
GL_STATIC_DRAW);
// Upload index data
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo);
glBufferData(GL_ELEMENT_ARRAY_BUFFER,
indices.size() * sizeof(unsigned int),
indices.data(),
GL_STATIC_DRAW);
// Position attribute
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(glm::vec3), (void*)0);
glEnableVertexAttribArray(0);
glBindVertexArray(0);
// Create shader
shader = std::make_unique<Shader>();
// Cloud vertex shader
const char* vertexShaderSource = R"(
#version 330 core
layout (location = 0) in vec3 aPos;
uniform mat4 uView;
uniform mat4 uProjection;
out vec3 WorldPos;
out vec3 LocalPos;
void main() {
LocalPos = aPos;
WorldPos = aPos;
// Remove translation from view matrix (billboard effect)
mat4 viewNoTranslation = uView;
viewNoTranslation[3][0] = 0.0;
viewNoTranslation[3][1] = 0.0;
viewNoTranslation[3][2] = 0.0;
vec4 pos = uProjection * viewNoTranslation * vec4(aPos, 1.0);
gl_Position = pos.xyww; // Put at far plane
}
)";
// Cloud fragment shader with procedural noise
const char* fragmentShaderSource = R"(
#version 330 core
in vec3 WorldPos;
in vec3 LocalPos;
uniform vec3 uCloudColor;
uniform float uDensity;
uniform float uWindOffset;
out vec4 FragColor;
// Simple 3D noise function
float hash(vec3 p) {
p = fract(p * vec3(0.1031, 0.1030, 0.0973));
p += dot(p, p.yxz + 19.19);
return fract((p.x + p.y) * p.z);
}
float noise(vec3 p) {
vec3 i = floor(p);
vec3 f = fract(p);
f = f * f * (3.0 - 2.0 * f);
return mix(
mix(mix(hash(i + vec3(0,0,0)), hash(i + vec3(1,0,0)), f.x),
mix(hash(i + vec3(0,1,0)), hash(i + vec3(1,1,0)), f.x), f.y),
mix(mix(hash(i + vec3(0,0,1)), hash(i + vec3(1,0,1)), f.x),
mix(hash(i + vec3(0,1,1)), hash(i + vec3(1,1,1)), f.x), f.y),
f.z);
}
// Fractal Brownian Motion for cloud-like patterns
float fbm(vec3 p) {
float value = 0.0;
float amplitude = 0.5;
float frequency = 1.0;
for (int i = 0; i < 4; i++) {
value += amplitude * noise(p * frequency);
frequency *= 2.0;
amplitude *= 0.5;
}
return value;
}
void main() {
// Normalize position for noise sampling
vec3 pos = normalize(LocalPos);
// Only render on upper hemisphere
if (pos.y < 0.1) {
discard;
}
// Apply wind offset to x coordinate
vec3 samplePos = vec3(pos.x + uWindOffset, pos.y, pos.z) * 3.0;
// Generate two cloud layers
float clouds1 = fbm(samplePos * 1.0);
float clouds2 = fbm(samplePos * 2.0 + vec3(100.0));
// Combine layers
float cloudPattern = clouds1 * 0.6 + clouds2 * 0.4;
// Apply density threshold to create cloud shapes
float cloudMask = smoothstep(0.4 + (1.0 - uDensity) * 0.3, 0.7, cloudPattern);
// Add some variation to cloud edges
float edgeNoise = noise(samplePos * 5.0);
cloudMask *= smoothstep(0.3, 0.7, edgeNoise);
// Fade clouds near horizon
float horizonFade = smoothstep(0.0, 0.3, pos.y);
cloudMask *= horizonFade;
// Final alpha
float alpha = cloudMask * 0.85;
if (alpha < 0.05) {
discard;
}
FragColor = vec4(uCloudColor, alpha);
}
)";
if (!shader->loadFromSource(vertexShaderSource, fragmentShaderSource)) {
LOG_ERROR("Failed to create cloud shader");
return false;
}
LOG_INFO("Cloud system initialized: ", triangleCount, " triangles");
return true;
}
void Clouds::generateMesh() {
vertices.clear();
indices.clear();
// Generate hemisphere mesh for clouds
for (int ring = 0; ring <= RINGS; ++ring) {
float phi = (ring / static_cast<float>(RINGS)) * (M_PI * 0.5f); // 0 to π/2
float y = RADIUS * cos(phi);
float ringRadius = RADIUS * sin(phi);
for (int segment = 0; segment <= SEGMENTS; ++segment) {
float theta = (segment / static_cast<float>(SEGMENTS)) * (2.0f * M_PI);
float x = ringRadius * cos(theta);
float z = ringRadius * sin(theta);
vertices.push_back(glm::vec3(x, y, z));
}
}
// Generate indices
for (int ring = 0; ring < RINGS; ++ring) {
for (int segment = 0; segment < SEGMENTS; ++segment) {
int current = ring * (SEGMENTS + 1) + segment;
int next = current + SEGMENTS + 1;
// Two triangles per quad
indices.push_back(current);
indices.push_back(next);
indices.push_back(current + 1);
indices.push_back(current + 1);
indices.push_back(next);
indices.push_back(next + 1);
}
}
triangleCount = static_cast<int>(indices.size()) / 3;
}
void Clouds::update(float deltaTime) {
if (!enabled) {
return;
}
// Accumulate wind movement
windOffset += deltaTime * windSpeed * 0.05f; // Slow drift
}
glm::vec3 Clouds::getCloudColor(float timeOfDay) const {
// Base cloud color (white/light gray)
glm::vec3 dayColor(0.95f, 0.95f, 1.0f);
// Dawn clouds (orange tint)
if (timeOfDay >= 5.0f && timeOfDay < 7.0f) {
float t = (timeOfDay - 5.0f) / 2.0f;
glm::vec3 dawnColor(1.0f, 0.7f, 0.5f);
return glm::mix(dawnColor, dayColor, t);
}
// Dusk clouds (orange/pink tint)
else if (timeOfDay >= 17.0f && timeOfDay < 19.0f) {
float t = (timeOfDay - 17.0f) / 2.0f;
glm::vec3 duskColor(1.0f, 0.6f, 0.4f);
return glm::mix(dayColor, duskColor, t);
}
// Night clouds (dark blue-gray)
else if (timeOfDay >= 20.0f || timeOfDay < 5.0f) {
return glm::vec3(0.15f, 0.15f, 0.25f);
}
return dayColor;
}
void Clouds::render(const Camera& camera, float timeOfDay) {
if (!enabled || !shader) {
return;
}
// Enable blending for transparent clouds
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
// Disable depth write (clouds are in sky)
glDepthMask(GL_FALSE);
// Enable depth test so clouds are behind skybox
glEnable(GL_DEPTH_TEST);
glDepthFunc(GL_LEQUAL);
shader->use();
// Set matrices
glm::mat4 view = camera.getViewMatrix();
glm::mat4 projection = camera.getProjectionMatrix();
shader->setUniform("uView", view);
shader->setUniform("uProjection", projection);
// Set cloud parameters
glm::vec3 cloudColor = getCloudColor(timeOfDay);
shader->setUniform("uCloudColor", cloudColor);
shader->setUniform("uDensity", density);
shader->setUniform("uWindOffset", windOffset);
// Render
glBindVertexArray(vao);
glDrawElements(GL_TRIANGLES, static_cast<GLsizei>(indices.size()), GL_UNSIGNED_INT, 0);
glBindVertexArray(0);
// Restore state
glDisable(GL_BLEND);
glDepthMask(GL_TRUE);
glDepthFunc(GL_LESS);
}
void Clouds::setDensity(float density) {
this->density = glm::clamp(density, 0.0f, 1.0f);
}
void Clouds::cleanup() {
if (vao) {
glDeleteVertexArrays(1, &vao);
vao = 0;
}
if (vbo) {
glDeleteBuffers(1, &vbo);
vbo = 0;
}
if (ebo) {
glDeleteBuffers(1, &ebo);
ebo = 0;
}
}
} // namespace rendering
} // namespace wowee

106
src/rendering/frustum.cpp Normal file
View file

@ -0,0 +1,106 @@
#include "rendering/frustum.hpp"
#include <algorithm>
namespace wowee {
namespace rendering {
void Frustum::extractFromMatrix(const glm::mat4& vp) {
// Extract planes from view-projection matrix
// Based on Gribb & Hartmann method (Fast Extraction of Viewing Frustum Planes)
// Left plane: row4 + row1
planes[LEFT].normal.x = vp[0][3] + vp[0][0];
planes[LEFT].normal.y = vp[1][3] + vp[1][0];
planes[LEFT].normal.z = vp[2][3] + vp[2][0];
planes[LEFT].distance = vp[3][3] + vp[3][0];
normalizePlane(planes[LEFT]);
// Right plane: row4 - row1
planes[RIGHT].normal.x = vp[0][3] - vp[0][0];
planes[RIGHT].normal.y = vp[1][3] - vp[1][0];
planes[RIGHT].normal.z = vp[2][3] - vp[2][0];
planes[RIGHT].distance = vp[3][3] - vp[3][0];
normalizePlane(planes[RIGHT]);
// Bottom plane: row4 + row2
planes[BOTTOM].normal.x = vp[0][3] + vp[0][1];
planes[BOTTOM].normal.y = vp[1][3] + vp[1][1];
planes[BOTTOM].normal.z = vp[2][3] + vp[2][1];
planes[BOTTOM].distance = vp[3][3] + vp[3][1];
normalizePlane(planes[BOTTOM]);
// Top plane: row4 - row2
planes[TOP].normal.x = vp[0][3] - vp[0][1];
planes[TOP].normal.y = vp[1][3] - vp[1][1];
planes[TOP].normal.z = vp[2][3] - vp[2][1];
planes[TOP].distance = vp[3][3] - vp[3][1];
normalizePlane(planes[TOP]);
// Near plane: row4 + row3
planes[NEAR].normal.x = vp[0][3] + vp[0][2];
planes[NEAR].normal.y = vp[1][3] + vp[1][2];
planes[NEAR].normal.z = vp[2][3] + vp[2][2];
planes[NEAR].distance = vp[3][3] + vp[3][2];
normalizePlane(planes[NEAR]);
// Far plane: row4 - row3
planes[FAR].normal.x = vp[0][3] - vp[0][2];
planes[FAR].normal.y = vp[1][3] - vp[1][2];
planes[FAR].normal.z = vp[2][3] - vp[2][2];
planes[FAR].distance = vp[3][3] - vp[3][2];
normalizePlane(planes[FAR]);
}
void Frustum::normalizePlane(Plane& plane) {
float length = glm::length(plane.normal);
if (length > 0.0001f) {
plane.normal /= length;
plane.distance /= length;
}
}
bool Frustum::containsPoint(const glm::vec3& point) const {
// Point must be in front of all planes
for (const auto& plane : planes) {
if (plane.distanceToPoint(point) < 0.0f) {
return false;
}
}
return true;
}
bool Frustum::intersectsSphere(const glm::vec3& center, float radius) const {
// Sphere is visible if distance from center to any plane is >= -radius
for (const auto& plane : planes) {
float distance = plane.distanceToPoint(center);
if (distance < -radius) {
// Sphere is completely behind this plane
return false;
}
}
return true;
}
bool Frustum::intersectsAABB(const glm::vec3& min, const glm::vec3& max) const {
// Test all 8 corners of the AABB
// If all corners are behind any plane, AABB is outside
// Otherwise, AABB is at least partially visible
for (const auto& plane : planes) {
// Find the positive vertex (corner furthest in plane normal direction)
glm::vec3 positiveVertex;
positiveVertex.x = (plane.normal.x >= 0.0f) ? max.x : min.x;
positiveVertex.y = (plane.normal.y >= 0.0f) ? max.y : min.y;
positiveVertex.z = (plane.normal.z >= 0.0f) ? max.z : min.z;
// If positive vertex is behind plane, entire box is behind
if (plane.distanceToPoint(positiveVertex) < 0.0f) {
return false;
}
}
return true;
}
} // namespace rendering
} // namespace wowee

View file

@ -0,0 +1,288 @@
#include "rendering/lens_flare.hpp"
#include "rendering/camera.hpp"
#include "rendering/shader.hpp"
#include "core/logger.hpp"
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
#include <cmath>
namespace wowee {
namespace rendering {
LensFlare::LensFlare() {
}
LensFlare::~LensFlare() {
cleanup();
}
bool LensFlare::initialize() {
LOG_INFO("Initializing lens flare system");
// Generate flare elements
generateFlareElements();
// Create VAO and VBO for quad rendering
glGenVertexArrays(1, &vao);
glGenBuffers(1, &vbo);
glBindVertexArray(vao);
glBindBuffer(GL_ARRAY_BUFFER, vbo);
// Position (x, y) and UV (u, v) for a quad
float quadVertices[] = {
// Pos UV
-0.5f, -0.5f, 0.0f, 0.0f,
0.5f, -0.5f, 1.0f, 0.0f,
0.5f, 0.5f, 1.0f, 1.0f,
-0.5f, -0.5f, 0.0f, 0.0f,
0.5f, 0.5f, 1.0f, 1.0f,
-0.5f, 0.5f, 0.0f, 1.0f
};
glBufferData(GL_ARRAY_BUFFER, sizeof(quadVertices), quadVertices, GL_STATIC_DRAW);
// Position attribute
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
// UV attribute
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void*)(2 * sizeof(float)));
glEnableVertexAttribArray(1);
glBindVertexArray(0);
// Create shader
shader = std::make_unique<Shader>();
// Lens flare vertex shader (2D screen-space rendering)
const char* vertexShaderSource = R"(
#version 330 core
layout (location = 0) in vec2 aPos;
layout (location = 1) in vec2 aUV;
uniform vec2 uPosition; // Screen-space position (-1 to 1)
uniform float uSize; // Size in screen space
uniform float uAspectRatio;
out vec2 TexCoord;
void main() {
// Scale by size and aspect ratio
vec2 scaledPos = aPos * uSize;
scaledPos.x /= uAspectRatio;
// Translate to position
vec2 finalPos = scaledPos + uPosition;
gl_Position = vec4(finalPos, 0.0, 1.0);
TexCoord = aUV;
}
)";
// Lens flare fragment shader (circular gradient)
const char* fragmentShaderSource = R"(
#version 330 core
in vec2 TexCoord;
uniform vec3 uColor;
uniform float uBrightness;
out vec4 FragColor;
void main() {
// Distance from center
vec2 center = vec2(0.5);
float dist = distance(TexCoord, center);
// Circular gradient with soft edges
float alpha = smoothstep(0.5, 0.0, dist);
// Add some variation - brighter in center
float centerGlow = smoothstep(0.5, 0.0, dist * 2.0);
alpha = max(alpha * 0.3, centerGlow);
// Apply brightness
alpha *= uBrightness;
if (alpha < 0.01) {
discard;
}
FragColor = vec4(uColor, alpha);
}
)";
if (!shader->loadFromSource(vertexShaderSource, fragmentShaderSource)) {
LOG_ERROR("Failed to create lens flare shader");
return false;
}
LOG_INFO("Lens flare system initialized: ", flareElements.size(), " elements");
return true;
}
void LensFlare::generateFlareElements() {
flareElements.clear();
// Main sun glow (at sun position)
flareElements.push_back({0.0f, 0.3f, glm::vec3(1.0f, 0.95f, 0.8f), 0.8f});
// Flare ghosts along sun-to-center axis
// These appear at various positions between sun and opposite side
// Bright white ghost near sun
flareElements.push_back({0.2f, 0.08f, glm::vec3(1.0f, 1.0f, 1.0f), 0.5f});
// Blue-tinted ghost
flareElements.push_back({0.4f, 0.15f, glm::vec3(0.3f, 0.5f, 1.0f), 0.4f});
// Small bright spot
flareElements.push_back({0.6f, 0.05f, glm::vec3(1.0f, 0.8f, 0.6f), 0.6f});
// Green-tinted ghost (chromatic aberration)
flareElements.push_back({0.8f, 0.12f, glm::vec3(0.4f, 1.0f, 0.5f), 0.3f});
// Large halo on opposite side
flareElements.push_back({-0.5f, 0.25f, glm::vec3(1.0f, 0.7f, 0.4f), 0.2f});
// Purple ghost far from sun
flareElements.push_back({-0.8f, 0.1f, glm::vec3(0.8f, 0.4f, 1.0f), 0.25f});
// Small red ghost
flareElements.push_back({-1.2f, 0.06f, glm::vec3(1.0f, 0.3f, 0.3f), 0.3f});
}
glm::vec2 LensFlare::worldToScreen(const Camera& camera, const glm::vec3& worldPos) const {
// Transform to clip space
glm::mat4 view = camera.getViewMatrix();
glm::mat4 projection = camera.getProjectionMatrix();
glm::mat4 viewProj = projection * view;
glm::vec4 clipPos = viewProj * glm::vec4(worldPos, 1.0f);
// Perspective divide
if (clipPos.w > 0.0f) {
glm::vec2 ndc = glm::vec2(clipPos.x / clipPos.w, clipPos.y / clipPos.w);
return ndc;
}
// Behind camera
return glm::vec2(10.0f, 10.0f); // Off-screen
}
float LensFlare::calculateSunVisibility(const Camera& camera, const glm::vec3& sunPosition) const {
// Get sun position in screen space
glm::vec2 sunScreen = worldToScreen(camera, sunPosition);
// Check if sun is behind camera
glm::vec3 camPos = camera.getPosition();
glm::vec3 camForward = camera.getForward();
glm::vec3 toSun = glm::normalize(sunPosition - camPos);
float dotProduct = glm::dot(camForward, toSun);
if (dotProduct < 0.0f) {
return 0.0f; // Sun is behind camera
}
// Check if sun is outside screen bounds (with some margin)
if (std::abs(sunScreen.x) > 1.5f || std::abs(sunScreen.y) > 1.5f) {
return 0.0f;
}
// Fade based on angle (stronger when looking directly at sun)
float angleFactor = glm::smoothstep(0.3f, 1.0f, dotProduct);
// Fade at screen edges
float edgeFade = 1.0f;
if (std::abs(sunScreen.x) > 0.8f) {
edgeFade *= glm::smoothstep(1.2f, 0.8f, std::abs(sunScreen.x));
}
if (std::abs(sunScreen.y) > 0.8f) {
edgeFade *= glm::smoothstep(1.2f, 0.8f, std::abs(sunScreen.y));
}
return angleFactor * edgeFade;
}
void LensFlare::render(const Camera& camera, const glm::vec3& sunPosition, float timeOfDay) {
if (!enabled || !shader) {
return;
}
// Only render lens flare during daytime (when sun is visible)
if (timeOfDay < 5.0f || timeOfDay > 19.0f) {
return;
}
// Calculate sun visibility
float visibility = calculateSunVisibility(camera, sunPosition);
if (visibility < 0.01f) {
return;
}
// Get sun screen position
glm::vec2 sunScreen = worldToScreen(camera, sunPosition);
glm::vec2 screenCenter(0.0f, 0.0f);
// Vector from sun to screen center
glm::vec2 sunToCenter = screenCenter - sunScreen;
// Enable additive blending for flare effect
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE); // Additive blending
// Disable depth test (render on top)
glDisable(GL_DEPTH_TEST);
shader->use();
// Set aspect ratio
float aspectRatio = camera.getAspectRatio();
shader->setUniform("uAspectRatio", aspectRatio);
glBindVertexArray(vao);
// Render each flare element
for (const auto& element : flareElements) {
// Calculate position along sun-to-center axis
glm::vec2 position = sunScreen + sunToCenter * element.position;
// Set uniforms
shader->setUniform("uPosition", position);
shader->setUniform("uSize", element.size);
shader->setUniform("uColor", element.color);
// Apply visibility and intensity
float brightness = element.brightness * visibility * intensityMultiplier;
shader->setUniform("uBrightness", brightness);
// Render quad
glDrawArrays(GL_TRIANGLES, 0, VERTICES_PER_QUAD);
}
glBindVertexArray(0);
// Restore state
glEnable(GL_DEPTH_TEST);
glDisable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); // Restore standard blending
}
void LensFlare::setIntensity(float intensity) {
this->intensityMultiplier = glm::clamp(intensity, 0.0f, 2.0f);
}
void LensFlare::cleanup() {
if (vao) {
glDeleteVertexArrays(1, &vao);
vao = 0;
}
if (vbo) {
glDeleteBuffers(1, &vbo);
vbo = 0;
}
}
} // namespace rendering
} // namespace wowee

414
src/rendering/lightning.cpp Normal file
View file

@ -0,0 +1,414 @@
#include "rendering/lightning.hpp"
#include "rendering/shader.hpp"
#include "rendering/camera.hpp"
#include "core/logger.hpp"
#include <GL/glew.h>
#include <random>
#include <cmath>
namespace wowee {
namespace rendering {
namespace {
std::random_device rd;
std::mt19937 gen(rd());
std::uniform_real_distribution<float> dist(0.0f, 1.0f);
float randomRange(float min, float max) {
return min + dist(gen) * (max - min);
}
}
Lightning::Lightning() {
flash.active = false;
flash.intensity = 0.0f;
flash.lifetime = 0.0f;
flash.maxLifetime = FLASH_LIFETIME;
bolts.resize(MAX_BOLTS);
for (auto& bolt : bolts) {
bolt.active = false;
bolt.lifetime = 0.0f;
bolt.maxLifetime = BOLT_LIFETIME;
bolt.brightness = 1.0f;
}
// Random initial strike time
nextStrikeTime = randomRange(MIN_STRIKE_INTERVAL, MAX_STRIKE_INTERVAL);
}
Lightning::~Lightning() {
shutdown();
}
bool Lightning::initialize() {
core::Logger::getInstance().info("Initializing lightning system...");
// Create bolt shader
const char* boltVertexSrc = R"(
#version 330 core
layout (location = 0) in vec3 aPos;
uniform mat4 uViewProjection;
uniform float uBrightness;
out float vBrightness;
void main() {
gl_Position = uViewProjection * vec4(aPos, 1.0);
vBrightness = uBrightness;
}
)";
const char* boltFragmentSrc = R"(
#version 330 core
in float vBrightness;
out vec4 FragColor;
void main() {
// Electric blue-white color
vec3 color = mix(vec3(0.6, 0.8, 1.0), vec3(1.0), vBrightness * 0.5);
FragColor = vec4(color, vBrightness);
}
)";
boltShader = std::make_unique<Shader>();
if (!boltShader->loadFromSource(boltVertexSrc, boltFragmentSrc)) {
core::Logger::getInstance().error("Failed to create bolt shader");
return false;
}
// Create flash shader (fullscreen quad)
const char* flashVertexSrc = R"(
#version 330 core
layout (location = 0) in vec2 aPos;
void main() {
gl_Position = vec4(aPos, 0.0, 1.0);
}
)";
const char* flashFragmentSrc = R"(
#version 330 core
uniform float uIntensity;
out vec4 FragColor;
void main() {
// Bright white flash with fade
vec3 color = vec3(1.0);
FragColor = vec4(color, uIntensity * 0.6);
}
)";
flashShader = std::make_unique<Shader>();
if (!flashShader->loadFromSource(flashVertexSrc, flashFragmentSrc)) {
core::Logger::getInstance().error("Failed to create flash shader");
return false;
}
// Create bolt VAO/VBO
glGenVertexArrays(1, &boltVAO);
glGenBuffers(1, &boltVBO);
glBindVertexArray(boltVAO);
glBindBuffer(GL_ARRAY_BUFFER, boltVBO);
// Reserve space for segments
glBufferData(GL_ARRAY_BUFFER, sizeof(glm::vec3) * MAX_SEGMENTS * 2, nullptr, GL_DYNAMIC_DRAW);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(glm::vec3), (void*)0);
// Create flash quad VAO/VBO
glGenVertexArrays(1, &flashVAO);
glGenBuffers(1, &flashVBO);
float flashQuad[] = {
-1.0f, -1.0f,
1.0f, -1.0f,
-1.0f, 1.0f,
1.0f, 1.0f
};
glBindVertexArray(flashVAO);
glBindBuffer(GL_ARRAY_BUFFER, flashVBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(flashQuad), flashQuad, GL_STATIC_DRAW);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), (void*)0);
glBindVertexArray(0);
core::Logger::getInstance().info("Lightning system initialized");
return true;
}
void Lightning::shutdown() {
if (boltVAO) {
glDeleteVertexArrays(1, &boltVAO);
glDeleteBuffers(1, &boltVBO);
boltVAO = 0;
boltVBO = 0;
}
if (flashVAO) {
glDeleteVertexArrays(1, &flashVAO);
glDeleteBuffers(1, &flashVBO);
flashVAO = 0;
flashVBO = 0;
}
boltShader.reset();
flashShader.reset();
}
void Lightning::update(float deltaTime, const Camera& camera) {
if (!enabled) {
return;
}
// Update strike timer
strikeTimer += deltaTime;
// Spawn random strikes based on intensity
if (strikeTimer >= nextStrikeTime) {
spawnRandomStrike(camera.getPosition());
strikeTimer = 0.0f;
// Calculate next strike time (higher intensity = more frequent)
float intervalRange = MAX_STRIKE_INTERVAL - MIN_STRIKE_INTERVAL;
float adjustedInterval = MIN_STRIKE_INTERVAL + intervalRange * (1.0f - intensity);
nextStrikeTime = randomRange(adjustedInterval * 0.8f, adjustedInterval * 1.2f);
}
updateBolts(deltaTime);
updateFlash(deltaTime);
}
void Lightning::updateBolts(float deltaTime) {
for (auto& bolt : bolts) {
if (!bolt.active) {
continue;
}
bolt.lifetime += deltaTime;
if (bolt.lifetime >= bolt.maxLifetime) {
bolt.active = false;
continue;
}
// Fade out
float t = bolt.lifetime / bolt.maxLifetime;
bolt.brightness = 1.0f - t;
}
}
void Lightning::updateFlash(float deltaTime) {
if (!flash.active) {
return;
}
flash.lifetime += deltaTime;
if (flash.lifetime >= flash.maxLifetime) {
flash.active = false;
flash.intensity = 0.0f;
return;
}
// Quick fade
float t = flash.lifetime / flash.maxLifetime;
flash.intensity = 1.0f - (t * t); // Quadratic fade
}
void Lightning::spawnRandomStrike(const glm::vec3& cameraPos) {
// Find inactive bolt
LightningBolt* bolt = nullptr;
for (auto& b : bolts) {
if (!b.active) {
bolt = &b;
break;
}
}
if (!bolt) {
return; // All bolts active
}
// Random position around camera
float angle = randomRange(0.0f, 2.0f * 3.14159f);
float distance = randomRange(50.0f, STRIKE_DISTANCE);
glm::vec3 strikePos;
strikePos.x = cameraPos.x + std::cos(angle) * distance;
strikePos.z = cameraPos.z + std::sin(angle) * distance;
strikePos.y = cameraPos.y + randomRange(80.0f, 150.0f); // High in sky
triggerStrike(strikePos);
}
void Lightning::triggerStrike(const glm::vec3& position) {
// Find inactive bolt
LightningBolt* bolt = nullptr;
for (auto& b : bolts) {
if (!b.active) {
bolt = &b;
break;
}
}
if (!bolt) {
return;
}
// Setup bolt
bolt->active = true;
bolt->lifetime = 0.0f;
bolt->brightness = 1.0f;
bolt->startPos = position;
bolt->endPos = position;
bolt->endPos.y = position.y - randomRange(100.0f, 200.0f); // Strike downward
// Generate segments
bolt->segments.clear();
bolt->branches.clear();
generateLightningBolt(*bolt);
// Trigger screen flash
flash.active = true;
flash.lifetime = 0.0f;
flash.intensity = 1.0f;
}
void Lightning::generateLightningBolt(LightningBolt& bolt) {
generateBoltSegments(bolt.startPos, bolt.endPos, bolt.segments, 0);
}
void Lightning::generateBoltSegments(const glm::vec3& start, const glm::vec3& end,
std::vector<glm::vec3>& segments, int depth) {
if (depth > 4) { // Max recursion depth
return;
}
int numSegments = 8 + static_cast<int>(randomRange(0.0f, 8.0f));
glm::vec3 direction = end - start;
float length = glm::length(direction);
direction = glm::normalize(direction);
glm::vec3 current = start;
segments.push_back(current);
for (int i = 1; i < numSegments; i++) {
float t = static_cast<float>(i) / static_cast<float>(numSegments);
glm::vec3 target = start + direction * (length * t);
// Add random offset perpendicular to direction
float offsetAmount = (1.0f - t) * 8.0f; // More offset at start
glm::vec3 perpendicular1 = glm::normalize(glm::cross(direction, glm::vec3(0.0f, 1.0f, 0.0f)));
glm::vec3 perpendicular2 = glm::normalize(glm::cross(direction, perpendicular1));
glm::vec3 offset = perpendicular1 * randomRange(-offsetAmount, offsetAmount) +
perpendicular2 * randomRange(-offsetAmount, offsetAmount);
current = target + offset;
segments.push_back(current);
// Random branches
if (dist(gen) < BRANCH_PROBABILITY && depth < 3) {
glm::vec3 branchEnd = current;
branchEnd += glm::vec3(randomRange(-20.0f, 20.0f),
randomRange(-30.0f, -10.0f),
randomRange(-20.0f, 20.0f));
generateBoltSegments(current, branchEnd, segments, depth + 1);
}
}
segments.push_back(end);
}
void Lightning::render([[maybe_unused]] const Camera& camera, const glm::mat4& view, const glm::mat4& projection) {
if (!enabled) {
return;
}
glm::mat4 viewProj = projection * view;
renderBolts(viewProj);
renderFlash();
}
void Lightning::renderBolts(const glm::mat4& viewProj) {
// Enable additive blending for electric glow
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE);
glDisable(GL_DEPTH_TEST); // Always visible
boltShader->use();
boltShader->setUniform("uViewProjection", viewProj);
glBindVertexArray(boltVAO);
glLineWidth(3.0f);
for (const auto& bolt : bolts) {
if (!bolt.active || bolt.segments.empty()) {
continue;
}
boltShader->setUniform("uBrightness", bolt.brightness);
// Upload segments
glBindBuffer(GL_ARRAY_BUFFER, boltVBO);
glBufferSubData(GL_ARRAY_BUFFER, 0,
bolt.segments.size() * sizeof(glm::vec3),
bolt.segments.data());
// Draw as line strip
glDrawArrays(GL_LINE_STRIP, 0, static_cast<GLsizei>(bolt.segments.size()));
}
glLineWidth(1.0f);
glBindVertexArray(0);
glEnable(GL_DEPTH_TEST);
glDisable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
}
void Lightning::renderFlash() {
if (!flash.active || flash.intensity <= 0.01f) {
return;
}
// Fullscreen flash overlay
glDisable(GL_DEPTH_TEST);
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
flashShader->use();
flashShader->setUniform("uIntensity", flash.intensity);
glBindVertexArray(flashVAO);
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
glBindVertexArray(0);
glEnable(GL_DEPTH_TEST);
glDisable(GL_BLEND);
}
void Lightning::setEnabled(bool enabled) {
this->enabled = enabled;
if (!enabled) {
// Clear active effects
for (auto& bolt : bolts) {
bolt.active = false;
}
flash.active = false;
}
}
void Lightning::setIntensity(float intensity) {
this->intensity = glm::clamp(intensity, 0.0f, 1.0f);
}
} // namespace rendering
} // namespace wowee

View file

@ -0,0 +1,466 @@
#include "rendering/m2_renderer.hpp"
#include "rendering/shader.hpp"
#include "rendering/camera.hpp"
#include "rendering/frustum.hpp"
#include "pipeline/asset_manager.hpp"
#include "pipeline/blp_loader.hpp"
#include "core/logger.hpp"
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
namespace wowee {
namespace rendering {
void M2Instance::updateModelMatrix() {
modelMatrix = glm::mat4(1.0f);
modelMatrix = glm::translate(modelMatrix, position);
// Rotation in radians
modelMatrix = glm::rotate(modelMatrix, rotation.x, glm::vec3(1.0f, 0.0f, 0.0f));
modelMatrix = glm::rotate(modelMatrix, rotation.y, glm::vec3(0.0f, 1.0f, 0.0f));
modelMatrix = glm::rotate(modelMatrix, rotation.z, glm::vec3(0.0f, 0.0f, 1.0f));
modelMatrix = glm::scale(modelMatrix, glm::vec3(scale));
}
M2Renderer::M2Renderer() {
}
M2Renderer::~M2Renderer() {
shutdown();
}
bool M2Renderer::initialize(pipeline::AssetManager* assets) {
assetManager = assets;
LOG_INFO("Initializing M2 renderer...");
// Create M2 shader
const char* vertexSrc = R"(
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aNormal;
layout (location = 2) in vec2 aTexCoord;
uniform mat4 uModel;
uniform mat4 uView;
uniform mat4 uProjection;
out vec3 FragPos;
out vec3 Normal;
out vec2 TexCoord;
void main() {
vec4 worldPos = uModel * vec4(aPos, 1.0);
FragPos = worldPos.xyz;
Normal = mat3(transpose(inverse(uModel))) * aNormal;
TexCoord = aTexCoord;
gl_Position = uProjection * uView * worldPos;
}
)";
const char* fragmentSrc = R"(
#version 330 core
in vec3 FragPos;
in vec3 Normal;
in vec2 TexCoord;
uniform vec3 uLightDir;
uniform vec3 uAmbientColor;
uniform sampler2D uTexture;
uniform bool uHasTexture;
uniform bool uAlphaTest;
out vec4 FragColor;
void main() {
vec4 texColor;
if (uHasTexture) {
texColor = texture(uTexture, TexCoord);
} else {
texColor = vec4(0.6, 0.5, 0.4, 1.0); // Fallback brownish
}
// Alpha test for leaves, fences, etc.
if (uAlphaTest && texColor.a < 0.5) {
discard;
}
vec3 normal = normalize(Normal);
vec3 lightDir = normalize(uLightDir);
// Two-sided lighting for foliage
float diff = max(abs(dot(normal, lightDir)), 0.3);
vec3 ambient = uAmbientColor * texColor.rgb;
vec3 diffuse = diff * texColor.rgb;
vec3 result = ambient + diffuse;
FragColor = vec4(result, texColor.a);
}
)";
shader = std::make_unique<Shader>();
if (!shader->loadFromSource(vertexSrc, fragmentSrc)) {
LOG_ERROR("Failed to create M2 shader");
return false;
}
// Create white fallback texture
uint8_t white[] = {255, 255, 255, 255};
glGenTextures(1, &whiteTexture);
glBindTexture(GL_TEXTURE_2D, whiteTexture);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 1, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, white);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glBindTexture(GL_TEXTURE_2D, 0);
LOG_INFO("M2 renderer initialized");
return true;
}
void M2Renderer::shutdown() {
LOG_INFO("Shutting down M2 renderer...");
// Delete GPU resources
for (auto& [id, model] : models) {
if (model.vao != 0) glDeleteVertexArrays(1, &model.vao);
if (model.vbo != 0) glDeleteBuffers(1, &model.vbo);
if (model.ebo != 0) glDeleteBuffers(1, &model.ebo);
}
models.clear();
instances.clear();
// Delete cached textures
for (auto& [path, texId] : textureCache) {
if (texId != 0 && texId != whiteTexture) {
glDeleteTextures(1, &texId);
}
}
textureCache.clear();
if (whiteTexture != 0) {
glDeleteTextures(1, &whiteTexture);
whiteTexture = 0;
}
shader.reset();
}
bool M2Renderer::loadModel(const pipeline::M2Model& model, uint32_t modelId) {
if (models.find(modelId) != models.end()) {
// Already loaded
return true;
}
if (model.vertices.empty() || model.indices.empty()) {
LOG_WARNING("M2 model has no geometry: ", model.name);
return false;
}
M2ModelGPU gpuModel;
gpuModel.name = model.name;
gpuModel.boundMin = model.boundMin;
gpuModel.boundMax = model.boundMax;
gpuModel.boundRadius = model.boundRadius;
gpuModel.indexCount = static_cast<uint32_t>(model.indices.size());
gpuModel.vertexCount = static_cast<uint32_t>(model.vertices.size());
// Create VAO
glGenVertexArrays(1, &gpuModel.vao);
glBindVertexArray(gpuModel.vao);
// Create VBO with interleaved vertex data
// Format: position (3), normal (3), texcoord (2)
std::vector<float> vertexData;
vertexData.reserve(model.vertices.size() * 8);
for (const auto& v : model.vertices) {
vertexData.push_back(v.position.x);
vertexData.push_back(v.position.y);
vertexData.push_back(v.position.z);
vertexData.push_back(v.normal.x);
vertexData.push_back(v.normal.y);
vertexData.push_back(v.normal.z);
vertexData.push_back(v.texCoords[0].x);
vertexData.push_back(v.texCoords[0].y);
}
glGenBuffers(1, &gpuModel.vbo);
glBindBuffer(GL_ARRAY_BUFFER, gpuModel.vbo);
glBufferData(GL_ARRAY_BUFFER, vertexData.size() * sizeof(float),
vertexData.data(), GL_STATIC_DRAW);
// Create EBO
glGenBuffers(1, &gpuModel.ebo);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, gpuModel.ebo);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, model.indices.size() * sizeof(uint16_t),
model.indices.data(), GL_STATIC_DRAW);
// Set up vertex attributes
const size_t stride = 8 * sizeof(float);
// Position
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, stride, (void*)0);
// Normal
glEnableVertexAttribArray(1);
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, stride, (void*)(3 * sizeof(float)));
// TexCoord
glEnableVertexAttribArray(2);
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, stride, (void*)(6 * sizeof(float)));
glBindVertexArray(0);
// Load ALL textures from the model into a local vector
std::vector<GLuint> allTextures;
if (assetManager) {
for (const auto& tex : model.textures) {
if (!tex.filename.empty()) {
allTextures.push_back(loadTexture(tex.filename));
} else {
allTextures.push_back(whiteTexture);
}
}
}
// Build per-batch GPU entries
if (!model.batches.empty()) {
for (const auto& batch : model.batches) {
M2ModelGPU::BatchGPU bgpu;
bgpu.indexStart = batch.indexStart;
bgpu.indexCount = batch.indexCount;
// Resolve texture: batch.textureIndex → textureLookup → allTextures
GLuint tex = whiteTexture;
if (batch.textureIndex < model.textureLookup.size()) {
uint16_t texIdx = model.textureLookup[batch.textureIndex];
if (texIdx < allTextures.size()) {
tex = allTextures[texIdx];
}
} else if (!allTextures.empty()) {
tex = allTextures[0];
}
bgpu.texture = tex;
bgpu.hasAlpha = (tex != 0 && tex != whiteTexture);
gpuModel.batches.push_back(bgpu);
}
} else {
// Fallback: single batch covering all indices with first texture
M2ModelGPU::BatchGPU bgpu;
bgpu.indexStart = 0;
bgpu.indexCount = gpuModel.indexCount;
bgpu.texture = allTextures.empty() ? whiteTexture : allTextures[0];
bgpu.hasAlpha = (bgpu.texture != 0 && bgpu.texture != whiteTexture);
gpuModel.batches.push_back(bgpu);
}
models[modelId] = std::move(gpuModel);
LOG_DEBUG("Loaded M2 model: ", model.name, " (", models[modelId].vertexCount, " vertices, ",
models[modelId].indexCount / 3, " triangles, ", models[modelId].batches.size(), " batches)");
return true;
}
uint32_t M2Renderer::createInstance(uint32_t modelId, const glm::vec3& position,
const glm::vec3& rotation, float scale) {
if (models.find(modelId) == models.end()) {
LOG_WARNING("Cannot create instance: model ", modelId, " not loaded");
return 0;
}
M2Instance instance;
instance.id = nextInstanceId++;
instance.modelId = modelId;
instance.position = position;
instance.rotation = rotation;
instance.scale = scale;
instance.updateModelMatrix();
instances.push_back(instance);
return instance.id;
}
uint32_t M2Renderer::createInstanceWithMatrix(uint32_t modelId, const glm::mat4& modelMatrix,
const glm::vec3& position) {
if (models.find(modelId) == models.end()) {
LOG_WARNING("Cannot create instance: model ", modelId, " not loaded");
return 0;
}
M2Instance instance;
instance.id = nextInstanceId++;
instance.modelId = modelId;
instance.position = position; // Used for frustum culling
instance.rotation = glm::vec3(0.0f);
instance.scale = 1.0f;
instance.modelMatrix = modelMatrix;
instances.push_back(instance);
return instance.id;
}
void M2Renderer::render(const Camera& camera, const glm::mat4& view, const glm::mat4& projection) {
(void)camera; // unused for now
if (instances.empty() || !shader) {
return;
}
// Debug: log once when we start rendering
static bool loggedOnce = false;
if (!loggedOnce) {
loggedOnce = true;
LOG_INFO("M2 render: ", instances.size(), " instances, ", models.size(), " models");
}
// Set up GL state for M2 rendering
glEnable(GL_DEPTH_TEST);
glDepthFunc(GL_LEQUAL);
glDisable(GL_BLEND); // No blend leaking from prior renderers
glDisable(GL_CULL_FACE); // Some M2 geometry is single-sided
// Make models render with a bright color for debugging
// glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); // Wireframe mode
// Build frustum for culling
Frustum frustum;
frustum.extractFromMatrix(projection * view);
shader->use();
shader->setUniform("uView", view);
shader->setUniform("uProjection", projection);
shader->setUniform("uLightDir", lightDir);
shader->setUniform("uAmbientColor", ambientColor);
lastDrawCallCount = 0;
for (const auto& instance : instances) {
auto it = models.find(instance.modelId);
if (it == models.end()) continue;
const M2ModelGPU& model = it->second;
if (!model.isValid()) continue;
// Frustum cull: test bounding sphere in world space
float worldRadius = model.boundRadius * instance.scale;
if (worldRadius > 0.0f && !frustum.intersectsSphere(instance.position, worldRadius)) {
continue;
}
shader->setUniform("uModel", instance.modelMatrix);
glBindVertexArray(model.vao);
for (const auto& batch : model.batches) {
if (batch.indexCount == 0) continue;
bool hasTexture = (batch.texture != 0);
shader->setUniform("uHasTexture", hasTexture);
shader->setUniform("uAlphaTest", batch.hasAlpha);
if (hasTexture) {
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, batch.texture);
shader->setUniform("uTexture", 0);
}
glDrawElements(GL_TRIANGLES, batch.indexCount, GL_UNSIGNED_SHORT,
(void*)(batch.indexStart * sizeof(uint16_t)));
lastDrawCallCount++;
}
// Check for GL errors (only first draw)
static bool checkedOnce = false;
if (!checkedOnce) {
checkedOnce = true;
GLenum err = glGetError();
if (err != GL_NO_ERROR) {
LOG_ERROR("GL error after M2 draw: ", err);
} else {
LOG_INFO("M2 draw successful: ", model.indexCount, " indices");
}
}
glBindVertexArray(0);
}
// Restore cull face state
glEnable(GL_CULL_FACE);
}
void M2Renderer::removeInstance(uint32_t instanceId) {
for (auto it = instances.begin(); it != instances.end(); ++it) {
if (it->id == instanceId) {
instances.erase(it);
return;
}
}
}
void M2Renderer::clear() {
for (auto& [id, model] : models) {
if (model.vao != 0) glDeleteVertexArrays(1, &model.vao);
if (model.vbo != 0) glDeleteBuffers(1, &model.vbo);
if (model.ebo != 0) glDeleteBuffers(1, &model.ebo);
}
models.clear();
instances.clear();
}
GLuint M2Renderer::loadTexture(const std::string& path) {
// Check cache
auto it = textureCache.find(path);
if (it != textureCache.end()) {
return it->second;
}
// Load BLP texture
pipeline::BLPImage blp = assetManager->loadTexture(path);
if (!blp.isValid()) {
LOG_WARNING("M2: Failed to load texture: ", path);
textureCache[path] = whiteTexture;
return whiteTexture;
}
GLuint textureID;
glGenTextures(1, &textureID);
glBindTexture(GL_TEXTURE_2D, textureID);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA,
blp.width, blp.height, 0,
GL_RGBA, GL_UNSIGNED_BYTE, blp.data.data());
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
glGenerateMipmap(GL_TEXTURE_2D);
glBindTexture(GL_TEXTURE_2D, 0);
textureCache[path] = textureID;
LOG_DEBUG("M2: Loaded texture: ", path, " (", blp.width, "x", blp.height, ")");
return textureID;
}
uint32_t M2Renderer::getTotalTriangleCount() const {
uint32_t total = 0;
for (const auto& instance : instances) {
auto it = models.find(instance.modelId);
if (it != models.end()) {
total += it->second.indexCount / 3;
}
}
return total;
}
} // namespace rendering
} // namespace wowee

View file

@ -0,0 +1,8 @@
#include "rendering/material.hpp"
// All implementations are inline in header
namespace wowee {
namespace rendering {
// Empty file - all methods are inline
} // namespace rendering
} // namespace wowee

56
src/rendering/mesh.cpp Normal file
View file

@ -0,0 +1,56 @@
#include "rendering/mesh.hpp"
namespace wowee {
namespace rendering {
Mesh::~Mesh() {
destroy();
}
void Mesh::create(const std::vector<Vertex>& vertices, const std::vector<uint32_t>& indices) {
indexCount = indices.size();
glGenVertexArrays(1, &VAO);
glGenBuffers(1, &VBO);
glGenBuffers(1, &EBO);
glBindVertexArray(VAO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, vertices.size() * sizeof(Vertex), vertices.data(), GL_STATIC_DRAW);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices.size() * sizeof(uint32_t), indices.data(), GL_STATIC_DRAW);
// Position
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, position));
// Normal
glEnableVertexAttribArray(1);
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, normal));
// TexCoord
glEnableVertexAttribArray(2);
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, texCoord));
glBindVertexArray(0);
}
void Mesh::destroy() {
if (VAO) glDeleteVertexArrays(1, &VAO);
if (VBO) glDeleteBuffers(1, &VBO);
if (EBO) glDeleteBuffers(1, &EBO);
VAO = VBO = EBO = 0;
}
void Mesh::draw() const {
if (VAO && indexCount > 0) {
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES, indexCount, GL_UNSIGNED_INT, 0);
glBindVertexArray(0);
}
}
} // namespace rendering
} // namespace wowee

213
src/rendering/minimap.cpp Normal file
View file

@ -0,0 +1,213 @@
#include "rendering/minimap.hpp"
#include "rendering/shader.hpp"
#include "rendering/camera.hpp"
#include "rendering/terrain_renderer.hpp"
#include "core/logger.hpp"
#include <GL/glew.h>
#include <glm/gtc/matrix_transform.hpp>
namespace wowee {
namespace rendering {
Minimap::Minimap() = default;
Minimap::~Minimap() {
shutdown();
}
bool Minimap::initialize(int size) {
mapSize = size;
// Create FBO
glGenFramebuffers(1, &fbo);
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
// Color texture
glGenTextures(1, &fboTexture);
glBindTexture(GL_TEXTURE_2D, fboTexture);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, mapSize, mapSize, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, fboTexture, 0);
// Depth renderbuffer
glGenRenderbuffers(1, &fboDepth);
glBindRenderbuffer(GL_RENDERBUFFER, fboDepth);
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, mapSize, mapSize);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, fboDepth);
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
LOG_ERROR("Minimap FBO incomplete");
glBindFramebuffer(GL_FRAMEBUFFER, 0);
return false;
}
glBindFramebuffer(GL_FRAMEBUFFER, 0);
// Screen quad (NDC fullscreen, we'll position via uniforms)
float quadVerts[] = {
// pos (x,y), uv (u,v)
-1.0f, -1.0f, 0.0f, 0.0f,
1.0f, -1.0f, 1.0f, 0.0f,
1.0f, 1.0f, 1.0f, 1.0f,
-1.0f, -1.0f, 0.0f, 0.0f,
1.0f, 1.0f, 1.0f, 1.0f,
-1.0f, 1.0f, 0.0f, 1.0f,
};
glGenVertexArrays(1, &quadVAO);
glGenBuffers(1, &quadVBO);
glBindVertexArray(quadVAO);
glBindBuffer(GL_ARRAY_BUFFER, quadVBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(quadVerts), quadVerts, GL_STATIC_DRAW);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void*)0);
glEnableVertexAttribArray(1);
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void*)(2 * sizeof(float)));
glBindVertexArray(0);
// Quad shader with circular mask and border
const char* vertSrc = R"(
#version 330 core
layout (location = 0) in vec2 aPos;
layout (location = 1) in vec2 aUV;
uniform vec4 uRect; // x, y, w, h in NDC
out vec2 TexCoord;
void main() {
vec2 pos = uRect.xy + aUV * uRect.zw;
gl_Position = vec4(pos * 2.0 - 1.0, 0.0, 1.0);
TexCoord = aUV;
}
)";
const char* fragSrc = R"(
#version 330 core
in vec2 TexCoord;
uniform sampler2D uMapTexture;
out vec4 FragColor;
void main() {
vec2 center = TexCoord - vec2(0.5);
float dist = length(center);
// Circular mask
if (dist > 0.5) discard;
// Gold border ring
float borderWidth = 0.02;
if (dist > 0.5 - borderWidth) {
FragColor = vec4(0.8, 0.65, 0.2, 1.0);
return;
}
vec4 texColor = texture(uMapTexture, TexCoord);
// Player dot at center
if (dist < 0.02) {
FragColor = vec4(1.0, 0.3, 0.3, 1.0);
return;
}
FragColor = texColor;
}
)";
quadShader = std::make_unique<Shader>();
if (!quadShader->loadFromSource(vertSrc, fragSrc)) {
LOG_ERROR("Failed to create minimap shader");
return false;
}
LOG_INFO("Minimap initialized (", mapSize, "x", mapSize, ")");
return true;
}
void Minimap::shutdown() {
if (fbo) { glDeleteFramebuffers(1, &fbo); fbo = 0; }
if (fboTexture) { glDeleteTextures(1, &fboTexture); fboTexture = 0; }
if (fboDepth) { glDeleteRenderbuffers(1, &fboDepth); fboDepth = 0; }
if (quadVAO) { glDeleteVertexArrays(1, &quadVAO); quadVAO = 0; }
if (quadVBO) { glDeleteBuffers(1, &quadVBO); quadVBO = 0; }
quadShader.reset();
}
void Minimap::render(const Camera& playerCamera, int screenWidth, int screenHeight) {
if (!enabled || !terrainRenderer || !fbo) return;
// 1. Render terrain from top-down into FBO
renderTerrainToFBO(playerCamera);
// 2. Draw the minimap quad on screen
renderQuad(screenWidth, screenHeight);
}
void Minimap::renderTerrainToFBO(const Camera& playerCamera) {
// Save current viewport
GLint prevViewport[4];
glGetIntegerv(GL_VIEWPORT, prevViewport);
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
glViewport(0, 0, mapSize, mapSize);
glClearColor(0.05f, 0.1f, 0.15f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// Create a top-down camera at the player's XY position
Camera topDownCamera;
glm::vec3 playerPos = playerCamera.getPosition();
topDownCamera.setPosition(glm::vec3(playerPos.x, playerPos.y, playerPos.z + 5000.0f));
topDownCamera.setRotation(0.0f, -89.9f); // Look straight down
topDownCamera.setAspectRatio(1.0f);
topDownCamera.setFov(1.0f); // Will be overridden by ortho below
// We need orthographic projection, but Camera only supports perspective.
// Use the terrain renderer's render with a custom view/projection.
// For now, render with the top-down camera (perspective, narrow FOV approximates ortho)
// The narrow FOV + high altitude gives a near-orthographic result.
// Calculate FOV that covers viewRadius at the altitude
float altitude = 5000.0f;
float fovDeg = glm::degrees(2.0f * std::atan(viewRadius / altitude));
topDownCamera.setFov(fovDeg);
terrainRenderer->render(topDownCamera);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
// Restore viewport
glViewport(prevViewport[0], prevViewport[1], prevViewport[2], prevViewport[3]);
}
void Minimap::renderQuad(int screenWidth, int screenHeight) {
glDisable(GL_DEPTH_TEST);
quadShader->use();
// Position minimap in top-right corner with margin
float margin = 10.0f;
float pixelW = static_cast<float>(mapSize) / screenWidth;
float pixelH = static_cast<float>(mapSize) / screenHeight;
float x = 1.0f - pixelW - margin / screenWidth;
float y = 1.0f - pixelH - margin / screenHeight;
// uRect: x, y, w, h in 0..1 screen space
quadShader->setUniform("uRect", glm::vec4(x, y, pixelW, pixelH));
quadShader->setUniform("uMapTexture", 0);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, fboTexture);
glBindVertexArray(quadVAO);
glDrawArrays(GL_TRIANGLES, 0, 6);
glBindVertexArray(0);
glEnable(GL_DEPTH_TEST);
}
} // namespace rendering
} // namespace wowee

View file

@ -0,0 +1,416 @@
#include "rendering/performance_hud.hpp"
#include "rendering/renderer.hpp"
#include "rendering/terrain_renderer.hpp"
#include "rendering/terrain_manager.hpp"
#include "rendering/water_renderer.hpp"
#include "rendering/skybox.hpp"
#include "rendering/celestial.hpp"
#include "rendering/starfield.hpp"
#include "rendering/clouds.hpp"
#include "rendering/lens_flare.hpp"
#include "rendering/weather.hpp"
#include "rendering/character_renderer.hpp"
#include "rendering/wmo_renderer.hpp"
#include "rendering/camera.hpp"
#include <imgui.h>
#include <algorithm>
#include <sstream>
#include <iomanip>
namespace wowee {
namespace rendering {
PerformanceHUD::PerformanceHUD() {
}
PerformanceHUD::~PerformanceHUD() {
}
void PerformanceHUD::update(float deltaTime) {
if (!enabled) {
return;
}
// Store frame time
frameTime = deltaTime;
frameTimeHistory.push_back(deltaTime);
// Keep history size limited
while (frameTimeHistory.size() > MAX_FRAME_HISTORY) {
frameTimeHistory.pop_front();
}
// Update stats periodically
updateTimer += deltaTime;
if (updateTimer >= UPDATE_INTERVAL) {
updateTimer = 0.0f;
calculateFPS();
}
}
void PerformanceHUD::calculateFPS() {
if (frameTimeHistory.empty()) {
return;
}
// Current FPS (from last frame time)
currentFPS = frameTime > 0.0001f ? 1.0f / frameTime : 0.0f;
// Average FPS
float sum = 0.0f;
for (float ft : frameTimeHistory) {
sum += ft;
}
float avgFrameTime = sum / frameTimeHistory.size();
averageFPS = avgFrameTime > 0.0001f ? 1.0f / avgFrameTime : 0.0f;
// Min/Max FPS (from last 2 seconds)
minFPS = 10000.0f;
maxFPS = 0.0f;
for (float ft : frameTimeHistory) {
if (ft > 0.0001f) {
float fps = 1.0f / ft;
minFPS = std::min(minFPS, fps);
maxFPS = std::max(maxFPS, fps);
}
}
}
void PerformanceHUD::render(const Renderer* renderer, const Camera* camera) {
if (!enabled || !renderer) {
return;
}
// Set window position based on setting
ImGuiWindowFlags flags = ImGuiWindowFlags_NoDecoration |
ImGuiWindowFlags_AlwaysAutoResize |
ImGuiWindowFlags_NoSavedSettings |
ImGuiWindowFlags_NoFocusOnAppearing |
ImGuiWindowFlags_NoNav;
const float PADDING = 10.0f;
ImGuiViewport* viewport = ImGui::GetMainViewport();
ImVec2 work_pos = viewport->WorkPos;
ImVec2 work_size = viewport->WorkSize;
ImVec2 window_pos, window_pos_pivot;
switch (position) {
case Position::TOP_LEFT:
window_pos.x = work_pos.x + PADDING;
window_pos.y = work_pos.y + PADDING;
window_pos_pivot.x = 0.0f;
window_pos_pivot.y = 0.0f;
break;
case Position::TOP_RIGHT:
window_pos.x = work_pos.x + work_size.x - PADDING;
window_pos.y = work_pos.y + PADDING;
window_pos_pivot.x = 1.0f;
window_pos_pivot.y = 0.0f;
break;
case Position::BOTTOM_LEFT:
window_pos.x = work_pos.x + PADDING;
window_pos.y = work_pos.y + work_size.y - PADDING;
window_pos_pivot.x = 0.0f;
window_pos_pivot.y = 1.0f;
break;
case Position::BOTTOM_RIGHT:
window_pos.x = work_pos.x + work_size.x - PADDING;
window_pos.y = work_pos.y + work_size.y - PADDING;
window_pos_pivot.x = 1.0f;
window_pos_pivot.y = 1.0f;
break;
}
ImGui::SetNextWindowPos(window_pos, ImGuiCond_Always, window_pos_pivot);
ImGui::SetNextWindowBgAlpha(0.7f); // Transparent background
if (!ImGui::Begin("Performance", nullptr, flags)) {
ImGui::End();
return;
}
// FPS section
if (showFPS) {
ImGui::TextColored(ImVec4(1.0f, 1.0f, 0.0f, 1.0f), "PERFORMANCE");
ImGui::Separator();
// Color-code FPS
ImVec4 fpsColor;
if (currentFPS >= 60.0f) {
fpsColor = ImVec4(0.0f, 1.0f, 0.0f, 1.0f); // Green
} else if (currentFPS >= 30.0f) {
fpsColor = ImVec4(1.0f, 1.0f, 0.0f, 1.0f); // Yellow
} else {
fpsColor = ImVec4(1.0f, 0.0f, 0.0f, 1.0f); // Red
}
ImGui::Text("FPS: ");
ImGui::SameLine();
ImGui::TextColored(fpsColor, "%.1f", currentFPS);
ImGui::Text("Avg: %.1f", averageFPS);
ImGui::Text("Min: %.1f", minFPS);
ImGui::Text("Max: %.1f", maxFPS);
ImGui::Text("Frame: %.2f ms", frameTime * 1000.0f);
// Frame time graph
if (!frameTimeHistory.empty()) {
std::vector<float> frameTimesMs;
frameTimesMs.reserve(frameTimeHistory.size());
for (float ft : frameTimeHistory) {
frameTimesMs.push_back(ft * 1000.0f); // Convert to ms
}
ImGui::PlotLines("##frametime", frameTimesMs.data(), static_cast<int>(frameTimesMs.size()),
0, nullptr, 0.0f, 33.33f, ImVec2(200, 40));
}
ImGui::Spacing();
}
// Renderer stats
if (showRenderer) {
auto* terrainRenderer = renderer->getTerrainRenderer();
if (terrainRenderer) {
ImGui::TextColored(ImVec4(0.0f, 1.0f, 1.0f, 1.0f), "RENDERING");
ImGui::Separator();
int totalChunks = terrainRenderer->getChunkCount();
int rendered = terrainRenderer->getRenderedChunkCount();
int culled = terrainRenderer->getCulledChunkCount();
int triangles = terrainRenderer->getTriangleCount();
ImGui::Text("Chunks: %d", totalChunks);
ImGui::Text("Rendered: %d", rendered);
ImGui::Text("Culled: %d", culled);
if (totalChunks > 0) {
float visiblePercent = (rendered * 100.0f) / totalChunks;
ImGui::Text("Visible: %.1f%%", visiblePercent);
}
ImGui::Text("Triangles: %s",
triangles >= 1000000 ?
(std::to_string(triangles / 1000) + "K").c_str() :
std::to_string(triangles).c_str());
ImGui::Spacing();
}
}
// Terrain streaming info
if (showTerrain) {
auto* terrainManager = renderer->getTerrainManager();
if (terrainManager) {
ImGui::TextColored(ImVec4(0.0f, 1.0f, 0.0f, 1.0f), "TERRAIN");
ImGui::Separator();
ImGui::Text("Loaded tiles: %d", terrainManager->getLoadedTileCount());
auto currentTile = terrainManager->getCurrentTile();
ImGui::Text("Current tile: [%d,%d]", currentTile.x, currentTile.y);
ImGui::Spacing();
}
// Water info
auto* waterRenderer = renderer->getWaterRenderer();
if (waterRenderer) {
ImGui::TextColored(ImVec4(0.2f, 0.5f, 1.0f, 1.0f), "WATER");
ImGui::Separator();
ImGui::Text("Surfaces: %d", waterRenderer->getSurfaceCount());
ImGui::Text("Enabled: %s", waterRenderer->isEnabled() ? "YES" : "NO");
ImGui::Spacing();
}
}
// Skybox info
if (showTerrain) {
auto* skybox = renderer->getSkybox();
if (skybox) {
ImGui::TextColored(ImVec4(0.5f, 0.8f, 1.0f, 1.0f), "SKY");
ImGui::Separator();
float time = skybox->getTimeOfDay();
int hours = static_cast<int>(time);
int minutes = static_cast<int>((time - hours) * 60);
ImGui::Text("Time: %02d:%02d", hours, minutes);
ImGui::Text("Auto: %s", skybox->isTimeProgressionEnabled() ? "YES" : "NO");
// Celestial info
auto* celestial = renderer->getCelestial();
if (celestial) {
ImGui::Text("Sun/Moon: %s", celestial->isEnabled() ? "YES" : "NO");
// Moon phase info
float phase = celestial->getMoonPhase();
const char* phaseName = "Unknown";
if (phase < 0.0625f || phase >= 0.9375f) phaseName = "New";
else if (phase < 0.1875f) phaseName = "Wax Cresc";
else if (phase < 0.3125f) phaseName = "1st Qtr";
else if (phase < 0.4375f) phaseName = "Wax Gibb";
else if (phase < 0.5625f) phaseName = "Full";
else if (phase < 0.6875f) phaseName = "Wan Gibb";
else if (phase < 0.8125f) phaseName = "Last Qtr";
else phaseName = "Wan Cresc";
ImGui::Text("Moon: %s (%.0f%%)", phaseName, phase * 100.0f);
ImGui::Text("Cycling: %s", celestial->isMoonPhaseCycling() ? "YES" : "NO");
}
// Star field info
auto* starField = renderer->getStarField();
if (starField) {
ImGui::Text("Stars: %d (%s)", starField->getStarCount(),
starField->isEnabled() ? "ON" : "OFF");
}
// Cloud info
auto* clouds = renderer->getClouds();
if (clouds) {
ImGui::Text("Clouds: %s (%.0f%%)",
clouds->isEnabled() ? "ON" : "OFF",
clouds->getDensity() * 100.0f);
}
// Lens flare info
auto* lensFlare = renderer->getLensFlare();
if (lensFlare) {
ImGui::Text("Lens Flare: %s (%.0f%%)",
lensFlare->isEnabled() ? "ON" : "OFF",
lensFlare->getIntensity() * 100.0f);
}
ImGui::Spacing();
}
}
// Weather info
if (showRenderer) {
auto* weather = renderer->getWeather();
if (weather) {
ImGui::TextColored(ImVec4(0.6f, 0.8f, 1.0f, 1.0f), "WEATHER");
ImGui::Separator();
const char* typeName = "None";
using WeatherType = rendering::Weather::Type;
auto type = weather->getWeatherType();
if (type == WeatherType::RAIN) typeName = "Rain";
else if (type == WeatherType::SNOW) typeName = "Snow";
ImGui::Text("Type: %s", typeName);
if (weather->isEnabled()) {
ImGui::Text("Particles: %d", weather->getParticleCount());
ImGui::Text("Intensity: %.0f%%", weather->getIntensity() * 100.0f);
}
ImGui::Spacing();
}
}
// Fog info
if (showRenderer) {
auto* terrainRenderer = renderer->getTerrainRenderer();
if (terrainRenderer) {
ImGui::TextColored(ImVec4(0.7f, 0.8f, 0.9f, 1.0f), "FOG");
ImGui::Separator();
ImGui::Text("Distance fog: %s", terrainRenderer->isFogEnabled() ? "ON" : "OFF");
ImGui::Spacing();
}
}
// Character info
if (showRenderer) {
auto* charRenderer = renderer->getCharacterRenderer();
if (charRenderer) {
ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.4f, 1.0f), "CHARACTERS");
ImGui::Separator();
ImGui::Text("Instances: %zu", charRenderer->getInstanceCount());
ImGui::Spacing();
}
}
// WMO building info
if (showRenderer) {
auto* wmoRenderer = renderer->getWMORenderer();
if (wmoRenderer) {
ImGui::TextColored(ImVec4(0.8f, 0.7f, 0.6f, 1.0f), "WMO BUILDINGS");
ImGui::Separator();
ImGui::Text("Models: %u", wmoRenderer->getModelCount());
ImGui::Text("Instances: %u", wmoRenderer->getInstanceCount());
ImGui::Text("Triangles: %u", wmoRenderer->getTotalTriangleCount());
ImGui::Text("Draw Calls: %u", wmoRenderer->getDrawCallCount());
ImGui::Spacing();
}
}
// Zone info
{
const std::string& zoneName = renderer->getCurrentZoneName();
if (!zoneName.empty()) {
ImGui::TextColored(ImVec4(1.0f, 0.9f, 0.3f, 1.0f), "ZONE");
ImGui::Separator();
ImGui::Text("%s", zoneName.c_str());
ImGui::Spacing();
}
}
// Camera info
if (showCamera && camera) {
ImGui::TextColored(ImVec4(1.0f, 0.5f, 0.0f, 1.0f), "CAMERA");
ImGui::Separator();
glm::vec3 pos = camera->getPosition();
ImGui::Text("Pos: %.1f, %.1f, %.1f", pos.x, pos.y, pos.z);
glm::vec3 forward = camera->getForward();
ImGui::Text("Dir: %.2f, %.2f, %.2f", forward.x, forward.y, forward.z);
ImGui::Spacing();
}
// Controls help
if (showControls) {
ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "CONTROLS");
ImGui::Separator();
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "F1: Toggle HUD");
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "F2: Wireframe");
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "F3: Single tile");
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "F4: Culling");
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "F5: Stats");
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "F6: Multi-tile");
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "F7: Streaming");
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "F8: Water");
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "F9: Time");
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "F10: Sun/Moon");
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "F11: Stars");
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "F12: Fog");
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "+/-: Change time");
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "C: Clouds");
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "[/]: Density");
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "L: Lens Flare");
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), ",/.: Intensity");
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "M: Moon Cycle");
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), ";/': Moon Phase");
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "W: Weather");
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "</>: Wx Intensity");
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "K: Spawn Character");
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "J: Remove Chars");
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "O: Spawn Test WMO");
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "Shift+O: Real WMO");
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "P: Clear WMOs");
}
ImGui::End();
}
} // namespace rendering
} // namespace wowee

911
src/rendering/renderer.cpp Normal file
View file

@ -0,0 +1,911 @@
#include "rendering/renderer.hpp"
#include "rendering/camera.hpp"
#include "rendering/camera_controller.hpp"
#include "rendering/scene.hpp"
#include "rendering/terrain_renderer.hpp"
#include "rendering/terrain_manager.hpp"
#include "rendering/performance_hud.hpp"
#include "rendering/water_renderer.hpp"
#include "rendering/skybox.hpp"
#include "rendering/celestial.hpp"
#include "rendering/starfield.hpp"
#include "rendering/clouds.hpp"
#include "rendering/lens_flare.hpp"
#include "rendering/weather.hpp"
#include "rendering/swim_effects.hpp"
#include "rendering/character_renderer.hpp"
#include "rendering/wmo_renderer.hpp"
#include "rendering/m2_renderer.hpp"
#include "rendering/minimap.hpp"
#include "pipeline/asset_manager.hpp"
#include "pipeline/m2_loader.hpp"
#include "pipeline/wmo_loader.hpp"
#include "pipeline/adt_loader.hpp"
#include "pipeline/terrain_mesh.hpp"
#include "core/window.hpp"
#include "core/logger.hpp"
#include "game/world.hpp"
#include "game/zone_manager.hpp"
#include "audio/music_manager.hpp"
#include <GL/glew.h>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtx/euler_angles.hpp>
#include <glm/gtc/quaternion.hpp>
#include <cctype>
#include <unordered_map>
#include <unordered_set>
namespace wowee {
namespace rendering {
struct EmoteInfo {
uint32_t animId;
bool loop;
std::string text;
};
// AnimationData.dbc IDs for WotLK HumanMale emotes
// Reference: https://wowdev.wiki/M2/AnimationList
static const std::unordered_map<std::string, EmoteInfo> EMOTE_TABLE = {
{"wave", {67, false, "waves."}},
{"bow", {66, false, "bows down graciously."}},
{"laugh", {70, false, "laughs."}},
{"point", {84, false, "points over there."}},
{"cheer", {68, false, "cheers!"}},
{"dance", {69, true, "begins to dance."}},
{"kneel", {75, false, "kneels down."}},
{"applaud", {80, false, "applauds."}},
{"shout", {81, false, "shouts."}},
{"chicken", {78, false, "clucks like a chicken."}},
{"cry", {77, false, "cries."}},
{"kiss", {76, false, "blows a kiss."}},
{"roar", {74, false, "roars with bestial vigor."}},
{"salute", {113, false, "salutes."}},
{"rude", {73, false, "makes a rude gesture."}},
{"flex", {82, false, "flexes muscles."}},
{"shy", {83, false, "acts shy."}},
{"beg", {79, false, "begs everyone around."}},
{"eat", {61, false, "begins to eat."}},
};
Renderer::Renderer() = default;
Renderer::~Renderer() = default;
bool Renderer::initialize(core::Window* win) {
window = win;
LOG_INFO("Initializing renderer");
// Create camera (in front of Stormwind gate, looking north)
camera = std::make_unique<Camera>();
camera->setPosition(glm::vec3(-8900.0f, -170.0f, 150.0f));
camera->setRotation(0.0f, -5.0f);
camera->setAspectRatio(window->getAspectRatio());
camera->setFov(60.0f);
// Create camera controller
cameraController = std::make_unique<CameraController>(camera.get());
cameraController->setMovementSpeed(100.0f); // Fast movement for terrain exploration
cameraController->setMouseSensitivity(0.15f);
// Create scene
scene = std::make_unique<Scene>();
// Create performance HUD
performanceHUD = std::make_unique<PerformanceHUD>();
performanceHUD->setPosition(PerformanceHUD::Position::TOP_LEFT);
// Create water renderer
waterRenderer = std::make_unique<WaterRenderer>();
if (!waterRenderer->initialize()) {
LOG_WARNING("Failed to initialize water renderer");
waterRenderer.reset();
}
// Create skybox
skybox = std::make_unique<Skybox>();
if (!skybox->initialize()) {
LOG_WARNING("Failed to initialize skybox");
skybox.reset();
} else {
skybox->setTimeOfDay(12.0f); // Start at noon
}
// Create celestial renderer (sun and moon)
celestial = std::make_unique<Celestial>();
if (!celestial->initialize()) {
LOG_WARNING("Failed to initialize celestial renderer");
celestial.reset();
}
// Create star field
starField = std::make_unique<StarField>();
if (!starField->initialize()) {
LOG_WARNING("Failed to initialize star field");
starField.reset();
}
// Create clouds
clouds = std::make_unique<Clouds>();
if (!clouds->initialize()) {
LOG_WARNING("Failed to initialize clouds");
clouds.reset();
} else {
clouds->setDensity(0.5f); // Medium cloud coverage
}
// Create lens flare
lensFlare = std::make_unique<LensFlare>();
if (!lensFlare->initialize()) {
LOG_WARNING("Failed to initialize lens flare");
lensFlare.reset();
}
// Create weather system
weather = std::make_unique<Weather>();
if (!weather->initialize()) {
LOG_WARNING("Failed to initialize weather");
weather.reset();
}
// Create swim effects
swimEffects = std::make_unique<SwimEffects>();
if (!swimEffects->initialize()) {
LOG_WARNING("Failed to initialize swim effects");
swimEffects.reset();
}
// Create character renderer
characterRenderer = std::make_unique<CharacterRenderer>();
if (!characterRenderer->initialize()) {
LOG_WARNING("Failed to initialize character renderer");
characterRenderer.reset();
}
// Create WMO renderer
wmoRenderer = std::make_unique<WMORenderer>();
if (!wmoRenderer->initialize()) {
LOG_WARNING("Failed to initialize WMO renderer");
wmoRenderer.reset();
}
// Create minimap
minimap = std::make_unique<Minimap>();
if (!minimap->initialize(200)) {
LOG_WARNING("Failed to initialize minimap");
minimap.reset();
}
// Create M2 renderer (for doodads)
m2Renderer = std::make_unique<M2Renderer>();
// Note: M2 renderer needs asset manager, will be initialized when terrain loads
// Create zone manager
zoneManager = std::make_unique<game::ZoneManager>();
zoneManager->initialize();
// Create music manager (initialized later with asset manager)
musicManager = std::make_unique<audio::MusicManager>();
LOG_INFO("Renderer initialized");
return true;
}
void Renderer::shutdown() {
if (terrainManager) {
terrainManager->unloadAll();
terrainManager.reset();
}
if (terrainRenderer) {
terrainRenderer->shutdown();
terrainRenderer.reset();
}
if (waterRenderer) {
waterRenderer->shutdown();
waterRenderer.reset();
}
if (skybox) {
skybox->shutdown();
skybox.reset();
}
if (celestial) {
celestial->shutdown();
celestial.reset();
}
if (starField) {
starField->shutdown();
starField.reset();
}
if (clouds) {
clouds.reset();
}
if (lensFlare) {
lensFlare.reset();
}
if (weather) {
weather.reset();
}
if (swimEffects) {
swimEffects->shutdown();
swimEffects.reset();
}
if (characterRenderer) {
characterRenderer->shutdown();
characterRenderer.reset();
}
if (wmoRenderer) {
wmoRenderer->shutdown();
wmoRenderer.reset();
}
if (m2Renderer) {
m2Renderer->shutdown();
m2Renderer.reset();
}
if (musicManager) {
musicManager->shutdown();
musicManager.reset();
}
zoneManager.reset();
performanceHUD.reset();
scene.reset();
cameraController.reset();
camera.reset();
LOG_INFO("Renderer shutdown");
}
void Renderer::beginFrame() {
// Black background (skybox will render over it)
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
}
void Renderer::endFrame() {
// Nothing needed here for now
}
void Renderer::setCharacterFollow(uint32_t instanceId) {
characterInstanceId = instanceId;
if (cameraController && instanceId > 0) {
cameraController->setFollowTarget(&characterPosition);
}
}
void Renderer::updateCharacterAnimation() {
// WoW WotLK AnimationData.dbc IDs
constexpr uint32_t ANIM_STAND = 0;
constexpr uint32_t ANIM_WALK = 4;
constexpr uint32_t ANIM_RUN = 5;
constexpr uint32_t ANIM_JUMP_START = 37;
constexpr uint32_t ANIM_JUMP_MID = 38;
constexpr uint32_t ANIM_JUMP_END = 39;
constexpr uint32_t ANIM_SIT_DOWN = 97; // SitGround — transition to sitting
constexpr uint32_t ANIM_SITTING = 97; // Hold on same animation (no separate idle)
constexpr uint32_t ANIM_SWIM_IDLE = 41; // Treading water (SwimIdle)
constexpr uint32_t ANIM_SWIM = 42; // Swimming forward (Swim)
CharAnimState newState = charAnimState;
bool moving = cameraController->isMoving();
bool grounded = cameraController->isGrounded();
bool jumping = cameraController->isJumping();
bool sprinting = cameraController->isSprinting();
bool sitting = cameraController->isSitting();
bool swim = cameraController->isSwimming();
switch (charAnimState) {
case CharAnimState::IDLE:
if (swim) {
newState = moving ? CharAnimState::SWIM : CharAnimState::SWIM_IDLE;
} else if (sitting && grounded) {
newState = CharAnimState::SIT_DOWN;
} else if (!grounded && jumping) {
newState = CharAnimState::JUMP_START;
} else if (!grounded) {
newState = CharAnimState::JUMP_MID;
} else if (moving && sprinting) {
newState = CharAnimState::RUN;
} else if (moving) {
newState = CharAnimState::WALK;
}
break;
case CharAnimState::WALK:
if (swim) {
newState = moving ? CharAnimState::SWIM : CharAnimState::SWIM_IDLE;
} else if (!grounded && jumping) {
newState = CharAnimState::JUMP_START;
} else if (!grounded) {
newState = CharAnimState::JUMP_MID;
} else if (!moving) {
newState = CharAnimState::IDLE;
} else if (sprinting) {
newState = CharAnimState::RUN;
}
break;
case CharAnimState::RUN:
if (swim) {
newState = moving ? CharAnimState::SWIM : CharAnimState::SWIM_IDLE;
} else if (!grounded && jumping) {
newState = CharAnimState::JUMP_START;
} else if (!grounded) {
newState = CharAnimState::JUMP_MID;
} else if (!moving) {
newState = CharAnimState::IDLE;
} else if (!sprinting) {
newState = CharAnimState::WALK;
}
break;
case CharAnimState::JUMP_START:
if (swim) {
newState = CharAnimState::SWIM_IDLE;
} else if (grounded) {
newState = CharAnimState::JUMP_END;
} else {
newState = CharAnimState::JUMP_MID;
}
break;
case CharAnimState::JUMP_MID:
if (swim) {
newState = CharAnimState::SWIM_IDLE;
} else if (grounded) {
newState = CharAnimState::JUMP_END;
}
break;
case CharAnimState::JUMP_END:
if (swim) {
newState = moving ? CharAnimState::SWIM : CharAnimState::SWIM_IDLE;
} else if (moving && sprinting) {
newState = CharAnimState::RUN;
} else if (moving) {
newState = CharAnimState::WALK;
} else {
newState = CharAnimState::IDLE;
}
break;
case CharAnimState::SIT_DOWN:
if (swim) {
newState = CharAnimState::SWIM_IDLE;
} else if (!sitting) {
newState = CharAnimState::IDLE;
}
break;
case CharAnimState::SITTING:
if (swim) {
newState = CharAnimState::SWIM_IDLE;
} else if (!sitting) {
newState = CharAnimState::IDLE;
}
break;
case CharAnimState::EMOTE:
if (swim) {
cancelEmote();
newState = CharAnimState::SWIM_IDLE;
} else if (jumping || !grounded) {
cancelEmote();
newState = CharAnimState::JUMP_START;
} else if (moving) {
cancelEmote();
newState = sprinting ? CharAnimState::RUN : CharAnimState::WALK;
} else if (sitting) {
cancelEmote();
newState = CharAnimState::SIT_DOWN;
}
break;
case CharAnimState::SWIM_IDLE:
if (!swim) {
newState = moving ? CharAnimState::WALK : CharAnimState::IDLE;
} else if (moving) {
newState = CharAnimState::SWIM;
}
break;
case CharAnimState::SWIM:
if (!swim) {
newState = moving ? CharAnimState::WALK : CharAnimState::IDLE;
} else if (!moving) {
newState = CharAnimState::SWIM_IDLE;
}
break;
}
if (newState != charAnimState) {
charAnimState = newState;
uint32_t animId = ANIM_STAND;
bool loop = true;
switch (charAnimState) {
case CharAnimState::IDLE: animId = ANIM_STAND; loop = true; break;
case CharAnimState::WALK: animId = ANIM_WALK; loop = true; break;
case CharAnimState::RUN: animId = ANIM_RUN; loop = true; break;
case CharAnimState::JUMP_START: animId = ANIM_JUMP_START; loop = false; break;
case CharAnimState::JUMP_MID: animId = ANIM_JUMP_MID; loop = false; break;
case CharAnimState::JUMP_END: animId = ANIM_JUMP_END; loop = false; break;
case CharAnimState::SIT_DOWN: animId = ANIM_SIT_DOWN; loop = false; break;
case CharAnimState::SITTING: animId = ANIM_SITTING; loop = true; break;
case CharAnimState::EMOTE: animId = emoteAnimId; loop = emoteLoop; break;
case CharAnimState::SWIM_IDLE: animId = ANIM_SWIM_IDLE; loop = true; break;
case CharAnimState::SWIM: animId = ANIM_SWIM; loop = true; break;
}
characterRenderer->playAnimation(characterInstanceId, animId, loop);
}
}
void Renderer::playEmote(const std::string& emoteName) {
auto it = EMOTE_TABLE.find(emoteName);
if (it == EMOTE_TABLE.end()) return;
const auto& info = it->second;
emoteActive = true;
emoteAnimId = info.animId;
emoteLoop = info.loop;
charAnimState = CharAnimState::EMOTE;
if (characterRenderer && characterInstanceId > 0) {
characterRenderer->playAnimation(characterInstanceId, emoteAnimId, emoteLoop);
}
}
void Renderer::cancelEmote() {
emoteActive = false;
emoteAnimId = 0;
emoteLoop = false;
}
std::string Renderer::getEmoteText(const std::string& emoteName) {
auto it = EMOTE_TABLE.find(emoteName);
if (it != EMOTE_TABLE.end()) {
return it->second.text;
}
return "";
}
void Renderer::setTargetPosition(const glm::vec3* pos) {
targetPosition = pos;
}
bool Renderer::isMoving() const {
return cameraController && cameraController->isMoving();
}
void Renderer::update(float deltaTime) {
if (cameraController) {
cameraController->update(deltaTime);
}
// Sync character model position/rotation and animation with follow target
if (characterInstanceId > 0 && characterRenderer && cameraController && cameraController->isThirdPerson()) {
characterRenderer->setInstancePosition(characterInstanceId, characterPosition);
// Only rotate character to face camera direction when right-click is held
// Left-click orbits camera without turning the character
if (cameraController->isRightMouseHeld() || cameraController->isMoving()) {
characterYaw = cameraController->getYaw();
} else if (targetPosition && !emoteActive && !cameraController->isMoving()) {
// Face target when idle
glm::vec3 toTarget = *targetPosition - characterPosition;
if (glm::length(glm::vec2(toTarget.x, toTarget.y)) > 0.1f) {
float targetYaw = glm::degrees(std::atan2(toTarget.y, toTarget.x));
// Smooth rotation toward target
float diff = targetYaw - characterYaw;
while (diff > 180.0f) diff -= 360.0f;
while (diff < -180.0f) diff += 360.0f;
float rotSpeed = 360.0f * deltaTime;
if (std::abs(diff) < rotSpeed) {
characterYaw = targetYaw;
} else {
characterYaw += (diff > 0 ? rotSpeed : -rotSpeed);
}
}
}
float yawRad = glm::radians(characterYaw);
characterRenderer->setInstanceRotation(characterInstanceId, glm::vec3(0.0f, 0.0f, yawRad));
// Update animation based on movement state
updateCharacterAnimation();
}
// Update terrain streaming
if (terrainManager && camera) {
terrainManager->update(*camera, deltaTime);
}
// Update skybox time progression
if (skybox) {
skybox->update(deltaTime);
}
// Update star field twinkle
if (starField) {
starField->update(deltaTime);
}
// Update clouds animation
if (clouds) {
clouds->update(deltaTime);
}
// Update celestial (moon phase cycling)
if (celestial) {
celestial->update(deltaTime);
}
// Update weather particles
if (weather && camera) {
weather->update(*camera, deltaTime);
}
// Update swim effects
if (swimEffects && camera && cameraController && waterRenderer) {
swimEffects->update(*camera, *cameraController, *waterRenderer, deltaTime);
}
// Update character animations
if (characterRenderer) {
characterRenderer->update(deltaTime);
}
// Update zone detection and music
if (zoneManager && musicManager && terrainManager && camera) {
// First check tile-based zone
auto tile = terrainManager->getCurrentTile();
uint32_t zoneId = zoneManager->getZoneId(tile.x, tile.y);
// Override with WMO-based detection (e.g., inside Stormwind)
if (wmoRenderer) {
glm::vec3 camPos = camera->getPosition();
uint32_t wmoModelId = 0;
if (wmoRenderer->isInsideWMO(camPos.x, camPos.y, camPos.z, &wmoModelId)) {
// Check if inside Stormwind WMO (model ID 10047)
if (wmoModelId == 10047) {
zoneId = 1519; // Stormwind City
}
}
}
if (zoneId != currentZoneId && zoneId != 0) {
currentZoneId = zoneId;
auto* info = zoneManager->getZoneInfo(zoneId);
if (info) {
currentZoneName = info->name;
LOG_INFO("Entered zone: ", info->name);
std::string music = zoneManager->getRandomMusic(zoneId);
if (!music.empty()) {
musicManager->crossfadeTo(music);
}
}
}
musicManager->update(deltaTime);
}
// Update performance HUD
if (performanceHUD) {
performanceHUD->update(deltaTime);
}
}
void Renderer::renderWorld(game::World* world) {
(void)world; // Unused for now
// Get time of day for sky-related rendering
float timeOfDay = skybox ? skybox->getTimeOfDay() : 12.0f;
// Render skybox first (furthest back)
if (skybox && camera) {
skybox->render(*camera, timeOfDay);
}
// Render stars after skybox
if (starField && camera) {
starField->render(*camera, timeOfDay);
}
// Render celestial bodies (sun/moon) after stars
if (celestial && camera) {
celestial->render(*camera, timeOfDay);
}
// Render clouds after celestial bodies
if (clouds && camera) {
clouds->render(*camera, timeOfDay);
}
// Render lens flare (screen-space effect, render after celestial bodies)
if (lensFlare && camera && celestial) {
glm::vec3 sunPosition = celestial->getSunPosition(timeOfDay);
lensFlare->render(*camera, sunPosition, timeOfDay);
}
// Render terrain if loaded and enabled
if (terrainEnabled && terrainLoaded && terrainRenderer && camera) {
// Check if camera is underwater for fog override
bool underwater = false;
if (waterRenderer && camera) {
glm::vec3 camPos = camera->getPosition();
auto waterH = waterRenderer->getWaterHeightAt(camPos.x, camPos.y);
if (waterH && camPos.z < *waterH) {
underwater = true;
}
}
if (underwater) {
float fogColor[3] = {0.05f, 0.15f, 0.25f};
terrainRenderer->setFog(fogColor, 10.0f, 200.0f);
glClearColor(0.05f, 0.15f, 0.25f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT); // Re-clear with underwater color
} else if (skybox) {
// Update terrain fog based on time of day (match sky color)
glm::vec3 horizonColor = skybox->getHorizonColor(timeOfDay);
float fogColorArray[3] = {horizonColor.r, horizonColor.g, horizonColor.b};
terrainRenderer->setFog(fogColorArray, 400.0f, 1200.0f);
}
terrainRenderer->render(*camera);
// Render water after terrain (transparency requires back-to-front rendering)
if (waterRenderer) {
// Use accumulated time for water animation
static float time = 0.0f;
time += 0.016f; // Approximate frame time
waterRenderer->render(*camera, time);
}
}
// Render weather particles (after terrain/water, before characters)
if (weather && camera) {
weather->render(*camera);
}
// Render swim effects (ripples and bubbles)
if (swimEffects && camera) {
swimEffects->render(*camera);
}
// Render characters (after weather)
if (characterRenderer && camera) {
glm::mat4 view = camera->getViewMatrix();
glm::mat4 projection = camera->getProjectionMatrix();
characterRenderer->render(*camera, view, projection);
}
// Render WMO buildings (after characters, before UI)
if (wmoRenderer && camera) {
glm::mat4 view = camera->getViewMatrix();
glm::mat4 projection = camera->getProjectionMatrix();
wmoRenderer->render(*camera, view, projection);
}
// Render M2 doodads (trees, rocks, etc.)
if (m2Renderer && camera) {
glm::mat4 view = camera->getViewMatrix();
glm::mat4 projection = camera->getProjectionMatrix();
m2Renderer->render(*camera, view, projection);
}
// Render minimap overlay
if (minimap && camera && window) {
minimap->render(*camera, window->getWidth(), window->getHeight());
}
}
bool Renderer::loadTestTerrain(pipeline::AssetManager* assetManager, const std::string& adtPath) {
if (!assetManager) {
LOG_ERROR("Asset manager is null");
return false;
}
LOG_INFO("Loading test terrain: ", adtPath);
// Create terrain renderer if not already created
if (!terrainRenderer) {
terrainRenderer = std::make_unique<TerrainRenderer>();
if (!terrainRenderer->initialize(assetManager)) {
LOG_ERROR("Failed to initialize terrain renderer");
terrainRenderer.reset();
return false;
}
}
// Create and initialize terrain manager
if (!terrainManager) {
terrainManager = std::make_unique<TerrainManager>();
if (!terrainManager->initialize(assetManager, terrainRenderer.get())) {
LOG_ERROR("Failed to initialize terrain manager");
terrainManager.reset();
return false;
}
// Set water renderer for terrain streaming
if (waterRenderer) {
terrainManager->setWaterRenderer(waterRenderer.get());
}
// Set M2 renderer for doodad loading during streaming
if (m2Renderer) {
terrainManager->setM2Renderer(m2Renderer.get());
}
// Set WMO renderer for building loading during streaming
if (wmoRenderer) {
terrainManager->setWMORenderer(wmoRenderer.get());
}
// Pass asset manager to character renderer for texture loading
if (characterRenderer) {
characterRenderer->setAssetManager(assetManager);
}
// Wire terrain renderer to minimap
if (minimap) {
minimap->setTerrainRenderer(terrainRenderer.get());
}
// Wire terrain manager, WMO renderer, and water renderer to camera controller
if (cameraController) {
cameraController->setTerrainManager(terrainManager.get());
if (wmoRenderer) {
cameraController->setWMORenderer(wmoRenderer.get());
}
if (waterRenderer) {
cameraController->setWaterRenderer(waterRenderer.get());
}
}
}
// Parse tile coordinates from ADT path
// Format: World\Maps\{MapName}\{MapName}_{X}_{Y}.adt
int tileX = 32, tileY = 49; // defaults
{
// Find last path separator
size_t lastSep = adtPath.find_last_of("\\/");
if (lastSep != std::string::npos) {
std::string filename = adtPath.substr(lastSep + 1);
// Find first underscore after map name
size_t firstUnderscore = filename.find('_');
if (firstUnderscore != std::string::npos) {
size_t secondUnderscore = filename.find('_', firstUnderscore + 1);
if (secondUnderscore != std::string::npos) {
size_t dot = filename.find('.', secondUnderscore);
if (dot != std::string::npos) {
tileX = std::stoi(filename.substr(firstUnderscore + 1, secondUnderscore - firstUnderscore - 1));
tileY = std::stoi(filename.substr(secondUnderscore + 1, dot - secondUnderscore - 1));
}
}
}
// Extract map name
std::string mapName = filename.substr(0, firstUnderscore != std::string::npos ? firstUnderscore : filename.size());
terrainManager->setMapName(mapName);
}
}
LOG_INFO("Loading initial tile [", tileX, ",", tileY, "] via terrain manager");
// Load the initial tile through TerrainManager (properly tracked for streaming)
if (!terrainManager->loadTile(tileX, tileY)) {
LOG_ERROR("Failed to load initial tile [", tileX, ",", tileY, "]");
return false;
}
terrainLoaded = true;
// Initialize music manager with asset manager
if (musicManager && assetManager && !cachedAssetManager) {
musicManager->initialize(assetManager);
cachedAssetManager = assetManager;
}
// Snap camera to ground now that terrain is loaded
if (cameraController) {
cameraController->reset();
}
LOG_INFO("Test terrain loaded successfully!");
LOG_INFO(" Chunks: ", terrainRenderer->getChunkCount());
LOG_INFO(" Triangles: ", terrainRenderer->getTriangleCount());
return true;
}
void Renderer::setWireframeMode(bool enabled) {
if (terrainRenderer) {
terrainRenderer->setWireframe(enabled);
}
}
bool Renderer::loadTerrainArea(const std::string& mapName, int centerX, int centerY, int radius) {
// Create terrain renderer if not already created
if (!terrainRenderer) {
LOG_ERROR("Terrain renderer not initialized");
return false;
}
// Create terrain manager if not already created
if (!terrainManager) {
terrainManager = std::make_unique<TerrainManager>();
// Wire terrain manager to camera controller for grounding
if (cameraController) {
cameraController->setTerrainManager(terrainManager.get());
}
}
LOG_INFO("Loading terrain area: ", mapName, " [", centerX, ",", centerY, "] radius=", radius);
terrainManager->setMapName(mapName);
terrainManager->setLoadRadius(radius);
terrainManager->setUnloadRadius(radius + 1);
// Load tiles in radius
for (int dy = -radius; dy <= radius; dy++) {
for (int dx = -radius; dx <= radius; dx++) {
int tileX = centerX + dx;
int tileY = centerY + dy;
if (tileX >= 0 && tileX <= 63 && tileY >= 0 && tileY <= 63) {
terrainManager->loadTile(tileX, tileY);
}
}
}
terrainLoaded = true;
// Initialize music manager with asset manager (if available from loadTestTerrain)
if (musicManager && cachedAssetManager) {
if (!musicManager->isInitialized()) {
musicManager->initialize(cachedAssetManager);
}
}
// Wire WMO and water renderer to camera controller
if (cameraController && wmoRenderer) {
cameraController->setWMORenderer(wmoRenderer.get());
}
if (cameraController && waterRenderer) {
cameraController->setWaterRenderer(waterRenderer.get());
}
// Snap camera to ground now that terrain is loaded
if (cameraController) {
cameraController->reset();
}
LOG_INFO("Terrain area loaded: ", terrainManager->getLoadedTileCount(), " tiles");
return true;
}
void Renderer::setTerrainStreaming(bool enabled) {
if (terrainManager) {
terrainManager->setStreamingEnabled(enabled);
LOG_INFO("Terrain streaming: ", enabled ? "ON" : "OFF");
}
}
void Renderer::renderHUD() {
if (performanceHUD && camera) {
performanceHUD->render(this, camera.get());
}
}
} // namespace rendering
} // namespace wowee

24
src/rendering/scene.cpp Normal file
View file

@ -0,0 +1,24 @@
#include "rendering/scene.hpp"
#include "rendering/mesh.hpp"
#include <algorithm>
namespace wowee {
namespace rendering {
void Scene::addMesh(std::shared_ptr<Mesh> mesh) {
meshes.push_back(mesh);
}
void Scene::removeMesh(std::shared_ptr<Mesh> mesh) {
auto it = std::find(meshes.begin(), meshes.end(), mesh);
if (it != meshes.end()) {
meshes.erase(it);
}
}
void Scene::clear() {
meshes.clear();
}
} // namespace rendering
} // namespace wowee

127
src/rendering/shader.cpp Normal file
View file

@ -0,0 +1,127 @@
#include "rendering/shader.hpp"
#include "core/logger.hpp"
#include <fstream>
#include <sstream>
namespace wowee {
namespace rendering {
Shader::~Shader() {
if (program) glDeleteProgram(program);
if (vertexShader) glDeleteShader(vertexShader);
if (fragmentShader) glDeleteShader(fragmentShader);
}
bool Shader::loadFromFile(const std::string& vertexPath, const std::string& fragmentPath) {
// Load vertex shader
std::ifstream vFile(vertexPath);
if (!vFile.is_open()) {
LOG_ERROR("Failed to open vertex shader: ", vertexPath);
return false;
}
std::stringstream vStream;
vStream << vFile.rdbuf();
std::string vertexSource = vStream.str();
// Load fragment shader
std::ifstream fFile(fragmentPath);
if (!fFile.is_open()) {
LOG_ERROR("Failed to open fragment shader: ", fragmentPath);
return false;
}
std::stringstream fStream;
fStream << fFile.rdbuf();
std::string fragmentSource = fStream.str();
return compile(vertexSource, fragmentSource);
}
bool Shader::loadFromSource(const std::string& vertexSource, const std::string& fragmentSource) {
return compile(vertexSource, fragmentSource);
}
bool Shader::compile(const std::string& vertexSource, const std::string& fragmentSource) {
GLint success;
GLchar infoLog[512];
// Compile vertex shader
const char* vCode = vertexSource.c_str();
vertexShader = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertexShader, 1, &vCode, nullptr);
glCompileShader(vertexShader);
glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);
if (!success) {
glGetShaderInfoLog(vertexShader, 512, nullptr, infoLog);
LOG_ERROR("Vertex shader compilation failed: ", infoLog);
return false;
}
// Compile fragment shader
const char* fCode = fragmentSource.c_str();
fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader, 1, &fCode, nullptr);
glCompileShader(fragmentShader);
glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &success);
if (!success) {
glGetShaderInfoLog(fragmentShader, 512, nullptr, infoLog);
LOG_ERROR("Fragment shader compilation failed: ", infoLog);
return false;
}
// Link program
program = glCreateProgram();
glAttachShader(program, vertexShader);
glAttachShader(program, fragmentShader);
glLinkProgram(program);
glGetProgramiv(program, GL_LINK_STATUS, &success);
if (!success) {
glGetProgramInfoLog(program, 512, nullptr, infoLog);
LOG_ERROR("Shader program linking failed: ", infoLog);
return false;
}
return true;
}
void Shader::use() const {
glUseProgram(program);
}
void Shader::unuse() const {
glUseProgram(0);
}
GLint Shader::getUniformLocation(const std::string& name) const {
return glGetUniformLocation(program, name.c_str());
}
void Shader::setUniform(const std::string& name, int value) {
glUniform1i(getUniformLocation(name), value);
}
void Shader::setUniform(const std::string& name, float value) {
glUniform1f(getUniformLocation(name), value);
}
void Shader::setUniform(const std::string& name, const glm::vec2& value) {
glUniform2fv(getUniformLocation(name), 1, &value[0]);
}
void Shader::setUniform(const std::string& name, const glm::vec3& value) {
glUniform3fv(getUniformLocation(name), 1, &value[0]);
}
void Shader::setUniform(const std::string& name, const glm::vec4& value) {
glUniform4fv(getUniformLocation(name), 1, &value[0]);
}
void Shader::setUniform(const std::string& name, const glm::mat3& value) {
glUniformMatrix3fv(getUniformLocation(name), 1, GL_FALSE, &value[0][0]);
}
void Shader::setUniform(const std::string& name, const glm::mat4& value) {
glUniformMatrix4fv(getUniformLocation(name), 1, GL_FALSE, &value[0][0]);
}
} // namespace rendering
} // namespace wowee

334
src/rendering/skybox.cpp Normal file
View file

@ -0,0 +1,334 @@
#include "rendering/skybox.hpp"
#include "rendering/shader.hpp"
#include "rendering/camera.hpp"
#include "core/logger.hpp"
#include <GL/glew.h>
#include <glm/gtc/matrix_transform.hpp>
#include <cmath>
#include <vector>
namespace wowee {
namespace rendering {
Skybox::Skybox() = default;
Skybox::~Skybox() {
shutdown();
}
bool Skybox::initialize() {
LOG_INFO("Initializing skybox");
// Create sky shader
skyShader = std::make_unique<Shader>();
// Vertex shader - position-only skybox
const char* vertexShaderSource = R"(
#version 330 core
layout (location = 0) in vec3 aPos;
uniform mat4 view;
uniform mat4 projection;
out vec3 WorldPos;
out float Altitude;
void main() {
WorldPos = aPos;
// Calculate altitude (0 at horizon, 1 at zenith)
Altitude = normalize(aPos).z;
// Remove translation from view matrix (keep rotation only)
mat4 viewNoTranslation = mat4(mat3(view));
gl_Position = projection * viewNoTranslation * vec4(aPos, 1.0);
// Ensure skybox is always at far plane
gl_Position = gl_Position.xyww;
}
)";
// Fragment shader - gradient sky with time of day
const char* fragmentShaderSource = R"(
#version 330 core
in vec3 WorldPos;
in float Altitude;
uniform vec3 horizonColor;
uniform vec3 zenithColor;
uniform float timeOfDay;
out vec4 FragColor;
void main() {
// Smooth gradient from horizon to zenith
float t = pow(max(Altitude, 0.0), 0.5); // Curve for more interesting gradient
vec3 skyColor = mix(horizonColor, zenithColor, t);
// Add atmospheric scattering effect (more saturated near horizon)
float scattering = 1.0 - t * 0.3;
skyColor *= scattering;
FragColor = vec4(skyColor, 1.0);
}
)";
if (!skyShader->loadFromSource(vertexShaderSource, fragmentShaderSource)) {
LOG_ERROR("Failed to create sky shader");
return false;
}
// Create sky dome mesh
createSkyDome();
LOG_INFO("Skybox initialized");
return true;
}
void Skybox::shutdown() {
destroySkyDome();
skyShader.reset();
}
void Skybox::render(const Camera& camera, float time) {
if (!renderingEnabled || vao == 0 || !skyShader) {
return;
}
// Render skybox first (before terrain), with depth test set to LEQUAL
glDepthFunc(GL_LEQUAL);
skyShader->use();
// Set uniforms
glm::mat4 view = camera.getViewMatrix();
glm::mat4 projection = camera.getProjectionMatrix();
skyShader->setUniform("view", view);
skyShader->setUniform("projection", projection);
skyShader->setUniform("timeOfDay", time);
// Get colors based on time of day
glm::vec3 horizon = getHorizonColor(time);
glm::vec3 zenith = getZenithColor(time);
skyShader->setUniform("horizonColor", horizon);
skyShader->setUniform("zenithColor", zenith);
// Render dome
glBindVertexArray(vao);
glDrawElements(GL_TRIANGLES, indexCount, GL_UNSIGNED_INT, nullptr);
glBindVertexArray(0);
// Restore depth function
glDepthFunc(GL_LESS);
}
void Skybox::update(float deltaTime) {
if (timeProgressionEnabled) {
timeOfDay += deltaTime * timeSpeed;
// Wrap around 24 hours
if (timeOfDay >= 24.0f) {
timeOfDay -= 24.0f;
}
}
}
void Skybox::setTimeOfDay(float time) {
// Clamp to 0-24 range
while (time < 0.0f) time += 24.0f;
while (time >= 24.0f) time -= 24.0f;
timeOfDay = time;
}
void Skybox::createSkyDome() {
// Create an extended dome that goes below horizon for better coverage
const int rings = 16; // Vertical resolution
const int sectors = 32; // Horizontal resolution
const float radius = 2000.0f; // Large enough to cover view without looking curved
std::vector<float> vertices;
std::vector<uint32_t> indices;
// Generate vertices - extend slightly below horizon
const float minPhi = -M_PI / 12.0f; // Start 15° below horizon
const float maxPhi = M_PI / 2.0f; // End at zenith
for (int ring = 0; ring <= rings; ring++) {
float phi = minPhi + (maxPhi - minPhi) * (static_cast<float>(ring) / rings);
float y = radius * std::sin(phi);
float ringRadius = radius * std::cos(phi);
for (int sector = 0; sector <= sectors; sector++) {
float theta = (2.0f * M_PI) * (static_cast<float>(sector) / sectors);
float x = ringRadius * std::cos(theta);
float z = ringRadius * std::sin(theta);
// Position
vertices.push_back(x);
vertices.push_back(z); // Z up in WoW coordinates
vertices.push_back(y);
}
}
// Generate indices
for (int ring = 0; ring < rings; ring++) {
for (int sector = 0; sector < sectors; sector++) {
int current = ring * (sectors + 1) + sector;
int next = current + sectors + 1;
// Two triangles per quad
indices.push_back(current);
indices.push_back(next);
indices.push_back(current + 1);
indices.push_back(current + 1);
indices.push_back(next);
indices.push_back(next + 1);
}
}
indexCount = static_cast<int>(indices.size());
// Create OpenGL buffers
glGenVertexArrays(1, &vao);
glGenBuffers(1, &vbo);
glGenBuffers(1, &ebo);
glBindVertexArray(vao);
// Upload vertex data
glBindBuffer(GL_ARRAY_BUFFER, vbo);
glBufferData(GL_ARRAY_BUFFER, vertices.size() * sizeof(float), vertices.data(), GL_STATIC_DRAW);
// Upload index data
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices.size() * sizeof(uint32_t), indices.data(), GL_STATIC_DRAW);
// Set vertex attributes (position only)
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
glBindVertexArray(0);
LOG_DEBUG("Sky dome created: ", (rings + 1) * (sectors + 1), " vertices, ", indexCount / 3, " triangles");
}
void Skybox::destroySkyDome() {
if (vao != 0) {
glDeleteVertexArrays(1, &vao);
vao = 0;
}
if (vbo != 0) {
glDeleteBuffers(1, &vbo);
vbo = 0;
}
if (ebo != 0) {
glDeleteBuffers(1, &ebo);
ebo = 0;
}
}
glm::vec3 Skybox::getHorizonColor(float time) const {
// Time-based horizon colors
// 0-6: Night (dark blue)
// 6-8: Dawn (orange/pink)
// 8-16: Day (light blue)
// 16-18: Dusk (orange/red)
// 18-24: Night (dark blue)
if (time < 5.0f || time >= 21.0f) {
// Night - dark blue/purple horizon
return glm::vec3(0.05f, 0.05f, 0.15f);
}
else if (time >= 5.0f && time < 7.0f) {
// Dawn - blend from night to orange
float t = (time - 5.0f) / 2.0f;
glm::vec3 night = glm::vec3(0.05f, 0.05f, 0.15f);
glm::vec3 dawn = glm::vec3(1.0f, 0.5f, 0.2f);
return glm::mix(night, dawn, t);
}
else if (time >= 7.0f && time < 9.0f) {
// Morning - blend from orange to blue
float t = (time - 7.0f) / 2.0f;
glm::vec3 dawn = glm::vec3(1.0f, 0.5f, 0.2f);
glm::vec3 day = glm::vec3(0.6f, 0.7f, 0.9f);
return glm::mix(dawn, day, t);
}
else if (time >= 9.0f && time < 17.0f) {
// Day - light blue horizon
return glm::vec3(0.6f, 0.7f, 0.9f);
}
else if (time >= 17.0f && time < 19.0f) {
// Dusk - blend from blue to orange/red
float t = (time - 17.0f) / 2.0f;
glm::vec3 day = glm::vec3(0.6f, 0.7f, 0.9f);
glm::vec3 dusk = glm::vec3(1.0f, 0.4f, 0.1f);
return glm::mix(day, dusk, t);
}
else {
// Evening - blend from orange to night
float t = (time - 19.0f) / 2.0f;
glm::vec3 dusk = glm::vec3(1.0f, 0.4f, 0.1f);
glm::vec3 night = glm::vec3(0.05f, 0.05f, 0.15f);
return glm::mix(dusk, night, t);
}
}
glm::vec3 Skybox::getZenithColor(float time) const {
// Zenith (top of sky) colors
if (time < 5.0f || time >= 21.0f) {
// Night - very dark blue, almost black
return glm::vec3(0.01f, 0.01f, 0.05f);
}
else if (time >= 5.0f && time < 7.0f) {
// Dawn - blend from night to light blue
float t = (time - 5.0f) / 2.0f;
glm::vec3 night = glm::vec3(0.01f, 0.01f, 0.05f);
glm::vec3 dawn = glm::vec3(0.3f, 0.4f, 0.7f);
return glm::mix(night, dawn, t);
}
else if (time >= 7.0f && time < 9.0f) {
// Morning - blend to bright blue
float t = (time - 7.0f) / 2.0f;
glm::vec3 dawn = glm::vec3(0.3f, 0.4f, 0.7f);
glm::vec3 day = glm::vec3(0.2f, 0.5f, 1.0f);
return glm::mix(dawn, day, t);
}
else if (time >= 9.0f && time < 17.0f) {
// Day - bright blue zenith
return glm::vec3(0.2f, 0.5f, 1.0f);
}
else if (time >= 17.0f && time < 19.0f) {
// Dusk - blend to darker blue
float t = (time - 17.0f) / 2.0f;
glm::vec3 day = glm::vec3(0.2f, 0.5f, 1.0f);
glm::vec3 dusk = glm::vec3(0.1f, 0.2f, 0.4f);
return glm::mix(day, dusk, t);
}
else {
// Evening - blend to night
float t = (time - 19.0f) / 2.0f;
glm::vec3 dusk = glm::vec3(0.1f, 0.2f, 0.4f);
glm::vec3 night = glm::vec3(0.01f, 0.01f, 0.05f);
return glm::mix(dusk, night, t);
}
}
glm::vec3 Skybox::getSkyColor(float altitude, float time) const {
// Blend between horizon and zenith based on altitude
glm::vec3 horizon = getHorizonColor(time);
glm::vec3 zenith = getZenithColor(time);
// Use power curve for more natural gradient
float t = std::pow(std::max(altitude, 0.0f), 0.5f);
return glm::mix(horizon, zenith, t);
}
} // namespace rendering
} // namespace wowee

259
src/rendering/starfield.cpp Normal file
View file

@ -0,0 +1,259 @@
#include "rendering/starfield.hpp"
#include "rendering/shader.hpp"
#include "rendering/camera.hpp"
#include "core/logger.hpp"
#include <GL/glew.h>
#include <glm/gtc/matrix_transform.hpp>
#include <cmath>
#include <random>
namespace wowee {
namespace rendering {
StarField::StarField() = default;
StarField::~StarField() {
shutdown();
}
bool StarField::initialize() {
LOG_INFO("Initializing star field");
// Create star shader
starShader = std::make_unique<Shader>();
// Vertex shader - simple point rendering
const char* vertexShaderSource = R"(
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in float aBrightness;
layout (location = 2) in float aTwinklePhase;
uniform mat4 view;
uniform mat4 projection;
uniform float time;
uniform float intensity;
out float Brightness;
void main() {
// Remove translation from view matrix (stars are infinitely far)
mat4 viewNoTranslation = mat4(mat3(view));
gl_Position = projection * viewNoTranslation * vec4(aPos, 1.0);
// Twinkle effect (subtle brightness variation)
float twinkle = sin(time * 2.0 + aTwinklePhase) * 0.2 + 0.8; // 0.6 to 1.0
Brightness = aBrightness * twinkle * intensity;
// Point size based on brightness
gl_PointSize = 2.0 + aBrightness * 2.0; // 2-4 pixels
}
)";
// Fragment shader - star color
const char* fragmentShaderSource = R"(
#version 330 core
in float Brightness;
out vec4 FragColor;
void main() {
// Circular point (not square)
vec2 coord = gl_PointCoord - vec2(0.5);
float dist = length(coord);
if (dist > 0.5) {
discard;
}
// Soften edges
float alpha = smoothstep(0.5, 0.3, dist);
// Star color (slightly blue-white)
vec3 starColor = vec3(0.9, 0.95, 1.0);
FragColor = vec4(starColor * Brightness, alpha * Brightness);
}
)";
if (!starShader->loadFromSource(vertexShaderSource, fragmentShaderSource)) {
LOG_ERROR("Failed to create star shader");
return false;
}
// Generate random stars
generateStars();
// Create OpenGL buffers
createStarBuffers();
LOG_INFO("Star field initialized: ", starCount, " stars");
return true;
}
void StarField::shutdown() {
destroyStarBuffers();
starShader.reset();
stars.clear();
}
void StarField::render(const Camera& camera, float timeOfDay) {
if (!renderingEnabled || vao == 0 || !starShader || stars.empty()) {
return;
}
// Get star intensity based on time of day
float intensity = getStarIntensity(timeOfDay);
// Don't render if stars would be invisible
if (intensity <= 0.01f) {
return;
}
// Enable blending for star glow
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
// Enable point sprites
glEnable(GL_PROGRAM_POINT_SIZE);
// Disable depth writing (stars are background)
glDepthMask(GL_FALSE);
starShader->use();
// Set uniforms
glm::mat4 view = camera.getViewMatrix();
glm::mat4 projection = camera.getProjectionMatrix();
starShader->setUniform("view", view);
starShader->setUniform("projection", projection);
starShader->setUniform("time", twinkleTime);
starShader->setUniform("intensity", intensity);
// Render stars as points
glBindVertexArray(vao);
glDrawArrays(GL_POINTS, 0, starCount);
glBindVertexArray(0);
// Restore state
glDepthMask(GL_TRUE);
glDisable(GL_PROGRAM_POINT_SIZE);
glDisable(GL_BLEND);
}
void StarField::update(float deltaTime) {
// Update twinkle animation
twinkleTime += deltaTime;
}
void StarField::generateStars() {
stars.clear();
stars.reserve(starCount);
// Random number generator
std::random_device rd;
std::mt19937 gen(rd());
std::uniform_real_distribution<float> phiDist(0.0f, M_PI / 2.0f); // 0 to 90 degrees (hemisphere)
std::uniform_real_distribution<float> thetaDist(0.0f, 2.0f * M_PI); // 0 to 360 degrees
std::uniform_real_distribution<float> brightnessDist(0.3f, 1.0f); // Varying brightness
std::uniform_real_distribution<float> twinkleDist(0.0f, 2.0f * M_PI); // Random twinkle phase
const float radius = 900.0f; // Slightly larger than skybox
for (int i = 0; i < starCount; i++) {
Star star;
// Spherical coordinates (hemisphere)
float phi = phiDist(gen); // Elevation angle
float theta = thetaDist(gen); // Azimuth angle
// Convert to Cartesian coordinates
float x = radius * std::sin(phi) * std::cos(theta);
float y = radius * std::sin(phi) * std::sin(theta);
float z = radius * std::cos(phi);
star.position = glm::vec3(x, y, z);
star.brightness = brightnessDist(gen);
star.twinklePhase = twinkleDist(gen);
stars.push_back(star);
}
LOG_DEBUG("Generated ", stars.size(), " stars");
}
void StarField::createStarBuffers() {
// Prepare vertex data (position, brightness, twinkle phase)
std::vector<float> vertexData;
vertexData.reserve(stars.size() * 5); // 3 pos + 1 brightness + 1 phase
for (const auto& star : stars) {
vertexData.push_back(star.position.x);
vertexData.push_back(star.position.y);
vertexData.push_back(star.position.z);
vertexData.push_back(star.brightness);
vertexData.push_back(star.twinklePhase);
}
// Create OpenGL buffers
glGenVertexArrays(1, &vao);
glGenBuffers(1, &vbo);
glBindVertexArray(vao);
// Upload vertex data
glBindBuffer(GL_ARRAY_BUFFER, vbo);
glBufferData(GL_ARRAY_BUFFER, vertexData.size() * sizeof(float), vertexData.data(), GL_STATIC_DRAW);
// Set vertex attributes
// Position
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
// Brightness
glVertexAttribPointer(1, 1, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(3 * sizeof(float)));
glEnableVertexAttribArray(1);
// Twinkle phase
glVertexAttribPointer(2, 1, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(4 * sizeof(float)));
glEnableVertexAttribArray(2);
glBindVertexArray(0);
}
void StarField::destroyStarBuffers() {
if (vao != 0) {
glDeleteVertexArrays(1, &vao);
vao = 0;
}
if (vbo != 0) {
glDeleteBuffers(1, &vbo);
vbo = 0;
}
}
float StarField::getStarIntensity(float timeOfDay) const {
// Stars visible at night (fade in/out at dusk/dawn)
// Full night: 20:00-4:00
if (timeOfDay >= 20.0f || timeOfDay < 4.0f) {
return 1.0f;
}
// Fade in at dusk: 18:00-20:00
else if (timeOfDay >= 18.0f && timeOfDay < 20.0f) {
return (timeOfDay - 18.0f) / 2.0f; // 0 to 1 over 2 hours
}
// Fade out at dawn: 4:00-6:00
else if (timeOfDay >= 4.0f && timeOfDay < 6.0f) {
return 1.0f - (timeOfDay - 4.0f) / 2.0f; // 1 to 0 over 2 hours
}
// Daytime: no stars
else {
return 0.0f;
}
}
} // namespace rendering
} // namespace wowee

View file

@ -0,0 +1,380 @@
#include "rendering/swim_effects.hpp"
#include "rendering/camera.hpp"
#include "rendering/camera_controller.hpp"
#include "rendering/water_renderer.hpp"
#include "rendering/shader.hpp"
#include "core/logger.hpp"
#include <glm/gtc/matrix_transform.hpp>
#include <random>
#include <cmath>
namespace wowee {
namespace rendering {
static std::mt19937& rng() {
static std::random_device rd;
static std::mt19937 gen(rd());
return gen;
}
static float randFloat(float lo, float hi) {
std::uniform_real_distribution<float> dist(lo, hi);
return dist(rng());
}
SwimEffects::SwimEffects() = default;
SwimEffects::~SwimEffects() { shutdown(); }
bool SwimEffects::initialize() {
LOG_INFO("Initializing swim effects");
// --- Ripple/splash shader (small white spray droplets) ---
rippleShader = std::make_unique<Shader>();
const char* rippleVS = R"(
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in float aSize;
layout (location = 2) in float aAlpha;
uniform mat4 uView;
uniform mat4 uProjection;
out float vAlpha;
void main() {
gl_Position = uProjection * uView * vec4(aPos, 1.0);
gl_PointSize = aSize;
vAlpha = aAlpha;
}
)";
const char* rippleFS = R"(
#version 330 core
in float vAlpha;
out vec4 FragColor;
void main() {
vec2 coord = gl_PointCoord - vec2(0.5);
float dist = length(coord);
if (dist > 0.5) discard;
// Soft circular splash droplet
float alpha = smoothstep(0.5, 0.2, dist) * vAlpha;
FragColor = vec4(0.85, 0.92, 1.0, alpha);
}
)";
if (!rippleShader->loadFromSource(rippleVS, rippleFS)) {
LOG_ERROR("Failed to create ripple shader");
return false;
}
// --- Bubble shader ---
bubbleShader = std::make_unique<Shader>();
const char* bubbleVS = R"(
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in float aSize;
layout (location = 2) in float aAlpha;
uniform mat4 uView;
uniform mat4 uProjection;
out float vAlpha;
void main() {
gl_Position = uProjection * uView * vec4(aPos, 1.0);
gl_PointSize = aSize;
vAlpha = aAlpha;
}
)";
const char* bubbleFS = R"(
#version 330 core
in float vAlpha;
out vec4 FragColor;
void main() {
vec2 coord = gl_PointCoord - vec2(0.5);
float dist = length(coord);
if (dist > 0.5) discard;
// Bubble with highlight
float edge = smoothstep(0.5, 0.35, dist);
float hollow = smoothstep(0.25, 0.35, dist);
float bubble = edge * hollow;
// Specular highlight near top-left
float highlight = smoothstep(0.3, 0.0, length(coord - vec2(-0.12, -0.12)));
float alpha = (bubble * 0.6 + highlight * 0.4) * vAlpha;
vec3 color = vec3(0.7, 0.85, 1.0);
FragColor = vec4(color, alpha);
}
)";
if (!bubbleShader->loadFromSource(bubbleVS, bubbleFS)) {
LOG_ERROR("Failed to create bubble shader");
return false;
}
// --- Ripple VAO/VBO ---
glGenVertexArrays(1, &rippleVAO);
glGenBuffers(1, &rippleVBO);
glBindVertexArray(rippleVAO);
glBindBuffer(GL_ARRAY_BUFFER, rippleVBO);
// layout: vec3 pos, float size, float alpha (stride = 5 floats)
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
glVertexAttribPointer(1, 1, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(3 * sizeof(float)));
glEnableVertexAttribArray(1);
glVertexAttribPointer(2, 1, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(4 * sizeof(float)));
glEnableVertexAttribArray(2);
glBindVertexArray(0);
// --- Bubble VAO/VBO ---
glGenVertexArrays(1, &bubbleVAO);
glGenBuffers(1, &bubbleVBO);
glBindVertexArray(bubbleVAO);
glBindBuffer(GL_ARRAY_BUFFER, bubbleVBO);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
glVertexAttribPointer(1, 1, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(3 * sizeof(float)));
glEnableVertexAttribArray(1);
glVertexAttribPointer(2, 1, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(4 * sizeof(float)));
glEnableVertexAttribArray(2);
glBindVertexArray(0);
ripples.reserve(MAX_RIPPLE_PARTICLES);
bubbles.reserve(MAX_BUBBLE_PARTICLES);
rippleVertexData.reserve(MAX_RIPPLE_PARTICLES * 5);
bubbleVertexData.reserve(MAX_BUBBLE_PARTICLES * 5);
LOG_INFO("Swim effects initialized");
return true;
}
void SwimEffects::shutdown() {
if (rippleVAO) { glDeleteVertexArrays(1, &rippleVAO); rippleVAO = 0; }
if (rippleVBO) { glDeleteBuffers(1, &rippleVBO); rippleVBO = 0; }
if (bubbleVAO) { glDeleteVertexArrays(1, &bubbleVAO); bubbleVAO = 0; }
if (bubbleVBO) { glDeleteBuffers(1, &bubbleVBO); bubbleVBO = 0; }
rippleShader.reset();
bubbleShader.reset();
ripples.clear();
bubbles.clear();
}
void SwimEffects::spawnRipple(const glm::vec3& pos, const glm::vec3& moveDir, float waterH) {
if (static_cast<int>(ripples.size()) >= MAX_RIPPLE_PARTICLES) return;
Particle p;
// Scatter splash droplets around the character at the water surface
float ox = randFloat(-1.5f, 1.5f);
float oy = randFloat(-1.5f, 1.5f);
p.position = glm::vec3(pos.x + ox, pos.y + oy, waterH + 0.3f);
// Spray outward + upward from movement direction
float spread = randFloat(-1.0f, 1.0f);
glm::vec3 perp(-moveDir.y, moveDir.x, 0.0f);
glm::vec3 outDir = -moveDir + perp * spread;
float speed = randFloat(1.5f, 4.0f);
p.velocity = glm::vec3(outDir.x * speed, outDir.y * speed, randFloat(1.0f, 3.0f));
p.lifetime = 0.0f;
p.maxLifetime = randFloat(0.5f, 1.0f);
p.size = randFloat(3.0f, 7.0f);
p.alpha = randFloat(0.5f, 0.8f);
ripples.push_back(p);
}
void SwimEffects::spawnBubble(const glm::vec3& pos, float /*waterH*/) {
if (static_cast<int>(bubbles.size()) >= MAX_BUBBLE_PARTICLES) return;
Particle p;
float ox = randFloat(-3.0f, 3.0f);
float oy = randFloat(-3.0f, 3.0f);
float oz = randFloat(-2.0f, 0.0f);
p.position = glm::vec3(pos.x + ox, pos.y + oy, pos.z + oz);
p.velocity = glm::vec3(randFloat(-0.3f, 0.3f), randFloat(-0.3f, 0.3f), randFloat(4.0f, 8.0f));
p.lifetime = 0.0f;
p.maxLifetime = randFloat(2.0f, 3.5f);
p.size = randFloat(6.0f, 12.0f);
p.alpha = 0.6f;
bubbles.push_back(p);
}
void SwimEffects::update(const Camera& camera, const CameraController& cc,
const WaterRenderer& water, float deltaTime) {
glm::vec3 camPos = camera.getPosition();
// Use character position for ripples in third-person mode
glm::vec3 charPos = camPos;
const glm::vec3* followTarget = cc.getFollowTarget();
if (cc.isThirdPerson() && followTarget) {
charPos = *followTarget;
}
// Check water at character position (for ripples) and camera position (for bubbles)
auto charWaterH = water.getWaterHeightAt(charPos.x, charPos.y);
auto camWaterH = water.getWaterHeightAt(camPos.x, camPos.y);
bool swimming = cc.isSwimming();
bool moving = cc.isMoving();
// --- Ripple/splash spawning ---
if (swimming && charWaterH) {
float wh = *charWaterH;
float spawnRate = moving ? 40.0f : 8.0f;
rippleSpawnAccum += spawnRate * deltaTime;
// Compute movement direction from camera yaw
float yawRad = glm::radians(cc.getYaw());
glm::vec3 moveDir(std::cos(yawRad), std::sin(yawRad), 0.0f);
if (glm::length(glm::vec2(moveDir)) > 0.001f) {
moveDir = glm::normalize(moveDir);
}
while (rippleSpawnAccum >= 1.0f) {
spawnRipple(charPos, moveDir, wh);
rippleSpawnAccum -= 1.0f;
}
} else {
rippleSpawnAccum = 0.0f;
ripples.clear();
}
// --- Bubble spawning ---
bool underwater = camWaterH && camPos.z < *camWaterH;
if (underwater) {
float bubbleRate = 20.0f;
bubbleSpawnAccum += bubbleRate * deltaTime;
while (bubbleSpawnAccum >= 1.0f) {
spawnBubble(camPos, *camWaterH);
bubbleSpawnAccum -= 1.0f;
}
} else {
bubbleSpawnAccum = 0.0f;
bubbles.clear();
}
// --- Update ripples (splash droplets with gravity) ---
for (int i = static_cast<int>(ripples.size()) - 1; i >= 0; --i) {
auto& p = ripples[i];
p.lifetime += deltaTime;
if (p.lifetime >= p.maxLifetime) {
ripples[i] = ripples.back();
ripples.pop_back();
continue;
}
// Apply gravity to splash droplets
p.velocity.z -= 9.8f * deltaTime;
p.position += p.velocity * deltaTime;
// Kill if fallen back below water
float surfaceZ = charWaterH ? *charWaterH : 0.0f;
if (p.position.z < surfaceZ && p.lifetime > 0.1f) {
ripples[i] = ripples.back();
ripples.pop_back();
continue;
}
float t = p.lifetime / p.maxLifetime;
p.alpha = glm::mix(0.7f, 0.0f, t);
p.size = glm::mix(5.0f, 2.0f, t);
}
// --- Update bubbles ---
float bubbleCeilH = camWaterH ? *camWaterH : 0.0f;
for (int i = static_cast<int>(bubbles.size()) - 1; i >= 0; --i) {
auto& p = bubbles[i];
p.lifetime += deltaTime;
if (p.lifetime >= p.maxLifetime || p.position.z >= bubbleCeilH) {
bubbles[i] = bubbles.back();
bubbles.pop_back();
continue;
}
// Wobble
float wobbleX = std::sin(p.lifetime * 3.0f) * 0.5f;
float wobbleY = std::cos(p.lifetime * 2.5f) * 0.5f;
p.position += (p.velocity + glm::vec3(wobbleX, wobbleY, 0.0f)) * deltaTime;
float t = p.lifetime / p.maxLifetime;
if (t > 0.8f) {
p.alpha = 0.6f * (1.0f - (t - 0.8f) / 0.2f);
} else {
p.alpha = 0.6f;
}
}
// --- Build vertex data ---
rippleVertexData.clear();
for (const auto& p : ripples) {
rippleVertexData.push_back(p.position.x);
rippleVertexData.push_back(p.position.y);
rippleVertexData.push_back(p.position.z);
rippleVertexData.push_back(p.size);
rippleVertexData.push_back(p.alpha);
}
bubbleVertexData.clear();
for (const auto& p : bubbles) {
bubbleVertexData.push_back(p.position.x);
bubbleVertexData.push_back(p.position.y);
bubbleVertexData.push_back(p.position.z);
bubbleVertexData.push_back(p.size);
bubbleVertexData.push_back(p.alpha);
}
}
void SwimEffects::render(const Camera& camera) {
if (rippleVertexData.empty() && bubbleVertexData.empty()) return;
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
glDepthMask(GL_FALSE);
glEnable(GL_PROGRAM_POINT_SIZE);
glm::mat4 view = camera.getViewMatrix();
glm::mat4 projection = camera.getProjectionMatrix();
// --- Render ripples (splash droplets above water surface) ---
if (!rippleVertexData.empty() && rippleShader) {
rippleShader->use();
rippleShader->setUniform("uView", view);
rippleShader->setUniform("uProjection", projection);
glBindVertexArray(rippleVAO);
glBindBuffer(GL_ARRAY_BUFFER, rippleVBO);
glBufferData(GL_ARRAY_BUFFER,
rippleVertexData.size() * sizeof(float),
rippleVertexData.data(),
GL_DYNAMIC_DRAW);
glDrawArrays(GL_POINTS, 0, static_cast<GLsizei>(rippleVertexData.size() / 5));
glBindVertexArray(0);
}
// --- Render bubbles ---
if (!bubbleVertexData.empty() && bubbleShader) {
bubbleShader->use();
bubbleShader->setUniform("uView", view);
bubbleShader->setUniform("uProjection", projection);
glBindVertexArray(bubbleVAO);
glBindBuffer(GL_ARRAY_BUFFER, bubbleVBO);
glBufferData(GL_ARRAY_BUFFER,
bubbleVertexData.size() * sizeof(float),
bubbleVertexData.data(),
GL_DYNAMIC_DRAW);
glDrawArrays(GL_POINTS, 0, static_cast<GLsizei>(bubbleVertexData.size() / 5));
glBindVertexArray(0);
}
glDisable(GL_BLEND);
glDepthMask(GL_TRUE);
glDisable(GL_PROGRAM_POINT_SIZE);
}
} // namespace rendering
} // namespace wowee

View file

@ -0,0 +1,826 @@
#include "rendering/terrain_manager.hpp"
#include "rendering/terrain_renderer.hpp"
#include "rendering/water_renderer.hpp"
#include "rendering/m2_renderer.hpp"
#include "rendering/wmo_renderer.hpp"
#include "rendering/camera.hpp"
#include "pipeline/asset_manager.hpp"
#include "pipeline/adt_loader.hpp"
#include "pipeline/m2_loader.hpp"
#include "pipeline/wmo_loader.hpp"
#include "pipeline/terrain_mesh.hpp"
#include "core/logger.hpp"
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/quaternion.hpp>
#include <glm/gtx/euler_angles.hpp>
#include <cmath>
#include <cctype>
#include <functional>
#include <unordered_set>
namespace wowee {
namespace rendering {
TerrainManager::TerrainManager() {
}
TerrainManager::~TerrainManager() {
// Stop worker thread before cleanup (containers clean up via destructors)
if (workerRunning.load()) {
workerRunning.store(false);
queueCV.notify_all();
if (workerThread.joinable()) {
workerThread.join();
}
}
}
bool TerrainManager::initialize(pipeline::AssetManager* assets, TerrainRenderer* renderer) {
assetManager = assets;
terrainRenderer = renderer;
if (!assetManager) {
LOG_ERROR("Asset manager is null");
return false;
}
if (!terrainRenderer) {
LOG_ERROR("Terrain renderer is null");
return false;
}
// Start background worker thread
workerRunning.store(true);
workerThread = std::thread(&TerrainManager::workerLoop, this);
LOG_INFO("Terrain manager initialized (async loading enabled)");
LOG_INFO(" Map: ", mapName);
LOG_INFO(" Load radius: ", loadRadius, " tiles");
LOG_INFO(" Unload radius: ", unloadRadius, " tiles");
return true;
}
void TerrainManager::update(const Camera& camera, float deltaTime) {
if (!streamingEnabled || !assetManager || !terrainRenderer) {
return;
}
// Always process ready tiles each frame (GPU uploads from background thread)
processReadyTiles();
timeSinceLastUpdate += deltaTime;
// Only update streaming periodically (not every frame)
if (timeSinceLastUpdate < updateInterval) {
return;
}
timeSinceLastUpdate = 0.0f;
// Get current tile from camera position
// GL coordinate mapping: GL Y = -(wowX - ZEROPOINT), GL X = -(wowZ - ZEROPOINT), GL Z = height
// worldToTile expects: worldX = -glY (maps to tileX), worldY = glX (maps to tileY)
glm::vec3 camPos = camera.getPosition();
TileCoord newTile = worldToTile(-camPos.y, camPos.x);
// Check if we've moved to a different tile
if (newTile.x != currentTile.x || newTile.y != currentTile.y) {
LOG_DEBUG("Camera moved to tile [", newTile.x, ",", newTile.y, "]");
currentTile = newTile;
}
// Stream tiles if we've moved significantly or initial load
if (newTile.x != lastStreamTile.x || newTile.y != lastStreamTile.y) {
LOG_INFO("Streaming: cam=(", camPos.x, ",", camPos.y, ",", camPos.z,
") tile=[", newTile.x, ",", newTile.y,
"] loaded=", loadedTiles.size());
streamTiles();
lastStreamTile = newTile;
}
}
// Synchronous fallback for initial tile loading (before worker thread is useful)
bool TerrainManager::loadTile(int x, int y) {
TileCoord coord = {x, y};
// Check if already loaded
if (loadedTiles.find(coord) != loadedTiles.end()) {
return true;
}
// Don't retry tiles that already failed
if (failedTiles.find(coord) != failedTiles.end()) {
return false;
}
LOG_INFO("Loading terrain tile [", x, ",", y, "] (synchronous)");
auto pending = prepareTile(x, y);
if (!pending) {
failedTiles[coord] = true;
return false;
}
finalizeTile(std::move(pending));
return true;
}
std::unique_ptr<PendingTile> TerrainManager::prepareTile(int x, int y) {
TileCoord coord = {x, y};
LOG_INFO("Preparing tile [", x, ",", y, "] (CPU work)");
// Load ADT file
std::string adtPath = getADTPath(coord);
auto adtData = assetManager->readFile(adtPath);
if (adtData.empty()) {
LOG_WARNING("Failed to load ADT file: ", adtPath);
return nullptr;
}
// Parse ADT
pipeline::ADTTerrain terrain = pipeline::ADTLoader::load(adtData);
if (!terrain.isLoaded()) {
LOG_ERROR("Failed to parse ADT terrain: ", adtPath);
return nullptr;
}
// Set tile coordinates so mesh knows where to position this tile in world
terrain.coord.x = x;
terrain.coord.y = y;
// Generate mesh
pipeline::TerrainMesh mesh = pipeline::TerrainMeshGenerator::generate(terrain);
if (mesh.validChunkCount == 0) {
LOG_ERROR("Failed to generate terrain mesh: ", adtPath);
return nullptr;
}
auto pending = std::make_unique<PendingTile>();
pending->coord = coord;
pending->terrain = std::move(terrain);
pending->mesh = std::move(mesh);
// Pre-load M2 doodads (CPU: read files, parse models)
if (!pending->terrain.doodadPlacements.empty()) {
std::unordered_set<uint32_t> preparedModelIds;
int skippedNameId = 0, skippedFileNotFound = 0, skippedInvalid = 0, skippedSkinNotFound = 0;
for (const auto& placement : pending->terrain.doodadPlacements) {
if (placement.nameId >= pending->terrain.doodadNames.size()) {
skippedNameId++;
continue;
}
std::string m2Path = pending->terrain.doodadNames[placement.nameId];
// Convert .mdx to .m2 if needed
if (m2Path.size() > 4) {
std::string ext = m2Path.substr(m2Path.size() - 4);
for (char& c : ext) c = std::tolower(c);
if (ext == ".mdx") {
m2Path = m2Path.substr(0, m2Path.size() - 4) + ".m2";
}
}
// Use path hash as globally unique model ID (nameId is per-tile local)
uint32_t modelId = static_cast<uint32_t>(std::hash<std::string>{}(m2Path));
// Parse model if not already done for this tile
if (preparedModelIds.find(modelId) == preparedModelIds.end()) {
std::vector<uint8_t> m2Data = assetManager->readFile(m2Path);
if (!m2Data.empty()) {
pipeline::M2Model m2Model = pipeline::M2Loader::load(m2Data);
// Try to load skin file
std::string skinPath = m2Path.substr(0, m2Path.size() - 3) + "00.skin";
std::vector<uint8_t> skinData = assetManager->readFile(skinPath);
if (!skinData.empty()) {
pipeline::M2Loader::loadSkin(skinData, m2Model);
} else {
skippedSkinNotFound++;
LOG_WARNING("M2 skin not found: ", skinPath);
}
if (m2Model.isValid()) {
PendingTile::M2Ready ready;
ready.modelId = modelId;
ready.model = std::move(m2Model);
ready.path = m2Path;
pending->m2Models.push_back(std::move(ready));
preparedModelIds.insert(modelId);
} else {
skippedInvalid++;
LOG_WARNING("M2 model invalid (no verts/indices): ", m2Path);
}
} else {
skippedFileNotFound++;
LOG_WARNING("M2 file not found: ", m2Path);
}
}
// Store placement data for instance creation on main thread
if (preparedModelIds.count(modelId)) {
const float ZEROPOINT = 32.0f * 533.33333f;
float wowX = placement.position[0];
float wowY = placement.position[1];
float wowZ = placement.position[2];
PendingTile::M2Placement p;
p.modelId = modelId;
p.uniqueId = placement.uniqueId;
p.position = glm::vec3(
-(wowZ - ZEROPOINT),
-(wowX - ZEROPOINT),
wowY
);
p.rotation = glm::vec3(
-placement.rotation[2] * 3.14159f / 180.0f,
-placement.rotation[0] * 3.14159f / 180.0f,
placement.rotation[1] * 3.14159f / 180.0f
);
p.scale = placement.scale / 1024.0f;
pending->m2Placements.push_back(p);
}
}
if (skippedNameId > 0 || skippedFileNotFound > 0 || skippedInvalid > 0) {
LOG_WARNING("Tile [", x, ",", y, "] doodad issues: ",
skippedNameId, " bad nameId, ",
skippedFileNotFound, " file not found, ",
skippedInvalid, " invalid model, ",
skippedSkinNotFound, " skin not found");
}
}
// Pre-load WMOs (CPU: read files, parse models and groups)
if (!pending->terrain.wmoPlacements.empty()) {
for (const auto& placement : pending->terrain.wmoPlacements) {
if (placement.nameId >= pending->terrain.wmoNames.size()) continue;
const std::string& wmoPath = pending->terrain.wmoNames[placement.nameId];
std::vector<uint8_t> wmoData = assetManager->readFile(wmoPath);
if (wmoData.empty()) continue;
pipeline::WMOModel wmoModel = pipeline::WMOLoader::load(wmoData);
if (wmoModel.nGroups > 0) {
std::string basePath = wmoPath;
std::string extension;
if (basePath.size() > 4) {
extension = basePath.substr(basePath.size() - 4);
std::string extLower = extension;
for (char& c : extLower) c = std::tolower(c);
if (extLower == ".wmo") {
basePath = basePath.substr(0, basePath.size() - 4);
}
}
for (uint32_t gi = 0; gi < wmoModel.nGroups; gi++) {
char groupSuffix[16];
snprintf(groupSuffix, sizeof(groupSuffix), "_%03u%s", gi, extension.c_str());
std::string groupPath = basePath + groupSuffix;
std::vector<uint8_t> groupData = assetManager->readFile(groupPath);
if (groupData.empty()) {
snprintf(groupSuffix, sizeof(groupSuffix), "_%03u.wmo", gi);
groupData = assetManager->readFile(basePath + groupSuffix);
}
if (groupData.empty()) {
snprintf(groupSuffix, sizeof(groupSuffix), "_%03u.WMO", gi);
groupData = assetManager->readFile(basePath + groupSuffix);
}
if (!groupData.empty()) {
pipeline::WMOLoader::loadGroup(groupData, wmoModel, gi);
}
}
}
if (!wmoModel.groups.empty()) {
const float ZEROPOINT = 32.0f * 533.33333f;
glm::vec3 pos(
-(placement.position[2] - ZEROPOINT),
-(placement.position[0] - ZEROPOINT),
placement.position[1]
);
glm::vec3 rot(
-placement.rotation[2] * 3.14159f / 180.0f,
-placement.rotation[0] * 3.14159f / 180.0f,
(placement.rotation[1] + 180.0f) * 3.14159f / 180.0f
);
// Pre-load WMO doodads (M2 models inside WMO)
if (!wmoModel.doodadSets.empty() && !wmoModel.doodads.empty()) {
glm::mat4 wmoMatrix(1.0f);
wmoMatrix = glm::translate(wmoMatrix, pos);
wmoMatrix = glm::rotate(wmoMatrix, rot.z, glm::vec3(0, 0, 1));
wmoMatrix = glm::rotate(wmoMatrix, rot.y, glm::vec3(0, 1, 0));
wmoMatrix = glm::rotate(wmoMatrix, rot.x, glm::vec3(1, 0, 0));
const auto& doodadSet = wmoModel.doodadSets[0];
for (uint32_t di = 0; di < doodadSet.count; di++) {
uint32_t doodadIdx = doodadSet.startIndex + di;
if (doodadIdx >= wmoModel.doodads.size()) break;
const auto& doodad = wmoModel.doodads[doodadIdx];
auto nameIt = wmoModel.doodadNames.find(doodad.nameIndex);
if (nameIt == wmoModel.doodadNames.end()) continue;
std::string m2Path = nameIt->second;
if (m2Path.empty()) continue;
if (m2Path.size() > 4) {
std::string ext = m2Path.substr(m2Path.size() - 4);
for (char& c : ext) c = std::tolower(c);
if (ext == ".mdx" || ext == ".mdl") {
m2Path = m2Path.substr(0, m2Path.size() - 4) + ".m2";
}
}
uint32_t doodadModelId = static_cast<uint32_t>(std::hash<std::string>{}(m2Path));
std::vector<uint8_t> m2Data = assetManager->readFile(m2Path);
if (m2Data.empty()) continue;
pipeline::M2Model m2Model = pipeline::M2Loader::load(m2Data);
std::string skinPath = m2Path.substr(0, m2Path.size() - 3) + "00.skin";
std::vector<uint8_t> skinData = assetManager->readFile(skinPath);
if (!skinData.empty()) {
pipeline::M2Loader::loadSkin(skinData, m2Model);
}
if (!m2Model.isValid()) continue;
// Build doodad's local transform (WoW coordinates)
// WMO doodads use quaternion rotation
glm::mat4 doodadLocal(1.0f);
doodadLocal = glm::translate(doodadLocal, doodad.position);
doodadLocal *= glm::mat4_cast(doodad.rotation);
doodadLocal = glm::scale(doodadLocal, glm::vec3(doodad.scale));
// Full world transform = WMO world transform * doodad local transform
glm::mat4 worldMatrix = wmoMatrix * doodadLocal;
// Extract world position for frustum culling
glm::vec3 worldPos = glm::vec3(worldMatrix[3]);
PendingTile::WMODoodadReady doodadReady;
doodadReady.modelId = doodadModelId;
doodadReady.model = std::move(m2Model);
doodadReady.worldPosition = worldPos;
doodadReady.modelMatrix = worldMatrix;
pending->wmoDoodads.push_back(std::move(doodadReady));
}
}
PendingTile::WMOReady ready;
ready.modelId = placement.uniqueId;
ready.model = std::move(wmoModel);
ready.position = pos;
ready.rotation = rot;
pending->wmoModels.push_back(std::move(ready));
}
}
}
LOG_INFO("Prepared tile [", x, ",", y, "]: ",
pending->m2Models.size(), " M2 models, ",
pending->m2Placements.size(), " M2 placements, ",
pending->wmoModels.size(), " WMOs, ",
pending->wmoDoodads.size(), " WMO doodads");
return pending;
}
void TerrainManager::finalizeTile(std::unique_ptr<PendingTile> pending) {
int x = pending->coord.x;
int y = pending->coord.y;
TileCoord coord = pending->coord;
LOG_INFO("Finalizing tile [", x, ",", y, "] (GPU upload)");
// Check if tile was already loaded (race condition guard) or failed
if (loadedTiles.find(coord) != loadedTiles.end()) {
return;
}
if (failedTiles.find(coord) != failedTiles.end()) {
return;
}
// Upload terrain to GPU
if (!terrainRenderer->loadTerrain(pending->mesh, pending->terrain.textures, x, y)) {
LOG_ERROR("Failed to upload terrain to GPU for tile [", x, ",", y, "]");
failedTiles[coord] = true;
return;
}
// Load water
if (waterRenderer) {
waterRenderer->loadFromTerrain(pending->terrain, true, x, y);
}
std::vector<uint32_t> m2InstanceIds;
std::vector<uint32_t> wmoInstanceIds;
std::vector<uint32_t> tileUniqueIds;
// Upload M2 models to GPU and create instances
if (m2Renderer && assetManager) {
if (!m2Renderer->getModelCount()) {
m2Renderer->initialize(assetManager);
}
// Upload unique models
std::unordered_set<uint32_t> uploadedModelIds;
for (auto& m2Ready : pending->m2Models) {
if (m2Renderer->loadModel(m2Ready.model, m2Ready.modelId)) {
uploadedModelIds.insert(m2Ready.modelId);
}
}
// Create instances (deduplicate by uniqueId across tile boundaries)
int loadedDoodads = 0;
int skippedDedup = 0;
for (const auto& p : pending->m2Placements) {
// Skip if this doodad was already placed by a neighboring tile
if (placedDoodadIds.count(p.uniqueId)) {
skippedDedup++;
continue;
}
uint32_t instId = m2Renderer->createInstance(p.modelId, p.position, p.rotation, p.scale);
if (instId) {
m2InstanceIds.push_back(instId);
placedDoodadIds.insert(p.uniqueId);
tileUniqueIds.push_back(p.uniqueId);
loadedDoodads++;
}
}
LOG_INFO(" Loaded doodads for tile [", x, ",", y, "]: ",
loadedDoodads, " instances (", uploadedModelIds.size(), " new models, ",
skippedDedup, " dedup skipped)");
}
// Upload WMO models to GPU and create instances
if (wmoRenderer && assetManager) {
if (!wmoRenderer->getModelCount()) {
wmoRenderer->initialize(assetManager);
}
int loadedWMOs = 0;
for (auto& wmoReady : pending->wmoModels) {
if (wmoRenderer->loadModel(wmoReady.model, wmoReady.modelId)) {
uint32_t wmoInstId = wmoRenderer->createInstance(wmoReady.modelId, wmoReady.position, wmoReady.rotation);
if (wmoInstId) {
wmoInstanceIds.push_back(wmoInstId);
loadedWMOs++;
}
}
}
// Upload WMO doodad M2 models
if (m2Renderer) {
for (auto& doodad : pending->wmoDoodads) {
m2Renderer->loadModel(doodad.model, doodad.modelId);
uint32_t wmoDoodadInstId = m2Renderer->createInstanceWithMatrix(
doodad.modelId, doodad.modelMatrix, doodad.worldPosition);
if (wmoDoodadInstId) m2InstanceIds.push_back(wmoDoodadInstId);
}
}
if (loadedWMOs > 0) {
LOG_INFO(" Loaded WMOs for tile [", x, ",", y, "]: ", loadedWMOs);
}
}
// Create tile entry
auto tile = std::make_unique<TerrainTile>();
tile->coord = coord;
tile->terrain = std::move(pending->terrain);
tile->mesh = std::move(pending->mesh);
tile->loaded = true;
tile->m2InstanceIds = std::move(m2InstanceIds);
tile->wmoInstanceIds = std::move(wmoInstanceIds);
tile->doodadUniqueIds = std::move(tileUniqueIds);
// Calculate world bounds
getTileBounds(coord, tile->minX, tile->minY, tile->maxX, tile->maxY);
loadedTiles[coord] = std::move(tile);
LOG_INFO(" Finalized tile [", x, ",", y, "]");
}
void TerrainManager::workerLoop() {
LOG_INFO("Terrain worker thread started");
while (workerRunning.load()) {
TileCoord coord;
bool hasWork = false;
{
std::unique_lock<std::mutex> lock(queueMutex);
queueCV.wait(lock, [this]() {
return !loadQueue.empty() || !workerRunning.load();
});
if (!workerRunning.load()) {
break;
}
if (!loadQueue.empty()) {
coord = loadQueue.front();
loadQueue.pop();
hasWork = true;
}
}
if (hasWork) {
auto pending = prepareTile(coord.x, coord.y);
std::lock_guard<std::mutex> lock(queueMutex);
if (pending) {
readyQueue.push(std::move(pending));
} else {
// Mark as failed so we don't re-enqueue
// We'll set failedTiles on the main thread in processReadyTiles
// For now, just remove from pending tracking
pendingTiles.erase(coord);
}
}
}
LOG_INFO("Terrain worker thread stopped");
}
void TerrainManager::processReadyTiles() {
// Process up to 2 ready tiles per frame to spread GPU work
int processed = 0;
const int maxPerFrame = 2;
while (processed < maxPerFrame) {
std::unique_ptr<PendingTile> pending;
{
std::lock_guard<std::mutex> lock(queueMutex);
if (readyQueue.empty()) {
break;
}
pending = std::move(readyQueue.front());
readyQueue.pop();
}
if (pending) {
TileCoord coord = pending->coord;
finalizeTile(std::move(pending));
pendingTiles.erase(coord);
processed++;
}
}
}
void TerrainManager::unloadTile(int x, int y) {
TileCoord coord = {x, y};
// Also remove from pending if it was queued but not yet loaded
pendingTiles.erase(coord);
auto it = loadedTiles.find(coord);
if (it == loadedTiles.end()) {
return;
}
LOG_INFO("Unloading terrain tile [", x, ",", y, "]");
const auto& tile = it->second;
// Remove doodad unique IDs from dedup set
for (uint32_t uid : tile->doodadUniqueIds) {
placedDoodadIds.erase(uid);
}
// Remove M2 doodad instances
if (m2Renderer) {
for (uint32_t id : tile->m2InstanceIds) {
m2Renderer->removeInstance(id);
}
LOG_DEBUG(" Removed ", tile->m2InstanceIds.size(), " M2 instances");
}
// Remove WMO instances
if (wmoRenderer) {
for (uint32_t id : tile->wmoInstanceIds) {
wmoRenderer->removeInstance(id);
}
LOG_DEBUG(" Removed ", tile->wmoInstanceIds.size(), " WMO instances");
}
// Remove terrain chunks for this tile
if (terrainRenderer) {
terrainRenderer->removeTile(x, y);
}
// Remove water surfaces for this tile
if (waterRenderer) {
waterRenderer->removeTile(x, y);
}
loadedTiles.erase(it);
}
void TerrainManager::unloadAll() {
// Stop worker thread
if (workerRunning.load()) {
workerRunning.store(false);
queueCV.notify_all();
if (workerThread.joinable()) {
workerThread.join();
}
}
// Clear queues
{
std::lock_guard<std::mutex> lock(queueMutex);
while (!loadQueue.empty()) loadQueue.pop();
while (!readyQueue.empty()) readyQueue.pop();
}
pendingTiles.clear();
placedDoodadIds.clear();
LOG_INFO("Unloading all terrain tiles");
loadedTiles.clear();
failedTiles.clear();
// Clear terrain renderer
if (terrainRenderer) {
terrainRenderer->clear();
}
// Clear water
if (waterRenderer) {
waterRenderer->clear();
}
}
TileCoord TerrainManager::worldToTile(float worldX, float worldY) const {
// WoW world coordinate system:
// - Tiles are 8533.33 units wide (TILE_SIZE)
// - Tile (32, 32) is roughly at world origin for continents
// - Coordinates increase going east (X) and south (Y)
int tileX = 32 + static_cast<int>(std::floor(worldX / TILE_SIZE));
int tileY = 32 - static_cast<int>(std::floor(worldY / TILE_SIZE));
// Clamp to valid range (0-63)
tileX = std::max(0, std::min(63, tileX));
tileY = std::max(0, std::min(63, tileY));
return {tileX, tileY};
}
void TerrainManager::getTileBounds(const TileCoord& coord, float& minX, float& minY,
float& maxX, float& maxY) const {
// Calculate world bounds for this tile
// Tile (32, 32) is at origin
float offsetX = (32 - coord.x) * TILE_SIZE;
float offsetY = (32 - coord.y) * TILE_SIZE;
minX = offsetX - TILE_SIZE;
minY = offsetY - TILE_SIZE;
maxX = offsetX;
maxY = offsetY;
}
std::string TerrainManager::getADTPath(const TileCoord& coord) const {
// Format: World\Maps\{MapName}\{MapName}_{X}_{Y}.adt
// Example: Azeroth_32_49.adt for tile at coord.x=32, coord.y=49
return "World\\Maps\\" + mapName + "\\" + mapName + "_" +
std::to_string(coord.x) + "_" + std::to_string(coord.y) + ".adt";
}
std::optional<float> TerrainManager::getHeightAt(float glX, float glY) const {
// Terrain mesh vertices are positioned as:
// vertex.position[0] = chunk.position[0] - (offsetY * unitSize) -> GL X
// vertex.position[1] = chunk.position[1] - (offsetX * unitSize) -> GL Y
// vertex.position[2] = chunk.position[2] + height -> GL Z (height)
//
// The 9x9 outer vertex grid has offsetX, offsetY in [0, 8].
// So the chunk spans:
// X: [chunk.position[0] - 8*unitSize, chunk.position[0]]
// Y: [chunk.position[1] - 8*unitSize, chunk.position[1]]
const float unitSize = CHUNK_SIZE / 8.0f;
for (const auto& [coord, tile] : loadedTiles) {
if (!tile || !tile->loaded) continue;
for (int cy = 0; cy < 16; cy++) {
for (int cx = 0; cx < 16; cx++) {
const auto& chunk = tile->terrain.getChunk(cx, cy);
if (!chunk.hasHeightMap()) continue;
float chunkMaxX = chunk.position[0];
float chunkMinX = chunk.position[0] - 8.0f * unitSize;
float chunkMaxY = chunk.position[1];
float chunkMinY = chunk.position[1] - 8.0f * unitSize;
if (glX < chunkMinX || glX > chunkMaxX ||
glY < chunkMinY || glY > chunkMaxY) {
continue;
}
// Fractional position within chunk (0-8 range)
float fracY = (chunk.position[0] - glX) / unitSize; // maps to offsetY
float fracX = (chunk.position[1] - glY) / unitSize; // maps to offsetX
fracX = glm::clamp(fracX, 0.0f, 8.0f);
fracY = glm::clamp(fracY, 0.0f, 8.0f);
// Bilinear interpolation on 9x9 outer grid
int gx0 = static_cast<int>(std::floor(fracX));
int gy0 = static_cast<int>(std::floor(fracY));
int gx1 = std::min(gx0 + 1, 8);
int gy1 = std::min(gy0 + 1, 8);
float tx = fracX - gx0;
float ty = fracY - gy0;
// Outer vertex heights from the 9x17 layout
// Outer vertex (gx, gy) is at index: gy * 17 + gx
float h00 = chunk.heightMap.heights[gy0 * 17 + gx0];
float h10 = chunk.heightMap.heights[gy0 * 17 + gx1];
float h01 = chunk.heightMap.heights[gy1 * 17 + gx0];
float h11 = chunk.heightMap.heights[gy1 * 17 + gx1];
float h = h00 * (1 - tx) * (1 - ty) +
h10 * tx * (1 - ty) +
h01 * (1 - tx) * ty +
h11 * tx * ty;
return chunk.position[2] + h;
}
}
}
return std::nullopt;
}
void TerrainManager::streamTiles() {
// Enqueue tiles in radius around current tile for async loading
{
std::lock_guard<std::mutex> lock(queueMutex);
for (int dy = -loadRadius; dy <= loadRadius; dy++) {
for (int dx = -loadRadius; dx <= loadRadius; dx++) {
int tileX = currentTile.x + dx;
int tileY = currentTile.y + dy;
// Check valid range
if (tileX < 0 || tileX > 63 || tileY < 0 || tileY > 63) {
continue;
}
TileCoord coord = {tileX, tileY};
// Skip if already loaded, pending, or failed
if (loadedTiles.find(coord) != loadedTiles.end()) continue;
if (pendingTiles.find(coord) != pendingTiles.end()) continue;
if (failedTiles.find(coord) != failedTiles.end()) continue;
loadQueue.push(coord);
pendingTiles[coord] = true;
}
}
}
// Notify worker thread that there's work
queueCV.notify_one();
// Unload tiles beyond unload radius (well past the camera far clip)
std::vector<TileCoord> tilesToUnload;
for (const auto& pair : loadedTiles) {
const TileCoord& coord = pair.first;
int dx = std::abs(coord.x - currentTile.x);
int dy = std::abs(coord.y - currentTile.y);
// Chebyshev distance
if (dx > unloadRadius || dy > unloadRadius) {
tilesToUnload.push_back(coord);
}
}
for (const auto& coord : tilesToUnload) {
unloadTile(coord.x, coord.y);
}
if (!tilesToUnload.empty()) {
LOG_INFO("Unloaded ", tilesToUnload.size(), " distant tiles, ",
loadedTiles.size(), " remain");
}
}
} // namespace rendering
} // namespace wowee

View file

@ -0,0 +1,520 @@
#include "rendering/terrain_renderer.hpp"
#include "rendering/frustum.hpp"
#include "pipeline/asset_manager.hpp"
#include "pipeline/blp_loader.hpp"
#include "core/logger.hpp"
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
#include <algorithm>
#include <limits>
namespace wowee {
namespace rendering {
TerrainRenderer::TerrainRenderer() {
}
TerrainRenderer::~TerrainRenderer() {
shutdown();
}
bool TerrainRenderer::initialize(pipeline::AssetManager* assets) {
assetManager = assets;
if (!assetManager) {
LOG_ERROR("Asset manager is null");
return false;
}
LOG_INFO("Initializing terrain renderer");
// Load terrain shader
shader = std::make_unique<Shader>();
if (!shader->loadFromFile("assets/shaders/terrain.vert", "assets/shaders/terrain.frag")) {
LOG_ERROR("Failed to load terrain shader");
return false;
}
// Create default white texture for fallback
uint8_t whitePixel[4] = {255, 255, 255, 255};
glGenTextures(1, &whiteTexture);
glBindTexture(GL_TEXTURE_2D, whiteTexture);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 1, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, whitePixel);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glBindTexture(GL_TEXTURE_2D, 0);
LOG_INFO("Terrain renderer initialized");
return true;
}
void TerrainRenderer::shutdown() {
LOG_INFO("Shutting down terrain renderer");
clear();
// Delete white texture
if (whiteTexture) {
glDeleteTextures(1, &whiteTexture);
whiteTexture = 0;
}
// Delete cached textures
for (auto& pair : textureCache) {
glDeleteTextures(1, &pair.second);
}
textureCache.clear();
shader.reset();
}
bool TerrainRenderer::loadTerrain(const pipeline::TerrainMesh& mesh,
const std::vector<std::string>& texturePaths,
int tileX, int tileY) {
LOG_INFO("Loading terrain mesh: ", mesh.validChunkCount, " chunks");
// Upload each chunk to GPU
for (int y = 0; y < 16; y++) {
for (int x = 0; x < 16; x++) {
const auto& chunk = mesh.getChunk(x, y);
if (!chunk.isValid()) {
continue;
}
TerrainChunkGPU gpuChunk = uploadChunk(chunk);
if (!gpuChunk.isValid()) {
LOG_WARNING("Failed to upload chunk [", x, ",", y, "]");
continue;
}
// Calculate bounding sphere for frustum culling
calculateBoundingSphere(gpuChunk, chunk);
// Load textures for this chunk
if (!chunk.layers.empty()) {
// Base layer (always present)
uint32_t baseTexId = chunk.layers[0].textureId;
if (baseTexId < texturePaths.size()) {
gpuChunk.baseTexture = loadTexture(texturePaths[baseTexId]);
} else {
gpuChunk.baseTexture = whiteTexture;
}
// Additional layers (with alpha blending)
for (size_t i = 1; i < chunk.layers.size() && i < 4; i++) {
const auto& layer = chunk.layers[i];
// Load layer texture
GLuint layerTex = whiteTexture;
if (layer.textureId < texturePaths.size()) {
layerTex = loadTexture(texturePaths[layer.textureId]);
}
gpuChunk.layerTextures.push_back(layerTex);
// Create alpha texture
GLuint alphaTex = 0;
if (!layer.alphaData.empty()) {
alphaTex = createAlphaTexture(layer.alphaData);
}
gpuChunk.alphaTextures.push_back(alphaTex);
}
} else {
// No layers, use default white texture
gpuChunk.baseTexture = whiteTexture;
}
gpuChunk.tileX = tileX;
gpuChunk.tileY = tileY;
chunks.push_back(gpuChunk);
}
}
LOG_INFO("Loaded ", chunks.size(), " terrain chunks to GPU");
return !chunks.empty();
}
TerrainChunkGPU TerrainRenderer::uploadChunk(const pipeline::ChunkMesh& chunk) {
TerrainChunkGPU gpuChunk;
gpuChunk.worldX = chunk.worldX;
gpuChunk.worldY = chunk.worldY;
gpuChunk.worldZ = chunk.worldZ;
gpuChunk.indexCount = static_cast<uint32_t>(chunk.indices.size());
// Debug: verify Z values in uploaded vertices
static int uploadLogCount = 0;
if (uploadLogCount < 3 && !chunk.vertices.empty()) {
float minZ = 999999.0f, maxZ = -999999.0f;
for (const auto& v : chunk.vertices) {
if (v.position[2] < minZ) minZ = v.position[2];
if (v.position[2] > maxZ) maxZ = v.position[2];
}
LOG_DEBUG("GPU upload Z range: [", minZ, ", ", maxZ, "] delta=", maxZ - minZ);
uploadLogCount++;
}
// Create VAO
glGenVertexArrays(1, &gpuChunk.vao);
glBindVertexArray(gpuChunk.vao);
// Create VBO
glGenBuffers(1, &gpuChunk.vbo);
glBindBuffer(GL_ARRAY_BUFFER, gpuChunk.vbo);
glBufferData(GL_ARRAY_BUFFER,
chunk.vertices.size() * sizeof(pipeline::TerrainVertex),
chunk.vertices.data(),
GL_STATIC_DRAW);
// Create IBO
glGenBuffers(1, &gpuChunk.ibo);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, gpuChunk.ibo);
glBufferData(GL_ELEMENT_ARRAY_BUFFER,
chunk.indices.size() * sizeof(pipeline::TerrainIndex),
chunk.indices.data(),
GL_STATIC_DRAW);
// Set up vertex attributes
// Location 0: Position (vec3)
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE,
sizeof(pipeline::TerrainVertex),
(void*)offsetof(pipeline::TerrainVertex, position));
// Location 1: Normal (vec3)
glEnableVertexAttribArray(1);
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE,
sizeof(pipeline::TerrainVertex),
(void*)offsetof(pipeline::TerrainVertex, normal));
// Location 2: TexCoord (vec2)
glEnableVertexAttribArray(2);
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE,
sizeof(pipeline::TerrainVertex),
(void*)offsetof(pipeline::TerrainVertex, texCoord));
// Location 3: LayerUV (vec2)
glEnableVertexAttribArray(3);
glVertexAttribPointer(3, 2, GL_FLOAT, GL_FALSE,
sizeof(pipeline::TerrainVertex),
(void*)offsetof(pipeline::TerrainVertex, layerUV));
glBindVertexArray(0);
return gpuChunk;
}
GLuint TerrainRenderer::loadTexture(const std::string& path) {
// Check cache first
auto it = textureCache.find(path);
if (it != textureCache.end()) {
return it->second;
}
// Load BLP texture
pipeline::BLPImage blp = assetManager->loadTexture(path);
if (!blp.isValid()) {
LOG_WARNING("Failed to load texture: ", path);
textureCache[path] = whiteTexture;
return whiteTexture;
}
// Create OpenGL texture
GLuint textureID;
glGenTextures(1, &textureID);
glBindTexture(GL_TEXTURE_2D, textureID);
// Upload texture data (BLP loader outputs RGBA8)
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA,
blp.width, blp.height, 0,
GL_RGBA, GL_UNSIGNED_BYTE, blp.data.data());
// Set texture parameters
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
// Generate mipmaps
glGenerateMipmap(GL_TEXTURE_2D);
glBindTexture(GL_TEXTURE_2D, 0);
// Cache texture
textureCache[path] = textureID;
LOG_DEBUG("Loaded texture: ", path, " (", blp.width, "x", blp.height, ")");
return textureID;
}
GLuint TerrainRenderer::createAlphaTexture(const std::vector<uint8_t>& alphaData) {
if (alphaData.empty()) {
return 0;
}
GLuint textureID;
glGenTextures(1, &textureID);
glBindTexture(GL_TEXTURE_2D, textureID);
// Alpha data is always expanded to 4096 bytes (64x64 at 8-bit) by terrain_mesh
int width = 64;
int height = static_cast<int>(alphaData.size()) / 64;
glTexImage2D(GL_TEXTURE_2D, 0, GL_RED,
width, height, 0,
GL_RED, GL_UNSIGNED_BYTE, alphaData.data());
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glBindTexture(GL_TEXTURE_2D, 0);
return textureID;
}
void TerrainRenderer::render(const Camera& camera) {
if (chunks.empty() || !shader) {
return;
}
// Enable depth testing
glEnable(GL_DEPTH_TEST);
glDepthFunc(GL_LESS);
// Disable backface culling temporarily to debug flashing
glDisable(GL_CULL_FACE);
// glEnable(GL_CULL_FACE);
// glCullFace(GL_BACK);
// Wireframe mode
if (wireframe) {
glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
} else {
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
}
// Use shader
shader->use();
// Set view/projection matrices
glm::mat4 view = camera.getViewMatrix();
glm::mat4 projection = camera.getProjectionMatrix();
glm::mat4 model = glm::mat4(1.0f);
shader->setUniform("uModel", model);
shader->setUniform("uView", view);
shader->setUniform("uProjection", projection);
// Set lighting
shader->setUniform("uLightDir", glm::vec3(lightDir[0], lightDir[1], lightDir[2]));
shader->setUniform("uLightColor", glm::vec3(lightColor[0], lightColor[1], lightColor[2]));
shader->setUniform("uAmbientColor", glm::vec3(ambientColor[0], ambientColor[1], ambientColor[2]));
// Set camera position
glm::vec3 camPos = camera.getPosition();
shader->setUniform("uViewPos", camPos);
// Set fog (disable by setting very far distances)
shader->setUniform("uFogColor", glm::vec3(fogColor[0], fogColor[1], fogColor[2]));
if (fogEnabled) {
shader->setUniform("uFogStart", fogStart);
shader->setUniform("uFogEnd", fogEnd);
} else {
shader->setUniform("uFogStart", 100000.0f); // Very far
shader->setUniform("uFogEnd", 100001.0f); // Effectively disabled
}
// Extract frustum for culling
Frustum frustum;
if (frustumCullingEnabled) {
glm::mat4 viewProj = projection * view;
frustum.extractFromMatrix(viewProj);
}
// Render each chunk
renderedChunks = 0;
culledChunks = 0;
for (const auto& chunk : chunks) {
if (!chunk.isValid()) {
continue;
}
// Frustum culling
if (frustumCullingEnabled && !isChunkVisible(chunk, frustum)) {
culledChunks++;
continue;
}
// Bind textures
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, chunk.baseTexture);
shader->setUniform("uBaseTexture", 0);
// Bind layer textures and alphas
bool hasLayer1 = chunk.layerTextures.size() > 0;
bool hasLayer2 = chunk.layerTextures.size() > 1;
bool hasLayer3 = chunk.layerTextures.size() > 2;
shader->setUniform("uHasLayer1", hasLayer1 ? 1 : 0);
shader->setUniform("uHasLayer2", hasLayer2 ? 1 : 0);
shader->setUniform("uHasLayer3", hasLayer3 ? 1 : 0);
if (hasLayer1) {
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, chunk.layerTextures[0]);
shader->setUniform("uLayer1Texture", 1);
glActiveTexture(GL_TEXTURE4);
glBindTexture(GL_TEXTURE_2D, chunk.alphaTextures[0]);
shader->setUniform("uLayer1Alpha", 4);
}
if (hasLayer2) {
glActiveTexture(GL_TEXTURE2);
glBindTexture(GL_TEXTURE_2D, chunk.layerTextures[1]);
shader->setUniform("uLayer2Texture", 2);
glActiveTexture(GL_TEXTURE5);
glBindTexture(GL_TEXTURE_2D, chunk.alphaTextures[1]);
shader->setUniform("uLayer2Alpha", 5);
}
if (hasLayer3) {
glActiveTexture(GL_TEXTURE3);
glBindTexture(GL_TEXTURE_2D, chunk.layerTextures[2]);
shader->setUniform("uLayer3Texture", 3);
glActiveTexture(GL_TEXTURE6);
glBindTexture(GL_TEXTURE_2D, chunk.alphaTextures[2]);
shader->setUniform("uLayer3Alpha", 6);
}
// Draw chunk
glBindVertexArray(chunk.vao);
glDrawElements(GL_TRIANGLES, chunk.indexCount, GL_UNSIGNED_INT, 0);
glBindVertexArray(0);
renderedChunks++;
}
// Reset wireframe
if (wireframe) {
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
}
}
void TerrainRenderer::removeTile(int tileX, int tileY) {
int removed = 0;
auto it = chunks.begin();
while (it != chunks.end()) {
if (it->tileX == tileX && it->tileY == tileY) {
if (it->vao) glDeleteVertexArrays(1, &it->vao);
if (it->vbo) glDeleteBuffers(1, &it->vbo);
if (it->ibo) glDeleteBuffers(1, &it->ibo);
for (GLuint alpha : it->alphaTextures) {
if (alpha) glDeleteTextures(1, &alpha);
}
it = chunks.erase(it);
removed++;
} else {
++it;
}
}
if (removed > 0) {
LOG_DEBUG("Removed ", removed, " terrain chunks for tile [", tileX, ",", tileY, "]");
}
}
void TerrainRenderer::clear() {
// Delete all GPU resources
for (auto& chunk : chunks) {
if (chunk.vao) glDeleteVertexArrays(1, &chunk.vao);
if (chunk.vbo) glDeleteBuffers(1, &chunk.vbo);
if (chunk.ibo) glDeleteBuffers(1, &chunk.ibo);
// Delete alpha textures (not cached)
for (GLuint alpha : chunk.alphaTextures) {
if (alpha) glDeleteTextures(1, &alpha);
}
}
chunks.clear();
renderedChunks = 0;
}
void TerrainRenderer::setLighting(const float lightDirIn[3], const float lightColorIn[3],
const float ambientColorIn[3]) {
lightDir[0] = lightDirIn[0];
lightDir[1] = lightDirIn[1];
lightDir[2] = lightDirIn[2];
lightColor[0] = lightColorIn[0];
lightColor[1] = lightColorIn[1];
lightColor[2] = lightColorIn[2];
ambientColor[0] = ambientColorIn[0];
ambientColor[1] = ambientColorIn[1];
ambientColor[2] = ambientColorIn[2];
}
void TerrainRenderer::setFog(const float fogColorIn[3], float fogStartIn, float fogEndIn) {
fogColor[0] = fogColorIn[0];
fogColor[1] = fogColorIn[1];
fogColor[2] = fogColorIn[2];
fogStart = fogStartIn;
fogEnd = fogEndIn;
}
int TerrainRenderer::getTriangleCount() const {
int total = 0;
for (const auto& chunk : chunks) {
total += chunk.indexCount / 3;
}
return total;
}
bool TerrainRenderer::isChunkVisible(const TerrainChunkGPU& chunk, const Frustum& frustum) {
// Test bounding sphere against frustum
return frustum.intersectsSphere(chunk.boundingSphereCenter, chunk.boundingSphereRadius);
}
void TerrainRenderer::calculateBoundingSphere(TerrainChunkGPU& gpuChunk,
const pipeline::ChunkMesh& meshChunk) {
if (meshChunk.vertices.empty()) {
gpuChunk.boundingSphereRadius = 0.0f;
gpuChunk.boundingSphereCenter = glm::vec3(0.0f);
return;
}
// Calculate AABB first
glm::vec3 min(std::numeric_limits<float>::max());
glm::vec3 max(std::numeric_limits<float>::lowest());
for (const auto& vertex : meshChunk.vertices) {
glm::vec3 pos(vertex.position[0], vertex.position[1], vertex.position[2]);
min = glm::min(min, pos);
max = glm::max(max, pos);
}
// Center is midpoint of AABB
gpuChunk.boundingSphereCenter = (min + max) * 0.5f;
// Radius is distance from center to furthest vertex
float maxDistSq = 0.0f;
for (const auto& vertex : meshChunk.vertices) {
glm::vec3 pos(vertex.position[0], vertex.position[1], vertex.position[2]);
glm::vec3 diff = pos - gpuChunk.boundingSphereCenter;
float distSq = glm::dot(diff, diff);
maxDistSq = std::max(maxDistSq, distSq);
}
gpuChunk.boundingSphereRadius = std::sqrt(maxDistSq);
}
} // namespace rendering
} // namespace wowee

51
src/rendering/texture.cpp Normal file
View file

@ -0,0 +1,51 @@
#include "rendering/texture.hpp"
#include "core/logger.hpp"
// Stub implementation - would use stb_image or similar
namespace wowee {
namespace rendering {
Texture::~Texture() {
if (textureID) {
glDeleteTextures(1, &textureID);
}
}
bool Texture::loadFromFile(const std::string& path) {
// TODO: Implement with stb_image or BLP loader
LOG_WARNING("Texture loading not yet implemented: ", path);
return false;
}
bool Texture::loadFromMemory(const unsigned char* data, int w, int h, int channels) {
width = w;
height = h;
glGenTextures(1, &textureID);
glBindTexture(GL_TEXTURE_2D, textureID);
GLenum format = (channels == 4) ? GL_RGBA : GL_RGB;
glTexImage2D(GL_TEXTURE_2D, 0, format, width, height, 0, format, GL_UNSIGNED_BYTE, data);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glGenerateMipmap(GL_TEXTURE_2D);
glBindTexture(GL_TEXTURE_2D, 0);
return true;
}
void Texture::bind(GLuint unit) const {
glActiveTexture(GL_TEXTURE0 + unit);
glBindTexture(GL_TEXTURE_2D, textureID);
}
void Texture::unbind() const {
glBindTexture(GL_TEXTURE_2D, 0);
}
} // namespace rendering
} // namespace wowee

View file

@ -0,0 +1,497 @@
#include "rendering/water_renderer.hpp"
#include "rendering/shader.hpp"
#include "rendering/camera.hpp"
#include "pipeline/adt_loader.hpp"
#include "core/logger.hpp"
#include <GL/glew.h>
#include <glm/gtc/matrix_transform.hpp>
#include <cmath>
namespace wowee {
namespace rendering {
WaterRenderer::WaterRenderer() = default;
WaterRenderer::~WaterRenderer() {
shutdown();
}
bool WaterRenderer::initialize() {
LOG_INFO("Initializing water renderer");
// Create water shader
waterShader = std::make_unique<Shader>();
// Vertex shader
const char* vertexShaderSource = R"(
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aNormal;
layout (location = 2) in vec2 aTexCoord;
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;
uniform float time;
out vec3 FragPos;
out vec3 Normal;
out vec2 TexCoord;
out float WaveOffset;
void main() {
// Simple pass-through for debugging (no wave animation)
vec3 pos = aPos;
FragPos = vec3(model * vec4(pos, 1.0));
Normal = mat3(transpose(inverse(model))) * aNormal;
TexCoord = aTexCoord;
WaveOffset = 0.0;
gl_Position = projection * view * vec4(FragPos, 1.0);
}
)";
// Fragment shader
const char* fragmentShaderSource = R"(
#version 330 core
in vec3 FragPos;
in vec3 Normal;
in vec2 TexCoord;
in float WaveOffset;
uniform vec3 viewPos;
uniform vec4 waterColor;
uniform float waterAlpha;
uniform float time;
out vec4 FragColor;
void main() {
// Normalize interpolated normal
vec3 norm = normalize(Normal);
// Simple directional light (sun)
vec3 lightDir = normalize(vec3(0.5, 1.0, 0.3));
float diff = max(dot(norm, lightDir), 0.0);
// Specular highlights (shininess for water)
vec3 viewDir = normalize(viewPos - FragPos);
vec3 reflectDir = reflect(-lightDir, norm);
float spec = pow(max(dot(viewDir, reflectDir), 0.0), 64.0);
// Animated texture coordinates for flowing effect
vec2 uv1 = TexCoord + vec2(time * 0.02, time * 0.01);
vec2 uv2 = TexCoord + vec2(-time * 0.01, time * 0.015);
// Combine lighting
vec3 ambient = vec3(0.3) * waterColor.rgb;
vec3 diffuse = vec3(0.6) * diff * waterColor.rgb;
vec3 specular = vec3(1.0) * spec;
// Add wave offset to brightness
float brightness = 1.0 + WaveOffset * 0.1;
vec3 result = (ambient + diffuse + specular) * brightness;
// Apply transparency
FragColor = vec4(result, waterAlpha);
}
)";
if (!waterShader->loadFromSource(vertexShaderSource, fragmentShaderSource)) {
LOG_ERROR("Failed to create water shader");
return false;
}
LOG_INFO("Water renderer initialized");
return true;
}
void WaterRenderer::shutdown() {
clear();
waterShader.reset();
}
void WaterRenderer::loadFromTerrain(const pipeline::ADTTerrain& terrain, bool append,
int tileX, int tileY) {
if (!append) {
LOG_INFO("Loading water from terrain (replacing)");
clear();
} else {
LOG_INFO("Loading water from terrain (appending)");
}
// Load water surfaces from MH2O data
int totalLayers = 0;
for (int chunkIdx = 0; chunkIdx < 256; chunkIdx++) {
const auto& chunkWater = terrain.waterData[chunkIdx];
if (!chunkWater.hasWater()) {
continue;
}
// Get the terrain chunk for position reference
int chunkX = chunkIdx % 16;
int chunkY = chunkIdx / 16;
const auto& terrainChunk = terrain.getChunk(chunkX, chunkY);
// Process each water layer in this chunk
for (const auto& layer : chunkWater.layers) {
WaterSurface surface;
// Use the chunk base position - layer offsets will be applied in mesh generation
// to match terrain's coordinate transformation
surface.position = glm::vec3(
terrainChunk.position[0],
terrainChunk.position[1],
layer.minHeight
);
// Debug log first few water surfaces
if (totalLayers < 5) {
LOG_DEBUG("Water layer ", totalLayers, ": chunk=", chunkIdx,
" liquidType=", layer.liquidType,
" offset=(", (int)layer.x, ",", (int)layer.y, ")",
" size=", (int)layer.width, "x", (int)layer.height,
" height range=[", layer.minHeight, ",", layer.maxHeight, "]");
}
surface.minHeight = layer.minHeight;
surface.maxHeight = layer.maxHeight;
surface.liquidType = layer.liquidType;
// Store dimensions
surface.xOffset = layer.x;
surface.yOffset = layer.y;
surface.width = layer.width;
surface.height = layer.height;
// Copy height data
if (!layer.heights.empty()) {
surface.heights = layer.heights;
} else {
// Flat water at minHeight if no height data
size_t numVertices = (layer.width + 1) * (layer.height + 1);
surface.heights.resize(numVertices, layer.minHeight);
}
// Copy render mask
surface.mask = layer.mask;
surface.tileX = tileX;
surface.tileY = tileY;
createWaterMesh(surface);
surfaces.push_back(surface);
totalLayers++;
}
}
LOG_INFO("Loaded ", totalLayers, " water layers from MH2O data");
}
void WaterRenderer::removeTile(int tileX, int tileY) {
int removed = 0;
auto it = surfaces.begin();
while (it != surfaces.end()) {
if (it->tileX == tileX && it->tileY == tileY) {
destroyWaterMesh(*it);
it = surfaces.erase(it);
removed++;
} else {
++it;
}
}
if (removed > 0) {
LOG_DEBUG("Removed ", removed, " water surfaces for tile [", tileX, ",", tileY, "]");
}
}
void WaterRenderer::clear() {
for (auto& surface : surfaces) {
destroyWaterMesh(surface);
}
surfaces.clear();
}
void WaterRenderer::render(const Camera& camera, float time) {
if (!renderingEnabled || surfaces.empty() || !waterShader) {
return;
}
// Enable alpha blending for transparent water
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
// Disable depth writing so terrain shows through water
glDepthMask(GL_FALSE);
waterShader->use();
// Set uniforms
glm::mat4 view = camera.getViewMatrix();
glm::mat4 projection = camera.getProjectionMatrix();
waterShader->setUniform("view", view);
waterShader->setUniform("projection", projection);
waterShader->setUniform("viewPos", camera.getPosition());
waterShader->setUniform("time", time);
// Render each water surface
for (const auto& surface : surfaces) {
if (surface.vao == 0) {
continue;
}
// Model matrix (identity, position already in vertices)
glm::mat4 model = glm::mat4(1.0f);
waterShader->setUniform("model", model);
// Set liquid-specific color and alpha
glm::vec4 color = getLiquidColor(surface.liquidType);
float alpha = getLiquidAlpha(surface.liquidType);
waterShader->setUniform("waterColor", color);
waterShader->setUniform("waterAlpha", alpha);
// Render
glBindVertexArray(surface.vao);
glDrawElements(GL_TRIANGLES, surface.indexCount, GL_UNSIGNED_INT, nullptr);
glBindVertexArray(0);
}
// Restore state
glDepthMask(GL_TRUE);
glDisable(GL_BLEND);
}
void WaterRenderer::createWaterMesh(WaterSurface& surface) {
// Variable-size grid based on water layer dimensions
const int gridWidth = surface.width + 1; // Vertices = tiles + 1
const int gridHeight = surface.height + 1;
const float TILE_SIZE = 33.33333f / 8.0f; // Size of one tile (same as terrain unitSize)
std::vector<float> vertices;
std::vector<uint32_t> indices;
// Generate vertices
// Match terrain coordinate transformation: pos[0] = baseX - (y * unitSize), pos[1] = baseY - (x * unitSize)
for (int y = 0; y < gridHeight; y++) {
for (int x = 0; x < gridWidth; x++) {
int index = y * gridWidth + x;
// Use per-vertex height data if available, otherwise flat at minHeight
float height;
if (index < static_cast<int>(surface.heights.size())) {
height = surface.heights[index];
} else {
height = surface.minHeight;
}
// Position - match terrain coordinate transformation (swap and negate)
// Terrain uses: X = baseX - (offsetY * unitSize), Y = baseY - (offsetX * unitSize)
// Also apply layer offset within chunk (xOffset, yOffset)
float posX = surface.position.x - ((surface.yOffset + y) * TILE_SIZE);
float posY = surface.position.y - ((surface.xOffset + x) * TILE_SIZE);
float posZ = height;
// Debug first surface's corner vertices
static int debugCount = 0;
if (debugCount < 4 && (x == 0 || x == gridWidth-1) && (y == 0 || y == gridHeight-1)) {
LOG_DEBUG("Water vertex: (", posX, ", ", posY, ", ", posZ, ")");
debugCount++;
}
vertices.push_back(posX);
vertices.push_back(posY);
vertices.push_back(posZ);
// Normal (pointing up for water surface)
vertices.push_back(0.0f);
vertices.push_back(0.0f);
vertices.push_back(1.0f);
// Texture coordinates
vertices.push_back(static_cast<float>(x) / std::max(1, gridWidth - 1));
vertices.push_back(static_cast<float>(y) / std::max(1, gridHeight - 1));
}
}
// Generate indices (triangles), respecting the render mask
for (int y = 0; y < gridHeight - 1; y++) {
for (int x = 0; x < gridWidth - 1; x++) {
// Check render mask - each bit represents a tile
bool renderTile = true;
if (!surface.mask.empty()) {
int tileIndex = y * surface.width + x;
int byteIndex = tileIndex / 8;
int bitIndex = tileIndex % 8;
if (byteIndex < static_cast<int>(surface.mask.size())) {
renderTile = (surface.mask[byteIndex] & (1 << bitIndex)) != 0;
}
}
if (!renderTile) {
continue; // Skip this tile
}
int topLeft = y * gridWidth + x;
int topRight = topLeft + 1;
int bottomLeft = (y + 1) * gridWidth + x;
int bottomRight = bottomLeft + 1;
// First triangle
indices.push_back(topLeft);
indices.push_back(bottomLeft);
indices.push_back(topRight);
// Second triangle
indices.push_back(topRight);
indices.push_back(bottomLeft);
indices.push_back(bottomRight);
}
}
if (indices.empty()) {
// No visible tiles
return;
}
surface.indexCount = static_cast<int>(indices.size());
// Create OpenGL buffers
glGenVertexArrays(1, &surface.vao);
glGenBuffers(1, &surface.vbo);
glGenBuffers(1, &surface.ebo);
glBindVertexArray(surface.vao);
// Upload vertex data
glBindBuffer(GL_ARRAY_BUFFER, surface.vbo);
glBufferData(GL_ARRAY_BUFFER, vertices.size() * sizeof(float), vertices.data(), GL_STATIC_DRAW);
// Upload index data
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, surface.ebo);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices.size() * sizeof(uint32_t), indices.data(), GL_STATIC_DRAW);
// Set vertex attributes
// Position
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
// Normal
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(3 * sizeof(float)));
glEnableVertexAttribArray(1);
// Texture coordinates
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(6 * sizeof(float)));
glEnableVertexAttribArray(2);
glBindVertexArray(0);
}
void WaterRenderer::destroyWaterMesh(WaterSurface& surface) {
if (surface.vao != 0) {
glDeleteVertexArrays(1, &surface.vao);
surface.vao = 0;
}
if (surface.vbo != 0) {
glDeleteBuffers(1, &surface.vbo);
surface.vbo = 0;
}
if (surface.ebo != 0) {
glDeleteBuffers(1, &surface.ebo);
surface.ebo = 0;
}
}
std::optional<float> WaterRenderer::getWaterHeightAt(float glX, float glY) const {
const float TILE_SIZE = 33.33333f / 8.0f;
std::optional<float> best;
for (size_t si = 0; si < surfaces.size(); si++) {
const auto& surface = surfaces[si];
float gy = (surface.position.x - glX) / TILE_SIZE - static_cast<float>(surface.yOffset);
float gx = (surface.position.y - glY) / TILE_SIZE - static_cast<float>(surface.xOffset);
if (gx < 0.0f || gx > static_cast<float>(surface.width) ||
gy < 0.0f || gy > static_cast<float>(surface.height)) {
continue;
}
int gridWidth = surface.width + 1;
// Bilinear interpolation
int ix = static_cast<int>(gx);
int iy = static_cast<int>(gy);
float fx = gx - ix;
float fy = gy - iy;
// Clamp to valid vertex range
if (ix >= surface.width) { ix = surface.width - 1; fx = 1.0f; }
if (iy >= surface.height) { iy = surface.height - 1; fy = 1.0f; }
int idx00 = iy * gridWidth + ix;
int idx10 = idx00 + 1;
int idx01 = idx00 + gridWidth;
int idx11 = idx01 + 1;
int total = static_cast<int>(surface.heights.size());
if (idx11 >= total) continue;
float h00 = surface.heights[idx00];
float h10 = surface.heights[idx10];
float h01 = surface.heights[idx01];
float h11 = surface.heights[idx11];
float h = h00 * (1-fx) * (1-fy) + h10 * fx * (1-fy) +
h01 * (1-fx) * fy + h11 * fx * fy;
if (!best || h > *best) {
best = h;
}
}
return best;
}
glm::vec4 WaterRenderer::getLiquidColor(uint8_t liquidType) const {
// WoW 3.3.5a LiquidType.dbc IDs:
// 1,5,9,13,17 = Water variants (still, slow, fast)
// 2,6,10,14 = Ocean
// 3,7,11,15 = Magma
// 4,8,12 = Slime
// Map to basic type using (id - 1) % 4 for standard IDs, or handle ranges
uint8_t basicType;
if (liquidType == 0) {
basicType = 0; // Water (fallback)
} else {
basicType = ((liquidType - 1) % 4);
}
switch (basicType) {
case 0: // Water
return glm::vec4(0.2f, 0.4f, 0.6f, 1.0f);
case 1: // Ocean
return glm::vec4(0.1f, 0.3f, 0.5f, 1.0f);
case 2: // Magma
return glm::vec4(0.9f, 0.3f, 0.05f, 1.0f);
case 3: // Slime
return glm::vec4(0.2f, 0.6f, 0.1f, 1.0f);
default:
return glm::vec4(0.2f, 0.4f, 0.6f, 1.0f); // Water fallback
}
}
float WaterRenderer::getLiquidAlpha(uint8_t liquidType) const {
uint8_t basicType = (liquidType == 0) ? 0 : ((liquidType - 1) % 4);
switch (basicType) {
case 2: return 0.85f; // Magma - mostly opaque
case 3: return 0.75f; // Slime - semi-opaque
default: return 0.55f; // Water/Ocean - semi-transparent
}
}
} // namespace rendering
} // namespace wowee

275
src/rendering/weather.cpp Normal file
View file

@ -0,0 +1,275 @@
#include "rendering/weather.hpp"
#include "rendering/camera.hpp"
#include "rendering/shader.hpp"
#include "core/logger.hpp"
#include <glm/gtc/matrix_transform.hpp>
#include <random>
#include <cmath>
namespace wowee {
namespace rendering {
Weather::Weather() {
}
Weather::~Weather() {
cleanup();
}
bool Weather::initialize() {
LOG_INFO("Initializing weather system");
// Create shader
shader = std::make_unique<Shader>();
// Vertex shader - point sprites with instancing
const char* vertexShaderSource = R"(
#version 330 core
layout (location = 0) in vec3 aPos;
uniform mat4 uView;
uniform mat4 uProjection;
uniform float uParticleSize;
void main() {
gl_Position = uProjection * uView * vec4(aPos, 1.0);
gl_PointSize = uParticleSize;
}
)";
// Fragment shader - simple particle with alpha
const char* fragmentShaderSource = R"(
#version 330 core
uniform vec4 uParticleColor;
out vec4 FragColor;
void main() {
// Circular particle shape
vec2 coord = gl_PointCoord - vec2(0.5);
float dist = length(coord);
if (dist > 0.5) {
discard;
}
// Soft edges
float alpha = smoothstep(0.5, 0.3, dist) * uParticleColor.a;
FragColor = vec4(uParticleColor.rgb, alpha);
}
)";
if (!shader->loadFromSource(vertexShaderSource, fragmentShaderSource)) {
LOG_ERROR("Failed to create weather shader");
return false;
}
// Create VAO and VBO for particle positions
glGenVertexArrays(1, &vao);
glGenBuffers(1, &vbo);
glBindVertexArray(vao);
glBindBuffer(GL_ARRAY_BUFFER, vbo);
// Position attribute
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(glm::vec3), (void*)0);
glEnableVertexAttribArray(0);
glBindVertexArray(0);
// Reserve space for particles
particles.reserve(MAX_PARTICLES);
particlePositions.reserve(MAX_PARTICLES);
LOG_INFO("Weather system initialized");
return true;
}
void Weather::update(const Camera& camera, float deltaTime) {
if (!enabled || weatherType == Type::NONE) {
return;
}
// Initialize particles if needed
if (particles.empty()) {
resetParticles(camera);
}
// Calculate active particle count based on intensity
int targetParticleCount = static_cast<int>(MAX_PARTICLES * intensity);
// Adjust particle count
while (static_cast<int>(particles.size()) < targetParticleCount) {
Particle p;
p.position = getRandomPosition(camera.getPosition());
p.position.y = camera.getPosition().y + SPAWN_HEIGHT;
p.lifetime = 0.0f;
if (weatherType == Type::RAIN) {
p.velocity = glm::vec3(0.0f, -50.0f, 0.0f); // Fast downward
p.maxLifetime = 5.0f;
} else { // SNOW
p.velocity = glm::vec3(0.0f, -5.0f, 0.0f); // Slow downward
p.maxLifetime = 10.0f;
}
particles.push_back(p);
}
while (static_cast<int>(particles.size()) > targetParticleCount) {
particles.pop_back();
}
// Update each particle
for (auto& particle : particles) {
updateParticle(particle, camera, deltaTime);
}
// Update position buffer
particlePositions.clear();
for (const auto& particle : particles) {
particlePositions.push_back(particle.position);
}
}
void Weather::updateParticle(Particle& particle, const Camera& camera, float deltaTime) {
// Update lifetime
particle.lifetime += deltaTime;
// Reset if lifetime exceeded or too far from camera
glm::vec3 cameraPos = camera.getPosition();
float distance = glm::length(particle.position - cameraPos);
if (particle.lifetime >= particle.maxLifetime || distance > SPAWN_VOLUME_SIZE ||
particle.position.y < cameraPos.y - 20.0f) {
// Respawn at top
particle.position = getRandomPosition(cameraPos);
particle.position.y = cameraPos.y + SPAWN_HEIGHT;
particle.lifetime = 0.0f;
}
// Add wind effect for snow
if (weatherType == Type::SNOW) {
float windX = std::sin(particle.lifetime * 0.5f) * 2.0f;
float windZ = std::cos(particle.lifetime * 0.3f) * 2.0f;
particle.velocity.x = windX;
particle.velocity.z = windZ;
}
// Update position
particle.position += particle.velocity * deltaTime;
}
void Weather::render(const Camera& camera) {
if (!enabled || weatherType == Type::NONE || particlePositions.empty() || !shader) {
return;
}
// Enable blending
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
// Disable depth write (particles are transparent)
glDepthMask(GL_FALSE);
// Enable point sprites
glEnable(GL_PROGRAM_POINT_SIZE);
shader->use();
// Set matrices
glm::mat4 view = camera.getViewMatrix();
glm::mat4 projection = camera.getProjectionMatrix();
shader->setUniform("uView", view);
shader->setUniform("uProjection", projection);
// Set particle appearance based on weather type
if (weatherType == Type::RAIN) {
// Rain: white/blue streaks, small size
shader->setUniform("uParticleColor", glm::vec4(0.7f, 0.8f, 0.9f, 0.6f));
shader->setUniform("uParticleSize", 3.0f);
} else { // SNOW
// Snow: white fluffy, larger size
shader->setUniform("uParticleColor", glm::vec4(1.0f, 1.0f, 1.0f, 0.9f));
shader->setUniform("uParticleSize", 8.0f);
}
// Upload particle positions
glBindVertexArray(vao);
glBindBuffer(GL_ARRAY_BUFFER, vbo);
glBufferData(GL_ARRAY_BUFFER,
particlePositions.size() * sizeof(glm::vec3),
particlePositions.data(),
GL_DYNAMIC_DRAW);
// Render particles as points
glDrawArrays(GL_POINTS, 0, static_cast<GLsizei>(particlePositions.size()));
glBindVertexArray(0);
// Restore state
glDisable(GL_BLEND);
glDepthMask(GL_TRUE);
glDisable(GL_PROGRAM_POINT_SIZE);
}
void Weather::resetParticles(const Camera& camera) {
particles.clear();
int particleCount = static_cast<int>(MAX_PARTICLES * intensity);
glm::vec3 cameraPos = camera.getPosition();
for (int i = 0; i < particleCount; ++i) {
Particle p;
p.position = getRandomPosition(cameraPos);
p.position.y = cameraPos.y + SPAWN_HEIGHT * (static_cast<float>(rand()) / RAND_MAX);
p.lifetime = 0.0f;
if (weatherType == Type::RAIN) {
p.velocity = glm::vec3(0.0f, -50.0f, 0.0f);
p.maxLifetime = 5.0f;
} else { // SNOW
p.velocity = glm::vec3(0.0f, -5.0f, 0.0f);
p.maxLifetime = 10.0f;
}
particles.push_back(p);
}
}
glm::vec3 Weather::getRandomPosition(const glm::vec3& center) const {
static std::random_device rd;
static std::mt19937 gen(rd());
static std::uniform_real_distribution<float> dist(-1.0f, 1.0f);
float x = center.x + dist(gen) * SPAWN_VOLUME_SIZE;
float z = center.z + dist(gen) * SPAWN_VOLUME_SIZE;
float y = center.y;
return glm::vec3(x, y, z);
}
void Weather::setIntensity(float intensity) {
this->intensity = glm::clamp(intensity, 0.0f, 1.0f);
}
int Weather::getParticleCount() const {
return static_cast<int>(particles.size());
}
void Weather::cleanup() {
if (vao) {
glDeleteVertexArrays(1, &vao);
vao = 0;
}
if (vbo) {
glDeleteBuffers(1, &vbo);
vbo = 0;
}
}
} // namespace rendering
} // namespace wowee

View file

@ -0,0 +1,835 @@
#include "rendering/wmo_renderer.hpp"
#include "rendering/shader.hpp"
#include "rendering/camera.hpp"
#include "pipeline/wmo_loader.hpp"
#include "pipeline/asset_manager.hpp"
#include "core/logger.hpp"
#include <GL/glew.h>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
#include <algorithm>
namespace wowee {
namespace rendering {
WMORenderer::WMORenderer() {
}
WMORenderer::~WMORenderer() {
shutdown();
}
bool WMORenderer::initialize(pipeline::AssetManager* assets) {
core::Logger::getInstance().info("Initializing WMO renderer...");
assetManager = assets;
// Create WMO shader with texture support
const char* vertexSrc = R"(
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aNormal;
layout (location = 2) in vec2 aTexCoord;
layout (location = 3) in vec4 aColor;
uniform mat4 uModel;
uniform mat4 uView;
uniform mat4 uProjection;
out vec3 FragPos;
out vec3 Normal;
out vec2 TexCoord;
out vec4 VertexColor;
void main() {
vec4 worldPos = uModel * vec4(aPos, 1.0);
FragPos = worldPos.xyz;
Normal = mat3(transpose(inverse(uModel))) * aNormal;
TexCoord = aTexCoord;
VertexColor = aColor;
gl_Position = uProjection * uView * worldPos;
}
)";
const char* fragmentSrc = R"(
#version 330 core
in vec3 FragPos;
in vec3 Normal;
in vec2 TexCoord;
in vec4 VertexColor;
uniform vec3 uLightDir;
uniform vec3 uViewPos;
uniform vec3 uAmbientColor;
uniform sampler2D uTexture;
uniform bool uHasTexture;
uniform bool uAlphaTest;
out vec4 FragColor;
void main() {
vec3 normal = normalize(Normal);
vec3 lightDir = normalize(uLightDir);
// Diffuse lighting
float diff = max(dot(normal, lightDir), 0.0);
vec3 diffuse = diff * vec3(1.0);
// Ambient
vec3 ambient = uAmbientColor;
// Sample texture or use vertex color
vec4 texColor;
if (uHasTexture) {
texColor = texture(uTexture, TexCoord);
// Alpha test only for cutout materials (lattice, grating, etc.)
if (uAlphaTest && texColor.a < 0.5) discard;
} else {
// MOCV vertex color alpha is a lighting blend factor, not transparency
texColor = vec4(VertexColor.rgb, 1.0);
}
// Combine lighting with texture
vec3 result = (ambient + diffuse) * texColor.rgb;
FragColor = vec4(result, 1.0);
}
)";
shader = std::make_unique<Shader>();
if (!shader->loadFromSource(vertexSrc, fragmentSrc)) {
core::Logger::getInstance().error("Failed to create WMO shader");
return false;
}
// Create default white texture for fallback
uint8_t whitePixel[4] = {255, 255, 255, 255};
glGenTextures(1, &whiteTexture);
glBindTexture(GL_TEXTURE_2D, whiteTexture);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 1, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, whitePixel);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glBindTexture(GL_TEXTURE_2D, 0);
core::Logger::getInstance().info("WMO renderer initialized");
return true;
}
void WMORenderer::shutdown() {
core::Logger::getInstance().info("Shutting down WMO renderer...");
// Free all GPU resources
for (auto& [id, model] : loadedModels) {
for (auto& group : model.groups) {
if (group.vao != 0) glDeleteVertexArrays(1, &group.vao);
if (group.vbo != 0) glDeleteBuffers(1, &group.vbo);
if (group.ebo != 0) glDeleteBuffers(1, &group.ebo);
}
}
// Free cached textures
for (auto& [path, texId] : textureCache) {
if (texId != 0 && texId != whiteTexture) {
glDeleteTextures(1, &texId);
}
}
textureCache.clear();
// Free white texture
if (whiteTexture != 0) {
glDeleteTextures(1, &whiteTexture);
whiteTexture = 0;
}
loadedModels.clear();
instances.clear();
shader.reset();
}
bool WMORenderer::loadModel(const pipeline::WMOModel& model, uint32_t id) {
if (!model.isValid()) {
core::Logger::getInstance().error("Cannot load invalid WMO model");
return false;
}
// Check if already loaded
if (loadedModels.find(id) != loadedModels.end()) {
core::Logger::getInstance().warning("WMO model ", id, " already loaded");
return true;
}
core::Logger::getInstance().info("Loading WMO model ", id, " with ", model.groups.size(), " groups, ",
model.textures.size(), " textures...");
ModelData modelData;
modelData.id = id;
modelData.boundingBoxMin = model.boundingBoxMin;
modelData.boundingBoxMax = model.boundingBoxMax;
core::Logger::getInstance().info(" WMO bounds: min=(", model.boundingBoxMin.x, ", ", model.boundingBoxMin.y, ", ", model.boundingBoxMin.z,
") max=(", model.boundingBoxMax.x, ", ", model.boundingBoxMax.y, ", ", model.boundingBoxMax.z, ")");
// Load textures for this model
core::Logger::getInstance().info(" WMO has ", model.textures.size(), " texture paths, ", model.materials.size(), " materials");
if (assetManager && !model.textures.empty()) {
for (size_t i = 0; i < model.textures.size(); i++) {
const auto& texPath = model.textures[i];
core::Logger::getInstance().debug(" Loading texture ", i, ": ", texPath);
GLuint texId = loadTexture(texPath);
modelData.textures.push_back(texId);
}
core::Logger::getInstance().info(" Loaded ", modelData.textures.size(), " textures for WMO");
}
// Store material -> texture index mapping
// IMPORTANT: mat.texture1 is a byte offset into MOTX, not an array index!
// We need to convert it using the textureOffsetToIndex map
core::Logger::getInstance().info(" textureOffsetToIndex map has ", model.textureOffsetToIndex.size(), " entries");
static int matLogCount = 0;
for (size_t i = 0; i < model.materials.size(); i++) {
const auto& mat = model.materials[i];
uint32_t texIndex = 0; // Default to first texture
auto it = model.textureOffsetToIndex.find(mat.texture1);
if (it != model.textureOffsetToIndex.end()) {
texIndex = it->second;
if (matLogCount < 20) {
core::Logger::getInstance().info(" Material ", i, ": texture1 offset ", mat.texture1, " -> texture index ", texIndex);
matLogCount++;
}
} else if (mat.texture1 < model.textures.size()) {
// Fallback: maybe it IS an index in some files?
texIndex = mat.texture1;
if (matLogCount < 20) {
core::Logger::getInstance().info(" Material ", i, ": using texture1 as direct index: ", texIndex);
matLogCount++;
}
} else {
if (matLogCount < 20) {
core::Logger::getInstance().info(" Material ", i, ": texture1 offset ", mat.texture1, " NOT FOUND, using default");
matLogCount++;
}
}
modelData.materialTextureIndices.push_back(texIndex);
modelData.materialBlendModes.push_back(mat.blendMode);
}
// Create GPU resources for each group
uint32_t loadedGroups = 0;
for (const auto& wmoGroup : model.groups) {
// Skip empty groups
if (wmoGroup.vertices.empty() || wmoGroup.indices.empty()) {
continue;
}
GroupResources resources;
if (createGroupResources(wmoGroup, resources)) {
modelData.groups.push_back(resources);
loadedGroups++;
}
}
if (loadedGroups == 0) {
core::Logger::getInstance().warning("No valid groups loaded for WMO ", id);
return false;
}
loadedModels[id] = std::move(modelData);
core::Logger::getInstance().info("WMO model ", id, " loaded successfully (", loadedGroups, " groups)");
return true;
}
void WMORenderer::unloadModel(uint32_t id) {
auto it = loadedModels.find(id);
if (it == loadedModels.end()) {
return;
}
// Free GPU resources
for (auto& group : it->second.groups) {
if (group.vao != 0) glDeleteVertexArrays(1, &group.vao);
if (group.vbo != 0) glDeleteBuffers(1, &group.vbo);
if (group.ebo != 0) glDeleteBuffers(1, &group.ebo);
}
loadedModels.erase(it);
core::Logger::getInstance().info("WMO model ", id, " unloaded");
}
uint32_t WMORenderer::createInstance(uint32_t modelId, const glm::vec3& position,
const glm::vec3& rotation, float scale) {
// Check if model is loaded
if (loadedModels.find(modelId) == loadedModels.end()) {
core::Logger::getInstance().error("Cannot create instance of unloaded WMO model ", modelId);
return 0;
}
WMOInstance instance;
instance.id = nextInstanceId++;
instance.modelId = modelId;
instance.position = position;
instance.rotation = rotation;
instance.scale = scale;
instance.updateModelMatrix();
instances.push_back(instance);
core::Logger::getInstance().info("Created WMO instance ", instance.id, " (model ", modelId, ")");
return instance.id;
}
void WMORenderer::removeInstance(uint32_t instanceId) {
auto it = std::find_if(instances.begin(), instances.end(),
[instanceId](const WMOInstance& inst) { return inst.id == instanceId; });
if (it != instances.end()) {
instances.erase(it);
core::Logger::getInstance().info("Removed WMO instance ", instanceId);
}
}
void WMORenderer::clearInstances() {
instances.clear();
core::Logger::getInstance().info("Cleared all WMO instances");
}
void WMORenderer::render(const Camera& camera, const glm::mat4& view, const glm::mat4& projection) {
if (!shader || instances.empty()) {
lastDrawCalls = 0;
return;
}
lastDrawCalls = 0;
// Set shader uniforms
shader->use();
shader->setUniform("uView", view);
shader->setUniform("uProjection", projection);
shader->setUniform("uViewPos", camera.getPosition());
shader->setUniform("uLightDir", glm::vec3(-0.3f, -0.7f, -0.6f)); // Default sun direction
shader->setUniform("uAmbientColor", glm::vec3(0.4f, 0.4f, 0.5f));
// Enable wireframe if requested
if (wireframeMode) {
glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
}
// WMOs are opaque — ensure blending is off (alpha test via discard in shader)
glDisable(GL_BLEND);
// Disable backface culling for WMOs (some faces may have wrong winding)
glDisable(GL_CULL_FACE);
// Render all instances
for (const auto& instance : instances) {
auto modelIt = loadedModels.find(instance.modelId);
if (modelIt == loadedModels.end()) {
continue;
}
const ModelData& model = modelIt->second;
shader->setUniform("uModel", instance.modelMatrix);
// Render all groups
for (const auto& group : model.groups) {
// Frustum culling
if (frustumCulling && !isGroupVisible(group, instance.modelMatrix, camera)) {
continue;
}
renderGroup(group, model, instance.modelMatrix, view, projection);
}
}
// Restore polygon mode
if (wireframeMode) {
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
}
// Re-enable backface culling
glEnable(GL_CULL_FACE);
}
uint32_t WMORenderer::getTotalTriangleCount() const {
uint32_t total = 0;
for (const auto& instance : instances) {
auto modelIt = loadedModels.find(instance.modelId);
if (modelIt != loadedModels.end()) {
total += modelIt->second.getTotalTriangles();
}
}
return total;
}
bool WMORenderer::createGroupResources(const pipeline::WMOGroup& group, GroupResources& resources) {
if (group.vertices.empty() || group.indices.empty()) {
return false;
}
resources.vertexCount = group.vertices.size();
resources.indexCount = group.indices.size();
resources.boundingBoxMin = group.boundingBoxMin;
resources.boundingBoxMax = group.boundingBoxMax;
// Create vertex data (position, normal, texcoord, color)
struct VertexData {
glm::vec3 position;
glm::vec3 normal;
glm::vec2 texCoord;
glm::vec4 color;
};
std::vector<VertexData> vertices;
vertices.reserve(group.vertices.size());
for (const auto& v : group.vertices) {
VertexData vd;
vd.position = v.position;
vd.normal = v.normal;
vd.texCoord = v.texCoord;
vd.color = v.color;
vertices.push_back(vd);
}
// Create VAO/VBO/EBO
glGenVertexArrays(1, &resources.vao);
glGenBuffers(1, &resources.vbo);
glGenBuffers(1, &resources.ebo);
glBindVertexArray(resources.vao);
// Upload vertex data
glBindBuffer(GL_ARRAY_BUFFER, resources.vbo);
glBufferData(GL_ARRAY_BUFFER, vertices.size() * sizeof(VertexData),
vertices.data(), GL_STATIC_DRAW);
// Upload index data
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, resources.ebo);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, group.indices.size() * sizeof(uint16_t),
group.indices.data(), GL_STATIC_DRAW);
// Vertex attributes
// Position
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(VertexData),
(void*)offsetof(VertexData, position));
// Normal
glEnableVertexAttribArray(1);
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(VertexData),
(void*)offsetof(VertexData, normal));
// TexCoord
glEnableVertexAttribArray(2);
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, sizeof(VertexData),
(void*)offsetof(VertexData, texCoord));
// Color
glEnableVertexAttribArray(3);
glVertexAttribPointer(3, 4, GL_FLOAT, GL_FALSE, sizeof(VertexData),
(void*)offsetof(VertexData, color));
glBindVertexArray(0);
// Store collision geometry for floor raycasting
resources.collisionVertices.reserve(group.vertices.size());
for (const auto& v : group.vertices) {
resources.collisionVertices.push_back(v.position);
}
resources.collisionIndices = group.indices;
// Compute actual bounding box from vertices (WMO header bboxes can be unreliable)
if (!resources.collisionVertices.empty()) {
resources.boundingBoxMin = resources.collisionVertices[0];
resources.boundingBoxMax = resources.collisionVertices[0];
for (const auto& v : resources.collisionVertices) {
resources.boundingBoxMin = glm::min(resources.boundingBoxMin, v);
resources.boundingBoxMax = glm::max(resources.boundingBoxMax, v);
}
}
// Create batches
if (!group.batches.empty()) {
for (const auto& batch : group.batches) {
GroupResources::Batch resBatch;
resBatch.startIndex = batch.startIndex;
resBatch.indexCount = batch.indexCount;
resBatch.materialId = batch.materialId;
resources.batches.push_back(resBatch);
}
} else {
// No batches defined - render entire group as one batch
GroupResources::Batch batch;
batch.startIndex = 0;
batch.indexCount = resources.indexCount;
batch.materialId = 0;
resources.batches.push_back(batch);
}
return true;
}
void WMORenderer::renderGroup(const GroupResources& group, const ModelData& model,
[[maybe_unused]] const glm::mat4& modelMatrix,
[[maybe_unused]] const glm::mat4& view,
[[maybe_unused]] const glm::mat4& projection) {
glBindVertexArray(group.vao);
static int debugLogCount = 0;
// Render each batch
for (const auto& batch : group.batches) {
// Bind texture for this batch's material
// materialId -> materialTextureIndices[materialId] -> textures[texIndex]
GLuint texId = whiteTexture;
bool hasTexture = false;
if (batch.materialId < model.materialTextureIndices.size()) {
uint32_t texIndex = model.materialTextureIndices[batch.materialId];
if (texIndex < model.textures.size()) {
texId = model.textures[texIndex];
hasTexture = (texId != 0 && texId != whiteTexture);
if (debugLogCount < 10) {
core::Logger::getInstance().debug(" Batch: materialId=", (int)batch.materialId,
" -> texIndex=", texIndex, " -> texId=", texId, " hasTexture=", hasTexture);
debugLogCount++;
}
}
}
// Determine if this material uses alpha-test cutout (blendMode 1)
bool alphaTest = false;
if (batch.materialId < model.materialBlendModes.size()) {
alphaTest = (model.materialBlendModes[batch.materialId] == 1);
}
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, texId);
shader->setUniform("uTexture", 0);
shader->setUniform("uHasTexture", hasTexture);
shader->setUniform("uAlphaTest", alphaTest);
glDrawElements(GL_TRIANGLES, batch.indexCount, GL_UNSIGNED_SHORT,
(void*)(batch.startIndex * sizeof(uint16_t)));
lastDrawCalls++;
}
glBindVertexArray(0);
}
bool WMORenderer::isGroupVisible(const GroupResources& group, const glm::mat4& modelMatrix,
const Camera& camera) const {
// Simple frustum culling using bounding box
// Transform bounding box corners to world space
glm::vec3 corners[8] = {
glm::vec3(group.boundingBoxMin.x, group.boundingBoxMin.y, group.boundingBoxMin.z),
glm::vec3(group.boundingBoxMax.x, group.boundingBoxMin.y, group.boundingBoxMin.z),
glm::vec3(group.boundingBoxMin.x, group.boundingBoxMax.y, group.boundingBoxMin.z),
glm::vec3(group.boundingBoxMax.x, group.boundingBoxMax.y, group.boundingBoxMin.z),
glm::vec3(group.boundingBoxMin.x, group.boundingBoxMin.y, group.boundingBoxMax.z),
glm::vec3(group.boundingBoxMax.x, group.boundingBoxMin.y, group.boundingBoxMax.z),
glm::vec3(group.boundingBoxMin.x, group.boundingBoxMax.y, group.boundingBoxMax.z),
glm::vec3(group.boundingBoxMax.x, group.boundingBoxMax.y, group.boundingBoxMax.z)
};
// Transform corners to world space
for (int i = 0; i < 8; i++) {
glm::vec4 worldPos = modelMatrix * glm::vec4(corners[i], 1.0f);
corners[i] = glm::vec3(worldPos);
}
// Simple check: if all corners are behind camera, cull
// (This is a very basic culling implementation - a full frustum test would be better)
glm::vec3 forward = camera.getForward();
glm::vec3 camPos = camera.getPosition();
int behindCount = 0;
for (int i = 0; i < 8; i++) {
glm::vec3 toCorner = corners[i] - camPos;
if (glm::dot(toCorner, forward) < 0.0f) {
behindCount++;
}
}
// If all corners are behind camera, cull
return behindCount < 8;
}
void WMORenderer::WMOInstance::updateModelMatrix() {
modelMatrix = glm::mat4(1.0f);
modelMatrix = glm::translate(modelMatrix, position);
// Apply MODF placement rotation (WoW-to-GL coordinate transform)
// WoW Ry(B)*Rx(A)*Rz(C) becomes GL Rz(B)*Ry(-A)*Rx(-C)
// rotation stored as (-C, -A, B) in radians by caller
// Apply in Z, Y, X order to get Rz(B) * Ry(-A) * Rx(-C)
modelMatrix = glm::rotate(modelMatrix, rotation.z, glm::vec3(0.0f, 0.0f, 1.0f));
modelMatrix = glm::rotate(modelMatrix, rotation.y, glm::vec3(0.0f, 1.0f, 0.0f));
modelMatrix = glm::rotate(modelMatrix, rotation.x, glm::vec3(1.0f, 0.0f, 0.0f));
modelMatrix = glm::scale(modelMatrix, glm::vec3(scale));
}
GLuint WMORenderer::loadTexture(const std::string& path) {
if (!assetManager) {
return whiteTexture;
}
// Check cache first
auto it = textureCache.find(path);
if (it != textureCache.end()) {
return it->second;
}
// Load BLP texture
pipeline::BLPImage blp = assetManager->loadTexture(path);
if (!blp.isValid()) {
core::Logger::getInstance().warning("WMO: Failed to load texture: ", path);
textureCache[path] = whiteTexture;
return whiteTexture;
}
core::Logger::getInstance().debug("WMO texture: ", path, " size=", blp.width, "x", blp.height);
// Create OpenGL texture
GLuint textureID;
glGenTextures(1, &textureID);
glBindTexture(GL_TEXTURE_2D, textureID);
// Upload texture data (BLP loader outputs RGBA8)
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA,
blp.width, blp.height, 0,
GL_RGBA, GL_UNSIGNED_BYTE, blp.data.data());
// Set texture parameters with mipmaps
glGenerateMipmap(GL_TEXTURE_2D);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
glBindTexture(GL_TEXTURE_2D, 0);
// Cache it
textureCache[path] = textureID;
core::Logger::getInstance().debug("WMO: Loaded texture: ", path, " (", blp.width, "x", blp.height, ")");
return textureID;
}
// Ray-AABB intersection (slab method)
// Returns true if the ray intersects the axis-aligned bounding box
static bool rayIntersectsAABB(const glm::vec3& origin, const glm::vec3& dir,
const glm::vec3& bmin, const glm::vec3& bmax) {
float tmin = -1e30f, tmax = 1e30f;
for (int i = 0; i < 3; i++) {
if (std::abs(dir[i]) < 1e-8f) {
// Ray is parallel to this slab — check if origin is inside
if (origin[i] < bmin[i] || origin[i] > bmax[i]) return false;
} else {
float invD = 1.0f / dir[i];
float t0 = (bmin[i] - origin[i]) * invD;
float t1 = (bmax[i] - origin[i]) * invD;
if (t0 > t1) std::swap(t0, t1);
tmin = std::max(tmin, t0);
tmax = std::min(tmax, t1);
if (tmin > tmax) return false;
}
}
return tmax >= 0.0f; // At least part of the ray is forward
}
// MöllerTrumbore ray-triangle intersection
// Returns distance along ray if hit, or negative if miss
static float rayTriangleIntersect(const glm::vec3& origin, const glm::vec3& dir,
const glm::vec3& v0, const glm::vec3& v1, const glm::vec3& v2) {
const float EPSILON = 1e-6f;
glm::vec3 e1 = v1 - v0;
glm::vec3 e2 = v2 - v0;
glm::vec3 h = glm::cross(dir, e2);
float a = glm::dot(e1, h);
if (a > -EPSILON && a < EPSILON) return -1.0f;
float f = 1.0f / a;
glm::vec3 s = origin - v0;
float u = f * glm::dot(s, h);
if (u < 0.0f || u > 1.0f) return -1.0f;
glm::vec3 q = glm::cross(s, e1);
float v = f * glm::dot(dir, q);
if (v < 0.0f || u + v > 1.0f) return -1.0f;
float t = f * glm::dot(e2, q);
return t > EPSILON ? t : -1.0f;
}
std::optional<float> WMORenderer::getFloorHeight(float glX, float glY, float glZ) const {
std::optional<float> bestFloor;
// World-space ray: from high above, pointing straight down
glm::vec3 worldOrigin(glX, glY, glZ + 500.0f);
glm::vec3 worldDir(0.0f, 0.0f, -1.0f);
for (const auto& instance : instances) {
auto it = loadedModels.find(instance.modelId);
if (it == loadedModels.end()) continue;
const ModelData& model = it->second;
// Transform ray into model-local space
glm::mat4 invModel = glm::inverse(instance.modelMatrix);
glm::vec3 localOrigin = glm::vec3(invModel * glm::vec4(worldOrigin, 1.0f));
glm::vec3 localDir = glm::normalize(glm::vec3(invModel * glm::vec4(worldDir, 0.0f)));
for (const auto& group : model.groups) {
// Quick bounding box check: does the ray intersect this group's AABB?
// Use proper ray-AABB intersection (slab method) which handles rotated rays
if (!rayIntersectsAABB(localOrigin, localDir, group.boundingBoxMin, group.boundingBoxMax)) {
continue;
}
// Raycast against triangles
const auto& verts = group.collisionVertices;
const auto& indices = group.collisionIndices;
for (size_t i = 0; i + 2 < indices.size(); i += 3) {
const glm::vec3& v0 = verts[indices[i]];
const glm::vec3& v1 = verts[indices[i + 1]];
const glm::vec3& v2 = verts[indices[i + 2]];
float t = rayTriangleIntersect(localOrigin, localDir, v0, v1, v2);
if (t > 0.0f) {
// Hit point in local space -> world space
glm::vec3 hitLocal = localOrigin + localDir * t;
glm::vec3 hitWorld = glm::vec3(instance.modelMatrix * glm::vec4(hitLocal, 1.0f));
// Only use floors below or near the query point
if (hitWorld.z <= glZ + 2.0f) {
if (!bestFloor || hitWorld.z > *bestFloor) {
bestFloor = hitWorld.z;
}
}
}
}
}
// (debug logging removed)
}
return bestFloor;
}
bool WMORenderer::checkWallCollision(const glm::vec3& from, const glm::vec3& to, glm::vec3& adjustedPos) const {
adjustedPos = to;
bool blocked = false;
glm::vec3 moveDir = to - from;
float moveDistXY = glm::length(glm::vec2(moveDir.x, moveDir.y));
if (moveDistXY < 0.001f) return false;
// Player collision radius
const float PLAYER_RADIUS = 2.5f;
for (const auto& instance : instances) {
auto it = loadedModels.find(instance.modelId);
if (it == loadedModels.end()) continue;
const ModelData& model = it->second;
glm::mat4 invModel = glm::inverse(instance.modelMatrix);
// Transform positions into local space
glm::vec3 localTo = glm::vec3(invModel * glm::vec4(to, 1.0f));
for (const auto& group : model.groups) {
// Quick bounding box check
float margin = PLAYER_RADIUS + 5.0f;
if (localTo.x < group.boundingBoxMin.x - margin || localTo.x > group.boundingBoxMax.x + margin ||
localTo.y < group.boundingBoxMin.y - margin || localTo.y > group.boundingBoxMax.y + margin ||
localTo.z < group.boundingBoxMin.z - margin || localTo.z > group.boundingBoxMax.z + margin) {
continue;
}
const auto& verts = group.collisionVertices;
const auto& indices = group.collisionIndices;
for (size_t i = 0; i + 2 < indices.size(); i += 3) {
const glm::vec3& v0 = verts[indices[i]];
const glm::vec3& v1 = verts[indices[i + 1]];
const glm::vec3& v2 = verts[indices[i + 2]];
// Get triangle normal
glm::vec3 edge1 = v1 - v0;
glm::vec3 edge2 = v2 - v0;
glm::vec3 normal = glm::cross(edge1, edge2);
float normalLen = glm::length(normal);
if (normalLen < 0.001f) continue;
normal /= normalLen;
// Skip mostly-horizontal triangles (floors/ceilings)
if (std::abs(normal.z) > 0.7f) continue;
// Signed distance from player to triangle plane
float planeDist = glm::dot(localTo - v0, normal);
float absPlaneDist = std::abs(planeDist);
if (absPlaneDist > PLAYER_RADIUS) continue;
// Project point onto plane
glm::vec3 projected = localTo - normal * planeDist;
// Check if projected point is inside triangle using same-side test
// Use edge cross products and check they all point same direction as normal
float d0 = glm::dot(glm::cross(v1 - v0, projected - v0), normal);
float d1 = glm::dot(glm::cross(v2 - v1, projected - v1), normal);
float d2 = glm::dot(glm::cross(v0 - v2, projected - v2), normal);
// Also check nearby: if projected point is close to a triangle edge
bool insideTriangle = (d0 >= 0.0f && d1 >= 0.0f && d2 >= 0.0f);
if (insideTriangle) {
// Push player away from wall
float pushDist = PLAYER_RADIUS - absPlaneDist;
if (pushDist > 0.0f) {
// Push in the direction the player is on (sign of planeDist)
float sign = planeDist > 0.0f ? 1.0f : -1.0f;
glm::vec3 pushLocal = normal * sign * pushDist;
// Transform push vector back to world space (direction, not point)
glm::vec3 pushWorld = glm::vec3(instance.modelMatrix * glm::vec4(pushLocal, 0.0f));
// Only apply horizontal push (don't push vertically)
adjustedPos.x += pushWorld.x;
adjustedPos.y += pushWorld.y;
blocked = true;
}
}
}
}
}
return blocked;
}
bool WMORenderer::isInsideWMO(float glX, float glY, float glZ, uint32_t* outModelId) const {
for (const auto& instance : instances) {
auto it = loadedModels.find(instance.modelId);
if (it == loadedModels.end()) continue;
const ModelData& model = it->second;
glm::mat4 invModel = glm::inverse(instance.modelMatrix);
glm::vec3 localPos = glm::vec3(invModel * glm::vec4(glX, glY, glZ, 1.0f));
// Check if inside any group's bounding box
for (const auto& group : model.groups) {
if (localPos.x >= group.boundingBoxMin.x && localPos.x <= group.boundingBoxMax.x &&
localPos.y >= group.boundingBoxMin.y && localPos.y <= group.boundingBoxMax.y &&
localPos.z >= group.boundingBoxMin.z && localPos.z <= group.boundingBoxMax.z) {
if (outModelId) *outModelId = instance.modelId;
return true;
}
}
}
return false;
}
} // namespace rendering
} // namespace wowee

150
src/ui/auth_screen.cpp Normal file
View file

@ -0,0 +1,150 @@
#include "ui/auth_screen.hpp"
#include <imgui.h>
#include <sstream>
namespace wowee { namespace ui {
AuthScreen::AuthScreen() {
}
void AuthScreen::render(auth::AuthHandler& authHandler) {
ImGui::SetNextWindowSize(ImVec2(500, 400), ImGuiCond_FirstUseEver);
ImGui::Begin("WoW 3.3.5a Authentication", nullptr, ImGuiWindowFlags_NoCollapse);
ImGui::Text("Connect to Authentication Server");
ImGui::Separator();
ImGui::Spacing();
// Server settings
ImGui::Text("Server Settings");
ImGui::InputText("Hostname", hostname, sizeof(hostname));
ImGui::InputInt("Port", &port);
if (port < 1) port = 1;
if (port > 65535) port = 65535;
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
// Credentials
ImGui::Text("Credentials");
ImGui::InputText("Username", username, sizeof(username));
// Password with visibility toggle
ImGuiInputTextFlags passwordFlags = showPassword ? 0 : ImGuiInputTextFlags_Password;
ImGui::InputText("Password", password, sizeof(password), passwordFlags);
ImGui::SameLine();
if (ImGui::Checkbox("Show", &showPassword)) {
// Checkbox state changed
}
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
// Connection status
if (!statusMessage.empty()) {
if (statusIsError) {
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.3f, 0.3f, 1.0f));
} else {
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.3f, 1.0f, 0.3f, 1.0f));
}
ImGui::TextWrapped("%s", statusMessage.c_str());
ImGui::PopStyleColor();
ImGui::Spacing();
}
// Connect button
if (authenticating) {
ImGui::Text("Authenticating...");
// Check authentication status
auto state = authHandler.getState();
if (state == auth::AuthState::AUTHENTICATED) {
setStatus("Authentication successful!", false);
authenticating = false;
// Call success callback
if (onSuccess) {
onSuccess();
}
} else if (state == auth::AuthState::FAILED) {
setStatus("Authentication failed", true);
authenticating = false;
}
} else {
if (ImGui::Button("Connect", ImVec2(120, 0))) {
attemptAuth(authHandler);
}
ImGui::SameLine();
if (ImGui::Button("Clear", ImVec2(120, 0))) {
statusMessage.clear();
}
}
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
// Single-player mode button
ImGui::TextColored(ImVec4(0.5f, 0.8f, 1.0f, 1.0f), "Single-Player Mode");
ImGui::TextWrapped("Skip server connection and play offline with local rendering.");
if (ImGui::Button("Start Single Player", ImVec2(240, 30))) {
// Call single-player callback
if (onSinglePlayer) {
onSinglePlayer();
}
}
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
// Info text
ImGui::TextWrapped("Enter your account credentials to connect to the authentication server.");
ImGui::TextWrapped("Default port is 3724.");
ImGui::End();
}
void AuthScreen::attemptAuth(auth::AuthHandler& authHandler) {
// Validate inputs
if (strlen(username) == 0) {
setStatus("Username cannot be empty", true);
return;
}
if (strlen(password) == 0) {
setStatus("Password cannot be empty", true);
return;
}
if (strlen(hostname) == 0) {
setStatus("Hostname cannot be empty", true);
return;
}
// Attempt connection
std::stringstream ss;
ss << "Connecting to " << hostname << ":" << port << "...";
setStatus(ss.str(), false);
if (authHandler.connect(hostname, static_cast<uint16_t>(port))) {
authenticating = true;
setStatus("Connected, authenticating...", false);
// Send authentication credentials
authHandler.authenticate(username, password);
} else {
setStatus("Failed to connect to server", true);
}
}
void AuthScreen::setStatus(const std::string& message, bool isError) {
statusMessage = message;
statusIsError = isError;
}
}} // namespace wowee::ui

211
src/ui/character_screen.cpp Normal file
View file

@ -0,0 +1,211 @@
#include "ui/character_screen.hpp"
#include <imgui.h>
#include <iomanip>
#include <sstream>
namespace wowee { namespace ui {
CharacterScreen::CharacterScreen() {
}
void CharacterScreen::render(game::GameHandler& gameHandler) {
ImGui::SetNextWindowSize(ImVec2(800, 600), ImGuiCond_FirstUseEver);
ImGui::Begin("Character Selection", nullptr, ImGuiWindowFlags_NoCollapse);
ImGui::Text("Select a Character");
ImGui::Separator();
ImGui::Spacing();
// Status message
if (!statusMessage.empty()) {
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.3f, 1.0f, 0.3f, 1.0f));
ImGui::TextWrapped("%s", statusMessage.c_str());
ImGui::PopStyleColor();
ImGui::Spacing();
}
// Get character list
const auto& characters = gameHandler.getCharacters();
// Request character list if not available
if (characters.empty() && gameHandler.getState() == game::WorldState::READY) {
ImGui::Text("Loading characters...");
gameHandler.requestCharacterList();
} else if (characters.empty()) {
ImGui::Text("No characters available.");
} else {
// Character table
if (ImGui::BeginTable("CharactersTable", 6, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg)) {
ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_WidthStretch);
ImGui::TableSetupColumn("Level", ImGuiTableColumnFlags_WidthFixed, 50.0f);
ImGui::TableSetupColumn("Race", ImGuiTableColumnFlags_WidthFixed, 100.0f);
ImGui::TableSetupColumn("Class", ImGuiTableColumnFlags_WidthFixed, 120.0f);
ImGui::TableSetupColumn("Zone", ImGuiTableColumnFlags_WidthFixed, 80.0f);
ImGui::TableSetupColumn("Guild", ImGuiTableColumnFlags_WidthFixed, 80.0f);
ImGui::TableHeadersRow();
for (size_t i = 0; i < characters.size(); ++i) {
const auto& character = characters[i];
ImGui::TableNextRow();
// Name column (selectable)
ImGui::TableSetColumnIndex(0);
bool isSelected = (selectedCharacterIndex == static_cast<int>(i));
// Apply faction color to character name
ImVec4 factionColor = getFactionColor(character.race);
ImGui::PushStyleColor(ImGuiCol_Text, factionColor);
if (ImGui::Selectable(character.name.c_str(), isSelected, ImGuiSelectableFlags_SpanAllColumns)) {
selectedCharacterIndex = static_cast<int>(i);
selectedCharacterGuid = character.guid;
}
ImGui::PopStyleColor();
// Level column
ImGui::TableSetColumnIndex(1);
ImGui::Text("%d", character.level);
// Race column
ImGui::TableSetColumnIndex(2);
ImGui::Text("%s", game::getRaceName(character.race));
// Class column
ImGui::TableSetColumnIndex(3);
ImGui::Text("%s", game::getClassName(character.characterClass));
// Zone column
ImGui::TableSetColumnIndex(4);
ImGui::Text("%d", character.zoneId);
// Guild column
ImGui::TableSetColumnIndex(5);
if (character.hasGuild()) {
ImGui::Text("Yes");
} else {
ImGui::TextDisabled("No");
}
}
ImGui::EndTable();
}
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
// Selected character details
if (selectedCharacterIndex >= 0 && selectedCharacterIndex < static_cast<int>(characters.size())) {
const auto& character = characters[selectedCharacterIndex];
ImGui::Text("Character Details:");
ImGui::Separator();
ImGui::Columns(2, nullptr, false);
// Left column
ImGui::Text("Name:");
ImGui::Text("Level:");
ImGui::Text("Race:");
ImGui::Text("Class:");
ImGui::Text("Gender:");
ImGui::Text("Location:");
ImGui::Text("Guild:");
if (character.hasPet()) {
ImGui::Text("Pet:");
}
ImGui::NextColumn();
// Right column
ImGui::TextColored(getFactionColor(character.race), "%s", character.name.c_str());
ImGui::Text("%d", character.level);
ImGui::Text("%s", game::getRaceName(character.race));
ImGui::Text("%s", game::getClassName(character.characterClass));
ImGui::Text("%s", game::getGenderName(character.gender));
ImGui::Text("Map %d, Zone %d", character.mapId, character.zoneId);
if (character.hasGuild()) {
ImGui::Text("Guild ID: %d", character.guildId);
} else {
ImGui::TextDisabled("None");
}
if (character.hasPet()) {
ImGui::Text("Level %d (Family %d)", character.pet.level, character.pet.family);
}
ImGui::Columns(1);
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
// Enter World button
if (ImGui::Button("Enter World", ImVec2(150, 40))) {
characterSelected = true;
std::stringstream ss;
ss << "Entering world with " << character.name << "...";
setStatus(ss.str());
gameHandler.selectCharacter(character.guid);
// Call callback
if (onCharacterSelected) {
onCharacterSelected(character.guid);
}
}
ImGui::SameLine();
// Display character GUID
std::stringstream guidStr;
guidStr << "GUID: 0x" << std::hex << std::uppercase << std::setfill('0') << std::setw(16) << character.guid;
ImGui::TextDisabled("%s", guidStr.str().c_str());
}
}
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
// Back/Refresh buttons
if (ImGui::Button("Refresh", ImVec2(120, 0))) {
if (gameHandler.getState() == game::WorldState::READY ||
gameHandler.getState() == game::WorldState::CHAR_LIST_RECEIVED) {
gameHandler.requestCharacterList();
setStatus("Refreshing character list...");
}
}
ImGui::End();
}
void CharacterScreen::setStatus(const std::string& message) {
statusMessage = message;
}
ImVec4 CharacterScreen::getFactionColor(game::Race race) const {
// Alliance races: blue
if (race == game::Race::HUMAN ||
race == game::Race::DWARF ||
race == game::Race::NIGHT_ELF ||
race == game::Race::GNOME ||
race == game::Race::DRAENEI) {
return ImVec4(0.3f, 0.5f, 1.0f, 1.0f); // Blue
}
// Horde races: red
if (race == game::Race::ORC ||
race == game::Race::UNDEAD ||
race == game::Race::TAUREN ||
race == game::Race::TROLL ||
race == game::Race::BLOOD_ELF) {
return ImVec4(1.0f, 0.3f, 0.3f, 1.0f); // Red
}
// Default: white
return ImVec4(1.0f, 1.0f, 1.0f, 1.0f);
}
}} // namespace wowee::ui

861
src/ui/game_screen.cpp Normal file
View file

@ -0,0 +1,861 @@
#include "ui/game_screen.hpp"
#include "core/application.hpp"
#include "core/input.hpp"
#include "rendering/renderer.hpp"
#include "rendering/character_renderer.hpp"
#include "rendering/camera.hpp"
#include "pipeline/asset_manager.hpp"
#include "pipeline/dbc_loader.hpp"
#include "core/logger.hpp"
#include <imgui.h>
#include <iomanip>
#include <sstream>
#include <cmath>
#include <unordered_set>
namespace {
constexpr float ZEROPOINT = 32.0f * 533.33333f;
glm::vec3 wowToGL(float wowX, float wowY, float wowZ) {
return { -(wowZ - ZEROPOINT), -(wowX - ZEROPOINT), wowY };
}
bool raySphereIntersect(const wowee::rendering::Ray& ray, const glm::vec3& center, float radius, float& tOut) {
glm::vec3 oc = ray.origin - center;
float b = glm::dot(oc, ray.direction);
float c = glm::dot(oc, oc) - radius * radius;
float discriminant = b * b - c;
if (discriminant < 0.0f) return false;
float t = -b - std::sqrt(discriminant);
if (t < 0.0f) t = -b + std::sqrt(discriminant);
if (t < 0.0f) return false;
tOut = t;
return true;
}
std::string getEntityName(const std::shared_ptr<wowee::game::Entity>& entity) {
if (entity->getType() == wowee::game::ObjectType::PLAYER) {
auto player = std::static_pointer_cast<wowee::game::Player>(entity);
if (!player->getName().empty()) return player->getName();
} else if (entity->getType() == wowee::game::ObjectType::UNIT) {
auto unit = std::static_pointer_cast<wowee::game::Unit>(entity);
if (!unit->getName().empty()) return unit->getName();
}
return "Unknown";
}
}
namespace wowee { namespace ui {
GameScreen::GameScreen() {
}
void GameScreen::render(game::GameHandler& gameHandler) {
// Process targeting input before UI windows
processTargetInput(gameHandler);
// Main menu bar
if (ImGui::BeginMainMenuBar()) {
if (ImGui::BeginMenu("View")) {
ImGui::MenuItem("Player Info", nullptr, &showPlayerInfo);
ImGui::MenuItem("Entity List", nullptr, &showEntityWindow);
ImGui::MenuItem("Chat", nullptr, &showChatWindow);
bool invOpen = inventoryScreen.isOpen();
if (ImGui::MenuItem("Inventory", "B", &invOpen)) {
inventoryScreen.setOpen(invOpen);
}
ImGui::EndMenu();
}
ImGui::EndMainMenuBar();
}
// Player unit frame (top-left)
renderPlayerFrame(gameHandler);
// Target frame (only when we have a target)
if (gameHandler.hasTarget()) {
renderTargetFrame(gameHandler);
}
// Render windows
if (showPlayerInfo) {
renderPlayerInfo(gameHandler);
}
if (showEntityWindow) {
renderEntityList(gameHandler);
}
if (showChatWindow) {
renderChatWindow(gameHandler);
}
// Inventory (B key toggle handled inside)
inventoryScreen.render(gameHandler.getInventory());
if (inventoryScreen.consumeEquipmentDirty()) {
updateCharacterGeosets(gameHandler.getInventory());
updateCharacterTextures(gameHandler.getInventory());
core::Application::getInstance().loadEquippedWeapons();
}
// Update renderer face-target position
auto* renderer = core::Application::getInstance().getRenderer();
if (renderer) {
static glm::vec3 targetGLPos;
if (gameHandler.hasTarget()) {
auto target = gameHandler.getTarget();
if (target) {
targetGLPos = wowToGL(target->getX(), target->getY(), target->getZ());
renderer->setTargetPosition(&targetGLPos);
} else {
renderer->setTargetPosition(nullptr);
}
} else {
renderer->setTargetPosition(nullptr);
}
}
}
void GameScreen::renderPlayerInfo(game::GameHandler& gameHandler) {
ImGui::SetNextWindowSize(ImVec2(350, 250), ImGuiCond_FirstUseEver);
ImGui::SetNextWindowPos(ImVec2(10, 30), ImGuiCond_FirstUseEver);
ImGui::Begin("Player Info", &showPlayerInfo);
const auto& movement = gameHandler.getMovementInfo();
ImGui::Text("Position & Movement");
ImGui::Separator();
ImGui::Spacing();
// Position
ImGui::Text("Position:");
ImGui::Indent();
ImGui::Text("X: %.2f", movement.x);
ImGui::Text("Y: %.2f", movement.y);
ImGui::Text("Z: %.2f", movement.z);
ImGui::Text("Orientation: %.2f rad (%.1f deg)", movement.orientation, movement.orientation * 180.0f / 3.14159f);
ImGui::Unindent();
ImGui::Spacing();
// Movement flags
ImGui::Text("Movement Flags: 0x%08X", movement.flags);
ImGui::Text("Time: %u ms", movement.time);
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
// Connection state
ImGui::Text("Connection State:");
ImGui::Indent();
auto state = gameHandler.getState();
switch (state) {
case game::WorldState::IN_WORLD:
ImGui::TextColored(ImVec4(0.3f, 1.0f, 0.3f, 1.0f), "In World");
break;
case game::WorldState::AUTHENTICATED:
ImGui::TextColored(ImVec4(1.0f, 1.0f, 0.3f, 1.0f), "Authenticated");
break;
case game::WorldState::ENTERING_WORLD:
ImGui::TextColored(ImVec4(1.0f, 1.0f, 0.3f, 1.0f), "Entering World...");
break;
default:
ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), "State: %d", static_cast<int>(state));
break;
}
ImGui::Unindent();
ImGui::End();
}
void GameScreen::renderEntityList(game::GameHandler& gameHandler) {
ImGui::SetNextWindowSize(ImVec2(500, 400), ImGuiCond_FirstUseEver);
ImGui::SetNextWindowPos(ImVec2(10, 290), ImGuiCond_FirstUseEver);
ImGui::Begin("Entities", &showEntityWindow);
const auto& entityManager = gameHandler.getEntityManager();
const auto& entities = entityManager.getEntities();
ImGui::Text("Entities in View: %zu", entities.size());
ImGui::Separator();
ImGui::Spacing();
if (entities.empty()) {
ImGui::TextDisabled("No entities in view");
} else {
// Entity table
if (ImGui::BeginTable("EntitiesTable", 5, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_ScrollY)) {
ImGui::TableSetupColumn("GUID", ImGuiTableColumnFlags_WidthFixed, 140.0f);
ImGui::TableSetupColumn("Type", ImGuiTableColumnFlags_WidthFixed, 100.0f);
ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_WidthStretch);
ImGui::TableSetupColumn("Position", ImGuiTableColumnFlags_WidthFixed, 150.0f);
ImGui::TableSetupColumn("Distance", ImGuiTableColumnFlags_WidthFixed, 80.0f);
ImGui::TableHeadersRow();
const auto& playerMovement = gameHandler.getMovementInfo();
float playerX = playerMovement.x;
float playerY = playerMovement.y;
float playerZ = playerMovement.z;
for (const auto& [guid, entity] : entities) {
ImGui::TableNextRow();
// GUID
ImGui::TableSetColumnIndex(0);
std::stringstream guidStr;
guidStr << "0x" << std::hex << std::uppercase << std::setfill('0') << std::setw(16) << guid;
ImGui::Text("%s", guidStr.str().c_str());
// Type
ImGui::TableSetColumnIndex(1);
switch (entity->getType()) {
case game::ObjectType::PLAYER:
ImGui::TextColored(ImVec4(0.3f, 1.0f, 0.3f, 1.0f), "Player");
break;
case game::ObjectType::UNIT:
ImGui::TextColored(ImVec4(1.0f, 1.0f, 0.3f, 1.0f), "Unit");
break;
case game::ObjectType::GAMEOBJECT:
ImGui::TextColored(ImVec4(0.3f, 0.8f, 1.0f, 1.0f), "GameObject");
break;
default:
ImGui::Text("Object");
break;
}
// Name (for players and units)
ImGui::TableSetColumnIndex(2);
if (entity->getType() == game::ObjectType::PLAYER) {
auto player = std::static_pointer_cast<game::Player>(entity);
ImGui::Text("%s", player->getName().c_str());
} else if (entity->getType() == game::ObjectType::UNIT) {
auto unit = std::static_pointer_cast<game::Unit>(entity);
if (!unit->getName().empty()) {
ImGui::Text("%s", unit->getName().c_str());
} else {
ImGui::TextDisabled("--");
}
} else {
ImGui::TextDisabled("--");
}
// Position
ImGui::TableSetColumnIndex(3);
ImGui::Text("%.1f, %.1f, %.1f", entity->getX(), entity->getY(), entity->getZ());
// Distance from player
ImGui::TableSetColumnIndex(4);
float dx = entity->getX() - playerX;
float dy = entity->getY() - playerY;
float dz = entity->getZ() - playerZ;
float distance = std::sqrt(dx*dx + dy*dy + dz*dz);
ImGui::Text("%.1f", distance);
}
ImGui::EndTable();
}
}
ImGui::End();
}
void GameScreen::renderChatWindow(game::GameHandler& gameHandler) {
ImGui::SetNextWindowSize(ImVec2(600, 300), ImGuiCond_FirstUseEver);
ImGui::SetNextWindowPos(ImVec2(520, 390), ImGuiCond_FirstUseEver);
ImGui::Begin("Chat", nullptr, ImGuiWindowFlags_NoCollapse);
// Chat history
const auto& chatHistory = gameHandler.getChatHistory();
ImGui::BeginChild("ChatHistory", ImVec2(0, -70), true, ImGuiWindowFlags_HorizontalScrollbar);
for (const auto& msg : chatHistory) {
ImVec4 color = getChatTypeColor(msg.type);
ImGui::PushStyleColor(ImGuiCol_Text, color);
std::stringstream ss;
if (msg.type == game::ChatType::TEXT_EMOTE) {
ss << "You " << msg.message;
} else {
ss << "[" << getChatTypeName(msg.type) << "] ";
if (!msg.senderName.empty()) {
ss << msg.senderName << ": ";
}
ss << msg.message;
}
ImGui::TextWrapped("%s", ss.str().c_str());
ImGui::PopStyleColor();
}
// Auto-scroll to bottom
if (ImGui::GetScrollY() >= ImGui::GetScrollMaxY()) {
ImGui::SetScrollHereY(1.0f);
}
ImGui::EndChild();
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
// Chat input
ImGui::Text("Type:");
ImGui::SameLine();
ImGui::SetNextItemWidth(100);
const char* chatTypes[] = { "SAY", "YELL", "PARTY", "GUILD" };
ImGui::Combo("##ChatType", &selectedChatType, chatTypes, 4);
ImGui::SameLine();
ImGui::Text("Message:");
ImGui::SameLine();
ImGui::SetNextItemWidth(-1);
if (refocusChatInput) {
ImGui::SetKeyboardFocusHere();
refocusChatInput = false;
}
if (ImGui::InputText("##ChatInput", chatInputBuffer, sizeof(chatInputBuffer), ImGuiInputTextFlags_EnterReturnsTrue)) {
sendChatMessage(gameHandler);
refocusChatInput = true;
}
if (ImGui::IsItemActive()) {
chatInputActive = true;
} else {
chatInputActive = false;
}
ImGui::End();
}
void GameScreen::processTargetInput(game::GameHandler& gameHandler) {
auto& io = ImGui::GetIO();
auto& input = core::Input::getInstance();
// Tab targeting (when keyboard not captured by UI)
if (!io.WantCaptureKeyboard) {
if (input.isKeyJustPressed(SDL_SCANCODE_TAB)) {
const auto& movement = gameHandler.getMovementInfo();
gameHandler.tabTarget(movement.x, movement.y, movement.z);
}
if (input.isKeyJustPressed(SDL_SCANCODE_ESCAPE)) {
gameHandler.clearTarget();
}
}
// Left-click targeting (when mouse not captured by UI)
if (!io.WantCaptureMouse && input.isMouseButtonJustPressed(SDL_BUTTON_LEFT)) {
auto* renderer = core::Application::getInstance().getRenderer();
auto* camera = renderer ? renderer->getCamera() : nullptr;
auto* window = core::Application::getInstance().getWindow();
if (camera && window) {
glm::vec2 mousePos = input.getMousePosition();
float screenW = static_cast<float>(window->getWidth());
float screenH = static_cast<float>(window->getHeight());
rendering::Ray ray = camera->screenToWorldRay(mousePos.x, mousePos.y, screenW, screenH);
float closestT = 1e30f;
uint64_t closestGuid = 0;
for (const auto& [guid, entity] : gameHandler.getEntityManager().getEntities()) {
auto t = entity->getType();
if (t != game::ObjectType::UNIT && t != game::ObjectType::PLAYER) continue;
glm::vec3 entityGL = wowToGL(entity->getX(), entity->getY(), entity->getZ());
// Add half-height offset so we target the body center, not feet
entityGL.z += 3.0f;
float hitT;
if (raySphereIntersect(ray, entityGL, 3.0f, hitT)) {
if (hitT < closestT) {
closestT = hitT;
closestGuid = guid;
}
}
}
if (closestGuid != 0) {
gameHandler.setTarget(closestGuid);
}
// Don't clear on miss — left-click is also used for camera orbit
}
}
}
void GameScreen::renderPlayerFrame(game::GameHandler& gameHandler) {
ImGui::SetNextWindowPos(ImVec2(10.0f, 30.0f), ImGuiCond_Always);
ImGui::SetNextWindowSize(ImVec2(250.0f, 0.0f), ImGuiCond_Always);
ImGuiWindowFlags flags = ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar |
ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_AlwaysAutoResize;
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 4.0f);
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.1f, 0.1f, 0.1f, 0.85f));
ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0.4f, 0.4f, 0.4f, 1.0f));
if (ImGui::Begin("##PlayerFrame", nullptr, flags)) {
// Use selected character info if available, otherwise defaults
std::string playerName = "Adventurer";
uint32_t playerLevel = 1;
uint32_t playerHp = 100;
uint32_t playerMaxHp = 100;
const auto& characters = gameHandler.getCharacters();
if (!characters.empty()) {
// Use the first (or most recently selected) character
const auto& ch = characters[0];
playerName = ch.name;
playerLevel = ch.level;
// Characters don't store HP; use level-scaled estimate
playerMaxHp = 20 + playerLevel * 10;
playerHp = playerMaxHp;
}
// Name in green (friendly player color)
ImGui::TextColored(ImVec4(0.3f, 1.0f, 0.3f, 1.0f), "%s", playerName.c_str());
ImGui::SameLine();
ImGui::TextDisabled("Lv %u", playerLevel);
// Health bar
float pct = static_cast<float>(playerHp) / static_cast<float>(playerMaxHp);
ImGui::PushStyleColor(ImGuiCol_PlotHistogram, ImVec4(0.2f, 0.8f, 0.2f, 1.0f));
char overlay[64];
snprintf(overlay, sizeof(overlay), "%u / %u", playerHp, playerMaxHp);
ImGui::ProgressBar(pct, ImVec2(-1, 18), overlay);
ImGui::PopStyleColor();
}
ImGui::End();
ImGui::PopStyleColor(2);
ImGui::PopStyleVar();
}
void GameScreen::renderTargetFrame(game::GameHandler& gameHandler) {
auto target = gameHandler.getTarget();
if (!target) return;
auto* window = core::Application::getInstance().getWindow();
float screenW = window ? static_cast<float>(window->getWidth()) : 1280.0f;
float frameW = 250.0f;
float frameX = (screenW - frameW) / 2.0f;
ImGui::SetNextWindowPos(ImVec2(frameX, 30.0f), ImGuiCond_Always);
ImGui::SetNextWindowSize(ImVec2(frameW, 0.0f), ImGuiCond_Always);
ImGuiWindowFlags flags = ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar |
ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_AlwaysAutoResize;
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 4.0f);
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.1f, 0.1f, 0.1f, 0.85f));
ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0.4f, 0.4f, 0.4f, 1.0f));
if (ImGui::Begin("##TargetFrame", nullptr, flags)) {
// Entity name and type
std::string name = getEntityName(target);
ImVec4 nameColor;
switch (target->getType()) {
case game::ObjectType::PLAYER:
nameColor = ImVec4(0.3f, 1.0f, 0.3f, 1.0f); // Green
break;
case game::ObjectType::UNIT:
nameColor = ImVec4(1.0f, 1.0f, 0.3f, 1.0f); // Yellow
break;
default:
nameColor = ImVec4(0.7f, 0.7f, 0.7f, 1.0f);
break;
}
ImGui::TextColored(nameColor, "%s", name.c_str());
// Level (for units/players)
if (target->getType() == game::ObjectType::UNIT || target->getType() == game::ObjectType::PLAYER) {
auto unit = std::static_pointer_cast<game::Unit>(target);
ImGui::SameLine();
ImGui::TextDisabled("Lv %u", unit->getLevel());
// Health bar
uint32_t hp = unit->getHealth();
uint32_t maxHp = unit->getMaxHealth();
if (maxHp > 0) {
float pct = static_cast<float>(hp) / static_cast<float>(maxHp);
ImGui::PushStyleColor(ImGuiCol_PlotHistogram,
pct > 0.5f ? ImVec4(0.2f, 0.8f, 0.2f, 1.0f) :
pct > 0.2f ? ImVec4(0.8f, 0.8f, 0.2f, 1.0f) :
ImVec4(0.8f, 0.2f, 0.2f, 1.0f));
char overlay[64];
snprintf(overlay, sizeof(overlay), "%u / %u", hp, maxHp);
ImGui::ProgressBar(pct, ImVec2(-1, 18), overlay);
ImGui::PopStyleColor();
} else {
ImGui::TextDisabled("No health data");
}
}
// Distance
const auto& movement = gameHandler.getMovementInfo();
float dx = target->getX() - movement.x;
float dy = target->getY() - movement.y;
float dz = target->getZ() - movement.z;
float distance = std::sqrt(dx*dx + dy*dy + dz*dz);
ImGui::TextDisabled("%.1f yd", distance);
}
ImGui::End();
ImGui::PopStyleColor(2);
ImGui::PopStyleVar();
}
void GameScreen::sendChatMessage(game::GameHandler& gameHandler) {
if (strlen(chatInputBuffer) > 0) {
std::string input(chatInputBuffer);
// Check for slash command emotes
if (input.size() > 1 && input[0] == '/') {
std::string command = input.substr(1);
// Convert to lowercase
for (char& c : command) c = std::tolower(c);
std::string emoteText = rendering::Renderer::getEmoteText(command);
if (!emoteText.empty()) {
// Play the emote animation
auto* renderer = core::Application::getInstance().getRenderer();
if (renderer) {
renderer->playEmote(command);
}
// Build emote message — targeted or untargeted
std::string chatText;
if (gameHandler.hasTarget()) {
auto target = gameHandler.getTarget();
if (target) {
std::string targetName = getEntityName(target);
chatText = command + " at " + targetName + ".";
} else {
chatText = emoteText;
}
} else {
chatText = emoteText;
}
// Add local chat message
game::MessageChatData msg;
msg.type = game::ChatType::TEXT_EMOTE;
msg.language = game::ChatLanguage::COMMON;
msg.message = chatText;
gameHandler.addLocalChatMessage(msg);
chatInputBuffer[0] = '\0';
return;
}
// Not a recognized emote — fall through and send as normal chat
}
game::ChatType type;
switch (selectedChatType) {
case 0: type = game::ChatType::SAY; break;
case 1: type = game::ChatType::YELL; break;
case 2: type = game::ChatType::PARTY; break;
case 3: type = game::ChatType::GUILD; break;
default: type = game::ChatType::SAY; break;
}
gameHandler.sendChatMessage(type, chatInputBuffer);
// Clear input
chatInputBuffer[0] = '\0';
}
}
const char* GameScreen::getChatTypeName(game::ChatType type) const {
switch (type) {
case game::ChatType::SAY: return "SAY";
case game::ChatType::YELL: return "YELL";
case game::ChatType::EMOTE: return "EMOTE";
case game::ChatType::TEXT_EMOTE: return "EMOTE";
case game::ChatType::PARTY: return "PARTY";
case game::ChatType::GUILD: return "GUILD";
case game::ChatType::OFFICER: return "OFFICER";
case game::ChatType::RAID: return "RAID";
case game::ChatType::RAID_LEADER: return "RAID LEADER";
case game::ChatType::RAID_WARNING: return "RAID WARNING";
case game::ChatType::WHISPER: return "WHISPER";
case game::ChatType::WHISPER_INFORM: return "TO";
case game::ChatType::SYSTEM: return "SYSTEM";
case game::ChatType::CHANNEL: return "CHANNEL";
case game::ChatType::ACHIEVEMENT: return "ACHIEVEMENT";
default: return "UNKNOWN";
}
}
ImVec4 GameScreen::getChatTypeColor(game::ChatType type) const {
switch (type) {
case game::ChatType::SAY:
return ImVec4(1.0f, 1.0f, 1.0f, 1.0f); // White
case game::ChatType::YELL:
return ImVec4(1.0f, 0.3f, 0.3f, 1.0f); // Red
case game::ChatType::EMOTE:
return ImVec4(1.0f, 0.7f, 0.3f, 1.0f); // Orange
case game::ChatType::TEXT_EMOTE:
return ImVec4(1.0f, 0.7f, 0.3f, 1.0f); // Orange
case game::ChatType::PARTY:
return ImVec4(0.5f, 0.5f, 1.0f, 1.0f); // Light blue
case game::ChatType::GUILD:
return ImVec4(0.3f, 1.0f, 0.3f, 1.0f); // Green
case game::ChatType::OFFICER:
return ImVec4(0.3f, 0.8f, 0.3f, 1.0f); // Dark green
case game::ChatType::RAID:
return ImVec4(1.0f, 0.5f, 0.0f, 1.0f); // Orange
case game::ChatType::WHISPER:
return ImVec4(1.0f, 0.5f, 1.0f, 1.0f); // Pink
case game::ChatType::WHISPER_INFORM:
return ImVec4(1.0f, 0.5f, 1.0f, 1.0f); // Pink
case game::ChatType::SYSTEM:
return ImVec4(1.0f, 1.0f, 0.3f, 1.0f); // Yellow
case game::ChatType::CHANNEL:
return ImVec4(1.0f, 0.7f, 0.7f, 1.0f); // Light pink
case game::ChatType::ACHIEVEMENT:
return ImVec4(1.0f, 1.0f, 0.0f, 1.0f); // Bright yellow
default:
return ImVec4(0.7f, 0.7f, 0.7f, 1.0f); // Gray
}
}
void GameScreen::updateCharacterGeosets(game::Inventory& inventory) {
auto& app = core::Application::getInstance();
auto* renderer = app.getRenderer();
if (!renderer) return;
uint32_t instanceId = renderer->getCharacterInstanceId();
if (instanceId == 0) return;
auto* charRenderer = renderer->getCharacterRenderer();
if (!charRenderer) return;
auto* assetManager = app.getAssetManager();
// Load ItemDisplayInfo.dbc for geosetGroup lookup
std::shared_ptr<pipeline::DBCFile> displayInfoDbc;
if (assetManager) {
displayInfoDbc = assetManager->loadDBC("ItemDisplayInfo.dbc");
}
// Helper: get geosetGroup field for an equipped item's displayInfoId
// DBC binary fields: 7=geosetGroup_1, 8=geosetGroup_2, 9=geosetGroup_3
auto getGeosetGroup = [&](uint32_t displayInfoId, int groupField) -> uint32_t {
if (!displayInfoDbc || displayInfoId == 0) return 0;
int32_t recIdx = displayInfoDbc->findRecordById(displayInfoId);
if (recIdx < 0) return 0;
return displayInfoDbc->getUInt32(static_cast<uint32_t>(recIdx), 7 + groupField);
};
// Helper: find first equipped item matching inventoryType, return its displayInfoId
auto findEquippedDisplayId = [&](std::initializer_list<uint8_t> types) -> uint32_t {
for (int s = 0; s < game::Inventory::NUM_EQUIP_SLOTS; s++) {
const auto& slot = inventory.getEquipSlot(static_cast<game::EquipSlot>(s));
if (!slot.empty()) {
for (uint8_t t : types) {
if (slot.item.inventoryType == t)
return slot.item.displayInfoId;
}
}
}
return 0;
};
// Helper: check if any equipment slot has the given inventoryType
auto hasEquippedType = [&](std::initializer_list<uint8_t> types) -> bool {
for (int s = 0; s < game::Inventory::NUM_EQUIP_SLOTS; s++) {
const auto& slot = inventory.getEquipSlot(static_cast<game::EquipSlot>(s));
if (!slot.empty()) {
for (uint8_t t : types) {
if (slot.item.inventoryType == t) return true;
}
}
}
return false;
};
// Base geosets always present
std::unordered_set<uint16_t> geosets;
for (uint16_t i = 0; i <= 18; i++) {
geosets.insert(i);
}
geosets.insert(101); // Hair
geosets.insert(201); // Facial
geosets.insert(701); // Ears
// Chest/Shirt: inventoryType 4 (shirt), 5 (chest), 20 (robe)
// geosetGroup_1 > 0 → use mesh variant (502+), otherwise bare (501) + texture only
{
uint32_t did = findEquippedDisplayId({4, 5, 20});
uint32_t gg = getGeosetGroup(did, 0);
geosets.insert(static_cast<uint16_t>(gg > 0 ? 501 + gg : (did > 0 ? 501 : 501)));
// geosetGroup_3 > 0 on robes also shows kilt legs (1302)
uint32_t gg3 = getGeosetGroup(did, 2);
if (gg3 > 0) {
geosets.insert(static_cast<uint16_t>(1301 + gg3));
}
}
// Legs: inventoryType 7
// geosetGroup_1 > 0 → kilt/skirt mesh (1302+), otherwise bare legs (1301) + texture
{
uint32_t did = findEquippedDisplayId({7});
uint32_t gg = getGeosetGroup(did, 0);
// Only add leg geoset if robe hasn't already set a kilt geoset
if (geosets.count(1302) == 0 && geosets.count(1303) == 0) {
geosets.insert(static_cast<uint16_t>(gg > 0 ? 1301 + gg : 1301));
}
}
// Feet/Boots: inventoryType 8
// geosetGroup_1 > 0 → boot mesh (402+), otherwise bare feet (401) + texture
{
uint32_t did = findEquippedDisplayId({8});
uint32_t gg = getGeosetGroup(did, 0);
geosets.insert(static_cast<uint16_t>(gg > 0 ? 401 + gg : 401));
}
// Gloves/Hands: inventoryType 10
// geosetGroup_1 > 0 → glove mesh (302+), otherwise bare hands (301)
{
uint32_t did = findEquippedDisplayId({10});
uint32_t gg = getGeosetGroup(did, 0);
geosets.insert(static_cast<uint16_t>(gg > 0 ? 301 + gg : 301));
}
// Back/Cloak: inventoryType 16 — geoset only, no skin texture (cloaks are separate models)
geosets.insert(hasEquippedType({16}) ? 1502 : 1501);
// Tabard: inventoryType 19
if (hasEquippedType({19})) {
geosets.insert(1201);
}
charRenderer->setActiveGeosets(instanceId, geosets);
}
void GameScreen::updateCharacterTextures(game::Inventory& inventory) {
auto& app = core::Application::getInstance();
auto* renderer = app.getRenderer();
if (!renderer) return;
auto* charRenderer = renderer->getCharacterRenderer();
if (!charRenderer) return;
auto* assetManager = app.getAssetManager();
if (!assetManager) return;
const auto& bodySkinPath = app.getBodySkinPath();
const auto& underwearPaths = app.getUnderwearPaths();
uint32_t skinSlot = app.getSkinTextureSlotIndex();
if (bodySkinPath.empty()) return;
// Component directory names indexed by region
static const char* componentDirs[] = {
"ArmUpperTexture", // 0
"ArmLowerTexture", // 1
"HandTexture", // 2
"TorsoUpperTexture", // 3
"TorsoLowerTexture", // 4
"LegUpperTexture", // 5
"LegLowerTexture", // 6
"FootTexture", // 7
};
// Load ItemDisplayInfo.dbc
auto displayInfoDbc = assetManager->loadDBC("ItemDisplayInfo.dbc");
if (!displayInfoDbc) return;
// Collect equipment texture regions from all equipped items
std::vector<std::pair<int, std::string>> regionLayers;
for (int s = 0; s < game::Inventory::NUM_EQUIP_SLOTS; s++) {
const auto& slot = inventory.getEquipSlot(static_cast<game::EquipSlot>(s));
if (slot.empty() || slot.item.displayInfoId == 0) continue;
int32_t recIdx = displayInfoDbc->findRecordById(slot.item.displayInfoId);
if (recIdx < 0) continue;
// DBC fields 15-22 = texture_1 through texture_8 (regions 0-7)
// (binary DBC has inventoryIcon_2 at field 6, shifting fields +1 vs CSV)
for (int region = 0; region < 8; region++) {
uint32_t fieldIdx = 15 + region;
std::string texName = displayInfoDbc->getString(static_cast<uint32_t>(recIdx), fieldIdx);
if (texName.empty()) continue;
// Actual MPQ files have a gender suffix: _M (male), _F (female), _U (unisex)
// Try gender-specific first, then unisex fallback
std::string base = "Item\\TextureComponents\\" +
std::string(componentDirs[region]) + "\\" + texName;
std::string malePath = base + "_M.blp";
std::string unisexPath = base + "_U.blp";
std::string fullPath;
if (assetManager->fileExists(malePath)) {
fullPath = malePath;
} else if (assetManager->fileExists(unisexPath)) {
fullPath = unisexPath;
} else {
// Last resort: try without suffix
fullPath = base + ".blp";
}
regionLayers.emplace_back(region, fullPath);
}
}
// Re-composite: base skin + underwear + equipment regions
GLuint newTex = charRenderer->compositeWithRegions(bodySkinPath, underwearPaths, regionLayers);
if (newTex != 0) {
charRenderer->setModelTexture(1, skinSlot, newTex);
}
// Cloak cape texture — separate from skin atlas, uses texture slot type-2 (Object Skin)
uint32_t cloakSlot = app.getCloakTextureSlotIndex();
if (cloakSlot > 0) {
// Find equipped cloak (inventoryType 16)
uint32_t cloakDisplayId = 0;
for (int s = 0; s < game::Inventory::NUM_EQUIP_SLOTS; s++) {
const auto& slot = inventory.getEquipSlot(static_cast<game::EquipSlot>(s));
if (!slot.empty() && slot.item.inventoryType == 16 && slot.item.displayInfoId != 0) {
cloakDisplayId = slot.item.displayInfoId;
break;
}
}
if (cloakDisplayId > 0) {
int32_t recIdx = displayInfoDbc->findRecordById(cloakDisplayId);
if (recIdx >= 0) {
// DBC field 3 = modelTexture_1 (cape texture name)
std::string capeName = displayInfoDbc->getString(static_cast<uint32_t>(recIdx), 3);
if (!capeName.empty()) {
std::string capePath = "Item\\ObjectComponents\\Cape\\" + capeName + ".blp";
GLuint capeTex = charRenderer->loadTexture(capePath);
if (capeTex != 0) {
charRenderer->setModelTexture(1, cloakSlot, capeTex);
LOG_INFO("Cloak texture applied: ", capePath);
}
}
}
} else {
// No cloak equipped — reset to white fallback
charRenderer->resetModelTexture(1, cloakSlot);
}
}
}
}} // namespace wowee::ui

633
src/ui/inventory_screen.cpp Normal file
View file

@ -0,0 +1,633 @@
#include "ui/inventory_screen.hpp"
#include "core/input.hpp"
#include <imgui.h>
#include <SDL2/SDL.h>
#include <cstdio>
namespace wowee {
namespace ui {
ImVec4 InventoryScreen::getQualityColor(game::ItemQuality quality) {
switch (quality) {
case game::ItemQuality::POOR: return ImVec4(0.62f, 0.62f, 0.62f, 1.0f); // Grey
case game::ItemQuality::COMMON: return ImVec4(1.0f, 1.0f, 1.0f, 1.0f); // White
case game::ItemQuality::UNCOMMON: return ImVec4(0.12f, 1.0f, 0.0f, 1.0f); // Green
case game::ItemQuality::RARE: return ImVec4(0.0f, 0.44f, 0.87f, 1.0f); // Blue
case game::ItemQuality::EPIC: return ImVec4(0.64f, 0.21f, 0.93f, 1.0f); // Purple
case game::ItemQuality::LEGENDARY: return ImVec4(1.0f, 0.50f, 0.0f, 1.0f); // Orange
default: return ImVec4(1.0f, 1.0f, 1.0f, 1.0f);
}
}
game::EquipSlot InventoryScreen::getEquipSlotForType(uint8_t inventoryType, game::Inventory& inv) {
switch (inventoryType) {
case 1: return game::EquipSlot::HEAD;
case 2: return game::EquipSlot::NECK;
case 3: return game::EquipSlot::SHOULDERS;
case 4: return game::EquipSlot::SHIRT;
case 5: return game::EquipSlot::CHEST;
case 6: return game::EquipSlot::WAIST;
case 7: return game::EquipSlot::LEGS;
case 8: return game::EquipSlot::FEET;
case 9: return game::EquipSlot::WRISTS;
case 10: return game::EquipSlot::HANDS;
case 11: {
// Ring: prefer empty slot, else RING1
if (inv.getEquipSlot(game::EquipSlot::RING1).empty())
return game::EquipSlot::RING1;
return game::EquipSlot::RING2;
}
case 12: {
// Trinket: prefer empty slot, else TRINKET1
if (inv.getEquipSlot(game::EquipSlot::TRINKET1).empty())
return game::EquipSlot::TRINKET1;
return game::EquipSlot::TRINKET2;
}
case 13: // One-Hand
case 21: // Main Hand
return game::EquipSlot::MAIN_HAND;
case 17: // Two-Hand
return game::EquipSlot::MAIN_HAND;
case 14: // Shield
case 22: // Off Hand
case 23: // Held In Off-hand
return game::EquipSlot::OFF_HAND;
case 15: // Ranged (bow/gun)
case 25: // Thrown
case 26: // Ranged
return game::EquipSlot::RANGED;
case 16: return game::EquipSlot::BACK;
case 19: return game::EquipSlot::TABARD;
case 20: return game::EquipSlot::CHEST; // Robe
default: return game::EquipSlot::NUM_SLOTS;
}
}
void InventoryScreen::pickupFromBackpack(game::Inventory& inv, int index) {
const auto& slot = inv.getBackpackSlot(index);
if (slot.empty()) return;
holdingItem = true;
heldItem = slot.item;
heldSource = HeldSource::BACKPACK;
heldBackpackIndex = index;
heldEquipSlot = game::EquipSlot::NUM_SLOTS;
inv.clearBackpackSlot(index);
}
void InventoryScreen::pickupFromEquipment(game::Inventory& inv, game::EquipSlot slot) {
const auto& es = inv.getEquipSlot(slot);
if (es.empty()) return;
holdingItem = true;
heldItem = es.item;
heldSource = HeldSource::EQUIPMENT;
heldBackpackIndex = -1;
heldEquipSlot = slot;
inv.clearEquipSlot(slot);
equipmentDirty = true;
}
void InventoryScreen::placeInBackpack(game::Inventory& inv, int index) {
if (!holdingItem) return;
const auto& target = inv.getBackpackSlot(index);
if (target.empty()) {
inv.setBackpackSlot(index, heldItem);
holdingItem = false;
} else {
// Swap
game::ItemDef targetItem = target.item;
inv.setBackpackSlot(index, heldItem);
heldItem = targetItem;
// Keep holding the swapped item - update source to this backpack slot
heldSource = HeldSource::BACKPACK;
heldBackpackIndex = index;
}
}
void InventoryScreen::placeInEquipment(game::Inventory& inv, game::EquipSlot slot) {
if (!holdingItem) return;
// Validate: check if the held item can go in this slot
if (heldItem.inventoryType > 0) {
game::EquipSlot validSlot = getEquipSlotForType(heldItem.inventoryType, inv);
if (validSlot == game::EquipSlot::NUM_SLOTS) return; // Not equippable
// For rings/trinkets, allow either slot
bool valid = (slot == validSlot);
if (!valid) {
if (heldItem.inventoryType == 11) // Ring
valid = (slot == game::EquipSlot::RING1 || slot == game::EquipSlot::RING2);
else if (heldItem.inventoryType == 12) // Trinket
valid = (slot == game::EquipSlot::TRINKET1 || slot == game::EquipSlot::TRINKET2);
}
if (!valid) return;
} else {
return; // No inventoryType means not equippable
}
const auto& target = inv.getEquipSlot(slot);
if (target.empty()) {
inv.setEquipSlot(slot, heldItem);
holdingItem = false;
} else {
// Swap
game::ItemDef targetItem = target.item;
inv.setEquipSlot(slot, heldItem);
heldItem = targetItem;
heldSource = HeldSource::EQUIPMENT;
heldEquipSlot = slot;
}
// Two-handed weapon in main hand clears the off-hand slot
if (slot == game::EquipSlot::MAIN_HAND &&
inv.getEquipSlot(game::EquipSlot::MAIN_HAND).item.inventoryType == 17) {
const auto& offHand = inv.getEquipSlot(game::EquipSlot::OFF_HAND);
if (!offHand.empty()) {
inv.addItem(offHand.item);
inv.clearEquipSlot(game::EquipSlot::OFF_HAND);
}
}
// Equipping off-hand unequips a 2H weapon from main hand
if (slot == game::EquipSlot::OFF_HAND &&
inv.getEquipSlot(game::EquipSlot::MAIN_HAND).item.inventoryType == 17) {
inv.addItem(inv.getEquipSlot(game::EquipSlot::MAIN_HAND).item);
inv.clearEquipSlot(game::EquipSlot::MAIN_HAND);
}
equipmentDirty = true;
}
void InventoryScreen::cancelPickup(game::Inventory& inv) {
if (!holdingItem) return;
// Return item to source
if (heldSource == HeldSource::BACKPACK && heldBackpackIndex >= 0) {
// If source slot is still empty, put it back
if (inv.getBackpackSlot(heldBackpackIndex).empty()) {
inv.setBackpackSlot(heldBackpackIndex, heldItem);
} else {
// Source was swapped into; find free slot
inv.addItem(heldItem);
}
} else if (heldSource == HeldSource::EQUIPMENT && heldEquipSlot != game::EquipSlot::NUM_SLOTS) {
if (inv.getEquipSlot(heldEquipSlot).empty()) {
inv.setEquipSlot(heldEquipSlot, heldItem);
equipmentDirty = true;
} else {
inv.addItem(heldItem);
}
} else {
// Fallback: just add to inventory
inv.addItem(heldItem);
}
holdingItem = false;
}
void InventoryScreen::renderHeldItem() {
if (!holdingItem) return;
ImGuiIO& io = ImGui::GetIO();
ImVec2 mousePos = io.MousePos;
float size = 36.0f;
ImVec2 pos(mousePos.x - size * 0.5f, mousePos.y - size * 0.5f);
ImDrawList* drawList = ImGui::GetForegroundDrawList();
ImVec4 qColor = getQualityColor(heldItem.quality);
ImU32 borderCol = ImGui::ColorConvertFloat4ToU32(qColor);
// Background
drawList->AddRectFilled(pos, ImVec2(pos.x + size, pos.y + size),
IM_COL32(40, 35, 30, 200));
drawList->AddRect(pos, ImVec2(pos.x + size, pos.y + size),
borderCol, 0.0f, 0, 2.0f);
// Item abbreviation
char abbr[4] = {};
if (!heldItem.name.empty()) {
abbr[0] = heldItem.name[0];
if (heldItem.name.size() > 1) abbr[1] = heldItem.name[1];
}
float textW = ImGui::CalcTextSize(abbr).x;
drawList->AddText(ImVec2(pos.x + (size - textW) * 0.5f, pos.y + 2.0f),
ImGui::ColorConvertFloat4ToU32(qColor), abbr);
// Stack count
if (heldItem.stackCount > 1) {
char countStr[16];
snprintf(countStr, sizeof(countStr), "%u", heldItem.stackCount);
float cw = ImGui::CalcTextSize(countStr).x;
drawList->AddText(ImVec2(pos.x + size - cw - 2.0f, pos.y + size - 14.0f),
IM_COL32(255, 255, 255, 220), countStr);
}
}
void InventoryScreen::render(game::Inventory& inventory) {
// B key toggle (edge-triggered)
bool uiWantsKeyboard = ImGui::GetIO().WantCaptureKeyboard;
bool bDown = !uiWantsKeyboard && core::Input::getInstance().isKeyPressed(SDL_SCANCODE_B);
if (bDown && !bKeyWasDown) {
open = !open;
}
bKeyWasDown = bDown;
if (!open) {
// Cancel held item if inventory closes
if (holdingItem) cancelPickup(inventory);
return;
}
// Escape cancels held item
if (holdingItem && !uiWantsKeyboard && core::Input::getInstance().isKeyPressed(SDL_SCANCODE_ESCAPE)) {
cancelPickup(inventory);
}
// Right-click anywhere while holding = cancel
if (holdingItem && ImGui::IsMouseClicked(ImGuiMouseButton_Right)) {
cancelPickup(inventory);
}
ImGuiIO& io = ImGui::GetIO();
float screenW = io.DisplaySize.x;
// Position inventory window on the right side of the screen
ImGui::SetNextWindowPos(ImVec2(screenW - 520.0f, 80.0f), ImGuiCond_FirstUseEver);
ImGui::SetNextWindowSize(ImVec2(500.0f, 560.0f), ImGuiCond_FirstUseEver);
ImGuiWindowFlags flags = ImGuiWindowFlags_NoCollapse;
if (!ImGui::Begin("Inventory", &open, flags)) {
ImGui::End();
return;
}
// Two-column layout: Equipment (left) | Backpack (right)
ImGui::BeginChild("EquipPanel", ImVec2(200.0f, 0.0f), true);
renderEquipmentPanel(inventory);
ImGui::EndChild();
ImGui::SameLine();
ImGui::BeginChild("BackpackPanel", ImVec2(0.0f, 0.0f), true);
renderBackpackPanel(inventory);
ImGui::EndChild();
ImGui::End();
// Draw held item at cursor (on top of everything)
renderHeldItem();
}
void InventoryScreen::renderEquipmentPanel(game::Inventory& inventory) {
ImGui::TextColored(ImVec4(1.0f, 0.84f, 0.0f, 1.0f), "Equipment");
ImGui::Separator();
static const game::EquipSlot leftSlots[] = {
game::EquipSlot::HEAD, game::EquipSlot::NECK,
game::EquipSlot::SHOULDERS, game::EquipSlot::BACK,
game::EquipSlot::CHEST, game::EquipSlot::SHIRT,
game::EquipSlot::TABARD, game::EquipSlot::WRISTS,
};
static const game::EquipSlot rightSlots[] = {
game::EquipSlot::HANDS, game::EquipSlot::WAIST,
game::EquipSlot::LEGS, game::EquipSlot::FEET,
game::EquipSlot::RING1, game::EquipSlot::RING2,
game::EquipSlot::TRINKET1, game::EquipSlot::TRINKET2,
};
constexpr float slotSize = 36.0f;
constexpr float spacing = 4.0f;
// Two columns of equipment
int rows = 8;
for (int r = 0; r < rows; r++) {
// Left slot
{
const auto& slot = inventory.getEquipSlot(leftSlots[r]);
const char* label = game::getEquipSlotName(leftSlots[r]);
char id[64];
snprintf(id, sizeof(id), "##eq_l_%d", r);
ImGui::PushID(id);
renderItemSlot(inventory, slot, slotSize, label,
SlotKind::EQUIPMENT, -1, leftSlots[r]);
ImGui::PopID();
}
ImGui::SameLine(slotSize + spacing + 60.0f);
// Right slot
{
const auto& slot = inventory.getEquipSlot(rightSlots[r]);
const char* label = game::getEquipSlotName(rightSlots[r]);
char id[64];
snprintf(id, sizeof(id), "##eq_r_%d", r);
ImGui::PushID(id);
renderItemSlot(inventory, slot, slotSize, label,
SlotKind::EQUIPMENT, -1, rightSlots[r]);
ImGui::PopID();
}
}
// Weapon row
ImGui::Spacing();
ImGui::Separator();
static const game::EquipSlot weaponSlots[] = {
game::EquipSlot::MAIN_HAND,
game::EquipSlot::OFF_HAND,
game::EquipSlot::RANGED,
};
for (int i = 0; i < 3; i++) {
if (i > 0) ImGui::SameLine();
const auto& slot = inventory.getEquipSlot(weaponSlots[i]);
const char* label = game::getEquipSlotName(weaponSlots[i]);
char id[64];
snprintf(id, sizeof(id), "##eq_w_%d", i);
ImGui::PushID(id);
renderItemSlot(inventory, slot, slotSize, label,
SlotKind::EQUIPMENT, -1, weaponSlots[i]);
ImGui::PopID();
}
}
void InventoryScreen::renderBackpackPanel(game::Inventory& inventory) {
ImGui::TextColored(ImVec4(1.0f, 0.84f, 0.0f, 1.0f), "Backpack");
ImGui::Separator();
constexpr float slotSize = 40.0f;
constexpr int columns = 4;
for (int i = 0; i < inventory.getBackpackSize(); i++) {
if (i % columns != 0) ImGui::SameLine();
const auto& slot = inventory.getBackpackSlot(i);
char id[32];
snprintf(id, sizeof(id), "##bp_%d", i);
ImGui::PushID(id);
renderItemSlot(inventory, slot, slotSize, nullptr,
SlotKind::BACKPACK, i, game::EquipSlot::NUM_SLOTS);
ImGui::PopID();
}
// Show extra bags if equipped
for (int bag = 0; bag < game::Inventory::NUM_BAG_SLOTS; bag++) {
int bagSize = inventory.getBagSize(bag);
if (bagSize <= 0) continue;
ImGui::Spacing();
ImGui::Separator();
char bagLabel[32];
snprintf(bagLabel, sizeof(bagLabel), "Bag %d", bag + 1);
ImGui::TextColored(ImVec4(0.8f, 0.8f, 0.0f, 1.0f), "%s", bagLabel);
for (int s = 0; s < bagSize; s++) {
if (s % columns != 0) ImGui::SameLine();
const auto& slot = inventory.getBagSlot(bag, s);
char sid[32];
snprintf(sid, sizeof(sid), "##bag%d_%d", bag, s);
ImGui::PushID(sid);
renderItemSlot(inventory, slot, slotSize, nullptr,
SlotKind::BACKPACK, -1, game::EquipSlot::NUM_SLOTS);
ImGui::PopID();
}
}
}
void InventoryScreen::renderItemSlot(game::Inventory& inventory, const game::ItemSlot& slot,
float size, const char* label,
SlotKind kind, int backpackIndex,
game::EquipSlot equipSlot) {
ImDrawList* drawList = ImGui::GetWindowDrawList();
ImVec2 pos = ImGui::GetCursorScreenPos();
bool isEmpty = slot.empty();
// Determine if this is a valid drop target for held item
bool validDrop = false;
if (holdingItem) {
if (kind == SlotKind::BACKPACK && backpackIndex >= 0) {
validDrop = true; // Can always drop in backpack
} else if (kind == SlotKind::EQUIPMENT && heldItem.inventoryType > 0) {
game::EquipSlot validSlot = getEquipSlotForType(heldItem.inventoryType, inventory);
validDrop = (equipSlot == validSlot);
if (!validDrop && heldItem.inventoryType == 11)
validDrop = (equipSlot == game::EquipSlot::RING1 || equipSlot == game::EquipSlot::RING2);
if (!validDrop && heldItem.inventoryType == 12)
validDrop = (equipSlot == game::EquipSlot::TRINKET1 || equipSlot == game::EquipSlot::TRINKET2);
}
}
if (isEmpty) {
// Empty slot: dark grey background
ImU32 bgCol = IM_COL32(30, 30, 30, 200);
ImU32 borderCol = IM_COL32(60, 60, 60, 200);
// Highlight valid drop targets
if (validDrop) {
bgCol = IM_COL32(20, 50, 20, 200);
borderCol = IM_COL32(0, 180, 0, 200);
}
drawList->AddRectFilled(pos, ImVec2(pos.x + size, pos.y + size), bgCol);
drawList->AddRect(pos, ImVec2(pos.x + size, pos.y + size), borderCol);
// Slot label for equipment slots
if (label) {
char abbr[4] = {};
abbr[0] = label[0];
if (label[1]) abbr[1] = label[1];
float textW = ImGui::CalcTextSize(abbr).x;
drawList->AddText(ImVec2(pos.x + (size - textW) * 0.5f, pos.y + size * 0.3f),
IM_COL32(80, 80, 80, 180), abbr);
}
ImGui::InvisibleButton("slot", ImVec2(size, size));
// Click interactions
if (ImGui::IsItemClicked(ImGuiMouseButton_Left) && holdingItem && validDrop) {
if (kind == SlotKind::BACKPACK && backpackIndex >= 0) {
placeInBackpack(inventory, backpackIndex);
} else if (kind == SlotKind::EQUIPMENT) {
placeInEquipment(inventory, equipSlot);
}
}
// Tooltip for empty equip slots
if (label && ImGui::IsItemHovered()) {
ImGui::BeginTooltip();
ImGui::TextColored(ImVec4(0.5f, 0.5f, 0.5f, 1.0f), "%s", label);
ImGui::TextColored(ImVec4(0.4f, 0.4f, 0.4f, 1.0f), "Empty");
ImGui::EndTooltip();
}
} else {
const auto& item = slot.item;
ImVec4 qColor = getQualityColor(item.quality);
ImU32 borderCol = ImGui::ColorConvertFloat4ToU32(qColor);
// Highlight valid drop targets with green tint
ImU32 bgCol = IM_COL32(40, 35, 30, 220);
if (holdingItem && validDrop) {
bgCol = IM_COL32(30, 55, 30, 220);
borderCol = IM_COL32(0, 200, 0, 220);
}
drawList->AddRectFilled(pos, ImVec2(pos.x + size, pos.y + size), bgCol);
drawList->AddRect(pos, ImVec2(pos.x + size, pos.y + size),
borderCol, 0.0f, 0, 2.0f);
// Item abbreviation (first 2 letters)
char abbr[4] = {};
if (!item.name.empty()) {
abbr[0] = item.name[0];
if (item.name.size() > 1) abbr[1] = item.name[1];
}
float textW = ImGui::CalcTextSize(abbr).x;
drawList->AddText(ImVec2(pos.x + (size - textW) * 0.5f, pos.y + 2.0f),
ImGui::ColorConvertFloat4ToU32(qColor), abbr);
// Stack count (bottom-right)
if (item.stackCount > 1) {
char countStr[16];
snprintf(countStr, sizeof(countStr), "%u", item.stackCount);
float cw = ImGui::CalcTextSize(countStr).x;
drawList->AddText(ImVec2(pos.x + size - cw - 2.0f, pos.y + size - 14.0f),
IM_COL32(255, 255, 255, 220), countStr);
}
ImGui::InvisibleButton("slot", ImVec2(size, size));
// Left-click: pickup or place/swap
if (ImGui::IsItemClicked(ImGuiMouseButton_Left)) {
if (!holdingItem) {
// Pick up this item
if (kind == SlotKind::BACKPACK && backpackIndex >= 0) {
pickupFromBackpack(inventory, backpackIndex);
} else if (kind == SlotKind::EQUIPMENT) {
pickupFromEquipment(inventory, equipSlot);
}
} else {
// Holding an item - place or swap
if (kind == SlotKind::BACKPACK && backpackIndex >= 0) {
placeInBackpack(inventory, backpackIndex);
} else if (kind == SlotKind::EQUIPMENT && validDrop) {
placeInEquipment(inventory, equipSlot);
}
}
}
// Right-click: auto-equip from backpack, or unequip from equipment
if (ImGui::IsItemClicked(ImGuiMouseButton_Right) && !holdingItem) {
if (kind == SlotKind::EQUIPMENT) {
// Unequip: move to free backpack slot
int freeSlot = inventory.findFreeBackpackSlot();
if (freeSlot >= 0) {
inventory.setBackpackSlot(freeSlot, item);
inventory.clearEquipSlot(equipSlot);
equipmentDirty = true;
}
} else if (kind == SlotKind::BACKPACK && backpackIndex >= 0 && item.inventoryType > 0) {
// Auto-equip: find the right slot
// Capture type before swap (item ref may become stale)
uint8_t equippingType = item.inventoryType;
game::EquipSlot targetSlot = getEquipSlotForType(equippingType, inventory);
if (targetSlot != game::EquipSlot::NUM_SLOTS) {
const auto& eqSlot = inventory.getEquipSlot(targetSlot);
if (eqSlot.empty()) {
inventory.setEquipSlot(targetSlot, item);
inventory.clearBackpackSlot(backpackIndex);
} else {
// Swap with equipped item
game::ItemDef equippedItem = eqSlot.item;
inventory.setEquipSlot(targetSlot, item);
inventory.setBackpackSlot(backpackIndex, equippedItem);
}
// Two-handed weapon in main hand clears the off-hand
if (targetSlot == game::EquipSlot::MAIN_HAND && equippingType == 17) {
const auto& offHand = inventory.getEquipSlot(game::EquipSlot::OFF_HAND);
if (!offHand.empty()) {
inventory.addItem(offHand.item);
inventory.clearEquipSlot(game::EquipSlot::OFF_HAND);
}
}
// Equipping off-hand unequips a 2H weapon from main hand
if (targetSlot == game::EquipSlot::OFF_HAND &&
inventory.getEquipSlot(game::EquipSlot::MAIN_HAND).item.inventoryType == 17) {
inventory.addItem(inventory.getEquipSlot(game::EquipSlot::MAIN_HAND).item);
inventory.clearEquipSlot(game::EquipSlot::MAIN_HAND);
}
equipmentDirty = true;
}
}
}
if (ImGui::IsItemHovered() && !holdingItem) {
renderItemTooltip(item);
}
}
}
void InventoryScreen::renderItemTooltip(const game::ItemDef& item) {
ImGui::BeginTooltip();
ImVec4 qColor = getQualityColor(item.quality);
ImGui::TextColored(qColor, "%s", item.name.c_str());
// Slot type
if (item.inventoryType > 0) {
const char* slotName = "";
switch (item.inventoryType) {
case 1: slotName = "Head"; break;
case 2: slotName = "Neck"; break;
case 3: slotName = "Shoulder"; break;
case 4: slotName = "Shirt"; break;
case 5: slotName = "Chest"; break;
case 6: slotName = "Waist"; break;
case 7: slotName = "Legs"; break;
case 8: slotName = "Feet"; break;
case 9: slotName = "Wrist"; break;
case 10: slotName = "Hands"; break;
case 11: slotName = "Finger"; break;
case 12: slotName = "Trinket"; break;
case 13: slotName = "One-Hand"; break;
case 14: slotName = "Shield"; break;
case 15: slotName = "Ranged"; break;
case 16: slotName = "Back"; break;
case 17: slotName = "Two-Hand"; break;
case 18: slotName = "Bag"; break;
case 19: slotName = "Tabard"; break;
case 20: slotName = "Robe"; break;
case 21: slotName = "Main Hand"; break;
case 22: slotName = "Off Hand"; break;
case 23: slotName = "Held In Off-hand"; break;
case 25: slotName = "Thrown"; break;
case 26: slotName = "Ranged"; break;
default: slotName = ""; break;
}
if (slotName[0]) {
if (!item.subclassName.empty()) {
ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "%s %s", slotName, item.subclassName.c_str());
} else {
ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "%s", slotName);
}
}
}
// Armor
if (item.armor > 0) {
ImGui::Text("%d Armor", item.armor);
}
// Stats
if (item.stamina != 0) ImGui::TextColored(ImVec4(0.0f, 1.0f, 0.0f, 1.0f), "+%d Stamina", item.stamina);
if (item.strength != 0) ImGui::TextColored(ImVec4(0.0f, 1.0f, 0.0f, 1.0f), "+%d Strength", item.strength);
if (item.agility != 0) ImGui::TextColored(ImVec4(0.0f, 1.0f, 0.0f, 1.0f), "+%d Agility", item.agility);
if (item.intellect != 0) ImGui::TextColored(ImVec4(0.0f, 1.0f, 0.0f, 1.0f), "+%d Intellect", item.intellect);
if (item.spirit != 0) ImGui::TextColored(ImVec4(0.0f, 1.0f, 0.0f, 1.0f), "+%d Spirit", item.spirit);
// Stack info
if (item.maxStack > 1) {
ImGui::TextColored(ImVec4(0.5f, 0.5f, 0.5f, 1.0f), "Stack: %u/%u", item.stackCount, item.maxStack);
}
ImGui::EndTooltip();
}
} // namespace ui
} // namespace wowee

180
src/ui/realm_screen.cpp Normal file
View file

@ -0,0 +1,180 @@
#include "ui/realm_screen.hpp"
#include <imgui.h>
namespace wowee { namespace ui {
RealmScreen::RealmScreen() {
}
void RealmScreen::render(auth::AuthHandler& authHandler) {
ImGui::SetNextWindowSize(ImVec2(700, 500), ImGuiCond_FirstUseEver);
ImGui::Begin("Realm Selection", nullptr, ImGuiWindowFlags_NoCollapse);
ImGui::Text("Select a Realm");
ImGui::Separator();
ImGui::Spacing();
// Status message
if (!statusMessage.empty()) {
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.3f, 1.0f, 0.3f, 1.0f));
ImGui::TextWrapped("%s", statusMessage.c_str());
ImGui::PopStyleColor();
ImGui::Spacing();
}
// Get realm list
const auto& realms = authHandler.getRealms();
if (realms.empty()) {
ImGui::Text("No realms available. Requesting realm list...");
authHandler.requestRealmList();
} else {
// Realm table
if (ImGui::BeginTable("RealmsTable", 5, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg)) {
ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_WidthStretch);
ImGui::TableSetupColumn("Type", ImGuiTableColumnFlags_WidthFixed, 60.0f);
ImGui::TableSetupColumn("Population", ImGuiTableColumnFlags_WidthFixed, 80.0f);
ImGui::TableSetupColumn("Characters", ImGuiTableColumnFlags_WidthFixed, 80.0f);
ImGui::TableSetupColumn("Status", ImGuiTableColumnFlags_WidthFixed, 100.0f);
ImGui::TableHeadersRow();
for (size_t i = 0; i < realms.size(); ++i) {
const auto& realm = realms[i];
ImGui::TableNextRow();
// Name column (selectable)
ImGui::TableSetColumnIndex(0);
bool isSelected = (selectedRealmIndex == static_cast<int>(i));
if (ImGui::Selectable(realm.name.c_str(), isSelected, ImGuiSelectableFlags_SpanAllColumns)) {
selectedRealmIndex = static_cast<int>(i);
}
// Type column
ImGui::TableSetColumnIndex(1);
if (realm.icon == 0) {
ImGui::Text("Normal");
} else if (realm.icon == 1) {
ImGui::Text("PvP");
} else if (realm.icon == 4) {
ImGui::Text("RP");
} else if (realm.icon == 6) {
ImGui::Text("RP-PvP");
} else {
ImGui::Text("Type %d", realm.icon);
}
// Population column
ImGui::TableSetColumnIndex(2);
ImVec4 popColor = getPopulationColor(realm.population);
ImGui::PushStyleColor(ImGuiCol_Text, popColor);
if (realm.population < 0.5f) {
ImGui::Text("Low");
} else if (realm.population < 1.0f) {
ImGui::Text("Medium");
} else if (realm.population < 2.0f) {
ImGui::Text("High");
} else {
ImGui::Text("Full");
}
ImGui::PopStyleColor();
// Characters column
ImGui::TableSetColumnIndex(3);
ImGui::Text("%d", realm.characters);
// Status column
ImGui::TableSetColumnIndex(4);
const char* status = getRealmStatus(realm.flags);
if (realm.lock) {
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.3f, 0.3f, 1.0f));
ImGui::Text("Locked");
ImGui::PopStyleColor();
} else {
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.3f, 1.0f, 0.3f, 1.0f));
ImGui::Text("%s", status);
ImGui::PopStyleColor();
}
}
ImGui::EndTable();
}
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
// Selected realm info
if (selectedRealmIndex >= 0 && selectedRealmIndex < static_cast<int>(realms.size())) {
const auto& realm = realms[selectedRealmIndex];
ImGui::Text("Selected Realm:");
ImGui::Indent();
ImGui::Text("Name: %s", realm.name.c_str());
ImGui::Text("Address: %s", realm.address.c_str());
ImGui::Text("Characters: %d", realm.characters);
if (realm.hasVersionInfo()) {
ImGui::Text("Version: %d.%d.%d (build %d)",
realm.majorVersion, realm.minorVersion, realm.patchVersion, realm.build);
}
ImGui::Unindent();
ImGui::Spacing();
// Connect button
if (!realm.lock) {
if (ImGui::Button("Enter Realm", ImVec2(120, 0))) {
realmSelected = true;
selectedRealmName = realm.name;
selectedRealmAddress = realm.address;
setStatus("Connecting to realm: " + realm.name);
// Call callback
if (onRealmSelected) {
onRealmSelected(selectedRealmName, selectedRealmAddress);
}
}
} else {
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.5f, 0.5f, 0.5f, 1.0f));
ImGui::Button("Realm Locked", ImVec2(120, 0));
ImGui::PopStyleColor();
}
}
}
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
// Refresh button
if (ImGui::Button("Refresh Realm List", ImVec2(150, 0))) {
authHandler.requestRealmList();
setStatus("Refreshing realm list...");
}
ImGui::End();
}
void RealmScreen::setStatus(const std::string& message) {
statusMessage = message;
}
const char* RealmScreen::getRealmStatus(uint8_t flags) const {
if (flags & 0x01) return "Invalid";
if (flags & 0x02) return "Offline";
return "Online";
}
ImVec4 RealmScreen::getPopulationColor(float population) const {
if (population < 0.5f) {
return ImVec4(0.3f, 1.0f, 0.3f, 1.0f); // Green - Low
} else if (population < 1.0f) {
return ImVec4(1.0f, 1.0f, 0.3f, 1.0f); // Yellow - Medium
} else if (population < 2.0f) {
return ImVec4(1.0f, 0.6f, 0.0f, 1.0f); // Orange - High
} else {
return ImVec4(1.0f, 0.3f, 0.3f, 1.0f); // Red - Full
}
}
}} // namespace wowee::ui

144
src/ui/ui_manager.cpp Normal file
View file

@ -0,0 +1,144 @@
#include "ui/ui_manager.hpp"
#include "core/window.hpp"
#include "core/application.hpp"
#include "core/logger.hpp"
#include "auth/auth_handler.hpp"
#include "game/game_handler.hpp"
#include <imgui.h>
#include <imgui_impl_sdl2.h>
#include <imgui_impl_opengl3.h>
namespace wowee {
namespace ui {
UIManager::UIManager() {
// Create screen instances
authScreen = std::make_unique<AuthScreen>();
realmScreen = std::make_unique<RealmScreen>();
characterScreen = std::make_unique<CharacterScreen>();
gameScreen = std::make_unique<GameScreen>();
}
UIManager::~UIManager() = default;
bool UIManager::initialize(core::Window* win) {
window = win;
LOG_INFO("Initializing UI manager");
// Initialize ImGui
IMGUI_CHECKVERSION();
ImGui::CreateContext();
ImGuiIO& io = ImGui::GetIO();
io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;
io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad;
// Setup ImGui style
ImGui::StyleColorsDark();
// Customize style for better WoW feel
ImGuiStyle& style = ImGui::GetStyle();
style.WindowRounding = 6.0f;
style.FrameRounding = 4.0f;
style.GrabRounding = 4.0f;
style.WindowBorderSize = 1.0f;
style.FrameBorderSize = 1.0f;
// WoW-inspired colors
ImVec4* colors = style.Colors;
colors[ImGuiCol_WindowBg] = ImVec4(0.08f, 0.08f, 0.12f, 0.94f);
colors[ImGuiCol_TitleBg] = ImVec4(0.10f, 0.10f, 0.15f, 1.00f);
colors[ImGuiCol_TitleBgActive] = ImVec4(0.15f, 0.15f, 0.25f, 1.00f);
colors[ImGuiCol_Button] = ImVec4(0.20f, 0.25f, 0.40f, 1.00f);
colors[ImGuiCol_ButtonHovered] = ImVec4(0.25f, 0.30f, 0.50f, 1.00f);
colors[ImGuiCol_ButtonActive] = ImVec4(0.15f, 0.20f, 0.35f, 1.00f);
colors[ImGuiCol_Header] = ImVec4(0.20f, 0.25f, 0.40f, 0.55f);
colors[ImGuiCol_HeaderHovered] = ImVec4(0.25f, 0.30f, 0.50f, 0.80f);
colors[ImGuiCol_HeaderActive] = ImVec4(0.20f, 0.25f, 0.45f, 1.00f);
// Initialize ImGui for SDL2 and OpenGL3
ImGui_ImplSDL2_InitForOpenGL(window->getSDLWindow(), window->getGLContext());
ImGui_ImplOpenGL3_Init("#version 330 core");
imguiInitialized = true;
LOG_INFO("UI manager initialized successfully");
return true;
}
void UIManager::shutdown() {
if (imguiInitialized) {
ImGui_ImplOpenGL3_Shutdown();
ImGui_ImplSDL2_Shutdown();
ImGui::DestroyContext();
imguiInitialized = false;
}
LOG_INFO("UI manager shutdown");
}
void UIManager::update(float deltaTime) {
if (!imguiInitialized) return;
// Start ImGui frame
ImGui_ImplOpenGL3_NewFrame();
ImGui_ImplSDL2_NewFrame();
ImGui::NewFrame();
}
void UIManager::render(core::AppState appState, auth::AuthHandler* authHandler, game::GameHandler* gameHandler) {
if (!imguiInitialized) return;
// Render appropriate screen based on application state
switch (appState) {
case core::AppState::AUTHENTICATION:
if (authHandler) {
authScreen->render(*authHandler);
}
break;
case core::AppState::REALM_SELECTION:
if (authHandler) {
realmScreen->render(*authHandler);
}
break;
case core::AppState::CHARACTER_SELECTION:
if (gameHandler) {
characterScreen->render(*gameHandler);
}
break;
case core::AppState::IN_GAME:
if (gameHandler) {
gameScreen->render(*gameHandler);
}
break;
case core::AppState::DISCONNECTED:
// Show disconnected message
ImGui::SetNextWindowSize(ImVec2(400, 150), ImGuiCond_Always);
ImGui::SetNextWindowPos(ImVec2(ImGui::GetIO().DisplaySize.x * 0.5f - 200,
ImGui::GetIO().DisplaySize.y * 0.5f - 75),
ImGuiCond_Always);
ImGui::Begin("Disconnected", nullptr, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize);
ImGui::TextWrapped("You have been disconnected from the server.");
ImGui::Spacing();
if (ImGui::Button("Return to Login", ImVec2(-1, 0))) {
// Will be handled by application
}
ImGui::End();
break;
}
// Render ImGui
ImGui::Render();
ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
}
void UIManager::processEvent(const SDL_Event& event) {
if (imguiInitialized) {
ImGui_ImplSDL2_ProcessEvent(&event);
}
}
} // namespace ui
} // namespace wowee