diff --git a/include/pipeline/asset_manager.hpp b/include/pipeline/asset_manager.hpp index 2ad9e6c0..869b87a3 100644 --- a/include/pipeline/asset_manager.hpp +++ b/include/pipeline/asset_manager.hpp @@ -59,6 +59,15 @@ public: */ void setExpansionDataPath(const std::string& path); + /** + * Set a base data path to fall back to when the primary manifest + * does not contain a requested file. Call this when the primary + * dataPath is an expansion-specific subset (e.g. Data/expansions/vanilla/) + * that only holds DBC overrides, not the full world asset set. + * @param basePath Path to the base extraction (Data/) that has a manifest.json + */ + void setBaseFallbackPath(const std::string& basePath); + /** * Load a DBC file * @param name DBC file name (e.g., "Map.dbc") @@ -144,6 +153,11 @@ private: AssetManifest manifest_; LooseFileReader looseReader_; + // Optional base-path fallback: used when manifest_ doesn't contain a file. + // Populated by setBaseFallbackPath(); ignored if baseFallbackDataPath_ is empty. + std::string baseFallbackDataPath_; + AssetManifest baseFallbackManifest_; + /** * Resolve filesystem path: check override dir first, then base manifest. * Returns empty string if not found. diff --git a/src/core/application.cpp b/src/core/application.cpp index 90c02172..45ac82b9 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -292,6 +292,11 @@ bool Application::initialize() { if (std::filesystem::exists(expansionManifest)) { assetPath = profile->dataPath; LOG_INFO("Using expansion-specific asset path: ", assetPath); + // Register base Data/ as fallback so world terrain files are found + // even when the expansion path only contains DBC overrides. + if (assetPath != dataPath) { + assetManager->setBaseFallbackPath(dataPath); + } } } } diff --git a/src/game/warden_emulator.cpp b/src/game/warden_emulator.cpp index 570f063b..5fadc408 100644 --- a/src/game/warden_emulator.cpp +++ b/src/game/warden_emulator.cpp @@ -121,6 +121,15 @@ bool WardenEmulator::initialize(const void* moduleCode, size_t moduleSize, uint3 return false; } + // Map a null guard page at address 0 (read-only, zeroed) so that NULL-pointer + // dereferences in the module don't crash the emulator with UC_ERR_MAP. + // This allows execution to continue past NULL reads, making diagnostics easier. + err = uc_mem_map(uc_, 0x0, 0x1000, UC_PROT_READ); + if (err != UC_ERR_OK) { + // Non-fatal — just log it; the emulator will still function + std::cerr << "[WardenEmulator] Note: could not map null guard page: " << uc_strerror(err) << '\n'; + } + // Add hooks for debugging and invalid memory access uc_hook hh; uc_hook_add(uc_, &hh, UC_HOOK_MEM_INVALID, (void*)hookMemInvalid, this, 1, 0); diff --git a/src/pipeline/asset_manager.cpp b/src/pipeline/asset_manager.cpp index 74b46219..89b063c5 100644 --- a/src/pipeline/asset_manager.cpp +++ b/src/pipeline/asset_manager.cpp @@ -137,8 +137,31 @@ std::string AssetManager::resolveFile(const std::string& normalizedPath) const { } } } - // Fall back to base manifest - return manifest_.resolveFilesystemPath(normalizedPath); + // Primary manifest + std::string primaryPath = manifest_.resolveFilesystemPath(normalizedPath); + if (!primaryPath.empty()) return primaryPath; + + // If a base-path fallback is configured (expansion-specific primary that only + // holds DBC overrides), retry against the base extraction. + if (!baseFallbackDataPath_.empty()) { + return baseFallbackManifest_.resolveFilesystemPath(normalizedPath); + } + return {}; +} + +void AssetManager::setBaseFallbackPath(const std::string& basePath) { + if (basePath.empty() || basePath == dataPath) return; // nothing to do + std::string manifestPath = basePath + "/manifest.json"; + if (!std::filesystem::exists(manifestPath)) { + LOG_DEBUG("AssetManager: base fallback manifest not found at ", manifestPath, + " — fallback disabled"); + return; + } + if (baseFallbackManifest_.load(manifestPath)) { + baseFallbackDataPath_ = basePath; + LOG_INFO("AssetManager: base fallback path set to '", basePath, + "' (", baseFallbackManifest_.getEntryCount(), " files)"); + } } BLPImage AssetManager::loadTexture(const std::string& path) { diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index 0eef9b83..f5a1bc1d 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -400,7 +400,7 @@ void GameScreen::render(game::GameHandler& gameHandler) { renderCastBar(gameHandler); renderMirrorTimers(gameHandler); renderQuestObjectiveTracker(gameHandler); - if (showNameplates_) renderNameplates(gameHandler); + renderNameplates(gameHandler); // player names always shown; NPC plates gated by showNameplates_ renderBattlegroundScore(gameHandler); renderCombatText(gameHandler); renderPartyFrames(gameHandler); @@ -4848,16 +4848,20 @@ void GameScreen::renderNameplates(game::GameHandler& gameHandler) { auto* unit = dynamic_cast(entityPtr.get()); if (!unit || unit->getMaxHealth() == 0) continue; + bool isPlayer = (entityPtr->getType() == game::ObjectType::PLAYER); bool isTarget = (guid == targetGuid); + // Player nameplates are always shown; NPC nameplates respect the V-key toggle + if (!isPlayer && !showNameplates_) continue; + // Convert canonical WoW position → render space, raise to head height glm::vec3 renderPos = core::coords::canonicalToRender( glm::vec3(unit->getX(), unit->getY(), unit->getZ())); renderPos.z += 2.3f; - // Cull distance: target up to 40 units; others up to 20 units + // Cull distance: target or other players up to 40 units; NPC others up to 20 units float dist = glm::length(renderPos - camPos); - float cullDist = isTarget ? 40.0f : 20.0f; + float cullDist = (isTarget || isPlayer) ? 40.0f : 20.0f; if (dist > cullDist) continue; // Project to clip space @@ -4874,8 +4878,8 @@ void GameScreen::renderNameplates(game::GameHandler& gameHandler) { float sx = (ndc.x * 0.5f + 0.5f) * screenW; float sy = (ndc.y * 0.5f + 0.5f) * screenH; - // Fade out in the last 5 units of range - float alpha = dist < 35.0f ? 1.0f : 1.0f - (dist - 35.0f) / 5.0f; + // Fade out in the last 5 units of cull range + float alpha = dist < (cullDist - 5.0f) ? 1.0f : 1.0f - (dist - (cullDist - 5.0f)) / 5.0f; auto A = [&](int v) { return static_cast(v * alpha); }; // Bar colour by hostility @@ -4920,10 +4924,12 @@ void GameScreen::renderNameplates(game::GameHandler& gameHandler) { ImVec2 textSize = ImGui::CalcTextSize(labelBuf); float nameX = sx - textSize.x * 0.5f; float nameY = sy - barH - 12.0f; - // Name color: hostile=red, non-hostile=yellow (WoW convention) - ImU32 nameColor = unit->isHostile() - ? IM_COL32(220, 80, 80, A(230)) - : IM_COL32(240, 200, 100, A(230)); + // Name color: other player=cyan, hostile=red, non-hostile=yellow (WoW convention) + ImU32 nameColor = isPlayer + ? IM_COL32( 80, 200, 255, A(230)) // cyan — other players + : unit->isHostile() + ? IM_COL32(220, 80, 80, A(230)) // red — hostile NPC + : IM_COL32(240, 200, 100, A(230)); // yellow — friendly NPC drawList->AddText(ImVec2(nameX + 1.0f, nameY + 1.0f), IM_COL32(0, 0, 0, A(160)), labelBuf); drawList->AddText(ImVec2(nameX, nameY), nameColor, labelBuf); @@ -10242,6 +10248,7 @@ void GameScreen::renderDingEffect() { if (dingTimer_ < 0.0f) dingTimer_ = 0.0f; float alpha = dingTimer_ < 0.8f ? (dingTimer_ / 0.8f) : 1.0f; // fade out last 0.8s + float elapsed = DING_DURATION - dingTimer_; // 0 → DING_DURATION ImGuiIO& io = ImGui::GetIO(); float cx = io.DisplaySize.x * 0.5f; @@ -10249,6 +10256,37 @@ void GameScreen::renderDingEffect() { ImDrawList* draw = ImGui::GetForegroundDrawList(); + // ---- Golden radial ring burst (3 waves staggered by 0.45s) ---- + { + constexpr float kMaxRadius = 420.0f; + constexpr float kRingWidth = 18.0f; + constexpr float kWaveLen = 1.4f; // each wave lasts 1.4s + constexpr int kNumWaves = 3; + constexpr float kStagger = 0.45f; // seconds between waves + + for (int w = 0; w < kNumWaves; ++w) { + float waveElapsed = elapsed - w * kStagger; + if (waveElapsed <= 0.0f || waveElapsed >= kWaveLen) continue; + + float t = waveElapsed / kWaveLen; // 0 → 1 + float radius = t * kMaxRadius; + float ringAlpha = (1.0f - t) * alpha; // fades as it expands + + ImU32 outerCol = IM_COL32(255, 215, 60, (int)(ringAlpha * 200)); + ImU32 innerCol = IM_COL32(255, 255, 150, (int)(ringAlpha * 120)); + + draw->AddCircle(ImVec2(cx, cy), radius, outerCol, 64, kRingWidth); + draw->AddCircle(ImVec2(cx, cy), radius * 0.92f, innerCol, 64, kRingWidth * 0.5f); + } + } + + // ---- Full-screen golden flash on first frame ---- + if (elapsed < 0.15f) { + float flashA = (1.0f - elapsed / 0.15f) * 0.45f; + draw->AddRectFilled(ImVec2(0, 0), io.DisplaySize, + IM_COL32(255, 200, 50, (int)(flashA * 255))); + } + // "LEVEL X!" text — visible for first 2.2s if (dingTimer_ > 0.8f) { ImFont* font = ImGui::GetFont();