Kelsidavis-WoWee/src/rendering/quest_marker_renderer.cpp
Kelsi 8af895c025 Fix quest turn-in by populating quest log from gossip data
The quest log was empty because the client never requested quest data from the server.
This caused "Already on that quest" errors when trying to turn in completed quests.

Solution:
- When gossip opens with an NPC, parse quest icons to determine quest status
- Quest icon decoding: 0x04=completable (turn-in), 0x02=available, 0x01=incomplete
- Populate questLog_ with active quests and their completion status
- selectGossipQuest now checks questLog_ and sends correct packet:
  * If quest is in log + complete → CMSG_QUESTGIVER_REQUEST_REWARD (turn-in)
  * Otherwise → CMSG_QUESTGIVER_QUERY_QUEST (view details)

Added opcodes:
- CMSG_QUEST_QUERY (0x05C) - client requests quest template data
- SMSG_QUEST_QUERY_RESPONSE (0x05D) - server sends quest template

Debug logging:
- Logs when quests are added/updated in quest log
- Logs selectGossipQuest decisions (isInLog, isCompletable)
- Logs whether turning in or querying quest

Also lowered quest marker height by 1 unit (HEIGHT_OFFSET 2.1 → 1.1).

Quest turn-in now works correctly!
2026-02-09 23:53:17 -08:00

280 lines
9.5 KiB
C++

