Compare commits
16 Commits
e8a7b19213
...
ec086b2cd4
| Author | SHA1 | Date | |
|---|---|---|---|
| ec086b2cd4 | |||
| 8be2d68b98 | |||
| b0cec977a5 | |||
| 41d39b9bf7 | |||
| 9e0e1b2873 | |||
| afcc1dd77d | |||
| d53a1cde34 | |||
| 8569b467e0 | |||
| 5a755e9995 | |||
| 1e97b3cfa3 | |||
| b9791589f2 | |||
| 108caf7ffd | |||
| 3c6466e2a0 | |||
| d2fa5b2782 | |||
| d1d0d891fa | |||
| ea74af146d |
@ -61,7 +61,21 @@ set(TETRIS_SOURCES
|
|||||||
)
|
)
|
||||||
|
|
||||||
if(APPLE)
|
if(APPLE)
|
||||||
|
set(APP_ICON "${CMAKE_SOURCE_DIR}/assets/favicon/AppIcon.icns")
|
||||||
|
if(EXISTS "${APP_ICON}")
|
||||||
|
add_executable(tetris MACOSX_BUNDLE ${TETRIS_SOURCES} "${APP_ICON}")
|
||||||
|
set_source_files_properties("${APP_ICON}" PROPERTIES MACOSX_PACKAGE_LOCATION "Resources")
|
||||||
|
set_target_properties(tetris PROPERTIES
|
||||||
|
MACOSX_BUNDLE_ICON_FILE "AppIcon"
|
||||||
|
MACOSX_BUNDLE_INFO_PLIST "${CMAKE_SOURCE_DIR}/cmake/MacBundleInfo.plist.in"
|
||||||
|
)
|
||||||
|
else()
|
||||||
|
message(WARNING "App icon not found at ${APP_ICON}; bundle will use default icon")
|
||||||
add_executable(tetris MACOSX_BUNDLE ${TETRIS_SOURCES})
|
add_executable(tetris MACOSX_BUNDLE ${TETRIS_SOURCES})
|
||||||
|
set_target_properties(tetris PROPERTIES
|
||||||
|
MACOSX_BUNDLE_INFO_PLIST "${CMAKE_SOURCE_DIR}/cmake/MacBundleInfo.plist.in"
|
||||||
|
)
|
||||||
|
endif()
|
||||||
else()
|
else()
|
||||||
add_executable(tetris ${TETRIS_SOURCES})
|
add_executable(tetris ${TETRIS_SOURCES})
|
||||||
endif()
|
endif()
|
||||||
@ -72,12 +86,6 @@ if (WIN32)
|
|||||||
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")
|
||||||
@ -132,6 +140,10 @@ target_link_libraries(tetris PRIVATE SDL3::SDL3 SDL3_ttf::SDL3_ttf SDL3_image::S
|
|||||||
if (WIN32)
|
if (WIN32)
|
||||||
target_link_libraries(tetris PRIVATE mfplat mfreadwrite mfuuid)
|
target_link_libraries(tetris PRIVATE mfplat mfreadwrite mfuuid)
|
||||||
endif()
|
endif()
|
||||||
|
if(APPLE)
|
||||||
|
# Needed for MP3 decoding via AudioToolbox on macOS
|
||||||
|
target_link_libraries(tetris PRIVATE "-framework AudioToolbox" "-framework CoreFoundation")
|
||||||
|
endif()
|
||||||
|
|
||||||
# Include production build configuration
|
# Include production build configuration
|
||||||
include(cmake/ProductionBuild.cmake)
|
include(cmake/ProductionBuild.cmake)
|
||||||
|
|||||||
@ -67,7 +67,7 @@ The distribution package includes:
|
|||||||
|
|
||||||
### Development Environment
|
### Development Environment
|
||||||
- **CMake** 3.20+
|
- **CMake** 3.20+
|
||||||
- **Visual Studio 2022** (or compatible C++ compiler)
|
- **Visual Studio 2026 (VS 18)** with Desktop development with C++ workload
|
||||||
- **vcpkg** package manager
|
- **vcpkg** package manager
|
||||||
- **SDL3** libraries (installed via vcpkg)
|
- **SDL3** libraries (installed via vcpkg)
|
||||||
|
|
||||||
|
|||||||
@ -8,12 +8,140 @@ Set-Location $root
|
|||||||
|
|
||||||
Write-Host "Working directory: $PWD"
|
Write-Host "Working directory: $PWD"
|
||||||
|
|
||||||
|
# Require Visual Studio 18 (2026)
|
||||||
|
function Find-VisualStudio {
|
||||||
|
$vswherePaths = @()
|
||||||
|
if ($env:ProgramFiles -ne $null) { $vswherePaths += Join-Path $env:ProgramFiles 'Microsoft Visual Studio\Installer\vswhere.exe' }
|
||||||
|
if (${env:ProgramFiles(x86)} -ne $null) { $vswherePaths += Join-Path ${env:ProgramFiles(x86)} 'Microsoft Visual Studio\Installer\vswhere.exe' }
|
||||||
|
|
||||||
|
foreach ($p in $vswherePaths) {
|
||||||
|
if (Test-Path $p) { return @{Tool=$p} }
|
||||||
|
}
|
||||||
|
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-VS18 {
|
||||||
|
$fv = Find-VisualStudio
|
||||||
|
$vswhere = $null
|
||||||
|
if ($fv -and $fv.Tool) { $vswhere = $fv.Tool }
|
||||||
|
if ($vswhere) {
|
||||||
|
try {
|
||||||
|
$inst18 = & $vswhere -version "[18.0,19.0)" -products * -requires Microsoft.Component.MSBuild -property installationPath 2>$null
|
||||||
|
if ($inst18) {
|
||||||
|
$candidate = 'Visual Studio 18 2026'
|
||||||
|
$has = & cmake --help | Select-String -Pattern $candidate -SimpleMatch -Quiet
|
||||||
|
if ($has) {
|
||||||
|
$inst18Path = $inst18.Trim()
|
||||||
|
$vcvars = Join-Path $inst18Path 'VC\\Auxiliary\\Build\\vcvarsall.bat'
|
||||||
|
if (Test-Path $vcvars) { return @{Generator=$candidate; InstallPath=$inst18Path} }
|
||||||
|
Write-Error "Visual Studio 18 detected at $inst18Path but C++ toolchain (vcvarsall.bat) is missing. Install the Desktop development with C++ workload."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
# fall through to path checks below
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# fallback: check common install locations (allow for non-standard drive)
|
||||||
|
$commonPaths = @(
|
||||||
|
'D:\Program Files\Microsoft Visual Studio\2026\Community',
|
||||||
|
'D:\Program Files\Microsoft Visual Studio\2026\Professional',
|
||||||
|
'D:\Program Files\Microsoft Visual Studio\2026\Enterprise',
|
||||||
|
'C:\Program Files\Microsoft Visual Studio\2026\Community',
|
||||||
|
'C:\Program Files\Microsoft Visual Studio\2026\Professional',
|
||||||
|
'C:\Program Files\Microsoft Visual Studio\2026\Enterprise'
|
||||||
|
)
|
||||||
|
foreach ($p in $commonPaths) {
|
||||||
|
if (Test-Path $p) {
|
||||||
|
$vcvars = Join-Path $p 'VC\\Auxiliary\\Build\\vcvarsall.bat'
|
||||||
|
if (Test-Path $vcvars) { return @{Generator='Visual Studio 18 2026'; InstallPath=$p} }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
# Resolve vcpkg root/toolchain (prefer env, then C:\vcpkg, then repo-local)
|
||||||
|
$vcpkgRoot = $env:VCPKG_ROOT
|
||||||
|
if (-not $vcpkgRoot -or -not (Test-Path $vcpkgRoot)) {
|
||||||
|
if (Test-Path 'C:\vcpkg') {
|
||||||
|
$vcpkgRoot = 'C:\vcpkg'
|
||||||
|
$env:VCPKG_ROOT = $vcpkgRoot
|
||||||
|
} elseif (Test-Path (Join-Path $root 'vcpkg')) {
|
||||||
|
$vcpkgRoot = Join-Path $root 'vcpkg'
|
||||||
|
$env:VCPKG_ROOT = $vcpkgRoot
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$toolchainFile = $null
|
||||||
|
if ($vcpkgRoot) {
|
||||||
|
$candidateToolchain = Join-Path $vcpkgRoot 'scripts\buildsystems\vcpkg.cmake'
|
||||||
|
if (Test-Path $candidateToolchain) { $toolchainFile = $candidateToolchain }
|
||||||
|
}
|
||||||
|
|
||||||
|
# Determine VS18 generator and reconfigure build-msvc if needed
|
||||||
|
$preferred = Get-VS18
|
||||||
|
if ($preferred) {
|
||||||
|
Write-Host "Detected Visual Studio: $($preferred.Generator) at $($preferred.InstallPath)"
|
||||||
|
} else {
|
||||||
|
Write-Error "Visual Studio 18 (2026) with Desktop development with C++ workload not found. Install VS 2026 and retry."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not $toolchainFile) {
|
||||||
|
Write-Error "vcpkg toolchain not found. Set VCPKG_ROOT or install vcpkg (expected scripts/buildsystems/vcpkg.cmake)."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# If build-msvc exists, check CMakeCache generator
|
||||||
|
$cacheFile = Join-Path $root 'build-msvc\CMakeCache.txt'
|
||||||
|
$reconfigured = $false
|
||||||
|
if (-not (Test-Path $cacheFile)) {
|
||||||
|
Write-Host "Configuring build-msvc with generator $($preferred.Generator) and vcpkg toolchain $toolchainFile"
|
||||||
|
& cmake -S . -B build-msvc -G "$($preferred.Generator)" -A x64 -DCMAKE_TOOLCHAIN_FILE="$toolchainFile" -DVCPKG_TARGET_TRIPLET=x64-windows
|
||||||
|
$cfgExit = $LASTEXITCODE
|
||||||
|
if ($cfgExit -ne 0) { Write-Error "CMake configure failed with exit code $cfgExit"; exit $cfgExit }
|
||||||
|
$reconfigured = $true
|
||||||
|
} else {
|
||||||
|
$genLine = Get-Content $cacheFile | Select-String -Pattern 'CMAKE_GENERATOR:INTERNAL=' -SimpleMatch
|
||||||
|
$toolLine = Get-Content $cacheFile | Select-String -Pattern 'CMAKE_TOOLCHAIN_FILE:FILEPATH=' -SimpleMatch
|
||||||
|
$currentGen = $null
|
||||||
|
$currentTool = $null
|
||||||
|
if ($genLine) { $currentGen = $genLine -replace '.*CMAKE_GENERATOR:INTERNAL=(.*)','$1' }
|
||||||
|
if ($toolLine) { $currentTool = $toolLine -replace '.*CMAKE_TOOLCHAIN_FILE:FILEPATH=(.*)','$1' }
|
||||||
|
$needsReconfigure = $false
|
||||||
|
if ($currentGen -ne $preferred.Generator) { $needsReconfigure = $true }
|
||||||
|
if (-not $currentTool -or ($currentTool -ne $toolchainFile)) { $needsReconfigure = $true }
|
||||||
|
|
||||||
|
if ($needsReconfigure) {
|
||||||
|
Write-Host "Generator or toolchain changed; cleaning build-msvc directory for fresh configure."
|
||||||
|
Remove-Item -Path (Join-Path $root 'build-msvc') -Recurse -Force -ErrorAction SilentlyContinue
|
||||||
|
Write-Host "Configuring build-msvc with generator $($preferred.Generator) and toolchain $toolchainFile"
|
||||||
|
& cmake -S . -B build-msvc -G "$($preferred.Generator)" -A x64 -DCMAKE_TOOLCHAIN_FILE="$toolchainFile" -DVCPKG_TARGET_TRIPLET=x64-windows
|
||||||
|
$cfgExit = $LASTEXITCODE
|
||||||
|
if ($cfgExit -ne 0) { Write-Error "CMake configure failed with exit code $cfgExit"; exit $cfgExit }
|
||||||
|
$reconfigured = $true
|
||||||
|
} else {
|
||||||
|
Write-Host "CMake cache matches required generator and toolchain."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
# Build Debug configuration
|
# Build Debug configuration
|
||||||
Write-Host "Running: cmake --build build-msvc --config Debug"
|
Write-Host "Running: cmake --build build-msvc --config Debug"
|
||||||
$proc = Start-Process -FilePath cmake -ArgumentList '--build','build-msvc','--config','Debug' -NoNewWindow -Wait -PassThru
|
& cmake --build build-msvc --config Debug
|
||||||
if ($proc.ExitCode -ne 0) {
|
$buildExit = $LASTEXITCODE
|
||||||
Write-Error "Build failed with exit code $($proc.ExitCode)"
|
if ($buildExit -ne 0) {
|
||||||
exit $proc.ExitCode
|
Write-Error "Build failed with exit code $buildExit"
|
||||||
|
$vcpkgLog = Join-Path $root 'build-msvc\vcpkg-manifest-install.log'
|
||||||
|
if (Test-Path $vcpkgLog) {
|
||||||
|
$log = Get-Content $vcpkgLog -Raw
|
||||||
|
if ($log -match 'Unable to find a valid Visual Studio instance') {
|
||||||
|
Write-Error "vcpkg could not locate a valid Visual Studio 18 toolchain. Install VS 2026 with Desktop development with C++ workload and retry."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exit $buildExit
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($NoRun) {
|
if ($NoRun) {
|
||||||
|
|||||||
@ -13,6 +13,8 @@ VERSION="$(date +"%Y.%m.%d")"
|
|||||||
CLEAN=0
|
CLEAN=0
|
||||||
PACKAGE_ONLY=0
|
PACKAGE_ONLY=0
|
||||||
PACKAGE_RUNTIME_DIR=""
|
PACKAGE_RUNTIME_DIR=""
|
||||||
|
APP_ICON_SRC="assets/favicon/favicon-512x512.png"
|
||||||
|
APP_ICON_ICNS="assets/favicon/AppIcon.icns"
|
||||||
|
|
||||||
print_usage() {
|
print_usage() {
|
||||||
cat <<'USAGE'
|
cat <<'USAGE'
|
||||||
@ -68,6 +70,34 @@ configure_paths() {
|
|||||||
PACKAGE_DIR="${OUTPUT_DIR}/TetrisGame-mac"
|
PACKAGE_DIR="${OUTPUT_DIR}/TetrisGame-mac"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
generate_icns_if_needed() {
|
||||||
|
if [[ -f "$APP_ICON_ICNS" ]]; then
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
if [[ ! -f "$APP_ICON_SRC" ]]; then
|
||||||
|
log WARN "Icon source PNG not found ($APP_ICON_SRC); skipping .icns generation"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
if ! command -v iconutil >/dev/null 2>&1; then
|
||||||
|
log WARN "iconutil not available; skipping .icns generation"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
log INFO "Generating AppIcon.icns from $APP_ICON_SRC ..."
|
||||||
|
tmpdir=$(mktemp -d)
|
||||||
|
iconset="$tmpdir/AppIcon.iconset"
|
||||||
|
mkdir -p "$iconset"
|
||||||
|
# Generate required sizes from 512 base; sips will downscale
|
||||||
|
for size in 16 32 64 128 256 512; do
|
||||||
|
sips -s format png "$APP_ICON_SRC" --resampleHeightWidth $size $size --out "$iconset/icon_${size}x${size}.png" >/dev/null
|
||||||
|
sips -s format png "$APP_ICON_SRC" --resampleHeightWidth $((size*2)) $((size*2)) --out "$iconset/icon_${size}x${size}@2x.png" >/dev/null
|
||||||
|
done
|
||||||
|
iconutil -c icns "$iconset" -o "$APP_ICON_ICNS" || log WARN "iconutil failed to create .icns"
|
||||||
|
rm -rf "$tmpdir"
|
||||||
|
if [[ -f "$APP_ICON_ICNS" ]]; then
|
||||||
|
log OK "Created $APP_ICON_ICNS"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
clean_previous() {
|
clean_previous() {
|
||||||
if (( CLEAN )); then
|
if (( CLEAN )); then
|
||||||
log INFO "Cleaning previous build artifacts..."
|
log INFO "Cleaning previous build artifacts..."
|
||||||
@ -296,6 +326,7 @@ main() {
|
|||||||
log INFO "Output: $OUTPUT_DIR"
|
log INFO "Output: $OUTPUT_DIR"
|
||||||
|
|
||||||
clean_previous
|
clean_previous
|
||||||
|
generate_icns_if_needed
|
||||||
configure_and_build
|
configure_and_build
|
||||||
resolve_executable
|
resolve_executable
|
||||||
prepare_package_dir
|
prepare_package_dir
|
||||||
@ -306,8 +337,32 @@ main() {
|
|||||||
create_launchers
|
create_launchers
|
||||||
validate_package
|
validate_package
|
||||||
create_zip
|
create_zip
|
||||||
|
create_dmg
|
||||||
|
|
||||||
log INFO "Done. Package available at $PACKAGE_DIR"
|
log INFO "Done. Package available at $PACKAGE_DIR"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
create_dmg() {
|
||||||
|
if [[ -z ${APP_BUNDLE_PATH:-} ]]; then
|
||||||
|
log INFO "No app bundle detected; skipping DMG creation"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
local app_name="${APP_BUNDLE_PATH##*/}"
|
||||||
|
local dmg_name="TetrisGame-mac-${VERSION}.dmg"
|
||||||
|
local dmg_path="$OUTPUT_DIR/$dmg_name"
|
||||||
|
|
||||||
|
if [[ ! -f "scripts/create-dmg.sh" ]]; then
|
||||||
|
log WARN "scripts/create-dmg.sh not found; skipping DMG creation"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
log INFO "Creating DMG installer: $dmg_path"
|
||||||
|
bash scripts/create-dmg.sh "$PACKAGE_DIR/$app_name" "$dmg_path" || log WARN "DMG creation failed"
|
||||||
|
|
||||||
|
if [[ -f "$dmg_path" ]]; then
|
||||||
|
log OK "DMG created: $dmg_path"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
main "$@"
|
main "$@"
|
||||||
|
|||||||
@ -18,6 +18,8 @@
|
|||||||
<string>1.0</string>
|
<string>1.0</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>1.0</string>
|
<string>1.0</string>
|
||||||
|
<key>CFBundleIconFile</key>
|
||||||
|
<string>AppIcon</string>
|
||||||
<key>LSMinimumSystemVersion</key>
|
<key>LSMinimumSystemVersion</key>
|
||||||
<string>12.0</string>
|
<string>12.0</string>
|
||||||
<key>NSHighResolutionCapable</key>
|
<key>NSHighResolutionCapable</key>
|
||||||
|
|||||||
72
scripts/create-dmg.sh
Normal file
72
scripts/create-dmg.sh
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Create a distributable DMG for the macOS Tetris app
|
||||||
|
# Usage: ./scripts/create-dmg.sh <app-bundle-path> <output-dmg>
|
||||||
|
# Example: ./scripts/create-dmg.sh dist/TetrisGame-mac/tetris.app dist/TetrisGame.dmg
|
||||||
|
|
||||||
|
if [[ $# -lt 2 ]]; then
|
||||||
|
echo "Usage: $0 <app-bundle-path> <output-dmg>"
|
||||||
|
echo "Example: $0 dist/TetrisGame-mac/tetris.app dist/TetrisGame.dmg"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
APP_BUNDLE="$1"
|
||||||
|
OUTPUT_DMG="$2"
|
||||||
|
|
||||||
|
if [[ ! -d "$APP_BUNDLE" ]]; then
|
||||||
|
echo "Error: App bundle not found at $APP_BUNDLE" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! "$APP_BUNDLE" =~ \.app$ ]]; then
|
||||||
|
echo "Error: First argument must be a .app bundle" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Remove existing DMG if present
|
||||||
|
rm -f "$OUTPUT_DMG"
|
||||||
|
|
||||||
|
APP_NAME=$(basename "$APP_BUNDLE" .app)
|
||||||
|
VOLUME_NAME="$APP_NAME"
|
||||||
|
TEMP_DMG="${OUTPUT_DMG%.dmg}-temp.dmg"
|
||||||
|
|
||||||
|
echo "[create-dmg] Creating temporary DMG..."
|
||||||
|
# Create a temporary read-write DMG (generous size to fit the app + padding)
|
||||||
|
hdiutil create -size 200m -fs HFS+ -volname "$VOLUME_NAME" "$TEMP_DMG"
|
||||||
|
|
||||||
|
echo "[create-dmg] Mounting temporary DMG..."
|
||||||
|
MOUNT_DIR=$(hdiutil attach "$TEMP_DMG" -nobrowse | grep "/Volumes/$VOLUME_NAME" | awk '{print $3}')
|
||||||
|
|
||||||
|
if [[ -z "$MOUNT_DIR" ]]; then
|
||||||
|
echo "Error: Failed to mount temporary DMG" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "[create-dmg] Copying app bundle to DMG..."
|
||||||
|
cp -R "$APP_BUNDLE" "$MOUNT_DIR/"
|
||||||
|
|
||||||
|
# Create Applications symlink for drag-and-drop installation
|
||||||
|
echo "[create-dmg] Creating Applications symlink..."
|
||||||
|
ln -s /Applications "$MOUNT_DIR/Applications"
|
||||||
|
|
||||||
|
# Set custom icon if available
|
||||||
|
VOLUME_ICON="$APP_BUNDLE/Contents/Resources/AppIcon.icns"
|
||||||
|
if [[ -f "$VOLUME_ICON" ]]; then
|
||||||
|
echo "[create-dmg] Setting custom volume icon..."
|
||||||
|
cp "$VOLUME_ICON" "$MOUNT_DIR/.VolumeIcon.icns"
|
||||||
|
SetFile -c icnC "$MOUNT_DIR/.VolumeIcon.icns" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Unmount
|
||||||
|
echo "[create-dmg] Ejecting temporary DMG..."
|
||||||
|
hdiutil detach "$MOUNT_DIR"
|
||||||
|
|
||||||
|
# Convert to compressed read-only DMG
|
||||||
|
echo "[create-dmg] Converting to compressed DMG..."
|
||||||
|
hdiutil convert "$TEMP_DMG" -format UDZO -o "$OUTPUT_DMG"
|
||||||
|
|
||||||
|
# Cleanup
|
||||||
|
rm -f "$TEMP_DMG"
|
||||||
|
|
||||||
|
echo "[create-dmg] Created: $OUTPUT_DMG"
|
||||||
44
scripts/generate-mac-icon.sh
Normal file
44
scripts/generate-mac-icon.sh
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
if [[ $# -lt 2 ]]; then
|
||||||
|
echo "Usage: $0 <source-png> <output-icns>"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
ICON_SRC="$1"
|
||||||
|
ICON_DEST="$2"
|
||||||
|
|
||||||
|
if [[ -f "$ICON_DEST" ]]; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -f "$ICON_SRC" ]]; then
|
||||||
|
echo "[generate-mac-icon] Source icon not found: $ICON_SRC" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! command -v iconutil >/dev/null 2>&1; then
|
||||||
|
echo "[generate-mac-icon] iconutil not found" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
TMPDIR=$(mktemp -d)
|
||||||
|
ICONSET="$TMPDIR/AppIcon.iconset"
|
||||||
|
mkdir -p "$ICONSET"
|
||||||
|
|
||||||
|
for SIZE in 16 32 64 128 256 512; do
|
||||||
|
sips -s format png "$ICON_SRC" --resampleHeightWidth $SIZE $SIZE --out "$ICONSET/icon_${SIZE}x${SIZE}.png" >/dev/null
|
||||||
|
sips -s format png "$ICON_SRC" --resampleHeightWidth $((SIZE * 2)) $((SIZE * 2)) --out "$ICONSET/icon_${SIZE}x${SIZE}@2x.png" >/dev/null
|
||||||
|
done
|
||||||
|
|
||||||
|
iconutil -c icns "$ICONSET" -o "$ICON_DEST"
|
||||||
|
rm -rf "$TMPDIR"
|
||||||
|
|
||||||
|
if [[ -f "$ICON_DEST" ]]; then
|
||||||
|
echo "[generate-mac-icon] Generated $ICON_DEST"
|
||||||
|
else
|
||||||
|
echo "[generate-mac-icon] Failed to create $ICON_DEST" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
@ -26,6 +26,9 @@ using Microsoft::WRL::ComPtr;
|
|||||||
#ifdef min
|
#ifdef min
|
||||||
#undef min
|
#undef min
|
||||||
#endif
|
#endif
|
||||||
|
#elif defined(__APPLE__)
|
||||||
|
#include <AudioToolbox/AudioToolbox.h>
|
||||||
|
#include <CoreFoundation/CoreFoundation.h>
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
Audio& Audio::instance(){ static Audio inst; return inst; }
|
Audio& Audio::instance(){ static Audio inst; return inst; }
|
||||||
@ -36,7 +39,7 @@ bool Audio::init(){ if(outSpec.freq!=0) return true; outSpec.format=SDL_AUDIO_S1
|
|||||||
#endif
|
#endif
|
||||||
return true; }
|
return true; }
|
||||||
|
|
||||||
#ifdef _WIN32
|
#if defined(_WIN32)
|
||||||
static bool decodeMP3(const std::string& path, std::vector<int16_t>& outPCM, int& outRate, int& outCh){
|
static bool decodeMP3(const std::string& path, std::vector<int16_t>& outPCM, int& outRate, int& outCh){
|
||||||
outPCM.clear(); outRate=44100; outCh=2;
|
outPCM.clear(); outRate=44100; outCh=2;
|
||||||
ComPtr<IMFSourceReader> reader;
|
ComPtr<IMFSourceReader> reader;
|
||||||
@ -47,14 +50,84 @@ static bool decodeMP3(const std::string& path, std::vector<int16_t>& outPCM, int
|
|||||||
reader->SetStreamSelection(MF_SOURCE_READER_FIRST_AUDIO_STREAM, TRUE);
|
reader->SetStreamSelection(MF_SOURCE_READER_FIRST_AUDIO_STREAM, TRUE);
|
||||||
while(true){ DWORD flags=0; ComPtr<IMFSample> sample; if(FAILED(reader->ReadSample(MF_SOURCE_READER_FIRST_AUDIO_STREAM,0,nullptr,&flags,nullptr,&sample))) break; if(flags & MF_SOURCE_READERF_ENDOFSTREAM) break; if(!sample) continue; ComPtr<IMFMediaBuffer> buffer; if(FAILED(sample->ConvertToContiguousBuffer(&buffer))) continue; BYTE* data=nullptr; DWORD maxLen=0, curLen=0; if(SUCCEEDED(buffer->Lock(&data,&maxLen,&curLen)) && curLen){ size_t samples = curLen/2; size_t oldSz = outPCM.size(); outPCM.resize(oldSz + samples); std::memcpy(outPCM.data()+oldSz, data, curLen); } if(data) buffer->Unlock(); }
|
while(true){ DWORD flags=0; ComPtr<IMFSample> sample; if(FAILED(reader->ReadSample(MF_SOURCE_READER_FIRST_AUDIO_STREAM,0,nullptr,&flags,nullptr,&sample))) break; if(flags & MF_SOURCE_READERF_ENDOFSTREAM) break; if(!sample) continue; ComPtr<IMFMediaBuffer> buffer; if(FAILED(sample->ConvertToContiguousBuffer(&buffer))) continue; BYTE* data=nullptr; DWORD maxLen=0, curLen=0; if(SUCCEEDED(buffer->Lock(&data,&maxLen,&curLen)) && curLen){ size_t samples = curLen/2; size_t oldSz = outPCM.size(); outPCM.resize(oldSz + samples); std::memcpy(outPCM.data()+oldSz, data, curLen); } if(data) buffer->Unlock(); }
|
||||||
outRate=44100; outCh=2; return !outPCM.empty(); }
|
outRate=44100; outCh=2; return !outPCM.empty(); }
|
||||||
|
#elif defined(__APPLE__)
|
||||||
|
// Decode MP3 files using macOS AudioToolbox so music works on Apple builds.
|
||||||
|
static bool decodeMP3(const std::string& path, std::vector<int16_t>& outPCM, int& outRate, int& outCh){
|
||||||
|
outPCM.clear();
|
||||||
|
outRate = 44100;
|
||||||
|
outCh = 2;
|
||||||
|
|
||||||
|
CFURLRef url = CFURLCreateFromFileSystemRepresentation(nullptr, reinterpret_cast<const UInt8*>(path.c_str()), path.size(), false);
|
||||||
|
if (!url) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "[Audio] Failed to create URL for %s", path.c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ExtAudioFileRef audioFile = nullptr;
|
||||||
|
OSStatus status = ExtAudioFileOpenURL(url, &audioFile);
|
||||||
|
CFRelease(url);
|
||||||
|
if (status != noErr || !audioFile) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "[Audio] ExtAudioFileOpenURL failed (%d) for %s", static_cast<int>(status), path.c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
AudioStreamBasicDescription clientFormat{};
|
||||||
|
clientFormat.mSampleRate = 44100.0;
|
||||||
|
clientFormat.mFormatID = kAudioFormatLinearPCM;
|
||||||
|
clientFormat.mFormatFlags = kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked;
|
||||||
|
clientFormat.mBitsPerChannel = 16;
|
||||||
|
clientFormat.mChannelsPerFrame = 2;
|
||||||
|
clientFormat.mFramesPerPacket = 1;
|
||||||
|
clientFormat.mBytesPerFrame = (clientFormat.mBitsPerChannel / 8) * clientFormat.mChannelsPerFrame;
|
||||||
|
clientFormat.mBytesPerPacket = clientFormat.mBytesPerFrame * clientFormat.mFramesPerPacket;
|
||||||
|
|
||||||
|
status = ExtAudioFileSetProperty(audioFile, kExtAudioFileProperty_ClientDataFormat, sizeof(clientFormat), &clientFormat);
|
||||||
|
if (status != noErr) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "[Audio] Failed to set client format (%d) for %s", static_cast<int>(status), path.c_str());
|
||||||
|
ExtAudioFileDispose(audioFile);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const UInt32 framesPerBuffer = 4096;
|
||||||
|
std::vector<int16_t> buffer(framesPerBuffer * clientFormat.mChannelsPerFrame);
|
||||||
|
while (true) {
|
||||||
|
AudioBufferList abl{};
|
||||||
|
abl.mNumberBuffers = 1;
|
||||||
|
abl.mBuffers[0].mNumberChannels = clientFormat.mChannelsPerFrame;
|
||||||
|
abl.mBuffers[0].mDataByteSize = framesPerBuffer * clientFormat.mBytesPerFrame;
|
||||||
|
abl.mBuffers[0].mData = buffer.data();
|
||||||
|
|
||||||
|
UInt32 framesToRead = framesPerBuffer;
|
||||||
|
status = ExtAudioFileRead(audioFile, &framesToRead, &abl);
|
||||||
|
if (status != noErr) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "[Audio] ExtAudioFileRead failed (%d) for %s", static_cast<int>(status), path.c_str());
|
||||||
|
ExtAudioFileDispose(audioFile);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (framesToRead == 0) {
|
||||||
|
break; // EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t samplesRead = static_cast<size_t>(framesToRead) * clientFormat.mChannelsPerFrame;
|
||||||
|
outPCM.insert(outPCM.end(), buffer.data(), buffer.data() + samplesRead);
|
||||||
|
}
|
||||||
|
|
||||||
|
ExtAudioFileDispose(audioFile);
|
||||||
|
outRate = static_cast<int>(clientFormat.mSampleRate);
|
||||||
|
outCh = static_cast<int>(clientFormat.mChannelsPerFrame);
|
||||||
|
return !outPCM.empty();
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
static bool decodeMP3(const std::string& path, std::vector<int16_t>& outPCM, int& outRate, int& outCh){
|
||||||
|
(void)outPCM; (void)outRate; (void)outCh; (void)path;
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "[Audio] MP3 unsupported on this platform: %s", path.c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
void Audio::addTrack(const std::string& path){ AudioTrack t; t.path=path;
|
void Audio::addTrack(const std::string& path){ AudioTrack t; t.path=path;
|
||||||
#ifdef _WIN32
|
|
||||||
if(decodeMP3(path, t.pcm, t.rate, t.channels)) t.ok=true; else SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "[Audio] Failed to decode %s", path.c_str());
|
if(decodeMP3(path, t.pcm, t.rate, t.channels)) t.ok=true; else SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "[Audio] Failed to decode %s", path.c_str());
|
||||||
#else
|
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "[Audio] MP3 unsupported on this platform (stub): %s", path.c_str());
|
|
||||||
#endif
|
|
||||||
tracks.push_back(std::move(t)); }
|
tracks.push_back(std::move(t)); }
|
||||||
|
|
||||||
void Audio::shuffle(){
|
void Audio::shuffle(){
|
||||||
@ -84,6 +157,25 @@ void Audio::start(){
|
|||||||
playing = true;
|
playing = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Audio::skipToNextTrack(){
|
||||||
|
if(!ensureStream()) return;
|
||||||
|
|
||||||
|
// If menu music is active, just restart the looped menu track
|
||||||
|
if(isMenuMusic){
|
||||||
|
if(menuTrack.ok){
|
||||||
|
menuTrack.cursor = 0;
|
||||||
|
playing = true;
|
||||||
|
SDL_ResumeAudioStreamDevice(audioStream);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(tracks.empty()) return;
|
||||||
|
nextTrack();
|
||||||
|
playing = true;
|
||||||
|
SDL_ResumeAudioStreamDevice(audioStream);
|
||||||
|
}
|
||||||
|
|
||||||
void Audio::toggleMute(){ muted=!muted; }
|
void Audio::toggleMute(){ muted=!muted; }
|
||||||
void Audio::setMuted(bool m){ muted=m; }
|
void Audio::setMuted(bool m){ muted=m; }
|
||||||
|
|
||||||
@ -252,15 +344,11 @@ void Audio::backgroundLoadingThread() {
|
|||||||
}
|
}
|
||||||
AudioTrack t;
|
AudioTrack t;
|
||||||
t.path = path;
|
t.path = path;
|
||||||
#ifdef _WIN32
|
if (decodeMP3(path, t.pcm, t.rate, t.channels)) {
|
||||||
if (mfInitialized && decodeMP3(path, t.pcm, t.rate, t.channels)) {
|
|
||||||
t.ok = true;
|
t.ok = true;
|
||||||
} else {
|
} else {
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "[Audio] Failed to decode %s", path.c_str());
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "[Audio] Failed to decode %s", path.c_str());
|
||||||
}
|
}
|
||||||
#else
|
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "[Audio] MP3 unsupported on this platform (stub): %s", path.c_str());
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// Thread-safe addition to tracks
|
// Thread-safe addition to tracks
|
||||||
if (loadingAbort.load()) {
|
if (loadingAbort.load()) {
|
||||||
@ -311,18 +399,11 @@ int Audio::getLoadedTrackCount() const {
|
|||||||
|
|
||||||
void Audio::setMenuTrack(const std::string& path) {
|
void Audio::setMenuTrack(const std::string& path) {
|
||||||
menuTrack.path = path;
|
menuTrack.path = path;
|
||||||
#ifdef _WIN32
|
|
||||||
// Ensure MF is started (might be redundant if init called, but safe)
|
|
||||||
if(!mfStarted){ if(FAILED(MFStartup(MF_VERSION))) { SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "[Audio] MFStartup failed"); } else mfStarted=true; }
|
|
||||||
|
|
||||||
if (decodeMP3(path, menuTrack.pcm, menuTrack.rate, menuTrack.channels)) {
|
if (decodeMP3(path, menuTrack.pcm, menuTrack.rate, menuTrack.channels)) {
|
||||||
menuTrack.ok = true;
|
menuTrack.ok = true;
|
||||||
} else {
|
} else {
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "[Audio] Failed to decode menu track %s", path.c_str());
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "[Audio] Failed to decode menu track %s", path.c_str());
|
||||||
}
|
}
|
||||||
#else
|
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "[Audio] MP3 unsupported (stub): %s", path.c_str());
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void Audio::playMenuMusic() {
|
void Audio::playMenuMusic() {
|
||||||
|
|||||||
@ -42,6 +42,7 @@ public:
|
|||||||
int getLoadedTrackCount() const; // get number of tracks loaded so far
|
int getLoadedTrackCount() const; // get number of tracks loaded so far
|
||||||
void shuffle(); // randomize order
|
void shuffle(); // randomize order
|
||||||
void start(); // begin playback
|
void start(); // begin playback
|
||||||
|
void skipToNextTrack(); // advance to the next music track
|
||||||
void toggleMute();
|
void toggleMute();
|
||||||
void setMuted(bool m);
|
void setMuted(bool m);
|
||||||
bool isMuted() const { return muted; }
|
bool isMuted() const { return muted; }
|
||||||
|
|||||||
@ -20,6 +20,9 @@
|
|||||||
#pragma comment(lib, "mfuuid.lib")
|
#pragma comment(lib, "mfuuid.lib")
|
||||||
#pragma comment(lib, "ole32.lib")
|
#pragma comment(lib, "ole32.lib")
|
||||||
using Microsoft::WRL::ComPtr;
|
using Microsoft::WRL::ComPtr;
|
||||||
|
#elif defined(__APPLE__)
|
||||||
|
#include <AudioToolbox/AudioToolbox.h>
|
||||||
|
#include <CoreFoundation/CoreFoundation.h>
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// SoundEffect implementation
|
// SoundEffect implementation
|
||||||
@ -143,7 +146,7 @@ bool SoundEffect::loadWAV(const std::string& filePath) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool SoundEffect::loadMP3(const std::string& filePath) {
|
bool SoundEffect::loadMP3(const std::string& filePath) {
|
||||||
#ifdef _WIN32
|
#if defined(_WIN32)
|
||||||
static bool mfInitialized = false;
|
static bool mfInitialized = false;
|
||||||
if (!mfInitialized) {
|
if (!mfInitialized) {
|
||||||
if (FAILED(MFStartup(MF_VERSION))) {
|
if (FAILED(MFStartup(MF_VERSION))) {
|
||||||
@ -222,6 +225,67 @@ bool SoundEffect::loadMP3(const std::string& filePath) {
|
|||||||
channels = 2;
|
channels = 2;
|
||||||
sampleRate = 44100;
|
sampleRate = 44100;
|
||||||
return true;
|
return true;
|
||||||
|
#elif defined(__APPLE__)
|
||||||
|
CFURLRef url = CFURLCreateFromFileSystemRepresentation(nullptr, reinterpret_cast<const UInt8*>(filePath.c_str()), filePath.size(), false);
|
||||||
|
if (!url) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "[SoundEffect] Failed to create URL for %s", filePath.c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ExtAudioFileRef audioFile = nullptr;
|
||||||
|
OSStatus status = ExtAudioFileOpenURL(url, &audioFile);
|
||||||
|
CFRelease(url);
|
||||||
|
if (status != noErr || !audioFile) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "[SoundEffect] ExtAudioFileOpenURL failed (%d) for %s", static_cast<int>(status), filePath.c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
AudioStreamBasicDescription clientFormat{};
|
||||||
|
clientFormat.mSampleRate = 44100.0;
|
||||||
|
clientFormat.mFormatID = kAudioFormatLinearPCM;
|
||||||
|
clientFormat.mFormatFlags = kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked;
|
||||||
|
clientFormat.mBitsPerChannel = 16;
|
||||||
|
clientFormat.mChannelsPerFrame = 2;
|
||||||
|
clientFormat.mFramesPerPacket = 1;
|
||||||
|
clientFormat.mBytesPerFrame = (clientFormat.mBitsPerChannel / 8) * clientFormat.mChannelsPerFrame;
|
||||||
|
clientFormat.mBytesPerPacket = clientFormat.mBytesPerFrame * clientFormat.mFramesPerPacket;
|
||||||
|
|
||||||
|
status = ExtAudioFileSetProperty(audioFile, kExtAudioFileProperty_ClientDataFormat, sizeof(clientFormat), &clientFormat);
|
||||||
|
if (status != noErr) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "[SoundEffect] Failed to set client format (%d) for %s", static_cast<int>(status), filePath.c_str());
|
||||||
|
ExtAudioFileDispose(audioFile);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const UInt32 framesPerBuffer = 2048;
|
||||||
|
std::vector<int16_t> buffer(framesPerBuffer * clientFormat.mChannelsPerFrame);
|
||||||
|
while (true) {
|
||||||
|
AudioBufferList abl{};
|
||||||
|
abl.mNumberBuffers = 1;
|
||||||
|
abl.mBuffers[0].mNumberChannels = clientFormat.mChannelsPerFrame;
|
||||||
|
abl.mBuffers[0].mDataByteSize = framesPerBuffer * clientFormat.mBytesPerFrame;
|
||||||
|
abl.mBuffers[0].mData = buffer.data();
|
||||||
|
|
||||||
|
UInt32 framesToRead = framesPerBuffer;
|
||||||
|
status = ExtAudioFileRead(audioFile, &framesToRead, &abl);
|
||||||
|
if (status != noErr) {
|
||||||
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "[SoundEffect] ExtAudioFileRead failed (%d) for %s", static_cast<int>(status), filePath.c_str());
|
||||||
|
ExtAudioFileDispose(audioFile);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (framesToRead == 0) {
|
||||||
|
break; // EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t samplesRead = static_cast<size_t>(framesToRead) * clientFormat.mChannelsPerFrame;
|
||||||
|
pcmData.insert(pcmData.end(), buffer.data(), buffer.data() + samplesRead);
|
||||||
|
}
|
||||||
|
|
||||||
|
ExtAudioFileDispose(audioFile);
|
||||||
|
channels = static_cast<int>(clientFormat.mChannelsPerFrame);
|
||||||
|
sampleRate = static_cast<int>(clientFormat.mSampleRate);
|
||||||
|
return !pcmData.empty();
|
||||||
#else
|
#else
|
||||||
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "[SoundEffect] MP3 support not available on this platform");
|
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "[SoundEffect] MP3 support not available on this platform");
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@ -352,6 +352,16 @@ bool ApplicationManager::initializeManagers() {
|
|||||||
consume = true;
|
consume = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// N: Skip to next song in the playlist (or restart menu track)
|
||||||
|
if (!consume && sc == SDL_SCANCODE_N) {
|
||||||
|
Audio::instance().skipToNextTrack();
|
||||||
|
if (!m_musicStarted && Audio::instance().getLoadedTrackCount() > 0) {
|
||||||
|
m_musicStarted = true;
|
||||||
|
m_musicEnabled = true;
|
||||||
|
}
|
||||||
|
consume = true;
|
||||||
|
}
|
||||||
|
|
||||||
if (!consume && sc == SDL_SCANCODE_H) {
|
if (!consume && sc == SDL_SCANCODE_H) {
|
||||||
AppState currentState = m_stateManager ? m_stateManager->getState() : AppState::Loading;
|
AppState currentState = m_stateManager ? m_stateManager->getState() : AppState::Loading;
|
||||||
if (currentState != AppState::Loading) {
|
if (currentState != AppState::Loading) {
|
||||||
|
|||||||
@ -131,14 +131,15 @@ bool AssetManager::loadFont(const std::string& id, const std::string& filepath,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create new font
|
// Create new font
|
||||||
|
std::string resolvedPath = AssetPath::resolveWithBase(filepath);
|
||||||
auto font = std::make_unique<FontAtlas>();
|
auto font = std::make_unique<FontAtlas>();
|
||||||
if (!font->init(filepath, baseSize)) {
|
if (!font->init(resolvedPath, baseSize)) {
|
||||||
setError("Failed to initialize font: " + filepath);
|
setError("Failed to initialize font: " + resolvedPath);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
m_fonts[id] = std::move(font);
|
m_fonts[id] = std::move(font);
|
||||||
logInfo("Loaded font: " + id + " from " + filepath + " (size: " + std::to_string(baseSize) + ")");
|
logInfo("Loaded font: " + id + " from " + resolvedPath + " (size: " + std::to_string(baseSize) + ")");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -167,14 +168,16 @@ bool AssetManager::loadMusicTrack(const std::string& filepath) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!fileExists(filepath)) {
|
std::string resolvedPath = AssetPath::resolveWithBase(filepath);
|
||||||
setError("Music file not found: " + filepath);
|
|
||||||
|
if (!fileExists(resolvedPath)) {
|
||||||
|
setError("Music file not found: " + resolvedPath);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
m_audioSystem->addTrackAsync(filepath);
|
m_audioSystem->addTrackAsync(resolvedPath);
|
||||||
logInfo("Added music track for loading: " + filepath);
|
logInfo("Added music track for loading: " + resolvedPath);
|
||||||
return true;
|
return true;
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
setError("Failed to add music track: " + std::string(e.what()));
|
setError("Failed to add music track: " + std::string(e.what()));
|
||||||
@ -188,16 +191,18 @@ bool AssetManager::loadSoundEffect(const std::string& id, const std::string& fil
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!fileExists(filepath)) {
|
std::string resolvedPath = AssetPath::resolveWithBase(filepath);
|
||||||
setError("Sound effect file not found: " + filepath);
|
|
||||||
|
if (!fileExists(resolvedPath)) {
|
||||||
|
setError("Sound effect file not found: " + resolvedPath);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (m_soundSystem->loadSound(id, filepath)) {
|
if (m_soundSystem->loadSound(id, resolvedPath)) {
|
||||||
logInfo("Loaded sound effect: " + id + " from " + filepath);
|
logInfo("Loaded sound effect: " + id + " from " + resolvedPath);
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
setError("Failed to load sound effect: " + id + " from " + filepath);
|
setError("Failed to load sound effect: " + id + " from " + resolvedPath);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -354,8 +359,9 @@ std::string AssetManager::getAssetPath(const std::string& relativePath) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool AssetManager::fileExists(const std::string& filepath) {
|
bool AssetManager::fileExists(const std::string& filepath) {
|
||||||
|
std::string resolved = AssetPath::resolveWithBase(filepath);
|
||||||
// Use SDL file I/O for consistency with main.cpp pattern
|
// Use SDL file I/O for consistency with main.cpp pattern
|
||||||
SDL_IOStream* file = SDL_IOFromFile(filepath.c_str(), "rb");
|
SDL_IOStream* file = SDL_IOFromFile(resolved.c_str(), "rb");
|
||||||
if (file) {
|
if (file) {
|
||||||
SDL_CloseIO(file);
|
SDL_CloseIO(file);
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
13
src/main.cpp
13
src/main.cpp
@ -610,11 +610,11 @@ int main(int, char **)
|
|||||||
|
|
||||||
// Primary UI font (Orbitron) used for major UI text: buttons, loading, HUD
|
// Primary UI font (Orbitron) used for major UI text: buttons, loading, HUD
|
||||||
FontAtlas pixelFont;
|
FontAtlas pixelFont;
|
||||||
pixelFont.init("assets/fonts/Orbitron.ttf", 22);
|
pixelFont.init(AssetPath::resolveWithBase("assets/fonts/Orbitron.ttf"), 22);
|
||||||
|
|
||||||
// Secondary font (Exo2) used for longer descriptions, settings, credits
|
// Secondary font (Exo2) used for longer descriptions, settings, credits
|
||||||
FontAtlas font;
|
FontAtlas font;
|
||||||
font.init("assets/fonts/Exo2.ttf", 20);
|
font.init(AssetPath::resolveWithBase("assets/fonts/Exo2.ttf"), 20);
|
||||||
|
|
||||||
ScoreManager scores;
|
ScoreManager scores;
|
||||||
std::atomic<bool> scoresLoadComplete{false};
|
std::atomic<bool> scoresLoadComplete{false};
|
||||||
@ -1033,6 +1033,15 @@ int main(int, char **)
|
|||||||
musicEnabled = !musicEnabled;
|
musicEnabled = !musicEnabled;
|
||||||
Settings::instance().setMusicEnabled(musicEnabled);
|
Settings::instance().setMusicEnabled(musicEnabled);
|
||||||
}
|
}
|
||||||
|
if (e.key.scancode == SDL_SCANCODE_N)
|
||||||
|
{
|
||||||
|
Audio::instance().skipToNextTrack();
|
||||||
|
if (!musicStarted && Audio::instance().getLoadedTrackCount() > 0) {
|
||||||
|
musicStarted = true;
|
||||||
|
musicEnabled = true;
|
||||||
|
Settings::instance().setMusicEnabled(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
if (e.key.scancode == SDL_SCANCODE_S)
|
if (e.key.scancode == SDL_SCANCODE_S)
|
||||||
{
|
{
|
||||||
// Toggle sound effects
|
// Toggle sound effects
|
||||||
|
|||||||
@ -70,6 +70,22 @@ inline std::string resolveWithBase(const std::string& path) {
|
|||||||
if (tryOpenFile(combinedStr)) {
|
if (tryOpenFile(combinedStr)) {
|
||||||
return combinedStr;
|
return combinedStr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if defined(__APPLE__)
|
||||||
|
// When running from a macOS bundle, SDL_GetBasePath may point at
|
||||||
|
// Contents/Resources while assets are in Contents/MacOS. Search that
|
||||||
|
// sibling too so Finder launches find the packaged assets.
|
||||||
|
std::filesystem::path basePath(base);
|
||||||
|
auto contentsDir = basePath.parent_path();
|
||||||
|
if (contentsDir.filename() == "Resources") {
|
||||||
|
auto macosDir = contentsDir.parent_path() / "MacOS";
|
||||||
|
std::filesystem::path alt = macosDir / p;
|
||||||
|
std::string altStr = alt.string();
|
||||||
|
if (tryOpenFile(altStr)) {
|
||||||
|
return altStr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return path;
|
return path;
|
||||||
|
|||||||
Reference in New Issue
Block a user