commit 2d9bff86
Add icon and some promts
Changed files
diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md
new file mode 100644
index 0000000..dfd2cb5
--- /dev/null
+++ b/.github/copilot-instructions.md
@@ -0,0 +1,157 @@
+# Sago Multi Scrambler Puzzle II - AI Coding Agent Instructions
+
+## Project Overview
+C++ puzzle game using SDL2, featuring image scrambling puzzles with splitting, shuffling, and flipping mechanics. Uses a state-based architecture with PhysFS for virtual filesystem abstraction and Dear ImGui for UI.
+
+## Architecture
+
+### State Machine Pattern
+All game screens inherit from `sago::GameStateInterface` (located in `src/sago/GameStateInterface.hpp`):
+- `IsActive()` - Returns false to pop state from stack
+- `Draw(SDL_Renderer*)` - Render to screen
+- `ProcessInput(const SDL_Event&, bool& processed)` - Handle events
+- `Update()` - Per-frame logic
+
+**Key States:**
+- `MainGameState` - Main menu (minimal implementation)
+- `CollectionListState` - Lists all available collections
+- `CollectionPlayState` - Plays puzzles sequentially from a collection, tracks progress
+- `ImageSelectState` - Browse and select images from a folder
+- `PuzzleSingleImageState` - Core puzzle gameplay (shuffle, flip, solve detection)
+- `PuzzlePieceEditorState` - Custom piece layout editor (WIP)
+- `SagoTextureSelector` - Texture selector for editor mode (WIP)
+
+### Global State & Resource Management
+`GlobalData` struct (in `src/globals.hpp`) is the singleton holding:
+- `SDL_Renderer* screen` - Rendering target
+- `sago::SagoSpriteHolder* spriteHolder` - Sprite cache
+- `sago::SagoDataHolder* dataHolder` - Texture/resource cache
+- Window dimensions (`xsize`/`ysize`), mouse state, flags
+
+Access via global `globalData` instance. Resources auto-reload when renderer recreates.
+
+### Sprite System
+Sprites defined in JSON files at `data/sprites/*.sprite`:
+```json
+{
+ "sprite_name": {
+ "texture": "texture_name",
+ "topx": 0, "topy": 0,
+ "height": 64, "width": 64,
+ "number_of_frames": 1,
+ "frame_time": 100
+ }
+}
+```
+Load via `globalData.spriteHolder->GetSprite("sprite_name")`, draw with `.Draw(renderer, SDL_GetTicks(), x, y)`.
+
+**UI Convention:** Nine-patch rectangles use `ui_rect_white_*` and `ui_rect_yellow_*` sprites (n, s, e, w, ne, nw, se, sw, fill variants). See `DrawRectYellow()`/`DrawRectWhite()` helpers in `MainGameState.cpp`.
+
+## Build System & Development
+
+### Building
+```bash
+cmake . # Configure (requires SDL2, SDL2_image, SDL2_ttf, SDL2_mixer, SDL2_gfx, Boost, physfs, rhash)
+make # Compile
+```
+**No test suite** — zero test infrastructure (no CTest, GTest, etc.).
+
+GitHub Actions CI: Builds in Docker ([extra/docker/Dockerfile](../extra/docker/Dockerfile)) on every push.
+
+### Running
+```bash
+./sago_multi_scrambler_puzzle2 # Main menu
+./sago_multi_scrambler_puzzle2 image.jpg # Direct puzzle
+./sago_multi_scrambler_puzzle2 --collection fairy_tales # Collection
+./sago_multi_scrambler_puzzle2 --folder /path/to/images # Custom folder
+./sago_multi_scrambler_puzzle2 --editor # Editor (WIP)
+./sago_multi_scrambler_puzzle2 --install-desktop-entry # Linux: install .desktop + icon
+```
+
+### File Structure
+- `src/` - Main source (game states, utilities)
+- `src/sago/` - Reusable library code (sprites, data holders, platform folders)
+- `src/Libs/` - Embedded third-party (Dear ImGui, rapidjson)
+- `src/editor/` - Editor-specific components
+- `data/` - Game assets (sprites, textures, fonts, collections)
+- `embedded_libs/PlatformFolders-4.2.0/` - Cross-platform paths library
+
+## Key Patterns & Conventions
+
+### Game Loop
+Standard flow: `InitGame()` → `RunGameState(state)` → `UninitGame()`
+
+`RunGameState()` (in `src/sago_common.cpp`) handles:
+1. Clear/draw background
+2. ImGui frame setup
+3. Call state's `Draw()` and `Update()`
+4. Process SDL events via state's `ProcessInput()`
+5. Render ImGui and present
+
+### PhysFS Virtual Filesystem
+All assets loaded through PhysFS (not direct file I/O):
+- **Call order matters:** `InitSagoFS()` must run **before** `InitGame()` — the window icon loads from PhysFS in `InitGame()`
+- Mount point: `data/` directory at startup (`InitSagoFS()` in `src/sago_common.cpp`)
+- Use `sago::GetFileContent(path)` for reading files (note: in `sago::` namespace)
+- Paths relative to mount: `"sprites/background.sprite"`, not `"data/sprites/..."`
+- Write directory: save folder (lowest priority in mount search order)
+
+### Save Files
+Platform-specific paths via `sago::platform_folders`:
+- Linux: `~/.local/share/sago_multi_scrambler_puzzle2/`
+- Windows: `Documents/My Games/sago_multi_scrambler_puzzle2/`
+- Function: `getPathToSaveFiles()` in `os.cpp`
+- ImGui settings: `imgui.ini` in save folder
+
+### Dear ImGui Integration
+- Initialized in `InitGame()` with docking enabled
+- Settings manually loaded/saved (not auto-saved)
+- Process events via `ImGui_ImplSDL2_ProcessEvent(&event)` in each state's `ProcessInput()`
+- New frame setup in `RunGameState()` loop
+
+## Common Tasks
+
+### Adding a New Game State
+1. Create header/cpp in `src/`, inherit from `sago::GameStateInterface`
+2. Implement all virtual methods (IsActive, Draw, ProcessInput, Update)
+3. Set `isActive = false` to exit state (pops from stack)
+4. Call `RunGameState(yourState)` after `InitGame()`
+
+### Adding UI Elements
+Use nine-patch helpers: `DrawRectYellow(renderer, x, y, height, width)` or `DrawRectWhite()`. These are duplicated across states - consider refactoring to shared utility if modifying.
+
+### Working with Images
+`PuzzleSingleImageState` loads images via `SDL_image`, splits them into logical pieces (stored in `pieces_logical`), then creates physical pieces (`pieces_physical`) scaled to screen. Key methods:
+- `LoadPictureFromFile()` - Load and create initial piece
+- `SplitPiece()` / `SplitPieceVertical()` / `SplitPieceHorisontal()` - Recursively divide pieces
+- `Shuffle()` - Randomize piece positions and rotations
+- `ResizeImagePhysical()` - Recalculate on window resize
+
+### Adding New Collections
+Place images in `data/collections/<collection_name>/`. Collections need a `collection.json` with a `puzzles` array (fields: `image`, `title`, `description`, `flip_mode`, `rectangular_mode`). Add a `README.md` for documentation. Collections are auto-discovered at runtime.
+
+### Application Icon
+- **Window icon**: `data/textures/app_icon.png` — 128×128 PNG loaded via PhysFS in `InitGame()`
+- **Desktop icon**: `extra/sago-multi-scrambler-puzzle2.svg` — SVG installed to `~/.local/share/icons/hicolor/scalable/apps/` by `--install-desktop-entry`
+- Rasterize SVG: `inkscape --export-type=png --export-width=128 --export-height=128 --export-filename=data/textures/app_icon.png extra/sago-multi-scrambler-puzzle2.svg`
+
+## Gotchas & Technical Notes
+
+- **PhysFS init order:** `InitSagoFS()` must be called before `InitGame()` — icon loading at window creation depends on it
+- **`sago::` namespace:** `GetFileContent()`, `FileExists()`, `WriteFileContent()` are all in the `sago::` namespace (from `sago/SagoMisc.hpp`)
+- **Duplicated Code:** `DrawRect()`/`DrawRectYellow()`/`DrawRectWhite()` exist in both `MainGameState.cpp` and `ImageSelectState.cpp` as static functions
+- **Header Links:** Some headers reference `https://github.com/sago007/saland` (old project) instead of correct `sago_multi_scrambler_puzzle2`
+- **Version Macros:** `GAMENAME` defined in two places: `globals.hpp` (with display name) and `version.h` (lowercase for save paths)
+- **Mouse Cursor:** Native cursor disabled, custom sprite drawn via `globalData.mouse`
+- **Resource Invalidation:** When window resizes, must call `dataHolder.invalidateAll()` to force texture reloads
+- **ImGui Backends:** Uses `imgui_impl_sdl2.cpp` and `imgui_impl_sdlrenderer2.cpp` from ImGui's backends
+
+## Dependencies
+External libraries required by CMake:
+- **SDL2** (core, image, ttf, mixer, gfx) - Graphics/audio/input
+- **Boost** (program_options) - Command-line parsing
+- **PhysFS** - Virtual filesystem
+- **rhash** - Hashing library (used for puzzle state tracking)
+- **platform_folders** - Cross-platform paths (embedded in `embedded_libs/`)
+
+All other libraries (ImGui, rapidjson) vendored in `src/Libs/`.
diff --git a/.github/prompts/add-collection.prompt.md b/.github/prompts/add-collection.prompt.md
new file mode 100644
index 0000000..24e8e32
--- /dev/null
+++ b/.github/prompts/add-collection.prompt.md
@@ -0,0 +1,42 @@
+---
+description: "Scaffold a new puzzle collection: creates collection.json and README.md under data/collections/<name>/"
+argument-hint: "Collection name (e.g. animals, landscapes)"
+agent: "agent"
+---
+
+Scaffold a new puzzle collection for this game. The collection name is: **$ARGUMENTS**
+
+## What to do
+
+1. Derive the folder name: lowercase, underscores for spaces (e.g. "Fairy Tales" → `fairy_tales`).
+
+2. Create `data/collections/<folder_name>/collection.json` using this exact schema — do not add extra fields:
+```json
+{
+ "name": "<Human-readable collection name>",
+ "description": "<One-sentence description of the collection>",
+ "puzzles": [
+ {
+ "image": "<filename.jpg>",
+ "title": "<Puzzle title>",
+ "description": "<One-sentence description of this image>",
+ "flip_mode": false,
+ "rectangular_mode": false
+ }
+ ]
+}
+```
+- `image` must be a filename only (no path) — images live in the same folder as `collection.json`
+- `flip_mode: true` enables piece-flipping gameplay; use `false` unless the user asks for it
+- `rectangular_mode: true` forces rectangular (non-rotated) pieces; use `false` unless asked
+
+3. Create `data/collections/<folder_name>/README.md` listing each image filename, its source URL (if known), and the image author/copyright. Follow the format used in [data/collections/fairy_tales/README.md](../../data/collections/fairy_tales/README.md).
+
+4. **Do not** copy any image files — only create the metadata files. Remind the user to place image files in `data/collections/<folder_name>/` before running the game.
+
+5. After creating the files, print a short summary: folder path, number of puzzle entries scaffolded, and a reminder about adding the actual image files.
+
+## Notes
+- Collections are auto-discovered at runtime — no code changes needed.
+- See [.github/copilot-instructions.md](../copilot-instructions.md) → "Adding New Collections" for full context.
+- If the user hasn't provided image filenames, create placeholder entries with `"image": "TODO.jpg"` and note they need to be filled in.
diff --git a/.github/prompts/new-game-state.prompt.md b/.github/prompts/new-game-state.prompt.md
new file mode 100644
index 0000000..493f595
--- /dev/null
+++ b/.github/prompts/new-game-state.prompt.md
@@ -0,0 +1,108 @@
+---
+description: "Scaffold a new game state: creates the .hpp and .cpp boilerplate for a sago::GameStateInterface subclass in src/"
+argument-hint: "State class name (e.g. HighScoreState, OptionsState)"
+agent: "agent"
+---
+
+Scaffold a new game state for this project. The class name is: **$ARGUMENTS**
+
+## What to do
+
+1. Derive the filename: class name as-is (e.g. `HighScoreState` → `HighScoreState.hpp` / `HighScoreState.cpp`). Files go in `src/`.
+
+2. Create `src/<ClassName>.hpp` using this exact pattern (based on existing states like [src/CollectionListState.hpp](../src/CollectionListState.hpp)):
+
+```cpp
+/*
+===========================================================================
+ * Sago Multi Scrambler Puzzle
+Copyright (C) 2022-2026 Poul Sander
+...license header...
+===========================================================================
+*/
+
+#pragma once
+
+#include "sago/GameStateInterface.hpp"
+
+class <ClassName> : public sago::GameStateInterface {
+public:
+ <ClassName>();
+ <ClassName>(const <ClassName>& orig) = delete;
+ virtual ~<ClassName>();
+
+ bool IsActive() override;
+ void ProcessInput(const SDL_Event& event, bool& processed) override;
+ void Draw(SDL_Renderer* target) override;
+ void Update() override;
+
+private:
+ bool isActive = true;
+};
+```
+
+3. Create `src/<ClassName>.cpp` using this exact pattern (based on [src/CollectionListState.cpp](../src/CollectionListState.cpp)):
+
+```cpp
+/*
+===========================================================================
+ * Sago Multi Scrambler Puzzle
+Copyright (C) 2022-2026 Poul Sander
+...license header...
+===========================================================================
+*/
+
+#include "<ClassName>.hpp"
+#include "sago_common.hpp"
+#include "globals.hpp"
+#include "SagoImGui.hpp"
+
+<ClassName>::<ClassName>() {
+}
+
+<ClassName>::~<ClassName>() {
+}
+
+bool <ClassName>::IsActive() {
+ return isActive;
+}
+
+void <ClassName>::ProcessInput(const SDL_Event& event, bool& processed) {
+ ImGui_ImplSDL2_ProcessEvent(&event);
+ if (event.type == SDL_KEYDOWN) {
+ if (event.key.keysym.sym == SDLK_ESCAPE) {
+ isActive = false;
+ processed = true;
+ }
+ }
+}
+
+void <ClassName>::Draw(SDL_Renderer* target) {
+ // TODO: implement drawing
+}
+
+void <ClassName>::Update() {
+ // TODO: implement per-frame logic
+}
+```
+
+4. **Do not** wire the state into `src/sago_multi_scrambler_puzzle2.cpp` unless the user explicitly asks — just create the two files.
+
+5. After creating the files, print:
+ - The two file paths created
+ - A snippet showing how to launch the state from `main()`:
+ ```cpp
+ #include "<ClassName>.hpp"
+ // ...
+ InitGame();
+ <ClassName> state;
+ RunGameState(state);
+ UninitGame();
+ ```
+
+## Key rules
+- Always `#include "SagoImGui.hpp"` in the .cpp so `ImGui_ImplSDL2_ProcessEvent` is available — forgetting this is the most common compile error.
+- Use tabs for indentation (matches the existing codebase style).
+- `isActive = false` is the only way to exit a state (pops from the game loop in `RunGameState()`).
+- Access renderer output size via `globalData.xsize` / `globalData.ysize` (not SDL_GetWindowSize).
+- See [.github/copilot-instructions.md](../copilot-instructions.md) → "Adding a New Game State" for full context.
diff --git a/data/textures/app_icon.png b/data/textures/app_icon.png
new file mode 100644
index 0000000..c6fcbaa
Binary files /dev/null and b/data/textures/app_icon.png differ
diff --git a/extra/sago-multi-scrambler-puzzle2.desktop b/extra/sago-multi-scrambler-puzzle2.desktop
new file mode 100644
index 0000000..7573427
--- /dev/null
+++ b/extra/sago-multi-scrambler-puzzle2.desktop
@@ -0,0 +1,10 @@
+[Desktop Entry]
+Version=1.0
+Type=Application
+Name=Sago Multi Scrambler Puzzle II
+Comment=Image scrambling puzzle game
+Exec=/home/poul/programmering/git/sago_multi_scrambler_puzzle2/sago_multi_scrambler_puzzle2 %f
+Icon=sago-multi-scrambler-puzzle2
+Terminal=false
+Categories=Game;LogicGame;
+MimeType=image/jpeg;image/png;image/jpg;
\ No newline at end of file
diff --git a/extra/sago-multi-scrambler-puzzle2.png b/extra/sago-multi-scrambler-puzzle2.png
new file mode 100644
index 0000000..d07aa6b
Binary files /dev/null and b/extra/sago-multi-scrambler-puzzle2.png differ
diff --git a/extra/sago-multi-scrambler-puzzle2.svg b/extra/sago-multi-scrambler-puzzle2.svg
new file mode 100644
index 0000000..8cc0096
--- /dev/null
+++ b/extra/sago-multi-scrambler-puzzle2.svg
@@ -0,0 +1,60 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="512" height="512">
+ <!-- Clip to rounded background shape -->
+ <defs>
+ <clipPath id="bg-clip">
+ <rect width="512" height="512" rx="72" ry="72"/>
+ </clipPath>
+ </defs>
+
+ <!-- Background -->
+ <rect width="512" height="512" rx="72" ry="72" fill="#eef2f7"/>
+
+ <!-- Clipped group so scattered pieces don't overflow -->
+ <g clip-path="url(#bg-clip)">
+
+ <!-- Piece 1: Cornflower Blue, top-left -->
+ <g transform="translate(128,138) rotate(-13)">
+ <rect x="-75" y="-50" width="150" height="100" rx="5" fill="#4a90d9" stroke="#2c6ea6" stroke-width="6"/>
+ </g>
+
+ <!-- Piece 2: Amber, top-center -->
+ <g transform="translate(292,88) rotate(9)">
+ <rect x="-70" y="-45" width="140" height="90" rx="5" fill="#f5a623" stroke="#c07a0c" stroke-width="6"/>
+ </g>
+
+ <!-- Piece 3: Green, top-right -->
+ <g transform="translate(408,162) rotate(-6)">
+ <rect x="-65" y="-50" width="130" height="100" rx="5" fill="#5cb85c" stroke="#3d8b3d" stroke-width="6"/>
+ </g>
+
+ <!-- Piece 4: Yellow, mid-left -->
+ <g transform="translate(78,295) rotate(19)">
+ <rect x="-78" y="-44" width="156" height="88" rx="5" fill="#f0e040" stroke="#b8a800" stroke-width="6"/>
+ </g>
+
+ <!-- Piece 5: Red, center -->
+ <g transform="translate(268,272) rotate(-8)">
+ <rect x="-75" y="-50" width="150" height="100" rx="5" fill="#d9534f" stroke="#a52c28" stroke-width="6"/>
+ </g>
+
+ <!-- Piece 6: Purple, mid-right -->
+ <g transform="translate(428,318) rotate(6)">
+ <rect x="-65" y="-46" width="130" height="92" rx="5" fill="#9b59b6" stroke="#6c3483" stroke-width="6"/>
+ </g>
+
+ <!-- Piece 7: Teal, bottom-left -->
+ <g transform="translate(142,410) rotate(-17)">
+ <rect x="-70" y="-50" width="140" height="100" rx="5" fill="#1abc9c" stroke="#148f77" stroke-width="6"/>
+ </g>
+
+ <!-- Piece 8: Orange, bottom-right -->
+ <g transform="translate(385,428) rotate(13)">
+ <rect x="-75" y="-44" width="150" height="88" rx="5" fill="#e67e22" stroke="#b15c0e" stroke-width="6"/>
+ </g>
+
+ </g>
+
+ <!-- Subtle border -->
+ <rect width="512" height="512" rx="72" ry="72" fill="none" stroke="#c8d0da" stroke-width="8"/>
+</svg>
diff --git a/src/sago_common.cpp b/src/sago_common.cpp
index f64c84f..c7ef8bf 100644
--- a/src/sago_common.cpp
+++ b/src/sago_common.cpp
@@ -29,6 +29,7 @@ https://github.com/sago007/saland
#include "sago_common.hpp"
#include "SagoImGui.hpp"
#include "sago/platform_folders.h"
+#include "sago/SagoMisc.hpp"
#include "os.hpp"
#include <iostream>
@@ -167,6 +168,18 @@ void InitGame() {
SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "2");
SDL_SetHint(SDL_HINT_MOUSE_RELATIVE_SCALING, "1");
win = SDL_CreateWindow(GAMENAME, SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, width, height, SDL_WINDOW_RESIZABLE);
+ // Set the window icon from the bundled PNG (loaded via PhysFS)
+ {
+ std::string icon_data = sago::GetFileContent("textures/app_icon.png");
+ if (!icon_data.empty()) {
+ SDL_RWops* rw = SDL_RWFromConstMem(icon_data.data(), static_cast<int>(icon_data.size()));
+ SDL_Surface* icon_surface = IMG_Load_RW(rw, 1);
+ if (icon_surface) {
+ SDL_SetWindowIcon(win, icon_surface);
+ SDL_FreeSurface(icon_surface);
+ }
+ }
+ }
globalData.screen = SDL_CreateRenderer(win, -1, rendererFlags);
//SDL_RenderSetLogicalSize(globalData.screen, width, height);
InitImGui(win, globalData.screen, width, height);
diff --git a/src/sago_multi_scrambler_puzzle2.cpp b/src/sago_multi_scrambler_puzzle2.cpp
index 4997372..87d5208 100644
--- a/src/sago_multi_scrambler_puzzle2.cpp
+++ b/src/sago_multi_scrambler_puzzle2.cpp
@@ -119,12 +119,34 @@ bool installDesktopEntry() {
desktop_file << "Name=Sago Multi Scrambler Puzzle II\n";
desktop_file << "Comment=Image scrambling puzzle game\n";
desktop_file << "Exec=" << exe_path << " %f\n";
+ desktop_file << "Icon=sago-multi-scrambler-puzzle2\n";
desktop_file << "Terminal=false\n";
desktop_file << "Categories=Game;LogicGame;\n";
desktop_file << "MimeType=image/jpeg;image/png;image/jpg;\n";
desktop_file.close();
+ // Install SVG icon into the hicolor icon theme
+ std::string svg_source;
+ try {
+ std::filesystem::path exe_dir = std::filesystem::path(exe_path).parent_path();
+ std::filesystem::path svg_src = exe_dir / "extra" / "sago-multi-scrambler-puzzle2.svg";
+ if (std::filesystem::exists(svg_src)) {
+ std::string icon_dir = sago::getDataHome() + "/icons/hicolor/scalable/apps";
+ OsCreateFolder(icon_dir);
+ std::filesystem::path icon_dest = icon_dir + "/sago-multi-scrambler-puzzle2.svg";
+ std::filesystem::copy_file(svg_src, icon_dest, std::filesystem::copy_options::overwrite_existing);
+ std::cout << "Icon installed at: " << icon_dest.string() << "\n";
+ // Update icon cache if possible
+ std::string icon_cache_cmd = "gtk-update-icon-cache -f \"" + sago::getDataHome() + "/icons/hicolor\" 2>/dev/null";
+ system(icon_cache_cmd.c_str());
+ } else {
+ std::cout << "Note: SVG icon not found at " << svg_src.string() << ". Desktop entry will use fallback icon.\n";
+ }
+ } catch (const std::exception& e) {
+ std::cout << "Note: Could not install icon: " << e.what() << "\n";
+ }
+
std::cout << "Desktop entry created successfully at: " << desktop_file_path << "\n";
// Update the desktop database