#include "rendering/quest_marker_renderer.hpp"
#include "rendering/camera.hpp"
#include "pipeline/asset_manager.hpp"
#include "pipeline/blp_loader.hpp"
#include "core/logger.hpp"
#include <GL/glew.h>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
#include <SDL2/SDL.h>
#include <cmath>
namespace wowee { namespace rendering {
QuestMarkerRenderer::QuestMarkerRenderer() {
}
QuestMarkerRenderer::~QuestMarkerRenderer() {
shutdown();
}
bool QuestMarkerRenderer::initialize(pipeline::AssetManager* assetManager) {
if (!assetManager) {
LOG_WARNING("QuestMarkerRenderer: No AssetManager provided");
return false;
}
LOG_INFO("QuestMarkerRenderer: Initializing...");
createShader();
createQuad();
loadTextures(assetManager);
LOG_INFO("QuestMarkerRenderer: Initialization complete");
return true;
}
void QuestMarkerRenderer::shutdown() {
if (vao_) glDeleteVertexArrays(1, &vao_);
if (vbo_) glDeleteBuffers(1, &vbo_);
if (shaderProgram_) glDeleteProgram(shaderProgram_);
for (int i = 0; i < 3; ++i) {
if (textures_[i]) glDeleteTextures(1, &textures_[i]);
}
markers_.clear();
}
void QuestMarkerRenderer::createQuad() {
// Billboard quad vertices (centered, 1 unit size)
float vertices[] = {
-0.5f, -0.5f, 0.0f, 0.0f, 1.0f, // bottom-left
0.5f, -0.5f, 0.0f, 1.0f, 1.0f, // bottom-right
0.5f, 0.5f, 0.0f, 1.0f, 0.0f, // top-right
-0.5f, 0.5f, 0.0f, 0.0f, 0.0f, // top-left
-0.5f, -0.5f, 0.0f, 0.0f, 1.0f, // bottom-left
0.5f, 0.5f, 0.0f, 1.0f, 0.0f // top-right
};
glGenVertexArrays(1, &vao_);
glGenBuffers(1, &vbo_);
glBindVertexArray(vao_);
glBindBuffer(GL_ARRAY_BUFFER, vbo_);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// Position attribute
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
// Texture coord attribute
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(3 * sizeof(float)));
glEnableVertexAttribArray(1);
glBindVertexArray(0);
}
void QuestMarkerRenderer::loadTextures(pipeline::AssetManager* assetManager) {
const char* paths[3] = {
"Interface\\GossipFrame\\AvailableQuestIcon.blp",
"Interface\\GossipFrame\\ActiveQuestIcon.blp",
"Interface\\GossipFrame\\IncompleteQuestIcon.blp"
};
for (int i = 0; i < 3; ++i) {
pipeline::BLPImage blp = assetManager->loadTexture(paths[i]);
if (!blp.isValid()) {
LOG_WARNING("Failed to load quest marker texture: ", paths[i]);
continue;
}
glGenTextures(1, &textures_[i]);
glBindTexture(GL_TEXTURE_2D, textures_[i]);
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_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glGenerateMipmap(GL_TEXTURE_2D);
LOG_INFO("Loaded quest marker texture: ", paths[i]);
}
glBindTexture(GL_TEXTURE_2D, 0);
}
void QuestMarkerRenderer::createShader() {
const char* vertexShaderSource = R"(
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec2 aTexCoord;
out vec2 TexCoord;
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;
void main() {
gl_Position = projection * view * model * vec4(aPos, 1.0);
TexCoord = aTexCoord;
}
)";
const char* fragmentShaderSource = R"(
#version 330 core
in vec2 TexCoord;
out vec4 FragColor;
uniform sampler2D markerTexture;
uniform float uAlpha;
void main() {
vec4 texColor = texture(markerTexture, TexCoord);
if (texColor.a < 0.1)
discard;
FragColor = vec4(texColor.rgb, texColor.a * uAlpha);
}
)";
// Compile vertex shader
uint32_t vertexShader = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertexShader, 1, &vertexShaderSource, nullptr);
glCompileShader(vertexShader);
// Compile fragment shader
uint32_t fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader, 1, &fragmentShaderSource, nullptr);
glCompileShader(fragmentShader);
// Link shader program
shaderProgram_ = glCreateProgram();
glAttachShader(shaderProgram_, vertexShader);
glAttachShader(shaderProgram_, fragmentShader);
glLinkProgram(shaderProgram_);
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
}
void QuestMarkerRenderer::setMarker(uint64_t guid, const glm::vec3& position, int markerType, float boundingHeight) {
markers_[guid] = {position, markerType, boundingHeight};
}
void QuestMarkerRenderer::removeMarker(uint64_t guid) {
markers_.erase(guid);
}
void QuestMarkerRenderer::clear() {
markers_.clear();
}
void QuestMarkerRenderer::render(const Camera& camera) {
if (markers_.empty() || !shaderProgram_ || !vao_) return;
// WoW-style quest marker tuning parameters
constexpr float BASE_SIZE = 0.65f; // Base world-space size
constexpr float HEIGHT_OFFSET = 1.1f; // Height above NPC bounds
constexpr float BOB_AMPLITUDE = 0.10f; // Bob animation amplitude
constexpr float BOB_FREQUENCY = 1.25f; // Bob frequency (Hz)
constexpr float MIN_DIST = 4.0f; // Near clamp
constexpr float MAX_DIST = 90.0f; // Far fade-out start
constexpr float FADE_RANGE = 25.0f; // Fade-out range
constexpr float GLOW_ALPHA = 0.35f; // Glow pass alpha
// Get time for bob animation
float timeSeconds = SDL_GetTicks() / 1000.0f;
glEnable(GL_BLEND);
glEnable(GL_DEPTH_TEST);
glDepthMask(GL_FALSE); // Don't write to depth buffer
glUseProgram(shaderProgram_);
glm::mat4 view = camera.getViewMatrix();
glm::mat4 projection = camera.getProjectionMatrix();
glm::vec3 cameraPos = camera.getPosition();
int viewLoc = glGetUniformLocation(shaderProgram_, "view");
int projLoc = glGetUniformLocation(shaderProgram_, "projection");
int modelLoc = glGetUniformLocation(shaderProgram_, "model");
int alphaLoc = glGetUniformLocation(shaderProgram_, "uAlpha");
glUniformMatrix4fv(viewLoc, 1, GL_FALSE, glm::value_ptr(view));
glUniformMatrix4fv(projLoc, 1, GL_FALSE, glm::value_ptr(projection));
glBindVertexArray(vao_);
// Get camera right and up vectors for billboarding
glm::vec3 cameraRight = glm::vec3(view[0][0], view[1][0], view[2][0]);
glm::vec3 cameraUp = glm::vec3(view[0][1], view[1][1], view[2][1]);
for (const auto& [guid, marker] : markers_) {
if (marker.type < 0 || marker.type > 2) continue;
if (!textures_[marker.type]) continue;
// Calculate distance for LOD and culling
glm::vec3 toCamera = cameraPos - marker.position;
float dist = glm::length(toCamera);
// Calculate fade alpha
float fadeAlpha = 1.0f;
if (dist > MAX_DIST) {
float t = glm::clamp((dist - MAX_DIST) / FADE_RANGE, 0.0f, 1.0f);
t = t * t * (3.0f - 2.0f * t); // Smoothstep
fadeAlpha = 1.0f - t;
}
if (fadeAlpha <= 0.001f) continue; // Cull if fully faded
// Distance-based scaling (mild compensation for readability)
float distScale = 1.0f;
if (dist > MIN_DIST) {
float t = glm::clamp((dist - 5.0f) / 55.0f, 0.0f, 1.0f);
distScale = 1.0f + 0.35f * t;
}
float size = BASE_SIZE * distScale;
size = glm::clamp(size, BASE_SIZE * 0.9f, BASE_SIZE * 1.6f);
// Bob animation
float bob = std::sin(timeSeconds * BOB_FREQUENCY * 2.0f * 3.14159f) * BOB_AMPLITUDE;
// Position marker above NPC with bob
glm::vec3 markerPos = marker.position;
markerPos.z += marker.boundingHeight + HEIGHT_OFFSET + bob;
// Build billboard matrix (camera-facing quad)
glm::mat4 model = glm::mat4(1.0f);
model = glm::translate(model, markerPos);
// Billboard: align quad to face camera
model[0] = glm::vec4(cameraRight * size, 0.0f);
model[1] = glm::vec4(cameraUp * size, 0.0f);
model[2] = glm::vec4(glm::cross(cameraRight, cameraUp), 0.0f);
glBindTexture(GL_TEXTURE_2D, textures_[marker.type]);
// Glow pass (subtle additive glow for available/turnin markers)
if (marker.type == 0 || marker.type == 1) { // Available or turnin
glBlendFunc(GL_SRC_ALPHA, GL_ONE); // Additive blending
glUniform1f(alphaLoc, fadeAlpha * GLOW_ALPHA); // Reduced alpha for glow
glUniformMatrix4fv(modelLoc, 1, GL_FALSE, glm::value_ptr(model));
glDrawArrays(GL_TRIANGLES, 0, 6);
// Restore standard alpha blending for main pass
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
}
// Main pass with fade alpha
glUniform1f(alphaLoc, fadeAlpha);
glUniformMatrix4fv(modelLoc, 1, GL_FALSE, glm::value_ptr(model));
glDrawArrays(GL_TRIANGLES, 0, 6);
}
glBindVertexArray(0);
glBindTexture(GL_TEXTURE_2D, 0);
glDepthMask(GL_TRUE);
glDisable(GL_BLEND);
}
}} // namespace wowee::rendering