Compare commits
11 Commits
59a9915570
...
e35aeee5ab
| 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(cpr CONFIG REQUIRED)
|
||||||
find_package(nlohmann_json CONFIG REQUIRED)
|
find_package(nlohmann_json CONFIG REQUIRED)
|
||||||
|
|
||||||
add_executable(tetris
|
set(TETRIS_SOURCES
|
||||||
src/main.cpp
|
src/main.cpp
|
||||||
src/gameplay/core/Game.cpp
|
src/gameplay/core/Game.cpp
|
||||||
src/core/GravityManager.cpp
|
src/core/GravityManager.cpp
|
||||||
@@ -58,12 +58,24 @@ add_executable(tetris
|
|||||||
src/states/PlayingState.cpp
|
src/states/PlayingState.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if(APPLE)
|
||||||
|
add_executable(tetris MACOSX_BUNDLE ${TETRIS_SOURCES})
|
||||||
|
else()
|
||||||
|
add_executable(tetris ${TETRIS_SOURCES})
|
||||||
|
endif()
|
||||||
|
|
||||||
if (WIN32)
|
if (WIN32)
|
||||||
# Embed the application icon into the executable
|
# Embed the application icon into the executable
|
||||||
set_source_files_properties(src/app_icon.rc PROPERTIES LANGUAGE RC)
|
set_source_files_properties(src/app_icon.rc PROPERTIES LANGUAGE RC)
|
||||||
target_sources(tetris PRIVATE src/app_icon.rc)
|
target_sources(tetris PRIVATE src/app_icon.rc)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
if(APPLE)
|
||||||
|
set_target_properties(tetris PROPERTIES
|
||||||
|
MACOSX_BUNDLE_INFO_PLIST "${CMAKE_SOURCE_DIR}/cmake/MacBundleInfo.plist.in"
|
||||||
|
)
|
||||||
|
endif()
|
||||||
|
|
||||||
if (WIN32)
|
if (WIN32)
|
||||||
# Ensure favicon.ico is available in the build directory for the resource compiler
|
# Ensure favicon.ico is available in the build directory for the resource compiler
|
||||||
set(FAVICON_SRC "${CMAKE_SOURCE_DIR}/assets/favicon/favicon.ico")
|
set(FAVICON_SRC "${CMAKE_SOURCE_DIR}/assets/favicon/favicon.ico")
|
||||||
@@ -88,6 +100,31 @@ if (WIN32)
|
|||||||
)
|
)
|
||||||
endif()
|
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)
|
target_link_libraries(tetris PRIVATE SDL3::SDL3 SDL3_ttf::SDL3_ttf SDL3_image::SDL3_image cpr::cpr nlohmann_json::nlohmann_json)
|
||||||
|
|
||||||
if (WIN32)
|
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()
|
endif()
|
||||||
|
|
||||||
# Installation rules for system-wide installation
|
# Installation rules for system-wide installation
|
||||||
install(TARGETS tetris
|
if(APPLE)
|
||||||
RUNTIME DESTINATION bin
|
install(TARGETS tetris
|
||||||
COMPONENT Runtime
|
BUNDLE DESTINATION .
|
||||||
)
|
COMPONENT Runtime
|
||||||
|
)
|
||||||
|
else()
|
||||||
|
install(TARGETS tetris
|
||||||
|
RUNTIME DESTINATION bin
|
||||||
|
COMPONENT Runtime
|
||||||
|
)
|
||||||
|
endif()
|
||||||
|
|
||||||
install(DIRECTORY assets/
|
install(DIRECTORY assets/
|
||||||
DESTINATION share/tetris/assets
|
DESTINATION share/tetris/assets
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
@echo off
|
@echo off
|
||||||
echo Converting MP3 files to WAV using Windows Media Player...
|
echo Convert MP3 files to OGG (preferred) for cross-platform playback...
|
||||||
echo.
|
echo.
|
||||||
|
|
||||||
REM Check if we have access to Windows Media Format SDK
|
REM Check if we have access to Windows Media Format SDK
|
||||||
set MUSIC_DIR=assets\music
|
set MUSIC_DIR=assets\music
|
||||||
|
|
||||||
REM List of MP3 files to convert
|
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.
|
echo.
|
||||||
for %%f in (%FILES%) do (
|
for %%f in (%FILES%) do (
|
||||||
echo - %MUSIC_DIR%\%%f
|
echo - %MUSIC_DIR%\%%f
|
||||||
@@ -17,13 +17,12 @@ for %%f in (%FILES%) do (
|
|||||||
echo.
|
echo.
|
||||||
echo Recommended settings for conversion:
|
echo Recommended settings for conversion:
|
||||||
echo - Sample Rate: 44100 Hz
|
echo - Sample Rate: 44100 Hz
|
||||||
echo - Bit Depth: 16-bit
|
|
||||||
echo - Channels: Stereo (2)
|
echo - Channels: Stereo (2)
|
||||||
echo - Format: PCM WAV
|
echo - Use OGG Vorbis quality ~4 (or convert to FLAC if you prefer lossless)
|
||||||
echo.
|
echo.
|
||||||
echo You can use:
|
echo You can use:
|
||||||
echo - Audacity (free): https://www.audacityteam.org/
|
echo - Audacity (free): https://www.audacityteam.org/
|
||||||
echo - VLC Media Player (free): Media ^> Convert/Save
|
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.
|
echo.
|
||||||
pause
|
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
|
# Deprecated shim: point developers to the OGG conversion workflow
|
||||||
# This script converts all MP3 sound effect files to WAV for better compatibility
|
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"
|
$oggScript = Join-Path $PSScriptRoot "convert_to_ogg.ps1"
|
||||||
$mp3Files = @(
|
if (Test-Path $oggScript) {
|
||||||
"amazing.mp3",
|
& $oggScript
|
||||||
"boom_tetris.mp3",
|
} else {
|
||||||
"great_move.mp3",
|
Write-Host "Missing convert_to_ogg.ps1" -ForegroundColor Red
|
||||||
"impressive.mp3",
|
exit 1
|
||||||
"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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
SmoothScroll=1
|
||||||
|
|
||||||
[Player]
|
[Player]
|
||||||
Name=GREGOR
|
Name=PLAYER
|
||||||
|
|
||||||
[Debug]
|
[Debug]
|
||||||
Enabled=1
|
Enabled=1
|
||||||
|
|||||||
@@ -558,16 +558,20 @@ bool ApplicationManager::initializeGame() {
|
|||||||
Audio::instance().init();
|
Audio::instance().init();
|
||||||
// Discover available tracks (up to 100) and queue for background loading
|
// Discover available tracks (up to 100) and queue for background loading
|
||||||
m_totalTracks = 0;
|
m_totalTracks = 0;
|
||||||
|
std::vector<std::string> trackPaths;
|
||||||
|
trackPaths.reserve(100);
|
||||||
for (int i = 1; i <= 100; ++i) {
|
for (int i = 1; i <= 100; ++i) {
|
||||||
char buf[128];
|
char base[128];
|
||||||
std::snprintf(buf, sizeof(buf), "assets/music/music%03d.mp3", i);
|
std::snprintf(base, sizeof(base), "assets/music/music%03d", i);
|
||||||
// Use simple file existence check via std::filesystem
|
std::string path = AssetPath::resolveWithExtensions(base, { ".mp3" });
|
||||||
if (std::filesystem::exists(buf)) {
|
if (path.empty()) {
|
||||||
Audio::instance().addTrackAsync(buf);
|
|
||||||
++m_totalTracks;
|
|
||||||
} else {
|
|
||||||
break;
|
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) {
|
if (m_totalTracks > 0) {
|
||||||
Audio::instance().startBackgroundLoading();
|
Audio::instance().startBackgroundLoading();
|
||||||
|
|||||||
@@ -208,27 +208,14 @@ bool AssetManager::loadSoundEffectWithFallback(const std::string& id, const std:
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try WAV first, then MP3 fallback (matching main.cpp pattern)
|
const std::string basePath = "assets/music/" + baseName;
|
||||||
std::string wavPath = "assets/music/" + baseName + ".wav";
|
std::string resolved = AssetPath::resolveWithExtensions(basePath, { ".wav", ".mp3" });
|
||||||
std::string mp3Path = "assets/music/" + baseName + ".mp3";
|
if (!resolved.empty() && m_soundSystem->loadSound(id, resolved)) {
|
||||||
|
logInfo("Loaded sound effect: " + id + " from " + resolved);
|
||||||
// Check WAV first
|
return true;
|
||||||
if (fileExists(wavPath)) {
|
|
||||||
if (m_soundSystem->loadSound(id, wavPath)) {
|
|
||||||
logInfo("Loaded sound effect: " + id + " from " + wavPath + " (WAV)");
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback to MP3
|
setError("Failed to load sound effect: " + id + " (no supported audio extension found)");
|
||||||
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)");
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+106
-66
@@ -14,6 +14,9 @@
|
|||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <thread>
|
||||||
|
#include <atomic>
|
||||||
|
|
||||||
#include "audio/Audio.h"
|
#include "audio/Audio.h"
|
||||||
#include "audio/SoundEffect.h"
|
#include "audio/SoundEffect.h"
|
||||||
@@ -681,6 +684,25 @@ int main(int, char **)
|
|||||||
}
|
}
|
||||||
SDL_SetRenderVSync(renderer, 1);
|
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;
|
FontAtlas font;
|
||||||
font.init("FreeSans.ttf", 24);
|
font.init("FreeSans.ttf", 24);
|
||||||
|
|
||||||
@@ -689,10 +711,13 @@ int main(int, char **)
|
|||||||
pixelFont.init("assets/fonts/PressStart2P-Regular.ttf", 16);
|
pixelFont.init("assets/fonts/PressStart2P-Regular.ttf", 16);
|
||||||
|
|
||||||
ScoreManager scores;
|
ScoreManager scores;
|
||||||
// Load scores asynchronously to prevent startup hang due to network request
|
std::atomic<bool> scoresLoadComplete{false};
|
||||||
std::thread([&scores]() {
|
// Load scores asynchronously but keep the worker alive until shutdown to avoid lifetime issues
|
||||||
|
std::jthread scoreLoader([&scores, &scoresLoadComplete]() {
|
||||||
scores.load();
|
scores.load();
|
||||||
}).detach();
|
scoresLoadComplete.store(true, std::memory_order_release);
|
||||||
|
});
|
||||||
|
std::jthread menuTrackLoader;
|
||||||
Starfield starfield;
|
Starfield starfield;
|
||||||
starfield.init(200, LOGICAL_W, LOGICAL_H);
|
starfield.init(200, LOGICAL_W, LOGICAL_H);
|
||||||
Starfield3D starfield3D;
|
Starfield3D starfield3D;
|
||||||
@@ -752,8 +777,18 @@ int main(int, char **)
|
|||||||
// Initialize sound effects system
|
// Initialize sound effects system
|
||||||
SoundEffectManager::instance().init();
|
SoundEffectManager::instance().init();
|
||||||
|
|
||||||
// Load sound effects
|
auto loadAudioAsset = [](const std::string& basePath, const std::string& id) {
|
||||||
SoundEffectManager::instance().loadSound("clear_line", "assets/music/clear_line.wav");
|
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)
|
// Load voice lines for line clears using WAV files (with MP3 fallback)
|
||||||
std::vector<std::string> singleSounds = {"well_played", "smooth_clear", "great_move"};
|
std::vector<std::string> singleSounds = {"well_played", "smooth_clear", "great_move"};
|
||||||
@@ -769,49 +804,25 @@ int main(int, char **)
|
|||||||
appendVoices(tripleSounds);
|
appendVoices(tripleSounds);
|
||||||
appendVoices(tetrisSounds);
|
appendVoices(tetrisSounds);
|
||||||
|
|
||||||
// Helper function to load sound with WAV/MP3 fallback and file existence check
|
auto loadVoice = [&](const std::string& id, const std::string& baseName) {
|
||||||
auto loadSoundWithFallback = [&](const std::string& id, const std::string& baseName) {
|
loadAudioAsset("assets/music/" + baseName, id);
|
||||||
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());
|
|
||||||
};
|
};
|
||||||
|
|
||||||
loadSoundWithFallback("nice_combo", "nice_combo");
|
loadVoice("nice_combo", "nice_combo");
|
||||||
loadSoundWithFallback("you_fire", "you_fire");
|
loadVoice("you_fire", "you_fire");
|
||||||
loadSoundWithFallback("well_played", "well_played");
|
loadVoice("well_played", "well_played");
|
||||||
loadSoundWithFallback("keep_that_ryhtm", "keep_that_ryhtm");
|
loadVoice("keep_that_ryhtm", "keep_that_ryhtm");
|
||||||
loadSoundWithFallback("great_move", "great_move");
|
loadVoice("great_move", "great_move");
|
||||||
loadSoundWithFallback("smooth_clear", "smooth_clear");
|
loadVoice("smooth_clear", "smooth_clear");
|
||||||
loadSoundWithFallback("impressive", "impressive");
|
loadVoice("impressive", "impressive");
|
||||||
loadSoundWithFallback("triple_strike", "triple_strike");
|
loadVoice("triple_strike", "triple_strike");
|
||||||
loadSoundWithFallback("amazing", "amazing");
|
loadVoice("amazing", "amazing");
|
||||||
loadSoundWithFallback("you_re_unstoppable", "you_re_unstoppable");
|
loadVoice("you_re_unstoppable", "you_re_unstoppable");
|
||||||
loadSoundWithFallback("boom_tetris", "boom_tetris");
|
loadVoice("boom_tetris", "boom_tetris");
|
||||||
loadSoundWithFallback("wonderful", "wonderful");
|
loadVoice("wonderful", "wonderful");
|
||||||
loadSoundWithFallback("lets_go", "lets_go"); // For level up
|
loadVoice("lets_go", "lets_go");
|
||||||
loadSoundWithFallback("hard_drop", "hard_drop_001");
|
loadVoice("hard_drop", "hard_drop_001");
|
||||||
loadSoundWithFallback("new_level", "new_level");
|
loadVoice("new_level", "new_level");
|
||||||
|
|
||||||
bool suppressLineVoiceForLevelUp = false;
|
bool suppressLineVoiceForLevelUp = false;
|
||||||
|
|
||||||
@@ -891,7 +902,7 @@ int main(int, char **)
|
|||||||
// Allow states to access the state manager for transitions
|
// Allow states to access the state manager for transitions
|
||||||
ctx.stateManager = &stateMgr;
|
ctx.stateManager = &stateMgr;
|
||||||
ctx.game = &game;
|
ctx.game = &game;
|
||||||
ctx.scores = &scores;
|
ctx.scores = nullptr; // populated once async load finishes
|
||||||
ctx.starfield = &starfield;
|
ctx.starfield = &starfield;
|
||||||
ctx.starfield3D = &starfield3D;
|
ctx.starfield3D = &starfield3D;
|
||||||
ctx.font = &font;
|
ctx.font = &font;
|
||||||
@@ -925,6 +936,15 @@ int main(int, char **)
|
|||||||
running = false;
|
running = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
auto ensureScoresLoaded = [&]() {
|
||||||
|
if (scoreLoader.joinable()) {
|
||||||
|
scoreLoader.join();
|
||||||
|
}
|
||||||
|
if (!ctx.scores) {
|
||||||
|
ctx.scores = &scores;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
auto beginStateFade = [&](AppState targetState, bool armGameplayCountdown) {
|
auto beginStateFade = [&](AppState targetState, bool armGameplayCountdown) {
|
||||||
if (!ctx.stateManager) {
|
if (!ctx.stateManager) {
|
||||||
return;
|
return;
|
||||||
@@ -1010,6 +1030,10 @@ int main(int, char **)
|
|||||||
// Playing, LevelSelect and GameOver currently use inline logic in main; we'll migrate later
|
// Playing, LevelSelect and GameOver currently use inline logic in main; we'll migrate later
|
||||||
while (running)
|
while (running)
|
||||||
{
|
{
|
||||||
|
if (!ctx.scores && scoresLoadComplete.load(std::memory_order_acquire)) {
|
||||||
|
ensureScoresLoaded();
|
||||||
|
}
|
||||||
|
|
||||||
int winW = 0, winH = 0;
|
int winW = 0, winH = 0;
|
||||||
SDL_GetWindowSize(window, &winW, &winH);
|
SDL_GetWindowSize(window, &winW, &winH);
|
||||||
|
|
||||||
@@ -1102,6 +1126,7 @@ int main(int, char **)
|
|||||||
playerName.pop_back();
|
playerName.pop_back();
|
||||||
} else if (e.key.scancode == SDL_SCANCODE_RETURN || e.key.scancode == SDL_SCANCODE_KP_ENTER) {
|
} else if (e.key.scancode == SDL_SCANCODE_RETURN || e.key.scancode == SDL_SCANCODE_KP_ENTER) {
|
||||||
if (playerName.empty()) playerName = "PLAYER";
|
if (playerName.empty()) playerName = "PLAYER";
|
||||||
|
ensureScoresLoaded();
|
||||||
scores.submit(game.score(), game.lines(), game.level(), game.elapsed(), playerName);
|
scores.submit(game.score(), game.lines(), game.level(), game.elapsed(), playerName);
|
||||||
Settings::instance().setPlayerName(playerName);
|
Settings::instance().setPlayerName(playerName);
|
||||||
isNewHighScore = false;
|
isNewHighScore = false;
|
||||||
@@ -1359,6 +1384,7 @@ int main(int, char **)
|
|||||||
SDL_StartTextInput(window);
|
SDL_StartTextInput(window);
|
||||||
} else {
|
} else {
|
||||||
isNewHighScore = false;
|
isNewHighScore = false;
|
||||||
|
ensureScoresLoaded();
|
||||||
scores.submit(game.score(), game.lines(), game.level(), game.elapsed());
|
scores.submit(game.score(), game.lines(), game.level(), game.elapsed());
|
||||||
}
|
}
|
||||||
state = AppState::GameOver;
|
state = AppState::GameOver;
|
||||||
@@ -1376,25 +1402,21 @@ int main(int, char **)
|
|||||||
|
|
||||||
// Count actual music files first
|
// Count actual music files first
|
||||||
totalTracks = 0;
|
totalTracks = 0;
|
||||||
for (int i = 1; i <= 100; ++i) { // Check up to 100 files
|
std::vector<std::string> trackPaths;
|
||||||
char buf[64];
|
trackPaths.reserve(100);
|
||||||
std::snprintf(buf, sizeof(buf), "assets/music/music%03d.mp3", i);
|
for (int i = 1; i <= 100; ++i) {
|
||||||
|
char base[64];
|
||||||
// Check if file exists
|
std::snprintf(base, sizeof(base), "assets/music/music%03d", i);
|
||||||
SDL_IOStream* file = SDL_IOFromFile(buf, "rb");
|
std::string path = AssetPath::resolveWithExtensions(base, { ".mp3" });
|
||||||
if (file) {
|
if (path.empty()) {
|
||||||
SDL_CloseIO(file);
|
break;
|
||||||
totalTracks++;
|
|
||||||
} else {
|
|
||||||
break; // No more consecutive files
|
|
||||||
}
|
}
|
||||||
|
trackPaths.push_back(path);
|
||||||
}
|
}
|
||||||
|
totalTracks = static_cast<int>(trackPaths.size());
|
||||||
|
|
||||||
// Add all found tracks to the background loading queue
|
for (const auto& track : trackPaths) {
|
||||||
for (int i = 1; i <= totalTracks; ++i) {
|
Audio::instance().addTrackAsync(track);
|
||||||
char buf[64];
|
|
||||||
std::snprintf(buf, sizeof(buf), "assets/music/music%03d.mp3", i);
|
|
||||||
Audio::instance().addTrackAsync(buf);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start background loading thread
|
// 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)
|
// Load menu track once on first menu entry (in background to avoid blocking)
|
||||||
static bool menuTrackLoaded = false;
|
static bool menuTrackLoaded = false;
|
||||||
if (!menuTrackLoaded) {
|
if (!menuTrackLoaded) {
|
||||||
std::thread([]() {
|
if (menuTrackLoader.joinable()) {
|
||||||
Audio::instance().setMenuTrack("assets/music/Every Block You Take.mp3");
|
menuTrackLoader.join();
|
||||||
}).detach();
|
}
|
||||||
|
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;
|
menuTrackLoaded = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1784,6 +1814,7 @@ int main(int, char **)
|
|||||||
// 4. Draw Text
|
// 4. Draw Text
|
||||||
// 4. Draw Text
|
// 4. Draw Text
|
||||||
// Title
|
// Title
|
||||||
|
ensureScoresLoaded();
|
||||||
bool realHighScore = scores.isHighScore(game.score());
|
bool realHighScore = scores.isHighScore(game.score());
|
||||||
const char* title = realHighScore ? "NEW HIGH SCORE!" : "GAME OVER";
|
const char* title = realHighScore ? "NEW HIGH SCORE!" : "GAME OVER";
|
||||||
int tW=0, tH=0; pixelFont.measure(title, 2.0f, tW, tH);
|
int tW=0, tH=0; pixelFont.measure(title, 2.0f, tW, tH);
|
||||||
@@ -1942,6 +1973,15 @@ int main(int, char **)
|
|||||||
// Save settings on exit
|
// Save settings on exit
|
||||||
Settings::instance().save();
|
Settings::instance().save();
|
||||||
|
|
||||||
|
if (scoreLoader.joinable()) {
|
||||||
|
scoreLoader.join();
|
||||||
|
if (!ctx.scores) {
|
||||||
|
ctx.scores = &scores;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (menuTrackLoader.joinable()) {
|
||||||
|
menuTrackLoader.join();
|
||||||
|
}
|
||||||
lineEffect.shutdown();
|
lineEffect.shutdown();
|
||||||
Audio::instance().shutdown();
|
Audio::instance().shutdown();
|
||||||
SoundEffectManager::instance().shutdown();
|
SoundEffectManager::instance().shutdown();
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <array>
|
#include <array>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
// Use dynamic logical dimensions from GlobalState instead of hardcoded values
|
// Use dynamic logical dimensions from GlobalState instead of hardcoded values
|
||||||
// This allows the UI to adapt when the window is resized or goes fullscreen
|
// 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
|
// High scores table with wave offset
|
||||||
float scoresStartY = topPlayersY + 70; // more spacing under title
|
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));
|
size_t maxDisplay = std::min(hs.size(), size_t(12));
|
||||||
|
|
||||||
// Draw table header
|
// Draw table header
|
||||||
|
|||||||
@@ -2,17 +2,27 @@
|
|||||||
|
|
||||||
#include <array>
|
#include <array>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <initializer_list>
|
||||||
|
|
||||||
#include <SDL3/SDL.h>
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
namespace AssetPath {
|
namespace AssetPath {
|
||||||
|
|
||||||
inline bool fileExists(const std::string& path) {
|
inline std::string& baseDirectory() {
|
||||||
if (path.empty()) {
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
SDL_IOStream* file = SDL_IOFromFile(candidate.c_str(), "rb");
|
||||||
SDL_IOStream* file = SDL_IOFromFile(path.c_str(), "rb");
|
|
||||||
if (file) {
|
if (file) {
|
||||||
SDL_CloseIO(file);
|
SDL_CloseIO(file);
|
||||||
return true;
|
return true;
|
||||||
@@ -20,13 +30,58 @@ inline bool fileExists(const std::string& path) {
|
|||||||
return false;
|
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) {
|
inline std::string resolveImagePath(const std::string& originalPath) {
|
||||||
if (originalPath.empty()) {
|
if (originalPath.empty()) {
|
||||||
return originalPath;
|
return originalPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fileExists(originalPath)) {
|
if (auto resolved = resolveWithBase(originalPath); resolved != originalPath || fileExists(resolved)) {
|
||||||
return originalPath;
|
return resolved;
|
||||||
}
|
}
|
||||||
|
|
||||||
const std::size_t dot = originalPath.find_last_of('.');
|
const std::size_t dot = originalPath.find_last_of('.');
|
||||||
@@ -45,12 +100,41 @@ inline std::string resolveImagePath(const std::string& originalPath) {
|
|||||||
if (candidate == originalPath) {
|
if (candidate == originalPath) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (fileExists(candidate)) {
|
std::string resolvedCandidate = resolveWithBase(candidate);
|
||||||
return candidate;
|
if (resolvedCandidate != candidate || fileExists(resolvedCandidate)) {
|
||||||
|
return resolvedCandidate;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return originalPath;
|
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
|
} // namespace AssetPath
|
||||||
|
|||||||
Reference in New Issue
Block a user