mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-03-22 23:30:14 +00:00
Compare commits
19 commits
570dec8b88
...
94e4a0bdb3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
94e4a0bdb3 | ||
|
|
956e2c8bb1 | ||
|
|
9c25713b72 | ||
|
|
53405ea322 | ||
|
|
401cb6928c | ||
|
|
64879b8aab | ||
|
|
35384b2c52 | ||
|
|
e1614d55a2 | ||
|
|
c849f9cdfa | ||
|
|
7557e388fb | ||
|
|
bdde5f305a | ||
|
|
2219ccde51 | ||
|
|
dd4b72e046 | ||
|
|
52accfde80 | ||
|
|
c26353eda1 | ||
|
|
5966fedc59 | ||
|
|
0e8f305087 | ||
|
|
fc68c6c6b7 | ||
|
|
1fab17e639 |
35 changed files with 1435 additions and 423 deletions
13
.github/workflows/release.yml
vendored
13
.github/workflows/release.yml
vendored
|
|
@ -59,6 +59,10 @@ jobs:
|
|||
- name: Configure
|
||||
run: cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
|
||||
|
||||
- name: Verify Release Config
|
||||
run: |
|
||||
cmake -LA -N build | grep -E '^CMAKE_BUILD_TYPE:STRING=Release$'
|
||||
|
||||
- name: Build
|
||||
run: cmake --build build --parallel $(nproc)
|
||||
|
||||
|
|
@ -123,6 +127,10 @@ jobs:
|
|||
-DCMAKE_PREFIX_PATH="$BREW" \
|
||||
-DOPENSSL_ROOT_DIR="$(brew --prefix openssl@3)"
|
||||
|
||||
- name: Verify Release Config
|
||||
run: |
|
||||
cmake -LA -N build | grep -E '^CMAKE_BUILD_TYPE:STRING=Release$'
|
||||
|
||||
- name: Build
|
||||
run: cmake --build build --parallel $(sysctl -n hw.logicalcpu)
|
||||
|
||||
|
|
@ -271,6 +279,11 @@ jobs:
|
|||
shell: msys2 {0}
|
||||
run: cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release
|
||||
|
||||
- name: Verify Release Config
|
||||
shell: msys2 {0}
|
||||
run: |
|
||||
cmake -LA -N build | grep -E '^CMAKE_BUILD_TYPE:STRING=Release$'
|
||||
|
||||
- name: Build
|
||||
shell: msys2 {0}
|
||||
run: cmake --build build --parallel $(nproc)
|
||||
|
|
|
|||
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -95,3 +95,7 @@ asset_pipeline/
|
|||
# Local texture dumps / extracted art should never be committed
|
||||
assets/textures/
|
||||
node_modules/
|
||||
|
||||
# Python cache artifacts
|
||||
tools/__pycache__/
|
||||
*.pyc
|
||||
|
|
|
|||
|
|
@ -6,6 +6,13 @@ set(CMAKE_CXX_STANDARD 20)
|
|||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
||||
|
||||
# Explicitly tag optimized configs so runtime defaults can enforce low-noise logging.
|
||||
add_compile_definitions(
|
||||
$<$<CONFIG:Release>:WOWEE_RELEASE_LOGGING>
|
||||
$<$<CONFIG:RelWithDebInfo>:WOWEE_RELEASE_LOGGING>
|
||||
$<$<CONFIG:MinSizeRel>:WOWEE_RELEASE_LOGGING>
|
||||
)
|
||||
|
||||
# Output directories
|
||||
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)
|
||||
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib)
|
||||
|
|
|
|||
BIN
assets/Original Music/TavernAlliance01.mp3
Normal file
BIN
assets/Original Music/TavernAlliance01.mp3
Normal file
Binary file not shown.
BIN
assets/Original Music/TavernAllianceREMIX.mp3
Normal file
BIN
assets/Original Music/TavernAllianceREMIX.mp3
Normal file
Binary file not shown.
3
build.bat
Normal file
3
build.bat
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
@echo off
|
||||
REM Convenience wrapper — launches the PowerShell build script.
|
||||
powershell -ExecutionPolicy Bypass -File "%~dp0build.ps1" %*
|
||||
44
build.ps1
Normal file
44
build.ps1
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
<#
|
||||
.SYNOPSIS
|
||||
Builds the wowee project (Windows equivalent of build.sh).
|
||||
|
||||
.DESCRIPTION
|
||||
Creates a build directory, runs CMake configure + build, and creates a
|
||||
directory junction for the Data folder so the binary can find assets.
|
||||
#>
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
Set-Location $ScriptDir
|
||||
|
||||
Write-Host "Building wowee..."
|
||||
|
||||
# Create build directory if it doesn't exist
|
||||
if (-not (Test-Path "build")) {
|
||||
New-Item -ItemType Directory -Path "build" | Out-Null
|
||||
}
|
||||
Set-Location "build"
|
||||
|
||||
# Configure with CMake
|
||||
Write-Host "Configuring with CMake..."
|
||||
& cmake .. -DCMAKE_BUILD_TYPE=Release
|
||||
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
||||
|
||||
# Build with all cores
|
||||
$numProcs = $env:NUMBER_OF_PROCESSORS
|
||||
if (-not $numProcs) { $numProcs = 4 }
|
||||
Write-Host "Building with $numProcs cores..."
|
||||
& cmake --build . --parallel $numProcs
|
||||
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
||||
|
||||
# Ensure Data junction exists in bin directory
|
||||
$binData = Join-Path (Get-Location) "bin\Data"
|
||||
if (-not (Test-Path $binData)) {
|
||||
$target = (Resolve-Path (Join-Path (Get-Location) "..\Data")).Path
|
||||
cmd /c mklink /J "$binData" "$target"
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "Build complete! Binary: build\bin\wowee.exe"
|
||||
Write-Host "Run with: cd build\bin && .\wowee.exe"
|
||||
3
debug_texture.bat
Normal file
3
debug_texture.bat
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
@echo off
|
||||
REM Convenience wrapper — launches the PowerShell texture debug script.
|
||||
powershell -ExecutionPolicy Bypass -File "%~dp0debug_texture.ps1" %*
|
||||
64
debug_texture.ps1
Normal file
64
debug_texture.ps1
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
<#
|
||||
.SYNOPSIS
|
||||
Converts raw RGBA texture dumps to PNG for visual inspection (Windows equivalent of debug_texture.sh).
|
||||
|
||||
.PARAMETER Width
|
||||
Texture width in pixels. Defaults to 1024.
|
||||
|
||||
.PARAMETER Height
|
||||
Texture height in pixels. Defaults to 1024.
|
||||
|
||||
.EXAMPLE
|
||||
.\debug_texture.ps1
|
||||
.\debug_texture.ps1 -Width 2048 -Height 2048
|
||||
#>
|
||||
|
||||
param(
|
||||
[int]$Width = 1024,
|
||||
[int]$Height = 1024
|
||||
)
|
||||
|
||||
$TempDir = $env:TEMP
|
||||
|
||||
Write-Host "Converting debug textures (${Width}x${Height})..."
|
||||
|
||||
# Find raw dumps — filenames include dimensions (e.g. wowee_composite_debug_1024x1024.raw)
|
||||
$rawFiles = Get-ChildItem -Path $TempDir -Filter "wowee_*_debug*.raw" -ErrorAction SilentlyContinue
|
||||
|
||||
if (-not $rawFiles) {
|
||||
Write-Host "No debug dumps found in $TempDir"
|
||||
Write-Host " (looked for $TempDir\wowee_*_debug*.raw)"
|
||||
exit 0
|
||||
}
|
||||
|
||||
foreach ($rawItem in $rawFiles) {
|
||||
$raw = $rawItem.FullName
|
||||
$png = $raw -replace '\.raw$', '.png'
|
||||
|
||||
# Try ImageMagick first, fall back to ffmpeg
|
||||
if (Get-Command magick -ErrorAction SilentlyContinue) {
|
||||
& magick -size "${Width}x${Height}" -depth 8 "rgba:$raw" "$png" 2>$null
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
Write-Host "Created $png (${Width}x${Height})"
|
||||
} else {
|
||||
Write-Host "Failed to convert $raw"
|
||||
}
|
||||
} elseif (Get-Command convert -ErrorAction SilentlyContinue) {
|
||||
& convert -size "${Width}x${Height}" -depth 8 "rgba:$raw" "$png" 2>$null
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
Write-Host "Created $png (${Width}x${Height})"
|
||||
} else {
|
||||
Write-Host "Failed to convert $raw"
|
||||
}
|
||||
} elseif (Get-Command ffmpeg -ErrorAction SilentlyContinue) {
|
||||
& ffmpeg -y -f rawvideo -pix_fmt rgba -s "${Width}x${Height}" -i "$raw" "$png" 2>$null
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
Write-Host "Created $png (${Width}x${Height})"
|
||||
} else {
|
||||
Write-Host "Failed to convert $raw"
|
||||
}
|
||||
} else {
|
||||
Write-Host "Need 'magick' (ImageMagick) or 'ffmpeg' to convert $raw"
|
||||
Write-Host " Install: winget install ImageMagick.ImageMagick"
|
||||
}
|
||||
}
|
||||
|
|
@ -8,8 +8,20 @@ H=${2:-1024}
|
|||
|
||||
echo "Converting debug textures (${W}x${H})..."
|
||||
|
||||
for raw in /tmp/wowee_composite_debug.raw /tmp/wowee_equip_composite_debug.raw; do
|
||||
if [ -f "$raw" ]; then
|
||||
TMPD="${TMPDIR:-/tmp}"
|
||||
|
||||
# Find raw dumps — filenames include dimensions (e.g. wowee_composite_debug_1024x1024.raw)
|
||||
shopt -s nullglob
|
||||
RAW_FILES=("$TMPD"/wowee_*_debug*.raw)
|
||||
shopt -u nullglob
|
||||
|
||||
if [ ${#RAW_FILES[@]} -eq 0 ]; then
|
||||
echo "No debug dumps found in $TMPD"
|
||||
echo " (looked for $TMPD/wowee_*_debug*.raw)"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
for raw in "${RAW_FILES[@]}"; do
|
||||
png="${raw%.raw}.png"
|
||||
# Try ImageMagick first, fall back to ffmpeg
|
||||
if command -v convert &>/dev/null; then
|
||||
|
|
@ -24,7 +36,4 @@ for raw in /tmp/wowee_composite_debug.raw /tmp/wowee_equip_composite_debug.raw;
|
|||
echo "Need 'convert' (ImageMagick) or 'ffmpeg' to convert $raw"
|
||||
echo " Install: sudo apt install imagemagick"
|
||||
fi
|
||||
else
|
||||
echo "Not found: $raw"
|
||||
fi
|
||||
done
|
||||
|
|
|
|||
|
|
@ -244,7 +244,7 @@ private:
|
|||
float x, y, z, orientation;
|
||||
};
|
||||
std::vector<PendingCreatureSpawn> pendingCreatureSpawns_;
|
||||
static constexpr int MAX_SPAWNS_PER_FRAME = 8;
|
||||
static constexpr int MAX_SPAWNS_PER_FRAME = 3;
|
||||
static constexpr int MAX_NEW_CREATURE_MODELS_PER_FRAME = 1;
|
||||
static constexpr uint16_t MAX_CREATURE_SPAWN_RETRIES = 300;
|
||||
std::unordered_set<uint64_t> pendingCreatureSpawnGuids_;
|
||||
|
|
|
|||
|
|
@ -26,6 +26,10 @@ enum class LogLevel {
|
|||
FATAL
|
||||
};
|
||||
|
||||
// Avoid direct token use of `ERROR` at call sites because Windows headers
|
||||
// define `ERROR` as a macro.
|
||||
inline constexpr LogLevel kLogLevelError = LogLevel::ERROR;
|
||||
|
||||
class Logger {
|
||||
public:
|
||||
static Logger& getInstance();
|
||||
|
|
@ -65,6 +69,13 @@ public:
|
|||
}
|
||||
|
||||
private:
|
||||
static constexpr int kDefaultMinLevelValue =
|
||||
#if defined(NDEBUG) || defined(WOWEE_RELEASE_LOGGING)
|
||||
static_cast<int>(LogLevel::WARNING);
|
||||
#else
|
||||
static_cast<int>(LogLevel::INFO);
|
||||
#endif
|
||||
|
||||
Logger() = default;
|
||||
~Logger() = default;
|
||||
Logger(const Logger&) = delete;
|
||||
|
|
@ -77,22 +88,61 @@ private:
|
|||
return oss.str();
|
||||
}
|
||||
|
||||
std::atomic<int> minLevel_{static_cast<int>(LogLevel::INFO)};
|
||||
std::atomic<int> minLevel_{kDefaultMinLevelValue};
|
||||
std::mutex mutex;
|
||||
std::ofstream fileStream;
|
||||
bool fileReady = false;
|
||||
bool echoToStdout_ = true;
|
||||
std::chrono::steady_clock::time_point lastFlushTime_{};
|
||||
uint32_t flushIntervalMs_ = 250;
|
||||
bool dedupeEnabled_ = true;
|
||||
uint32_t dedupeWindowMs_ = 250;
|
||||
LogLevel lastLevel_ = LogLevel::DEBUG;
|
||||
std::string lastMessage_;
|
||||
std::chrono::steady_clock::time_point lastMessageTime_{};
|
||||
uint64_t suppressedCount_ = 0;
|
||||
void emitLineLocked(LogLevel level, const std::string& message);
|
||||
void flushSuppressedLocked();
|
||||
void ensureFile();
|
||||
};
|
||||
|
||||
// Convenience macros
|
||||
#define LOG_DEBUG(...) wowee::core::Logger::getInstance().debug(__VA_ARGS__)
|
||||
#define LOG_INFO(...) wowee::core::Logger::getInstance().info(__VA_ARGS__)
|
||||
#define LOG_WARNING(...) wowee::core::Logger::getInstance().warning(__VA_ARGS__)
|
||||
#define LOG_ERROR(...) wowee::core::Logger::getInstance().error(__VA_ARGS__)
|
||||
#define LOG_FATAL(...) wowee::core::Logger::getInstance().fatal(__VA_ARGS__)
|
||||
// Convenience macros.
|
||||
// Guard calls at the macro site so variadic arguments are not evaluated
|
||||
// when the corresponding level is disabled.
|
||||
#define LOG_DEBUG(...) do { \
|
||||
auto& _wowee_logger = wowee::core::Logger::getInstance(); \
|
||||
if (_wowee_logger.shouldLog(wowee::core::LogLevel::DEBUG)) { \
|
||||
_wowee_logger.debug(__VA_ARGS__); \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
#define LOG_INFO(...) do { \
|
||||
auto& _wowee_logger = wowee::core::Logger::getInstance(); \
|
||||
if (_wowee_logger.shouldLog(wowee::core::LogLevel::INFO)) { \
|
||||
_wowee_logger.info(__VA_ARGS__); \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
#define LOG_WARNING(...) do { \
|
||||
auto& _wowee_logger = wowee::core::Logger::getInstance(); \
|
||||
if (_wowee_logger.shouldLog(wowee::core::LogLevel::WARNING)) { \
|
||||
_wowee_logger.warning(__VA_ARGS__); \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
#define LOG_ERROR(...) do { \
|
||||
auto& _wowee_logger = wowee::core::Logger::getInstance(); \
|
||||
if (_wowee_logger.shouldLog(wowee::core::kLogLevelError)) { \
|
||||
_wowee_logger.error(__VA_ARGS__); \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
#define LOG_FATAL(...) do { \
|
||||
auto& _wowee_logger = wowee::core::Logger::getInstance(); \
|
||||
if (_wowee_logger.shouldLog(wowee::core::LogLevel::FATAL)) { \
|
||||
_wowee_logger.fatal(__VA_ARGS__); \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
} // namespace core
|
||||
} // namespace wowee
|
||||
|
|
|
|||
|
|
@ -171,6 +171,8 @@ private:
|
|||
// Cached isInsideWMO result (throttled to avoid per-frame cost)
|
||||
bool cachedInsideWMO = false;
|
||||
bool cachedInsideInteriorWMO = false;
|
||||
int insideStateCheckCounter_ = 0;
|
||||
glm::vec3 lastInsideStateCheckPos_ = glm::vec3(0.0f);
|
||||
int insideWMOCheckCounter = 0;
|
||||
glm::vec3 lastInsideWMOCheckPos = glm::vec3(0.0f);
|
||||
|
||||
|
|
|
|||
|
|
@ -238,6 +238,7 @@ private:
|
|||
glm::vec3 shadowCenter = glm::vec3(0.0f);
|
||||
bool shadowCenterInitialized = false;
|
||||
bool shadowsEnabled = true;
|
||||
uint32_t shadowFrameCounter_ = 0;
|
||||
|
||||
|
||||
public:
|
||||
|
|
|
|||
|
|
@ -390,13 +390,19 @@ private:
|
|||
std::vector<std::vector<uint32_t>> cellTriangles;
|
||||
|
||||
// Pre-classified triangle lists per cell (built at load time)
|
||||
std::vector<std::vector<uint32_t>> cellFloorTriangles; // abs(normal.z) >= 0.45
|
||||
std::vector<std::vector<uint32_t>> cellWallTriangles; // abs(normal.z) < 0.55
|
||||
std::vector<std::vector<uint32_t>> cellFloorTriangles; // abs(normal.z) >= 0.35
|
||||
std::vector<std::vector<uint32_t>> cellWallTriangles; // abs(normal.z) < 0.35
|
||||
|
||||
// Pre-computed per-triangle Z bounds for fast vertical reject
|
||||
struct TriBounds { float minZ; float maxZ; };
|
||||
std::vector<TriBounds> triBounds; // indexed by triStart/3
|
||||
|
||||
// Pre-computed per-triangle normals (unit length, indexed by triStart/3)
|
||||
std::vector<glm::vec3> triNormals;
|
||||
|
||||
// Scratch bitset for deduplicating triangle queries (sized to numTriangles)
|
||||
mutable std::vector<uint8_t> triVisited;
|
||||
|
||||
// Build the spatial grid from collision geometry
|
||||
void buildCollisionGrid();
|
||||
|
||||
|
|
@ -675,7 +681,7 @@ private:
|
|||
std::unordered_map<GridCell, std::vector<uint32_t>, GridCellHash> spatialGrid;
|
||||
std::unordered_map<uint32_t, size_t> instanceIndexById;
|
||||
mutable std::vector<size_t> candidateScratch;
|
||||
mutable std::vector<uint32_t> wallTriScratch; // Scratch for wall collision grid queries
|
||||
mutable std::vector<uint32_t> triScratch_; // Scratch for collision grid queries
|
||||
mutable std::unordered_set<uint32_t> candidateIdScratch;
|
||||
|
||||
// Parallel visibility culling
|
||||
|
|
|
|||
|
|
@ -120,6 +120,9 @@ private:
|
|||
|
||||
bool musicInitAttempted = false;
|
||||
bool musicPlaying = false;
|
||||
bool missingIntroTracksLogged_ = false;
|
||||
bool introTracksScanned_ = false;
|
||||
std::vector<std::string> introTracks_;
|
||||
bool loginMusicVolumeAdjusted_ = false;
|
||||
int savedMusicVolume_ = 30;
|
||||
};
|
||||
|
|
|
|||
3
rebuild.bat
Normal file
3
rebuild.bat
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
@echo off
|
||||
REM Convenience wrapper — launches the PowerShell clean rebuild script.
|
||||
powershell -ExecutionPolicy Bypass -File "%~dp0rebuild.ps1" %*
|
||||
50
rebuild.ps1
Normal file
50
rebuild.ps1
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
<#
|
||||
.SYNOPSIS
|
||||
Clean rebuilds the wowee project (Windows equivalent of rebuild.sh).
|
||||
|
||||
.DESCRIPTION
|
||||
Removes the build directory, reconfigures from scratch, rebuilds, and
|
||||
creates a directory junction for the Data folder.
|
||||
#>
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
Set-Location $ScriptDir
|
||||
|
||||
Write-Host "Clean rebuilding wowee..."
|
||||
|
||||
# Remove build directory completely
|
||||
if (Test-Path "build") {
|
||||
Write-Host "Removing old build directory..."
|
||||
Remove-Item -Recurse -Force "build"
|
||||
}
|
||||
|
||||
# Create fresh build directory
|
||||
New-Item -ItemType Directory -Path "build" | Out-Null
|
||||
Set-Location "build"
|
||||
|
||||
# Configure with CMake
|
||||
Write-Host "Configuring with CMake..."
|
||||
& cmake .. -DCMAKE_BUILD_TYPE=Release
|
||||
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
||||
|
||||
# Build with all cores
|
||||
$numProcs = $env:NUMBER_OF_PROCESSORS
|
||||
if (-not $numProcs) { $numProcs = 4 }
|
||||
Write-Host "Building with $numProcs cores..."
|
||||
& cmake --build . --parallel $numProcs
|
||||
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
||||
|
||||
# Create Data junction in bin directory
|
||||
Write-Host "Creating Data junction..."
|
||||
$binData = Join-Path (Get-Location) "bin\Data"
|
||||
if (-not (Test-Path $binData)) {
|
||||
$target = (Resolve-Path (Join-Path (Get-Location) "..\Data")).Path
|
||||
cmd /c mklink /J "$binData" "$target"
|
||||
Write-Host " Created Data junction -> $target"
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "Clean build complete! Binary: build\bin\wowee.exe"
|
||||
Write-Host "Run with: cd build\bin && .\wowee.exe"
|
||||
|
|
@ -296,7 +296,8 @@ void AmbientSoundManager::updatePositionalEmitters(float deltaTime, const glm::v
|
|||
const int MAX_ACTIVE_WATER = 3; // Max 3 water sounds at once
|
||||
|
||||
for (auto& emitter : emitters_) {
|
||||
float distance = glm::distance(emitter.position, cameraPos);
|
||||
const glm::vec3 delta = emitter.position - cameraPos;
|
||||
const float distSq = glm::dot(delta, delta);
|
||||
|
||||
// Determine max distance based on type
|
||||
float maxDist = MAX_AMBIENT_DISTANCE;
|
||||
|
|
@ -317,7 +318,8 @@ void AmbientSoundManager::updatePositionalEmitters(float deltaTime, const glm::v
|
|||
}
|
||||
|
||||
// Update active state based on distance AND limits
|
||||
bool withinRange = (distance < maxDist);
|
||||
const float maxDistSq = maxDist * maxDist;
|
||||
const bool withinRange = (distSq < maxDistSq);
|
||||
|
||||
if (isFire && withinRange && activeFireCount < MAX_ACTIVE_FIRE) {
|
||||
emitter.active = true;
|
||||
|
|
@ -336,6 +338,9 @@ void AmbientSoundManager::updatePositionalEmitters(float deltaTime, const glm::v
|
|||
// Update play timer
|
||||
emitter.lastPlayTime += deltaTime;
|
||||
|
||||
// We only need the true distance for volume attenuation once the emitter is active.
|
||||
const float distance = std::sqrt(distSq);
|
||||
|
||||
// Handle different emitter types
|
||||
switch (emitter.type) {
|
||||
case AmbientType::FIREPLACE_SMALL:
|
||||
|
|
|
|||
|
|
@ -1,12 +1,59 @@
|
|||
#include "auth/auth_packets.hpp"
|
||||
#include "core/logger.hpp"
|
||||
#include "network/net_platform.hpp"
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cstring>
|
||||
#include <array>
|
||||
|
||||
namespace wowee {
|
||||
namespace auth {
|
||||
|
||||
namespace {
|
||||
bool detectOutboundIPv4(std::array<uint8_t, 4>& outIp) {
|
||||
net::ensureInit();
|
||||
|
||||
socket_t s = ::socket(AF_INET, SOCK_DGRAM, 0);
|
||||
if (s == INVALID_SOCK) {
|
||||
return false;
|
||||
}
|
||||
|
||||
sockaddr_in remote{};
|
||||
remote.sin_family = AF_INET;
|
||||
remote.sin_port = htons(53);
|
||||
if (inet_pton(AF_INET, "1.1.1.1", &remote.sin_addr) != 1) {
|
||||
net::closeSocket(s);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (::connect(s, reinterpret_cast<sockaddr*>(&remote), sizeof(remote)) != 0) {
|
||||
net::closeSocket(s);
|
||||
return false;
|
||||
}
|
||||
|
||||
sockaddr_in local{};
|
||||
#ifdef _WIN32
|
||||
int localLen = sizeof(local);
|
||||
#else
|
||||
socklen_t localLen = sizeof(local);
|
||||
#endif
|
||||
if (::getsockname(s, reinterpret_cast<sockaddr*>(&local), &localLen) != 0) {
|
||||
net::closeSocket(s);
|
||||
return false;
|
||||
}
|
||||
|
||||
net::closeSocket(s);
|
||||
|
||||
const uint32_t ip = ntohl(local.sin_addr.s_addr);
|
||||
outIp[0] = static_cast<uint8_t>((ip >> 24) & 0xFF);
|
||||
outIp[1] = static_cast<uint8_t>((ip >> 16) & 0xFF);
|
||||
outIp[2] = static_cast<uint8_t>((ip >> 8) & 0xFF);
|
||||
outIp[3] = static_cast<uint8_t>(ip & 0xFF);
|
||||
|
||||
return (ip != 0);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
network::Packet LogonChallengePacket::build(const std::string& account, const ClientInfo& info) {
|
||||
// Convert account to uppercase
|
||||
std::string upperAccount = account;
|
||||
|
|
@ -66,8 +113,20 @@ network::Packet LogonChallengePacket::build(const std::string& account, const Cl
|
|||
// Timezone
|
||||
packet.writeUInt32(info.timezone);
|
||||
|
||||
// IP address (always 0)
|
||||
// Client IP: use the real outbound local IPv4 when detectable.
|
||||
// Fallback to 0.0.0.0 if detection fails.
|
||||
{
|
||||
std::array<uint8_t, 4> localIp{0, 0, 0, 0};
|
||||
if (detectOutboundIPv4(localIp)) {
|
||||
packet.writeUInt8(localIp[0]);
|
||||
packet.writeUInt8(localIp[1]);
|
||||
packet.writeUInt8(localIp[2]);
|
||||
packet.writeUInt8(localIp[3]);
|
||||
} else {
|
||||
packet.writeUInt32(0);
|
||||
LOG_DEBUG("LOGON_CHALLENGE client IP detection failed; using 0.0.0.0 fallback");
|
||||
}
|
||||
}
|
||||
|
||||
// Account length and name
|
||||
packet.writeUInt8(static_cast<uint8_t>(upperAccount.length()));
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <vector>
|
||||
|
||||
namespace wowee {
|
||||
namespace auth {
|
||||
|
|
@ -41,39 +42,46 @@ bool computeIntegrityHashWin32WithExe(const std::array<uint8_t, 16>& checksumSal
|
|||
// that distribution rather than a stock 1.12.1 client, so when using Turtle's executable we include
|
||||
// Turtle-specific DLLs as well.
|
||||
const bool isTurtleExe = (exeName == "TurtleWoW.exe");
|
||||
const char* kFilesBase[] = {
|
||||
nullptr, // exeName
|
||||
"fmod.dll",
|
||||
"ijl15.dll",
|
||||
"dbghelp.dll",
|
||||
"unicows.dll",
|
||||
// Some macOS client layouts use FMOD dylib naming instead of fmod.dll.
|
||||
// We accept the first matching filename in each alias group.
|
||||
std::vector<std::vector<std::string>> fileGroups = {
|
||||
{ exeName },
|
||||
{ "fmod.dll", "fmod.dylib", "libfmod.dylib", "fmodex.dll", "fmodex.dylib", "libfmod.so" },
|
||||
{ "ijl15.dll" },
|
||||
{ "dbghelp.dll" },
|
||||
{ "unicows.dll" },
|
||||
};
|
||||
const char* kFilesTurtleExtra[] = {
|
||||
"twloader.dll",
|
||||
"twdiscord.dll",
|
||||
};
|
||||
|
||||
std::vector<std::string> files;
|
||||
files.reserve(1 + 4 + (isTurtleExe ? (sizeof(kFilesTurtleExtra) / sizeof(kFilesTurtleExtra[0])) : 0));
|
||||
for (const char* f : kFilesBase) {
|
||||
files.push_back(f ? std::string(f) : exeName);
|
||||
}
|
||||
if (isTurtleExe) {
|
||||
for (const char* f : kFilesTurtleExtra) files.push_back(std::string(f));
|
||||
fileGroups.push_back({ "twloader.dll" });
|
||||
fileGroups.push_back({ "twdiscord.dll" });
|
||||
}
|
||||
|
||||
std::vector<uint8_t> allFiles;
|
||||
std::string err;
|
||||
for (const auto& nameStr : files) {
|
||||
for (const auto& group : fileGroups) {
|
||||
bool foundInGroup = false;
|
||||
std::string groupErr;
|
||||
|
||||
for (const auto& nameStr : group) {
|
||||
std::vector<uint8_t> bytes;
|
||||
std::string path = miscDir;
|
||||
if (!path.empty() && path.back() != '/') path += '/';
|
||||
path += nameStr;
|
||||
|
||||
std::string err;
|
||||
if (!readWholeFile(path, bytes, err)) {
|
||||
outError = err;
|
||||
if (groupErr.empty()) groupErr = err;
|
||||
continue;
|
||||
}
|
||||
|
||||
allFiles.insert(allFiles.end(), bytes.begin(), bytes.end());
|
||||
foundInGroup = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!foundInGroup) {
|
||||
outError = groupErr.empty() ? "missing required integrity file group" : groupErr;
|
||||
return false;
|
||||
}
|
||||
allFiles.insert(allFiles.end(), bytes.begin(), bytes.end());
|
||||
}
|
||||
|
||||
// HMAC_SHA1(checksumSalt, allFiles)
|
||||
|
|
|
|||
|
|
@ -578,25 +578,100 @@ void Application::reloadExpansionData() {
|
|||
|
||||
void Application::logoutToLogin() {
|
||||
LOG_INFO("Logout requested");
|
||||
|
||||
// Disconnect TransportManager from WMORenderer before tearing down
|
||||
if (gameHandler && gameHandler->getTransportManager()) {
|
||||
gameHandler->getTransportManager()->setWMORenderer(nullptr);
|
||||
}
|
||||
|
||||
if (gameHandler) {
|
||||
gameHandler->disconnect();
|
||||
}
|
||||
|
||||
// --- Per-session flags ---
|
||||
npcsSpawned = false;
|
||||
playerCharacterSpawned = false;
|
||||
weaponsSheathed_ = false;
|
||||
wasAutoAttacking_ = false;
|
||||
loadedMapId_ = 0xFFFFFFFF;
|
||||
lastTaxiFlight_ = false;
|
||||
taxiLandingClampTimer_ = 0.0f;
|
||||
worldEntryMovementGraceTimer_ = 0.0f;
|
||||
facingSendCooldown_ = 0.0f;
|
||||
lastSentCanonicalYaw_ = 1000.0f;
|
||||
taxiStreamCooldown_ = 0.0f;
|
||||
idleYawned_ = false;
|
||||
|
||||
// --- Charge state ---
|
||||
chargeActive_ = false;
|
||||
chargeTimer_ = 0.0f;
|
||||
chargeDuration_ = 0.0f;
|
||||
chargeTargetGuid_ = 0;
|
||||
|
||||
// --- Player identity ---
|
||||
spawnedPlayerGuid_ = 0;
|
||||
spawnedAppearanceBytes_ = 0;
|
||||
spawnedFacialFeatures_ = 0;
|
||||
|
||||
// --- Mount state ---
|
||||
mountInstanceId_ = 0;
|
||||
mountModelId_ = 0;
|
||||
pendingMountDisplayId_ = 0;
|
||||
|
||||
// --- Creature instance tracking ---
|
||||
creatureInstances_.clear();
|
||||
creatureModelIds_.clear();
|
||||
creatureRenderPosCache_.clear();
|
||||
creatureWeaponsAttached_.clear();
|
||||
creatureWeaponAttachAttempts_.clear();
|
||||
deadCreatureGuids_.clear();
|
||||
nonRenderableCreatureDisplayIds_.clear();
|
||||
creaturePermanentFailureGuids_.clear();
|
||||
|
||||
// --- Creature spawn queues ---
|
||||
pendingCreatureSpawns_.clear();
|
||||
pendingCreatureSpawnGuids_.clear();
|
||||
creatureSpawnRetryCounts_.clear();
|
||||
|
||||
// --- Player instance tracking ---
|
||||
playerInstances_.clear();
|
||||
onlinePlayerAppearance_.clear();
|
||||
pendingOnlinePlayerEquipment_.clear();
|
||||
deferredEquipmentQueue_.clear();
|
||||
pendingPlayerSpawns_.clear();
|
||||
pendingPlayerSpawnGuids_.clear();
|
||||
|
||||
// --- GameObject instance tracking ---
|
||||
gameObjectInstances_.clear();
|
||||
pendingGameObjectSpawns_.clear();
|
||||
pendingTransportMoves_.clear();
|
||||
pendingTransportDoodadBatches_.clear();
|
||||
|
||||
world.reset();
|
||||
|
||||
if (renderer) {
|
||||
// Remove old player model so it doesn't persist into next session
|
||||
if (auto* charRenderer = renderer->getCharacterRenderer()) {
|
||||
charRenderer->removeInstance(1);
|
||||
}
|
||||
// Clear all world geometry renderers
|
||||
if (auto* wmo = renderer->getWMORenderer()) {
|
||||
wmo->clearInstances();
|
||||
}
|
||||
if (auto* m2 = renderer->getM2Renderer()) {
|
||||
m2->clear();
|
||||
}
|
||||
// TerrainManager will be re-initialized on next world entry
|
||||
if (auto* questMarkers = renderer->getQuestMarkerRenderer()) {
|
||||
questMarkers->clear();
|
||||
}
|
||||
renderer->clearMount();
|
||||
renderer->setCharacterFollow(0);
|
||||
if (auto* music = renderer->getMusicManager()) {
|
||||
music->stopMusic(0.0f);
|
||||
}
|
||||
}
|
||||
|
||||
// Clear stale realm/character selection so switching servers starts fresh
|
||||
if (uiManager) {
|
||||
uiManager->getRealmScreen().reset();
|
||||
|
|
@ -711,7 +786,7 @@ void Application::update(float deltaTime) {
|
|||
if (gameHandler) {
|
||||
static float creatureResyncTimer = 0.0f;
|
||||
creatureResyncTimer += deltaTime;
|
||||
if (creatureResyncTimer >= 1.0f) {
|
||||
if (creatureResyncTimer >= 3.0f) {
|
||||
creatureResyncTimer = 0.0f;
|
||||
|
||||
glm::vec3 playerPos(0.0f);
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@
|
|||
#include <ctime>
|
||||
#include <filesystem>
|
||||
#include <cstdlib>
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
|
||||
namespace wowee {
|
||||
namespace core {
|
||||
|
|
@ -28,20 +30,35 @@ void Logger::ensureFile() {
|
|||
flushIntervalMs_ = static_cast<uint32_t>(parsed);
|
||||
}
|
||||
}
|
||||
if (const char* dedupe = std::getenv("WOWEE_LOG_DEDUPE")) {
|
||||
dedupeEnabled_ = !(dedupe[0] == '0' || dedupe[0] == 'f' || dedupe[0] == 'F' ||
|
||||
dedupe[0] == 'n' || dedupe[0] == 'N');
|
||||
}
|
||||
if (const char* dedupeMs = std::getenv("WOWEE_LOG_DEDUPE_MS")) {
|
||||
char* end = nullptr;
|
||||
unsigned long parsed = std::strtoul(dedupeMs, &end, 10);
|
||||
if (end != dedupeMs && parsed <= 60000ul) {
|
||||
dedupeWindowMs_ = static_cast<uint32_t>(parsed);
|
||||
}
|
||||
}
|
||||
if (const char* level = std::getenv("WOWEE_LOG_LEVEL")) {
|
||||
std::string v(level);
|
||||
std::transform(v.begin(), v.end(), v.begin(), [](unsigned char c) {
|
||||
return static_cast<char>(std::tolower(c));
|
||||
});
|
||||
if (v == "debug") setLogLevel(LogLevel::DEBUG);
|
||||
else if (v == "info") setLogLevel(LogLevel::INFO);
|
||||
else if (v == "warn" || v == "warning") setLogLevel(LogLevel::WARNING);
|
||||
else if (v == "error") setLogLevel(kLogLevelError);
|
||||
else if (v == "fatal") setLogLevel(LogLevel::FATAL);
|
||||
}
|
||||
std::error_code ec;
|
||||
std::filesystem::create_directories("logs", ec);
|
||||
fileStream.open("logs/wowee.log", std::ios::out | std::ios::trunc);
|
||||
lastFlushTime_ = std::chrono::steady_clock::now();
|
||||
}
|
||||
|
||||
void Logger::log(LogLevel level, const std::string& message) {
|
||||
if (!shouldLog(level)) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(mutex);
|
||||
ensureFile();
|
||||
|
||||
void Logger::emitLineLocked(LogLevel level, const std::string& message) {
|
||||
// Get current time
|
||||
auto now = std::chrono::system_clock::now();
|
||||
auto time = std::chrono::system_clock::to_time_t(now);
|
||||
|
|
@ -66,7 +83,7 @@ void Logger::log(LogLevel level, const std::string& message) {
|
|||
case LogLevel::DEBUG: line << "DEBUG"; break;
|
||||
case LogLevel::INFO: line << "INFO "; break;
|
||||
case LogLevel::WARNING: line << "WARN "; break;
|
||||
case LogLevel::ERROR: line << "ERROR"; break;
|
||||
case kLogLevelError: line << "ERROR"; break;
|
||||
case LogLevel::FATAL: line << "FATAL"; break;
|
||||
}
|
||||
|
||||
|
|
@ -92,6 +109,38 @@ void Logger::log(LogLevel level, const std::string& message) {
|
|||
}
|
||||
}
|
||||
|
||||
void Logger::flushSuppressedLocked() {
|
||||
if (suppressedCount_ == 0) return;
|
||||
emitLineLocked(lastLevel_, "Previous message repeated " + std::to_string(suppressedCount_) + " times");
|
||||
suppressedCount_ = 0;
|
||||
}
|
||||
|
||||
void Logger::log(LogLevel level, const std::string& message) {
|
||||
if (!shouldLog(level)) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(mutex);
|
||||
ensureFile();
|
||||
|
||||
auto nowSteady = std::chrono::steady_clock::now();
|
||||
if (dedupeEnabled_ && !lastMessage_.empty() &&
|
||||
level == lastLevel_ && message == lastMessage_) {
|
||||
auto elapsedMs = std::chrono::duration_cast<std::chrono::milliseconds>(nowSteady - lastMessageTime_).count();
|
||||
if (elapsedMs >= 0 && elapsedMs <= static_cast<long long>(dedupeWindowMs_)) {
|
||||
++suppressedCount_;
|
||||
lastMessageTime_ = nowSteady;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
flushSuppressedLocked();
|
||||
emitLineLocked(level, message);
|
||||
lastLevel_ = level;
|
||||
lastMessage_ = message;
|
||||
lastMessageTime_ = nowSteady;
|
||||
}
|
||||
|
||||
void Logger::setLogLevel(LogLevel level) {
|
||||
minLevel_.store(static_cast<int>(level), std::memory_order_relaxed);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1235,7 +1235,7 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
++wardenPacketsAfterGate_;
|
||||
}
|
||||
if (preLogicalOp && isAuthCharPipelineOpcode(*preLogicalOp)) {
|
||||
LOG_INFO("AUTH/CHAR RX opcode=0x", std::hex, opcode, std::dec,
|
||||
LOG_DEBUG("AUTH/CHAR RX opcode=0x", std::hex, opcode, std::dec,
|
||||
" state=", worldStateName(state),
|
||||
" size=", packet.getSize());
|
||||
}
|
||||
|
|
@ -3462,7 +3462,7 @@ bool GameHandler::loadWardenCRFile(const std::string& moduleHashHex) {
|
|||
for (int i = 0; i < 9; i++) {
|
||||
char s[16]; snprintf(s, sizeof(s), "%s=0x%02X ", names[i], wardenCheckOpcodes_[i]); opcHex += s;
|
||||
}
|
||||
LOG_INFO("Warden: Check opcodes: ", opcHex);
|
||||
LOG_DEBUG("Warden: Check opcodes: ", opcHex);
|
||||
}
|
||||
|
||||
size_t entryCount = (static_cast<size_t>(fileSize) - CR_HEADER_SIZE) / CR_ENTRY_SIZE;
|
||||
|
|
@ -3512,17 +3512,20 @@ void GameHandler::handleWardenData(network::Packet& packet) {
|
|||
// Decrypt the payload
|
||||
std::vector<uint8_t> decrypted = wardenCrypto_->decrypt(data);
|
||||
|
||||
// Log decrypted data
|
||||
{
|
||||
// Avoid expensive hex formatting when DEBUG logs are disabled.
|
||||
if (core::Logger::getInstance().shouldLog(core::LogLevel::DEBUG)) {
|
||||
std::string hex;
|
||||
size_t logSize = std::min(decrypted.size(), size_t(256));
|
||||
hex.reserve(logSize * 3);
|
||||
for (size_t i = 0; i < logSize; ++i) {
|
||||
char b[4]; snprintf(b, sizeof(b), "%02x ", decrypted[i]); hex += b;
|
||||
char b[4];
|
||||
snprintf(b, sizeof(b), "%02x ", decrypted[i]);
|
||||
hex += b;
|
||||
}
|
||||
if (decrypted.size() > 64)
|
||||
if (decrypted.size() > 64) {
|
||||
hex += "... (" + std::to_string(decrypted.size() - 64) + " more)";
|
||||
LOG_INFO("Warden: Decrypted (", decrypted.size(), " bytes): ", hex);
|
||||
}
|
||||
LOG_DEBUG("Warden: Decrypted (", decrypted.size(), " bytes): ", hex);
|
||||
}
|
||||
|
||||
if (decrypted.empty()) {
|
||||
|
|
@ -3541,7 +3544,7 @@ void GameHandler::handleWardenData(network::Packet& packet) {
|
|||
}
|
||||
if (socket && socket->isConnected()) {
|
||||
socket->send(response);
|
||||
LOG_INFO("Warden: Sent response (", plaintext.size(), " bytes plaintext)");
|
||||
LOG_DEBUG("Warden: Sent response (", plaintext.size(), " bytes plaintext)");
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -3564,7 +3567,7 @@ void GameHandler::handleWardenData(network::Packet& packet) {
|
|||
{
|
||||
std::string hashHex;
|
||||
for (auto b : wardenModuleHash_) { char s[4]; snprintf(s, 4, "%02x", b); hashHex += s; }
|
||||
LOG_INFO("Warden: MODULE_USE hash=", hashHex, " size=", wardenModuleSize_);
|
||||
LOG_DEBUG("Warden: MODULE_USE hash=", hashHex, " size=", wardenModuleSize_);
|
||||
|
||||
// Try to load pre-computed challenge/response entries
|
||||
loadWardenCRFile(hashHex);
|
||||
|
|
@ -3574,7 +3577,7 @@ void GameHandler::handleWardenData(network::Packet& packet) {
|
|||
std::vector<uint8_t> resp = { 0x00 }; // WARDEN_CMSG_MODULE_MISSING
|
||||
sendWardenResponse(resp);
|
||||
wardenState_ = WardenState::WAIT_MODULE_CACHE;
|
||||
LOG_INFO("Warden: Sent MODULE_MISSING, waiting for module data chunks");
|
||||
LOG_DEBUG("Warden: Sent MODULE_MISSING, waiting for module data chunks");
|
||||
break;
|
||||
}
|
||||
|
||||
|
|
@ -3598,7 +3601,7 @@ void GameHandler::handleWardenData(network::Packet& packet) {
|
|||
decrypted.begin() + 3,
|
||||
decrypted.begin() + 3 + chunkSize);
|
||||
|
||||
LOG_INFO("Warden: MODULE_CACHE chunk ", chunkSize, " bytes, total ",
|
||||
LOG_DEBUG("Warden: MODULE_CACHE chunk ", chunkSize, " bytes, total ",
|
||||
wardenModuleData_.size(), "/", wardenModuleSize_);
|
||||
|
||||
// Check if module download is complete
|
||||
|
|
@ -3627,7 +3630,7 @@ void GameHandler::handleWardenData(network::Packet& packet) {
|
|||
std::ofstream wf(cachePath, std::ios::binary);
|
||||
if (wf) {
|
||||
wf.write(reinterpret_cast<const char*>(wardenModuleData_.data()), wardenModuleData_.size());
|
||||
LOG_INFO("Warden: Cached module to ", cachePath);
|
||||
LOG_DEBUG("Warden: Cached module to ", cachePath);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -3644,7 +3647,7 @@ void GameHandler::handleWardenData(network::Packet& packet) {
|
|||
// Send MODULE_OK (opcode 0x01)
|
||||
std::vector<uint8_t> resp = { 0x01 }; // WARDEN_CMSG_MODULE_OK
|
||||
sendWardenResponse(resp);
|
||||
LOG_INFO("Warden: Sent MODULE_OK");
|
||||
LOG_DEBUG("Warden: Sent MODULE_OK");
|
||||
}
|
||||
// No response for intermediate chunks
|
||||
break;
|
||||
|
|
@ -3670,7 +3673,7 @@ void GameHandler::handleWardenData(network::Packet& packet) {
|
|||
}
|
||||
|
||||
if (match) {
|
||||
LOG_INFO("Warden: Found matching CR entry for seed");
|
||||
LOG_DEBUG("Warden: Found matching CR entry for seed");
|
||||
|
||||
// Log the reply we're sending
|
||||
{
|
||||
|
|
@ -3678,7 +3681,7 @@ void GameHandler::handleWardenData(network::Packet& packet) {
|
|||
for (int i = 0; i < 20; i++) {
|
||||
char s[4]; snprintf(s, 4, "%02x", match->reply[i]); replyHex += s;
|
||||
}
|
||||
LOG_INFO("Warden: Sending pre-computed reply=", replyHex);
|
||||
LOG_DEBUG("Warden: Sending pre-computed reply=", replyHex);
|
||||
}
|
||||
|
||||
// Send HASH_RESULT (opcode 0x04 + 20-byte reply)
|
||||
|
|
@ -3693,7 +3696,7 @@ void GameHandler::handleWardenData(network::Packet& packet) {
|
|||
std::vector<uint8_t> newDecryptKey(match->serverKey, match->serverKey + 16);
|
||||
wardenCrypto_->replaceKeys(newEncryptKey, newDecryptKey);
|
||||
|
||||
LOG_INFO("Warden: Switched to CR key set");
|
||||
LOG_DEBUG("Warden: Switched to CR key set");
|
||||
|
||||
wardenState_ = WardenState::WAIT_CHECKS;
|
||||
break;
|
||||
|
|
@ -3721,7 +3724,7 @@ void GameHandler::handleWardenData(network::Packet& packet) {
|
|||
const auto& firstCR = wardenCREntries_[0];
|
||||
std::string expectedHex;
|
||||
for (int i = 0; i < 20; i++) { char s[4]; snprintf(s, 4, "%02x", firstCR.reply[i]); expectedHex += s; }
|
||||
LOG_INFO("Warden: Empirical test — expected reply from CR[0]=", expectedHex);
|
||||
LOG_DEBUG("Warden: Empirical test — expected reply from CR[0]=", expectedHex);
|
||||
|
||||
// Test 1: SHA1(moduleImage)
|
||||
{
|
||||
|
|
@ -3729,7 +3732,7 @@ void GameHandler::handleWardenData(network::Packet& packet) {
|
|||
auto h = auth::Crypto::sha1(data);
|
||||
bool match = (std::memcmp(h.data(), firstCR.reply, 20) == 0);
|
||||
std::string hex; for (auto b : h) { char s[4]; snprintf(s, 4, "%02x", b); hex += s; }
|
||||
LOG_INFO("Warden: SHA1(moduleImage)=", hex, match ? " MATCH!" : "");
|
||||
LOG_DEBUG("Warden: SHA1(moduleImage)=", hex, match ? " MATCH!" : "");
|
||||
}
|
||||
// Test 2: SHA1(seed || moduleImage)
|
||||
{
|
||||
|
|
@ -3739,7 +3742,7 @@ void GameHandler::handleWardenData(network::Packet& packet) {
|
|||
auto h = auth::Crypto::sha1(data);
|
||||
bool match = (std::memcmp(h.data(), firstCR.reply, 20) == 0);
|
||||
std::string hex; for (auto b : h) { char s[4]; snprintf(s, 4, "%02x", b); hex += s; }
|
||||
LOG_INFO("Warden: SHA1(seed||image)=", hex, match ? " MATCH!" : "");
|
||||
LOG_DEBUG("Warden: SHA1(seed||image)=", hex, match ? " MATCH!" : "");
|
||||
}
|
||||
// Test 3: SHA1(moduleImage || seed)
|
||||
{
|
||||
|
|
@ -3748,21 +3751,21 @@ void GameHandler::handleWardenData(network::Packet& packet) {
|
|||
auto h = auth::Crypto::sha1(data);
|
||||
bool match = (std::memcmp(h.data(), firstCR.reply, 20) == 0);
|
||||
std::string hex; for (auto b : h) { char s[4]; snprintf(s, 4, "%02x", b); hex += s; }
|
||||
LOG_INFO("Warden: SHA1(image||seed)=", hex, match ? " MATCH!" : "");
|
||||
LOG_DEBUG("Warden: SHA1(image||seed)=", hex, match ? " MATCH!" : "");
|
||||
}
|
||||
// Test 4: SHA1(decompressedData)
|
||||
{
|
||||
auto h = auth::Crypto::sha1(decompressedData);
|
||||
bool match = (std::memcmp(h.data(), firstCR.reply, 20) == 0);
|
||||
std::string hex; for (auto b : h) { char s[4]; snprintf(s, 4, "%02x", b); hex += s; }
|
||||
LOG_INFO("Warden: SHA1(decompressed)=", hex, match ? " MATCH!" : "");
|
||||
LOG_DEBUG("Warden: SHA1(decompressed)=", hex, match ? " MATCH!" : "");
|
||||
}
|
||||
// Test 5: SHA1(rawModuleData)
|
||||
{
|
||||
auto h = auth::Crypto::sha1(wardenModuleData_);
|
||||
bool match = (std::memcmp(h.data(), firstCR.reply, 20) == 0);
|
||||
std::string hex; for (auto b : h) { char s[4]; snprintf(s, 4, "%02x", b); hex += s; }
|
||||
LOG_INFO("Warden: SHA1(rawModule)=", hex, match ? " MATCH!" : "");
|
||||
LOG_DEBUG("Warden: SHA1(rawModule)=", hex, match ? " MATCH!" : "");
|
||||
}
|
||||
// Test 6: Check if all CR replies are the same (constant hash)
|
||||
{
|
||||
|
|
@ -3773,7 +3776,7 @@ void GameHandler::handleWardenData(network::Packet& packet) {
|
|||
break;
|
||||
}
|
||||
}
|
||||
LOG_INFO("Warden: All ", wardenCREntries_.size(), " CR replies identical? ", allSame ? "YES" : "NO");
|
||||
LOG_DEBUG("Warden: All ", wardenCREntries_.size(), " CR replies identical? ", allSame ? "YES" : "NO");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -3786,7 +3789,7 @@ void GameHandler::handleWardenData(network::Packet& packet) {
|
|||
{
|
||||
std::string hex;
|
||||
for (auto b : reply) { char s[4]; snprintf(s, 4, "%02x", b); hex += s; }
|
||||
LOG_INFO("Warden: Sending SHA1(moduleImage)=", hex);
|
||||
LOG_DEBUG("Warden: Sending SHA1(moduleImage)=", hex);
|
||||
}
|
||||
|
||||
// Send HASH_RESULT (opcode 0x04 + 20-byte hash)
|
||||
|
|
@ -3807,7 +3810,7 @@ void GameHandler::handleWardenData(network::Packet& packet) {
|
|||
wardenCrypto_->replaceKeys(ek, dk);
|
||||
for (auto& b : newEncryptKey) b = 0;
|
||||
for (auto& b : newDecryptKey) b = 0;
|
||||
LOG_INFO("Warden: Derived and applied key update from seed");
|
||||
LOG_DEBUG("Warden: Derived and applied key update from seed");
|
||||
}
|
||||
|
||||
wardenState_ = WardenState::WAIT_CHECKS;
|
||||
|
|
@ -3815,7 +3818,7 @@ void GameHandler::handleWardenData(network::Packet& packet) {
|
|||
}
|
||||
|
||||
case 0x02: { // WARDEN_SMSG_CHEAT_CHECKS_REQUEST
|
||||
LOG_INFO("Warden: CHEAT_CHECKS_REQUEST (", decrypted.size(), " bytes)");
|
||||
LOG_DEBUG("Warden: CHEAT_CHECKS_REQUEST (", decrypted.size(), " bytes)");
|
||||
|
||||
if (decrypted.size() < 3) {
|
||||
LOG_ERROR("Warden: CHEAT_CHECKS_REQUEST too short");
|
||||
|
|
@ -3833,14 +3836,14 @@ void GameHandler::handleWardenData(network::Packet& packet) {
|
|||
strings.emplace_back(reinterpret_cast<const char*>(decrypted.data() + pos), slen);
|
||||
pos += slen;
|
||||
}
|
||||
LOG_INFO("Warden: String table: ", strings.size(), " entries");
|
||||
LOG_DEBUG("Warden: String table: ", strings.size(), " entries");
|
||||
for (size_t i = 0; i < strings.size(); i++) {
|
||||
LOG_INFO("Warden: [", i, "] = \"", strings[i], "\"");
|
||||
LOG_DEBUG("Warden: [", i, "] = \"", strings[i], "\"");
|
||||
}
|
||||
|
||||
// XOR byte is the last byte of the packet
|
||||
uint8_t xorByte = decrypted.back();
|
||||
LOG_INFO("Warden: XOR byte = 0x", [&]{ char s[4]; snprintf(s,4,"%02x",xorByte); return std::string(s); }());
|
||||
LOG_DEBUG("Warden: XOR byte = 0x", [&]{ char s[4]; snprintf(s,4,"%02x",xorByte); return std::string(s); }());
|
||||
|
||||
// Check type enum indices
|
||||
enum CheckType { CT_MEM=0, CT_PAGE_A=1, CT_PAGE_B=2, CT_MPQ=3, CT_LUA=4,
|
||||
|
|
@ -3958,7 +3961,7 @@ void GameHandler::handleWardenData(network::Packet& packet) {
|
|||
pos++;
|
||||
checkCount++;
|
||||
|
||||
LOG_INFO("Warden: Check #", checkCount, " type=", checkTypeNames[ct],
|
||||
LOG_DEBUG("Warden: Check #", checkCount, " type=", checkTypeNames[ct],
|
||||
" at offset ", pos - 1);
|
||||
|
||||
switch (ct) {
|
||||
|
|
@ -3984,10 +3987,10 @@ void GameHandler::handleWardenData(network::Packet& packet) {
|
|||
| (uint32_t(decrypted[pos+2])<<16) | (uint32_t(decrypted[pos+3])<<24);
|
||||
pos += 4;
|
||||
uint8_t readLen = decrypted[pos++];
|
||||
LOG_INFO("Warden: MEM offset=0x", [&]{char s[12];snprintf(s,12,"%08x",offset);return std::string(s);}(),
|
||||
LOG_DEBUG("Warden: MEM offset=0x", [&]{char s[12];snprintf(s,12,"%08x",offset);return std::string(s);}(),
|
||||
" len=", (int)readLen);
|
||||
if (!moduleName.empty()) {
|
||||
LOG_INFO("Warden: MEM module=\"", moduleName, "\"");
|
||||
LOG_DEBUG("Warden: MEM module=\"", moduleName, "\"");
|
||||
}
|
||||
|
||||
// Lazy-load WoW.exe PE image on first MEM_CHECK
|
||||
|
|
@ -4001,7 +4004,7 @@ void GameHandler::handleWardenData(network::Packet& packet) {
|
|||
// Read bytes from PE image (includes patched runtime globals)
|
||||
std::vector<uint8_t> memBuf(readLen, 0);
|
||||
if (wardenMemory_->isLoaded() && wardenMemory_->readMemory(offset, readLen, memBuf.data())) {
|
||||
LOG_INFO("Warden: MEM_CHECK served from PE image");
|
||||
LOG_DEBUG("Warden: MEM_CHECK served from PE image");
|
||||
} else {
|
||||
LOG_WARNING("Warden: MEM_CHECK fallback to zeros for 0x",
|
||||
[&]{char s[12];snprintf(s,12,"%08x",offset);return std::string(s);}());
|
||||
|
|
@ -4054,7 +4057,7 @@ void GameHandler::handleWardenData(network::Packet& packet) {
|
|||
pageResult = 0x4A; // PatternFound
|
||||
}
|
||||
}
|
||||
LOG_INFO("Warden: PAGE_A request bytes=", consume,
|
||||
LOG_DEBUG("Warden: PAGE_A request bytes=", consume,
|
||||
" result=0x", [&]{char s[4];snprintf(s,4,"%02x",pageResult);return std::string(s);}());
|
||||
pos += consume;
|
||||
resultData.push_back(pageResult);
|
||||
|
|
@ -4093,7 +4096,7 @@ void GameHandler::handleWardenData(network::Packet& packet) {
|
|||
pageResult = 0x4A; // PatternFound
|
||||
}
|
||||
}
|
||||
LOG_INFO("Warden: PAGE_B request bytes=", consume,
|
||||
LOG_DEBUG("Warden: PAGE_B request bytes=", consume,
|
||||
" result=0x", [&]{char s[4];snprintf(s,4,"%02x",pageResult);return std::string(s);}());
|
||||
pos += consume;
|
||||
resultData.push_back(pageResult);
|
||||
|
|
@ -4104,7 +4107,7 @@ void GameHandler::handleWardenData(network::Packet& packet) {
|
|||
if (pos + 1 > checkEnd) { pos = checkEnd; break; }
|
||||
uint8_t strIdx = decrypted[pos++];
|
||||
std::string filePath = resolveWardenString(strIdx);
|
||||
LOG_INFO("Warden: MPQ file=\"", (filePath.empty() ? "?" : filePath), "\"");
|
||||
LOG_DEBUG("Warden: MPQ file=\"", (filePath.empty() ? "?" : filePath), "\"");
|
||||
|
||||
bool found = false;
|
||||
std::vector<uint8_t> hash(20, 0);
|
||||
|
|
@ -4150,7 +4153,7 @@ void GameHandler::handleWardenData(network::Packet& packet) {
|
|||
if (pos + 1 > checkEnd) { pos = checkEnd; break; }
|
||||
uint8_t strIdx = decrypted[pos++];
|
||||
std::string luaVar = resolveWardenString(strIdx);
|
||||
LOG_INFO("Warden: LUA str=\"", (luaVar.empty() ? "?" : luaVar), "\"");
|
||||
LOG_DEBUG("Warden: LUA str=\"", (luaVar.empty() ? "?" : luaVar), "\"");
|
||||
// Response: [uint8 result=0][uint16 len=0]
|
||||
// Lua string doesn't exist
|
||||
resultData.push_back(0x01); // not found
|
||||
|
|
@ -4162,7 +4165,7 @@ void GameHandler::handleWardenData(network::Packet& packet) {
|
|||
pos += 24; // skip seed + sha1
|
||||
uint8_t strIdx = decrypted[pos++];
|
||||
std::string driverName = resolveWardenString(strIdx);
|
||||
LOG_INFO("Warden: DRIVER=\"", (driverName.empty() ? "?" : driverName), "\"");
|
||||
LOG_DEBUG("Warden: DRIVER=\"", (driverName.empty() ? "?" : driverName), "\"");
|
||||
// Response: [uint8 result=1] (driver NOT found = clean)
|
||||
resultData.push_back(0x01);
|
||||
break;
|
||||
|
|
@ -4219,7 +4222,7 @@ void GameHandler::handleWardenData(network::Packet& packet) {
|
|||
}
|
||||
}
|
||||
|
||||
LOG_INFO("Warden: Parsed ", checkCount, " checks, result data size=", resultData.size());
|
||||
LOG_DEBUG("Warden: Parsed ", checkCount, " checks, result data size=", resultData.size());
|
||||
|
||||
// --- Compute checksum: XOR of 5 uint32s from SHA1(resultData) ---
|
||||
auto resultHash = auth::Crypto::sha1(resultData);
|
||||
|
|
@ -4244,18 +4247,18 @@ void GameHandler::handleWardenData(network::Packet& packet) {
|
|||
resp.push_back((checksum >> 24) & 0xFF);
|
||||
resp.insert(resp.end(), resultData.begin(), resultData.end());
|
||||
sendWardenResponse(resp);
|
||||
LOG_INFO("Warden: Sent CHEAT_CHECKS_RESULT (", resp.size(), " bytes, ",
|
||||
LOG_DEBUG("Warden: Sent CHEAT_CHECKS_RESULT (", resp.size(), " bytes, ",
|
||||
checkCount, " checks, checksum=0x",
|
||||
[&]{char s[12];snprintf(s,12,"%08x",checksum);return std::string(s);}(), ")");
|
||||
break;
|
||||
}
|
||||
|
||||
case 0x03: // WARDEN_SMSG_MODULE_INITIALIZE
|
||||
LOG_INFO("Warden: MODULE_INITIALIZE (", decrypted.size(), " bytes, no response needed)");
|
||||
LOG_DEBUG("Warden: MODULE_INITIALIZE (", decrypted.size(), " bytes, no response needed)");
|
||||
break;
|
||||
|
||||
default:
|
||||
LOG_INFO("Warden: Unknown opcode 0x", std::hex, (int)wardenOpcode, std::dec,
|
||||
LOG_DEBUG("Warden: Unknown opcode 0x", std::hex, (int)wardenOpcode, std::dec,
|
||||
" (state=", (int)wardenState_, ", size=", decrypted.size(), ")");
|
||||
break;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -529,78 +529,165 @@ bool WardenModule::parseExecutableFormat(const std::vector<uint8_t>& exeData) {
|
|||
std::cout << "[WardenModule] Allocated " << moduleSize_ << " bytes of executable memory at "
|
||||
<< moduleMemory_ << '\n';
|
||||
|
||||
// Parse copy/skip pairs (MaNGOS/TrinityCore format)
|
||||
// Format: repeated [2B copy_count][copy_count bytes data][2B skip_count]
|
||||
// Copy = copy from source to dest, Skip = advance dest pointer (zeros)
|
||||
// Terminates when copy_count == 0
|
||||
size_t pos = 4; // Skip 4-byte size header
|
||||
auto readU16LE = [&](size_t at) -> uint16_t {
|
||||
return static_cast<uint16_t>(exeData[at] | (exeData[at + 1] << 8));
|
||||
};
|
||||
|
||||
enum class PairFormat {
|
||||
CopyDataSkip, // [copy][data][skip]
|
||||
SkipCopyData, // [skip][copy][data]
|
||||
CopySkipData // [copy][skip][data]
|
||||
};
|
||||
|
||||
auto tryParsePairs = [&](PairFormat format,
|
||||
std::vector<uint8_t>& imageOut,
|
||||
size_t& relocPosOut,
|
||||
size_t& finalOffsetOut,
|
||||
int& pairCountOut) -> bool {
|
||||
imageOut.assign(moduleSize_, 0);
|
||||
size_t pos = 4; // Skip 4-byte final size header
|
||||
size_t destOffset = 0;
|
||||
int pairCount = 0;
|
||||
|
||||
while (pos + 2 <= exeData.size()) {
|
||||
// Read copy count (2 bytes LE)
|
||||
uint16_t copyCount = exeData[pos] | (exeData[pos + 1] << 8);
|
||||
uint16_t copyCount = 0;
|
||||
uint16_t skipCount = 0;
|
||||
|
||||
switch (format) {
|
||||
case PairFormat::CopyDataSkip: {
|
||||
copyCount = readU16LE(pos);
|
||||
pos += 2;
|
||||
|
||||
if (copyCount == 0) {
|
||||
break; // End of copy/skip pairs
|
||||
relocPosOut = pos;
|
||||
finalOffsetOut = destOffset;
|
||||
pairCountOut = pairCount;
|
||||
imageOut.resize(moduleSize_);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (copyCount > 0) {
|
||||
if (pos + copyCount > exeData.size()) {
|
||||
std::cerr << "[WardenModule] Copy section extends beyond data bounds" << '\n';
|
||||
#ifdef _WIN32
|
||||
VirtualFree(moduleMemory_, 0, MEM_RELEASE);
|
||||
#else
|
||||
munmap(moduleMemory_, moduleSize_);
|
||||
#endif
|
||||
moduleMemory_ = nullptr;
|
||||
if (pos + copyCount > exeData.size() || destOffset + copyCount > moduleSize_) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (destOffset + copyCount > moduleSize_) {
|
||||
std::cerr << "[WardenModule] Copy section exceeds module size" << '\n';
|
||||
#ifdef _WIN32
|
||||
VirtualFree(moduleMemory_, 0, MEM_RELEASE);
|
||||
#else
|
||||
munmap(moduleMemory_, moduleSize_);
|
||||
#endif
|
||||
moduleMemory_ = nullptr;
|
||||
return false;
|
||||
}
|
||||
|
||||
std::memcpy(
|
||||
static_cast<uint8_t*>(moduleMemory_) + destOffset,
|
||||
exeData.data() + pos,
|
||||
copyCount
|
||||
);
|
||||
std::memcpy(imageOut.data() + destOffset, exeData.data() + pos, copyCount);
|
||||
pos += copyCount;
|
||||
destOffset += copyCount;
|
||||
}
|
||||
|
||||
// Read skip count (2 bytes LE)
|
||||
uint16_t skipCount = 0;
|
||||
if (pos + 2 <= exeData.size()) {
|
||||
skipCount = exeData[pos] | (exeData[pos + 1] << 8);
|
||||
if (pos + 2 > exeData.size()) {
|
||||
return false;
|
||||
}
|
||||
skipCount = readU16LE(pos);
|
||||
pos += 2;
|
||||
break;
|
||||
}
|
||||
|
||||
// Advance dest pointer by skipCount (gaps are zero-filled from memset)
|
||||
case PairFormat::SkipCopyData: {
|
||||
if (pos + 4 > exeData.size()) {
|
||||
return false;
|
||||
}
|
||||
skipCount = readU16LE(pos);
|
||||
pos += 2;
|
||||
copyCount = readU16LE(pos);
|
||||
pos += 2;
|
||||
|
||||
if (skipCount == 0 && copyCount == 0) {
|
||||
relocPosOut = pos;
|
||||
finalOffsetOut = destOffset;
|
||||
pairCountOut = pairCount;
|
||||
imageOut.resize(moduleSize_);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (destOffset + skipCount > moduleSize_) {
|
||||
return false;
|
||||
}
|
||||
destOffset += skipCount;
|
||||
|
||||
pairCount++;
|
||||
std::cout << "[WardenModule] Pair " << pairCount << ": copy " << copyCount
|
||||
<< ", skip " << skipCount << " (dest offset=" << destOffset << ")" << '\n';
|
||||
if (pos + copyCount > exeData.size() || destOffset + copyCount > moduleSize_) {
|
||||
return false;
|
||||
}
|
||||
std::memcpy(imageOut.data() + destOffset, exeData.data() + pos, copyCount);
|
||||
pos += copyCount;
|
||||
destOffset += copyCount;
|
||||
break;
|
||||
}
|
||||
|
||||
// Save position — remaining decompressed data contains relocation entries
|
||||
relocDataOffset_ = pos;
|
||||
case PairFormat::CopySkipData: {
|
||||
if (pos + 4 > exeData.size()) {
|
||||
return false;
|
||||
}
|
||||
copyCount = readU16LE(pos);
|
||||
pos += 2;
|
||||
skipCount = readU16LE(pos);
|
||||
pos += 2;
|
||||
|
||||
std::cout << "[WardenModule] Parsed " << pairCount << " skip/copy pairs, final offset: "
|
||||
<< destOffset << "/" << finalCodeSize << '\n';
|
||||
if (copyCount == 0 && skipCount == 0) {
|
||||
relocPosOut = pos;
|
||||
finalOffsetOut = destOffset;
|
||||
pairCountOut = pairCount;
|
||||
imageOut.resize(moduleSize_);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (pos + copyCount > exeData.size() || destOffset + copyCount > moduleSize_) {
|
||||
return false;
|
||||
}
|
||||
std::memcpy(imageOut.data() + destOffset, exeData.data() + pos, copyCount);
|
||||
pos += copyCount;
|
||||
destOffset += copyCount;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (destOffset + skipCount > moduleSize_) {
|
||||
return false;
|
||||
}
|
||||
destOffset += skipCount;
|
||||
pairCount++;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
std::vector<uint8_t> parsedImage;
|
||||
size_t parsedRelocPos = 0;
|
||||
size_t parsedFinalOffset = 0;
|
||||
int parsedPairCount = 0;
|
||||
|
||||
PairFormat usedFormat = PairFormat::CopyDataSkip;
|
||||
bool parsed = tryParsePairs(PairFormat::CopyDataSkip, parsedImage, parsedRelocPos, parsedFinalOffset, parsedPairCount);
|
||||
if (!parsed) {
|
||||
usedFormat = PairFormat::SkipCopyData;
|
||||
parsed = tryParsePairs(PairFormat::SkipCopyData, parsedImage, parsedRelocPos, parsedFinalOffset, parsedPairCount);
|
||||
}
|
||||
if (!parsed) {
|
||||
usedFormat = PairFormat::CopySkipData;
|
||||
parsed = tryParsePairs(PairFormat::CopySkipData, parsedImage, parsedRelocPos, parsedFinalOffset, parsedPairCount);
|
||||
}
|
||||
|
||||
if (parsed) {
|
||||
std::memcpy(moduleMemory_, parsedImage.data(), parsedImage.size());
|
||||
relocDataOffset_ = parsedRelocPos;
|
||||
|
||||
const char* formatName = "copy/data/skip";
|
||||
if (usedFormat == PairFormat::SkipCopyData) formatName = "skip/copy/data";
|
||||
if (usedFormat == PairFormat::CopySkipData) formatName = "copy/skip/data";
|
||||
|
||||
std::cout << "[WardenModule] Parsed " << parsedPairCount << " pairs using format "
|
||||
<< formatName << ", final offset: " << parsedFinalOffset << "/" << finalCodeSize << '\n';
|
||||
std::cout << "[WardenModule] Relocation data starts at decompressed offset " << relocDataOffset_
|
||||
<< " (" << (exeData.size() - relocDataOffset_) << " bytes remaining)" << '\n';
|
||||
return true;
|
||||
}
|
||||
|
||||
// Fallback: copy raw payload (without the 4-byte size header) into module memory.
|
||||
// This keeps loading alive for servers where packet flow can continue with hash/check fallbacks.
|
||||
if (exeData.size() > 4) {
|
||||
size_t rawCopySize = std::min(moduleSize_, exeData.size() - 4);
|
||||
std::memcpy(moduleMemory_, exeData.data() + 4, rawCopySize);
|
||||
}
|
||||
relocDataOffset_ = 0;
|
||||
std::cerr << "[WardenModule] Could not parse copy/skip pairs (all known layouts failed); using raw payload fallback" << '\n';
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ static wowee::core::LogLevel readLogLevelFromEnv() {
|
|||
if (level == "debug") return wowee::core::LogLevel::DEBUG;
|
||||
if (level == "info") return wowee::core::LogLevel::INFO;
|
||||
if (level == "warn" || level == "warning") return wowee::core::LogLevel::WARNING;
|
||||
if (level == "error") return wowee::core::LogLevel::ERROR;
|
||||
if (level == "error") return wowee::core::kLogLevelError;
|
||||
if (level == "fatal") return wowee::core::LogLevel::FATAL;
|
||||
return wowee::core::LogLevel::WARNING;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -458,12 +458,27 @@ bool MPQManager::loadPatchArchives() {
|
|||
{"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 =
|
||||
(archive.size() >= 10) &&
|
||||
(toLowerCopy(archive).rfind("patch-", 0) != 0) && // not patch-*.MPQ
|
||||
(toLowerCopy(archive).rfind("patch.", 0) != 0); // not patch.MPQ
|
||||
(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;
|
||||
}
|
||||
|
|
@ -471,9 +486,10 @@ bool MPQManager::loadPatchArchives() {
|
|||
continue;
|
||||
}
|
||||
|
||||
std::string fullPath = dataPath + "/" + archive;
|
||||
if (std::filesystem::exists(fullPath)) {
|
||||
if (loadArchive(fullPath, priority)) {
|
||||
// Case-insensitive file lookup
|
||||
auto it = lowerToActual.find(lowerArchive);
|
||||
if (it != lowerToActual.end()) {
|
||||
if (loadArchive(it->second, priority)) {
|
||||
loadedPatches++;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -65,6 +65,23 @@ std::optional<float> selectClosestFloor(const std::optional<float>& a,
|
|||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<float> selectReachableFloor3(const std::optional<float>& a,
|
||||
const std::optional<float>& b,
|
||||
const std::optional<float>& c,
|
||||
float refZ,
|
||||
float maxStepUp) {
|
||||
std::optional<float> best;
|
||||
auto consider = [&](const std::optional<float>& h) {
|
||||
if (!h) return;
|
||||
if (*h > refZ + maxStepUp) return;
|
||||
if (!best || *h > *best) best = *h;
|
||||
};
|
||||
consider(a);
|
||||
consider(b);
|
||||
consider(c);
|
||||
return best;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
CameraController::CameraController(Camera* cam) : camera(cam) {
|
||||
|
|
@ -126,6 +143,8 @@ void CameraController::update(float deltaTime) {
|
|||
if (!enabled || !camera) {
|
||||
return;
|
||||
}
|
||||
// Keep physics integration stable during render hitches to avoid floor tunneling.
|
||||
const float physicsDeltaTime = std::min(deltaTime, 1.0f / 30.0f);
|
||||
|
||||
// During taxi flights, skip movement logic but keep camera orbit/zoom controls.
|
||||
if (externalFollow_) {
|
||||
|
|
@ -360,6 +379,7 @@ void CameraController::update(float deltaTime) {
|
|||
if (thirdPerson && followTarget) {
|
||||
// Move the follow target (character position) instead of the camera
|
||||
glm::vec3 targetPos = *followTarget;
|
||||
const glm::vec3 prevTargetPos = *followTarget;
|
||||
if (!externalFollow_) {
|
||||
if (wmoRenderer) {
|
||||
wmoRenderer->setCollisionFocus(targetPos, COLLISION_FOCUS_RADIUS_THIRD_PERSON);
|
||||
|
|
@ -403,6 +423,68 @@ void CameraController::update(float deltaTime) {
|
|||
float depthFromFeet = (*waterH - targetPos.z);
|
||||
inWater = (floorH && ((*waterH - *floorH) >= MIN_SWIM_WATER_DEPTH)) ||
|
||||
(!floorH && (depthFromFeet >= MIN_SWIM_WATER_DEPTH));
|
||||
|
||||
// Ramp exit assist: when swimming forward near the surface toward a
|
||||
// reachable floor (dock/shore ramp), switch to walking sooner.
|
||||
if (swimming && inWater && floorH && nowForward) {
|
||||
float floorDelta = *floorH - targetPos.z;
|
||||
float waterOverFloor = *waterH - *floorH;
|
||||
bool nearSurface = depthFromFeet <= 1.45f;
|
||||
bool reachableRamp = (floorDelta >= -0.30f && floorDelta <= 1.10f);
|
||||
bool shallowRampWater = waterOverFloor <= 1.55f;
|
||||
bool notDiving = forward3D.z > -0.20f;
|
||||
if (nearSurface && reachableRamp && shallowRampWater && notDiving) {
|
||||
inWater = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Forward plank/ramp assist: sample structure floors ahead so water exit
|
||||
// can happen when the ramp is in front of us (not only under our feet).
|
||||
if (swimming && inWater && nowForward && forward3D.z > -0.20f) {
|
||||
auto queryFloorAt = [&](float x, float y, float probeZ) -> std::optional<float> {
|
||||
std::optional<float> best;
|
||||
if (terrainManager) {
|
||||
best = terrainManager->getHeightAt(x, y);
|
||||
}
|
||||
if (wmoRenderer) {
|
||||
float nz = 1.0f;
|
||||
auto wh = wmoRenderer->getFloorHeight(x, y, probeZ, &nz);
|
||||
if (wh && nz >= 0.40f && (!best || *wh > *best)) best = wh;
|
||||
}
|
||||
if (m2Renderer && !externalFollow_) {
|
||||
float nz = 1.0f;
|
||||
auto mh = m2Renderer->getFloorHeight(x, y, probeZ, &nz);
|
||||
if (mh && nz >= 0.35f && (!best || *mh > *best)) best = mh;
|
||||
}
|
||||
return best;
|
||||
};
|
||||
|
||||
glm::vec2 fwd2(forward.x, forward.y);
|
||||
float fwdLen = glm::length(fwd2);
|
||||
if (fwdLen > 1e-4f) {
|
||||
fwd2 /= fwdLen;
|
||||
std::optional<float> aheadFloor;
|
||||
const float probeZ = targetPos.z + 2.0f;
|
||||
const float dists[] = {0.45f, 0.90f, 1.25f};
|
||||
for (float d : dists) {
|
||||
float sx = targetPos.x + fwd2.x * d;
|
||||
float sy = targetPos.y + fwd2.y * d;
|
||||
auto h = queryFloorAt(sx, sy, probeZ);
|
||||
if (h && (!aheadFloor || *h > *aheadFloor)) aheadFloor = h;
|
||||
}
|
||||
|
||||
if (aheadFloor) {
|
||||
float floorDelta = *aheadFloor - targetPos.z;
|
||||
float waterOverFloor = *waterH - *aheadFloor;
|
||||
bool nearSurface = depthFromFeet <= 1.65f;
|
||||
bool reachableRamp = (floorDelta >= -0.35f && floorDelta <= 1.25f);
|
||||
bool shallowRampWater = waterOverFloor <= 1.75f;
|
||||
if (nearSurface && reachableRamp && shallowRampWater) {
|
||||
inWater = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Keep swimming through water-data gaps at chunk boundaries.
|
||||
|
|
@ -442,7 +524,7 @@ void CameraController::update(float deltaTime) {
|
|||
|
||||
if (glm::length(swimMove) > 0.001f) {
|
||||
swimMove = glm::normalize(swimMove);
|
||||
targetPos += swimMove * swimSpeed * deltaTime;
|
||||
targetPos += swimMove * swimSpeed * physicsDeltaTime;
|
||||
}
|
||||
|
||||
// Spacebar = swim up (continuous, not a jump)
|
||||
|
|
@ -451,7 +533,7 @@ void CameraController::update(float deltaTime) {
|
|||
verticalVelocity = SWIM_BUOYANCY;
|
||||
} else {
|
||||
// Gentle sink when not pressing space
|
||||
verticalVelocity += SWIM_GRAVITY * deltaTime;
|
||||
verticalVelocity += SWIM_GRAVITY * physicsDeltaTime;
|
||||
if (verticalVelocity < SWIM_SINK_SPEED) {
|
||||
verticalVelocity = SWIM_SINK_SPEED;
|
||||
}
|
||||
|
|
@ -459,15 +541,15 @@ void CameraController::update(float deltaTime) {
|
|||
// you afloat unless you're intentionally diving.
|
||||
if (!diveIntent) {
|
||||
float surfaceErr = (waterSurfaceZ - targetPos.z);
|
||||
verticalVelocity += surfaceErr * 7.0f * deltaTime;
|
||||
verticalVelocity *= std::max(0.0f, 1.0f - 3.2f * deltaTime);
|
||||
verticalVelocity += surfaceErr * 7.0f * physicsDeltaTime;
|
||||
verticalVelocity *= std::max(0.0f, 1.0f - 3.2f * physicsDeltaTime);
|
||||
if (std::abs(surfaceErr) < 0.06f && std::abs(verticalVelocity) < 0.35f) {
|
||||
verticalVelocity = 0.0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
targetPos.z += verticalVelocity * deltaTime;
|
||||
targetPos.z += verticalVelocity * physicsDeltaTime;
|
||||
|
||||
// Don't rise above water surface
|
||||
if (waterH && targetPos.z > *waterH - WATER_SURFACE_OFFSET) {
|
||||
|
|
@ -486,17 +568,42 @@ void CameraController::update(float deltaTime) {
|
|||
if (updateFloorCache) {
|
||||
floorQueryFrameCounter = 0;
|
||||
lastFloorQueryPos = targetPos;
|
||||
constexpr float MAX_SWIM_FLOOR_ABOVE_FEET = 0.25f;
|
||||
constexpr float MIN_SWIM_CEILING_ABOVE_FEET = 0.30f;
|
||||
constexpr float MAX_SWIM_CEILING_ABOVE_FEET = 1.80f;
|
||||
std::optional<float> ceilingH;
|
||||
auto considerFloor = [&](const std::optional<float>& h) {
|
||||
if (!h) return;
|
||||
// Swim-floor guard: only accept surfaces at or very slightly above feet.
|
||||
if (*h <= targetPos.z + MAX_SWIM_FLOOR_ABOVE_FEET) {
|
||||
if (!floorH || *h > *floorH) floorH = h;
|
||||
}
|
||||
// Swim-ceiling guard: detect structures just above feet so upward swim
|
||||
// can't clip through docks/platform undersides.
|
||||
float dz = *h - targetPos.z;
|
||||
if (dz >= MIN_SWIM_CEILING_ABOVE_FEET && dz <= MAX_SWIM_CEILING_ABOVE_FEET) {
|
||||
if (!ceilingH || *h < *ceilingH) ceilingH = h;
|
||||
}
|
||||
};
|
||||
|
||||
if (terrainManager) {
|
||||
floorH = terrainManager->getHeightAt(targetPos.x, targetPos.y);
|
||||
considerFloor(terrainManager->getHeightAt(targetPos.x, targetPos.y));
|
||||
}
|
||||
if (wmoRenderer) {
|
||||
auto wh = wmoRenderer->getFloorHeight(targetPos.x, targetPos.y, targetPos.z + 2.0f);
|
||||
if (wh && (!floorH || *wh > *floorH)) floorH = wh;
|
||||
considerFloor(wh);
|
||||
}
|
||||
if (m2Renderer && !externalFollow_) {
|
||||
auto mh = m2Renderer->getFloorHeight(targetPos.x, targetPos.y, targetPos.z);
|
||||
if (mh && (!floorH || *mh > *floorH)) floorH = mh;
|
||||
auto mh = m2Renderer->getFloorHeight(targetPos.x, targetPos.y, targetPos.z + 2.0f);
|
||||
considerFloor(mh);
|
||||
}
|
||||
|
||||
if (ceilingH && verticalVelocity > 0.0f) {
|
||||
float ceilingLimit = *ceilingH - 0.35f;
|
||||
if (targetPos.z > ceilingLimit) {
|
||||
targetPos.z = ceilingLimit;
|
||||
verticalVelocity = 0.0f;
|
||||
}
|
||||
}
|
||||
|
||||
cachedFloorHeight = floorH;
|
||||
|
|
@ -557,7 +664,7 @@ void CameraController::update(float deltaTime) {
|
|||
|
||||
if (glm::length(movement) > 0.001f) {
|
||||
movement = glm::normalize(movement);
|
||||
targetPos += movement * speed * deltaTime;
|
||||
targetPos += movement * speed * physicsDeltaTime;
|
||||
}
|
||||
|
||||
// Jump with input buffering and coyote time
|
||||
|
|
@ -572,12 +679,12 @@ void CameraController::update(float deltaTime) {
|
|||
coyoteTimer = 0.0f;
|
||||
}
|
||||
|
||||
jumpBufferTimer -= deltaTime;
|
||||
coyoteTimer -= deltaTime;
|
||||
jumpBufferTimer -= physicsDeltaTime;
|
||||
coyoteTimer -= physicsDeltaTime;
|
||||
|
||||
// Apply gravity
|
||||
verticalVelocity += gravity * deltaTime;
|
||||
targetPos.z += verticalVelocity * deltaTime;
|
||||
verticalVelocity += gravity * physicsDeltaTime;
|
||||
targetPos.z += verticalVelocity * physicsDeltaTime;
|
||||
}
|
||||
} else {
|
||||
// External follow (e.g., taxi): trust server position without grounding.
|
||||
|
|
@ -589,16 +696,23 @@ void CameraController::update(float deltaTime) {
|
|||
// Refresh inside-WMO state before collision/grounding so we don't use stale
|
||||
// terrain-first caches while entering enclosed tunnel/building spaces.
|
||||
if (wmoRenderer && !externalFollow_) {
|
||||
const float insideDist = glm::length(targetPos - lastInsideStateCheckPos_);
|
||||
if (++insideStateCheckCounter_ >= 2 || insideDist > 0.35f) {
|
||||
insideStateCheckCounter_ = 0;
|
||||
lastInsideStateCheckPos_ = targetPos;
|
||||
|
||||
bool prevInside = cachedInsideWMO;
|
||||
bool prevInsideInterior = cachedInsideInteriorWMO;
|
||||
cachedInsideWMO = wmoRenderer->isInsideWMO(targetPos.x, targetPos.y, targetPos.z + 1.0f, nullptr);
|
||||
cachedInsideInteriorWMO = wmoRenderer->isInsideInteriorWMO(targetPos.x, targetPos.y, targetPos.z + 1.0f);
|
||||
cachedInsideInteriorWMO = cachedInsideWMO &&
|
||||
wmoRenderer->isInsideInteriorWMO(targetPos.x, targetPos.y, targetPos.z + 1.0f);
|
||||
if (cachedInsideWMO != prevInside || cachedInsideInteriorWMO != prevInsideInterior) {
|
||||
hasCachedFloor_ = false;
|
||||
hasCachedCamFloor = false;
|
||||
cachedPivotLift_ = 0.0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sweep collisions in small steps to reduce tunneling through thin walls/floors.
|
||||
// Skip entirely when stationary to avoid wasting collision calls.
|
||||
|
|
@ -654,15 +768,17 @@ void CameraController::update(float deltaTime) {
|
|||
// WMO tunnel/bridge ramps are often steeper than outdoor terrain ramps.
|
||||
constexpr float MIN_WALKABLE_NORMAL_TERRAIN = 0.7f; // ~45°
|
||||
constexpr float MIN_WALKABLE_NORMAL_WMO = 0.45f; // allow tunnel ramps
|
||||
constexpr float MIN_WALKABLE_NORMAL_M2 = 0.45f; // allow bridge/deck ramps
|
||||
|
||||
std::optional<float> groundH;
|
||||
std::optional<float> centerTerrainH;
|
||||
std::optional<float> centerWmoH;
|
||||
std::optional<float> centerM2H;
|
||||
{
|
||||
// Collision cache: skip expensive checks if barely moved (15cm threshold)
|
||||
float distMoved = glm::length(glm::vec2(targetPos.x, targetPos.y) -
|
||||
glm::vec2(lastCollisionCheckPos_.x, lastCollisionCheckPos_.y));
|
||||
bool useCached = hasCachedFloor_ && distMoved < COLLISION_CACHE_DISTANCE;
|
||||
bool useCached = grounded && hasCachedFloor_ && distMoved < COLLISION_CACHE_DISTANCE;
|
||||
if (useCached) {
|
||||
// Never trust cached ground while actively descending or when
|
||||
// vertical drift from cached floor is meaningful.
|
||||
|
|
@ -678,6 +794,7 @@ void CameraController::update(float deltaTime) {
|
|||
// Full collision check
|
||||
std::optional<float> terrainH;
|
||||
std::optional<float> wmoH;
|
||||
std::optional<float> m2H;
|
||||
if (terrainManager) {
|
||||
terrainH = terrainManager->getHeightAt(targetPos.x, targetPos.y);
|
||||
}
|
||||
|
|
@ -689,6 +806,13 @@ void CameraController::update(float deltaTime) {
|
|||
if (wmoRenderer) {
|
||||
wmoH = wmoRenderer->getFloorHeight(targetPos.x, targetPos.y, wmoProbeZ, &wmoNormalZ);
|
||||
}
|
||||
if (m2Renderer && !externalFollow_) {
|
||||
float m2NormalZ = 1.0f;
|
||||
m2H = m2Renderer->getFloorHeight(targetPos.x, targetPos.y, wmoProbeZ, &m2NormalZ);
|
||||
if (m2H && m2NormalZ < MIN_WALKABLE_NORMAL_M2) {
|
||||
m2H = std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
// Reject steep WMO slopes
|
||||
float minWalkableWmo = cachedInsideWMO ? MIN_WALKABLE_NORMAL_WMO : MIN_WALKABLE_NORMAL_TERRAIN;
|
||||
|
|
@ -704,6 +828,7 @@ void CameraController::update(float deltaTime) {
|
|||
}
|
||||
centerTerrainH = terrainH;
|
||||
centerWmoH = wmoH;
|
||||
centerM2H = m2H;
|
||||
|
||||
// Guard against extremely bad WMO void ramps, but keep normal tunnel
|
||||
// transitions valid. Only reject when the WMO sample is implausibly far
|
||||
|
|
@ -739,10 +864,10 @@ void CameraController::update(float deltaTime) {
|
|||
// to avoid oscillating between top terrain and deep WMO floors.
|
||||
groundH = selectClosestFloor(terrainH, wmoH, targetPos.z);
|
||||
} else {
|
||||
groundH = selectReachableFloor(terrainH, wmoH, targetPos.z, stepUpBudget);
|
||||
groundH = selectReachableFloor3(terrainH, wmoH, m2H, targetPos.z, stepUpBudget);
|
||||
}
|
||||
} else {
|
||||
groundH = selectReachableFloor(terrainH, wmoH, targetPos.z, stepUpBudget);
|
||||
groundH = selectReachableFloor3(terrainH, wmoH, m2H, targetPos.z, stepUpBudget);
|
||||
}
|
||||
|
||||
// Update cache
|
||||
|
|
@ -759,13 +884,29 @@ void CameraController::update(float deltaTime) {
|
|||
// Transition safety: if no reachable floor was selected, choose the higher
|
||||
// of terrain/WMO center surfaces when it is still near the player.
|
||||
// This avoids dropping into void gaps at terrain<->WMO seams.
|
||||
const bool nearWmoSpace = cachedInsideWMO || centerWmoH.has_value();
|
||||
bool nearStructureSpace = nearWmoSpace || centerM2H.has_value();
|
||||
if (!nearStructureSpace && hasRealGround_) {
|
||||
// Plank-gap hint: center probes can miss sparse bridge segments.
|
||||
// Probe once around last known ground before allowing a full drop.
|
||||
if (wmoRenderer) {
|
||||
auto whHint = wmoRenderer->getFloorHeight(targetPos.x, targetPos.y, lastGroundZ + 1.5f);
|
||||
if (whHint && std::abs(*whHint - lastGroundZ) <= 2.0f) nearStructureSpace = true;
|
||||
}
|
||||
if (!nearStructureSpace && m2Renderer && !externalFollow_) {
|
||||
float nz = 1.0f;
|
||||
auto mhHint = m2Renderer->getFloorHeight(targetPos.x, targetPos.y, lastGroundZ + 1.5f, &nz);
|
||||
if (mhHint && nz >= MIN_WALKABLE_NORMAL_M2 &&
|
||||
std::abs(*mhHint - lastGroundZ) <= 2.0f) nearStructureSpace = true;
|
||||
}
|
||||
}
|
||||
if (!groundH) {
|
||||
auto highestCenter = selectHighestFloor(centerTerrainH, centerWmoH, std::nullopt);
|
||||
auto highestCenter = selectHighestFloor(centerTerrainH, centerWmoH, centerM2H);
|
||||
if (highestCenter) {
|
||||
float dz = targetPos.z - *highestCenter;
|
||||
// Keep this fallback narrow: only for WMO seam cases, or very short
|
||||
// transient misses while still almost touching the last floor.
|
||||
bool allowFallback = cachedInsideWMO || (noGroundTimer_ < 0.10f && dz < 0.6f);
|
||||
bool allowFallback = nearStructureSpace || (noGroundTimer_ < 0.10f && dz < 0.6f);
|
||||
if (allowFallback && dz >= -0.5f && dz < 2.0f) {
|
||||
groundH = highestCenter;
|
||||
}
|
||||
|
|
@ -774,7 +915,7 @@ void CameraController::update(float deltaTime) {
|
|||
|
||||
// Continuity guard only for WMO seam overlap: avoid instantly switching to a
|
||||
// much lower floor sample at tunnel mouths (bad WMO ramp chains into void).
|
||||
if (groundH && hasRealGround_ && cachedInsideWMO && !cachedInsideInteriorWMO) {
|
||||
if (groundH && hasRealGround_ && nearWmoSpace && !cachedInsideInteriorWMO) {
|
||||
float dropFromLast = lastGroundZ - *groundH;
|
||||
if (dropFromLast > 1.5f) {
|
||||
if (centerTerrainH && *centerTerrainH > *groundH + 1.5f) {
|
||||
|
|
@ -785,7 +926,7 @@ void CameraController::update(float deltaTime) {
|
|||
|
||||
// Seam stability: while overlapping WMO shells, cap how fast floor height can
|
||||
// step downward in a single frame to avoid following bad ramp samples into void.
|
||||
if (groundH && cachedInsideWMO && !cachedInsideInteriorWMO && lastGroundZ > 1.0f) {
|
||||
if (groundH && nearWmoSpace && !cachedInsideInteriorWMO && lastGroundZ > 1.0f) {
|
||||
float maxDropPerFrame = (verticalVelocity < -8.0f) ? 2.0f : 0.60f;
|
||||
float minAllowed = lastGroundZ - maxDropPerFrame;
|
||||
// Extra seam guard: outside interior groups, avoid accepting floors that
|
||||
|
|
@ -802,9 +943,19 @@ void CameraController::update(float deltaTime) {
|
|||
}
|
||||
}
|
||||
|
||||
// Structure continuity guard: if a floor query suddenly jumps far below
|
||||
// recent support while near dock/bridge geometry, keep a conservative
|
||||
// support height to avoid dropping through sparse collision seams.
|
||||
if (groundH && hasRealGround_ && nearStructureSpace && !nowJump) {
|
||||
float dropFromLast = lastGroundZ - *groundH;
|
||||
if (dropFromLast > 1.0f && verticalVelocity > -6.0f) {
|
||||
*groundH = std::max(*groundH, lastGroundZ - 0.20f);
|
||||
}
|
||||
}
|
||||
|
||||
// 1b. Multi-sample WMO floors when in/near WMO space to avoid
|
||||
// falling through narrow board/plank gaps where center ray misses.
|
||||
if (wmoRenderer && cachedInsideWMO) {
|
||||
if (wmoRenderer && nearWmoSpace) {
|
||||
constexpr float WMO_FOOTPRINT = 0.35f;
|
||||
const glm::vec2 wmoOffsets[] = {
|
||||
{0.0f, 0.0f},
|
||||
|
|
@ -827,7 +978,7 @@ void CameraController::update(float deltaTime) {
|
|||
|
||||
// Keep to nearby, walkable steps only.
|
||||
if (*wh > targetPos.z + stepUpBudget) continue;
|
||||
if (*wh < targetPos.z - 2.5f) continue;
|
||||
if (*wh < lastGroundZ - 3.5f) continue;
|
||||
|
||||
if (!groundH || *wh > *groundH) {
|
||||
groundH = wh;
|
||||
|
|
@ -835,14 +986,112 @@ void CameraController::update(float deltaTime) {
|
|||
}
|
||||
}
|
||||
|
||||
// WMO recovery probe: when no floor is found while descending, do a wider
|
||||
// footprint sample around the player to catch narrow plank/stair misses.
|
||||
if (!groundH && wmoRenderer && hasRealGround_ && verticalVelocity <= 0.0f) {
|
||||
constexpr float RESCUE_FOOTPRINT = 0.65f;
|
||||
const glm::vec2 rescueOffsets[] = {
|
||||
{0.0f, 0.0f},
|
||||
{ RESCUE_FOOTPRINT, 0.0f}, {-RESCUE_FOOTPRINT, 0.0f},
|
||||
{0.0f, RESCUE_FOOTPRINT}, {0.0f, -RESCUE_FOOTPRINT},
|
||||
{ RESCUE_FOOTPRINT, RESCUE_FOOTPRINT},
|
||||
{ RESCUE_FOOTPRINT, -RESCUE_FOOTPRINT},
|
||||
{-RESCUE_FOOTPRINT, RESCUE_FOOTPRINT},
|
||||
{-RESCUE_FOOTPRINT, -RESCUE_FOOTPRINT}
|
||||
};
|
||||
float rescueProbeZ = std::max(lastGroundZ, targetPos.z) + stepUpBudget + 1.2f;
|
||||
std::optional<float> rescueFloor;
|
||||
for (const auto& o : rescueOffsets) {
|
||||
float nz = 1.0f;
|
||||
auto wh = wmoRenderer->getFloorHeight(targetPos.x + o.x, targetPos.y + o.y, rescueProbeZ, &nz);
|
||||
if (!wh) continue;
|
||||
if (nz < MIN_WALKABLE_NORMAL_WMO) continue;
|
||||
if (*wh > lastGroundZ + stepUpBudget + 0.75f) continue;
|
||||
if (*wh < lastGroundZ - 6.0f) continue;
|
||||
if (!rescueFloor || *wh > *rescueFloor) {
|
||||
rescueFloor = wh;
|
||||
}
|
||||
}
|
||||
if (rescueFloor) {
|
||||
groundH = rescueFloor;
|
||||
}
|
||||
}
|
||||
|
||||
// M2 recovery probe: Booty Bay-style wooden platforms can be represented
|
||||
// as M2 collision where center probes intermittently miss.
|
||||
if (!groundH && m2Renderer && !externalFollow_ && hasRealGround_ && verticalVelocity <= 0.0f) {
|
||||
constexpr float RESCUE_FOOTPRINT = 0.75f;
|
||||
const glm::vec2 rescueOffsets[] = {
|
||||
{0.0f, 0.0f},
|
||||
{ RESCUE_FOOTPRINT, 0.0f}, {-RESCUE_FOOTPRINT, 0.0f},
|
||||
{0.0f, RESCUE_FOOTPRINT}, {0.0f, -RESCUE_FOOTPRINT},
|
||||
{ RESCUE_FOOTPRINT, RESCUE_FOOTPRINT},
|
||||
{ RESCUE_FOOTPRINT, -RESCUE_FOOTPRINT},
|
||||
{-RESCUE_FOOTPRINT, RESCUE_FOOTPRINT},
|
||||
{-RESCUE_FOOTPRINT, -RESCUE_FOOTPRINT}
|
||||
};
|
||||
float rescueProbeZ = std::max(lastGroundZ, targetPos.z) + stepUpBudget + 1.4f;
|
||||
std::optional<float> rescueFloor;
|
||||
for (const auto& o : rescueOffsets) {
|
||||
float nz = 1.0f;
|
||||
auto mh = m2Renderer->getFloorHeight(targetPos.x + o.x, targetPos.y + o.y, rescueProbeZ, &nz);
|
||||
if (!mh) continue;
|
||||
if (nz < MIN_WALKABLE_NORMAL_M2) continue;
|
||||
if (*mh > lastGroundZ + stepUpBudget + 0.90f) continue;
|
||||
if (*mh < lastGroundZ - 6.0f) continue;
|
||||
if (!rescueFloor || *mh > *rescueFloor) {
|
||||
rescueFloor = mh;
|
||||
}
|
||||
}
|
||||
if (rescueFloor) {
|
||||
groundH = rescueFloor;
|
||||
}
|
||||
}
|
||||
|
||||
// Path recovery probe: sample structure floors along the movement segment
|
||||
// (prev -> current) to catch narrow plank gaps missed at endpoints.
|
||||
if (!groundH && hasRealGround_ && (wmoRenderer || (m2Renderer && !externalFollow_))) {
|
||||
std::optional<float> segmentFloor;
|
||||
const float probeZ = std::max(lastGroundZ, targetPos.z) + stepUpBudget + 1.2f;
|
||||
const float ts[] = {0.25f, 0.5f, 0.75f};
|
||||
for (float t : ts) {
|
||||
float sx = prevTargetPos.x + (targetPos.x - prevTargetPos.x) * t;
|
||||
float sy = prevTargetPos.y + (targetPos.y - prevTargetPos.y) * t;
|
||||
|
||||
if (wmoRenderer) {
|
||||
float nz = 1.0f;
|
||||
auto wh = wmoRenderer->getFloorHeight(sx, sy, probeZ, &nz);
|
||||
if (wh && nz >= MIN_WALKABLE_NORMAL_WMO &&
|
||||
*wh <= lastGroundZ + stepUpBudget + 0.9f &&
|
||||
*wh >= lastGroundZ - 3.0f) {
|
||||
if (!segmentFloor || *wh > *segmentFloor) segmentFloor = wh;
|
||||
}
|
||||
}
|
||||
if (m2Renderer && !externalFollow_) {
|
||||
float nz = 1.0f;
|
||||
auto mh = m2Renderer->getFloorHeight(sx, sy, probeZ, &nz);
|
||||
if (mh && nz >= MIN_WALKABLE_NORMAL_M2 &&
|
||||
*mh <= lastGroundZ + stepUpBudget + 0.9f &&
|
||||
*mh >= lastGroundZ - 3.0f) {
|
||||
if (!segmentFloor || *mh > *segmentFloor) segmentFloor = mh;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (segmentFloor) {
|
||||
groundH = segmentFloor;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Multi-sample for M2 objects (rugs, planks, bridges, ships) —
|
||||
// these are narrow and need offset probes to detect reliably.
|
||||
if (m2Renderer && !externalFollow_) {
|
||||
constexpr float FOOTPRINT = 0.4f;
|
||||
constexpr float FOOTPRINT = 0.6f;
|
||||
const glm::vec2 offsets[] = {
|
||||
{0.0f, 0.0f},
|
||||
{FOOTPRINT, 0.0f}, {-FOOTPRINT, 0.0f},
|
||||
{0.0f, FOOTPRINT}, {0.0f, -FOOTPRINT}
|
||||
{0.0f, FOOTPRINT}, {0.0f, -FOOTPRINT},
|
||||
{FOOTPRINT, FOOTPRINT}, {FOOTPRINT, -FOOTPRINT},
|
||||
{-FOOTPRINT, FOOTPRINT}, {-FOOTPRINT, -FOOTPRINT}
|
||||
};
|
||||
float m2ProbeZ = std::max(targetPos.z, lastGroundZ) + 6.0f;
|
||||
for (const auto& o : offsets) {
|
||||
|
|
@ -895,15 +1144,33 @@ void CameraController::update(float deltaTime) {
|
|||
}
|
||||
} else {
|
||||
hasRealGround_ = false;
|
||||
noGroundTimer_ += deltaTime;
|
||||
noGroundTimer_ += physicsDeltaTime;
|
||||
|
||||
float dropFromLastGround = lastGroundZ - targetPos.z;
|
||||
bool seamSizedGap = dropFromLastGround <= 0.35f;
|
||||
bool seamSizedGap = dropFromLastGround <= (nearStructureSpace ? 2.5f : 0.35f);
|
||||
if (noGroundTimer_ < NO_GROUND_GRACE && seamSizedGap) {
|
||||
// Micro-gap grace only: keep continuity for tiny seam misses,
|
||||
// but never convert air into persistent ground.
|
||||
targetPos.z = std::max(targetPos.z, lastGroundZ - 0.10f);
|
||||
// Near WMO floors, prefer continuity over falling on transient
|
||||
// floor-query misses (stairs/planks/portal seams).
|
||||
float maxSlip = nearStructureSpace ? 1.0f : 0.10f;
|
||||
targetPos.z = std::max(targetPos.z, lastGroundZ - maxSlip);
|
||||
if (nearStructureSpace && verticalVelocity < -2.0f) {
|
||||
verticalVelocity = -2.0f;
|
||||
}
|
||||
grounded = false;
|
||||
} else if (nearStructureSpace && noGroundTimer_ < 1.0f && dropFromLastGround <= 3.0f) {
|
||||
// Extended WMO rescue window: hold close to last valid floor so we
|
||||
// do not tunnel through walkable geometry during short hitches.
|
||||
targetPos.z = std::max(targetPos.z, lastGroundZ - 0.35f);
|
||||
if (verticalVelocity < -1.5f) {
|
||||
verticalVelocity = -1.5f;
|
||||
}
|
||||
grounded = false;
|
||||
} else if (nearStructureSpace && noGroundTimer_ < 1.20f && dropFromLastGround <= 4.0f && !nowJump) {
|
||||
// Extended adhesion for sparse dock/bridge collision: keep us on the
|
||||
// last valid support long enough for adjacent structure probes to hit.
|
||||
targetPos.z = std::max(targetPos.z, lastGroundZ - 0.10f);
|
||||
if (verticalVelocity < -0.5f) verticalVelocity = -0.5f;
|
||||
grounded = true;
|
||||
} else {
|
||||
grounded = false;
|
||||
}
|
||||
|
|
@ -918,7 +1185,7 @@ void CameraController::update(float deltaTime) {
|
|||
// Player is safely on real geometry — save periodically
|
||||
continuousFallTime_ = 0.0f;
|
||||
autoUnstuckFired_ = false;
|
||||
safePosSaveTimer_ += deltaTime;
|
||||
safePosSaveTimer_ += physicsDeltaTime;
|
||||
if (safePosSaveTimer_ >= SAFE_POS_SAVE_INTERVAL) {
|
||||
safePosSaveTimer_ = 0.0f;
|
||||
lastSafePos_ = targetPos;
|
||||
|
|
@ -926,7 +1193,7 @@ void CameraController::update(float deltaTime) {
|
|||
}
|
||||
} else if (!grounded && !swimming && !externalFollow_) {
|
||||
// Falling (or standing on nothing past grace period) — accumulate fall time
|
||||
continuousFallTime_ += deltaTime;
|
||||
continuousFallTime_ += physicsDeltaTime;
|
||||
if (continuousFallTime_ >= AUTO_UNSTUCK_FALL_TIME && !autoUnstuckFired_) {
|
||||
autoUnstuckFired_ = true;
|
||||
if (autoUnstuckCallback_) {
|
||||
|
|
@ -1005,28 +1272,8 @@ void CameraController::update(float deltaTime) {
|
|||
// Find max safe distance using raycast + sphere radius
|
||||
collisionDistance = currentDistance;
|
||||
|
||||
// WMO raycast collision: zoom in when camera would clip through walls/floors
|
||||
if (wmoRenderer && currentDistance > MIN_DISTANCE) {
|
||||
glm::vec3 camRayOrigin = pivot;
|
||||
glm::vec3 camRayDir = camDir;
|
||||
float wmoHitDist = wmoRenderer->raycastBoundingBoxes(camRayOrigin, camRayDir, currentDistance);
|
||||
if (wmoHitDist < currentDistance) {
|
||||
// Hit WMO geometry — pull camera in to avoid clipping
|
||||
constexpr float CAM_RADIUS = 0.3f;
|
||||
collisionDistance = std::max(MIN_DISTANCE, wmoHitDist - CAM_RADIUS);
|
||||
}
|
||||
}
|
||||
|
||||
// M2 raycast collision: zoom in when camera would clip through doodads
|
||||
if (m2Renderer && !externalFollow_ && currentDistance > MIN_DISTANCE) {
|
||||
glm::vec3 camRayOrigin = pivot;
|
||||
glm::vec3 camRayDir = camDir;
|
||||
float m2HitDist = m2Renderer->raycastBoundingBoxes(camRayOrigin, camRayDir, currentDistance);
|
||||
if (m2HitDist < collisionDistance) {
|
||||
constexpr float CAM_RADIUS = 0.3f;
|
||||
collisionDistance = std::max(MIN_DISTANCE, m2HitDist - CAM_RADIUS);
|
||||
}
|
||||
}
|
||||
// WMO/M2 camera collision disabled — was pulling camera through
|
||||
// geometry at doorway transitions and causing erratic zoom behaviour.
|
||||
|
||||
// Camera collision: terrain-only floor clamping
|
||||
auto getTerrainFloorAt = [&](float x, float y) -> std::optional<float> {
|
||||
|
|
@ -1179,27 +1426,27 @@ void CameraController::update(float deltaTime) {
|
|||
|
||||
if (glm::length(movement) > 0.001f) {
|
||||
movement = glm::normalize(movement);
|
||||
newPos += movement * swimSpeed * deltaTime;
|
||||
newPos += movement * swimSpeed * physicsDeltaTime;
|
||||
}
|
||||
|
||||
if (nowJump) {
|
||||
verticalVelocity = SWIM_BUOYANCY;
|
||||
} else {
|
||||
verticalVelocity += SWIM_GRAVITY * deltaTime;
|
||||
verticalVelocity += SWIM_GRAVITY * physicsDeltaTime;
|
||||
if (verticalVelocity < SWIM_SINK_SPEED) {
|
||||
verticalVelocity = SWIM_SINK_SPEED;
|
||||
}
|
||||
if (!diveIntent) {
|
||||
float surfaceErr = (waterSurfaceCamZ - newPos.z);
|
||||
verticalVelocity += surfaceErr * 7.0f * deltaTime;
|
||||
verticalVelocity *= std::max(0.0f, 1.0f - 3.2f * deltaTime);
|
||||
verticalVelocity += surfaceErr * 7.0f * physicsDeltaTime;
|
||||
verticalVelocity *= std::max(0.0f, 1.0f - 3.2f * physicsDeltaTime);
|
||||
if (std::abs(surfaceErr) < 0.06f && std::abs(verticalVelocity) < 0.35f) {
|
||||
verticalVelocity = 0.0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
newPos.z += verticalVelocity * deltaTime;
|
||||
newPos.z += verticalVelocity * physicsDeltaTime;
|
||||
|
||||
// Don't rise above water surface (feet at water level)
|
||||
if (waterH && (newPos.z - eyeHeight) > *waterH - WATER_SURFACE_OFFSET) {
|
||||
|
|
@ -1213,7 +1460,7 @@ void CameraController::update(float deltaTime) {
|
|||
|
||||
if (glm::length(movement) > 0.001f) {
|
||||
movement = glm::normalize(movement);
|
||||
newPos += movement * speed * deltaTime;
|
||||
newPos += movement * speed * physicsDeltaTime;
|
||||
}
|
||||
|
||||
// Jump with input buffering and coyote time
|
||||
|
|
@ -1227,12 +1474,12 @@ void CameraController::update(float deltaTime) {
|
|||
coyoteTimer = 0.0f;
|
||||
}
|
||||
|
||||
jumpBufferTimer -= deltaTime;
|
||||
coyoteTimer -= deltaTime;
|
||||
jumpBufferTimer -= physicsDeltaTime;
|
||||
coyoteTimer -= physicsDeltaTime;
|
||||
|
||||
// Apply gravity
|
||||
verticalVelocity += gravity * deltaTime;
|
||||
newPos.z += verticalVelocity * deltaTime;
|
||||
verticalVelocity += gravity * physicsDeltaTime;
|
||||
newPos.z += verticalVelocity * physicsDeltaTime;
|
||||
}
|
||||
|
||||
// Wall sweep collision before grounding (skip when stationary).
|
||||
|
|
|
|||
|
|
@ -812,10 +812,10 @@ VkTexture* CharacterRenderer::compositeTextures(const std::vector<std::string>&
|
|||
}
|
||||
}
|
||||
|
||||
// Debug: dump composite to /tmp for visual inspection
|
||||
// Debug: dump composite to temp dir for visual inspection
|
||||
{
|
||||
std::string dumpPath = "/tmp/wowee_composite_debug_" +
|
||||
std::to_string(width) + "x" + std::to_string(height) + ".raw";
|
||||
std::string dumpPath = (std::filesystem::temp_directory_path() / ("wowee_composite_debug_" +
|
||||
std::to_string(width) + "x" + std::to_string(height) + ".raw")).string();
|
||||
std::ofstream dump(dumpPath, std::ios::binary);
|
||||
if (dump) {
|
||||
dump.write(reinterpret_cast<const char*>(composite.data()),
|
||||
|
|
@ -1310,8 +1310,9 @@ void CharacterRenderer::playAnimation(uint32_t instanceId, uint32_t animationId,
|
|||
}
|
||||
|
||||
void CharacterRenderer::update(float deltaTime, const glm::vec3& cameraPos) {
|
||||
// Distance culling for animation updates (150 unit radius)
|
||||
const float animUpdateRadiusSq = 150.0f * 150.0f;
|
||||
// Distance culling for animation updates in dense areas.
|
||||
const float animUpdateRadius = static_cast<float>(envSizeOrDefault("WOWEE_CHAR_ANIM_RADIUS", 120));
|
||||
const float animUpdateRadiusSq = animUpdateRadius * animUpdateRadius;
|
||||
|
||||
// Update fade-in opacity
|
||||
for (auto& [id, inst] : instances) {
|
||||
|
|
@ -1404,6 +1405,7 @@ void CharacterRenderer::update(float deltaTime, const glm::vec3& cameraPos) {
|
|||
for (auto& pair : instances) {
|
||||
auto& instance = pair.second;
|
||||
if (instance.weaponAttachments.empty()) continue;
|
||||
if (glm::distance2(instance.position, cameraPos) > animUpdateRadiusSq) continue;
|
||||
|
||||
glm::mat4 charModelMat = instance.hasOverrideModelMatrix
|
||||
? instance.overrideModelMatrix
|
||||
|
|
@ -1614,6 +1616,12 @@ void CharacterRenderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet,
|
|||
if (instances.empty() || !opaquePipeline_) {
|
||||
return;
|
||||
}
|
||||
const float renderRadius = static_cast<float>(envSizeOrDefault("WOWEE_CHAR_RENDER_RADIUS", 130));
|
||||
const float renderRadiusSq = renderRadius * renderRadius;
|
||||
const float nearNoConeCullSq = 16.0f * 16.0f;
|
||||
const float backfaceDotCull = -0.30f;
|
||||
const glm::vec3 camPos = camera.getPosition();
|
||||
const glm::vec3 camForward = camera.getForward();
|
||||
|
||||
uint32_t frameIndex = vkCtx_->getCurrentFrame();
|
||||
uint32_t frameSlot = frameIndex % 2u;
|
||||
|
|
@ -1647,6 +1655,18 @@ void CharacterRenderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet,
|
|||
|
||||
// Skip invisible instances (e.g., player in first-person mode)
|
||||
if (!instance.visible) continue;
|
||||
// Character instance culling: avoid drawing far-away / strongly behind-camera
|
||||
// actors in dense city scenes.
|
||||
if (!instance.hasOverrideModelMatrix) {
|
||||
glm::vec3 toInst = instance.position - camPos;
|
||||
float distSq = glm::dot(toInst, toInst);
|
||||
if (distSq > renderRadiusSq) continue;
|
||||
if (distSq > nearNoConeCullSq) {
|
||||
float invDist = 1.0f / std::sqrt(distSq);
|
||||
float facingDot = glm::dot(toInst, camForward) * invDist;
|
||||
if (facingDot < backfaceDotCull) continue;
|
||||
}
|
||||
}
|
||||
|
||||
auto modelIt = models.find(instance.modelId);
|
||||
if (modelIt == models.end()) continue;
|
||||
|
|
|
|||
|
|
@ -2081,8 +2081,10 @@ void M2Renderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, const
|
|||
|
||||
lastDrawCallCount = 0;
|
||||
|
||||
// Adaptive render distance: balanced for performance without excessive pop-in
|
||||
const float maxRenderDistance = (instances.size() > 2000) ? 350.0f : 1000.0f;
|
||||
// Adaptive render distance: tiered by instance density to cap draw calls
|
||||
const float maxRenderDistance = (instances.size() > 2000) ? 300.0f
|
||||
: (instances.size() > 1000) ? 500.0f
|
||||
: 1000.0f;
|
||||
const float maxRenderDistanceSq = maxRenderDistance * maxRenderDistance;
|
||||
const float fadeStartFraction = 0.75f;
|
||||
const glm::vec3 camPos = camera.getPosition();
|
||||
|
|
|
|||
|
|
@ -363,6 +363,8 @@ void QuestMarkerRenderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSe
|
|||
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 CULL_DIST = MAX_DIST + FADE_RANGE;
|
||||
constexpr float CULL_DIST_SQ = CULL_DIST * CULL_DIST;
|
||||
|
||||
// Get time for bob animation
|
||||
float timeSeconds = SDL_GetTicks() / 1000.0f;
|
||||
|
|
@ -373,6 +375,7 @@ void QuestMarkerRenderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSe
|
|||
// 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]);
|
||||
const glm::vec3 cameraForward = glm::cross(cameraRight, cameraUp);
|
||||
|
||||
// Bind pipeline
|
||||
vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline_);
|
||||
|
|
@ -391,7 +394,9 @@ void QuestMarkerRenderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSe
|
|||
|
||||
// Calculate distance for LOD and culling
|
||||
glm::vec3 toCamera = cameraPos - marker.position;
|
||||
float dist = glm::length(toCamera);
|
||||
float distSq = glm::dot(toCamera, toCamera);
|
||||
if (distSq > CULL_DIST_SQ) continue;
|
||||
float dist = std::sqrt(distSq);
|
||||
|
||||
// Calculate fade alpha
|
||||
float fadeAlpha = 1.0f;
|
||||
|
|
@ -425,7 +430,7 @@ void QuestMarkerRenderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSe
|
|||
// 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);
|
||||
model[2] = glm::vec4(cameraForward, 0.0f);
|
||||
|
||||
// Bind material descriptor set (set 1) for this marker's texture
|
||||
vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout_,
|
||||
|
|
|
|||
|
|
@ -99,6 +99,15 @@ static bool envFlagEnabled(const char* key, bool defaultValue) {
|
|||
return !(v == "0" || v == "false" || v == "off" || v == "no");
|
||||
}
|
||||
|
||||
static int envIntOrDefault(const char* key, int defaultValue) {
|
||||
const char* raw = std::getenv(key);
|
||||
if (!raw || !*raw) return defaultValue;
|
||||
char* end = nullptr;
|
||||
long n = std::strtol(raw, &end, 10);
|
||||
if (end == raw) return defaultValue;
|
||||
return static_cast<int>(n);
|
||||
}
|
||||
|
||||
static std::vector<std::string> parseEmoteCommands(const std::string& raw) {
|
||||
std::vector<std::string> out;
|
||||
std::string cur;
|
||||
|
|
@ -2678,15 +2687,19 @@ void Renderer::update(float deltaTime) {
|
|||
}
|
||||
}
|
||||
|
||||
const bool canQueryWmo = (camera && wmoRenderer);
|
||||
const glm::vec3 camPos = camera ? camera->getPosition() : glm::vec3(0.0f);
|
||||
uint32_t insideWmoId = 0;
|
||||
const bool insideWmo = canQueryWmo &&
|
||||
wmoRenderer->isInsideWMO(camPos.x, camPos.y, camPos.z, &insideWmoId);
|
||||
|
||||
// Ambient environmental sounds: fireplaces, water, birds, etc.
|
||||
if (ambientSoundManager && camera && wmoRenderer && cameraController) {
|
||||
glm::vec3 camPos = camera->getPosition();
|
||||
uint32_t wmoId = 0;
|
||||
bool isIndoor = wmoRenderer->isInsideWMO(camPos.x, camPos.y, camPos.z, &wmoId);
|
||||
bool isIndoor = insideWmo;
|
||||
bool isSwimming = cameraController->isSwimming();
|
||||
|
||||
// Check if inside blacksmith (96048 = Goldshire blacksmith)
|
||||
bool isBlacksmith = (wmoId == 96048);
|
||||
bool isBlacksmith = (insideWmoId == 96048);
|
||||
|
||||
// Sync weather audio with visual weather system
|
||||
if (weather) {
|
||||
|
|
@ -2747,9 +2760,8 @@ void Renderer::update(float deltaTime) {
|
|||
|
||||
// Override with WMO-based detection (e.g., inside Stormwind, taverns, blacksmiths)
|
||||
if (wmoRenderer) {
|
||||
glm::vec3 camPos = camera->getPosition();
|
||||
uint32_t wmoModelId = 0;
|
||||
if (wmoRenderer->isInsideWMO(camPos.x, camPos.y, camPos.z, &wmoModelId)) {
|
||||
uint32_t wmoModelId = insideWmoId;
|
||||
if (insideWmo) {
|
||||
// Check if inside Stormwind WMO (model ID 10047)
|
||||
if (wmoModelId == 10047) {
|
||||
zoneId = 1519; // Stormwind City
|
||||
|
|
@ -3839,6 +3851,19 @@ void Renderer::renderShadowPass() {
|
|||
if (!shadowsEnabled || shadowDepthImage == VK_NULL_HANDLE) return;
|
||||
if (currentCmd == VK_NULL_HANDLE) return;
|
||||
|
||||
const int baseInterval = std::max(1, envIntOrDefault("WOWEE_SHADOW_INTERVAL", 1));
|
||||
const int denseInterval = std::max(baseInterval, envIntOrDefault("WOWEE_SHADOW_INTERVAL_DENSE", 3));
|
||||
const uint32_t denseCharThreshold = static_cast<uint32_t>(std::max(1, envIntOrDefault("WOWEE_DENSE_CHAR_THRESHOLD", 120)));
|
||||
const uint32_t denseM2Threshold = static_cast<uint32_t>(std::max(1, envIntOrDefault("WOWEE_DENSE_M2_THRESHOLD", 900)));
|
||||
const bool denseScene =
|
||||
(characterRenderer && characterRenderer->getInstanceCount() >= denseCharThreshold) ||
|
||||
(m2Renderer && m2Renderer->getInstanceCount() >= denseM2Threshold);
|
||||
const int shadowInterval = denseScene ? denseInterval : baseInterval;
|
||||
if (++shadowFrameCounter_ < static_cast<uint32_t>(shadowInterval)) {
|
||||
return;
|
||||
}
|
||||
shadowFrameCounter_ = 0;
|
||||
|
||||
// Compute and store light space matrix; write to per-frame UBO
|
||||
lightSpaceMatrix = computeLightSpaceMatrix();
|
||||
// Zero matrix means character position isn't set yet — skip shadow pass entirely.
|
||||
|
|
@ -3890,15 +3915,17 @@ void Renderer::renderShadowPass() {
|
|||
vkCmdSetScissor(currentCmd, 0, 1, &sc);
|
||||
|
||||
// Phase 7/8: render shadow casters
|
||||
constexpr float kShadowCullRadius = 180.0f; // match kShadowHalfExtent
|
||||
const float baseShadowCullRadius = static_cast<float>(std::max(40, envIntOrDefault("WOWEE_SHADOW_CULL_RADIUS", 180)));
|
||||
const float denseShadowCullRadius = static_cast<float>(std::max(30, envIntOrDefault("WOWEE_SHADOW_CULL_RADIUS_DENSE", 90)));
|
||||
const float shadowCullRadius = denseScene ? std::min(baseShadowCullRadius, denseShadowCullRadius) : baseShadowCullRadius;
|
||||
if (wmoRenderer) {
|
||||
wmoRenderer->renderShadow(currentCmd, lightSpaceMatrix, shadowCenter, kShadowCullRadius);
|
||||
wmoRenderer->renderShadow(currentCmd, lightSpaceMatrix, shadowCenter, shadowCullRadius);
|
||||
}
|
||||
if (m2Renderer) {
|
||||
m2Renderer->renderShadow(currentCmd, lightSpaceMatrix, globalTime, shadowCenter, kShadowCullRadius);
|
||||
m2Renderer->renderShadow(currentCmd, lightSpaceMatrix, globalTime, shadowCenter, shadowCullRadius);
|
||||
}
|
||||
if (characterRenderer) {
|
||||
characterRenderer->renderShadow(currentCmd, lightSpaceMatrix, shadowCenter, kShadowCullRadius);
|
||||
characterRenderer->renderShadow(currentCmd, lightSpaceMatrix, shadowCenter, shadowCullRadius);
|
||||
}
|
||||
|
||||
vkCmdEndRenderPass(currentCmd);
|
||||
|
|
|
|||
|
|
@ -1263,6 +1263,8 @@ void WMORenderer::gatherCandidates(const glm::vec3& queryMin, const glm::vec3& q
|
|||
}
|
||||
|
||||
void WMORenderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, const Camera& camera) {
|
||||
++currentFrameId;
|
||||
|
||||
if (!opaquePipeline_ || instances.empty()) {
|
||||
lastDrawCalls = 0;
|
||||
return;
|
||||
|
|
@ -2474,6 +2476,8 @@ void WMORenderer::GroupResources::buildCollisionGrid() {
|
|||
|
||||
size_t numTriangles = collisionIndices.size() / 3;
|
||||
triBounds.resize(numTriangles);
|
||||
triNormals.resize(numTriangles);
|
||||
triVisited.resize(numTriangles, 0);
|
||||
|
||||
float invCellW = gridCellsX / std::max(0.01f, extentX);
|
||||
float invCellH = gridCellsY / std::max(0.01f, extentY);
|
||||
|
|
@ -2494,16 +2498,23 @@ void WMORenderer::GroupResources::buildCollisionGrid() {
|
|||
float triMaxZ = std::max({v0.z, v1.z, v2.z});
|
||||
triBounds[i / 3] = { triMinZ, triMaxZ };
|
||||
|
||||
// Classify floor vs wall by normal.
|
||||
// Wall threshold matches MAX_WALK_SLOPE_DOT (cos 50° ≈ 0.6428) so that
|
||||
// surfaces too steep to walk on are always tested for wall collision.
|
||||
// Precompute and store unit normal
|
||||
glm::vec3 edge1 = v1 - v0;
|
||||
glm::vec3 edge2 = v2 - v0;
|
||||
glm::vec3 normal = glm::cross(edge1, edge2);
|
||||
float normalLen = glm::length(normal);
|
||||
float absNz = (normalLen > 0.001f) ? std::abs(normal.z / normalLen) : 0.0f;
|
||||
if (normalLen > 0.001f) {
|
||||
normal /= normalLen;
|
||||
} else {
|
||||
normal = glm::vec3(0.0f, 0.0f, 1.0f);
|
||||
}
|
||||
triNormals[i / 3] = normal;
|
||||
|
||||
// Classify floor vs wall by normal.
|
||||
// Wall threshold matches the runtime skip in checkWallCollision (absNz >= 0.35).
|
||||
float absNz = std::abs(normal.z);
|
||||
bool isFloor = (absNz >= 0.35f); // ~70° max slope (relaxed for steep stairs)
|
||||
bool isWall = (absNz < 0.65f); // Matches walkable slope threshold
|
||||
bool isWall = (absNz < 0.35f); // Matches checkWallCollision skip threshold
|
||||
|
||||
int cellMinX = std::max(0, static_cast<int>((triMinX - gridOrigin.x) * invCellW));
|
||||
int cellMinY = std::max(0, static_cast<int>((triMinY - gridOrigin.y) * invCellH));
|
||||
|
|
@ -2556,18 +2567,30 @@ void WMORenderer::GroupResources::getTrianglesInRange(
|
|||
|
||||
if (cellMinX > cellMaxX || cellMinY > cellMaxY) return;
|
||||
|
||||
// Collect unique triangle indices from all overlapping cells
|
||||
// Collect unique triangle indices using visited bitset (O(n) dedup)
|
||||
bool multiCell = (cellMinX != cellMaxX || cellMinY != cellMaxY);
|
||||
if (multiCell && !triVisited.empty()) {
|
||||
for (int cy = cellMinY; cy <= cellMaxY; ++cy) {
|
||||
for (int cx = cellMinX; cx <= cellMaxX; ++cx) {
|
||||
const auto& cell = cellTriangles[cy * gridCellsX + cx];
|
||||
for (uint32_t tri : cell) {
|
||||
uint32_t idx = tri / 3;
|
||||
if (!triVisited[idx]) {
|
||||
triVisited[idx] = 1;
|
||||
out.push_back(tri);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Clear visited bits
|
||||
for (uint32_t tri : out) triVisited[tri / 3] = 0;
|
||||
} else {
|
||||
for (int cy = cellMinY; cy <= cellMaxY; ++cy) {
|
||||
for (int cx = cellMinX; cx <= cellMaxX; ++cx) {
|
||||
const auto& cell = cellTriangles[cy * gridCellsX + cx];
|
||||
out.insert(out.end(), cell.begin(), cell.end());
|
||||
}
|
||||
}
|
||||
|
||||
// Remove duplicates (triangles spanning multiple cells)
|
||||
if (cellMinX != cellMaxX || cellMinY != cellMaxY) {
|
||||
std::sort(out.begin(), out.end());
|
||||
out.erase(std::unique(out.begin(), out.end()), out.end());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -2589,16 +2612,28 @@ void WMORenderer::GroupResources::getFloorTrianglesInRange(
|
|||
|
||||
if (cellMinX > cellMaxX || cellMinY > cellMaxY) return;
|
||||
|
||||
bool multiCell = (cellMinX != cellMaxX || cellMinY != cellMaxY);
|
||||
if (multiCell && !triVisited.empty()) {
|
||||
for (int cy = cellMinY; cy <= cellMaxY; ++cy) {
|
||||
for (int cx = cellMinX; cx <= cellMaxX; ++cx) {
|
||||
const auto& cell = cellFloorTriangles[cy * gridCellsX + cx];
|
||||
for (uint32_t tri : cell) {
|
||||
uint32_t idx = tri / 3;
|
||||
if (!triVisited[idx]) {
|
||||
triVisited[idx] = 1;
|
||||
out.push_back(tri);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (uint32_t tri : out) triVisited[tri / 3] = 0;
|
||||
} else {
|
||||
for (int cy = cellMinY; cy <= cellMaxY; ++cy) {
|
||||
for (int cx = cellMinX; cx <= cellMaxX; ++cx) {
|
||||
const auto& cell = cellFloorTriangles[cy * gridCellsX + cx];
|
||||
out.insert(out.end(), cell.begin(), cell.end());
|
||||
}
|
||||
}
|
||||
|
||||
if (cellMinX != cellMaxX || cellMinY != cellMaxY) {
|
||||
std::sort(out.begin(), out.end());
|
||||
out.erase(std::unique(out.begin(), out.end()), out.end());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -2620,22 +2655,35 @@ void WMORenderer::GroupResources::getWallTrianglesInRange(
|
|||
|
||||
if (cellMinX > cellMaxX || cellMinY > cellMaxY) return;
|
||||
|
||||
bool multiCell = (cellMinX != cellMaxX || cellMinY != cellMaxY);
|
||||
if (multiCell && !triVisited.empty()) {
|
||||
for (int cy = cellMinY; cy <= cellMaxY; ++cy) {
|
||||
for (int cx = cellMinX; cx <= cellMaxX; ++cx) {
|
||||
const auto& cell = cellWallTriangles[cy * gridCellsX + cx];
|
||||
for (uint32_t tri : cell) {
|
||||
uint32_t idx = tri / 3;
|
||||
if (!triVisited[idx]) {
|
||||
triVisited[idx] = 1;
|
||||
out.push_back(tri);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (uint32_t tri : out) triVisited[tri / 3] = 0;
|
||||
} else {
|
||||
for (int cy = cellMinY; cy <= cellMaxY; ++cy) {
|
||||
for (int cx = cellMinX; cx <= cellMaxX; ++cx) {
|
||||
const auto& cell = cellWallTriangles[cy * gridCellsX + cx];
|
||||
out.insert(out.end(), cell.begin(), cell.end());
|
||||
}
|
||||
}
|
||||
|
||||
if (cellMinX != cellMaxX || cellMinY != cellMaxY) {
|
||||
std::sort(out.begin(), out.end());
|
||||
out.erase(std::unique(out.begin(), out.end()), out.end());
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<float> WMORenderer::getFloorHeight(float glX, float glY, float glZ, float* outNormalZ) const {
|
||||
// All floor caching disabled - even per-frame cache can return stale results
|
||||
// when player Z changes between queries, causing fall-through at stairs.
|
||||
// Per-frame cache disabled: camera and player query the same (x,y) at
|
||||
// different Z within a single frame. The allowAbove filter depends on glZ,
|
||||
// so caching by (x,y) alone returns wrong floors across Z contexts.
|
||||
|
||||
QueryTimer timer(&queryTimeMs, &queryCallCount);
|
||||
std::optional<float> bestFloor;
|
||||
|
|
@ -2660,9 +2708,9 @@ std::optional<float> WMORenderer::getFloorHeight(float glX, float glY, float glZ
|
|||
group.getTrianglesInRange(
|
||||
localOrigin.x - 1.0f, localOrigin.y - 1.0f,
|
||||
localOrigin.x + 1.0f, localOrigin.y + 1.0f,
|
||||
wallTriScratch);
|
||||
triScratch_);
|
||||
|
||||
for (uint32_t triStart : wallTriScratch) {
|
||||
for (uint32_t triStart : triScratch_) {
|
||||
const glm::vec3& v0 = verts[indices[triStart]];
|
||||
const glm::vec3& v1 = verts[indices[triStart + 1]];
|
||||
const glm::vec3& v2 = verts[indices[triStart + 2]];
|
||||
|
|
@ -2676,18 +2724,16 @@ std::optional<float> WMORenderer::getFloorHeight(float glX, float glY, float glZ
|
|||
glm::vec3 hitLocal = localOrigin + localDir * t;
|
||||
glm::vec3 hitWorld = glm::vec3(instance.modelMatrix * glm::vec4(hitLocal, 1.0f));
|
||||
|
||||
float allowAbove = model.isLowPlatform ? 12.0f : 2.0f;
|
||||
if (hitWorld.z <= glZ + allowAbove) {
|
||||
// Accept floors at or below glZ (the caller already elevates
|
||||
// glZ by stepUpBudget to handle step-up range). Among those,
|
||||
// pick the highest (closest to feet).
|
||||
if (hitWorld.z <= glZ) {
|
||||
if (!bestFloor || hitWorld.z > *bestFloor) {
|
||||
bestFloor = hitWorld.z;
|
||||
bestFromLowPlatform = model.isLowPlatform;
|
||||
|
||||
// Compute local normal and transform to world space
|
||||
glm::vec3 localNormal = glm::cross(v1 - v0, v2 - v0);
|
||||
float len = glm::length(localNormal);
|
||||
if (len > 0.001f) {
|
||||
localNormal /= len;
|
||||
// Ensure normal points upward
|
||||
// Use precomputed normal, ensure upward, transform to world
|
||||
glm::vec3 localNormal = group.triNormals[triStart / 3];
|
||||
if (localNormal.z < 0.0f) localNormal = -localNormal;
|
||||
glm::vec3 worldNormal = glm::normalize(
|
||||
glm::vec3(instance.modelMatrix * glm::vec4(localNormal, 0.0f)));
|
||||
|
|
@ -2696,11 +2742,46 @@ std::optional<float> WMORenderer::getFloorHeight(float glX, float glY, float glZ
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Full scan: test all instances (active group fast path removed to fix
|
||||
// bridge clipping where early-return missed other WMO instances)
|
||||
// Fast path: current active interior group and its neighbors are usually
|
||||
// the right answer for player-floor queries while moving in cities/buildings.
|
||||
if (activeGroup_.isValid() && activeGroup_.instanceIdx < instances.size()) {
|
||||
const auto& instance = instances[activeGroup_.instanceIdx];
|
||||
auto it = loadedModels.find(instance.modelId);
|
||||
if (it != loadedModels.end() && instance.modelId == activeGroup_.modelId) {
|
||||
const ModelData& model = it->second;
|
||||
glm::vec3 localOrigin = glm::vec3(instance.invModelMatrix * glm::vec4(worldOrigin, 1.0f));
|
||||
glm::vec3 localDir = glm::normalize(glm::vec3(instance.invModelMatrix * glm::vec4(worldDir, 0.0f)));
|
||||
|
||||
auto testGroupIdx = [&](uint32_t gi) {
|
||||
if (gi >= model.groups.size()) return;
|
||||
if (gi < instance.worldGroupBounds.size()) {
|
||||
const auto& [gMin, gMax] = instance.worldGroupBounds[gi];
|
||||
if (glX < gMin.x || glX > gMax.x ||
|
||||
glY < gMin.y || glY > gMax.y ||
|
||||
glZ - 4.0f > gMax.z) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
const auto& group = model.groups[gi];
|
||||
if (!rayIntersectsAABB(localOrigin, localDir, group.boundingBoxMin, group.boundingBoxMax)) {
|
||||
return;
|
||||
}
|
||||
testGroupFloor(instance, model, group, localOrigin, localDir);
|
||||
};
|
||||
|
||||
if (activeGroup_.groupIdx >= 0) {
|
||||
testGroupIdx(static_cast<uint32_t>(activeGroup_.groupIdx));
|
||||
}
|
||||
for (uint32_t ngi : activeGroup_.neighborGroups) {
|
||||
testGroupIdx(ngi);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Full scan: test all instances (active group result above is not
|
||||
// early-returned because overlapping WMO instances need full coverage).
|
||||
glm::vec3 queryMin(glX - 2.0f, glY - 2.0f, glZ - 8.0f);
|
||||
glm::vec3 queryMax(glX + 2.0f, glY + 2.0f, glZ + 10.0f);
|
||||
gatherCandidates(queryMin, queryMax, candidateScratch);
|
||||
|
|
@ -2720,6 +2801,9 @@ std::optional<float> WMORenderer::getFloorHeight(float glX, float glY, float glZ
|
|||
float zMarginUp = model.isLowPlatform ? 20.0f : 4.0f;
|
||||
|
||||
// Broad-phase reject in world space to avoid expensive matrix transforms.
|
||||
if (bestFloor && instance.worldBoundsMax.z <= (*bestFloor + 0.05f)) {
|
||||
continue;
|
||||
}
|
||||
if (glX < instance.worldBoundsMin.x || glX > instance.worldBoundsMax.x ||
|
||||
glY < instance.worldBoundsMin.y || glY > instance.worldBoundsMax.y ||
|
||||
glZ < instance.worldBoundsMin.z - zMarginDown || glZ > instance.worldBoundsMax.z + zMarginUp) {
|
||||
|
|
@ -2859,9 +2943,9 @@ bool WMORenderer::checkWallCollision(const glm::vec3& from, const glm::vec3& to,
|
|||
float rangeMinY = std::min(localFrom.y, localTo.y) - PLAYER_RADIUS - 1.5f;
|
||||
float rangeMaxX = std::max(localFrom.x, localTo.x) + PLAYER_RADIUS + 1.5f;
|
||||
float rangeMaxY = std::max(localFrom.y, localTo.y) + PLAYER_RADIUS + 1.5f;
|
||||
group.getWallTrianglesInRange(rangeMinX, rangeMinY, rangeMaxX, rangeMaxY, wallTriScratch);
|
||||
group.getWallTrianglesInRange(rangeMinX, rangeMinY, rangeMaxX, rangeMaxY, triScratch_);
|
||||
|
||||
for (uint32_t triStart : wallTriScratch) {
|
||||
for (uint32_t triStart : triScratch_) {
|
||||
// Use pre-computed Z bounds for fast vertical reject
|
||||
const auto& tb = group.triBounds[triStart / 3];
|
||||
|
||||
|
|
@ -2880,13 +2964,9 @@ bool WMORenderer::checkWallCollision(const glm::vec3& from, const glm::vec3& to,
|
|||
const glm::vec3& v1 = verts[indices[triStart + 1]];
|
||||
const glm::vec3& v2 = verts[indices[triStart + 2]];
|
||||
|
||||
// Triangle normal for swept test and push fallback
|
||||
glm::vec3 edge1 = v1 - v0;
|
||||
glm::vec3 edge2 = v2 - v0;
|
||||
glm::vec3 normal = glm::cross(edge1, edge2);
|
||||
float normalLen = glm::length(normal);
|
||||
if (normalLen < 0.001f) continue;
|
||||
normal /= normalLen;
|
||||
// Use precomputed normal for swept test and push fallback
|
||||
glm::vec3 normal = group.triNormals[triStart / 3];
|
||||
if (glm::dot(normal, normal) < 0.5f) continue; // degenerate
|
||||
|
||||
// Recompute plane distances with current (possibly pushed) localTo
|
||||
float fromDist = glm::dot(localFrom - v0, normal);
|
||||
|
|
@ -3229,19 +3309,15 @@ float WMORenderer::raycastBoundingBoxes(const glm::vec3& origin, const glm::vec3
|
|||
float rMinY = std::min(localOrigin.y, localEnd.y) - 1.0f;
|
||||
float rMaxX = std::max(localOrigin.x, localEnd.x) + 1.0f;
|
||||
float rMaxY = std::max(localOrigin.y, localEnd.y) + 1.0f;
|
||||
group.getWallTrianglesInRange(rMinX, rMinY, rMaxX, rMaxY, wallTriScratch);
|
||||
group.getWallTrianglesInRange(rMinX, rMinY, rMaxX, rMaxY, triScratch_);
|
||||
|
||||
for (uint32_t triStart : wallTriScratch) {
|
||||
for (uint32_t triStart : triScratch_) {
|
||||
const glm::vec3& v0 = verts[indices[triStart]];
|
||||
const glm::vec3& v1 = verts[indices[triStart + 1]];
|
||||
const glm::vec3& v2 = verts[indices[triStart + 2]];
|
||||
glm::vec3 triNormal = glm::cross(v1 - v0, v2 - v0);
|
||||
float normalLenSq = glm::dot(triNormal, triNormal);
|
||||
if (normalLenSq < 1e-8f) {
|
||||
continue;
|
||||
}
|
||||
triNormal /= std::sqrt(normalLenSq);
|
||||
// Wall list pre-filters at 0.55; apply stricter camera threshold
|
||||
glm::vec3 triNormal = group.triNormals[triStart / 3];
|
||||
if (glm::dot(triNormal, triNormal) < 0.5f) continue; // degenerate
|
||||
// Wall list pre-filters at 0.35; apply stricter camera threshold
|
||||
if (std::abs(triNormal.z) > MAX_WALKABLE_ABS_NORMAL_Z) {
|
||||
continue;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -206,7 +206,8 @@ void AuthScreen::render(auth::AuthHandler& authHandler) {
|
|||
}
|
||||
}
|
||||
}
|
||||
if (renderer) {
|
||||
// Login screen music disabled
|
||||
if (false && renderer) {
|
||||
auto* music = renderer->getMusicManager();
|
||||
if (music) {
|
||||
if (!loginMusicVolumeAdjusted_) {
|
||||
|
|
@ -220,6 +221,9 @@ void AuthScreen::render(auth::AuthHandler& authHandler) {
|
|||
music->update(ImGui::GetIO().DeltaTime);
|
||||
if (!music->isPlaying()) {
|
||||
static std::mt19937 rng(std::random_device{}());
|
||||
if (!introTracksScanned_) {
|
||||
introTracksScanned_ = true;
|
||||
|
||||
// Tracks in assets/ root
|
||||
static const std::array<const char*, 1> kRootTracks = {
|
||||
"Raise the Mug, Sound the Warcry.mp3",
|
||||
|
|
@ -239,31 +243,42 @@ void AuthScreen::render(auth::AuthHandler& authHandler) {
|
|||
"You No Take Candle!.mp3",
|
||||
};
|
||||
|
||||
std::vector<std::string> availableTracks;
|
||||
auto tryAddTrack = [&](const std::filesystem::path& base, const char* track) {
|
||||
std::filesystem::path p = base / track;
|
||||
if (std::filesystem::exists(p)) {
|
||||
availableTracks.push_back(p.string());
|
||||
introTracks_.push_back(p.string());
|
||||
}
|
||||
};
|
||||
for (const char* track : kRootTracks) {
|
||||
tryAddTrack("assets", track);
|
||||
if (availableTracks.empty())
|
||||
if (introTracks_.empty()) {
|
||||
tryAddTrack(std::filesystem::current_path() / "assets", track);
|
||||
}
|
||||
}
|
||||
for (const char* track : kOriginalTracks) {
|
||||
tryAddTrack(std::filesystem::path("assets") / "Original Music", track);
|
||||
tryAddTrack(std::filesystem::current_path() / "assets" / "Original Music", track);
|
||||
}
|
||||
|
||||
if (!availableTracks.empty()) {
|
||||
std::uniform_int_distribution<size_t> pick(0, availableTracks.size() - 1);
|
||||
const std::string& path = availableTracks[pick(rng)];
|
||||
std::sort(introTracks_.begin(), introTracks_.end());
|
||||
introTracks_.erase(std::unique(introTracks_.begin(), introTracks_.end()), introTracks_.end());
|
||||
}
|
||||
|
||||
if (!introTracks_.empty()) {
|
||||
std::uniform_int_distribution<size_t> pick(0, introTracks_.size() - 1);
|
||||
const size_t idx = pick(rng);
|
||||
const std::string path = introTracks_[idx];
|
||||
music->playFilePath(path, true, 1800.0f);
|
||||
LOG_INFO("AuthScreen: Playing login intro track: ", path);
|
||||
musicPlaying = music->isPlaying();
|
||||
if (musicPlaying) {
|
||||
LOG_INFO("AuthScreen: Playing login intro track: ", path);
|
||||
} else {
|
||||
// Drop bad paths to avoid retrying the same failed file every frame.
|
||||
introTracks_.erase(introTracks_.begin() + idx);
|
||||
}
|
||||
} else if (!missingIntroTracksLogged_) {
|
||||
LOG_WARNING("AuthScreen: No login intro tracks found in assets/");
|
||||
missingIntroTracksLogged_ = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -548,6 +548,12 @@ bool Extractor::enumerateFiles(const Options& opts,
|
|||
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;
|
||||
}
|
||||
|
||||
std::string norm = normalizeWowPath(fileName);
|
||||
if (opts.onlyUsedDbcs && !wantedDbcs.empty() && !wantedDbcs.contains(norm)) {
|
||||
continue;
|
||||
|
|
@ -624,24 +630,36 @@ bool Extractor::run(const Options& opts) {
|
|||
std::atomic<size_t> fileIndex{0};
|
||||
size_t totalFiles = files.size();
|
||||
|
||||
auto workerFn = [&]() {
|
||||
// Each thread opens ALL archives independently (StormLib is not thread-safe per handle).
|
||||
// Sorted highest-priority last, so we iterate in reverse to find the winning version.
|
||||
struct ThreadArchive {
|
||||
// Open archives ONCE in main thread — StormLib has global state that is not
|
||||
// thread-safe even with separate handles, so we serialize all MPQ reads.
|
||||
struct SharedArchive {
|
||||
HANDLE handle;
|
||||
int priority;
|
||||
std::string path;
|
||||
};
|
||||
std::vector<ThreadArchive> threadHandles;
|
||||
std::vector<SharedArchive> sharedHandles;
|
||||
for (const auto& ad : archives) {
|
||||
HANDLE h = nullptr;
|
||||
if (SFileOpenArchive(ad.path.c_str(), 0, 0, &h)) {
|
||||
threadHandles.push_back({h, ad.priority});
|
||||
sharedHandles.push_back({h, ad.priority, ad.path});
|
||||
} else {
|
||||
std::cerr << " Failed to open archive: " << ad.path << "\n";
|
||||
}
|
||||
}
|
||||
if (threadHandles.empty()) {
|
||||
std::cerr << "Worker thread: failed to open any archives\n";
|
||||
return;
|
||||
if (sharedHandles.empty()) {
|
||||
std::cerr << "Failed to open any archives for extraction\n";
|
||||
return false;
|
||||
}
|
||||
if (sharedHandles.size() < archives.size()) {
|
||||
std::cerr << " Opened " << sharedHandles.size()
|
||||
<< "/" << archives.size() << " archives\n";
|
||||
}
|
||||
|
||||
// Mutex protecting all StormLib calls (open/read/close are not thread-safe)
|
||||
std::mutex mpqMutex;
|
||||
|
||||
auto workerFn = [&]() {
|
||||
int failLogCount = 0;
|
||||
|
||||
while (true) {
|
||||
size_t idx = fileIndex.fetch_add(1);
|
||||
|
|
@ -654,9 +672,14 @@ bool Extractor::run(const Options& opts) {
|
|||
std::string mappedPath = PathMapper::mapPath(wowPath);
|
||||
std::string fullOutputPath = effectiveOutputDir + "/" + mappedPath;
|
||||
|
||||
// Read file data from MPQ under lock
|
||||
std::vector<uint8_t> data;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mpqMutex);
|
||||
|
||||
// Search archives in reverse priority order (highest priority first)
|
||||
HANDLE hFile = nullptr;
|
||||
for (auto it = threadHandles.rbegin(); it != threadHandles.rend(); ++it) {
|
||||
for (auto it = sharedHandles.rbegin(); it != sharedHandles.rend(); ++it) {
|
||||
if (SFileOpenFileEx(it->handle, wowPath.c_str(), 0, &hFile)) {
|
||||
break;
|
||||
}
|
||||
|
|
@ -664,6 +687,11 @@ bool Extractor::run(const Options& opts) {
|
|||
}
|
||||
if (!hFile) {
|
||||
stats.filesFailed++;
|
||||
if (failLogCount < 5) {
|
||||
failLogCount++;
|
||||
std::cerr << " FAILED open: " << wowPath
|
||||
<< " (tried " << sharedHandles.size() << " archives)\n";
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
@ -674,15 +702,22 @@ bool Extractor::run(const Options& opts) {
|
|||
continue;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> data(fileSize);
|
||||
data.resize(fileSize);
|
||||
DWORD bytesRead = 0;
|
||||
if (!SFileReadFile(hFile, data.data(), fileSize, &bytesRead, nullptr)) {
|
||||
SFileCloseFile(hFile);
|
||||
stats.filesFailed++;
|
||||
if (failLogCount < 5) {
|
||||
failLogCount++;
|
||||
std::cerr << " FAILED read: " << wowPath
|
||||
<< " (size=" << fileSize << ")\n";
|
||||
}
|
||||
continue;
|
||||
}
|
||||
SFileCloseFile(hFile);
|
||||
data.resize(bytesRead);
|
||||
}
|
||||
// Lock released — CRC computation and disk write happen in parallel
|
||||
|
||||
// Compute CRC32
|
||||
uint32_t crc = ManifestWriter::computeCRC32(data.data(), data.size());
|
||||
|
|
@ -694,6 +729,11 @@ bool Extractor::run(const Options& opts) {
|
|||
std::ofstream out(fullOutputPath, std::ios::binary);
|
||||
if (!out.is_open()) {
|
||||
stats.filesFailed++;
|
||||
if (failLogCount < 5) {
|
||||
failLogCount++;
|
||||
std::lock_guard<std::mutex> lock(manifestMutex);
|
||||
std::cerr << " FAILED write: " << fullOutputPath << "\n";
|
||||
}
|
||||
continue;
|
||||
}
|
||||
out.write(reinterpret_cast<const char*>(data.data()), data.size());
|
||||
|
|
@ -721,10 +761,6 @@ bool Extractor::run(const Options& opts) {
|
|||
<< std::flush;
|
||||
}
|
||||
}
|
||||
|
||||
for (auto& th : threadHandles) {
|
||||
SFileCloseArchive(th.handle);
|
||||
}
|
||||
};
|
||||
|
||||
std::cout << "Extracting " << totalFiles << " files using " << numThreads << " threads...\n";
|
||||
|
|
@ -737,10 +773,30 @@ bool Extractor::run(const Options& opts) {
|
|||
t.join();
|
||||
}
|
||||
|
||||
std::cout << "\r Extracted " << stats.filesExtracted.load() << " files ("
|
||||
// Close archives (opened once in main thread)
|
||||
for (auto& sh : sharedHandles) {
|
||||
SFileCloseArchive(sh.handle);
|
||||
}
|
||||
|
||||
auto extracted = stats.filesExtracted.load();
|
||||
auto failed = stats.filesFailed.load();
|
||||
auto skipped = stats.filesSkipped.load();
|
||||
std::cout << "\n Extracted " << extracted << " files ("
|
||||
<< stats.bytesExtracted.load() / (1024 * 1024) << " MB), "
|
||||
<< stats.filesSkipped.load() << " skipped, "
|
||||
<< stats.filesFailed.load() << " failed\n";
|
||||
<< skipped << " skipped, "
|
||||
<< failed << " failed\n";
|
||||
|
||||
// If most files failed, print a diagnostic hint
|
||||
if (failed > 0 && failed > extracted * 10) {
|
||||
std::cerr << "\nWARNING: " << failed << " out of " << totalFiles
|
||||
<< " files failed to extract.\n"
|
||||
<< " This usually means worker threads could not open one or more MPQ archives.\n"
|
||||
<< " Common causes:\n"
|
||||
<< " - MPQ files on a network/external drive with access restrictions\n"
|
||||
<< " - Another program (WoW client, antivirus) has the MPQ files locked\n"
|
||||
<< " - Too many threads for the OS file-handle limit (try --threads 1)\n"
|
||||
<< " Re-run with --verbose for detailed diagnostics.\n";
|
||||
}
|
||||
|
||||
// Merge with existing manifest so partial extractions don't nuke prior entries
|
||||
std::string manifestPath = effectiveOutputDir + "/manifest.json";
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue