Compare commits

...

11 Commits

Author SHA1 Message Date
klevze e35aeee5ab Merge branch 'feature/mac' into develop 2025-11-30 15:52:51 +01:00
klevze b46246b74f fixed assertion 2025-11-30 15:52:39 +01:00
klevze 7f07036f07 fixes for mac music 2025-11-30 15:19:14 +01:00
klevze da32b5be1a fixed paths 2025-11-30 14:50:27 +01:00
klevze d206b7b1af path fix 2025-11-30 14:47:03 +01:00
klevze 69b3a60564 fix 2025-11-30 14:39:08 +01:00
klevze dc8a317837 fix 2025-11-30 14:35:45 +01:00
klevze dd0ac54ee5 cmake fix 2025-11-30 14:32:52 +01:00
klevze 33994ec9db fix for max 2025-11-30 14:16:33 +01:00
klevze 344a27a336 fix 2025-11-30 14:07:00 +01:00
klevze 0e4064a187 installer script for mac 2025-11-30 14:04:27 +01:00
29 changed files with 669 additions and 177 deletions
+38 -1
View File
@@ -29,7 +29,7 @@ find_package(SDL3_image CONFIG REQUIRED)
find_package(cpr CONFIG REQUIRED)
find_package(nlohmann_json CONFIG REQUIRED)
add_executable(tetris
set(TETRIS_SOURCES
src/main.cpp
src/gameplay/core/Game.cpp
src/core/GravityManager.cpp
@@ -58,12 +58,24 @@ add_executable(tetris
src/states/PlayingState.cpp
)
if(APPLE)
add_executable(tetris MACOSX_BUNDLE ${TETRIS_SOURCES})
else()
add_executable(tetris ${TETRIS_SOURCES})
endif()
if (WIN32)
# Embed the application icon into the executable
set_source_files_properties(src/app_icon.rc PROPERTIES LANGUAGE RC)
target_sources(tetris PRIVATE src/app_icon.rc)
endif()
if(APPLE)
set_target_properties(tetris PROPERTIES
MACOSX_BUNDLE_INFO_PLIST "${CMAKE_SOURCE_DIR}/cmake/MacBundleInfo.plist.in"
)
endif()
if (WIN32)
# Ensure favicon.ico is available in the build directory for the resource compiler
set(FAVICON_SRC "${CMAKE_SOURCE_DIR}/assets/favicon/favicon.ico")
@@ -88,6 +100,31 @@ if (WIN32)
)
endif()
if(APPLE)
set(_mac_copy_commands)
if(EXISTS "${CMAKE_SOURCE_DIR}/assets")
list(APPEND _mac_copy_commands
COMMAND ${CMAKE_COMMAND} -E copy_directory "${CMAKE_SOURCE_DIR}/assets" "$<TARGET_FILE_DIR:tetris>/assets"
)
endif()
if(EXISTS "${CMAKE_SOURCE_DIR}/fonts")
list(APPEND _mac_copy_commands
COMMAND ${CMAKE_COMMAND} -E copy_directory "${CMAKE_SOURCE_DIR}/fonts" "$<TARGET_FILE_DIR:tetris>/fonts"
)
endif()
if(EXISTS "${CMAKE_SOURCE_DIR}/FreeSans.ttf")
list(APPEND _mac_copy_commands
COMMAND ${CMAKE_COMMAND} -E copy "${CMAKE_SOURCE_DIR}/FreeSans.ttf" "$<TARGET_FILE_DIR:tetris>/FreeSans.ttf"
)
endif()
if(_mac_copy_commands)
add_custom_command(TARGET tetris POST_BUILD
${_mac_copy_commands}
COMMENT "Copying game assets into macOS bundle"
)
endif()
endif()
target_link_libraries(tetris PRIVATE SDL3::SDL3 SDL3_ttf::SDL3_ttf SDL3_image::SDL3_image cpr::cpr nlohmann_json::nlohmann_json)
if (WIN32)
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+313
View 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
View 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>
+11 -4
View File
@@ -63,10 +63,17 @@ if(WIN32)
endif()
# Installation rules for system-wide installation
install(TARGETS tetris
RUNTIME DESTINATION bin
COMPONENT Runtime
)
if(APPLE)
install(TARGETS tetris
BUNDLE DESTINATION .
COMPONENT Runtime
)
else()
install(TARGETS tetris
RUNTIME DESTINATION bin
COMPONENT Runtime
)
endif()
install(DIRECTORY assets/
DESTINATION share/tetris/assets
+5 -6
View File
@@ -1,14 +1,14 @@
@echo off
echo Converting MP3 files to WAV using Windows Media Player...
echo Convert MP3 files to OGG (preferred) for cross-platform playback...
echo.
REM Check if we have access to Windows Media Format SDK
set MUSIC_DIR=assets\music
REM List of MP3 files to convert
set FILES=amazing.mp3 boom_tetris.mp3 great_move.mp3 impressive.mp3 keep_that_ryhtm.mp3 lets_go.mp3 nice_combo.mp3 smooth_clear.mp3 triple_strike.mp3 well_played.mp3 wonderful.mp3 you_fire.mp3 you_re_unstoppable.mp3
set FILES=amazing.mp3 boom_tetris.mp3 great_move.mp3 impressive.mp3 keep_that_ryhtm.mp3 lets_go.mp3 nice_combo.mp3 smooth_clear.mp3 triple_strike.mp3 well_played.mp3 wonderful.mp3 you_fire.mp3 you_re_unstoppable.mp3 hard_drop_001.mp3 new_level.mp3
echo Please convert these MP3 files to WAV format manually:
echo Please convert these MP3 files to OGG Vorbis manually (or run convert_to_ogg.ps1 on Windows):
echo.
for %%f in (%FILES%) do (
echo - %MUSIC_DIR%\%%f
@@ -17,13 +17,12 @@ for %%f in (%FILES%) do (
echo.
echo Recommended settings for conversion:
echo - Sample Rate: 44100 Hz
echo - Bit Depth: 16-bit
echo - Channels: Stereo (2)
echo - Format: PCM WAV
echo - Use OGG Vorbis quality ~4 (or convert to FLAC if you prefer lossless)
echo.
echo You can use:
echo - Audacity (free): https://www.audacityteam.org/
echo - VLC Media Player (free): Media ^> Convert/Save
echo - Any audio converter software
echo - ffmpeg (CLI): ffmpeg -i input.mp3 -c:a libvorbis -qscale:a 4 output.ogg
echo.
pause
+45
View 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
+9 -61
View File
@@ -1,63 +1,11 @@
# Convert MP3 sound effects to WAV format
# This script converts all MP3 sound effect files to WAV for better compatibility
# Deprecated shim: point developers to the OGG conversion workflow
Write-Host "convert_to_wav.ps1 is deprecated." -ForegroundColor Yellow
Write-Host "Use convert_to_ogg.ps1 for generating assets with OGG Vorbis." -ForegroundColor Yellow
$musicDir = "assets\music"
$mp3Files = @(
"amazing.mp3",
"boom_tetris.mp3",
"great_move.mp3",
"impressive.mp3",
"keep_that_ryhtm.mp3",
"lets_go.mp3",
"nice_combo.mp3",
"smooth_clear.mp3",
"triple_strike.mp3",
"well_played.mp3",
"wonderful.mp3",
"you_fire.mp3",
"you_re_unstoppable.mp3"
)
Write-Host "Converting MP3 sound effects to WAV format..." -ForegroundColor Green
foreach ($mp3File in $mp3Files) {
$mp3Path = Join-Path $musicDir $mp3File
$wavFile = $mp3File -replace "\.mp3$", ".wav"
$wavPath = Join-Path $musicDir $wavFile
if (Test-Path $mp3Path) {
Write-Host "Converting $mp3File to $wavFile..." -ForegroundColor Yellow
# Try ffmpeg first (most common)
$ffmpegResult = $null
try {
$ffmpegResult = & ffmpeg -i $mp3Path -acodec pcm_s16le -ar 44100 -ac 2 $wavPath -y 2>&1
if ($LASTEXITCODE -eq 0) {
Write-Host "✓ Successfully converted $mp3File" -ForegroundColor Green
continue
}
} catch {
# FFmpeg not available, try other methods
}
# Try Windows Media Format SDK (if available)
try {
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName Microsoft.VisualBasic
# Use Windows built-in audio conversion
$shell = New-Object -ComObject Shell.Application
# This is a fallback method - may not work on all systems
Write-Host "⚠ FFmpeg not found. Please install FFmpeg or convert manually." -ForegroundColor Red
} catch {
Write-Host "⚠ Could not convert $mp3File automatically." -ForegroundColor Red
}
} else {
Write-Host "⚠ File not found: $mp3Path" -ForegroundColor Red
}
$oggScript = Join-Path $PSScriptRoot "convert_to_ogg.ps1"
if (Test-Path $oggScript) {
& $oggScript
} else {
Write-Host "Missing convert_to_ogg.ps1" -ForegroundColor Red
exit 1
}
Write-Host "`nConversion complete! If FFmpeg was not found, please:" -ForegroundColor Cyan
Write-Host "1. Install FFmpeg: https://ffmpeg.org/download.html" -ForegroundColor White
Write-Host "2. Or use an audio converter like Audacity" -ForegroundColor White
Write-Host "3. Convert to: 44.1kHz, 16-bit, Stereo WAV" -ForegroundColor White
+1 -1
View File
@@ -12,7 +12,7 @@ Sound=1
SmoothScroll=1
[Player]
Name=GREGOR
Name=PLAYER
[Debug]
Enabled=1
+11 -7
View File
@@ -558,16 +558,20 @@ bool ApplicationManager::initializeGame() {
Audio::instance().init();
// Discover available tracks (up to 100) and queue for background loading
m_totalTracks = 0;
std::vector<std::string> trackPaths;
trackPaths.reserve(100);
for (int i = 1; i <= 100; ++i) {
char buf[128];
std::snprintf(buf, sizeof(buf), "assets/music/music%03d.mp3", i);
// Use simple file existence check via std::filesystem
if (std::filesystem::exists(buf)) {
Audio::instance().addTrackAsync(buf);
++m_totalTracks;
} else {
char base[128];
std::snprintf(base, sizeof(base), "assets/music/music%03d", i);
std::string path = AssetPath::resolveWithExtensions(base, { ".mp3" });
if (path.empty()) {
break;
}
trackPaths.push_back(path);
}
m_totalTracks = static_cast<int>(trackPaths.size());
for (const auto& path : trackPaths) {
Audio::instance().addTrackAsync(path);
}
if (m_totalTracks > 0) {
Audio::instance().startBackgroundLoading();
+6 -19
View File
@@ -208,27 +208,14 @@ bool AssetManager::loadSoundEffectWithFallback(const std::string& id, const std:
return false;
}
// Try WAV first, then MP3 fallback (matching main.cpp pattern)
std::string wavPath = "assets/music/" + baseName + ".wav";
std::string mp3Path = "assets/music/" + baseName + ".mp3";
// Check WAV first
if (fileExists(wavPath)) {
if (m_soundSystem->loadSound(id, wavPath)) {
logInfo("Loaded sound effect: " + id + " from " + wavPath + " (WAV)");
return true;
}
const std::string basePath = "assets/music/" + baseName;
std::string resolved = AssetPath::resolveWithExtensions(basePath, { ".wav", ".mp3" });
if (!resolved.empty() && m_soundSystem->loadSound(id, resolved)) {
logInfo("Loaded sound effect: " + id + " from " + resolved);
return true;
}
// Fallback to MP3
if (fileExists(mp3Path)) {
if (m_soundSystem->loadSound(id, mp3Path)) {
logInfo("Loaded sound effect: " + id + " from " + mp3Path + " (MP3 fallback)");
return true;
}
}
setError("Failed to load sound effect: " + id + " (tried both WAV and MP3)");
setError("Failed to load sound effect: " + id + " (no supported audio extension found)");
return false;
}
+109 -69
View File
@@ -14,6 +14,9 @@
#include <cmath>
#include <cstdlib>
#include <memory>
#include <filesystem>
#include <thread>
#include <atomic>
#include "audio/Audio.h"
#include "audio/SoundEffect.h"
@@ -681,6 +684,25 @@ int main(int, char **)
}
SDL_SetRenderVSync(renderer, 1);
if (const char* basePathRaw = SDL_GetBasePath()) {
std::filesystem::path exeDir(basePathRaw);
AssetPath::setBasePath(exeDir.string());
#if defined(__APPLE__)
// On macOS bundles launched from Finder start in /, so re-root relative paths.
std::error_code ec;
std::filesystem::current_path(exeDir, ec);
if (ec) {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"Failed to set working directory to %s: %s",
exeDir.string().c_str(), ec.message().c_str());
}
#endif
} else {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
"SDL_GetBasePath() failed; asset lookups rely on current directory: %s",
SDL_GetError());
}
FontAtlas font;
font.init("FreeSans.ttf", 24);
@@ -689,10 +711,13 @@ int main(int, char **)
pixelFont.init("assets/fonts/PressStart2P-Regular.ttf", 16);
ScoreManager scores;
// Load scores asynchronously to prevent startup hang due to network request
std::thread([&scores]() {
std::atomic<bool> scoresLoadComplete{false};
// Load scores asynchronously but keep the worker alive until shutdown to avoid lifetime issues
std::jthread scoreLoader([&scores, &scoresLoadComplete]() {
scores.load();
}).detach();
scoresLoadComplete.store(true, std::memory_order_release);
});
std::jthread menuTrackLoader;
Starfield starfield;
starfield.init(200, LOGICAL_W, LOGICAL_H);
Starfield3D starfield3D;
@@ -751,9 +776,19 @@ int main(int, char **)
// Initialize sound effects system
SoundEffectManager::instance().init();
// Load sound effects
SoundEffectManager::instance().loadSound("clear_line", "assets/music/clear_line.wav");
auto loadAudioAsset = [](const std::string& basePath, const std::string& id) {
std::string resolved = AssetPath::resolveWithExtensions(basePath, { ".wav", ".mp3" });
if (resolved.empty()) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Missing audio asset for %s (base %s)", id.c_str(), basePath.c_str());
return;
}
if (!SoundEffectManager::instance().loadSound(id, resolved)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to load %s from %s", id.c_str(), resolved.c_str());
}
};
loadAudioAsset("assets/music/clear_line", "clear_line");
// Load voice lines for line clears using WAV files (with MP3 fallback)
std::vector<std::string> singleSounds = {"well_played", "smooth_clear", "great_move"};
@@ -769,49 +804,25 @@ int main(int, char **)
appendVoices(tripleSounds);
appendVoices(tetrisSounds);
// Helper function to load sound with WAV/MP3 fallback and file existence check
auto loadSoundWithFallback = [&](const std::string& id, const std::string& baseName) {
std::string wavPath = "assets/music/" + baseName + ".wav";
std::string mp3Path = "assets/music/" + baseName + ".mp3";
// Check if WAV file exists first
SDL_IOStream* wavFile = SDL_IOFromFile(wavPath.c_str(), "rb");
if (wavFile) {
SDL_CloseIO(wavFile);
if (SoundEffectManager::instance().loadSound(id, wavPath)) {
(void)0;
return;
}
}
// Fallback to MP3 if WAV doesn't exist or fails to load
SDL_IOStream* mp3File = SDL_IOFromFile(mp3Path.c_str(), "rb");
if (mp3File) {
SDL_CloseIO(mp3File);
if (SoundEffectManager::instance().loadSound(id, mp3Path)) {
(void)0;
return;
}
}
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed to load sound: %s (tried both WAV and MP3)", id.c_str());
auto loadVoice = [&](const std::string& id, const std::string& baseName) {
loadAudioAsset("assets/music/" + baseName, id);
};
loadSoundWithFallback("nice_combo", "nice_combo");
loadSoundWithFallback("you_fire", "you_fire");
loadSoundWithFallback("well_played", "well_played");
loadSoundWithFallback("keep_that_ryhtm", "keep_that_ryhtm");
loadSoundWithFallback("great_move", "great_move");
loadSoundWithFallback("smooth_clear", "smooth_clear");
loadSoundWithFallback("impressive", "impressive");
loadSoundWithFallback("triple_strike", "triple_strike");
loadSoundWithFallback("amazing", "amazing");
loadSoundWithFallback("you_re_unstoppable", "you_re_unstoppable");
loadSoundWithFallback("boom_tetris", "boom_tetris");
loadSoundWithFallback("wonderful", "wonderful");
loadSoundWithFallback("lets_go", "lets_go"); // For level up
loadSoundWithFallback("hard_drop", "hard_drop_001");
loadSoundWithFallback("new_level", "new_level");
loadVoice("nice_combo", "nice_combo");
loadVoice("you_fire", "you_fire");
loadVoice("well_played", "well_played");
loadVoice("keep_that_ryhtm", "keep_that_ryhtm");
loadVoice("great_move", "great_move");
loadVoice("smooth_clear", "smooth_clear");
loadVoice("impressive", "impressive");
loadVoice("triple_strike", "triple_strike");
loadVoice("amazing", "amazing");
loadVoice("you_re_unstoppable", "you_re_unstoppable");
loadVoice("boom_tetris", "boom_tetris");
loadVoice("wonderful", "wonderful");
loadVoice("lets_go", "lets_go");
loadVoice("hard_drop", "hard_drop_001");
loadVoice("new_level", "new_level");
bool suppressLineVoiceForLevelUp = false;
@@ -891,7 +902,7 @@ int main(int, char **)
// Allow states to access the state manager for transitions
ctx.stateManager = &stateMgr;
ctx.game = &game;
ctx.scores = &scores;
ctx.scores = nullptr; // populated once async load finishes
ctx.starfield = &starfield;
ctx.starfield3D = &starfield3D;
ctx.font = &font;
@@ -925,6 +936,15 @@ int main(int, char **)
running = false;
};
auto ensureScoresLoaded = [&]() {
if (scoreLoader.joinable()) {
scoreLoader.join();
}
if (!ctx.scores) {
ctx.scores = &scores;
}
};
auto beginStateFade = [&](AppState targetState, bool armGameplayCountdown) {
if (!ctx.stateManager) {
return;
@@ -1010,6 +1030,10 @@ int main(int, char **)
// Playing, LevelSelect and GameOver currently use inline logic in main; we'll migrate later
while (running)
{
if (!ctx.scores && scoresLoadComplete.load(std::memory_order_acquire)) {
ensureScoresLoaded();
}
int winW = 0, winH = 0;
SDL_GetWindowSize(window, &winW, &winH);
@@ -1102,6 +1126,7 @@ int main(int, char **)
playerName.pop_back();
} else if (e.key.scancode == SDL_SCANCODE_RETURN || e.key.scancode == SDL_SCANCODE_KP_ENTER) {
if (playerName.empty()) playerName = "PLAYER";
ensureScoresLoaded();
scores.submit(game.score(), game.lines(), game.level(), game.elapsed(), playerName);
Settings::instance().setPlayerName(playerName);
isNewHighScore = false;
@@ -1359,6 +1384,7 @@ int main(int, char **)
SDL_StartTextInput(window);
} else {
isNewHighScore = false;
ensureScoresLoaded();
scores.submit(game.score(), game.lines(), game.level(), game.elapsed());
}
state = AppState::GameOver;
@@ -1376,25 +1402,21 @@ int main(int, char **)
// Count actual music files first
totalTracks = 0;
for (int i = 1; i <= 100; ++i) { // Check up to 100 files
char buf[64];
std::snprintf(buf, sizeof(buf), "assets/music/music%03d.mp3", i);
// Check if file exists
SDL_IOStream* file = SDL_IOFromFile(buf, "rb");
if (file) {
SDL_CloseIO(file);
totalTracks++;
} else {
break; // No more consecutive files
std::vector<std::string> trackPaths;
trackPaths.reserve(100);
for (int i = 1; i <= 100; ++i) {
char base[64];
std::snprintf(base, sizeof(base), "assets/music/music%03d", i);
std::string path = AssetPath::resolveWithExtensions(base, { ".mp3" });
if (path.empty()) {
break;
}
trackPaths.push_back(path);
}
// Add all found tracks to the background loading queue
for (int i = 1; i <= totalTracks; ++i) {
char buf[64];
std::snprintf(buf, sizeof(buf), "assets/music/music%03d.mp3", i);
Audio::instance().addTrackAsync(buf);
totalTracks = static_cast<int>(trackPaths.size());
for (const auto& track : trackPaths) {
Audio::instance().addTrackAsync(track);
}
// Start background loading thread
@@ -1449,9 +1471,17 @@ int main(int, char **)
// Load menu track once on first menu entry (in background to avoid blocking)
static bool menuTrackLoaded = false;
if (!menuTrackLoaded) {
std::thread([]() {
Audio::instance().setMenuTrack("assets/music/Every Block You Take.mp3");
}).detach();
if (menuTrackLoader.joinable()) {
menuTrackLoader.join();
}
menuTrackLoader = std::jthread([]() {
std::string menuTrack = AssetPath::resolveWithExtensions("assets/music/Every Block You Take", { ".mp3" });
if (!menuTrack.empty()) {
Audio::instance().setMenuTrack(menuTrack);
} else {
SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "Menu track not found (Every Block You Take)");
}
});
menuTrackLoaded = true;
}
@@ -1784,6 +1814,7 @@ int main(int, char **)
// 4. Draw Text
// 4. Draw Text
// Title
ensureScoresLoaded();
bool realHighScore = scores.isHighScore(game.score());
const char* title = realHighScore ? "NEW HIGH SCORE!" : "GAME OVER";
int tW=0, tH=0; pixelFont.measure(title, 2.0f, tW, tH);
@@ -1942,6 +1973,15 @@ int main(int, char **)
// Save settings on exit
Settings::instance().save();
if (scoreLoader.joinable()) {
scoreLoader.join();
if (!ctx.scores) {
ctx.scores = &scores;
}
}
if (menuTrackLoader.joinable()) {
menuTrackLoader.join();
}
lineEffect.shutdown();
Audio::instance().shutdown();
SoundEffectManager::instance().shutdown();
+3 -1
View File
@@ -9,6 +9,7 @@
#include <algorithm>
#include <array>
#include <cmath>
#include <vector>
// Use dynamic logical dimensions from GlobalState instead of hardcoded values
// This allows the UI to adapt when the window is resized or goes fullscreen
@@ -239,7 +240,8 @@ void MenuState::render(SDL_Renderer* renderer, float logicalScale, SDL_Rect logi
// High scores table with wave offset
float scoresStartY = topPlayersY + 70; // more spacing under title
const auto &hs = ctx.scores ? ctx.scores->all() : *(new std::vector<ScoreEntry>());
static const std::vector<ScoreEntry> EMPTY_SCORES;
const auto& hs = ctx.scores ? ctx.scores->all() : EMPTY_SCORES;
size_t maxDisplay = std::min(hs.size(), size_t(12));
// Draw table header
+92 -8
View File
@@ -2,17 +2,27 @@
#include <array>
#include <string>
#include <filesystem>
#include <initializer_list>
#include <SDL3/SDL.h>
namespace AssetPath {
inline bool fileExists(const std::string& path) {
if (path.empty()) {
inline std::string& baseDirectory() {
static std::string base;
return base;
}
inline void setBasePath(std::string path) {
baseDirectory() = std::move(path);
}
inline bool tryOpenFile(const std::string& candidate) {
if (candidate.empty()) {
return false;
}
SDL_IOStream* file = SDL_IOFromFile(path.c_str(), "rb");
SDL_IOStream* file = SDL_IOFromFile(candidate.c_str(), "rb");
if (file) {
SDL_CloseIO(file);
return true;
@@ -20,13 +30,58 @@ inline bool fileExists(const std::string& path) {
return false;
}
inline bool fileExists(const std::string& path) {
if (path.empty()) {
return false;
}
if (tryOpenFile(path)) {
return true;
}
std::filesystem::path p(path);
if (!p.is_absolute()) {
const std::string& base = baseDirectory();
if (!base.empty()) {
std::filesystem::path combined = std::filesystem::path(base) / p;
if (tryOpenFile(combined.string())) {
return true;
}
}
}
return false;
}
inline std::string resolveWithBase(const std::string& path) {
if (path.empty()) {
return path;
}
if (tryOpenFile(path)) {
return path;
}
std::filesystem::path p(path);
if (!p.is_absolute()) {
const std::string& base = baseDirectory();
if (!base.empty()) {
std::filesystem::path combined = std::filesystem::path(base) / p;
std::string combinedStr = combined.string();
if (tryOpenFile(combinedStr)) {
return combinedStr;
}
}
}
return path;
}
inline std::string resolveImagePath(const std::string& originalPath) {
if (originalPath.empty()) {
return originalPath;
}
if (fileExists(originalPath)) {
return originalPath;
if (auto resolved = resolveWithBase(originalPath); resolved != originalPath || fileExists(resolved)) {
return resolved;
}
const std::size_t dot = originalPath.find_last_of('.');
@@ -45,12 +100,41 @@ inline std::string resolveImagePath(const std::string& originalPath) {
if (candidate == originalPath) {
continue;
}
if (fileExists(candidate)) {
return candidate;
std::string resolvedCandidate = resolveWithBase(candidate);
if (resolvedCandidate != candidate || fileExists(resolvedCandidate)) {
return resolvedCandidate;
}
}
return originalPath;
}
inline std::string resolveWithExtensions(const std::string& basePathWithoutExt, std::initializer_list<const char*> extensions) {
for (const char* ext : extensions) {
std::string candidate = basePathWithoutExt + ext;
std::string resolved = resolveWithBase(candidate);
if (resolved != candidate || fileExists(resolved)) {
return resolved;
}
}
return {};
}
inline std::string resolveAudioPath(const std::string& basePathWithoutExt) {
static constexpr std::array<const char*, 4> AUDIO_EXTENSIONS = {
".ogg",
".flac",
".wav",
".mp3"
};
for (const char* ext : AUDIO_EXTENSIONS) {
std::string candidate = basePathWithoutExt + ext;
std::string resolved = resolveWithBase(candidate);
if (resolved != candidate || fileExists(resolved)) {
return resolved;
}
}
return {};
}
} // namespace AssetPath