Compare commits
11 Commits
59a9915570
...
e35aeee5ab
| Author | SHA1 | Date | |
|---|---|---|---|
| e35aeee5ab | |||
| b46246b74f | |||
| 7f07036f07 | |||
| da32b5be1a | |||
| d206b7b1af | |||
| 69b3a60564 | |||
| dc8a317837 | |||
| dd0ac54ee5 | |||
| 33994ec9db | |||
| 344a27a336 | |||
| 0e4064a187 |
@ -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)
|
||||||
|
|||||||
BIN
assets/music/Every Block You Take.ogg
Normal file
BIN
assets/music/Every Block You Take.ogg
Normal file
Binary file not shown.
BIN
assets/music/amazing.ogg
Normal file
BIN
assets/music/amazing.ogg
Normal file
Binary file not shown.
BIN
assets/music/boom_tetris.ogg
Normal file
BIN
assets/music/boom_tetris.ogg
Normal file
Binary file not shown.
BIN
assets/music/great_move.ogg
Normal file
BIN
assets/music/great_move.ogg
Normal file
Binary file not shown.
BIN
assets/music/hard_drop_001.ogg
Normal file
BIN
assets/music/hard_drop_001.ogg
Normal file
Binary file not shown.
BIN
assets/music/impressive.ogg
Normal file
BIN
assets/music/impressive.ogg
Normal file
Binary file not shown.
BIN
assets/music/keep_that_ryhtm.ogg
Normal file
BIN
assets/music/keep_that_ryhtm.ogg
Normal file
Binary file not shown.
BIN
assets/music/lets_go.ogg
Normal file
BIN
assets/music/lets_go.ogg
Normal file
Binary file not shown.
BIN
assets/music/new_level.ogg
Normal file
BIN
assets/music/new_level.ogg
Normal file
Binary file not shown.
BIN
assets/music/nice_combo.ogg
Normal file
BIN
assets/music/nice_combo.ogg
Normal file
Binary file not shown.
BIN
assets/music/smooth_clear.ogg
Normal file
BIN
assets/music/smooth_clear.ogg
Normal file
Binary file not shown.
BIN
assets/music/triple_strike.ogg
Normal file
BIN
assets/music/triple_strike.ogg
Normal file
Binary file not shown.
BIN
assets/music/well_played.ogg
Normal file
BIN
assets/music/well_played.ogg
Normal file
Binary file not shown.
BIN
assets/music/wonderful.ogg
Normal file
BIN
assets/music/wonderful.ogg
Normal file
Binary file not shown.
BIN
assets/music/you_fire.ogg
Normal file
BIN
assets/music/you_fire.ogg
Normal file
Binary file not shown.
BIN
assets/music/you_re_unstoppable.ogg
Normal file
BIN
assets/music/you_re_unstoppable.ogg
Normal file
Binary file not shown.
313
build-production-mac.sh
Normal file
313
build-production-mac.sh
Normal file
@ -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 "$@"
|
||||||
26
cmake/MacBundleInfo.plist.in
Normal file
26
cmake/MacBundleInfo.plist.in
Normal file
@ -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
|
||||||
|
|||||||
45
convert_to_ogg.ps1
Normal file
45
convert_to_ogg.ps1
Normal file
@ -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
|
||||||
@ -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
|
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
178
src/main.cpp
178
src/main.cpp
@ -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;
|
||||||
@ -751,9 +776,19 @@ 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 (int i = 1; i <= totalTracks; ++i) {
|
for (const auto& track : trackPaths) {
|
||||||
char buf[64];
|
Audio::instance().addTrackAsync(track);
|
||||||
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