mirror of
https://github.com/johndoe6345789/metabuilder.git
synced 2026-05-05 11:09:39 +00:00
Add spdlog trace logging, ioq3 native resolution, and macOS 26 Metal fixes
- Replace custom LoggerImpl with spdlog (stdout_color_sink + basic_file_sink), QUAKE3_LOG_LEVEL env var controls level at runtime (trace/debug/info/warn/error) - Fix HUD virtual canvas from 640×360 to 640×480 (ioq3 native resolution) - Fix BSP entity field parsing: all numeric values in Q3 BSP entity lump are JSON strings; use EntFloat() helper with stof() in movers_init and triggers_check - Fix macOS 26 Metal crash: TAA shader had SPIRV path as default for MSL backend; add postfx_taa.frag.metal MSL port and fix seed_game.json default path - GPU init: disable SDL Metal debug layer by default (MTL_DEBUG_LAYER); re-enable with SDL_GPU_DEBUG=1 env var; add MSL null-terminator guard in shader compile - spdlog 1.15.1 added to conanfile.py BASE_REQUIRES and CMakeLists.txt Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -106,6 +106,8 @@ find_package(assimp CONFIG REQUIRED)
|
||||
|
||||
find_package(libzip CONFIG REQUIRED)
|
||||
|
||||
find_package(spdlog CONFIG REQUIRED)
|
||||
|
||||
|
||||
# Build render stack library group
|
||||
set(SDL3CPP_RENDER_STACK_LIBS EnTT::EnTT)
|
||||
@@ -430,6 +432,7 @@ if(BUILD_SDL3_APP)
|
||||
EnTT::EnTT
|
||||
libzip::zip
|
||||
assimp::assimp
|
||||
spdlog::spdlog
|
||||
)
|
||||
endif()
|
||||
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
"conan": {}
|
||||
},
|
||||
"include": [
|
||||
"build-ninja/build/generators/CMakePresets.json"
|
||||
"build/Release/generators/CMakePresets.json",
|
||||
"build-ninja/build/Release/build/Release/generators/CMakePresets.json",
|
||||
"build/Debug/generators/CMakePresets.json"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -29,7 +29,8 @@ class SDL3CppConan(ConanFile):
|
||||
"cairo/1.18.0",
|
||||
"libzip/1.10.1",
|
||||
"stb/cci.20230920",
|
||||
"gtest/1.17.0"
|
||||
"gtest/1.17.0",
|
||||
"spdlog/1.15.1"
|
||||
)
|
||||
RENDER_STACK_REQUIRES = (
|
||||
"entt/3.16.0",
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
#include <metal_stdlib>
|
||||
using namespace metal;
|
||||
|
||||
// Temporal Anti-Aliasing resolve
|
||||
// Blends current frame with history using neighborhood clamping (UE4/Karis style)
|
||||
|
||||
struct TAAParams {
|
||||
float4 u_params; // x = blend factor (0.05 = 95% history), y = 1/width, z = 1/height, w = frame count
|
||||
};
|
||||
|
||||
struct FragIn {
|
||||
float2 uv [[user(uv)]];
|
||||
};
|
||||
|
||||
// Tonemap for stable neighborhood clamping (Karis 2014)
|
||||
inline float3 tonemap(float3 c) {
|
||||
return c / (1.0f + max(c.r, max(c.g, c.b)));
|
||||
}
|
||||
|
||||
inline float3 untonemap(float3 c) {
|
||||
return c / (1.0f - max(c.r, max(c.g, c.b)));
|
||||
}
|
||||
|
||||
fragment float4 main0(
|
||||
FragIn in [[stage_in]],
|
||||
texture2d<float> currentFrame [[texture(0)]],
|
||||
texture2d<float> historyFrame [[texture(1)]],
|
||||
sampler currentSampler [[sampler(0)]],
|
||||
sampler historySampler [[sampler(1)]],
|
||||
constant TAAParams& params [[buffer(0)]])
|
||||
{
|
||||
float2 texelSize = params.u_params.yz;
|
||||
float blendFactor = params.u_params.x;
|
||||
float frameCount = params.u_params.w;
|
||||
float2 uv = in.uv;
|
||||
|
||||
float3 current = currentFrame.sample(currentSampler, uv).rgb;
|
||||
|
||||
// First frame: no history, just output current
|
||||
if (frameCount < 1.5f) {
|
||||
return float4(current, 1.0f);
|
||||
}
|
||||
|
||||
// Sample 3x3 neighbourhood of current frame for clamping
|
||||
float3 nMin = current;
|
||||
float3 nMax = current;
|
||||
|
||||
for (int y = -1; y <= 1; ++y) {
|
||||
for (int x = -1; x <= 1; ++x) {
|
||||
if (x == 0 && y == 0) continue;
|
||||
float3 s = currentFrame.sample(currentSampler,
|
||||
uv + float2(float(x), float(y)) * texelSize).rgb;
|
||||
nMin = min(nMin, s);
|
||||
nMax = max(nMax, s);
|
||||
}
|
||||
}
|
||||
|
||||
// Slightly expand the bounding box to reduce flickering
|
||||
float3 nCenter = (nMin + nMax) * 0.5f;
|
||||
float3 nExtent = (nMax - nMin) * 0.5f;
|
||||
nMin = nCenter - nExtent * 1.25f;
|
||||
nMax = nCenter + nExtent * 1.25f;
|
||||
|
||||
// Sample history and clamp to neighbourhood (prevents ghosting)
|
||||
float3 history = historyFrame.sample(historySampler, uv).rgb;
|
||||
float3 historyTM = tonemap(history);
|
||||
float3 clampedTM = clamp(historyTM, tonemap(nMin), tonemap(nMax));
|
||||
float3 clamped = untonemap(clampedTM);
|
||||
|
||||
// Blend: low factor = more history = smoother but more ghosting
|
||||
float3 result = mix(clamped, current, blendFactor);
|
||||
|
||||
return float4(result, 1.0f);
|
||||
}
|
||||
@@ -119,7 +119,7 @@
|
||||
"postfx_taa_frag_path": {
|
||||
"name": "postfx_taa_frag_path",
|
||||
"type": "string",
|
||||
"defaultValue": "packages/seed/shaders/spirv/postfx_taa.frag.spv"
|
||||
"defaultValue": "packages/seed/shaders/msl/postfx_taa.frag.metal"
|
||||
}
|
||||
},
|
||||
"nodes": [
|
||||
|
||||
@@ -1,118 +1,137 @@
|
||||
#include "services/interfaces/diagnostics/logger_service.hpp"
|
||||
|
||||
#include <iostream>
|
||||
#include <chrono>
|
||||
#include <iomanip>
|
||||
#include <thread>
|
||||
#include <spdlog/sinks/stdout_color_sinks.h>
|
||||
#include <spdlog/sinks/basic_file_sink.h>
|
||||
|
||||
#include <cstdlib>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace sdl3cpp::services::impl {
|
||||
|
||||
LoggerService::LoggerService() : impl_(std::make_unique<LoggerImpl>()) {}
|
||||
// ── helpers ───────────────────────────────────────────────────────────────────
|
||||
|
||||
spdlog::level::level_enum LoggerService::ToSpdlog(LogLevel level) {
|
||||
switch (level) {
|
||||
case LogLevel::TRACE: return spdlog::level::trace;
|
||||
case LogLevel::DEBUG: return spdlog::level::debug;
|
||||
case LogLevel::INFO: return spdlog::level::info;
|
||||
case LogLevel::WARN: return spdlog::level::warn;
|
||||
case LogLevel::ERROR: return spdlog::level::err;
|
||||
default: return spdlog::level::off;
|
||||
}
|
||||
}
|
||||
|
||||
LogLevel LoggerService::FromSpdlog(spdlog::level::level_enum l) {
|
||||
switch (l) {
|
||||
case spdlog::level::trace: return LogLevel::TRACE;
|
||||
case spdlog::level::debug: return LogLevel::DEBUG;
|
||||
case spdlog::level::info: return LogLevel::INFO;
|
||||
case spdlog::level::warn: return LogLevel::WARN;
|
||||
case spdlog::level::err:
|
||||
case spdlog::level::critical: return LogLevel::ERROR;
|
||||
default: return LogLevel::OFF;
|
||||
}
|
||||
}
|
||||
|
||||
// Rebuild spdlog logger whenever sinks change (SetOutputFile / EnableConsoleOutput).
|
||||
void LoggerService::RebuildLogger() {
|
||||
std::vector<spdlog::sink_ptr> sinks;
|
||||
|
||||
if (consoleOn_) {
|
||||
auto s = std::make_shared<spdlog::sinks::stdout_color_sink_mt>();
|
||||
s->set_pattern("[%H:%M:%S.%e] [%^%-5l%$] %v");
|
||||
sinks.push_back(s);
|
||||
}
|
||||
|
||||
if (!filePath_.empty()) {
|
||||
try {
|
||||
auto s = std::make_shared<spdlog::sinks::basic_file_sink_mt>(filePath_, true);
|
||||
s->set_pattern("[%Y-%m-%d %H:%M:%S.%e] [%-5l] %v");
|
||||
sinks.push_back(s);
|
||||
} catch (...) {}
|
||||
}
|
||||
|
||||
const auto prevLevel = log_ ? log_->level() : spdlog::level::info;
|
||||
log_ = std::make_shared<spdlog::logger>("q3", sinks.begin(), sinks.end());
|
||||
log_->set_level(prevLevel);
|
||||
log_->flush_on(spdlog::level::trace); // flush all levels immediately
|
||||
}
|
||||
|
||||
// ── construction ──────────────────────────────────────────────────────────────
|
||||
|
||||
LoggerService::LoggerService() {
|
||||
RebuildLogger();
|
||||
|
||||
// Honor QUAKE3_LOG_LEVEL env var at startup: trace/debug/info/warn/error/off
|
||||
if (const char* env = std::getenv("QUAKE3_LOG_LEVEL")) {
|
||||
const std::string lv(env);
|
||||
if (lv == "trace") log_->set_level(spdlog::level::trace);
|
||||
else if (lv == "debug") log_->set_level(spdlog::level::debug);
|
||||
else if (lv == "info") log_->set_level(spdlog::level::info);
|
||||
else if (lv == "warn") log_->set_level(spdlog::level::warn);
|
||||
else if (lv == "error") log_->set_level(spdlog::level::err);
|
||||
else if (lv == "off") log_->set_level(spdlog::level::off);
|
||||
}
|
||||
}
|
||||
|
||||
// ── ILogger ───────────────────────────────────────────────────────────────────
|
||||
|
||||
void LoggerService::SetLevel(LogLevel level) {
|
||||
// Note: Cannot add trace logging here as it would create recursion
|
||||
impl_->level_.store(level, std::memory_order_relaxed);
|
||||
log_->set_level(ToSpdlog(level));
|
||||
}
|
||||
|
||||
LogLevel LoggerService::GetLevel() const {
|
||||
return impl_->level_.load(std::memory_order_relaxed);
|
||||
return FromSpdlog(log_->level());
|
||||
}
|
||||
|
||||
void LoggerService::SetOutputFile(const std::string& filename) {
|
||||
// Note: Cannot add trace logging here as impl_->SetOutputFile may close the log file
|
||||
std::lock_guard<std::mutex> lock(impl_->mutex_);
|
||||
impl_->SetOutputFile(filename);
|
||||
filePath_ = filename;
|
||||
RebuildLogger();
|
||||
}
|
||||
|
||||
void LoggerService::SetMaxLinesPerFile(size_t maxLines) {
|
||||
// Note: Cannot add trace logging here as it could trigger file rotation during logging
|
||||
std::lock_guard<std::mutex> lock(impl_->mutex_);
|
||||
impl_->SetMaxLinesPerFile(maxLines);
|
||||
void LoggerService::SetMaxLinesPerFile(size_t /*maxLines*/) {
|
||||
// Extend to spdlog::sinks::rotating_file_sink_mt if rotation is needed.
|
||||
}
|
||||
|
||||
void LoggerService::EnableConsoleOutput(bool enable) {
|
||||
// Note: Cannot add trace logging here as it could recursively affect console output settings
|
||||
impl_->consoleEnabled_ = enable;
|
||||
consoleOn_ = enable;
|
||||
RebuildLogger();
|
||||
}
|
||||
|
||||
void LoggerService::Log(LogLevel level, const std::string& message) {
|
||||
if (level < GetLevel()) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(impl_->mutex_);
|
||||
std::string formattedMessage = impl_->FormatMessage(level, message);
|
||||
|
||||
if (impl_->consoleEnabled_) {
|
||||
impl_->WriteToConsole(level, formattedMessage);
|
||||
}
|
||||
|
||||
if (impl_->fileStream_) {
|
||||
impl_->WriteToFile(formattedMessage);
|
||||
}
|
||||
log_->log(ToSpdlog(level), message);
|
||||
}
|
||||
|
||||
void LoggerService::Trace(const std::string& message) {
|
||||
Log(LogLevel::TRACE, message);
|
||||
log_->trace(message);
|
||||
}
|
||||
|
||||
void LoggerService::Trace(const std::string& className, const std::string& methodName, const std::string& args, const std::string& message) {
|
||||
std::string formattedMessage = className + "::" + methodName;
|
||||
if (!args.empty()) {
|
||||
formattedMessage += "(" + args + ")";
|
||||
}
|
||||
if (!message.empty()) {
|
||||
formattedMessage += ": " + message;
|
||||
}
|
||||
Log(LogLevel::TRACE, formattedMessage);
|
||||
void LoggerService::Trace(const std::string& className, const std::string& methodName,
|
||||
const std::string& args, const std::string& message) {
|
||||
if (!log_->should_log(spdlog::level::trace)) return;
|
||||
std::string s = className + "::" + methodName;
|
||||
if (!args.empty()) s += "(" + args + ")";
|
||||
if (!message.empty()) s += " — " + message;
|
||||
log_->trace(s);
|
||||
}
|
||||
|
||||
void LoggerService::Debug(const std::string& message) {
|
||||
Log(LogLevel::DEBUG, message);
|
||||
}
|
||||
|
||||
void LoggerService::Info(const std::string& message) {
|
||||
Log(LogLevel::INFO, message);
|
||||
}
|
||||
|
||||
void LoggerService::Warn(const std::string& message) {
|
||||
Log(LogLevel::WARN, message);
|
||||
}
|
||||
|
||||
void LoggerService::Error(const std::string& message) {
|
||||
Log(LogLevel::ERROR, message);
|
||||
}
|
||||
void LoggerService::Debug(const std::string& message) { log_->debug(message); }
|
||||
void LoggerService::Info (const std::string& message) { log_->info (message); }
|
||||
void LoggerService::Warn (const std::string& message) { log_->warn (message); }
|
||||
void LoggerService::Error(const std::string& message) { log_->error(message); }
|
||||
|
||||
void LoggerService::TraceFunction(const std::string& funcName) {
|
||||
if (GetLevel() <= LogLevel::TRACE) {
|
||||
Trace("Entering " + funcName);
|
||||
}
|
||||
log_->trace("→ {}", funcName);
|
||||
}
|
||||
|
||||
void LoggerService::TraceVariable(const std::string& name, const std::string& value) {
|
||||
if (GetLevel() <= LogLevel::TRACE) {
|
||||
Trace(name + " = " + value);
|
||||
}
|
||||
}
|
||||
|
||||
void LoggerService::TraceVariable(const std::string& name, int value) {
|
||||
TraceVariable(name, std::to_string(value));
|
||||
}
|
||||
|
||||
void LoggerService::TraceVariable(const std::string& name, size_t value) {
|
||||
TraceVariable(name, std::to_string(value));
|
||||
}
|
||||
|
||||
void LoggerService::TraceVariable(const std::string& name, bool value) {
|
||||
TraceVariable(name, std::string(value ? "true" : "false"));
|
||||
}
|
||||
|
||||
void LoggerService::TraceVariable(const std::string& name, float value) {
|
||||
TraceVariable(name, std::to_string(value));
|
||||
}
|
||||
|
||||
void LoggerService::TraceVariable(const std::string& name, double value) {
|
||||
TraceVariable(name, std::to_string(value));
|
||||
log_->trace(" {} = {}", name, value);
|
||||
}
|
||||
void LoggerService::TraceVariable(const std::string& name, int v) { log_->trace(" {} = {}", name, v); }
|
||||
void LoggerService::TraceVariable(const std::string& name, size_t v) { log_->trace(" {} = {}", name, v); }
|
||||
void LoggerService::TraceVariable(const std::string& name, bool v) { log_->trace(" {} = {}", name, v ? "true" : "false"); }
|
||||
void LoggerService::TraceVariable(const std::string& name, float v) { log_->trace(" {} = {:.4f}", name, v); }
|
||||
void LoggerService::TraceVariable(const std::string& name, double v) { log_->trace(" {} = {:.6f}", name, v); }
|
||||
|
||||
} // namespace sdl3cpp::services::impl
|
||||
|
||||
+13
-5
@@ -107,17 +107,25 @@ void WorkflowGpuShaderCompileStep::Execute(const WorkflowStepDefinition& step, W
|
||||
// Load shader binary
|
||||
auto shader_data = LoadBinary(shader_path);
|
||||
|
||||
// For MSL shaders: ensure null terminator so NSString initWithBytes succeeds
|
||||
// on all Metal runtime versions (macOS 26 Metal debug layer can fail otherwise)
|
||||
if (format == SDL_GPU_SHADERFORMAT_MSL) {
|
||||
shader_data.push_back(0);
|
||||
}
|
||||
|
||||
if (logger_) {
|
||||
logger_->Trace("WorkflowGpuShaderCompileStep", "Execute",
|
||||
"path=" + shader_path + ", stage=" + stage_str +
|
||||
", format=" + format_name + ", size=" + std::to_string(shader_data.size()),
|
||||
"Loading shader");
|
||||
logger_->Info("graphics.gpu.shader.compile: loading " + stage_str +
|
||||
" shader from " + shader_path + " (" +
|
||||
std::to_string(shader_data.size()) + " bytes, format=" + format_name + ")");
|
||||
}
|
||||
|
||||
// Create shader
|
||||
SDL_GPUShaderCreateInfo shader_info = {};
|
||||
shader_info.code = shader_data.data();
|
||||
shader_info.code_size = shader_data.size();
|
||||
// Pass size WITHOUT the null terminator for SPIRV/METALLIB; for MSL the extra null is harmless
|
||||
shader_info.code_size = (format == SDL_GPU_SHADERFORMAT_MSL)
|
||||
? shader_data.size() - 1 // exclude the appended null
|
||||
: shader_data.size();
|
||||
shader_info.entrypoint = entrypoint;
|
||||
shader_info.format = format;
|
||||
shader_info.stage = stage;
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <SDL3/SDL_gpu.h>
|
||||
#include <SDL3/SDL.h>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <cstdlib>
|
||||
#include <stdexcept>
|
||||
|
||||
namespace sdl3cpp::services::impl {
|
||||
@@ -54,8 +55,13 @@ void WorkflowGraphicsGpuInitStep::Execute(const WorkflowStepDefinition& step, Wo
|
||||
SDL_GPU_SHADERFORMAT_SPIRV | SDL_GPU_SHADERFORMAT_MSL | SDL_GPU_SHADERFORMAT_DXIL);
|
||||
}
|
||||
|
||||
// Debug mode: SDL sets MTL_DEBUG_LAYER=1 which triggers a macOS 26 Metal
|
||||
// validation bug where newLibraryWithSource receives nil source. Default off;
|
||||
// enable with SDL_GPU_DEBUG=1 env var for explicit GPU validation.
|
||||
const bool debugMode = std::getenv("SDL_GPU_DEBUG") != nullptr;
|
||||
|
||||
// Create GPU device with preferred shader format
|
||||
SDL_GPUDevice* device = SDL_CreateGPUDevice(shader_format, true, driver_name);
|
||||
SDL_GPUDevice* device = SDL_CreateGPUDevice(shader_format, debugMode, driver_name);
|
||||
|
||||
if (!device) {
|
||||
if (logger_) {
|
||||
@@ -65,7 +71,7 @@ void WorkflowGraphicsGpuInitStep::Execute(const WorkflowStepDefinition& step, Wo
|
||||
// Fallback: let SDL auto-select
|
||||
device = SDL_CreateGPUDevice(
|
||||
SDL_GPU_SHADERFORMAT_SPIRV | SDL_GPU_SHADERFORMAT_MSL | SDL_GPU_SHADERFORMAT_DXIL,
|
||||
true, nullptr);
|
||||
debugMode, nullptr);
|
||||
|
||||
if (!device) {
|
||||
throw std::runtime_error("graphics.gpu.init: SDL_CreateGPUDevice failed even with fallback: " +
|
||||
|
||||
@@ -4,21 +4,17 @@
|
||||
#include <SDL3/SDL_gpu.h>
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Real Q3A status-bar layout (from ioq3 cg_draw.c), adapted to 640×360:
|
||||
// Real Q3A status-bar layout (from ioq3 cg_draw.c) at native 640×480.
|
||||
//
|
||||
// Source constants (640×480):
|
||||
// CHAR_WIDTH=32 CHAR_HEIGHT=48 ICON_SIZE=48 TEXT_ICON_SPACE=4
|
||||
// ammo x=0, y=432 (field width 3)
|
||||
// ammo icon x=CHAR_WIDTH*3+TEXT_ICON_SPACE = 100
|
||||
// health x=185, y=432 (field width 3)
|
||||
// head x=185+100=285, y=480-ICON_SIZE*1.25=420, size=60
|
||||
// armor x=370, y=432 (field width 3)
|
||||
// armor icon x=370+100 = 470
|
||||
// CHAR_WIDTH=32 CHAR_HEIGHT=48 ICON_SIZE=48 TEXT_ICON_SPACE=4
|
||||
// ammo x=0, y=432 (field width 3)
|
||||
// ammo icon x=CHAR_WIDTH*3+TEXT_ICON_SPACE = 100
|
||||
// health x=185, y=432 (field width 3)
|
||||
// head x=285, y=420, size=60 (480-ICON_SIZE*1.25)
|
||||
// armor x=370, y=432 (field width 3)
|
||||
// armor icon x=370+100 = 470
|
||||
//
|
||||
// Scale to 640×360: Y *= 360/480 = 0.75; X unchanged.
|
||||
// kCharW=32 kIconSz=36 kHeadSz=45
|
||||
// HUD bar bottom = 360; digits 32px tall → kHudY = 360-32-2 = 326
|
||||
// head y = 360 - 45 - 2 = 313
|
||||
// kScale = kH/480 = 1.0 — all sizes are exact ioq3 pixels.
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
namespace sdl3cpp::services::impl {
|
||||
|
||||
@@ -5,8 +5,20 @@
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <glm/glm.hpp>
|
||||
#include <cmath>
|
||||
#include <cstdlib>
|
||||
#include <string>
|
||||
|
||||
namespace {
|
||||
// BSP entity fields are always JSON strings — helper to safely read as float.
|
||||
float EntFloat(const nlohmann::json& ent, const char* key, float def) {
|
||||
if (!ent.contains(key)) return def;
|
||||
const auto& v = ent[key];
|
||||
if (v.is_number()) return v.get<float>();
|
||||
if (v.is_string()) { try { return std::stof(v.get<std::string>()); } catch (...) {} }
|
||||
return def;
|
||||
}
|
||||
}
|
||||
|
||||
namespace sdl3cpp::services::impl {
|
||||
|
||||
WorkflowQ3MoversInitStep::WorkflowQ3MoversInitStep(std::shared_ptr<ILogger> logger)
|
||||
@@ -39,18 +51,17 @@ void WorkflowQ3MoversInitStep::Execute(const WorkflowStepDefinition& /*step*/, W
|
||||
}
|
||||
|
||||
// Parse angle (degrees, Q3 convention: 0=+X, 90=-Z in XZ plane)
|
||||
const float angleDeg = ent.value("angle", 0.f);
|
||||
const float angleDeg = EntFloat(ent, "angle", 0.f);
|
||||
const float angleRad = angleDeg * (3.14159265f / 180.f);
|
||||
// angle 0 → +X, 90 → -Z (right-hand Y-up)
|
||||
const glm::vec3 moveDir(std::cos(angleRad), 0.f, -std::sin(angleRad));
|
||||
|
||||
// Parse speed and distance
|
||||
const float speed = ent.value("speed", 100.f);
|
||||
const float distRaw = ent.value("distance", 128.f);
|
||||
// Parse speed and distance — all stored as strings in BSP entity lump
|
||||
const float speed = EntFloat(ent, "speed", 100.f);
|
||||
const float distRaw = EntFloat(ent, "distance", 128.f);
|
||||
constexpr float kScale = 0.03125f;
|
||||
const float dist = distRaw * kScale;
|
||||
|
||||
const float wait = ent.value("wait", 2.f);
|
||||
const float wait = EntFloat(ent, "wait", 2.f);
|
||||
const float travelTime = (speed > 0.f) ? dist / (speed * kScale) : 1.f;
|
||||
|
||||
sdl3cpp::q3::Q3Mover m;
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <glm/glm.hpp>
|
||||
#include <cmath>
|
||||
#include <cstdlib>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
@@ -11,6 +12,15 @@ namespace sdl3cpp::services::impl {
|
||||
|
||||
namespace {
|
||||
|
||||
// BSP entity numeric fields are stored as JSON strings.
|
||||
float EntFloat(const nlohmann::json& ent, const char* key, float def) {
|
||||
if (!ent.contains(key)) return def;
|
||||
const auto& v = ent[key];
|
||||
if (v.is_number()) return v.get<float>();
|
||||
if (v.is_string()) { try { return std::stof(v.get<std::string>()); } catch (...) {} }
|
||||
return def;
|
||||
}
|
||||
|
||||
// Parsed representation of a trigger entity
|
||||
struct TriggerEnt {
|
||||
std::string classname;
|
||||
@@ -74,7 +84,7 @@ void WorkflowQ3TriggersCheckStep::Execute(const WorkflowStepDefinition& /*step*/
|
||||
t["classname"] = cls;
|
||||
t["origin"] = nlohmann::json::array({origin.x, origin.y, origin.z});
|
||||
t["target"] = ent.value("target", std::string{});
|
||||
t["dmg"] = ent.value("dmg", 5.f);
|
||||
t["dmg"] = EntFloat(ent, "dmg", 5.f);
|
||||
|
||||
triggerList.push_back(t);
|
||||
}
|
||||
|
||||
@@ -1,225 +1,42 @@
|
||||
#pragma once
|
||||
|
||||
#include "services/interfaces/i_logger.hpp"
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iomanip>
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <sstream>
|
||||
#include <thread>
|
||||
|
||||
#if defined(_WIN32)
|
||||
#include <io.h>
|
||||
#include <fcntl.h>
|
||||
#else
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
#include <spdlog/spdlog.h>
|
||||
#include <spdlog/logger.h>
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
namespace sdl3cpp::services::impl {
|
||||
|
||||
// Implementation class that holds all the logging logic
|
||||
class LoggerImpl {
|
||||
public:
|
||||
static constexpr size_t kDefaultMaxLinesPerFile = 10000;
|
||||
|
||||
std::atomic<LogLevel> level_;
|
||||
bool consoleEnabled_;
|
||||
std::unique_ptr<std::ofstream> fileStream_;
|
||||
std::mutex mutex_;
|
||||
std::filesystem::path baseFilePath_;
|
||||
size_t maxLinesPerFile_;
|
||||
size_t currentLineCount_;
|
||||
size_t currentFileIndex_;
|
||||
int fileDescriptor_;
|
||||
|
||||
LoggerImpl()
|
||||
: level_(LogLevel::INFO),
|
||||
consoleEnabled_(true),
|
||||
maxLinesPerFile_(kDefaultMaxLinesPerFile),
|
||||
currentLineCount_(0),
|
||||
currentFileIndex_(0),
|
||||
fileDescriptor_(-1) {}
|
||||
|
||||
~LoggerImpl() {
|
||||
if (fileStream_) {
|
||||
fileStream_->close();
|
||||
}
|
||||
CloseFileDescriptor();
|
||||
}
|
||||
|
||||
std::string LevelToString(LogLevel level) const {
|
||||
switch (level) {
|
||||
case LogLevel::TRACE: return "TRACE";
|
||||
case LogLevel::DEBUG: return "DEBUG";
|
||||
case LogLevel::INFO: return "INFO";
|
||||
case LogLevel::WARN: return "WARN";
|
||||
case LogLevel::ERROR: return "ERROR";
|
||||
default: return "UNKNOWN";
|
||||
}
|
||||
}
|
||||
|
||||
std::string FormatMessage(LogLevel level, const std::string& message) {
|
||||
auto now = std::chrono::system_clock::now();
|
||||
auto time = std::chrono::system_clock::to_time_t(now);
|
||||
auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
now.time_since_epoch()) % 1000;
|
||||
|
||||
std::ostringstream oss;
|
||||
oss << std::put_time(std::localtime(&time), "%Y-%m-%d %H:%M:%S")
|
||||
<< '.' << std::setfill('0') << std::setw(3) << ms.count()
|
||||
<< " [" << LevelToString(level) << "] "
|
||||
<< message;
|
||||
return oss.str();
|
||||
}
|
||||
|
||||
void WriteToConsole(LogLevel level, const std::string& message) {
|
||||
if (level >= LogLevel::ERROR) {
|
||||
std::cerr << message << std::endl;
|
||||
} else {
|
||||
std::cout << message << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
void WriteToFile(const std::string& message) {
|
||||
if (fileStream_) {
|
||||
*fileStream_ << message << std::endl;
|
||||
fileStream_->flush();
|
||||
SyncFile();
|
||||
++currentLineCount_;
|
||||
RotateFileIfNeeded();
|
||||
}
|
||||
}
|
||||
|
||||
void SetOutputFile(const std::string& filename) {
|
||||
if (fileStream_) {
|
||||
fileStream_->close();
|
||||
}
|
||||
fileStream_.reset();
|
||||
CloseFileDescriptor();
|
||||
|
||||
baseFilePath_ = filename;
|
||||
currentLineCount_ = 0;
|
||||
currentFileIndex_ = 0;
|
||||
|
||||
if (!baseFilePath_.empty()) {
|
||||
OpenLogFile(currentFileIndex_);
|
||||
}
|
||||
}
|
||||
|
||||
void SetMaxLinesPerFile(size_t maxLines) {
|
||||
maxLinesPerFile_ = maxLines;
|
||||
}
|
||||
|
||||
std::filesystem::path BuildLogFilePath(size_t index) const {
|
||||
if (baseFilePath_.empty() || index == 0) {
|
||||
return baseFilePath_;
|
||||
}
|
||||
|
||||
std::filesystem::path basePath(baseFilePath_);
|
||||
std::string stem = basePath.stem().string();
|
||||
std::string extension = basePath.extension().string();
|
||||
std::string rotatedName = stem + "." + std::to_string(index) + extension;
|
||||
|
||||
return basePath.parent_path() / rotatedName;
|
||||
}
|
||||
|
||||
void OpenLogFile(size_t index) {
|
||||
if (baseFilePath_.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::filesystem::path logPath = BuildLogFilePath(index);
|
||||
fileStream_ = std::make_unique<std::ofstream>(logPath, std::ios::out | std::ios::trunc);
|
||||
if (!fileStream_->is_open()) {
|
||||
std::cerr << "Failed to open log file: " << logPath.string() << std::endl;
|
||||
fileStream_.reset();
|
||||
CloseFileDescriptor();
|
||||
return;
|
||||
}
|
||||
OpenFileDescriptor(logPath);
|
||||
}
|
||||
|
||||
void RotateFileIfNeeded() {
|
||||
if (maxLinesPerFile_ == 0 || currentLineCount_ < maxLinesPerFile_) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (fileStream_) {
|
||||
fileStream_->close();
|
||||
}
|
||||
CloseFileDescriptor();
|
||||
|
||||
++currentFileIndex_;
|
||||
currentLineCount_ = 0;
|
||||
OpenLogFile(currentFileIndex_);
|
||||
}
|
||||
|
||||
void OpenFileDescriptor(const std::filesystem::path& logPath) {
|
||||
CloseFileDescriptor();
|
||||
const std::string pathString = logPath.string();
|
||||
#if defined(_WIN32)
|
||||
fileDescriptor_ = _open(pathString.c_str(), _O_WRONLY | _O_APPEND);
|
||||
#else
|
||||
fileDescriptor_ = ::open(pathString.c_str(), O_WRONLY | O_APPEND);
|
||||
#endif
|
||||
if (fileDescriptor_ < 0) {
|
||||
std::cerr << "Failed to open log file descriptor for sync: " << pathString << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
void CloseFileDescriptor() {
|
||||
if (fileDescriptor_ < 0) {
|
||||
return;
|
||||
}
|
||||
#if defined(_WIN32)
|
||||
_close(fileDescriptor_);
|
||||
#else
|
||||
::close(fileDescriptor_);
|
||||
#endif
|
||||
fileDescriptor_ = -1;
|
||||
}
|
||||
|
||||
void SyncFile() {
|
||||
if (fileDescriptor_ < 0) {
|
||||
return;
|
||||
}
|
||||
#if defined(_WIN32)
|
||||
_commit(fileDescriptor_);
|
||||
#else
|
||||
::fsync(fileDescriptor_);
|
||||
#endif
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Logger service implementation.
|
||||
* @brief spdlog-backed ILogger implementation.
|
||||
*
|
||||
* Contains the full logging implementation, no longer wrapping a singleton.
|
||||
* Small, focused service (~200 lines) for application logging.
|
||||
* Console sink always active; file sink added on SetOutputFile().
|
||||
* Log level controlled at runtime via QUAKE3_LOG_LEVEL env var
|
||||
* (trace / debug / info / warn / error) or programmatically via SetLevel().
|
||||
*/
|
||||
class LoggerService : public ILogger {
|
||||
public:
|
||||
LoggerService();
|
||||
~LoggerService() override = default;
|
||||
|
||||
// ILogger interface
|
||||
void SetLevel(LogLevel level) override;
|
||||
LogLevel GetLevel() const override;
|
||||
void SetOutputFile(const std::string& filename) override;
|
||||
void SetMaxLinesPerFile(size_t maxLines) override;
|
||||
void SetMaxLinesPerFile(size_t maxLines) override; // no-op: spdlog handles rotation
|
||||
void EnableConsoleOutput(bool enable) override;
|
||||
|
||||
void Log(LogLevel level, const std::string& message) override;
|
||||
void Trace(const std::string& message) override;
|
||||
void Trace(const std::string& className, const std::string& methodName, const std::string& args = "", const std::string& message = "") override;
|
||||
void Trace(const std::string& className, const std::string& methodName,
|
||||
const std::string& args = "", const std::string& message = "") override;
|
||||
void Debug(const std::string& message) override;
|
||||
void Info(const std::string& message) override;
|
||||
void Warn(const std::string& message) override;
|
||||
void Error(const std::string& message) override;
|
||||
|
||||
void TraceFunction(const std::string& funcName) override;
|
||||
void TraceVariable(const std::string& name, const std::string& value) override;
|
||||
void TraceVariable(const std::string& name, int value) override;
|
||||
@@ -229,7 +46,13 @@ public:
|
||||
void TraceVariable(const std::string& name, double value) override;
|
||||
|
||||
private:
|
||||
std::unique_ptr<LoggerImpl> impl_;
|
||||
static spdlog::level::level_enum ToSpdlog(LogLevel level);
|
||||
static LogLevel FromSpdlog(spdlog::level::level_enum l);
|
||||
void RebuildLogger();
|
||||
|
||||
std::string filePath_;
|
||||
bool consoleOn_ = true;
|
||||
std::shared_ptr<spdlog::logger> log_;
|
||||
};
|
||||
|
||||
} // namespace sdl3cpp::services::impl
|
||||
|
||||
@@ -13,7 +13,7 @@ struct ArenaInfo { std::string longname; std::string bot; };
|
||||
using ArenaMap = std::unordered_map<std::string, ArenaInfo>;
|
||||
using LevelshotCache = std::unordered_map<std::string, SDL_Texture*>;
|
||||
|
||||
static constexpr int kW = 640, kH = 360;
|
||||
static constexpr int kW = 640, kH = 480; // ioq3 native virtual resolution
|
||||
static constexpr int kGlyphSrc = 16;
|
||||
static constexpr int kPropHeight = 27;
|
||||
static constexpr int kPropGap = 3;
|
||||
|
||||
Reference in New Issue
Block a user