From f520511139114d4fd97cee19086496e8e8209658 Mon Sep 17 00:00:00 2001 From: Kelsi Davis Date: Sat, 4 Apr 2026 01:16:28 -0700 Subject: [PATCH 1/7] fix: chdir to executable directory at startup for relative asset paths The binary assumed it was always launched from its own directory, causing shader/asset loads to fail when run from any other working directory. --- src/main.cpp | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/main.cpp b/src/main.cpp index 2bb2f75e..5bcd939f 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -6,6 +6,11 @@ #include #include #include +#ifdef __APPLE__ +#include +#include +#include +#endif #ifdef __linux__ #include #include @@ -97,6 +102,24 @@ int main([[maybe_unused]] int argc, [[maybe_unused]] char* argv[]) { std::signal(SIGTERM, crashHandler); std::signal(SIGINT, crashHandler); #endif + // Change working directory to the executable's directory so relative asset + // paths (assets/shaders/, Data/, etc.) resolve correctly from any launch location. +#ifdef __APPLE__ + { + uint32_t bufSize = 0; + _NSGetExecutablePath(nullptr, &bufSize); + std::string exePath(bufSize, '\0'); + _NSGetExecutablePath(exePath.data(), &bufSize); + chdir(dirname(exePath.data())); + } +#elif defined(__linux__) + { + char buf[4096]; + ssize_t len = readlink("/proc/self/exe", buf, sizeof(buf) - 1); + if (len > 0) { buf[len] = '\0'; chdir(dirname(buf)); } + } +#endif + try { wowee::core::Logger::getInstance().setLogLevel(readLogLevelFromEnv()); LOG_INFO("=== Wowee Native Client ==="); From bde9bd20d806bc45e7ccbdbb608bf7a333020875 Mon Sep 17 00:00:00 2001 From: Kelsi Davis Date: Sat, 4 Apr 2026 01:16:28 -0700 Subject: [PATCH 2/7] fix(rendering): use separate timer for global sequence bones Global sequence bones (hair, cape, physics) need time values spanning their full duration (up to ~968733ms), but animationTime wraps at the current animation's sequence duration (~2000ms for walk). This caused vertex spikes projecting from fingers/neck/ponytail as bones got stuck in the first ~2s of their loop. Add a separate globalSequenceTime accumulator that is not wrapped at the animation duration. --- include/rendering/character_renderer.hpp | 5 +++-- src/rendering/character_renderer.cpp | 24 +++++++++++++++--------- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/include/rendering/character_renderer.hpp b/include/rendering/character_renderer.hpp index a667f069..025f9924 100644 --- a/include/rendering/character_renderer.hpp +++ b/include/rendering/character_renderer.hpp @@ -158,6 +158,7 @@ private: uint32_t currentAnimationId = 0; int currentSequenceIndex = -1; // Index into M2Model::sequences float animationTime = 0.0f; + float globalSequenceTime = 0.0f; // Separate timer for global sequences (accumulates without wrapping at sequence duration) bool animationLoop = true; bool isDead = false; // Prevents movement while in death state std::vector boneMatrices; // Current bone transforms @@ -206,8 +207,8 @@ private: void calculateBindPose(M2ModelGPU& gpuModel); void updateAnimation(CharacterInstance& instance, float deltaTime); void calculateBoneMatrices(CharacterInstance& instance); - glm::mat4 getBoneTransform(const pipeline::M2Bone& bone, float time, int sequenceIndex, - const std::vector& globalSeqDurations); + glm::mat4 getBoneTransform(const pipeline::M2Bone& bone, float animTime, float globalSeqTime, + int sequenceIndex, const std::vector& globalSeqDurations); glm::mat4 getModelMatrix(const CharacterInstance& instance) const; void destroyModelGPU(M2ModelGPU& gpuModel, bool defer = false); void destroyInstanceBones(CharacterInstance& inst, bool defer = false); diff --git a/src/rendering/character_renderer.cpp b/src/rendering/character_renderer.cpp index def7d941..79412156 100644 --- a/src/rendering/character_renderer.cpp +++ b/src/rendering/character_renderer.cpp @@ -1690,6 +1690,9 @@ void CharacterRenderer::update(float deltaTime, const glm::vec3& cameraPos) { float distSq = glm::distance2(inst.position, cameraPos); if (distSq >= animUpdateRadiusSq) continue; + // Advance global sequence timer (accumulates independently of animation wrapping) + inst.globalSequenceTime += deltaTime * 1000.0f; + // Always advance animation time (cheap) if (inst.cachedModel && !inst.cachedModel->data.sequences.empty()) { if (inst.currentSequenceIndex < 0) { @@ -1852,8 +1855,10 @@ int CharacterRenderer::findKeyframeIndex(const std::vector& timestamps } // Resolve sequence index and time for a track, handling global sequences. +// globalSeqTime is a separate accumulating timer that is NOT wrapped at the +// current animation's sequence duration, so global sequences get full range. static void resolveTrackTime(const pipeline::M2AnimationTrack& track, - int seqIdx, float time, + int seqIdx, float animTime, float globalSeqTime, const std::vector& globalSeqDurations, int& outSeqIdx, float& outTime) { if (track.globalSequence >= 0 && @@ -1861,14 +1866,14 @@ static void resolveTrackTime(const pipeline::M2AnimationTrack& track, outSeqIdx = 0; float dur = static_cast(globalSeqDurations[track.globalSequence]); if (dur > 0.0f) { - outTime = std::fmod(time, dur); + outTime = std::fmod(globalSeqTime, dur); if (outTime < 0.0f) outTime += dur; } else { outTime = 0.0f; } } else { outSeqIdx = seqIdx; - outTime = time; + outTime = animTime; } } @@ -1959,7 +1964,8 @@ void CharacterRenderer::calculateBoneMatrices(CharacterInstance& instance) { // Local transform includes pivot bracket: T(pivot)*T*R*S*T(-pivot) // At rest this is identity, so no separate bind pose is needed - glm::mat4 localTransform = getBoneTransform(bone, instance.animationTime, instance.currentSequenceIndex, gsd); + glm::mat4 localTransform = getBoneTransform(bone, instance.animationTime, instance.globalSequenceTime, + instance.currentSequenceIndex, gsd); // Compose with parent if (bone.parentBone >= 0 && static_cast(bone.parentBone) < numBones) { @@ -1970,16 +1976,16 @@ void CharacterRenderer::calculateBoneMatrices(CharacterInstance& instance) { } } -glm::mat4 CharacterRenderer::getBoneTransform(const pipeline::M2Bone& bone, float time, int sequenceIndex, - const std::vector& globalSeqDurations) { +glm::mat4 CharacterRenderer::getBoneTransform(const pipeline::M2Bone& bone, float animTime, float globalSeqTime, + int sequenceIndex, const std::vector& globalSeqDurations) { // Resolve global sequences: bones with globalSequence >= 0 use sequence 0 // with time wrapped at the global sequence duration, independent of the // character's current animation. int tSeq, rSeq, sSeq; float tTime, rTime, sTime; - resolveTrackTime(bone.translation, sequenceIndex, time, globalSeqDurations, tSeq, tTime); - resolveTrackTime(bone.rotation, sequenceIndex, time, globalSeqDurations, rSeq, rTime); - resolveTrackTime(bone.scale, sequenceIndex, time, globalSeqDurations, sSeq, sTime); + resolveTrackTime(bone.translation, sequenceIndex, animTime, globalSeqTime, globalSeqDurations, tSeq, tTime); + resolveTrackTime(bone.rotation, sequenceIndex, animTime, globalSeqTime, globalSeqDurations, rSeq, rTime); + resolveTrackTime(bone.scale, sequenceIndex, animTime, globalSeqTime, globalSeqDurations, sSeq, sTime); glm::vec3 translation = interpolateVec3(bone.translation, tSeq, tTime, glm::vec3(0.0f)); glm::quat rotation = interpolateQuat(bone.rotation, rSeq, rTime); From c95147390b46f8c361ef2da47de978380fe41f1d Mon Sep 17 00:00:00 2001 From: Kelsi Davis Date: Sat, 4 Apr 2026 01:16:28 -0700 Subject: [PATCH 3/7] fix(rendering,game): init bone SSBO to identity; stop movement before cast Bone SSBO buffers were allocated for MAX_BONES (240) entries but only the first numBones were written. Uninitialized GPU memory in the remaining slots caused vertex spikes when any bone index exceeded the model's actual bone count. Also send MSG_MOVE_STOP before spell casts so the server doesn't reject cast-time spells (e.g. hearthstone) with "can't do that while moving". --- src/game/spell_handler.cpp | 8 ++++++++ src/rendering/character_renderer.cpp | 21 +++++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/src/game/spell_handler.cpp b/src/game/spell_handler.cpp index 59e0d29f..47346c39 100644 --- a/src/game/spell_handler.cpp +++ b/src/game/spell_handler.cpp @@ -222,6 +222,14 @@ void SpellHandler::castSpell(uint32_t spellId, uint64_t targetGuid) { return; } + // Stop movement before casting — servers reject cast-time spells while moving + const uint32_t moveFlags = owner_.movementInfo.flags; + const bool isMoving = (moveFlags & 0x0Fu) != 0; // FORWARD|BACKWARD|STRAFE_LEFT|STRAFE_RIGHT + if (isMoving) { + owner_.movementInfo.flags &= ~0x0Fu; + owner_.sendMovement(Opcode::MSG_MOVE_STOP); + } + uint64_t target = targetGuid != 0 ? targetGuid : owner_.targetGuid; // Self-targeted spells like hearthstone should not send a target if (spellId == 8690) target = 0; diff --git a/src/rendering/character_renderer.cpp b/src/rendering/character_renderer.cpp index 79412156..2b347ac7 100644 --- a/src/rendering/character_renderer.cpp +++ b/src/rendering/character_renderer.cpp @@ -2024,6 +2024,13 @@ void CharacterRenderer::prepareRender(uint32_t frameIndex) { &instance.boneBuffer[frameIndex], &instance.boneAlloc[frameIndex], &allocInfo); instance.boneMapped[frameIndex] = allocInfo.pMappedData; + // Initialize all bone slots to identity so out-of-range indices + // produce correct (neutral) transforms instead of GPU garbage + if (instance.boneMapped[frameIndex]) { + auto* dst = static_cast(instance.boneMapped[frameIndex]); + for (int j = 0; j < MAX_BONES; j++) dst[j] = glm::mat4(1.0f); + } + VkDescriptorSetAllocateInfo ai{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO}; ai.descriptorPool = boneDescPool_; ai.descriptorSetCount = 1; @@ -2147,6 +2154,13 @@ void CharacterRenderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, &instance.boneBuffer[frameIndex], &instance.boneAlloc[frameIndex], &allocInfo); instance.boneMapped[frameIndex] = allocInfo.pMappedData; + // Initialize all bone slots to identity so out-of-range indices + // produce correct (neutral) transforms instead of GPU garbage + if (instance.boneMapped[frameIndex]) { + auto* dst = static_cast(instance.boneMapped[frameIndex]); + for (int j = 0; j < MAX_BONES; j++) dst[j] = glm::mat4(1.0f); + } + // Allocate descriptor set for bone SSBO VkDescriptorSetAllocateInfo ai{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO}; ai.descriptorPool = boneDescPool_; @@ -2787,6 +2801,13 @@ void CharacterRenderer::renderShadow(VkCommandBuffer cmd, const glm::mat4& light &inst.boneBuffer[frameIndex], &inst.boneAlloc[frameIndex], &ai); inst.boneMapped[frameIndex] = ai.pMappedData; + // Initialize all bone slots to identity so out-of-range indices + // produce correct (neutral) transforms instead of GPU garbage + if (inst.boneMapped[frameIndex]) { + auto* dst = static_cast(inst.boneMapped[frameIndex]); + for (int j = 0; j < MAX_BONES; j++) dst[j] = glm::mat4(1.0f); + } + VkDescriptorSetAllocateInfo dsAI{VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO}; dsAI.descriptorPool = boneDescPool_; dsAI.descriptorSetCount = 1; From 3f408341e13ee8aa90ef0bb6d4061d8f4c27682e Mon Sep 17 00:00:00 2001 From: Kelsi Davis Date: Sat, 4 Apr 2026 01:16:28 -0700 Subject: [PATCH 4/7] fix(rendering): correct alpha test on opaque batches and hair transparency MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - alphaTestPipeline_ uses blendDisabled() so surviving pixels are fully opaque (was blendAlpha, causing hair to blend with background) - Remove alphaCutout from alphaTest condition — opaque materials like capes no longer alpha-test just because their texture has an alpha channel - Two-pass batch rendering: opaque (blendMode 0) draws first to establish depth, then alpha-key/blend draws on top --- assets/shaders/character.frag.spv | Bin 16048 -> 17632 bytes src/rendering/character_renderer.cpp | 91 +++++++++++++++++++++++++-- 2 files changed, 87 insertions(+), 4 deletions(-) diff --git a/assets/shaders/character.frag.spv b/assets/shaders/character.frag.spv index 99c68edf24ddec7f733c836d9232807899737158..f970ac7cfaa372c9f83b94572e6aea8b0bcdf821 100644 GIT binary patch literal 17632 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 bitanAccum(vertCount, glm::vec3(0.0f)); // Copy base vertex data + size_t numBones = model.bones.size(); + int outOfRangeCount = 0, ge128Count = 0, nonzeroWeightOOR = 0; for (size_t i = 0; i < vertCount; i++) { const auto& src = model.vertices[i]; auto& dst = gpuVerts[i]; @@ -1490,6 +1492,22 @@ void CharacterRenderer::setupModelBuffers(M2ModelGPU& gpuModel) { dst.normal = src.normal; dst.texCoords = src.texCoords[0]; // Use first UV set dst.tangent = glm::vec4(1.0f, 0.0f, 0.0f, 1.0f); // default + + // Diagnostic: check bone indices + for (int j = 0; j < 4; j++) { + uint8_t bi = src.boneIndices[j]; + uint8_t bw = src.boneWeights[j]; + if (bi >= numBones) { + outOfRangeCount++; + if (bw > 0) nonzeroWeightOOR++; + } + if (bi >= 128) ge128Count++; + } + } + if (outOfRangeCount > 0 || ge128Count > 0) { + LOG_WARNING("VERTEX DIAG: model bones=", numBones, " verts=", vertCount, + " outOfRange=", outOfRangeCount, " (nonzeroWeight=", nonzeroWeightOOR, ")", + " ge128=", ge128Count); } // Accumulate tangent/bitangent per triangle @@ -1959,6 +1977,19 @@ void CharacterRenderer::calculateBoneMatrices(CharacterInstance& instance) { const auto& gsd = model.globalSequenceDurations; + // One-time diagnostic: check bone ordering (parents must precede children) + static bool checkedBoneOrder = false; + if (!checkedBoneOrder) { + checkedBoneOrder = true; + for (size_t i = 0; i < numBones; i++) { + const auto& bone = model.bones[i]; + if (bone.parentBone >= 0 && static_cast(bone.parentBone) >= i) { + LOG_WARNING("Bone ", i, " references parent ", bone.parentBone, + " which comes AFTER it — will use stale matrix!"); + } + } + } + for (size_t i = 0; i < numBones; i++) { const auto& bone = model.bones[i]; @@ -1973,6 +2004,26 @@ void CharacterRenderer::calculateBoneMatrices(CharacterInstance& instance) { } else { instance.boneMatrices[i] = localTransform; } + + // Diagnostic: detect bones with extreme translation + float tx = std::abs(instance.boneMatrices[i][3][0]); + float ty = std::abs(instance.boneMatrices[i][3][1]); + float tz = std::abs(instance.boneMatrices[i][3][2]); + static int diagFrames = 0; + if (diagFrames < 3 && (tx > 50.0f || ty > 50.0f || tz > 50.0f)) { + LOG_WARNING("BONE DIAG: bone[", i, "] keyBone=", bone.keyBoneId, + " flags=0x", std::hex, bone.flags, std::dec, + " parent=", bone.parentBone, + " pivot=(", bone.pivot.x, ",", bone.pivot.y, ",", bone.pivot.z, ")", + " mat_t=(", instance.boneMatrices[i][3][0], ",", + instance.boneMatrices[i][3][1], ",", instance.boneMatrices[i][3][2], ")", + " local_t=(", localTransform[3][0], ",", localTransform[3][1], ",", + localTransform[3][2], ")", + " animTime=", instance.animationTime, + " gsTime=", instance.globalSequenceTime, + " seqIdx=", instance.currentSequenceIndex); + } + if (i == numBones - 1) diagFrames++; } } @@ -2297,8 +2348,39 @@ void CharacterRenderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, return whiteTexture_.get(); }; - // Draw batches (submeshes) with per-batch textures + // One-time batch diagnostic for first character instance + static bool batchDiagDone = false; + if (!batchDiagDone && !instance.hasOverrideModelMatrix) { + batchDiagDone = true; + for (const auto& b : gpuModel.data.batches) { + uint16_t bm = 0, mf = 0; + if (b.materialIndex < gpuModel.data.materials.size()) { + bm = gpuModel.data.materials[b.materialIndex].blendMode; + mf = gpuModel.data.materials[b.materialIndex].flags; + } + uint16_t bg = static_cast(b.submeshId / 100); + bool active = instance.activeGeosets.empty() || + instance.activeGeosets.count(b.submeshId); + LOG_WARNING("BATCH DIAG: submesh=", b.submeshId, " group=", bg, + " blend=", bm, " matFlags=0x", std::hex, mf, std::dec, + " texIdx=", b.textureIndex, " matIdx=", b.materialIndex, + " active=", active); + } + } + + // Draw batches in two passes: opaque (blendMode 0) first, then + // alpha-key/blend after. This ensures capes and body parts write + // depth before hair overlay, preventing hair→cape z-fight. + auto getBatchBlendMode = [&](const pipeline::M2Batch& b) -> uint16_t { + if (b.materialIndex < gpuModel.data.materials.size()) + return gpuModel.data.materials[b.materialIndex].blendMode; + return 0; + }; + for (int pass = 0; pass < 2; pass++) { for (const auto& batch : gpuModel.data.batches) { + uint16_t bm = getBatchBlendMode(batch); + if (pass == 0 && bm != 0) continue; // pass 0: opaque only + if (pass == 1 && bm == 0) continue; // pass 1: non-opaque only if (applyGeosetFilter) { if (instance.activeGeosets.find(batch.submeshId) == instance.activeGeosets.end()) { continue; @@ -2449,7 +2531,7 @@ void CharacterRenderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, // Create per-batch material UBO CharMaterialUBO matData{}; matData.opacity = instance.opacity; - matData.alphaTest = (blendNeedsCutout || alphaCutout) ? 1 : 0; + matData.alphaTest = blendNeedsCutout ? 1 : 0; matData.colorKeyBlack = (blendNeedsCutout || colorKeyBlack) ? 1 : 0; matData.unlit = unlit ? 1 : 0; matData.emissiveBoost = emissiveBoost; @@ -2509,6 +2591,7 @@ void CharacterRenderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, vkCmdDrawIndexed(cmd, batch.indexCount, 1, batch.indexStart, 0, 0); } + } // end pass loop } else { // Draw entire model with first texture VkTexture* texPtr = !gpuModel.textureIds.empty() ? gpuModel.textureIds[0] : whiteTexture_.get(); @@ -3425,7 +3508,7 @@ void CharacterRenderer::recreatePipelines() { " pipelineLayout=", (void*)pipelineLayout_); opaquePipeline_ = buildCharPipeline(PipelineBuilder::blendDisabled(), true); - alphaTestPipeline_ = buildCharPipeline(PipelineBuilder::blendAlpha(), true); + alphaTestPipeline_ = buildCharPipeline(PipelineBuilder::blendDisabled(), true); alphaPipeline_ = buildCharPipeline(PipelineBuilder::blendAlpha(), false); additivePipeline_ = buildCharPipeline(PipelineBuilder::blendAdditive(), false); From f577411a15b99d8cc41a23c553665e15c23b97dc Mon Sep 17 00:00:00 2001 From: Kelsi Davis Date: Sat, 4 Apr 2026 01:16:28 -0700 Subject: [PATCH 5/7] fix(chat): resolve /r reply target when name arrives after whisper Whisper sender name may not be in the player name cache when the packet arrives. Store the sender GUID and lazily resolve the name from the cache in getLastWhisperSender(). Also backfill lastWhisperSender_ when the SMSG_NAME_QUERY_RESPONSE arrives. --- assets/shaders/character.frag.spv | Bin 17632 -> 17488 bytes include/game/game_handler.hpp | 12 +++++++++++- src/game/chat_handler.cpp | 23 +++++++++++++++-------- src/game/entity_controller.cpp | 5 +++++ 4 files changed, 31 insertions(+), 9 deletions(-) diff --git a/assets/shaders/character.frag.spv b/assets/shaders/character.frag.spv index f970ac7cfaa372c9f83b94572e6aea8b0bcdf821..43789272f26cf2430fef49603a82af4bbb54dbe4 100644 GIT binary patch literal 17488 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 + } + return ""; + } void setLastWhisperSender(const std::string& name) { lastWhisperSender_ = name; } // Party/Raid management @@ -2436,6 +2445,7 @@ private: std::string afkMessage_; std::string dndMessage_; std::string lastWhisperSender_; + uint64_t lastWhisperSenderGuid_ = 0; // ---- Online item tracking ---- struct OnlineItemInfo { diff --git a/src/game/chat_handler.cpp b/src/game/chat_handler.cpp index ec9d6e46..0c3dcd83 100644 --- a/src/game/chat_handler.cpp +++ b/src/game/chat_handler.cpp @@ -255,15 +255,22 @@ void ChatHandler::handleMessageChat(network::Packet& packet) { } // Track whisper sender for /r command - if (data.type == ChatType::WHISPER && !data.senderName.empty()) { - owner_.lastWhisperSender_ = data.senderName; + if (data.type == ChatType::WHISPER) { + // Always store GUID so getLastWhisperSender() can resolve the name + // from the player name cache even if name wasn't available yet + if (data.senderGuid != 0) + owner_.lastWhisperSenderGuid_ = data.senderGuid; + if (!data.senderName.empty()) + owner_.lastWhisperSender_ = data.senderName; - if (owner_.afkStatus_ && !data.senderName.empty()) { - std::string reply = owner_.afkMessage_.empty() ? "Away from Keyboard" : owner_.afkMessage_; - sendChatMessage(ChatType::WHISPER, " " + reply, data.senderName); - } else if (owner_.dndStatus_ && !data.senderName.empty()) { - std::string reply = owner_.dndMessage_.empty() ? "Do Not Disturb" : owner_.dndMessage_; - sendChatMessage(ChatType::WHISPER, " " + reply, data.senderName); + if (!data.senderName.empty()) { + if (owner_.afkStatus_) { + std::string reply = owner_.afkMessage_.empty() ? "Away from Keyboard" : owner_.afkMessage_; + sendChatMessage(ChatType::WHISPER, " " + reply, data.senderName); + } else if (owner_.dndStatus_) { + std::string reply = owner_.dndMessage_.empty() ? "Do Not Disturb" : owner_.dndMessage_; + sendChatMessage(ChatType::WHISPER, " " + reply, data.senderName); + } } } diff --git a/src/game/entity_controller.cpp b/src/game/entity_controller.cpp index 6a9cc826..f788fca3 100644 --- a/src/game/entity_controller.cpp +++ b/src/game/entity_controller.cpp @@ -1988,6 +1988,11 @@ void EntityController::handleNameQueryResponse(network::Packet& packet) { } } + // Backfill whisper reply target if the name arrived after the whisper. + if (owner_.lastWhisperSenderGuid_ == data.guid && owner_.lastWhisperSender_.empty()) { + owner_.lastWhisperSender_ = data.name; + } + // Backfill mail inbox sender names for (auto& mail : owner_.mailInbox_) { if (mail.messageType == 0 && mail.senderGuid == data.guid) { From 2fd9473f3b9cf69009464f88386e0f961d089170 Mon Sep 17 00:00:00 2001 From: Kelsi Davis Date: Sat, 4 Apr 2026 01:16:28 -0700 Subject: [PATCH 6/7] fix(rendering): alpha-to-coverage for hair, skip eye glow geosets, add missing include MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Enable alpha-to-coverage on alphaTestPipeline for smooth hair edges when MSAA is active (both init and recreatePipelines paths) - Shader uses fwidth()-based alpha rescaling for clean coverage - Skip group 17/18 geosets (DK/NE eye glow) when no geoset filter is set — prevents blue eye glow on all NPCs - Add missing include for dirname() on Linux --- assets/shaders/character.frag.glsl | 9 ++++++- assets/shaders/character.frag.spv | Bin 17488 -> 17796 bytes src/main.cpp | 1 + src/rendering/character_renderer.cpp | 34 ++++++++++++++++++++------- 4 files changed, 35 insertions(+), 9 deletions(-) diff --git a/assets/shaders/character.frag.glsl b/assets/shaders/character.frag.glsl index b28fa314..669eb84f 100644 --- a/assets/shaders/character.frag.glsl +++ b/assets/shaders/character.frag.glsl @@ -126,7 +126,14 @@ void main() { vec4 texColor = texture(uTexture, finalUV); - if (alphaTest != 0 && texColor.a < 0.5) discard; + if (alphaTest != 0) { + // Screen-space sharpened alpha for alpha-to-coverage anti-aliasing. + // Rescales alpha so the 0.5 cutoff maps to exactly the texel boundary, + // giving smooth edges when MSAA + alpha-to-coverage is active. + float aGrad = fwidth(texColor.a); + texColor.a = clamp((texColor.a - 0.5) / max(aGrad, 0.001) * 0.5 + 0.5, 0.0, 1.0); + if (texColor.a < 1.0 / 255.0) discard; + } if (colorKeyBlack != 0) { float lum = dot(texColor.rgb, vec3(0.299, 0.587, 0.114)); float ck = smoothstep(0.12, 0.30, lum); diff --git a/assets/shaders/character.frag.spv b/assets/shaders/character.frag.spv index 43789272f26cf2430fef49603a82af4bbb54dbe4..7277d275ce297eb3c61a83248ddb3d65fc8af675 100644 GIT binary patch literal 17796 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 Hz5jm!!;cd^ literal 17488 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 #include #include +#include #include // Keep a persistent X11 connection for emergency mouse release in signal handlers. diff --git a/src/rendering/character_renderer.cpp b/src/rendering/character_renderer.cpp index 5cbfecd3..1d15508b 100644 --- a/src/rendering/character_renderer.cpp +++ b/src/rendering/character_renderer.cpp @@ -253,8 +253,9 @@ bool CharacterRenderer::initialize(VkContext* ctx, VkDescriptorSetLayout perFram }; // --- Build pipelines --- - auto buildCharPipeline = [&](VkPipelineColorBlendAttachmentState blendState, bool depthWrite) -> VkPipeline { - return PipelineBuilder() + auto buildCharPipeline = [&](VkPipelineColorBlendAttachmentState blendState, + bool depthWrite, bool alphaToCoverage = false) -> VkPipeline { + auto builder = PipelineBuilder() .setShaders(charVert.stageInfo(VK_SHADER_STAGE_VERTEX_BIT), charFrag.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT)) .setVertexInput({charBinding}, charAttrs) @@ -262,7 +263,10 @@ bool CharacterRenderer::initialize(VkContext* ctx, VkDescriptorSetLayout perFram .setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE) .setDepthTest(true, depthWrite, VK_COMPARE_OP_LESS_OR_EQUAL) .setColorBlendAttachment(blendState) - .setMultisample(samples) + .setMultisample(samples); + if (alphaToCoverage) + builder.setAlphaToCoverage(true); + return builder .setLayout(pipelineLayout_) .setRenderPass(mainPass) .setDynamicStates({VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR}) @@ -270,7 +274,7 @@ bool CharacterRenderer::initialize(VkContext* ctx, VkDescriptorSetLayout perFram }; opaquePipeline_ = buildCharPipeline(PipelineBuilder::blendDisabled(), true); - alphaTestPipeline_ = buildCharPipeline(PipelineBuilder::blendDisabled(), true); + alphaTestPipeline_ = buildCharPipeline(PipelineBuilder::blendDisabled(), true, true); alphaPipeline_ = buildCharPipeline(PipelineBuilder::blendAlpha(), false); additivePipeline_ = buildCharPipeline(PipelineBuilder::blendAdditive(), false); @@ -2385,6 +2389,12 @@ void CharacterRenderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, if (instance.activeGeosets.find(batch.submeshId) == instance.activeGeosets.end()) { continue; } + } else { + // Even without a geoset filter, skip eye glow (group 17) + // and group 18 unless explicitly opted in. These geosets are + // only for DK/NE eye glow and should be off by default. + uint16_t grp = batch.submeshId / 100; + if (grp == 17 || grp == 18) continue; } // Resolve texture for this batch (prefer hair textures for hair geosets). @@ -2950,6 +2960,10 @@ void CharacterRenderer::renderShadow(VkCommandBuffer cmd, const glm::mat4& light if (blendMode >= 2) continue; // skip transparent if (applyGeosetFilter && inst.activeGeosets.find(batch.submeshId) == inst.activeGeosets.end()) continue; + if (!applyGeosetFilter) { + uint16_t grp = batch.submeshId / 100; + if (grp == 17 || grp == 18) continue; + } vkCmdDrawIndexed(cmd, batch.indexCount, 1, batch.indexStart, 0, 0); } } @@ -3487,8 +3501,9 @@ void CharacterRenderer::recreatePipelines() { {5, 0, VK_FORMAT_R32G32B32A32_SFLOAT, static_cast(offsetof(CharVertexGPU, tangent))}, }; - auto buildCharPipeline = [&](VkPipelineColorBlendAttachmentState blendState, bool depthWrite) -> VkPipeline { - return PipelineBuilder() + auto buildCharPipeline = [&](VkPipelineColorBlendAttachmentState blendState, + bool depthWrite, bool alphaToCoverage = false) -> VkPipeline { + auto builder = PipelineBuilder() .setShaders(charVert.stageInfo(VK_SHADER_STAGE_VERTEX_BIT), charFrag.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT)) .setVertexInput({charBinding}, charAttrs) @@ -3496,7 +3511,10 @@ void CharacterRenderer::recreatePipelines() { .setRasterization(VK_POLYGON_MODE_FILL, VK_CULL_MODE_NONE) .setDepthTest(true, depthWrite, VK_COMPARE_OP_LESS_OR_EQUAL) .setColorBlendAttachment(blendState) - .setMultisample(samples) + .setMultisample(samples); + if (alphaToCoverage) + builder.setAlphaToCoverage(true); + return builder .setLayout(pipelineLayout_) .setRenderPass(mainPass) .setDynamicStates({VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR}) @@ -3508,7 +3526,7 @@ void CharacterRenderer::recreatePipelines() { " pipelineLayout=", (void*)pipelineLayout_); opaquePipeline_ = buildCharPipeline(PipelineBuilder::blendDisabled(), true); - alphaTestPipeline_ = buildCharPipeline(PipelineBuilder::blendDisabled(), true); + alphaTestPipeline_ = buildCharPipeline(PipelineBuilder::blendDisabled(), true, true); alphaPipeline_ = buildCharPipeline(PipelineBuilder::blendAlpha(), false); additivePipeline_ = buildCharPipeline(PipelineBuilder::blendAdditive(), false); From 2343b768ce6b0152141382e23f94cfd289902289 Mon Sep 17 00:00:00 2001 From: Kelsi Davis Date: Sat, 4 Apr 2026 01:16:28 -0700 Subject: [PATCH 7/7] fix: warden mmap on macOS, add external listfile support to asset extractor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drop PROT_EXEC from warden module mmap when using Unicorn emulation (not needed — module image is copied into emulator address space). Use MAP_JIT on macOS for the native fallback path. Add --listfile option to asset_extract and SFileAddListFileEntries support for resolving unnamed MPQ hash table entries from external listfiles. --- src/game/warden_module.cpp | 18 ++++++- tools/asset_extract/extractor.cpp | 87 +++++++++++++++++++++++++------ tools/asset_extract/extractor.hpp | 1 + tools/asset_extract/main.cpp | 21 ++++++++ 4 files changed, 110 insertions(+), 17 deletions(-) diff --git a/src/game/warden_module.cpp b/src/game/warden_module.cpp index 36be3f58..f3ea6723 100644 --- a/src/game/warden_module.cpp +++ b/src/game/warden_module.cpp @@ -535,11 +535,25 @@ bool WardenModule::parseExecutableFormat(const std::vector& exeData) { return false; } #else + // When using Unicorn emulation the module image is copied into the + // emulator's address space, so we only need read/write access here. + // Native execution paths (non-Unicorn) need PROT_EXEC; on macOS this + // requires MAP_JIT due to hardened-runtime restrictions. + #ifdef HAVE_UNICORN + int mmapProt = PROT_READ | PROT_WRITE; + int mmapFlags = MAP_PRIVATE | MAP_ANONYMOUS; + #elif defined(__APPLE__) + int mmapProt = PROT_READ | PROT_WRITE | PROT_EXEC; + int mmapFlags = MAP_PRIVATE | MAP_ANONYMOUS | MAP_JIT; + #else + int mmapProt = PROT_READ | PROT_WRITE | PROT_EXEC; + int mmapFlags = MAP_PRIVATE | MAP_ANONYMOUS; + #endif moduleMemory_ = mmap( nullptr, finalCodeSize, - PROT_READ | PROT_WRITE | PROT_EXEC, - MAP_PRIVATE | MAP_ANONYMOUS, + mmapProt, + mmapFlags, -1, 0 ); diff --git a/tools/asset_extract/extractor.cpp b/tools/asset_extract/extractor.cpp index 1df2d510..3c61bef3 100644 --- a/tools/asset_extract/extractor.cpp +++ b/tools/asset_extract/extractor.cpp @@ -537,10 +537,56 @@ static std::vector discoverArchives(const std::string& mpqDir, return result; } +// Read a text file into a vector of lines (for external listfile loading) +static std::vector readLines(const std::string& path) { + std::vector lines; + std::ifstream f(path); + if (!f) return lines; + std::string line; + while (std::getline(f, line)) { + // Trim trailing \r + if (!line.empty() && line.back() == '\r') line.pop_back(); + if (!line.empty()) lines.push_back(std::move(line)); + } + return lines; +} + +// Extract the (listfile) from an MPQ archive into a set of filenames +static void extractInternalListfile(HANDLE hMpq, std::set& out) { + HANDLE hFile = nullptr; + if (!SFileOpenFileEx(hMpq, "(listfile)", 0, &hFile)) return; + + DWORD size = SFileGetFileSize(hFile, nullptr); + if (size == SFILE_INVALID_SIZE || size == 0) { + SFileCloseFile(hFile); + return; + } + + std::vector buf(size); + DWORD bytesRead = 0; + if (!SFileReadFile(hFile, buf.data(), size, &bytesRead, nullptr)) { + SFileCloseFile(hFile); + return; + } + SFileCloseFile(hFile); + + // Parse newline/CR-delimited entries + std::string entry; + for (DWORD i = 0; i < bytesRead; ++i) { + if (buf[i] == '\n' || buf[i] == '\r') { + if (!entry.empty()) { + out.insert(std::move(entry)); + entry.clear(); + } + } else { + entry += buf[i]; + } + } + if (!entry.empty()) out.insert(std::move(entry)); +} + bool Extractor::enumerateFiles(const Options& opts, std::vector& outFiles) { - // Open all archives, enumerate files from highest priority to lowest. - // Use a set to deduplicate (highest-priority version wins). auto archives = discoverArchives(opts.mpqDir, opts.expansion, opts.locale); if (archives.empty()) { std::cerr << "No MPQ archives found in: " << opts.mpqDir << "\n"; @@ -549,12 +595,20 @@ bool Extractor::enumerateFiles(const Options& opts, std::cout << "Found " << archives.size() << " MPQ archives\n"; + // Load external listfile into memory once (avoids repeated file I/O) + std::vector externalEntries; + std::vector externalPtrs; + if (!opts.listFile.empty()) { + externalEntries = readLines(opts.listFile); + externalPtrs.reserve(externalEntries.size()); + for (const auto& e : externalEntries) externalPtrs.push_back(e.c_str()); + std::cout << " Loaded external listfile: " << externalEntries.size() << " entries\n"; + } + const auto wantedDbcs = buildWantedDbcSet(opts); + std::set seenNormalized; // Enumerate from highest priority first so first-seen files win - std::set seenNormalized; - std::vector> fileList; // (original name, archive path) - for (auto it = archives.rbegin(); it != archives.rend(); ++it) { HANDLE hMpq = nullptr; if (!SFileOpenArchive(it->path.c_str(), 0, 0, &hMpq)) { @@ -562,6 +616,14 @@ bool Extractor::enumerateFiles(const Options& opts, continue; } + // Inject external listfile entries into archive's in-memory name table. + // SFileAddListFileEntries is fast — it only hashes the names against the + // archive's hash table, no file I/O involved. + if (!externalPtrs.empty()) { + SFileAddListFileEntries(hMpq, externalPtrs.data(), + static_cast(externalPtrs.size())); + } + if (opts.verbose) { std::cout << " Scanning: " << it->path << " (priority " << it->priority << ")\n"; } @@ -571,28 +633,20 @@ bool Extractor::enumerateFiles(const Options& opts, if (hFind) { do { std::string fileName = findData.cFileName; - // Skip internal listfile/attributes if (fileName == "(listfile)" || fileName == "(attributes)" || fileName == "(signature)" || fileName == "(patch_metadata)") { continue; } - if (shouldSkipFile(opts, fileName)) { - continue; - } + if (shouldSkipFile(opts, fileName)) continue; - // Verify file actually exists in this archive's hash table - // (listfiles can reference files from other archives) - if (!SFileHasFile(hMpq, fileName.c_str())) { - continue; - } + if (!SFileHasFile(hMpq, fileName.c_str())) continue; std::string norm = normalizeWowPath(fileName); if (opts.onlyUsedDbcs && !wantedDbcs.empty() && !wantedDbcs.contains(norm)) { continue; } if (seenNormalized.insert(norm).second) { - // First time seeing this file — this is the highest-priority version outFiles.push_back(fileName); } } while (SFileFindNextFile(hFind, &findData)); @@ -674,6 +728,9 @@ bool Extractor::run(const Options& opts) { for (const auto& ad : archives) { HANDLE h = nullptr; if (SFileOpenArchive(ad.path.c_str(), 0, 0, &h)) { + if (!opts.listFile.empty()) { + SFileAddListFile(h, opts.listFile.c_str()); + } sharedHandles.push_back({h, ad.priority, ad.path}); } else { std::cerr << " Failed to open archive: " << ad.path << "\n"; diff --git a/tools/asset_extract/extractor.hpp b/tools/asset_extract/extractor.hpp index e9aa646d..48588273 100644 --- a/tools/asset_extract/extractor.hpp +++ b/tools/asset_extract/extractor.hpp @@ -26,6 +26,7 @@ public: bool onlyUsedDbcs = false; // Extract only the DBC files wowee uses (implies DBFilesClient/*.dbc filter) std::string dbcCsvOutputDir; // When set, write CSVs into this directory instead of outputDir/expansions//db std::string referenceManifest; // If set, only extract files NOT in this manifest (delta extraction) + std::string listFile; // External listfile for MPQ enumeration (resolves unnamed hash entries) }; struct Stats { diff --git a/tools/asset_extract/main.cpp b/tools/asset_extract/main.cpp index 6d9c27f5..0add3e99 100644 --- a/tools/asset_extract/main.cpp +++ b/tools/asset_extract/main.cpp @@ -20,6 +20,7 @@ static void printUsage(const char* prog) { << " --skip-dbc Do not extract DBFilesClient/*.dbc (visual assets only)\n" << " --dbc-csv Convert selected DBFilesClient/*.dbc to CSV under\n" << " /expansions//db/*.csv (for committing)\n" + << " --listfile External listfile for MPQ file enumeration (auto-detected)\n" << " --reference-manifest \n" << " Only extract files NOT in this manifest (delta extraction)\n" << " --dbc-csv-out Write CSV DBCs into (overrides default output path)\n" @@ -53,6 +54,8 @@ int main(int argc, char** argv) { opts.generateDbcCsv = true; } else if (std::strcmp(argv[i], "--dbc-csv-out") == 0 && i + 1 < argc) { opts.dbcCsvOutputDir = argv[++i]; + } else if (std::strcmp(argv[i], "--listfile") == 0 && i + 1 < argc) { + opts.listFile = argv[++i]; } else if (std::strcmp(argv[i], "--reference-manifest") == 0 && i + 1 < argc) { opts.referenceManifest = argv[++i]; } else if (std::strcmp(argv[i], "--verify") == 0) { @@ -99,6 +102,24 @@ int main(int argc, char** argv) { } opts.locale = locale; + // Auto-detect external listfile if not specified + if (opts.listFile.empty()) { + // Look next to the binary, then in the source tree + namespace fs = std::filesystem; + std::string binDir = fs::path(argv[0]).parent_path().string(); + for (const auto& candidate : { + binDir + "/listfile.txt", + binDir + "/../../../tools/asset_extract/listfile.txt", + opts.mpqDir + "/listfile.txt", + }) { + if (fs::exists(candidate)) { + opts.listFile = candidate; + std::cout << "Auto-detected listfile: " << candidate << "\n"; + break; + } + } + } + std::cout << "=== Wowee Asset Extractor ===\n"; std::cout << "MPQ directory: " << opts.mpqDir << "\n"; std::cout << "Output: " << opts.outputDir << "\n";