mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-05-05 00:33:51 +00:00
Initial commit: wowee native WoW 3.3.5a client
This commit is contained in:
commit
ce6cb8f38e
147 changed files with 32347 additions and 0 deletions
142
src/audio/music_manager.cpp
Normal file
142
src/audio/music_manager.cpp
Normal 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
289
src/auth/auth_handler.cpp
Normal 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
32
src/auth/auth_opcodes.cpp
Normal 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
312
src/auth/auth_packets.cpp
Normal 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
152
src/auth/big_num.cpp
Normal 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
33
src/auth/crypto.cpp
Normal 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
75
src/auth/rc4.cpp
Normal 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
269
src/auth/srp.cpp
Normal 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
1403
src/core/application.cpp
Normal file
File diff suppressed because it is too large
Load diff
81
src/core/input.cpp
Normal file
81
src/core/input.cpp
Normal 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
52
src/core/logger.cpp
Normal 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
134
src/core/window.cpp
Normal 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
48
src/game/character.cpp
Normal 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
37
src/game/entity.cpp
Normal 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
843
src/game/game_handler.cpp
Normal 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
274
src/game/inventory.cpp
Normal 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
374
src/game/npc_manager.cpp
Normal 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
7
src/game/opcodes.cpp
Normal 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
7
src/game/player.cpp
Normal 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
15
src/game/world.cpp
Normal 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
865
src/game/world_packets.cpp
Normal 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
116
src/game/zone_manager.cpp
Normal 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
32
src/main.cpp
Normal 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
90
src/network/packet.cpp
Normal 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
9
src/network/socket.cpp
Normal 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
227
src/network/tcp_socket.cpp
Normal 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
|
||||
236
src/network/world_socket.cpp
Normal file
236
src/network/world_socket.cpp
Normal 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
564
src/pipeline/adt_loader.cpp
Normal 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
|
||||
154
src/pipeline/asset_manager.cpp
Normal file
154
src/pipeline/asset_manager.cpp
Normal 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
437
src/pipeline/blp_loader.cpp
Normal 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
162
src/pipeline/dbc_loader.cpp
Normal 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
744
src/pipeline/m2_loader.cpp
Normal 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
|
||||
358
src/pipeline/mpq_manager.cpp
Normal file
358
src/pipeline/mpq_manager.cpp
Normal 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
|
||||
345
src/pipeline/terrain_mesh.cpp
Normal file
345
src/pipeline/terrain_mesh.cpp
Normal 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
556
src/pipeline/wmo_loader.cpp
Normal 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
56
src/rendering/camera.cpp
Normal 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
|
||||
525
src/rendering/camera_controller.cpp
Normal file
525
src/rendering/camera_controller.cpp
Normal 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
412
src/rendering/celestial.cpp
Normal 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
|
||||
1240
src/rendering/character_renderer.cpp
Normal file
1240
src/rendering/character_renderer.cpp
Normal file
File diff suppressed because it is too large
Load diff
312
src/rendering/clouds.cpp
Normal file
312
src/rendering/clouds.cpp
Normal 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
106
src/rendering/frustum.cpp
Normal 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
|
||||
288
src/rendering/lens_flare.cpp
Normal file
288
src/rendering/lens_flare.cpp
Normal 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
414
src/rendering/lightning.cpp
Normal 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
|
||||
466
src/rendering/m2_renderer.cpp
Normal file
466
src/rendering/m2_renderer.cpp
Normal 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
|
||||
8
src/rendering/material.cpp
Normal file
8
src/rendering/material.cpp
Normal 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
56
src/rendering/mesh.cpp
Normal 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
213
src/rendering/minimap.cpp
Normal 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
|
||||
416
src/rendering/performance_hud.cpp
Normal file
416
src/rendering/performance_hud.cpp
Normal 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
911
src/rendering/renderer.cpp
Normal 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
24
src/rendering/scene.cpp
Normal 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
127
src/rendering/shader.cpp
Normal 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
334
src/rendering/skybox.cpp
Normal 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
259
src/rendering/starfield.cpp
Normal 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
|
||||
380
src/rendering/swim_effects.cpp
Normal file
380
src/rendering/swim_effects.cpp
Normal 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
|
||||
826
src/rendering/terrain_manager.cpp
Normal file
826
src/rendering/terrain_manager.cpp
Normal 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
|
||||
520
src/rendering/terrain_renderer.cpp
Normal file
520
src/rendering/terrain_renderer.cpp
Normal 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
51
src/rendering/texture.cpp
Normal 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
|
||||
497
src/rendering/water_renderer.cpp
Normal file
497
src/rendering/water_renderer.cpp
Normal 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
275
src/rendering/weather.cpp
Normal 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
|
||||
835
src/rendering/wmo_renderer.cpp
Normal file
835
src/rendering/wmo_renderer.cpp
Normal 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öller–Trumbore 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
150
src/ui/auth_screen.cpp
Normal 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
211
src/ui/character_screen.cpp
Normal 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
861
src/ui/game_screen.cpp
Normal 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
633
src/ui/inventory_screen.cpp
Normal 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
180
src/ui/realm_screen.cpp
Normal 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
144
src/ui/ui_manager.cpp
Normal 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
|
||||
Loading…
Add table
Add a link
Reference in a new issue