From ea74af146d2eca31a85c1d321cd9d58eea7d443d Mon Sep 17 00:00:00 2001 From: Gregor Klevze Date: Wed, 10 Dec 2025 18:24:48 +0100 Subject: [PATCH 01/15] mac fix --- CMakeLists.txt | 4 ++ src/audio/Audio.cpp | 100 +++++++++++++++++++++++++++++++++++--------- 2 files changed, 85 insertions(+), 19 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 1e22b56..78b9383 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -132,6 +132,10 @@ target_link_libraries(tetris PRIVATE SDL3::SDL3 SDL3_ttf::SDL3_ttf SDL3_image::S if (WIN32) target_link_libraries(tetris PRIVATE mfplat mfreadwrite mfuuid) 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(cmake/ProductionBuild.cmake) diff --git a/src/audio/Audio.cpp b/src/audio/Audio.cpp index ca666d0..da80838 100644 --- a/src/audio/Audio.cpp +++ b/src/audio/Audio.cpp @@ -26,6 +26,9 @@ using Microsoft::WRL::ComPtr; #ifdef min #undef min #endif +#elif defined(__APPLE__) +#include +#include #endif 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 return true; } -#ifdef _WIN32 +#if defined(_WIN32) static bool decodeMP3(const std::string& path, std::vector& outPCM, int& outRate, int& outCh){ outPCM.clear(); outRate=44100; outCh=2; ComPtr reader; @@ -47,15 +50,85 @@ static bool decodeMP3(const std::string& path, std::vector& outPCM, int reader->SetStreamSelection(MF_SOURCE_READER_FIRST_AUDIO_STREAM, TRUE); while(true){ DWORD flags=0; ComPtr 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 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(); } +#elif defined(__APPLE__) +// Decode MP3 files using macOS AudioToolbox so music works on Apple builds. +static bool decodeMP3(const std::string& path, std::vector& outPCM, int& outRate, int& outCh){ + outPCM.clear(); + outRate = 44100; + outCh = 2; + + CFURLRef url = CFURLCreateFromFileSystemRepresentation(nullptr, reinterpret_cast(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(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(status), path.c_str()); + ExtAudioFileDispose(audioFile); + return false; + } + + const UInt32 framesPerBuffer = 4096; + std::vector 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(status), path.c_str()); + ExtAudioFileDispose(audioFile); + return false; + } + + if (framesToRead == 0) { + break; // EOF + } + + size_t samplesRead = static_cast(framesToRead) * clientFormat.mChannelsPerFrame; + outPCM.insert(outPCM.end(), buffer.data(), buffer.data() + samplesRead); + } + + ExtAudioFileDispose(audioFile); + outRate = static_cast(clientFormat.mSampleRate); + outCh = static_cast(clientFormat.mChannelsPerFrame); + return !outPCM.empty(); +} +#else +static bool decodeMP3(const std::string& path, std::vector& 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 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()); -#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)); } + 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()); + tracks.push_back(std::move(t)); } void Audio::shuffle(){ std::lock_guard lock(tracksMutex); @@ -252,15 +325,11 @@ void Audio::backgroundLoadingThread() { } AudioTrack t; t.path = path; -#ifdef _WIN32 - if (mfInitialized && decodeMP3(path, t.pcm, t.rate, t.channels)) { + 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 // Thread-safe addition to tracks if (loadingAbort.load()) { @@ -311,18 +380,11 @@ int Audio::getLoadedTrackCount() const { void Audio::setMenuTrack(const std::string& 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)) { menuTrack.ok = true; } else { 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() { From d1d0d891fa8556de2b1535343d6ffa19a3749240 Mon Sep 17 00:00:00 2001 From: Gregor Klevze Date: Wed, 10 Dec 2025 18:35:05 +0100 Subject: [PATCH 02/15] fix. sound fx --- src/audio/SoundEffect.cpp | 66 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 65 insertions(+), 1 deletion(-) diff --git a/src/audio/SoundEffect.cpp b/src/audio/SoundEffect.cpp index 24317bd..2a34b66 100644 --- a/src/audio/SoundEffect.cpp +++ b/src/audio/SoundEffect.cpp @@ -20,6 +20,9 @@ #pragma comment(lib, "mfuuid.lib") #pragma comment(lib, "ole32.lib") using Microsoft::WRL::ComPtr; +#elif defined(__APPLE__) +#include +#include #endif // SoundEffect implementation @@ -143,7 +146,7 @@ bool SoundEffect::loadWAV(const std::string& filePath) { } bool SoundEffect::loadMP3(const std::string& filePath) { -#ifdef _WIN32 +#if defined(_WIN32) static bool mfInitialized = false; if (!mfInitialized) { if (FAILED(MFStartup(MF_VERSION))) { @@ -222,6 +225,67 @@ bool SoundEffect::loadMP3(const std::string& filePath) { channels = 2; sampleRate = 44100; return true; +#elif defined(__APPLE__) + CFURLRef url = CFURLCreateFromFileSystemRepresentation(nullptr, reinterpret_cast(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(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(status), filePath.c_str()); + ExtAudioFileDispose(audioFile); + return false; + } + + const UInt32 framesPerBuffer = 2048; + std::vector 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(status), filePath.c_str()); + ExtAudioFileDispose(audioFile); + return false; + } + + if (framesToRead == 0) { + break; // EOF + } + + size_t samplesRead = static_cast(framesToRead) * clientFormat.mChannelsPerFrame; + pcmData.insert(pcmData.end(), buffer.data(), buffer.data() + samplesRead); + } + + ExtAudioFileDispose(audioFile); + channels = static_cast(clientFormat.mChannelsPerFrame); + sampleRate = static_cast(clientFormat.mSampleRate); + return !pcmData.empty(); #else SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "[SoundEffect] MP3 support not available on this platform"); return false; From d2fa5b2782fcb4eb75cc2e0d9c8c580d7015dbfb Mon Sep 17 00:00:00 2001 From: Gregor Klevze Date: Wed, 10 Dec 2025 18:40:46 +0100 Subject: [PATCH 03/15] Added N to play next song --- src/audio/Audio.cpp | 19 +++++++++++++++++++ src/audio/Audio.h | 1 + src/core/application/ApplicationManager.cpp | 10 ++++++++++ 3 files changed, 30 insertions(+) diff --git a/src/audio/Audio.cpp b/src/audio/Audio.cpp index da80838..d547585 100644 --- a/src/audio/Audio.cpp +++ b/src/audio/Audio.cpp @@ -157,6 +157,25 @@ void Audio::start(){ 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::setMuted(bool m){ muted=m; } diff --git a/src/audio/Audio.h b/src/audio/Audio.h index 0541d03..35f520a 100644 --- a/src/audio/Audio.h +++ b/src/audio/Audio.h @@ -42,6 +42,7 @@ public: int getLoadedTrackCount() const; // get number of tracks loaded so far void shuffle(); // randomize order void start(); // begin playback + void skipToNextTrack(); // advance to the next music track void toggleMute(); void setMuted(bool m); bool isMuted() const { return muted; } diff --git a/src/core/application/ApplicationManager.cpp b/src/core/application/ApplicationManager.cpp index f626e04..30d8bc8 100644 --- a/src/core/application/ApplicationManager.cpp +++ b/src/core/application/ApplicationManager.cpp @@ -352,6 +352,16 @@ bool ApplicationManager::initializeManagers() { 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) { AppState currentState = m_stateManager ? m_stateManager->getState() : AppState::Loading; if (currentState != AppState::Loading) { From 3c6466e2a0fa2bdd07fc6e46b5c4d88d68364d9f Mon Sep 17 00:00:00 2001 From: Gregor Klevze Date: Wed, 10 Dec 2025 19:38:06 +0100 Subject: [PATCH 04/15] fix mac resources --- src/utils/ImagePathResolver.h | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/utils/ImagePathResolver.h b/src/utils/ImagePathResolver.h index 90c1f7f..75f72e8 100644 --- a/src/utils/ImagePathResolver.h +++ b/src/utils/ImagePathResolver.h @@ -70,6 +70,22 @@ inline std::string resolveWithBase(const std::string& path) { if (tryOpenFile(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; From 108caf7ffdc32ca4bee40d256f96fbcf856cf817 Mon Sep 17 00:00:00 2001 From: Gregor Klevze Date: Wed, 10 Dec 2025 19:40:43 +0100 Subject: [PATCH 05/15] fox fonts on macc --- src/core/assets/AssetManager.cpp | 32 +++++++++++++++++++------------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/src/core/assets/AssetManager.cpp b/src/core/assets/AssetManager.cpp index 2ad7be8..dc1d23d 100644 --- a/src/core/assets/AssetManager.cpp +++ b/src/core/assets/AssetManager.cpp @@ -131,14 +131,15 @@ bool AssetManager::loadFont(const std::string& id, const std::string& filepath, } // Create new font + std::string resolvedPath = AssetPath::resolveWithBase(filepath); auto font = std::make_unique(); - if (!font->init(filepath, baseSize)) { - setError("Failed to initialize font: " + filepath); + if (!font->init(resolvedPath, baseSize)) { + setError("Failed to initialize font: " + resolvedPath); return false; } 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; } @@ -167,14 +168,16 @@ bool AssetManager::loadMusicTrack(const std::string& filepath) { return false; } - if (!fileExists(filepath)) { - setError("Music file not found: " + filepath); + std::string resolvedPath = AssetPath::resolveWithBase(filepath); + + if (!fileExists(resolvedPath)) { + setError("Music file not found: " + resolvedPath); return false; } try { - m_audioSystem->addTrackAsync(filepath); - logInfo("Added music track for loading: " + filepath); + m_audioSystem->addTrackAsync(resolvedPath); + logInfo("Added music track for loading: " + resolvedPath); return true; } catch (const std::exception& e) { 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; } - if (!fileExists(filepath)) { - setError("Sound effect file not found: " + filepath); + std::string resolvedPath = AssetPath::resolveWithBase(filepath); + + if (!fileExists(resolvedPath)) { + setError("Sound effect file not found: " + resolvedPath); return false; } - if (m_soundSystem->loadSound(id, filepath)) { - logInfo("Loaded sound effect: " + id + " from " + filepath); + if (m_soundSystem->loadSound(id, resolvedPath)) { + logInfo("Loaded sound effect: " + id + " from " + resolvedPath); return true; } else { - setError("Failed to load sound effect: " + id + " from " + filepath); + setError("Failed to load sound effect: " + id + " from " + resolvedPath); return false; } } @@ -354,8 +359,9 @@ std::string AssetManager::getAssetPath(const std::string& relativePath) { } bool AssetManager::fileExists(const std::string& filepath) { + std::string resolved = AssetPath::resolveWithBase(filepath); // 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) { SDL_CloseIO(file); return true; From b9791589f21459098abd7e9b6a7f4abf90b7e73d Mon Sep 17 00:00:00 2001 From: Gregor Klevze Date: Wed, 10 Dec 2025 19:42:38 +0100 Subject: [PATCH 06/15] resources fix --- src/main.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index dfa928d..4b7250f 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -610,11 +610,11 @@ int main(int, char **) // Primary UI font (Orbitron) used for major UI text: buttons, loading, HUD 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 FontAtlas font; - font.init("assets/fonts/Exo2.ttf", 20); + font.init(AssetPath::resolveWithBase("assets/fonts/Exo2.ttf"), 20); ScoreManager scores; std::atomic scoresLoadComplete{false}; From 1e97b3cfa3fda2fdc01d26ab0e68875c05dadd7e Mon Sep 17 00:00:00 2001 From: Gregor Klevze Date: Wed, 10 Dec 2025 19:48:07 +0100 Subject: [PATCH 07/15] N keyboard to skip to next song --- src/main.cpp | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/main.cpp b/src/main.cpp index 4b7250f..1971586 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1033,6 +1033,15 @@ int main(int, char **) musicEnabled = !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) { // Toggle sound effects From 5a755e99956492d09d67e5306fac7706beffb835 Mon Sep 17 00:00:00 2001 From: Gregor Klevze Date: Wed, 10 Dec 2025 19:53:11 +0100 Subject: [PATCH 08/15] mac icon added --- CMakeLists.txt | 10 +++++++++- build-production-mac.sh | 31 +++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 78b9383..fd7834f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -61,7 +61,15 @@ set(TETRIS_SOURCES ) if(APPLE) - add_executable(tetris MACOSX_BUNDLE ${TETRIS_SOURCES}) + 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.icns") + else() + message(WARNING "App icon not found at ${APP_ICON}; bundle will use default icon") + add_executable(tetris MACOSX_BUNDLE ${TETRIS_SOURCES}) + endif() else() add_executable(tetris ${TETRIS_SOURCES}) endif() diff --git a/build-production-mac.sh b/build-production-mac.sh index 3ca9424..5ad5762 100644 --- a/build-production-mac.sh +++ b/build-production-mac.sh @@ -13,6 +13,8 @@ VERSION="$(date +"%Y.%m.%d")" CLEAN=0 PACKAGE_ONLY=0 PACKAGE_RUNTIME_DIR="" +APP_ICON_SRC="assets/favicon/favicon-512x512.png" +APP_ICON_ICNS="assets/favicon/AppIcon.icns" print_usage() { cat <<'USAGE' @@ -68,6 +70,34 @@ configure_paths() { 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() { if (( CLEAN )); then log INFO "Cleaning previous build artifacts..." @@ -296,6 +326,7 @@ main() { log INFO "Output: $OUTPUT_DIR" clean_previous + generate_icns_if_needed configure_and_build resolve_executable prepare_package_dir From 8569b467e0c85ea480132304bbf3863ce6ce5261 Mon Sep 17 00:00:00 2001 From: Gregor Klevze Date: Wed, 10 Dec 2025 20:00:16 +0100 Subject: [PATCH 09/15] icon fix --- scripts/generate-mac-icon.sh | 43 ++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 scripts/generate-mac-icon.sh diff --git a/scripts/generate-mac-icon.sh b/scripts/generate-mac-icon.sh new file mode 100644 index 0000000..dc66a52 --- /dev/null +++ b/scripts/generate-mac-icon.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash +set -euo pipefail + +if [[ $# -lt 2 ]]; then + echo "Usage: $0 " + 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 From d53a1cde34430bd7bf96fe5c697f32ad6ca47992 Mon Sep 17 00:00:00 2001 From: Gregor Klevze Date: Wed, 10 Dec 2025 20:05:54 +0100 Subject: [PATCH 10/15] fix --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index fd7834f..1d86d04 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -65,7 +65,7 @@ if(APPLE) 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.icns") + set_target_properties(tetris PROPERTIES MACOSX_BUNDLE_ICON_FILE "AppIcon") else() message(WARNING "App icon not found at ${APP_ICON}; bundle will use default icon") add_executable(tetris MACOSX_BUNDLE ${TETRIS_SOURCES}) From afcc1dd77de844b8d63247d9d205b79b39ff39ff Mon Sep 17 00:00:00 2001 From: Gregor Klevze Date: Wed, 10 Dec 2025 20:10:35 +0100 Subject: [PATCH 11/15] script --- scripts/generate-mac-icon.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/generate-mac-icon.sh b/scripts/generate-mac-icon.sh index dc66a52..331a320 100644 --- a/scripts/generate-mac-icon.sh +++ b/scripts/generate-mac-icon.sh @@ -1,4 +1,5 @@ #!/usr/bin/env bash + set -euo pipefail if [[ $# -lt 2 ]]; then From 9e0e1b287397b56152623e6fcc8837ba828cf11b Mon Sep 17 00:00:00 2001 From: Gregor Klevze Date: Wed, 10 Dec 2025 20:37:31 +0100 Subject: [PATCH 12/15] fixed bundlle --- CMakeLists.txt | 14 +++++++------- cmake/MacBundleInfo.plist.in | 2 ++ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 1d86d04..ba39b6f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -65,10 +65,16 @@ if(APPLE) 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") + 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}) + set_target_properties(tetris PROPERTIES + MACOSX_BUNDLE_INFO_PLIST "${CMAKE_SOURCE_DIR}/cmake/MacBundleInfo.plist.in" + ) endif() else() add_executable(tetris ${TETRIS_SOURCES}) @@ -80,12 +86,6 @@ if (WIN32) 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") diff --git a/cmake/MacBundleInfo.plist.in b/cmake/MacBundleInfo.plist.in index 2edadcf..73e760b 100644 --- a/cmake/MacBundleInfo.plist.in +++ b/cmake/MacBundleInfo.plist.in @@ -18,6 +18,8 @@ 1.0 CFBundleVersion 1.0 + CFBundleIconFile + AppIcon LSMinimumSystemVersion 12.0 NSHighResolutionCapable From 41d39b9bf7a7b21390bf58399116ba875cbf13b5 Mon Sep 17 00:00:00 2001 From: Gregor Klevze Date: Wed, 10 Dec 2025 20:40:01 +0100 Subject: [PATCH 13/15] script to create dmg --- scripts/create-dmg.sh | 72 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 scripts/create-dmg.sh diff --git a/scripts/create-dmg.sh b/scripts/create-dmg.sh new file mode 100644 index 0000000..9d3ac04 --- /dev/null +++ b/scripts/create-dmg.sh @@ -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 +# Example: ./scripts/create-dmg.sh dist/TetrisGame-mac/tetris.app dist/TetrisGame.dmg + +if [[ $# -lt 2 ]]; then + echo "Usage: $0 " + 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" From b0cec977a5ce0da20ba46ee1d73915a3d801159f Mon Sep 17 00:00:00 2001 From: Gregor Klevze Date: Wed, 10 Dec 2025 20:40:14 +0100 Subject: [PATCH 14/15] create dmg file script --- build-production-mac.sh | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/build-production-mac.sh b/build-production-mac.sh index 5ad5762..5cac243 100644 --- a/build-production-mac.sh +++ b/build-production-mac.sh @@ -337,8 +337,32 @@ main() { create_launchers validate_package create_zip + create_dmg 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 "$@" From 8be2d68b98c0d0d6ba8680e0b951de1caf7b3eb3 Mon Sep 17 00:00:00 2001 From: Gregor Klevze Date: Mon, 15 Dec 2025 19:50:01 +0100 Subject: [PATCH 15/15] update for VisualStudio 18 (2026) --- DEPLOYMENT.md | 2 +- build-debug-and-run.ps1 | 136 ++++++++++++++++++++++++++++++++++++++-- 2 files changed, 133 insertions(+), 5 deletions(-) diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md index 4913e93..5c66cfc 100644 --- a/DEPLOYMENT.md +++ b/DEPLOYMENT.md @@ -67,7 +67,7 @@ The distribution package includes: ### Development Environment - **CMake** 3.20+ -- **Visual Studio 2022** (or compatible C++ compiler) +- **Visual Studio 2026 (VS 18)** with Desktop development with C++ workload - **vcpkg** package manager - **SDL3** libraries (installed via vcpkg) diff --git a/build-debug-and-run.ps1 b/build-debug-and-run.ps1 index 125d423..8fbcffc 100644 --- a/build-debug-and-run.ps1 +++ b/build-debug-and-run.ps1 @@ -8,12 +8,140 @@ Set-Location $root 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 Write-Host "Running: cmake --build build-msvc --config Debug" -$proc = Start-Process -FilePath cmake -ArgumentList '--build','build-msvc','--config','Debug' -NoNewWindow -Wait -PassThru -if ($proc.ExitCode -ne 0) { - Write-Error "Build failed with exit code $($proc.ExitCode)" - exit $proc.ExitCode +& cmake --build build-msvc --config Debug +$buildExit = $LASTEXITCODE +if ($buildExit -ne 0) { + 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) {