Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e35aeee5ab | |||
| b46246b74f | |||
| 7f07036f07 | |||
| da32b5be1a | |||
| d206b7b1af | |||
| 69b3a60564 | |||
| dc8a317837 | |||
| dd0ac54ee5 | |||
| 33994ec9db | |||
| 344a27a336 | |||
| 0e4064a187 |
+38
-1
@@ -29,7 +29,7 @@ find_package(SDL3_image CONFIG REQUIRED)
|
||||
find_package(cpr CONFIG REQUIRED)
|
||||
find_package(nlohmann_json CONFIG REQUIRED)
|
||||
|
||||
add_executable(tetris
|
||||
set(TETRIS_SOURCES
|
||||
src/main.cpp
|
||||
src/gameplay/core/Game.cpp
|
||||
src/core/GravityManager.cpp
|
||||
@@ -58,12 +58,24 @@ add_executable(tetris
|
||||
src/states/PlayingState.cpp
|
||||
)
|
||||
|
||||
if(APPLE)
|
||||
add_executable(tetris MACOSX_BUNDLE ${TETRIS_SOURCES})
|
||||
else()
|
||||
add_executable(tetris ${TETRIS_SOURCES})
|
||||
endif()
|
||||
|
||||
if (WIN32)
|
||||
# Embed the application icon into the executable
|
||||
set_source_files_properties(src/app_icon.rc PROPERTIES LANGUAGE RC)
|
||||
target_sources(tetris PRIVATE src/app_icon.rc)
|
||||
endif()
|
||||
|
||||
if(APPLE)
|
||||
set_target_properties(tetris PROPERTIES
|
||||
MACOSX_BUNDLE_INFO_PLIST "${CMAKE_SOURCE_DIR}/cmake/MacBundleInfo.plist.in"
|
||||
)
|
||||
endif()
|
||||
|
||||
if (WIN32)
|
||||
# Ensure favicon.ico is available in the build directory for the resource compiler
|
||||
set(FAVICON_SRC "${CMAKE_SOURCE_DIR}/assets/favicon/favicon.ico")
|
||||
@@ -88,6 +100,31 @@ if (WIN32)
|
||||
)
|
||||
endif()
|
||||
|
||||
if(APPLE)
|
||||
set(_mac_copy_commands)
|
||||
if(EXISTS "${CMAKE_SOURCE_DIR}/assets")
|
||||
list(APPEND _mac_copy_commands
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_directory "${CMAKE_SOURCE_DIR}/assets" "$<TARGET_FILE_DIR:tetris>/assets"
|
||||
)
|
||||
endif()
|
||||
if(EXISTS "${CMAKE_SOURCE_DIR}/fonts")
|
||||
list(APPEND _mac_copy_commands
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_directory "${CMAKE_SOURCE_DIR}/fonts" "$<TARGET_FILE_DIR:tetris>/fonts"
|
||||
)
|
||||
endif()
|
||||
if(EXISTS "${CMAKE_SOURCE_DIR}/FreeSans.ttf")
|
||||
list(APPEND _mac_copy_commands
|
||||
COMMAND ${CMAKE_COMMAND} -E copy "${CMAKE_SOURCE_DIR}/FreeSans.ttf" "$<TARGET_FILE_DIR:tetris>/FreeSans.ttf"
|
||||
)
|
||||
endif()
|
||||
if(_mac_copy_commands)
|
||||
add_custom_command(TARGET tetris POST_BUILD
|
||||
${_mac_copy_commands}
|
||||
COMMENT "Copying game assets into macOS bundle"
|
||||
)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
target_link_libraries(tetris PRIVATE SDL3::SDL3 SDL3_ttf::SDL3_ttf SDL3_image::SDL3_image cpr::cpr nlohmann_json::nlohmann_json)
|
||||
|
||||
if (WIN32)
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,313 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# macOS Production Build Script for the SDL3 Tetris project
|
||||
# Mirrors the Windows PowerShell workflow but uses common POSIX tooling so it
|
||||
# can be executed on macOS runners or local developer machines.
|
||||
|
||||
PROJECT_NAME="tetris"
|
||||
BUILD_DIR="build-release"
|
||||
OUTPUT_DIR="dist"
|
||||
PACKAGE_DIR=""
|
||||
VERSION="$(date +"%Y.%m.%d")"
|
||||
CLEAN=0
|
||||
PACKAGE_ONLY=0
|
||||
PACKAGE_RUNTIME_DIR=""
|
||||
|
||||
print_usage() {
|
||||
cat <<'USAGE'
|
||||
Usage: ./build-production-mac.sh [options]
|
||||
|
||||
Options:
|
||||
-c, --clean Remove existing build + dist folders before running
|
||||
-p, --package-only Skip compilation and only rebuild the distributable
|
||||
-o, --output DIR Customize output directory (default: dist)
|
||||
-h, --help Show this help text
|
||||
USAGE
|
||||
}
|
||||
|
||||
log() {
|
||||
local level=$1; shift
|
||||
case "$level" in
|
||||
INFO) printf '\033[36m[INFO]\033[0m %s\n' "$*" ;;
|
||||
OK) printf '\033[32m[ OK ]\033[0m %s\n' "$*" ;;
|
||||
WARN) printf '\033[33m[WARN]\033[0m %s\n' "$*" ;;
|
||||
ERR) printf '\033[31m[ERR ]\033[0m %s\n' "$*" ;;
|
||||
*) printf '%s\n' "$*" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
require_macos() {
|
||||
if [[ $(uname) != "Darwin" ]]; then
|
||||
log ERR "This script is intended for macOS hosts."
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
parse_args() {
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
-c|--clean) CLEAN=1; shift ;;
|
||||
-p|--package-only) PACKAGE_ONLY=1; shift ;;
|
||||
-o|--output)
|
||||
if [[ $# -lt 2 ]]; then
|
||||
log ERR "--output requires a directory argument"
|
||||
exit 1
|
||||
fi
|
||||
OUTPUT_DIR="$2"; shift 2 ;;
|
||||
-h|--help) print_usage; exit 0 ;;
|
||||
*)
|
||||
log ERR "Unknown argument: $1"
|
||||
print_usage
|
||||
exit 1 ;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
configure_paths() {
|
||||
PACKAGE_DIR="${OUTPUT_DIR}/TetrisGame-mac"
|
||||
}
|
||||
|
||||
clean_previous() {
|
||||
if (( CLEAN )); then
|
||||
log INFO "Cleaning previous build artifacts..."
|
||||
rm -rf "$BUILD_DIR" "$OUTPUT_DIR"
|
||||
log OK "Previous artifacts removed"
|
||||
fi
|
||||
}
|
||||
|
||||
configure_and_build() {
|
||||
if (( PACKAGE_ONLY )); then
|
||||
return
|
||||
fi
|
||||
|
||||
log INFO "Configuring CMake (Release)..."
|
||||
cmake -S . -B "$BUILD_DIR" -DCMAKE_BUILD_TYPE=Release
|
||||
log OK "CMake configure complete"
|
||||
|
||||
log INFO "Building Release target..."
|
||||
cmake --build "$BUILD_DIR" --config Release
|
||||
log OK "Build finished"
|
||||
}
|
||||
|
||||
resolve_executable() {
|
||||
local candidates=(
|
||||
"$BUILD_DIR/Release/${PROJECT_NAME}"
|
||||
"$BUILD_DIR/${PROJECT_NAME}"
|
||||
"$BUILD_DIR/${PROJECT_NAME}.app/Contents/MacOS/${PROJECT_NAME}"
|
||||
)
|
||||
|
||||
for path in "${candidates[@]}"; do
|
||||
if [[ -x "$path" ]]; then
|
||||
EXECUTABLE_PATH="$path"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ -z ${EXECUTABLE_PATH:-} ]]; then
|
||||
log ERR "Unable to locate built executable."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$EXECUTABLE_PATH" == *.app/Contents/MacOS/* ]]; then
|
||||
APP_BUNDLE_PATH="${EXECUTABLE_PATH%/Contents/MacOS/*}"
|
||||
else
|
||||
APP_BUNDLE_PATH=""
|
||||
fi
|
||||
|
||||
log OK "Using executable: $EXECUTABLE_PATH"
|
||||
}
|
||||
|
||||
prepare_package_dir() {
|
||||
log INFO "Creating package directory $PACKAGE_DIR ..."
|
||||
rm -rf "$PACKAGE_DIR"
|
||||
mkdir -p "$PACKAGE_DIR"
|
||||
}
|
||||
|
||||
copy_binary_or_bundle() {
|
||||
if [[ -n ${APP_BUNDLE_PATH:-} ]]; then
|
||||
log INFO "Copying app bundle..."
|
||||
rsync -a "$APP_BUNDLE_PATH" "$PACKAGE_DIR/"
|
||||
local app_name="${APP_BUNDLE_PATH##*/}"
|
||||
PACKAGE_BINARY="${app_name}/Contents/MacOS/${PROJECT_NAME}"
|
||||
PACKAGE_RUNTIME_DIR="$PACKAGE_DIR/${app_name}/Contents/MacOS"
|
||||
else
|
||||
log INFO "Copying executable..."
|
||||
cp "$EXECUTABLE_PATH" "$PACKAGE_DIR/${PROJECT_NAME}"
|
||||
chmod +x "$PACKAGE_DIR/${PROJECT_NAME}"
|
||||
PACKAGE_BINARY="${PROJECT_NAME}"
|
||||
PACKAGE_RUNTIME_DIR="$PACKAGE_DIR"
|
||||
fi
|
||||
log OK "Binary ready (${PACKAGE_BINARY})"
|
||||
}
|
||||
|
||||
copy_assets() {
|
||||
if [[ -n ${APP_BUNDLE_PATH:-} ]]; then
|
||||
log INFO "Assets already bundled inside the .app; skipping external copy."
|
||||
return
|
||||
fi
|
||||
log INFO "Copying assets..."
|
||||
local folders=("assets" "fonts")
|
||||
for folder in "${folders[@]}"; do
|
||||
if [[ -d "$folder" ]]; then
|
||||
rsync -a "$folder" "$PACKAGE_DIR/"
|
||||
log OK "Copied $folder"
|
||||
fi
|
||||
done
|
||||
if [[ -f "FreeSans.ttf" ]]; then
|
||||
cp "FreeSans.ttf" "$PACKAGE_DIR/"
|
||||
log OK "Copied FreeSans.ttf"
|
||||
fi
|
||||
}
|
||||
|
||||
copy_dependencies() {
|
||||
log INFO "Collecting dynamic libraries from vcpkg..."
|
||||
local triplets=("arm64-osx" "x64-osx" "universal-osx")
|
||||
local bases=("$BUILD_DIR/vcpkg_installed" "vcpkg_installed")
|
||||
local copied_names=()
|
||||
|
||||
for base in "${bases[@]}"; do
|
||||
for triplet in "${triplets[@]}"; do
|
||||
for sub in lib bin; do
|
||||
local dir="$base/$triplet/$sub"
|
||||
if [[ -d "$dir" ]]; then
|
||||
while IFS= read -r -d '' dylib; do
|
||||
local name=$(basename "$dylib")
|
||||
local seen=0
|
||||
for existing in "${copied_names[@]}"; do
|
||||
if [[ "$existing" == "$name" ]]; then
|
||||
seen=1
|
||||
break
|
||||
fi
|
||||
done
|
||||
if (( !seen )); then
|
||||
cp "$dylib" "$PACKAGE_RUNTIME_DIR/"
|
||||
copied_names+=("$name")
|
||||
log OK "Copied $name"
|
||||
fi
|
||||
done < <(find "$dir" -maxdepth 1 -type f -name '*.dylib' -print0)
|
||||
fi
|
||||
done
|
||||
done
|
||||
done
|
||||
|
||||
if [[ ${#copied_names[@]} -eq 0 ]]; then
|
||||
log WARN "No .dylib files found; ensure vcpkg installed macOS triplet dependencies."
|
||||
fi
|
||||
}
|
||||
|
||||
ensure_rpath() {
|
||||
local binary="$PACKAGE_DIR/$PACKAGE_BINARY"
|
||||
if [[ ! -f "$binary" ]]; then
|
||||
return
|
||||
fi
|
||||
if command -v otool >/dev/null 2>&1 && command -v install_name_tool >/dev/null 2>&1; then
|
||||
if ! otool -l "$binary" | grep -A2 LC_RPATH | grep -q '@executable_path'; then
|
||||
log INFO "Adding @executable_path rpath"
|
||||
install_name_tool -add_rpath "@executable_path" "$binary"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
create_launchers() {
|
||||
local launch_command="./${PROJECT_NAME}"
|
||||
if [[ "$PACKAGE_BINARY" == *.app/Contents/MacOS/* ]]; then
|
||||
local app_dir="${PACKAGE_BINARY%%/Contents/*}"
|
||||
launch_command="open \"./${app_dir}\""
|
||||
fi
|
||||
|
||||
cat > "$PACKAGE_DIR/Launch-Tetris.command" <<EOF
|
||||
#!/usr/bin/env bash
|
||||
cd "\$(dirname \"\$0\")"
|
||||
${launch_command}
|
||||
EOF
|
||||
chmod +x "$PACKAGE_DIR/Launch-Tetris.command"
|
||||
|
||||
cat > "$PACKAGE_DIR/README-mac.txt" <<EOF
|
||||
Tetris SDL3 Game - macOS Release $VERSION
|
||||
=========================================
|
||||
|
||||
Requirements:
|
||||
- macOS 12 Monterey or newer (Apple Silicon or Intel)
|
||||
- GPU with Metal support
|
||||
|
||||
Installation:
|
||||
1. Unzip the archive anywhere (e.g., ~/Games/Tetris).
|
||||
2. Ensure the executable bit stays set (script already does this).
|
||||
3. Double-click Launch-Tetris.command or run the binary/app manually from Terminal or Finder.
|
||||
|
||||
Files:
|
||||
- tetris / Launch-Tetris.command (start the game)
|
||||
- assets/ (art, audio, fonts)
|
||||
- *.dylib from SDL3 and related dependencies
|
||||
- FreeSans.ttf font
|
||||
|
||||
Enjoy!
|
||||
EOF
|
||||
log OK "Created README and launcher"
|
||||
}
|
||||
|
||||
validate_package() {
|
||||
log INFO "Validating package contents..."
|
||||
local missing=()
|
||||
local required=("$PACKAGE_BINARY")
|
||||
if [[ -z ${APP_BUNDLE_PATH:-} ]]; then
|
||||
required+=(assets FreeSans.ttf)
|
||||
fi
|
||||
for required in "${required[@]}"; do
|
||||
if [[ ! -e "$PACKAGE_DIR/$required" ]]; then
|
||||
missing+=("$required")
|
||||
fi
|
||||
done
|
||||
if (( ${#missing[@]} > 0 )); then
|
||||
log WARN "Missing: ${missing[*]}"
|
||||
else
|
||||
log OK "Package looks complete"
|
||||
fi
|
||||
}
|
||||
|
||||
create_zip() {
|
||||
mkdir -p "$OUTPUT_DIR"
|
||||
local zip_name="TetrisGame-mac-${VERSION}.zip"
|
||||
local zip_path="$OUTPUT_DIR/$zip_name"
|
||||
log INFO "Creating zip archive $zip_path ..."
|
||||
if command -v ditto >/dev/null 2>&1; then
|
||||
ditto -c -k --keepParent "$PACKAGE_DIR" "$zip_path"
|
||||
else
|
||||
(cd "$OUTPUT_DIR" && zip -r "$zip_name" "$(basename "$PACKAGE_DIR")")
|
||||
fi
|
||||
log OK "Zip created"
|
||||
}
|
||||
|
||||
main() {
|
||||
parse_args "$@"
|
||||
require_macos
|
||||
configure_paths
|
||||
|
||||
if [[ ! -f "CMakeLists.txt" ]]; then
|
||||
log ERR "Run from repository root (CMakeLists.txt missing)."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log INFO "======================================"
|
||||
log INFO " macOS Production Builder"
|
||||
log INFO "======================================"
|
||||
log INFO "Version: $VERSION"
|
||||
log INFO "Output: $OUTPUT_DIR"
|
||||
|
||||
clean_previous
|
||||
configure_and_build
|
||||
resolve_executable
|
||||
prepare_package_dir
|
||||
copy_binary_or_bundle
|
||||
copy_assets
|
||||
copy_dependencies
|
||||
ensure_rpath
|
||||
create_launchers
|
||||
validate_package
|
||||
create_zip
|
||||
|
||||
log INFO "Done. Package available at $PACKAGE_DIR"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@@ -0,0 +1,26 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>English</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>${MACOSX_BUNDLE_EXECUTABLE_NAME}</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>com.example.tetris</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>Tetris</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1.0</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>12.0</string>
|
||||
<key>NSHighResolutionCapable</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -63,10 +63,17 @@ if(WIN32)
|
||||
endif()
|
||||
|
||||
# Installation rules for system-wide installation
|
||||
install(TARGETS tetris
|
||||
RUNTIME DESTINATION bin
|
||||
COMPONENT Runtime
|
||||
)
|
||||
if(APPLE)
|
||||
install(TARGETS tetris
|
||||
BUNDLE DESTINATION .
|
||||
COMPONENT Runtime
|
||||
)
|
||||
else()
|
||||
install(TARGETS tetris
|
||||
RUNTIME DESTINATION bin
|
||||
COMPONENT Runtime
|
||||
)
|
||||
endif()
|
||||
|
||||
install(DIRECTORY assets/
|
||||
DESTINATION share/tetris/assets
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
+9
-61
@@ -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
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ Sound=1
|
||||
SmoothScroll=1
|
||||
|
||||
[Player]
|
||||
Name=GREGOR
|
||||
Name=PLAYER
|
||||
|
||||
[Debug]
|
||||
Enabled=1
|
||||
|
||||
@@ -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<std::string> 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<int>(trackPaths.size());
|
||||
for (const auto& path : trackPaths) {
|
||||
Audio::instance().addTrackAsync(path);
|
||||
}
|
||||
if (m_totalTracks > 0) {
|
||||
Audio::instance().startBackgroundLoading();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
+109
-69
@@ -14,6 +14,9 @@
|
||||
#include <cmath>
|
||||
#include <cstdlib>
|
||||
#include <memory>
|
||||
#include <filesystem>
|
||||
#include <thread>
|
||||
#include <atomic>
|
||||
|
||||
#include "audio/Audio.h"
|
||||
#include "audio/SoundEffect.h"
|
||||
@@ -681,6 +684,25 @@ int main(int, char **)
|
||||
}
|
||||
SDL_SetRenderVSync(renderer, 1);
|
||||
|
||||
if (const char* basePathRaw = SDL_GetBasePath()) {
|
||||
std::filesystem::path exeDir(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) {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"Failed to set working directory to %s: %s",
|
||||
exeDir.string().c_str(), ec.message().c_str());
|
||||
}
|
||||
#endif
|
||||
} else {
|
||||
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"SDL_GetBasePath() failed; asset lookups rely on current directory: %s",
|
||||
SDL_GetError());
|
||||
}
|
||||
|
||||
FontAtlas font;
|
||||
font.init("FreeSans.ttf", 24);
|
||||
|
||||
@@ -689,10 +711,13 @@ 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]() {
|
||||
std::atomic<bool> scoresLoadComplete{false};
|
||||
// Load scores asynchronously but keep the worker alive until shutdown to avoid lifetime issues
|
||||
std::jthread scoreLoader([&scores, &scoresLoadComplete]() {
|
||||
scores.load();
|
||||
}).detach();
|
||||
scoresLoadComplete.store(true, std::memory_order_release);
|
||||
});
|
||||
std::jthread menuTrackLoader;
|
||||
Starfield starfield;
|
||||
starfield.init(200, LOGICAL_W, LOGICAL_H);
|
||||
Starfield3D starfield3D;
|
||||
@@ -751,9 +776,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<std::string> singleSounds = {"well_played", "smooth_clear", "great_move"};
|
||||
@@ -769,49 +804,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;
|
||||
|
||||
@@ -891,7 +902,7 @@ int main(int, char **)
|
||||
// Allow states to access the state manager for transitions
|
||||
ctx.stateManager = &stateMgr;
|
||||
ctx.game = &game;
|
||||
ctx.scores = &scores;
|
||||
ctx.scores = nullptr; // populated once async load finishes
|
||||
ctx.starfield = &starfield;
|
||||
ctx.starfield3D = &starfield3D;
|
||||
ctx.font = &font;
|
||||
@@ -925,6 +936,15 @@ int main(int, char **)
|
||||
running = false;
|
||||
};
|
||||
|
||||
auto ensureScoresLoaded = [&]() {
|
||||
if (scoreLoader.joinable()) {
|
||||
scoreLoader.join();
|
||||
}
|
||||
if (!ctx.scores) {
|
||||
ctx.scores = &scores;
|
||||
}
|
||||
};
|
||||
|
||||
auto beginStateFade = [&](AppState targetState, bool armGameplayCountdown) {
|
||||
if (!ctx.stateManager) {
|
||||
return;
|
||||
@@ -1010,6 +1030,10 @@ int main(int, char **)
|
||||
// Playing, LevelSelect and GameOver currently use inline logic in main; we'll migrate later
|
||||
while (running)
|
||||
{
|
||||
if (!ctx.scores && scoresLoadComplete.load(std::memory_order_acquire)) {
|
||||
ensureScoresLoaded();
|
||||
}
|
||||
|
||||
int winW = 0, winH = 0;
|
||||
SDL_GetWindowSize(window, &winW, &winH);
|
||||
|
||||
@@ -1102,6 +1126,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;
|
||||
@@ -1359,6 +1384,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;
|
||||
@@ -1376,25 +1402,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<std::string> 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<int>(trackPaths.size());
|
||||
|
||||
for (const auto& track : trackPaths) {
|
||||
Audio::instance().addTrackAsync(track);
|
||||
}
|
||||
|
||||
// Start background loading thread
|
||||
@@ -1449,9 +1471,17 @@ int main(int, char **)
|
||||
// Load menu track once on first menu entry (in background to avoid blocking)
|
||||
static bool menuTrackLoaded = false;
|
||||
if (!menuTrackLoaded) {
|
||||
std::thread([]() {
|
||||
Audio::instance().setMenuTrack("assets/music/Every Block You Take.mp3");
|
||||
}).detach();
|
||||
if (menuTrackLoader.joinable()) {
|
||||
menuTrackLoader.join();
|
||||
}
|
||||
menuTrackLoader = std::jthread([]() {
|
||||
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)");
|
||||
}
|
||||
});
|
||||
menuTrackLoaded = true;
|
||||
}
|
||||
|
||||
@@ -1784,6 +1814,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);
|
||||
@@ -1942,6 +1973,15 @@ int main(int, char **)
|
||||
// Save settings on exit
|
||||
Settings::instance().save();
|
||||
|
||||
if (scoreLoader.joinable()) {
|
||||
scoreLoader.join();
|
||||
if (!ctx.scores) {
|
||||
ctx.scores = &scores;
|
||||
}
|
||||
}
|
||||
if (menuTrackLoader.joinable()) {
|
||||
menuTrackLoader.join();
|
||||
}
|
||||
lineEffect.shutdown();
|
||||
Audio::instance().shutdown();
|
||||
SoundEffectManager::instance().shutdown();
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <vector>
|
||||
|
||||
// Use dynamic logical dimensions from GlobalState instead of hardcoded values
|
||||
// This allows the UI to adapt when the window is resized or goes fullscreen
|
||||
@@ -239,7 +240,8 @@ void MenuState::render(SDL_Renderer* renderer, float logicalScale, SDL_Rect logi
|
||||
|
||||
// High scores table with wave offset
|
||||
float scoresStartY = topPlayersY + 70; // more spacing under title
|
||||
const auto &hs = ctx.scores ? ctx.scores->all() : *(new std::vector<ScoreEntry>());
|
||||
static const std::vector<ScoreEntry> EMPTY_SCORES;
|
||||
const auto& hs = ctx.scores ? ctx.scores->all() : EMPTY_SCORES;
|
||||
size_t maxDisplay = std::min(hs.size(), size_t(12));
|
||||
|
||||
// Draw table header
|
||||
|
||||
@@ -2,17 +2,27 @@
|
||||
|
||||
#include <array>
|
||||
#include <string>
|
||||
#include <filesystem>
|
||||
#include <initializer_list>
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
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<const char*> 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<const char*, 4> 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
|
||||
|
||||
Reference in New Issue
Block a user