diff --git a/assets/music/Every Block You Take.ogg b/assets/music/Every Block You Take.ogg new file mode 100644 index 0000000..f3857b1 Binary files /dev/null and b/assets/music/Every Block You Take.ogg differ diff --git a/assets/music/amazing.ogg b/assets/music/amazing.ogg new file mode 100644 index 0000000..fe54410 Binary files /dev/null and b/assets/music/amazing.ogg differ diff --git a/assets/music/boom_tetris.ogg b/assets/music/boom_tetris.ogg new file mode 100644 index 0000000..00e9c3d Binary files /dev/null and b/assets/music/boom_tetris.ogg differ diff --git a/assets/music/great_move.ogg b/assets/music/great_move.ogg new file mode 100644 index 0000000..4045c7e Binary files /dev/null and b/assets/music/great_move.ogg differ diff --git a/assets/music/hard_drop_001.ogg b/assets/music/hard_drop_001.ogg new file mode 100644 index 0000000..81d7f32 Binary files /dev/null and b/assets/music/hard_drop_001.ogg differ diff --git a/assets/music/impressive.ogg b/assets/music/impressive.ogg new file mode 100644 index 0000000..e0eecf9 Binary files /dev/null and b/assets/music/impressive.ogg differ diff --git a/assets/music/keep_that_ryhtm.ogg b/assets/music/keep_that_ryhtm.ogg new file mode 100644 index 0000000..562c2f3 Binary files /dev/null and b/assets/music/keep_that_ryhtm.ogg differ diff --git a/assets/music/lets_go.ogg b/assets/music/lets_go.ogg new file mode 100644 index 0000000..caab19a Binary files /dev/null and b/assets/music/lets_go.ogg differ diff --git a/assets/music/new_level.ogg b/assets/music/new_level.ogg new file mode 100644 index 0000000..116b95d Binary files /dev/null and b/assets/music/new_level.ogg differ diff --git a/assets/music/nice_combo.ogg b/assets/music/nice_combo.ogg new file mode 100644 index 0000000..3b8c400 Binary files /dev/null and b/assets/music/nice_combo.ogg differ diff --git a/assets/music/smooth_clear.ogg b/assets/music/smooth_clear.ogg new file mode 100644 index 0000000..87bf3d7 Binary files /dev/null and b/assets/music/smooth_clear.ogg differ diff --git a/assets/music/triple_strike.ogg b/assets/music/triple_strike.ogg new file mode 100644 index 0000000..f96843c Binary files /dev/null and b/assets/music/triple_strike.ogg differ diff --git a/assets/music/well_played.ogg b/assets/music/well_played.ogg new file mode 100644 index 0000000..5e199e0 Binary files /dev/null and b/assets/music/well_played.ogg differ diff --git a/assets/music/wonderful.ogg b/assets/music/wonderful.ogg new file mode 100644 index 0000000..1bfee75 Binary files /dev/null and b/assets/music/wonderful.ogg differ diff --git a/assets/music/you_fire.ogg b/assets/music/you_fire.ogg new file mode 100644 index 0000000..3fabd06 Binary files /dev/null and b/assets/music/you_fire.ogg differ diff --git a/assets/music/you_re_unstoppable.ogg b/assets/music/you_re_unstoppable.ogg new file mode 100644 index 0000000..ed0fdf8 Binary files /dev/null and b/assets/music/you_re_unstoppable.ogg differ diff --git a/convert_instructions.bat b/convert_instructions.bat index bf2da84..41a3dea 100644 --- a/convert_instructions.bat +++ b/convert_instructions.bat @@ -1,14 +1,14 @@ @echo off -echo Converting MP3 files to WAV using Windows Media Player... +echo Convert MP3 files to OGG (preferred) for cross-platform playback... echo. REM Check if we have access to Windows Media Format SDK set MUSIC_DIR=assets\music REM List of MP3 files to convert -set FILES=amazing.mp3 boom_tetris.mp3 great_move.mp3 impressive.mp3 keep_that_ryhtm.mp3 lets_go.mp3 nice_combo.mp3 smooth_clear.mp3 triple_strike.mp3 well_played.mp3 wonderful.mp3 you_fire.mp3 you_re_unstoppable.mp3 +set FILES=amazing.mp3 boom_tetris.mp3 great_move.mp3 impressive.mp3 keep_that_ryhtm.mp3 lets_go.mp3 nice_combo.mp3 smooth_clear.mp3 triple_strike.mp3 well_played.mp3 wonderful.mp3 you_fire.mp3 you_re_unstoppable.mp3 hard_drop_001.mp3 new_level.mp3 -echo Please convert these MP3 files to WAV format manually: +echo Please convert these MP3 files to OGG Vorbis manually (or run convert_to_ogg.ps1 on Windows): echo. for %%f in (%FILES%) do ( echo - %MUSIC_DIR%\%%f @@ -17,13 +17,12 @@ for %%f in (%FILES%) do ( echo. echo Recommended settings for conversion: echo - Sample Rate: 44100 Hz -echo - Bit Depth: 16-bit echo - Channels: Stereo (2) -echo - Format: PCM WAV +echo - Use OGG Vorbis quality ~4 (or convert to FLAC if you prefer lossless) echo. echo You can use: echo - Audacity (free): https://www.audacityteam.org/ echo - VLC Media Player (free): Media ^> Convert/Save -echo - Any audio converter software +echo - ffmpeg (CLI): ffmpeg -i input.mp3 -c:a libvorbis -qscale:a 4 output.ogg echo. pause diff --git a/convert_to_ogg.ps1 b/convert_to_ogg.ps1 new file mode 100644 index 0000000..35b5da8 --- /dev/null +++ b/convert_to_ogg.ps1 @@ -0,0 +1,45 @@ +# Convert MP3 sound effects to OGG Vorbis format for cross-platform playback +# Requires ffmpeg (https://ffmpeg.org/). OGG keeps files small while SDL's decoders +# work everywhere the game ships. + +$musicDir = "assets\music" +$sourceFiles = @( + "amazing.mp3", + "boom_tetris.mp3", + "great_move.mp3", + "impressive.mp3", + "keep_that_ryhtm.mp3", + "lets_go.mp3", + "nice_combo.mp3", + "smooth_clear.mp3", + "triple_strike.mp3", + "well_played.mp3", + "wonderful.mp3", + "you_fire.mp3", + "you_re_unstoppable.mp3", + "hard_drop_001.mp3", + "new_level.mp3", + "Every Block You Take.mp3" +) + +if (!(Get-Command ffmpeg -ErrorAction SilentlyContinue)) { + Write-Host "ffmpeg is required. Install via https://ffmpeg.org/ or winget install Gyan.FFmpeg" -ForegroundColor Red + exit 1 +} + +Write-Host "Converting MP3 sound effects to OGG..." -ForegroundColor Cyan +foreach ($file in $sourceFiles) { + $src = Join-Path $musicDir $file + if (!(Test-Path $src)) { + Write-Host "Skipping (missing): $src" -ForegroundColor Yellow + continue + } + $ogg = ($file -replace ".mp3$", ".ogg") + $dest = Join-Path $musicDir $ogg + Write-Host "-> $ogg" -ForegroundColor Green + & ffmpeg -y -i $src -c:a libvorbis -qscale:a 4 $dest > $null 2>&1 + if ($LASTEXITCODE -ne 0) { + Write-Host "Failed to convert $file" -ForegroundColor Red + } +} +Write-Host "Conversion complete." -ForegroundColor Green diff --git a/convert_to_wav.ps1 b/convert_to_wav.ps1 index 577f52b..d77e27f 100644 --- a/convert_to_wav.ps1 +++ b/convert_to_wav.ps1 @@ -1,63 +1,11 @@ -# Convert MP3 sound effects to WAV format -# This script converts all MP3 sound effect files to WAV for better compatibility +# Deprecated shim: point developers to the OGG conversion workflow +Write-Host "convert_to_wav.ps1 is deprecated." -ForegroundColor Yellow +Write-Host "Use convert_to_ogg.ps1 for generating assets with OGG Vorbis." -ForegroundColor Yellow -$musicDir = "assets\music" -$mp3Files = @( - "amazing.mp3", - "boom_tetris.mp3", - "great_move.mp3", - "impressive.mp3", - "keep_that_ryhtm.mp3", - "lets_go.mp3", - "nice_combo.mp3", - "smooth_clear.mp3", - "triple_strike.mp3", - "well_played.mp3", - "wonderful.mp3", - "you_fire.mp3", - "you_re_unstoppable.mp3" -) - -Write-Host "Converting MP3 sound effects to WAV format..." -ForegroundColor Green - -foreach ($mp3File in $mp3Files) { - $mp3Path = Join-Path $musicDir $mp3File - $wavFile = $mp3File -replace "\.mp3$", ".wav" - $wavPath = Join-Path $musicDir $wavFile - - if (Test-Path $mp3Path) { - Write-Host "Converting $mp3File to $wavFile..." -ForegroundColor Yellow - - # Try ffmpeg first (most common) - $ffmpegResult = $null - try { - $ffmpegResult = & ffmpeg -i $mp3Path -acodec pcm_s16le -ar 44100 -ac 2 $wavPath -y 2>&1 - if ($LASTEXITCODE -eq 0) { - Write-Host "✓ Successfully converted $mp3File" -ForegroundColor Green - continue - } - } catch { - # FFmpeg not available, try other methods - } - - # Try Windows Media Format SDK (if available) - try { - Add-Type -AssemblyName System.Windows.Forms - Add-Type -AssemblyName Microsoft.VisualBasic - - # Use Windows built-in audio conversion - $shell = New-Object -ComObject Shell.Application - # This is a fallback method - may not work on all systems - Write-Host "⚠ FFmpeg not found. Please install FFmpeg or convert manually." -ForegroundColor Red - } catch { - Write-Host "⚠ Could not convert $mp3File automatically." -ForegroundColor Red - } - } else { - Write-Host "⚠ File not found: $mp3Path" -ForegroundColor Red - } +$oggScript = Join-Path $PSScriptRoot "convert_to_ogg.ps1" +if (Test-Path $oggScript) { + & $oggScript +} else { + Write-Host "Missing convert_to_ogg.ps1" -ForegroundColor Red + exit 1 } - -Write-Host "`nConversion complete! If FFmpeg was not found, please:" -ForegroundColor Cyan -Write-Host "1. Install FFmpeg: https://ffmpeg.org/download.html" -ForegroundColor White -Write-Host "2. Or use an audio converter like Audacity" -ForegroundColor White -Write-Host "3. Convert to: 44.1kHz, 16-bit, Stereo WAV" -ForegroundColor White diff --git a/settings.ini b/settings.ini index 200523e..384ce2a 100644 --- a/settings.ini +++ b/settings.ini @@ -12,7 +12,7 @@ Sound=1 SmoothScroll=1 [Player] -Name=GREGOR +Name=PLAYER [Debug] Enabled=1 diff --git a/src/core/application/ApplicationManager.cpp b/src/core/application/ApplicationManager.cpp index c50bdd4..d6e26d6 100644 --- a/src/core/application/ApplicationManager.cpp +++ b/src/core/application/ApplicationManager.cpp @@ -558,16 +558,20 @@ bool ApplicationManager::initializeGame() { Audio::instance().init(); // Discover available tracks (up to 100) and queue for background loading m_totalTracks = 0; + std::vector trackPaths; + trackPaths.reserve(100); for (int i = 1; i <= 100; ++i) { - char buf[128]; - std::snprintf(buf, sizeof(buf), "assets/music/music%03d.mp3", i); - // Use simple file existence check via std::filesystem - if (std::filesystem::exists(buf)) { - Audio::instance().addTrackAsync(buf); - ++m_totalTracks; - } else { + char base[128]; + std::snprintf(base, sizeof(base), "assets/music/music%03d", i); + std::string path = AssetPath::resolveWithExtensions(base, { ".mp3" }); + if (path.empty()) { break; } + trackPaths.push_back(path); + } + m_totalTracks = static_cast(trackPaths.size()); + for (const auto& path : trackPaths) { + Audio::instance().addTrackAsync(path); } if (m_totalTracks > 0) { Audio::instance().startBackgroundLoading(); diff --git a/src/core/assets/AssetManager.cpp b/src/core/assets/AssetManager.cpp index be7b210..2ad7be8 100644 --- a/src/core/assets/AssetManager.cpp +++ b/src/core/assets/AssetManager.cpp @@ -208,27 +208,14 @@ bool AssetManager::loadSoundEffectWithFallback(const std::string& id, const std: return false; } - // Try WAV first, then MP3 fallback (matching main.cpp pattern) - std::string wavPath = "assets/music/" + baseName + ".wav"; - std::string mp3Path = "assets/music/" + baseName + ".mp3"; - - // Check WAV first - if (fileExists(wavPath)) { - if (m_soundSystem->loadSound(id, wavPath)) { - logInfo("Loaded sound effect: " + id + " from " + wavPath + " (WAV)"); - return true; - } + const std::string basePath = "assets/music/" + baseName; + std::string resolved = AssetPath::resolveWithExtensions(basePath, { ".wav", ".mp3" }); + if (!resolved.empty() && m_soundSystem->loadSound(id, resolved)) { + logInfo("Loaded sound effect: " + id + " from " + resolved); + return true; } - // Fallback to MP3 - if (fileExists(mp3Path)) { - if (m_soundSystem->loadSound(id, mp3Path)) { - logInfo("Loaded sound effect: " + id + " from " + mp3Path + " (MP3 fallback)"); - return true; - } - } - - setError("Failed to load sound effect: " + id + " (tried both WAV and MP3)"); + setError("Failed to load sound effect: " + id + " (no supported audio extension found)"); return false; } diff --git a/src/main.cpp b/src/main.cpp index 8b5ba9b..5c71528 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include "audio/Audio.h" #include "audio/SoundEffect.h" @@ -682,11 +683,11 @@ int main(int, char **) } SDL_SetRenderVSync(renderer, 1); -#if defined(__APPLE__) - // On macOS bundles launched from Finder start in /, so re-root relative paths. if (const char* basePathRaw = SDL_GetBasePath()) { std::filesystem::path exeDir(basePathRaw); - SDL_free(const_cast(basePathRaw)); + AssetPath::setBasePath(exeDir.string()); +#if defined(__APPLE__) + // On macOS bundles launched from Finder start in /, so re-root relative paths. std::error_code ec; std::filesystem::current_path(exeDir, ec); if (ec) { @@ -694,12 +695,13 @@ int main(int, char **) "Failed to set working directory to %s: %s", exeDir.string().c_str(), ec.message().c_str()); } +#endif + SDL_free(const_cast(basePathRaw)); } else { SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "SDL_GetBasePath() failed; asset lookups rely on current directory: %s", SDL_GetError()); } -#endif FontAtlas font; font.init("FreeSans.ttf", 24); @@ -709,10 +711,15 @@ int main(int, char **) pixelFont.init("assets/fonts/PressStart2P-Regular.ttf", 16); ScoreManager scores; - // Load scores asynchronously to prevent startup hang due to network request - std::thread([&scores]() { + // Load scores asynchronously but keep the worker alive until shutdown to avoid lifetime issues + std::jthread scoreLoader([&scores]() { scores.load(); - }).detach(); + }); + const auto ensureScoresLoaded = [&]() { + if (scoreLoader.joinable()) { + scoreLoader.join(); + } + }; Starfield starfield; starfield.init(200, LOGICAL_W, LOGICAL_H); Starfield3D starfield3D; @@ -771,9 +778,19 @@ int main(int, char **) // Initialize sound effects system SoundEffectManager::instance().init(); - - // Load sound effects - SoundEffectManager::instance().loadSound("clear_line", "assets/music/clear_line.wav"); + + auto loadAudioAsset = [](const std::string& basePath, const std::string& id) { + std::string resolved = AssetPath::resolveWithExtensions(basePath, { ".wav", ".mp3" }); + if (resolved.empty()) { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Missing audio asset for %s (base %s)", id.c_str(), basePath.c_str()); + return; + } + if (!SoundEffectManager::instance().loadSound(id, resolved)) { + SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to load %s from %s", id.c_str(), resolved.c_str()); + } + }; + + loadAudioAsset("assets/music/clear_line", "clear_line"); // Load voice lines for line clears using WAV files (with MP3 fallback) std::vector singleSounds = {"well_played", "smooth_clear", "great_move"}; @@ -789,49 +806,25 @@ int main(int, char **) appendVoices(tripleSounds); appendVoices(tetrisSounds); - // Helper function to load sound with WAV/MP3 fallback and file existence check - auto loadSoundWithFallback = [&](const std::string& id, const std::string& baseName) { - std::string wavPath = "assets/music/" + baseName + ".wav"; - std::string mp3Path = "assets/music/" + baseName + ".mp3"; - - // Check if WAV file exists first - SDL_IOStream* wavFile = SDL_IOFromFile(wavPath.c_str(), "rb"); - if (wavFile) { - SDL_CloseIO(wavFile); - if (SoundEffectManager::instance().loadSound(id, wavPath)) { - (void)0; - return; - } - } - - // Fallback to MP3 if WAV doesn't exist or fails to load - SDL_IOStream* mp3File = SDL_IOFromFile(mp3Path.c_str(), "rb"); - if (mp3File) { - SDL_CloseIO(mp3File); - if (SoundEffectManager::instance().loadSound(id, mp3Path)) { - (void)0; - return; - } - } - - SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to load sound: %s (tried both WAV and MP3)", id.c_str()); + auto loadVoice = [&](const std::string& id, const std::string& baseName) { + loadAudioAsset("assets/music/" + baseName, id); }; - - loadSoundWithFallback("nice_combo", "nice_combo"); - loadSoundWithFallback("you_fire", "you_fire"); - loadSoundWithFallback("well_played", "well_played"); - loadSoundWithFallback("keep_that_ryhtm", "keep_that_ryhtm"); - loadSoundWithFallback("great_move", "great_move"); - loadSoundWithFallback("smooth_clear", "smooth_clear"); - loadSoundWithFallback("impressive", "impressive"); - loadSoundWithFallback("triple_strike", "triple_strike"); - loadSoundWithFallback("amazing", "amazing"); - loadSoundWithFallback("you_re_unstoppable", "you_re_unstoppable"); - loadSoundWithFallback("boom_tetris", "boom_tetris"); - loadSoundWithFallback("wonderful", "wonderful"); - loadSoundWithFallback("lets_go", "lets_go"); // For level up - loadSoundWithFallback("hard_drop", "hard_drop_001"); - loadSoundWithFallback("new_level", "new_level"); + + loadVoice("nice_combo", "nice_combo"); + loadVoice("you_fire", "you_fire"); + loadVoice("well_played", "well_played"); + loadVoice("keep_that_ryhtm", "keep_that_ryhtm"); + loadVoice("great_move", "great_move"); + loadVoice("smooth_clear", "smooth_clear"); + loadVoice("impressive", "impressive"); + loadVoice("triple_strike", "triple_strike"); + loadVoice("amazing", "amazing"); + loadVoice("you_re_unstoppable", "you_re_unstoppable"); + loadVoice("boom_tetris", "boom_tetris"); + loadVoice("wonderful", "wonderful"); + loadVoice("lets_go", "lets_go"); + loadVoice("hard_drop", "hard_drop_001"); + loadVoice("new_level", "new_level"); bool suppressLineVoiceForLevelUp = false; @@ -1122,6 +1115,7 @@ int main(int, char **) playerName.pop_back(); } else if (e.key.scancode == SDL_SCANCODE_RETURN || e.key.scancode == SDL_SCANCODE_KP_ENTER) { if (playerName.empty()) playerName = "PLAYER"; + ensureScoresLoaded(); scores.submit(game.score(), game.lines(), game.level(), game.elapsed(), playerName); Settings::instance().setPlayerName(playerName); isNewHighScore = false; @@ -1379,6 +1373,7 @@ int main(int, char **) SDL_StartTextInput(window); } else { isNewHighScore = false; + ensureScoresLoaded(); scores.submit(game.score(), game.lines(), game.level(), game.elapsed()); } state = AppState::GameOver; @@ -1396,25 +1391,21 @@ int main(int, char **) // Count actual music files first totalTracks = 0; - for (int i = 1; i <= 100; ++i) { // Check up to 100 files - char buf[64]; - std::snprintf(buf, sizeof(buf), "assets/music/music%03d.mp3", i); - - // Check if file exists - SDL_IOStream* file = SDL_IOFromFile(buf, "rb"); - if (file) { - SDL_CloseIO(file); - totalTracks++; - } else { - break; // No more consecutive files + std::vector trackPaths; + trackPaths.reserve(100); + for (int i = 1; i <= 100; ++i) { + char base[64]; + std::snprintf(base, sizeof(base), "assets/music/music%03d", i); + std::string path = AssetPath::resolveWithExtensions(base, { ".mp3" }); + if (path.empty()) { + break; } + trackPaths.push_back(path); } - - // Add all found tracks to the background loading queue - for (int i = 1; i <= totalTracks; ++i) { - char buf[64]; - std::snprintf(buf, sizeof(buf), "assets/music/music%03d.mp3", i); - Audio::instance().addTrackAsync(buf); + totalTracks = static_cast(trackPaths.size()); + + for (const auto& track : trackPaths) { + Audio::instance().addTrackAsync(track); } // Start background loading thread @@ -1470,7 +1461,12 @@ int main(int, char **) static bool menuTrackLoaded = false; if (!menuTrackLoaded) { std::thread([]() { - Audio::instance().setMenuTrack("assets/music/Every Block You Take.mp3"); + std::string menuTrack = AssetPath::resolveWithExtensions("assets/music/Every Block You Take", { ".mp3" }); + if (!menuTrack.empty()) { + Audio::instance().setMenuTrack(menuTrack); + } else { + SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "Menu track not found (Every Block You Take)"); + } }).detach(); menuTrackLoaded = true; } @@ -1804,6 +1800,7 @@ int main(int, char **) // 4. Draw Text // 4. Draw Text // Title + ensureScoresLoaded(); bool realHighScore = scores.isHighScore(game.score()); const char* title = realHighScore ? "NEW HIGH SCORE!" : "GAME OVER"; int tW=0, tH=0; pixelFont.measure(title, 2.0f, tW, tH); diff --git a/src/utils/ImagePathResolver.h b/src/utils/ImagePathResolver.h index 5fc442b..90c1f7f 100644 --- a/src/utils/ImagePathResolver.h +++ b/src/utils/ImagePathResolver.h @@ -2,17 +2,27 @@ #include #include +#include +#include #include namespace AssetPath { -inline bool fileExists(const std::string& path) { - if (path.empty()) { +inline std::string& baseDirectory() { + static std::string base; + return base; +} + +inline void setBasePath(std::string path) { + baseDirectory() = std::move(path); +} + +inline bool tryOpenFile(const std::string& candidate) { + if (candidate.empty()) { return false; } - - SDL_IOStream* file = SDL_IOFromFile(path.c_str(), "rb"); + SDL_IOStream* file = SDL_IOFromFile(candidate.c_str(), "rb"); if (file) { SDL_CloseIO(file); return true; @@ -20,13 +30,58 @@ inline bool fileExists(const std::string& path) { return false; } +inline bool fileExists(const std::string& path) { + if (path.empty()) { + return false; + } + + if (tryOpenFile(path)) { + return true; + } + + std::filesystem::path p(path); + if (!p.is_absolute()) { + const std::string& base = baseDirectory(); + if (!base.empty()) { + std::filesystem::path combined = std::filesystem::path(base) / p; + if (tryOpenFile(combined.string())) { + return true; + } + } + } + return false; +} + +inline std::string resolveWithBase(const std::string& path) { + if (path.empty()) { + return path; + } + + if (tryOpenFile(path)) { + return path; + } + + std::filesystem::path p(path); + if (!p.is_absolute()) { + const std::string& base = baseDirectory(); + if (!base.empty()) { + std::filesystem::path combined = std::filesystem::path(base) / p; + std::string combinedStr = combined.string(); + if (tryOpenFile(combinedStr)) { + return combinedStr; + } + } + } + return path; +} + inline std::string resolveImagePath(const std::string& originalPath) { if (originalPath.empty()) { return originalPath; } - if (fileExists(originalPath)) { - return originalPath; + if (auto resolved = resolveWithBase(originalPath); resolved != originalPath || fileExists(resolved)) { + return resolved; } const std::size_t dot = originalPath.find_last_of('.'); @@ -45,12 +100,41 @@ inline std::string resolveImagePath(const std::string& originalPath) { if (candidate == originalPath) { continue; } - if (fileExists(candidate)) { - return candidate; + std::string resolvedCandidate = resolveWithBase(candidate); + if (resolvedCandidate != candidate || fileExists(resolvedCandidate)) { + return resolvedCandidate; } } return originalPath; } +inline std::string resolveWithExtensions(const std::string& basePathWithoutExt, std::initializer_list extensions) { + for (const char* ext : extensions) { + std::string candidate = basePathWithoutExt + ext; + std::string resolved = resolveWithBase(candidate); + if (resolved != candidate || fileExists(resolved)) { + return resolved; + } + } + return {}; +} + +inline std::string resolveAudioPath(const std::string& basePathWithoutExt) { + static constexpr std::array AUDIO_EXTENSIONS = { + ".ogg", + ".flac", + ".wav", + ".mp3" + }; + for (const char* ext : AUDIO_EXTENSIONS) { + std::string candidate = basePathWithoutExt + ext; + std::string resolved = resolveWithBase(candidate); + if (resolved != candidate || fileExists(resolved)) { + return resolved; + } + } + return {}; +} + } // namespace AssetPath