mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-04-03 20:03:50 +00:00
chore: remove dead code, document water shader wave parameters
- Delete 4 legacy GLSL 330 shaders (basic.vert/frag, terrain.vert/frag) left over from OpenGL→Vulkan migration — Vulkan equivalents exist as *.glsl files compiled to SPIR-V by the build system - Delete orphaned mpq_manager.hpp/cpp (694 lines) — not in CMakeLists, not included by any file, unreferenced StormLib integration attempt - Add comments to water.frag.glsl wave constants explaining the multi-octave noise design: non-axis-aligned directions prevent tiling, frequency increases and amplitude decreases per octave for natural water appearance
This commit is contained in:
parent
529985a961
commit
1e06ea86d7
7 changed files with 10 additions and 934 deletions
|
|
@ -1,38 +0,0 @@
|
||||||
#version 330 core
|
|
||||||
|
|
||||||
in vec3 FragPos;
|
|
||||||
in vec3 Normal;
|
|
||||||
in vec2 TexCoord;
|
|
||||||
|
|
||||||
out vec4 FragColor;
|
|
||||||
|
|
||||||
uniform vec3 uLightPos;
|
|
||||||
uniform vec3 uViewPos;
|
|
||||||
uniform vec4 uColor;
|
|
||||||
uniform sampler2D uTexture;
|
|
||||||
uniform bool uUseTexture;
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
// Ambient
|
|
||||||
vec3 ambient = 0.3 * vec3(1.0);
|
|
||||||
|
|
||||||
// Diffuse
|
|
||||||
vec3 norm = normalize(Normal);
|
|
||||||
vec3 lightDir = normalize(uLightPos - FragPos);
|
|
||||||
float diff = max(dot(norm, lightDir), 0.0);
|
|
||||||
vec3 diffuse = diff * vec3(1.0);
|
|
||||||
|
|
||||||
// Specular
|
|
||||||
vec3 viewDir = normalize(uViewPos - FragPos);
|
|
||||||
vec3 reflectDir = reflect(-lightDir, norm);
|
|
||||||
float spec = pow(max(dot(viewDir, reflectDir), 0.0), 32.0);
|
|
||||||
vec3 specular = 0.5 * spec * vec3(1.0);
|
|
||||||
|
|
||||||
vec3 result = (ambient + diffuse + specular);
|
|
||||||
|
|
||||||
if (uUseTexture) {
|
|
||||||
FragColor = texture(uTexture, TexCoord) * vec4(result, 1.0);
|
|
||||||
} else {
|
|
||||||
FragColor = uColor * vec4(result, 1.0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,22 +0,0 @@
|
||||||
#version 330 core
|
|
||||||
|
|
||||||
layout (location = 0) in vec3 aPosition;
|
|
||||||
layout (location = 1) in vec3 aNormal;
|
|
||||||
layout (location = 2) in vec2 aTexCoord;
|
|
||||||
|
|
||||||
out vec3 FragPos;
|
|
||||||
out vec3 Normal;
|
|
||||||
out vec2 TexCoord;
|
|
||||||
|
|
||||||
uniform mat4 uModel;
|
|
||||||
uniform mat4 uView;
|
|
||||||
uniform mat4 uProjection;
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
FragPos = vec3(uModel * vec4(aPosition, 1.0));
|
|
||||||
// Use mat3(uModel) directly - avoids expensive inverse() per vertex
|
|
||||||
Normal = mat3(uModel) * aNormal;
|
|
||||||
TexCoord = aTexCoord;
|
|
||||||
|
|
||||||
gl_Position = uProjection * uView * vec4(FragPos, 1.0);
|
|
||||||
}
|
|
||||||
|
|
@ -1,146 +0,0 @@
|
||||||
#version 330 core
|
|
||||||
|
|
||||||
in vec3 FragPos;
|
|
||||||
in vec3 Normal;
|
|
||||||
in vec2 TexCoord;
|
|
||||||
in vec2 LayerUV;
|
|
||||||
|
|
||||||
out vec4 FragColor;
|
|
||||||
|
|
||||||
// Texture layers (up to 4)
|
|
||||||
uniform sampler2D uBaseTexture;
|
|
||||||
uniform sampler2D uLayer1Texture;
|
|
||||||
uniform sampler2D uLayer2Texture;
|
|
||||||
uniform sampler2D uLayer3Texture;
|
|
||||||
|
|
||||||
// Alpha maps for blending
|
|
||||||
uniform sampler2D uLayer1Alpha;
|
|
||||||
uniform sampler2D uLayer2Alpha;
|
|
||||||
uniform sampler2D uLayer3Alpha;
|
|
||||||
|
|
||||||
// Layer control
|
|
||||||
uniform int uLayerCount;
|
|
||||||
uniform bool uHasLayer1;
|
|
||||||
uniform bool uHasLayer2;
|
|
||||||
uniform bool uHasLayer3;
|
|
||||||
|
|
||||||
// Lighting
|
|
||||||
uniform vec3 uLightDir;
|
|
||||||
uniform vec3 uLightColor;
|
|
||||||
uniform vec3 uAmbientColor;
|
|
||||||
|
|
||||||
// Camera
|
|
||||||
uniform vec3 uViewPos;
|
|
||||||
|
|
||||||
// Fog
|
|
||||||
uniform vec3 uFogColor;
|
|
||||||
uniform float uFogStart;
|
|
||||||
uniform float uFogEnd;
|
|
||||||
|
|
||||||
// Shadow mapping
|
|
||||||
uniform sampler2DShadow uShadowMap;
|
|
||||||
uniform mat4 uLightSpaceMatrix;
|
|
||||||
uniform bool uShadowEnabled;
|
|
||||||
uniform float uShadowStrength;
|
|
||||||
|
|
||||||
float calcShadow() {
|
|
||||||
vec4 lsPos = uLightSpaceMatrix * vec4(FragPos, 1.0);
|
|
||||||
vec3 proj = lsPos.xyz / lsPos.w * 0.5 + 0.5;
|
|
||||||
if (proj.z > 1.0) return 1.0;
|
|
||||||
float edgeDist = max(abs(proj.x - 0.5), abs(proj.y - 0.5));
|
|
||||||
float coverageFade = 1.0 - smoothstep(0.40, 0.49, edgeDist);
|
|
||||||
vec3 norm = normalize(Normal);
|
|
||||||
vec3 lightDir = normalize(-uLightDir);
|
|
||||||
float bias = max(0.005 * (1.0 - dot(norm, lightDir)), 0.001);
|
|
||||||
// 5-tap PCF tuned for slightly sharper detail while keeping stability.
|
|
||||||
vec2 texel = vec2(1.0 / 2048.0);
|
|
||||||
float ref = proj.z - bias;
|
|
||||||
vec2 off = texel * 0.7;
|
|
||||||
float shadow = 0.0;
|
|
||||||
shadow += texture(uShadowMap, vec3(proj.xy, ref)) * 0.55;
|
|
||||||
shadow += texture(uShadowMap, vec3(proj.xy + vec2(off.x, 0.0), ref)) * 0.1125;
|
|
||||||
shadow += texture(uShadowMap, vec3(proj.xy - vec2(off.x, 0.0), ref)) * 0.1125;
|
|
||||||
shadow += texture(uShadowMap, vec3(proj.xy + vec2(0.0, off.y), ref)) * 0.1125;
|
|
||||||
shadow += texture(uShadowMap, vec3(proj.xy - vec2(0.0, off.y), ref)) * 0.1125;
|
|
||||||
return mix(1.0, shadow, coverageFade);
|
|
||||||
}
|
|
||||||
|
|
||||||
float sampleAlpha(sampler2D tex, vec2 uv) {
|
|
||||||
// Slight blur near alpha-map borders to hide seams between chunks.
|
|
||||||
vec2 edge = min(uv, 1.0 - uv);
|
|
||||||
float border = min(edge.x, edge.y);
|
|
||||||
float doBlur = step(border, 2.0 / 64.0); // within ~2 texels of edge
|
|
||||||
if (doBlur < 0.5) {
|
|
||||||
return texture(tex, uv).r;
|
|
||||||
}
|
|
||||||
vec2 texel = vec2(1.0 / 64.0);
|
|
||||||
float a = 0.0;
|
|
||||||
a += texture(tex, uv + vec2(-texel.x, 0.0)).r;
|
|
||||||
a += texture(tex, uv + vec2(texel.x, 0.0)).r;
|
|
||||||
a += texture(tex, uv + vec2(0.0, -texel.y)).r;
|
|
||||||
a += texture(tex, uv + vec2(0.0, texel.y)).r;
|
|
||||||
return a * 0.25;
|
|
||||||
}
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
// Sample base texture
|
|
||||||
vec4 baseColor = texture(uBaseTexture, TexCoord);
|
|
||||||
vec4 finalColor = baseColor;
|
|
||||||
|
|
||||||
// Apply texture layers with alpha blending
|
|
||||||
// TexCoord = tiling UVs for texture sampling (repeats across chunk)
|
|
||||||
// LayerUV = 0-1 per-chunk UVs for alpha map sampling
|
|
||||||
float a1 = uHasLayer1 ? sampleAlpha(uLayer1Alpha, LayerUV) : 0.0;
|
|
||||||
float a2 = uHasLayer2 ? sampleAlpha(uLayer2Alpha, LayerUV) : 0.0;
|
|
||||||
float a3 = uHasLayer3 ? sampleAlpha(uLayer3Alpha, LayerUV) : 0.0;
|
|
||||||
|
|
||||||
// Normalize weights to reduce quilting seams at chunk borders.
|
|
||||||
float w0 = 1.0;
|
|
||||||
float w1 = a1;
|
|
||||||
float w2 = a2;
|
|
||||||
float w3 = a3;
|
|
||||||
float sum = w0 + w1 + w2 + w3;
|
|
||||||
if (sum > 0.0) {
|
|
||||||
w0 /= sum; w1 /= sum; w2 /= sum; w3 /= sum;
|
|
||||||
}
|
|
||||||
|
|
||||||
finalColor = baseColor * w0;
|
|
||||||
if (uHasLayer1) {
|
|
||||||
vec4 layer1Color = texture(uLayer1Texture, TexCoord);
|
|
||||||
finalColor += layer1Color * w1;
|
|
||||||
}
|
|
||||||
if (uHasLayer2) {
|
|
||||||
vec4 layer2Color = texture(uLayer2Texture, TexCoord);
|
|
||||||
finalColor += layer2Color * w2;
|
|
||||||
}
|
|
||||||
if (uHasLayer3) {
|
|
||||||
vec4 layer3Color = texture(uLayer3Texture, TexCoord);
|
|
||||||
finalColor += layer3Color * w3;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Normalize normal
|
|
||||||
vec3 norm = normalize(Normal);
|
|
||||||
vec3 lightDir = normalize(-uLightDir);
|
|
||||||
|
|
||||||
// Ambient lighting
|
|
||||||
vec3 ambient = uAmbientColor * finalColor.rgb;
|
|
||||||
|
|
||||||
// Diffuse lighting (two-sided for terrain hills)
|
|
||||||
float diff = abs(dot(norm, lightDir));
|
|
||||||
diff = max(diff, 0.2); // Minimum light to prevent completely dark faces
|
|
||||||
vec3 diffuse = diff * uLightColor * finalColor.rgb;
|
|
||||||
|
|
||||||
// Shadow
|
|
||||||
float shadow = uShadowEnabled ? calcShadow() : 1.0;
|
|
||||||
shadow = mix(1.0, shadow, clamp(uShadowStrength, 0.0, 1.0));
|
|
||||||
|
|
||||||
// Combine lighting (terrain is purely diffuse — no specular on ground)
|
|
||||||
vec3 result = ambient + shadow * diffuse;
|
|
||||||
|
|
||||||
// Apply fog
|
|
||||||
float distance = length(uViewPos - FragPos);
|
|
||||||
float fogFactor = clamp((uFogEnd - distance) / (uFogEnd - uFogStart), 0.0, 1.0);
|
|
||||||
result = mix(uFogColor, result, fogFactor);
|
|
||||||
|
|
||||||
FragColor = vec4(result, 1.0);
|
|
||||||
}
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
#version 330 core
|
|
||||||
|
|
||||||
layout(location = 0) in vec3 aPosition;
|
|
||||||
layout(location = 1) in vec3 aNormal;
|
|
||||||
layout(location = 2) in vec2 aTexCoord;
|
|
||||||
layout(location = 3) in vec2 aLayerUV;
|
|
||||||
|
|
||||||
out vec3 FragPos;
|
|
||||||
out vec3 Normal;
|
|
||||||
out vec2 TexCoord;
|
|
||||||
out vec2 LayerUV;
|
|
||||||
|
|
||||||
uniform mat4 uModel;
|
|
||||||
uniform mat4 uView;
|
|
||||||
uniform mat4 uProjection;
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
vec4 worldPos = uModel * vec4(aPosition, 1.0);
|
|
||||||
FragPos = worldPos.xyz;
|
|
||||||
|
|
||||||
// Terrain uses identity model matrix, so normal passes through directly
|
|
||||||
Normal = aNormal;
|
|
||||||
|
|
||||||
TexCoord = aTexCoord;
|
|
||||||
LayerUV = aLayerUV;
|
|
||||||
|
|
||||||
gl_Position = uProjection * uView * worldPos;
|
|
||||||
}
|
|
||||||
|
|
@ -47,12 +47,16 @@ layout(location = 0) out vec4 outColor;
|
||||||
// Dual-scroll detail normals (multi-octave ripple overlay)
|
// Dual-scroll detail normals (multi-octave ripple overlay)
|
||||||
// ============================================================
|
// ============================================================
|
||||||
vec3 dualScrollWaveNormal(vec2 p, float time) {
|
vec3 dualScrollWaveNormal(vec2 p, float time) {
|
||||||
vec2 d1 = normalize(vec2(0.86, 0.51));
|
// Three wave octaves at different angles, frequencies, and speeds.
|
||||||
vec2 d2 = normalize(vec2(-0.47, 0.88));
|
// Directions are non-axis-aligned to prevent visible tiling patterns.
|
||||||
vec2 d3 = normalize(vec2(0.32, -0.95));
|
// Frequency increases and amplitude decreases per octave (standard
|
||||||
float f1 = 0.19, f2 = 0.43, f3 = 0.72;
|
// multi-octave noise layering for natural water appearance).
|
||||||
float s1 = 0.95, s2 = 1.73, s3 = 2.40;
|
vec2 d1 = normalize(vec2(0.86, 0.51)); // ~30° from +X
|
||||||
float a1 = 0.22, a2 = 0.10, a3 = 0.05;
|
vec2 d2 = normalize(vec2(-0.47, 0.88)); // ~118° (opposing cross-wave)
|
||||||
|
vec2 d3 = normalize(vec2(0.32, -0.95)); // ~-71° (third axis for variety)
|
||||||
|
float f1 = 0.19, f2 = 0.43, f3 = 0.72; // spatial frequency (higher = tighter ripples)
|
||||||
|
float s1 = 0.95, s2 = 1.73, s3 = 2.40; // scroll speed (higher octaves move faster)
|
||||||
|
float a1 = 0.22, a2 = 0.10, a3 = 0.05; // amplitude (decreasing for natural falloff)
|
||||||
|
|
||||||
vec2 p1 = p + d1 * (time * s1 * 4.0);
|
vec2 p1 = p + d1 * (time * s1 * 4.0);
|
||||||
vec2 p2 = p + d2 * (time * s2 * 4.0);
|
vec2 p2 = p + d2 * (time * s2 * 4.0);
|
||||||
|
|
|
||||||
|
|
@ -1,129 +0,0 @@
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include <string>
|
|
||||||
#include <vector>
|
|
||||||
#include <cstdint>
|
|
||||||
#include <memory>
|
|
||||||
#include <map>
|
|
||||||
#include <unordered_map>
|
|
||||||
#include <unordered_set>
|
|
||||||
#include <mutex>
|
|
||||||
#include <shared_mutex>
|
|
||||||
|
|
||||||
// Forward declare StormLib handle
|
|
||||||
typedef void* HANDLE;
|
|
||||||
|
|
||||||
namespace wowee {
|
|
||||||
namespace pipeline {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* MPQManager - Manages MPQ archive loading and file reading
|
|
||||||
*
|
|
||||||
* WoW 3.3.5a stores all game assets in MPQ archives.
|
|
||||||
* This manager loads multiple archives and provides unified file access.
|
|
||||||
*/
|
|
||||||
class MPQManager {
|
|
||||||
public:
|
|
||||||
MPQManager();
|
|
||||||
~MPQManager();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize the MPQ system
|
|
||||||
* @param dataPath Path to WoW Data directory
|
|
||||||
* @return true if initialization succeeded
|
|
||||||
*/
|
|
||||||
bool initialize(const std::string& dataPath);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shutdown and close all archives
|
|
||||||
*/
|
|
||||||
void shutdown();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Load a single MPQ archive
|
|
||||||
* @param path Full path to MPQ file
|
|
||||||
* @param priority Priority for file resolution (higher = checked first)
|
|
||||||
* @return true if archive loaded successfully
|
|
||||||
*/
|
|
||||||
bool loadArchive(const std::string& path, int priority = 0);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if a file exists in any loaded archive
|
|
||||||
* @param filename Virtual file path (e.g., "World\\Maps\\Azeroth\\Azeroth.wdt")
|
|
||||||
* @return true if file exists
|
|
||||||
*/
|
|
||||||
bool fileExists(const std::string& filename) const;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Read a file from MPQ archives
|
|
||||||
* @param filename Virtual file path
|
|
||||||
* @return File contents as byte vector (empty if not found)
|
|
||||||
*/
|
|
||||||
std::vector<uint8_t> readFile(const std::string& filename) const;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get file size without reading it
|
|
||||||
* @param filename Virtual file path
|
|
||||||
* @return File size in bytes (0 if not found)
|
|
||||||
*/
|
|
||||||
uint32_t getFileSize(const std::string& filename) const;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if MPQ system is initialized
|
|
||||||
*/
|
|
||||||
bool isInitialized() const { return initialized; }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get list of loaded archives
|
|
||||||
*/
|
|
||||||
const std::vector<std::string>& getLoadedArchives() const { return archiveNames; }
|
|
||||||
|
|
||||||
private:
|
|
||||||
struct ArchiveEntry {
|
|
||||||
HANDLE handle;
|
|
||||||
std::string path;
|
|
||||||
int priority;
|
|
||||||
};
|
|
||||||
|
|
||||||
bool initialized = false;
|
|
||||||
std::string dataPath;
|
|
||||||
std::vector<ArchiveEntry> archives;
|
|
||||||
std::vector<std::string> archiveNames;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Find archive containing a file
|
|
||||||
* @param filename File to search for
|
|
||||||
* @return Archive handle or nullptr if not found
|
|
||||||
*/
|
|
||||||
HANDLE findFileArchive(const std::string& filename) const;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Load patch archives (e.g., patch.MPQ, patch-2.MPQ, etc.)
|
|
||||||
*/
|
|
||||||
bool loadPatchArchives();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Load locale-specific archives
|
|
||||||
* @param locale Locale string (e.g., "enUS")
|
|
||||||
*/
|
|
||||||
bool loadLocaleArchives(const std::string& locale);
|
|
||||||
|
|
||||||
void logMissingFileOnce(const std::string& filename) const;
|
|
||||||
|
|
||||||
// Cache for mapping "virtual filename" -> archive handle (or INVALID_HANDLE_VALUE for not found).
|
|
||||||
// This avoids scanning every archive for repeated lookups, which can otherwise appear as a hang
|
|
||||||
// on screens that trigger many asset probes (character select, character preview, etc.).
|
|
||||||
//
|
|
||||||
// Important: caching misses can blow up memory if the game probes many unique non-existent filenames.
|
|
||||||
// Miss caching is disabled by default and must be explicitly enabled.
|
|
||||||
mutable std::shared_mutex fileArchiveCacheMutex_;
|
|
||||||
mutable std::unordered_map<std::string, HANDLE> fileArchiveCache_;
|
|
||||||
size_t fileArchiveCacheMaxEntries_ = 500000;
|
|
||||||
bool fileArchiveCacheMisses_ = false;
|
|
||||||
|
|
||||||
mutable std::mutex missingFileMutex_;
|
|
||||||
mutable std::unordered_set<std::string> missingFileWarnings_;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace pipeline
|
|
||||||
} // namespace wowee
|
|
||||||
|
|
@ -1,565 +0,0 @@
|
||||||
#include "pipeline/mpq_manager.hpp"
|
|
||||||
#include "core/logger.hpp"
|
|
||||||
#include <algorithm>
|
|
||||||
#include <chrono>
|
|
||||||
#include <cstdlib>
|
|
||||||
#include <limits>
|
|
||||||
#include <filesystem>
|
|
||||||
#include <fstream>
|
|
||||||
#include <sstream>
|
|
||||||
#include <cctype>
|
|
||||||
|
|
||||||
#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 {
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
std::string toLowerCopy(std::string value) {
|
|
||||||
std::transform(value.begin(), value.end(), value.begin(),
|
|
||||||
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string normalizeVirtualFilenameForLookup(std::string value) {
|
|
||||||
// StormLib uses backslash-separated virtual paths; treat lookups as case-insensitive.
|
|
||||||
std::replace(value.begin(), value.end(), '/', '\\');
|
|
||||||
value = toLowerCopy(std::move(value));
|
|
||||||
while (!value.empty() && (value.front() == '\\' || value.front() == '/')) {
|
|
||||||
value.erase(value.begin());
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool envFlagEnabled(const char* name) {
|
|
||||||
const char* v = std::getenv(name);
|
|
||||||
if (!v || !*v) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
std::string s = toLowerCopy(v);
|
|
||||||
return s == "1" || s == "true" || s == "yes" || s == "on";
|
|
||||||
}
|
|
||||||
|
|
||||||
size_t envSizeTOrDefault(const char* name, size_t defValue) {
|
|
||||||
const char* v = std::getenv(name);
|
|
||||||
if (!v || !*v) return defValue;
|
|
||||||
char* end = nullptr;
|
|
||||||
unsigned long long value = std::strtoull(v, &end, 10);
|
|
||||||
if (end == v || value == 0) return defValue;
|
|
||||||
if (value > static_cast<unsigned long long>(std::numeric_limits<size_t>::max())) return defValue;
|
|
||||||
return static_cast<size_t>(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
// Guard against cache blowups from huge numbers of unique probes.
|
|
||||||
fileArchiveCacheMaxEntries_ = envSizeTOrDefault("WOWEE_MPQ_ARCHIVE_CACHE_MAX", fileArchiveCacheMaxEntries_);
|
|
||||||
fileArchiveCacheMisses_ = envFlagEnabled("WOWEE_MPQ_CACHE_MISSES");
|
|
||||||
LOG_INFO("MPQ archive lookup cache: maxEntries=", fileArchiveCacheMaxEntries_,
|
|
||||||
" cacheMisses=", (fileArchiveCacheMisses_ ? "yes" : "no"));
|
|
||||||
|
|
||||||
// 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 — auto-detect from available locale directories
|
|
||||||
{
|
|
||||||
// Prefer the locale override from environment, then scan for installed ones
|
|
||||||
const char* localeEnv = std::getenv("WOWEE_LOCALE");
|
|
||||||
std::string detectedLocale;
|
|
||||||
if (localeEnv && localeEnv[0] != '\0') {
|
|
||||||
detectedLocale = localeEnv;
|
|
||||||
LOG_INFO("Using locale from WOWEE_LOCALE env: ", detectedLocale);
|
|
||||||
} else {
|
|
||||||
// Priority order: enUS first, then other common locales
|
|
||||||
static const std::array<const char*, 12> knownLocales = {
|
|
||||||
"enUS", "enGB", "deDE", "frFR", "esES", "esMX",
|
|
||||||
"zhCN", "zhTW", "koKR", "ruRU", "ptBR", "itIT"
|
|
||||||
};
|
|
||||||
for (const char* loc : knownLocales) {
|
|
||||||
if (std::filesystem::exists(dataPath + "/" + loc)) {
|
|
||||||
detectedLocale = loc;
|
|
||||||
LOG_INFO("Auto-detected WoW locale: ", detectedLocale);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (detectedLocale.empty()) {
|
|
||||||
detectedLocale = "enUS";
|
|
||||||
LOG_WARNING("No locale directory found in data path; defaulting to enUS");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
loadLocaleArchives(detectedLocale);
|
|
||||||
}
|
|
||||||
|
|
||||||
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();
|
|
||||||
{
|
|
||||||
std::lock_guard<std::shared_mutex> lock(fileArchiveCacheMutex_);
|
|
||||||
fileArchiveCache_.clear();
|
|
||||||
}
|
|
||||||
{
|
|
||||||
std::lock_guard<std::mutex> lock(missingFileMutex_);
|
|
||||||
missingFileWarnings_.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;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Archive set/priority changed, so cached filename -> archive mappings may be stale.
|
|
||||||
{
|
|
||||||
std::lock_guard<std::shared_mutex> lock(fileArchiveCacheMutex_);
|
|
||||||
fileArchiveCache_.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
|
||||||
std::string stormFilename = filename;
|
|
||||||
std::replace(stormFilename.begin(), stormFilename.end(), '/', '\\');
|
|
||||||
// Open the file
|
|
||||||
HANDLE file = INVALID_HANDLE_VALUE;
|
|
||||||
if (SFileOpenFileEx(archive, stormFilename.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(),
|
|
||||||
[](unsigned char a, unsigned char b) { return std::tolower(a) == std::tolower(b); })) {
|
|
||||||
searchPath = entry.path();
|
|
||||||
found = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!found) {
|
|
||||||
logMissingFileOnce(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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
logMissingFileOnce(filename);
|
|
||||||
return std::vector<uint8_t>();
|
|
||||||
}
|
|
||||||
|
|
||||||
void MPQManager::logMissingFileOnce(const std::string& filename) const {
|
|
||||||
std::string normalized = toLowerCopy(filename);
|
|
||||||
std::lock_guard<std::mutex> lock(missingFileMutex_);
|
|
||||||
if (missingFileWarnings_.insert(normalized).second) {
|
|
||||||
LOG_WARNING("File not found: ", filename);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string stormFilename = filename;
|
|
||||||
std::replace(stormFilename.begin(), stormFilename.end(), '/', '\\');
|
|
||||||
HANDLE file = INVALID_HANDLE_VALUE;
|
|
||||||
if (!SFileOpenFileEx(archive, stormFilename.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
|
|
||||||
std::string cacheKey = normalizeVirtualFilenameForLookup(filename);
|
|
||||||
{
|
|
||||||
std::shared_lock<std::shared_mutex> lock(fileArchiveCacheMutex_);
|
|
||||||
auto it = fileArchiveCache_.find(cacheKey);
|
|
||||||
if (it != fileArchiveCache_.end()) {
|
|
||||||
return it->second;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string stormFilename = filename;
|
|
||||||
std::replace(stormFilename.begin(), stormFilename.end(), '/', '\\');
|
|
||||||
|
|
||||||
const auto start = std::chrono::steady_clock::now();
|
|
||||||
HANDLE found = INVALID_HANDLE_VALUE;
|
|
||||||
// Search archives in priority order (already sorted)
|
|
||||||
for (const auto& entry : archives) {
|
|
||||||
if (SFileHasFile(entry.handle, stormFilename.c_str())) {
|
|
||||||
found = entry.handle;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const auto end = std::chrono::steady_clock::now();
|
|
||||||
const auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count();
|
|
||||||
|
|
||||||
// Avoid caching misses unless explicitly enabled; miss caching can explode memory when
|
|
||||||
// code probes many unique non-existent paths (common with HD patch sets).
|
|
||||||
if (found == INVALID_HANDLE_VALUE && !fileArchiveCacheMisses_) {
|
|
||||||
if (ms >= 100) {
|
|
||||||
LOG_WARNING("Slow MPQ lookup: '", filename, "' scanned ", archives.size(), " archives in ", ms, " ms");
|
|
||||||
}
|
|
||||||
return found;
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
std::lock_guard<std::shared_mutex> lock(fileArchiveCacheMutex_);
|
|
||||||
if (fileArchiveCache_.size() >= fileArchiveCacheMaxEntries_) {
|
|
||||||
// Simple safety valve: clear the cache rather than allowing an unbounded growth.
|
|
||||||
LOG_WARNING("MPQ archive lookup cache cleared (size=", fileArchiveCache_.size(),
|
|
||||||
" reached maxEntries=", fileArchiveCacheMaxEntries_, ")");
|
|
||||||
fileArchiveCache_.clear();
|
|
||||||
}
|
|
||||||
// Another thread may have raced to populate; if so, prefer the existing value.
|
|
||||||
auto [it, inserted] = fileArchiveCache_.emplace(std::move(cacheKey), found);
|
|
||||||
if (!inserted) {
|
|
||||||
found = it->second;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// With caching this should only happen once per unique filename; keep threshold conservative.
|
|
||||||
if (ms >= 100) {
|
|
||||||
LOG_WARNING("Slow MPQ lookup: '", filename, "' scanned ", archives.size(), " archives in ", ms, " ms");
|
|
||||||
}
|
|
||||||
|
|
||||||
return found;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
return INVALID_HANDLE_VALUE;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool MPQManager::loadPatchArchives() {
|
|
||||||
#ifndef HAVE_STORMLIB
|
|
||||||
return false;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
const bool disableLetterPatches = envFlagEnabled("WOWEE_DISABLE_LETTER_PATCHES");
|
|
||||||
const bool disableNumericPatches = envFlagEnabled("WOWEE_DISABLE_NUMERIC_PATCHES");
|
|
||||||
|
|
||||||
if (disableLetterPatches) {
|
|
||||||
LOG_WARNING("MPQ letter patches disabled via WOWEE_DISABLE_LETTER_PATCHES=1");
|
|
||||||
}
|
|
||||||
if (disableNumericPatches) {
|
|
||||||
LOG_WARNING("MPQ numeric patches disabled via WOWEE_DISABLE_NUMERIC_PATCHES=1");
|
|
||||||
}
|
|
||||||
|
|
||||||
// WoW 3.3.5a patch archives (in order of priority, highest first)
|
|
||||||
std::vector<std::pair<std::string, int>> patchArchives = {
|
|
||||||
// Lettered patch MPQs are used by some clients/distributions (e.g. Patch-A.mpq..Patch-E.mpq).
|
|
||||||
// Treat them as higher priority than numeric patch MPQs.
|
|
||||||
// Keep priorities well above numeric patch-*.MPQ so lettered patches always win when both exist.
|
|
||||||
{"Patch-Z.mpq", 925}, {"Patch-Y.mpq", 924}, {"Patch-X.mpq", 923}, {"Patch-W.mpq", 922},
|
|
||||||
{"Patch-V.mpq", 921}, {"Patch-U.mpq", 920}, {"Patch-T.mpq", 919}, {"Patch-S.mpq", 918},
|
|
||||||
{"Patch-R.mpq", 917}, {"Patch-Q.mpq", 916}, {"Patch-P.mpq", 915}, {"Patch-O.mpq", 914},
|
|
||||||
{"Patch-N.mpq", 913}, {"Patch-M.mpq", 912}, {"Patch-L.mpq", 911}, {"Patch-K.mpq", 910},
|
|
||||||
{"Patch-J.mpq", 909}, {"Patch-I.mpq", 908}, {"Patch-H.mpq", 907}, {"Patch-G.mpq", 906},
|
|
||||||
{"Patch-F.mpq", 905}, {"Patch-E.mpq", 904}, {"Patch-D.mpq", 903}, {"Patch-C.mpq", 902},
|
|
||||||
{"Patch-B.mpq", 901}, {"Patch-A.mpq", 900},
|
|
||||||
// Lowercase variants (Linux case-sensitive filesystems).
|
|
||||||
{"patch-z.mpq", 825}, {"patch-y.mpq", 824}, {"patch-x.mpq", 823}, {"patch-w.mpq", 822},
|
|
||||||
{"patch-v.mpq", 821}, {"patch-u.mpq", 820}, {"patch-t.mpq", 819}, {"patch-s.mpq", 818},
|
|
||||||
{"patch-r.mpq", 817}, {"patch-q.mpq", 816}, {"patch-p.mpq", 815}, {"patch-o.mpq", 814},
|
|
||||||
{"patch-n.mpq", 813}, {"patch-m.mpq", 812}, {"patch-l.mpq", 811}, {"patch-k.mpq", 810},
|
|
||||||
{"patch-j.mpq", 809}, {"patch-i.mpq", 808}, {"patch-h.mpq", 807}, {"patch-g.mpq", 806},
|
|
||||||
{"patch-f.mpq", 805}, {"patch-e.mpq", 804}, {"patch-d.mpq", 803}, {"patch-c.mpq", 802},
|
|
||||||
{"patch-b.mpq", 801}, {"patch-a.mpq", 800},
|
|
||||||
|
|
||||||
{"patch-5.MPQ", 500},
|
|
||||||
{"patch-4.MPQ", 400},
|
|
||||||
{"patch-3.MPQ", 300},
|
|
||||||
{"patch-2.MPQ", 200},
|
|
||||||
{"patch.MPQ", 150},
|
|
||||||
};
|
|
||||||
|
|
||||||
// Build a case-insensitive lookup of files in the data directory so that
|
|
||||||
// Patch-A.MPQ, patch-a.mpq, PATCH-A.MPQ, etc. all resolve correctly on
|
|
||||||
// case-sensitive filesystems (Linux).
|
|
||||||
std::unordered_map<std::string, std::string> lowerToActual; // lowercase name → actual path
|
|
||||||
if (std::filesystem::is_directory(dataPath)) {
|
|
||||||
for (const auto& entry : std::filesystem::directory_iterator(dataPath)) {
|
|
||||||
if (!entry.is_regular_file()) continue;
|
|
||||||
std::string fname = entry.path().filename().string();
|
|
||||||
std::string lower = toLowerCopy(fname);
|
|
||||||
lowerToActual[lower] = entry.path().string();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int loadedPatches = 0;
|
|
||||||
for (const auto& [archive, priority] : patchArchives) {
|
|
||||||
// Classify letter vs numeric patch for the disable flags
|
|
||||||
std::string lowerArchive = toLowerCopy(archive);
|
|
||||||
const bool isLetterPatch =
|
|
||||||
(lowerArchive.size() >= 11) && // "patch-X.mpq" = 11 chars
|
|
||||||
(lowerArchive.rfind("patch-", 0) == 0) && // starts with "patch-"
|
|
||||||
(lowerArchive[6] >= 'a' && lowerArchive[6] <= 'z'); // letter after dash
|
|
||||||
if (isLetterPatch && disableLetterPatches) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (!isLetterPatch && disableNumericPatches) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Case-insensitive file lookup
|
|
||||||
auto it = lowerToActual.find(lowerArchive);
|
|
||||||
if (it != lowerToActual.end()) {
|
|
||||||
if (loadArchive(it->second, 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 (including speech MPQs for NPC voices)
|
|
||||||
std::vector<std::pair<std::string, int>> localeArchives = {
|
|
||||||
{"locale-" + locale + ".MPQ", 250},
|
|
||||||
{"speech-" + locale + ".MPQ", 240}, // Base speech/NPC voices
|
|
||||||
{"expansion-speech-" + locale + ".MPQ", 245}, // TBC speech
|
|
||||||
{"lichking-speech-" + locale + ".MPQ", 248}, // WotLK speech
|
|
||||||
{"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
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue