From 369e032bcf1557b5a463097052b5f81f3cd2083d Mon Sep 17 00:00:00 2001 From: Poul Sander Date: Fri, 20 Feb 2026 17:51:17 +0100 Subject: [PATCH 1/7] Theme editor initial commit --- source/code/main.cpp | 16 + source/code/mainVars.inc | 1 + source/code/theme_editor/ThemeEditorState.cpp | 578 ++++++++++++++++++ source/code/theme_editor/ThemeEditorState.hpp | 108 ++++ source/code/themes.cpp | 174 +++++- source/code/themes.hpp | 64 +- 6 files changed, 939 insertions(+), 2 deletions(-) create mode 100644 source/code/theme_editor/ThemeEditorState.cpp create mode 100644 source/code/theme_editor/ThemeEditorState.hpp diff --git a/source/code/main.cpp b/source/code/main.cpp index c9bea2d7..52e78f07 100644 --- a/source/code/main.cpp +++ b/source/code/main.cpp @@ -70,6 +70,7 @@ Source information and contacts persons can be found at #include "editor/SagoTextureSelector.hpp" #include "puzzle_editor/PuzzleEditorState.hpp" +#include "theme_editor/ThemeEditorState.hpp" #include "SagoImGui.hpp" /******************************************************************************* @@ -930,6 +931,7 @@ static void ParseArguments(int argc, char* argv[], globalConfig& conf) { ("always-sixteen-nine", "Use 16:9 format even in Window mode") ("editor", "Start the sprite editor/browser") ("puzzle-editor", "Start the build in puzzle editor") + ("theme-editor", "Start the theme editor") ("puzzle-level-file", boost::program_options::value(), "Sets the default puzzle file to load") ("puzzle-single-level", boost::program_options::value(), "Start the specific puzzle level directly") #ifdef REPLAY_IMPLEMENTED @@ -1031,6 +1033,9 @@ static void ParseArguments(int argc, char* argv[], globalConfig& conf) { if (vm.count("puzzle-editor")) { puzzleEditor = true; } + if (vm.count("theme-editor")) { + themeEditor = true; + } if (vm.count("puzzle-level-file")) { conf.puzzleName = vm["puzzle-level-file"].as(); } @@ -1335,6 +1340,17 @@ int main(int argc, char* argv[]) { RunImGuiGameState(s); ImGui::SaveIniSettingsToDisk(imgui_inifile.c_str()); } + else if (themeEditor) { + InitImGui(sdlWindow, renderer, globalData.xsize, globalData.ysize); + ImGuiIO& io = ImGui::GetIO(); + io.IniFilename = nullptr; + std::string imgui_inifile = getPathToSaveFiles() + "/imgui_theme_editor.ini"; + ImGui::LoadIniSettingsFromDisk(imgui_inifile.c_str()); + ThemeEditorState s; + s.Init(); + RunImGuiGameState(s); + ImGui::SaveIniSettingsToDisk(imgui_inifile.c_str()); + } else if (globalData.replayArgument.length()) { ReplayPlayer rp; RunGameState(rp); diff --git a/source/code/mainVars.inc b/source/code/mainVars.inc index f7c48a25..34f768e8 100644 --- a/source/code/mainVars.inc +++ b/source/code/mainVars.inc @@ -92,6 +92,7 @@ bool twoPlayers; //True if two playerImGui_ImplSDL2_ProcessEvent(&event);s ar static bool singlePuzzle = false; static bool editor = false; static bool puzzleEditor = false; +static bool themeEditor = false; static int singlePuzzleNr = 0; static std::string singlePuzzleFile; diff --git a/source/code/theme_editor/ThemeEditorState.cpp b/source/code/theme_editor/ThemeEditorState.cpp new file mode 100644 index 00000000..c2a4e473 --- /dev/null +++ b/source/code/theme_editor/ThemeEditorState.cpp @@ -0,0 +1,578 @@ +/* +=========================================================================== +blockattack - Block Attack - Rise of the Blocks +Copyright (C) 2005-2026 Poul Sander + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see http://www.gnu.org/licenses/ + +Source information and contacts persons can be found at +https://blockattack.net +=========================================================================== +*/ + +#include "ThemeEditorState.hpp" + +#include +#include + +#include "imgui.h" +#include "imgui_internal.h" +#include "backends/imgui_impl_sdl2.h" +#include "backends/imgui_impl_sdlrenderer2.h" +#include "../sago/SagoMisc.hpp" +#include "../BlockGameSdl.hpp" + +ThemeEditorState::ThemeEditorState() { + current_border.name = "standard"; + current_border.border_sprite = "board_back_back"; + current_border.border_sprite_offset = {-60, -68}; + current_border.score_label_offset = {310, 80}; + current_border.time_label_offset = {310, 129}; + current_border.chain_label_offset = {310, 178}; + current_border.speed_label_offset = {310, 227}; + + current_background.name = "standard"; + current_background.background_sprite = "background"; + current_background.background_sprite_16x9 = "background_sixteen_nine"; + current_background.background_scale = ImgScale::Stretch; +} + +ThemeEditorState::~ThemeEditorState() { + // Restore the original theme when editor exits + globalData.theme = saved_theme; +} + +bool ThemeEditorState::IsActive() { + return isActive; +} + +void ThemeEditorState::ProcessInput(const SDL_Event& event, bool& processed) { + (void)event; + (void)processed; + // Handle any custom input processing here if needed +} + +void ThemeEditorState::Init() { + // Initialize the theme system + ThemesInit(); + + // Populate lists + border_names = ThemesGetBorderNames(); + background_names = ThemesGetBackgroundNames(); + decoration_names = ThemesGetDecorationNames(); + + // Save current theme and set up preview + saved_theme = globalData.theme; + preview_theme = ThemesGet(0); + + // Create preview game instance positioned in the right 40% of screen + // Editor takes 60% of width, so preview starts at 60% + some padding + int preview_x = static_cast(globalData.xsize * 0.62f); + preview_game = std::make_shared(preview_x, 100, &globalData.spriteHolder->GetDataHolder()); + preview_game->name = "Preview"; + preview_game->putSampleBlocks(); + + // Initialize preview with current border/background + preview_theme.border = current_border; + preview_theme.background = current_background; + globalData.theme = preview_theme; +} + +void ThemeEditorState::Update() { + // Update preview if needed + if (preview_needs_update) { + preview_needs_update = false; + UpdatePreview(); + } +} + +void ThemeEditorState::UpdatePreview() { + // Update preview theme based on current tab + switch (current_tab) { + case 0: // Border Editor + preview_theme.border = current_border; + break; + case 1: // Background Editor + preview_theme.background = current_background; + break; + case 2: // Theme Composer + // Combine selected components + if (static_cast(selected_border_index) < border_names.size()) { + preview_theme.border = ThemesGetBorder(border_names[selected_border_index]); + } + if (static_cast(selected_background_index) < background_names.size()) { + preview_theme.background = ThemesGetBackground(background_names[selected_background_index]); + } + preview_theme.back_board = board_backgrounds[selected_board_background]; + break; + } + + // Apply theme to global for preview rendering + globalData.theme = preview_theme; +} + +void ThemeEditorState::Draw(SDL_Renderer* target) { + (void)target; + // Main window - take up left 60% of screen to leave room for preview + const float editor_width = globalData.xsize * 0.6f; + ImGui::SetNextWindowPos(ImVec2(0, 0), ImGuiCond_Always); + ImGui::SetNextWindowSize(ImVec2(editor_width, (float)globalData.ysize), ImGuiCond_Always); + + ImGui::Begin("Theme Editor", nullptr, ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoCollapse); + + // Tab bar + if (ImGui::BeginTabBar("EditorTabs")) { + if (ImGui::BeginTabItem("Border Editor")) { + current_tab = 0; + DrawBorderEditor(); + ImGui::EndTabItem(); + } + if (ImGui::BeginTabItem("Background Editor")) { + current_tab = 1; + DrawBackgroundEditor(); + ImGui::EndTabItem(); + } + if (ImGui::BeginTabItem("Theme Composer")) { + current_tab = 2; + DrawThemeComposer(); + ImGui::EndTabItem(); + } + ImGui::EndTabBar(); + } + + ImGui::Separator(); + if (ImGui::Button("Exit Editor")) { + isActive = false; + } + + ImGui::End(); + + // Draw preview label in the right portion of screen + ImGui::SetNextWindowPos(ImVec2(editor_width + 10, 10), ImGuiCond_Always); + ImGui::SetNextWindowSize(ImVec2(globalData.xsize - editor_width - 20, 80), ImGuiCond_Always); + ImGui::Begin("Preview", nullptr, ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoCollapse); + ImGui::Text("Live Preview:"); + ImGui::TextWrapped("Changes are shown in real-time below"); + ImGui::End(); + + // Draw the preview + if (preview_game) { + preview_game->DoPaintJob(); + } +} + +void ThemeEditorState::DrawBorderEditor() { + ImGui::Text("Create and edit custom borders"); + ImGui::Separator(); + + // Load existing border + ImGui::Text("Load existing border:"); + if (ImGui::BeginCombo("##BorderLoad", border_names.empty() ? "No borders" : current_border.name.c_str())) { + for (const auto& name : border_names) { + bool is_selected = (current_border.name == name); + if (ImGui::Selectable(name.c_str(), is_selected)) { + LoadBorder(name); + } + if (is_selected) { + ImGui::SetItemDefaultFocus(); + } + } + ImGui::EndCombo(); + } + + ImGui::Separator(); + ImGui::Text("Border Settings:"); + + // Border name + ImGui::InputText("Border Name", border_name_buffer, sizeof(border_name_buffer)); + + // Border sprite + if (ImGui::InputText("Border Sprite", border_sprite_buffer, sizeof(border_sprite_buffer))) { + current_border.border_sprite = border_sprite_buffer; + preview_needs_update = true; + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Examples: board_back_back, board_back_back_mirror"); + } + + // Validate sprite button + ImGui::SameLine(); + if (ImGui::Button("Validate##border")) { + if (ValidateSprite(border_sprite_buffer)) { + ImGui::OpenPopup("Valid Sprite"); + } else { + ImGui::OpenPopup("Invalid Sprite"); + } + } + + if (ImGui::BeginPopup("Valid Sprite")) { + ImGui::Text("Sprite exists!"); + ImGui::EndPopup(); + } + if (ImGui::BeginPopup("Invalid Sprite")) { + ImGui::Text("Warning: Sprite does not exist!"); + ImGui::EndPopup(); + } + + // Border sprite offset + if (ImGui::DragInt2("Border Sprite Offset", &border_offset_x)) { + current_border.border_sprite_offset = {border_offset_x, border_offset_y}; + preview_needs_update = true; + } + + // Label offsets + ImGui::Separator(); + ImGui::Text("Label Positions:"); + if (ImGui::DragInt2("Score Label Offset", &score_offset_x)) { + current_border.score_label_offset = {score_offset_x, score_offset_y}; + preview_needs_update = true; + } + if (ImGui::DragInt2("Time Label Offset", &time_offset_x)) { + current_border.time_label_offset = {time_offset_x, time_offset_y}; + preview_needs_update = true; + } + if (ImGui::DragInt2("Chain Label Offset", &chain_offset_x)) { + current_border.chain_label_offset = {chain_offset_x, chain_offset_y}; + preview_needs_update = true; + } + if (ImGui::DragInt2("Speed Label Offset", &speed_offset_x)) { + current_border.speed_label_offset = {speed_offset_x, speed_offset_y}; + preview_needs_update = true; + } + + ImGui::Separator(); + if (ImGui::Button("Save Border")) { + SaveCurrentBorder(); + } + ImGui::SameLine(); + ImGui::TextColored(ImVec4(0.5f, 0.5f, 0.5f, 1.0f), "Saved to: borders/.json"); +} + +void ThemeEditorState::DrawBackgroundEditor() { + ImGui::Text("Create and edit custom backgrounds"); + ImGui::Separator(); + + // Load existing background + ImGui::Text("Load existing background:"); + if (ImGui::BeginCombo("##BackgroundLoad", background_names.empty() ? "No backgrounds" : current_background.name.c_str())) { + for (const auto& name : background_names) { + bool is_selected = (current_background.name == name); + if (ImGui::Selectable(name.c_str(), is_selected)) { + LoadBackground(name); + } + if (is_selected) { + ImGui::SetItemDefaultFocus(); + } + } + ImGui::EndCombo(); + } + + ImGui::Separator(); + ImGui::Text("Background Settings:"); + + // Background name + ImGui::InputText("Background Name", background_name_buffer, sizeof(background_name_buffer)); + + // Background sprite + if (ImGui::InputText("Background Sprite", background_sprite_buffer, sizeof(background_sprite_buffer))) { + current_background.background_sprite = background_sprite_buffer; + preview_needs_update = true; + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Examples: background, background_autumn, custom_background_"); + } + + // Validate sprite button + ImGui::SameLine(); + if (ImGui::Button("Validate##background")) { + if (ValidateSprite(background_sprite_buffer)) { + ImGui::OpenPopup("Valid BG Sprite"); + } else { + ImGui::OpenPopup("Invalid BG Sprite"); + } + } + + if (ImGui::BeginPopup("Valid BG Sprite")) { + ImGui::Text("Sprite exists!"); + ImGui::EndPopup(); + } + if (ImGui::BeginPopup("Invalid BG Sprite")) { + ImGui::Text("Warning: Sprite does not exist!"); + ImGui::EndPopup(); + } + + // 16:9 background sprite (optional) + if (ImGui::InputText("Background Sprite 16:9 (optional)", background_sprite_16x9_buffer, sizeof(background_sprite_16x9_buffer))) { + current_background.background_sprite_16x9 = background_sprite_16x9_buffer; + preview_needs_update = true; + } + + // Scaling mode + ImGui::Separator(); + ImGui::Text("Scaling Mode:"); + const char* scale_modes[] = { "Stretch", "Tile", "Resize", "Cut" }; + if (ImGui::Combo("##ScaleMode", &background_scale_mode, scale_modes, 4)) { + switch (background_scale_mode) { + case 0: current_background.background_scale = ImgScale::Stretch; break; + case 1: current_background.background_scale = ImgScale::Tile; break; + case 2: current_background.background_scale = ImgScale::Resize; break; + case 3: current_background.background_scale = ImgScale::Cut; break; + } + preview_needs_update = true; + } + + // Tile animation speed + ImGui::Separator(); + ImGui::Text("Tile Animation (0 = disabled):"); + if (ImGui::DragInt("Move Speed X (ms)", &tile_move_speed_x, 1.0f, 0, 10000)) { + current_background.tileMoveSpeedX = tile_move_speed_x; + preview_needs_update = true; + } + if (ImGui::DragInt("Move Speed Y (ms)", &tile_move_speed_y, 1.0f, 0, 10000)) { + current_background.tileMoveSpeedY = tile_move_speed_y; + preview_needs_update = true; + } + + ImGui::Separator(); + if (ImGui::Button("Save Background")) { + SaveCurrentBackground(); + } + ImGui::SameLine(); + ImGui::TextColored(ImVec4(0.5f, 0.5f, 0.5f, 1.0f), "Saved to: backgrounds/.json"); +} + +void ThemeEditorState::DrawThemeComposer() { + ImGui::Text("Combine borders and backgrounds into a complete theme"); + ImGui::Separator(); + + // Theme name + ImGui::InputText("Theme Name", theme_name_buffer, sizeof(theme_name_buffer)); + + // Border selection + ImGui::Separator(); + ImGui::Text("Select Border:"); + if (ImGui::BeginCombo("##BorderSelect", static_cast(selected_border_index) < border_names.size() ? border_names[selected_border_index].c_str() : "")) { + for (size_t i = 0; i < border_names.size(); i++) { + bool is_selected = (static_cast(selected_border_index) == i); + if (ImGui::Selectable(border_names[i].c_str(), is_selected)) { + selected_border_index = i; + preview_needs_update = true; + } + if (is_selected) { + ImGui::SetItemDefaultFocus(); + } + } + ImGui::EndCombo(); + } + + // Background selection + ImGui::Text("Select Background:"); + if (ImGui::BeginCombo("##BackgroundSelect", static_cast(selected_background_index) < background_names.size() ? background_names[selected_background_index].c_str() : "")) { + for (size_t i = 0; i < background_names.size(); i++) { + bool is_selected = (static_cast(selected_background_index) == i); + if (ImGui::Selectable(background_names[i].c_str(), is_selected)) { + selected_background_index = i; + preview_needs_update = true; + } + if (is_selected) { + ImGui::SetItemDefaultFocus(); + } + } + ImGui::EndCombo(); + } + + // Decoration selection + ImGui::Text("Select Decoration:"); + if (ImGui::BeginCombo("##DecorationSelect", static_cast(selected_decoration_index) < decoration_names.size() ? decoration_names[selected_decoration_index].c_str() : "")) { + for (size_t i = 0; i < decoration_names.size(); i++) { + bool is_selected = (static_cast(selected_decoration_index) == i); + if (ImGui::Selectable(decoration_names[i].c_str(), is_selected)) { + selected_decoration_index = i; + preview_needs_update = true; + } + if (is_selected) { + ImGui::SetItemDefaultFocus(); + } + } + ImGui::EndCombo(); + } + + // Board background selection + ImGui::Text("Select Board Background:"); + if (ImGui::BeginCombo("##BoardBGSelect", board_backgrounds[selected_board_background].c_str())) { + for (size_t i = 0; i < board_backgrounds.size(); i++) { + bool is_selected = (static_cast(selected_board_background) == i); + if (ImGui::Selectable(board_backgrounds[i].c_str(), is_selected)) { + selected_board_background = i; + preview_needs_update = true; + } + if (is_selected) { + ImGui::SetItemDefaultFocus(); + } + } + ImGui::EndCombo(); + } + + ImGui::Separator(); + if (ImGui::Button("Save Theme")) { + SaveCurrentTheme(); + } + ImGui::SameLine(); + ImGui::TextColored(ImVec4(0.5f, 0.5f, 0.5f, 1.0f), "Saved to: custom_themes.json"); +} + +void ThemeEditorState::DrawPreview(SDL_Renderer* target) { + (void)target; + // TODO: Implement preview rendering using BlockGameSdl +} + +void ThemeEditorState::SaveCurrentBorder() { + // Update current_border from UI fields + std::string name = border_name_buffer; + if (name.empty()) { + std::cerr << "Error: Border name cannot be empty\n"; + return; + } + + current_border.name = name; + current_border.border_sprite = border_sprite_buffer; + current_border.border_sprite_offset = {border_offset_x, border_offset_y}; + current_border.score_label_offset = {score_offset_x, score_offset_y}; + current_border.time_label_offset = {time_offset_x, time_offset_y}; + current_border.chain_label_offset = {chain_offset_x, chain_offset_y}; + current_border.speed_label_offset = {speed_offset_x, speed_offset_y}; + + // Validate sprite + if (!ValidateSprite(current_border.border_sprite)) { + std::cerr << "Warning: Saving border with non-existent sprite: " << current_border.border_sprite << "\n"; + } + + // Save to file + ThemesSaveBorderData(name, current_border); + std::cout << "Saved border: " << name << "\n"; + + // Refresh border list + border_names = ThemesGetBorderNames(); +} + +void ThemeEditorState::SaveCurrentBackground() { + // Update current_background from UI fields + std::string name = background_name_buffer; + if (name.empty()) { + std::cerr << "Error: Background name cannot be empty\n"; + return; + } + + current_background.name = name; + current_background.background_sprite = background_sprite_buffer; + current_background.background_sprite_16x9 = background_sprite_16x9_buffer; + + // Convert scale mode + switch (background_scale_mode) { + case 0: current_background.background_scale = ImgScale::Stretch; break; + case 1: current_background.background_scale = ImgScale::Tile; break; + case 2: current_background.background_scale = ImgScale::Resize; break; + case 3: current_background.background_scale = ImgScale::Cut; break; + } + + current_background.tileMoveSpeedX = tile_move_speed_x; + current_background.tileMoveSpeedY = tile_move_speed_y; + + // Validate sprite + if (!ValidateSprite(current_background.background_sprite)) { + std::cerr << "Warning: Saving background with non-existent sprite: " << current_background.background_sprite << "\n"; + } + + // Save to file + ThemesSaveBackgroundData(name, current_background); + std::cout << "Saved background: " << name << "\n"; + + // Refresh background list + background_names = ThemesGetBackgroundNames(); +} + +void ThemeEditorState::SaveCurrentTheme() { + std::string name = theme_name_buffer; + if (name.empty()) { + std::cerr << "Error: Theme name cannot be empty\n"; + return; + } + + Theme theme; + theme.theme_name = name; + + // Set components + if (static_cast(selected_border_index) < border_names.size()) { + theme.border = ThemesGetBorder(border_names[selected_border_index]); + } + if (static_cast(selected_background_index) < background_names.size()) { + theme.background = ThemesGetBackground(background_names[selected_background_index]); + } + theme.back_board = board_backgrounds[selected_board_background]; + + // Add or replace the theme + ThemesAddOrReplace(theme); + + // Save to custom themes file + ThemesSaveCustomSlots(); + + std::cout << "Saved theme: " << name << "\n"; +} + +void ThemeEditorState::LoadBorder(const std::string& name) { + current_border = ThemesGetBorder(name); + + // Update UI fields + strncpy(border_name_buffer, current_border.name.c_str(), sizeof(border_name_buffer) - 1); + strncpy(border_sprite_buffer, current_border.border_sprite.c_str(), sizeof(border_sprite_buffer) - 1); + border_offset_x = current_border.border_sprite_offset.first; + border_offset_y = current_border.border_sprite_offset.second; + score_offset_x = current_border.score_label_offset.first; + score_offset_y = current_border.score_label_offset.second; + time_offset_x = current_border.time_label_offset.first; + time_offset_y = current_border.time_label_offset.second; + chain_offset_x = current_border.chain_label_offset.first; + chain_offset_y = current_border.chain_label_offset.second; + speed_offset_x = current_border.speed_label_offset.first; + speed_offset_y = current_border.speed_label_offset.second; +} + +void ThemeEditorState::LoadBackground(const std::string& name) { + current_background = ThemesGetBackground(name); + + // Update UI fields + strncpy(background_name_buffer, current_background.name.c_str(), sizeof(background_name_buffer) - 1); + strncpy(background_sprite_buffer, current_background.background_sprite.c_str(), sizeof(background_sprite_buffer) - 1); + strncpy(background_sprite_16x9_buffer, current_background.background_sprite_16x9.c_str(), sizeof(background_sprite_16x9_buffer) - 1); + + // Convert scale mode + switch (current_background.background_scale) { + case ImgScale::Stretch: background_scale_mode = 0; break; + case ImgScale::Tile: background_scale_mode = 1; break; + case ImgScale::Resize: background_scale_mode = 2; break; + case ImgScale::Cut: background_scale_mode = 3; break; + } + + tile_move_speed_x = current_background.tileMoveSpeedX; + tile_move_speed_y = current_background.tileMoveSpeedY; +} + +bool ThemeEditorState::ValidateSprite(const std::string& sprite_name) { + return ThemesValidateSpriteName(sprite_name); +} + +std::vector ThemeEditorState::GetAvailableSprites() { + // TODO: Implement getting available sprite names from globalData.spriteHolder + return std::vector(); +} diff --git a/source/code/theme_editor/ThemeEditorState.hpp b/source/code/theme_editor/ThemeEditorState.hpp new file mode 100644 index 00000000..effdaecd --- /dev/null +++ b/source/code/theme_editor/ThemeEditorState.hpp @@ -0,0 +1,108 @@ +/* +=========================================================================== +blockattack - Block Attack - Rise of the Blocks +Copyright (C) 2005-2026 Poul Sander + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see http://www.gnu.org/licenses/ + +Source information and contacts persons can be found at +https://blockattack.net +=========================================================================== +*/ + +#pragma once + +#include "../global.hpp" +#include "../sago/GameStateInterface.hpp" +#include "../themes.hpp" +#include +#include +#include + +class BlockGameSdl; + +class ThemeEditorState : public sago::GameStateInterface { +public: + ThemeEditorState(); + ThemeEditorState(const ThemeEditorState& orig) = delete; + virtual ~ThemeEditorState(); + + void Init(); + + bool IsActive() override; + void ProcessInput(const SDL_Event& event, bool& processed) override; + void Draw(SDL_Renderer* target) override; + void Update() override; + +private: + void DrawBorderEditor(); + void DrawBackgroundEditor(); + void DrawThemeComposer(); + void DrawPreview(SDL_Renderer* target); + + void SaveCurrentBorder(); + void SaveCurrentBackground(); + void SaveCurrentTheme(); + + void LoadBorder(const std::string& name); + void LoadBackground(const std::string& name); + + bool ValidateSprite(const std::string& sprite_name); + std::vector GetAvailableSprites(); + + bool isActive = true; + int current_tab = 0; // 0=Border, 1=Background, 2=Theme + + // Border editor state + ThemeBorderData current_border; + char border_name_buffer[256] = ""; + char border_sprite_buffer[256] = "board_back_back"; + int border_offset_x = -60; + int border_offset_y = -68; + int score_offset_x = 310; + int score_offset_y = 80; + int time_offset_x = 310; + int time_offset_y = 129; + int chain_offset_x = 310; + int chain_offset_y = 178; + int speed_offset_x = 310; + int speed_offset_y = 227; + + // Background editor state + BackGroundData current_background; + char background_name_buffer[256] = ""; + char background_sprite_buffer[256] = "background"; + char background_sprite_16x9_buffer[256] = "background_sixteen_nine"; + int background_scale_mode = 0; // 0=Stretch, 1=Tile, 2=Resize, 3=Cut + int tile_move_speed_x = 0; + int tile_move_speed_y = 0; + + // Theme composer state + char theme_name_buffer[256] = "custom_theme"; + int selected_border_index = 0; + int selected_background_index = 0; + int selected_decoration_index = 0; + int selected_board_background = 0; // 0=back_board, 1=back_board_sample_snow, 2=trans_cover + std::vector border_names; + std::vector background_names; + std::vector decoration_names; + const std::vector board_backgrounds = {"back_board", "back_board_sample_snow", "trans_cover"}; + + // Preview + std::shared_ptr preview_game; + Theme preview_theme; + Theme saved_theme; // Backup of the global theme + bool preview_needs_update = true; + void UpdatePreview(); +}; diff --git a/source/code/themes.cpp b/source/code/themes.cpp index 5a3645bd..8010f224 100644 --- a/source/code/themes.cpp +++ b/source/code/themes.cpp @@ -24,6 +24,7 @@ Source information and contacts persons can be found at #include "themes.hpp" #include "sago/SagoMisc.hpp" +#include "os.hpp" #include #include #include @@ -82,6 +83,18 @@ void to_json(json& j, const BackGroundData& p) { j = json{ {"background_name", p.name}, {"background_sprite", p.background_sprite}, {"background_sprite_16x9", p.background_sprite_16x9}, {"background_scale", p.background_scale}, {"tileMoveSpeedX", p.tileMoveSpeedX}, {"tileMoveSpeedY", p.tileMoveSpeedY} }; } +void to_json(json& j, const ThemeBorderData& p) { + j = json{ + {"border_name", p.name}, + {"border_sprite", p.border_sprite}, + {"border_sprite_offset", {p.border_sprite_offset.first, p.border_sprite_offset.second}}, + {"score_label_offset", {p.score_label_offset.first, p.score_label_offset.second}}, + {"time_label_offset", {p.time_label_offset.first, p.time_label_offset.second}}, + {"chain_label_offset", {p.chain_label_offset.first, p.chain_label_offset.second}}, + {"speed_label_offset", {p.speed_label_offset.first, p.speed_label_offset.second}} + }; +} + void to_json(json& j, const Theme& p) { j = json{ {"theme_name", p.theme_name}, {"back_board", p.back_board}, {"background_name", p.background.name}, {"decoration_name", p.decoration.name} }; } @@ -91,7 +104,7 @@ void to_json(json& j, const DecorationData& p) { } void to_json(json& j, const ThemeFileData& p) { - j = json{ {"background_data", p.background_data}, {"decoration_data", p.decoration_data}, {"themes", p.themes} }; + j = json{ {"background_data", p.background_data}, {"decoration_data", p.decoration_data}, {"border_data", p.border_data}, {"themes", p.themes} }; } void from_json(const json& j, BackGroundData& p) { @@ -103,6 +116,31 @@ void from_json(const json& j, BackGroundData& p) { j.at("tileMoveSpeedY").get_to(p.tileMoveSpeedY); } +void from_json(const json& j, ThemeBorderData& p) { + j.at("border_name").get_to(p.name); + j.at("border_sprite").get_to(p.border_sprite); + if (j.contains("border_sprite_offset")) { + auto offset = j.at("border_sprite_offset"); + p.border_sprite_offset = {offset[0], offset[1]}; + } + if (j.contains("score_label_offset")) { + auto offset = j.at("score_label_offset"); + p.score_label_offset = {offset[0], offset[1]}; + } + if (j.contains("time_label_offset")) { + auto offset = j.at("time_label_offset"); + p.time_label_offset = {offset[0], offset[1]}; + } + if (j.contains("chain_label_offset")) { + auto offset = j.at("chain_label_offset"); + p.chain_label_offset = {offset[0], offset[1]}; + } + if (j.contains("speed_label_offset")) { + auto offset = j.at("speed_label_offset"); + p.speed_label_offset = {offset[0], offset[1]}; + } +} + void from_json(const json& j, Theme& p) { j.at("theme_name").get_to(p.theme_name); j.at("back_board").get_to(p.back_board); @@ -122,6 +160,9 @@ void from_json(const json& j, ThemeFileData& p) { if (j.contains("decoration_data")) { j.at("decoration_data").get_to(p.decoration_data); } + if (j.contains("border_data")) { + j.at("border_data").get_to(p.border_data); + } if (j.contains("themes")) { j.at("themes").get_to(p.themes); } @@ -162,6 +203,9 @@ static void ThemesReadDataFromFile(const std::string& filename) { for (const auto& dec : tfd.decoration_data) { decoration_data[dec.name] = dec; } + for (const auto& border : tfd.border_data) { + border_data[border.name] = border; + } for (const Theme& theme : tfd.themes) { if (globalData.verboseLevel) { std::cout << "Adding theme " << theme.theme_name << "\n"; @@ -213,6 +257,9 @@ static void ThemesDumpData() { for (auto& pair : decoration_data) { tfd.decoration_data.push_back(pair.second); } + for (auto& pair : border_data) { + tfd.border_data.push_back(pair.second); + } json j = tfd; std::string s = j.dump(4); sago::WriteFileContent("themes_dump.json", s); @@ -252,6 +299,8 @@ void ThemesInit() { ThemesInitBackGroundData(); ThemesInitCustomBackgrounds(); ThemesInitBorderData(); + ThemesLoadCustomBorders(); + ThemesLoadCustomBackgrounds(); themes.resize(1); //Add the default theme ThemesFillMissingFields(themes[0]); const std::vector& theme_files = sago::GetFileList("themes"); @@ -395,4 +444,127 @@ void ThemesInitCustomBackgrounds() { } sprite_stream << "}\n"; sago::WriteFileContent("sprites/custom_backgrounds.sprite", sprite_stream.str()); +} + +void ThemesSaveBorderData(const std::string& name, const ThemeBorderData& data) { + ThemesInit(); + // Create borders directory if it doesn't exist + OsCreateFolder("borders"); + + json j = data; + std::string s = j.dump(4); + std::string filename = fmt::format("borders/{}.json", name); + sago::WriteFileContent(filename.c_str(), s); + + // Add to the border_data map + border_data[name] = data; +} + +void ThemesSaveBackgroundData(const std::string& name, const BackGroundData& data) { + ThemesInit(); + // Create backgrounds directory if it doesn't exist + OsCreateFolder("backgrounds"); + + json j = data; + std::string s = j.dump(4); + std::string filename = fmt::format("backgrounds/{}.json", name); + sago::WriteFileContent(filename.c_str(), s); + + // Add to the background_data map + background_data[name] = data; +} + +void ThemesLoadCustomBorders() { + const std::vector& border_files = sago::GetFileList("borders"); + for (const std::string& filename : border_files) { + if (boost::algorithm::ends_with(filename, ".json")) { + std::string filepath = fmt::format("borders/{}", filename); + std::string s = sago::GetFileContent(filepath); + if (s.empty()) { + continue; + } + try { + json j = json::parse(s); + ThemeBorderData border = j; + border_data[border.name] = border; + if (globalData.verboseLevel) { + std::cout << "Loaded custom border: " << border.name << "\n"; + } + } + catch (const std::exception& e) { + std::cerr << "Error loading border file " << filepath << ": " << e.what() << "\n"; + } + } + } +} + +void ThemesLoadCustomBackgrounds() { + const std::vector& background_files = sago::GetFileList("backgrounds"); + for (const std::string& filename : background_files) { + if (boost::algorithm::ends_with(filename, ".json")) { + std::string filepath = fmt::format("backgrounds/{}", filename); + std::string s = sago::GetFileContent(filepath); + if (s.empty()) { + continue; + } + try { + json j = json::parse(s); + BackGroundData background = j; + background_data[background.name] = background; + if (globalData.verboseLevel) { + std::cout << "Loaded custom background: " << background.name << "\n"; + } + } + catch (const std::exception& e) { + std::cerr << "Error loading background file " << filepath << ": " << e.what() << "\n"; + } + } + } +} + +bool ThemesValidateSpriteName(const std::string& sprite_name) { + ThemesInit(); + if (sprite_name.empty()) { + return false; + } + // Note: GetSprite returns a default sprite if not found, so we can't easily validate + // The sprite will be validated when actually used in the game + // For now, just check that it's not empty + return true; +} + +std::vector ThemesGetBorderNames() { + ThemesInit(); + std::vector names; + for (const auto& pair : border_data) { + names.push_back(pair.first); + } + return names; +} + +std::vector ThemesGetBackgroundNames() { + ThemesInit(); + std::vector names; + for (const auto& pair : background_data) { + names.push_back(pair.first); + } + return names; +} + +std::vector ThemesGetDecorationNames() { + ThemesInit(); + std::vector names; + for (const auto& pair : decoration_data) { + names.push_back(pair.first); + } + return names; +} + +BackGroundData ThemesGetBackground(const std::string& name) { + ThemesInit(); + auto it = background_data.find(name); + if (it == background_data.end()) { + return background_data["standard"]; + } + return it->second; } \ No newline at end of file diff --git a/source/code/themes.hpp b/source/code/themes.hpp index 5c9c0990..7b3747c0 100644 --- a/source/code/themes.hpp +++ b/source/code/themes.hpp @@ -70,6 +70,7 @@ struct Theme { struct ThemeFileData { std::vector background_data; std::vector decoration_data; + std::vector border_data; std::vector themes; }; @@ -77,6 +78,11 @@ void ThemesFillMissingFields(Theme& theme); void ThemesAddOrReplace(const Theme& theme); +/** + * @brief Initializes the theme system + */ +void ThemesInit(); + /** * @brief returns a theme from a list @@ -113,4 +119,60 @@ ThemeBorderData ThemesGetBorder(const std::string& name); ThemeBorderData ThemesGetNextBorder(const std::string& current); -void ThemesInitCustomBackgrounds(); \ No newline at end of file +void ThemesInitCustomBackgrounds(); + +/** + * @brief Saves a border to the borders/ directory + * @param name The name of the border + * @param data The border data to save + */ +void ThemesSaveBorderData(const std::string& name, const ThemeBorderData& data); + +/** + * @brief Saves a background to the backgrounds/ directory + * @param name The name of the background + * @param data The background data to save + */ +void ThemesSaveBackgroundData(const std::string& name, const BackGroundData& data); + +/** + * @brief Loads custom borders from the borders/ directory + */ +void ThemesLoadCustomBorders(); + +/** + * @brief Loads custom backgrounds from the backgrounds/ directory + */ +void ThemesLoadCustomBackgrounds(); + +/** + * @brief Validates that a sprite name exists in the sprite holder + * @param sprite_name The sprite name to validate + * @return true if the sprite exists, false otherwise + */ +bool ThemesValidateSpriteName(const std::string& sprite_name); + +/** + * @brief Gets all available border names + * @return Vector of border names + */ +std::vector ThemesGetBorderNames(); + +/** + * @brief Gets all available background names + * @return Vector of background names + */ +std::vector ThemesGetBackgroundNames(); + +/** + * @brief Gets all available decoration names + * @return Vector of decoration names + */ +std::vector ThemesGetDecorationNames(); + +/** + * @brief Gets a background by name + * @param name The background name + * @return The background data + */ +BackGroundData ThemesGetBackground(const std::string& name); \ No newline at end of file From f9efd40a0120ea7f22b4119e16e48c9a9260d49a Mon Sep 17 00:00:00 2001 From: Poul Sander Date: Fri, 22 May 2026 17:58:34 +0200 Subject: [PATCH 2/7] Align the drawing slightly --- source/code/theme_editor/ThemeEditorState.cpp | 29 +++++++++++-------- 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/source/code/theme_editor/ThemeEditorState.cpp b/source/code/theme_editor/ThemeEditorState.cpp index c2a4e473..0037086b 100644 --- a/source/code/theme_editor/ThemeEditorState.cpp +++ b/source/code/theme_editor/ThemeEditorState.cpp @@ -123,11 +123,16 @@ void ThemeEditorState::UpdatePreview() { } void ThemeEditorState::Draw(SDL_Renderer* target) { - (void)target; + // Get actual physical renderer size and keep logicalResize in sync every frame + int display_w = 1; + int display_h = 1; + SDL_GetRendererOutputSize(target, &display_w, &display_h); + globalData.logicalResize.SetPhysicalSize(display_w, display_h); + // Main window - take up left 60% of screen to leave room for preview - const float editor_width = globalData.xsize * 0.6f; + const float editor_width = display_w * 0.5f; ImGui::SetNextWindowPos(ImVec2(0, 0), ImGuiCond_Always); - ImGui::SetNextWindowSize(ImVec2(editor_width, (float)globalData.ysize), ImGuiCond_Always); + ImGui::SetNextWindowSize(ImVec2(editor_width, (float)display_h), ImGuiCond_Always); ImGui::Begin("Theme Editor", nullptr, ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoCollapse); @@ -158,16 +163,14 @@ void ThemeEditorState::Draw(SDL_Renderer* target) { ImGui::End(); - // Draw preview label in the right portion of screen - ImGui::SetNextWindowPos(ImVec2(editor_width + 10, 10), ImGuiCond_Always); - ImGui::SetNextWindowSize(ImVec2(globalData.xsize - editor_width - 20, 80), ImGuiCond_Always); - ImGui::Begin("Preview", nullptr, ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoCollapse); - ImGui::Text("Live Preview:"); - ImGui::TextWrapped("Changes are shown in real-time below"); - ImGui::End(); - - // Draw the preview + // Draw the preview: convert physical position (ImGui space) to logical space for DoPaintJob if (preview_game) { + const int preview_physical_x = static_cast(editor_width) + 250; + const int preview_physical_y = 150; + int preview_logical_x = 0; + int preview_logical_y = 0; + globalData.logicalResize.PhysicalToLogical(preview_physical_x, preview_physical_y, preview_logical_x, preview_logical_y); + preview_game->SetTopXY(preview_logical_x, preview_logical_y); preview_game->DoPaintJob(); } } @@ -546,6 +549,7 @@ void ThemeEditorState::LoadBorder(const std::string& name) { chain_offset_y = current_border.chain_label_offset.second; speed_offset_x = current_border.speed_label_offset.first; speed_offset_y = current_border.speed_label_offset.second; + preview_needs_update = true; } void ThemeEditorState::LoadBackground(const std::string& name) { @@ -566,6 +570,7 @@ void ThemeEditorState::LoadBackground(const std::string& name) { tile_move_speed_x = current_background.tileMoveSpeedX; tile_move_speed_y = current_background.tileMoveSpeedY; + preview_needs_update = true; } bool ThemeEditorState::ValidateSprite(const std::string& sprite_name) { From 421480de0608ae8f16a1b2db66bdda958990786f Mon Sep 17 00:00:00 2001 From: Poul Sander Date: Sun, 24 May 2026 23:20:11 +0200 Subject: [PATCH 3/7] Add field_offset along with label_offset --- source/code/BlockGameSdl.hpp | 8 ++--- source/code/theme_editor/ThemeEditorState.cpp | 36 +++++++++++++++++++ source/code/theme_editor/ThemeEditorState.hpp | 8 +++++ source/code/themes.cpp | 26 +++++++++++++- source/code/themes.hpp | 4 +++ 5 files changed, 77 insertions(+), 5 deletions(-) diff --git a/source/code/BlockGameSdl.hpp b/source/code/BlockGameSdl.hpp index f93c3da8..67e105e7 100644 --- a/source/code/BlockGameSdl.hpp +++ b/source/code/BlockGameSdl.hpp @@ -519,7 +519,7 @@ class BlockGameSdl : public BlockGame { std::string strHolder; strHolder = std::to_string(this->GetScore()+this->GetHandicap()); player_score.SetText(strHolder); - player_score.Draw(globalData.screen, this->GetTopX()+border.score_label_offset.first,this->GetTopY()+border.score_label_offset.second+22, sago::SagoTextField::Alignment::left, sago::SagoTextField::VerticalAlignment::top, &globalData.logicalResize); + player_score.Draw(globalData.screen, this->GetTopX()+border.score_field_offset.first,this->GetTopY()+border.score_field_offset.second, sago::SagoTextField::Alignment::left, sago::SagoTextField::VerticalAlignment::top, &globalData.logicalResize); if (this->GetAIenabled()) { player_name.SetText(_("AI")); } @@ -570,14 +570,14 @@ class BlockGameSdl : public BlockGame { } player_time.SetText(strHolder); } - player_time.Draw(globalData.screen, this->GetTopX()+border.time_label_offset.first,this->GetTopY()+border.time_label_offset.second+22, sago::SagoTextField::Alignment::left, sago::SagoTextField::VerticalAlignment::top, &globalData.logicalResize); + player_time.Draw(globalData.screen, this->GetTopX()+border.time_field_offset.first,this->GetTopY()+border.time_field_offset.second, sago::SagoTextField::Alignment::left, sago::SagoTextField::VerticalAlignment::top, &globalData.logicalResize); strHolder = std::to_string(this->GetChains()); player_chain.SetText(strHolder); - player_chain.Draw(globalData.screen, this->GetTopX()+border.chain_label_offset.first,this->GetTopY()+border.chain_label_offset.second+22, sago::SagoTextField::Alignment::left, sago::SagoTextField::VerticalAlignment::top, &globalData.logicalResize); + player_chain.Draw(globalData.screen, this->GetTopX()+border.chain_field_offset.first,this->GetTopY()+border.chain_field_offset.second, sago::SagoTextField::Alignment::left, sago::SagoTextField::VerticalAlignment::top, &globalData.logicalResize); //drawspeedLevel: strHolder = std::to_string(this->GetSpeedLevel()); player_speed.SetText(strHolder); - player_speed.Draw(globalData.screen, this->GetTopX()+border.speed_label_offset.first,this->GetTopY()+border.speed_label_offset.second+22, sago::SagoTextField::Alignment::left, sago::SagoTextField::VerticalAlignment::top, &globalData.logicalResize); + player_speed.Draw(globalData.screen, this->GetTopX()+border.speed_field_offset.first,this->GetTopY()+border.speed_field_offset.second, sago::SagoTextField::Alignment::left, sago::SagoTextField::VerticalAlignment::top, &globalData.logicalResize); if ((this->isStageClear()) &&(this->GetTopY()+700+50*(this->GetStageClearLimit()-this->GetLinesCleared())-this->GetPixels()-1<600+this->GetTopY())) { oldBubleX = this->GetTopX()+280; oldBubleY = this->GetTopY()+650+50*(this->GetStageClearLimit()-this->GetLinesCleared())-this->GetPixels()-1; diff --git a/source/code/theme_editor/ThemeEditorState.cpp b/source/code/theme_editor/ThemeEditorState.cpp index 0037086b..e4541ce7 100644 --- a/source/code/theme_editor/ThemeEditorState.cpp +++ b/source/code/theme_editor/ThemeEditorState.cpp @@ -41,6 +41,10 @@ ThemeEditorState::ThemeEditorState() { current_border.time_label_offset = {310, 129}; current_border.chain_label_offset = {310, 178}; current_border.speed_label_offset = {310, 227}; + current_border.score_field_offset = {310, 102}; + current_border.time_field_offset = {310, 151}; + current_border.chain_field_offset = {310, 200}; + current_border.speed_field_offset = {310, 249}; current_background.name = "standard"; current_background.background_sprite = "background"; @@ -254,6 +258,26 @@ void ThemeEditorState::DrawBorderEditor() { preview_needs_update = true; } + // Field offsets + ImGui::Separator(); + ImGui::Text("Field Positions:"); + if (ImGui::DragInt2("Score Field Offset", &score_field_x)) { + current_border.score_field_offset = {score_field_x, score_field_y}; + preview_needs_update = true; + } + if (ImGui::DragInt2("Time Field Offset", &time_field_x)) { + current_border.time_field_offset = {time_field_x, time_field_y}; + preview_needs_update = true; + } + if (ImGui::DragInt2("Chain Field Offset", &chain_field_x)) { + current_border.chain_field_offset = {chain_field_x, chain_field_y}; + preview_needs_update = true; + } + if (ImGui::DragInt2("Speed Field Offset", &speed_field_x)) { + current_border.speed_field_offset = {speed_field_x, speed_field_y}; + preview_needs_update = true; + } + ImGui::Separator(); if (ImGui::Button("Save Border")) { SaveCurrentBorder(); @@ -455,6 +479,10 @@ void ThemeEditorState::SaveCurrentBorder() { current_border.time_label_offset = {time_offset_x, time_offset_y}; current_border.chain_label_offset = {chain_offset_x, chain_offset_y}; current_border.speed_label_offset = {speed_offset_x, speed_offset_y}; + current_border.score_field_offset = {score_field_x, score_field_y}; + current_border.time_field_offset = {time_field_x, time_field_y}; + current_border.chain_field_offset = {chain_field_x, chain_field_y}; + current_border.speed_field_offset = {speed_field_x, speed_field_y}; // Validate sprite if (!ValidateSprite(current_border.border_sprite)) { @@ -549,6 +577,14 @@ void ThemeEditorState::LoadBorder(const std::string& name) { chain_offset_y = current_border.chain_label_offset.second; speed_offset_x = current_border.speed_label_offset.first; speed_offset_y = current_border.speed_label_offset.second; + score_field_x = current_border.score_field_offset.first; + score_field_y = current_border.score_field_offset.second; + time_field_x = current_border.time_field_offset.first; + time_field_y = current_border.time_field_offset.second; + chain_field_x = current_border.chain_field_offset.first; + chain_field_y = current_border.chain_field_offset.second; + speed_field_x = current_border.speed_field_offset.first; + speed_field_y = current_border.speed_field_offset.second; preview_needs_update = true; } diff --git a/source/code/theme_editor/ThemeEditorState.hpp b/source/code/theme_editor/ThemeEditorState.hpp index effdaecd..9152dda4 100644 --- a/source/code/theme_editor/ThemeEditorState.hpp +++ b/source/code/theme_editor/ThemeEditorState.hpp @@ -78,6 +78,14 @@ class ThemeEditorState : public sago::GameStateInterface { int chain_offset_y = 178; int speed_offset_x = 310; int speed_offset_y = 227; + int score_field_x = 310; + int score_field_y = 102; + int time_field_x = 310; + int time_field_y = 151; + int chain_field_x = 310; + int chain_field_y = 200; + int speed_field_x = 310; + int speed_field_y = 249; // Background editor state BackGroundData current_background; diff --git a/source/code/themes.cpp b/source/code/themes.cpp index 8010f224..cd367193 100644 --- a/source/code/themes.cpp +++ b/source/code/themes.cpp @@ -91,7 +91,11 @@ void to_json(json& j, const ThemeBorderData& p) { {"score_label_offset", {p.score_label_offset.first, p.score_label_offset.second}}, {"time_label_offset", {p.time_label_offset.first, p.time_label_offset.second}}, {"chain_label_offset", {p.chain_label_offset.first, p.chain_label_offset.second}}, - {"speed_label_offset", {p.speed_label_offset.first, p.speed_label_offset.second}} + {"speed_label_offset", {p.speed_label_offset.first, p.speed_label_offset.second}}, + {"score_field_offset", {p.score_field_offset.first, p.score_field_offset.second}}, + {"time_field_offset", {p.time_field_offset.first, p.time_field_offset.second}}, + {"chain_field_offset", {p.chain_field_offset.first, p.chain_field_offset.second}}, + {"speed_field_offset", {p.speed_field_offset.first, p.speed_field_offset.second}} }; } @@ -139,6 +143,22 @@ void from_json(const json& j, ThemeBorderData& p) { auto offset = j.at("speed_label_offset"); p.speed_label_offset = {offset[0], offset[1]}; } + if (j.contains("score_field_offset")) { + auto offset = j.at("score_field_offset"); + p.score_field_offset = {offset[0], offset[1]}; + } + if (j.contains("time_field_offset")) { + auto offset = j.at("time_field_offset"); + p.time_field_offset = {offset[0], offset[1]}; + } + if (j.contains("chain_field_offset")) { + auto offset = j.at("chain_field_offset"); + p.chain_field_offset = {offset[0], offset[1]}; + } + if (j.contains("speed_field_offset")) { + auto offset = j.at("speed_field_offset"); + p.speed_field_offset = {offset[0], offset[1]}; + } } void from_json(const json& j, Theme& p) { @@ -241,6 +261,10 @@ static void ThemesInitBorderData() { mirror.time_label_offset = {-100,129}; mirror.chain_label_offset = {-100, 178}; mirror.speed_label_offset = {-100, 227}; + mirror.score_field_offset = {-100, 102}; + mirror.time_field_offset = {-100, 151}; + mirror.chain_field_offset = {-100, 200}; + mirror.speed_field_offset = {-100, 249}; border_data[mirror.name] = mirror; } diff --git a/source/code/themes.hpp b/source/code/themes.hpp index 7b3747c0..f2601b73 100644 --- a/source/code/themes.hpp +++ b/source/code/themes.hpp @@ -51,6 +51,10 @@ struct ThemeBorderData { std::pair time_label_offset = {310,129}; std::pair chain_label_offset = {310, 178}; std::pair speed_label_offset = {310, 227}; + std::pair score_field_offset = {310, 102}; + std::pair time_field_offset = {310, 151}; + std::pair chain_field_offset = {310, 200}; + std::pair speed_field_offset = {310, 249}; }; From 2710f33d3499d917a45826e29964931fec12026b Mon Sep 17 00:00:00 2001 From: Poul Sander Date: Tue, 1 Sep 2026 18:44:57 +0200 Subject: [PATCH 4/7] Add option for overriding border_name per theme --- source/code/themes.cpp | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/source/code/themes.cpp b/source/code/themes.cpp index cd367193..2c532228 100644 --- a/source/code/themes.cpp +++ b/source/code/themes.cpp @@ -100,7 +100,7 @@ void to_json(json& j, const ThemeBorderData& p) { } void to_json(json& j, const Theme& p) { - j = json{ {"theme_name", p.theme_name}, {"back_board", p.back_board}, {"background_name", p.background.name}, {"decoration_name", p.decoration.name} }; + j = json{ {"theme_name", p.theme_name}, {"back_board", p.back_board}, {"background_name", p.background.name}, {"decoration_name", p.decoration.name}, {"border_name", p.border.name} }; } void to_json(json& j, const DecorationData& p) { @@ -166,6 +166,9 @@ void from_json(const json& j, Theme& p) { j.at("back_board").get_to(p.back_board); j.at("background_name").get_to(p.background.name); j.at("decoration_name").get_to(p.decoration.name); + if (j.contains("border_name")) { + j.at("border_name").get_to(p.border.name); + } } void from_json(const json& j, DecorationData& p) { @@ -204,6 +207,16 @@ void ThemesFillMissingFields(Theme& theme) { theme.background.name = "standard"; } theme.background = background_data[theme.background.name]; + if (border_data.find(theme.border.name) == border_data.end()) { + //If the theme does not define a border then use the standard. + theme.border.name = "standard"; + } + theme.border = border_data[theme.border.name]; + if (decoration_data.find(theme.decoration.name) == decoration_data.end()) { + //If the theme does not define a decoration then use the standard smilies. + theme.decoration.name = "smilies"; + } + theme.decoration = decoration_data[theme.decoration.name]; } static void ThemesReadDataFromFile(const std::string& filename) { From a982e4c182a733389f3f67a58215e03f04d34318 Mon Sep 17 00:00:00 2001 From: Poul Sander Date: Tue, 1 Sep 2026 19:07:25 +0200 Subject: [PATCH 5/7] Add an option for going directly to the Themes screen --- CHANGELOG.md | 2 ++ man/blockattack.man | 8 +++++++- source/code/main.cpp | 8 ++++++++ source/code/mainVars.inc | 1 + 4 files changed, 18 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 577646ff..d7c33fb5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,8 @@ ## Unreleased ### Added + - `--theme-menu` starts the game directly on the theme selection screen (Configure -> Themes) + ### Removed ### Changed diff --git a/man/blockattack.man b/man/blockattack.man index 00426beb..13cfa89b 100644 --- a/man/blockattack.man +++ b/man/blockattack.man @@ -1,5 +1,5 @@ .\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.49.3. -.TH BLOCKATTACK "6" "October 2025" "blockattack 2.11.0-SNAPSHOT" "Games" +.TH BLOCKATTACK "6" "September 2026" "blockattack 2.11.0-SNAPSHOT" "Games" .SH NAME blockattack \- a puzzle game inspired by Tetris Attack .SH SYNOPSIS @@ -52,6 +52,12 @@ Start the sprite editor/browser \fB\-\-puzzle\-editor\fR Start the build in puzzle editor .TP +\fB\-\-theme\-editor\fR +Start the theme editor +.TP +\fB\-\-theme\-menu\fR +Go directly to the theme selection screen +.TP \fB\-\-puzzle\-level\-file\fR arg Sets the default puzzle file to load .TP diff --git a/source/code/main.cpp b/source/code/main.cpp index 52e78f07..9f3f2d18 100644 --- a/source/code/main.cpp +++ b/source/code/main.cpp @@ -71,6 +71,7 @@ Source information and contacts persons can be found at #include "editor/SagoTextureSelector.hpp" #include "puzzle_editor/PuzzleEditorState.hpp" #include "theme_editor/ThemeEditorState.hpp" +#include "menudef_themes.hpp" #include "SagoImGui.hpp" /******************************************************************************* @@ -932,6 +933,7 @@ static void ParseArguments(int argc, char* argv[], globalConfig& conf) { ("editor", "Start the sprite editor/browser") ("puzzle-editor", "Start the build in puzzle editor") ("theme-editor", "Start the theme editor") + ("theme-menu", "Go directly to the theme selection screen") ("puzzle-level-file", boost::program_options::value(), "Sets the default puzzle file to load") ("puzzle-single-level", boost::program_options::value(), "Start the specific puzzle level directly") #ifdef REPLAY_IMPLEMENTED @@ -1036,6 +1038,9 @@ static void ParseArguments(int argc, char* argv[], globalConfig& conf) { if (vm.count("theme-editor")) { themeEditor = true; } + if (vm.count("theme-menu")) { + themeMenu = true; + } if (vm.count("puzzle-level-file")) { conf.puzzleName = vm["puzzle-level-file"].as(); } @@ -1359,6 +1364,9 @@ int main(int argc, char* argv[]) { if (!gameShutdownProperly) { SafeModeMenu(); } + if (themeMenu) { + OpenThemesMenu(); + } //game loop MainMenu(); } diff --git a/source/code/mainVars.inc b/source/code/mainVars.inc index 34f768e8..a1aeb965 100644 --- a/source/code/mainVars.inc +++ b/source/code/mainVars.inc @@ -93,6 +93,7 @@ static bool singlePuzzle = false; static bool editor = false; static bool puzzleEditor = false; static bool themeEditor = false; +static bool themeMenu = false; static int singlePuzzleNr = 0; static std::string singlePuzzleFile; From 31888be439e660459c33ec9eb0ab2c79291c5bf0 Mon Sep 17 00:00:00 2001 From: Poul Sander Date: Tue, 1 Sep 2026 19:32:49 +0200 Subject: [PATCH 6/7] Also add the option for garbage_data per theme --- source/code/BlockGame.cpp | 3 +- source/code/BlockGameSdl.hpp | 32 ++++++++-------- source/code/global.hpp | 15 -------- source/code/main.cpp | 15 -------- source/code/menudef_themes.cpp | 9 +++++ source/code/themes.cpp | 69 +++++++++++++++++++++++++++++++++- source/code/themes.hpp | 23 ++++++++++++ 7 files changed, 118 insertions(+), 48 deletions(-) diff --git a/source/code/BlockGame.cpp b/source/code/BlockGame.cpp index 87c173ab..4e22dfae 100644 --- a/source/code/BlockGame.cpp +++ b/source/code/BlockGame.cpp @@ -519,9 +519,10 @@ void BlockGame::putSampleBlocks() { for (size_t j = 6; j < 9; j++) { board[i][j] = GARBAGE+1; } - for (size_t j = 9; j < 12; j++) { + for (size_t j = 9; j < 11; j++) { board[i][j] = GARBAGE+2; } + board[i][11] = GARBAGE_GRAY*GARBAGE; } } diff --git a/source/code/BlockGameSdl.hpp b/source/code/BlockGameSdl.hpp index 67e105e7..795a1a64 100644 --- a/source/code/BlockGameSdl.hpp +++ b/source/code/BlockGameSdl.hpp @@ -238,6 +238,8 @@ class BlockGameSdl : public BlockGame { //Draws all the bricks to the board (including garbage) void PaintBricks() const { + const GarbageData& garbageSet = globalData.theme.garbage; + const sago::SagoSpriteHolder& sprites = *globalData.spriteHolder; for (int i=0; i<13; ++i) { for (int j=0; j<6; ++j) { int basicBrick = board[j][i]%10; //The basic brick, stored on the least significant digit @@ -279,51 +281,51 @@ class BlockGameSdl : public BlockGame { under = board[j][i-1]; } if ((left == number)&&(right == number)&&(over == number)&&(under == number)) { - DrawImgBoardBounded(globalData.garbageFill, j*bsize, bsize*12-i*bsize-pixels); + DrawImgBoardBounded(sprites.GetSprite(garbageSet.fill), j*bsize, bsize*12-i*bsize-pixels); } if ((left != number)&&(right == number)&&(over == number)&&(under == number)) { - DrawImgBoardBounded(globalData.garbageL, j*bsize, bsize*12-i*bsize-pixels); + DrawImgBoardBounded(sprites.GetSprite(garbageSet.l), j*bsize, bsize*12-i*bsize-pixels); } if ((left == number)&&(right != number)&&(over == number)&&(under == number)) { - DrawImgBoardBounded(globalData.garbageR, j*bsize, bsize*12-i*bsize-pixels); + DrawImgBoardBounded(sprites.GetSprite(garbageSet.r), j*bsize, bsize*12-i*bsize-pixels); } if ((left == number)&&(right == number)&&(over != number)&&(under == number)) { - DrawImgBoardBounded(globalData.garbageT, j*bsize, bsize*12-i*bsize-pixels); + DrawImgBoardBounded(sprites.GetSprite(garbageSet.t), j*bsize, bsize*12-i*bsize-pixels); } if ((left == number)&&(right == number)&&(over == number)&&(under != number)) { - DrawImgBoardBounded(globalData.garbageB, j*bsize, bsize*12-i*bsize-pixels); + DrawImgBoardBounded(sprites.GetSprite(garbageSet.b), j*bsize, bsize*12-i*bsize-pixels); } if ((left != number)&&(right == number)&&(over != number)&&(under == number)) { - DrawImgBoardBounded(globalData.garbageTL, j*bsize, bsize*12-i*bsize-pixels); + DrawImgBoardBounded(sprites.GetSprite(garbageSet.tl), j*bsize, bsize*12-i*bsize-pixels); } if ((left != number)&&(right == number)&&(over == number)&&(under != number)) { - DrawImgBoardBounded(globalData.garbageBL, j*bsize, bsize*12-i*bsize-pixels); + DrawImgBoardBounded(sprites.GetSprite(garbageSet.bl), j*bsize, bsize*12-i*bsize-pixels); } if ((left == number)&&(right != number)&&(over != number)&&(under == number)) { - DrawImgBoardBounded(globalData.garbageTR, j*bsize, bsize*12-i*bsize-pixels); + DrawImgBoardBounded(sprites.GetSprite(garbageSet.tr), j*bsize, bsize*12-i*bsize-pixels); } if ((left == number)&&(right != number)&&(over == number)&&(under != number)) { - DrawImgBoardBounded(globalData.garbageBR, j*bsize, bsize*12-i*bsize-pixels); + DrawImgBoardBounded(sprites.GetSprite(garbageSet.br), j*bsize, bsize*12-i*bsize-pixels); } if ((left == number)&&(right != number)&&(over != number)&&(under != number)) { - DrawImgBoardBounded(globalData.garbageMR, j*bsize, bsize*12-i*bsize-pixels); + DrawImgBoardBounded(sprites.GetSprite(garbageSet.mr), j*bsize, bsize*12-i*bsize-pixels); } if ((left == number)&&(right == number)&&(over != number)&&(under != number)) { - DrawImgBoardBounded(globalData.garbageM, j*bsize, bsize*12-i*bsize-pixels); + DrawImgBoardBounded(sprites.GetSprite(garbageSet.m), j*bsize, bsize*12-i*bsize-pixels); } if ((left != number)&&(right == number)&&(over != number)&&(under != number)) { - DrawImgBoardBounded(globalData.garbageML, j*bsize, bsize*12-i*bsize-pixels); + DrawImgBoardBounded(sprites.GetSprite(garbageSet.ml), j*bsize, bsize*12-i*bsize-pixels); } } if ((board[j][i]/1000000)%10==2) { if (j==0) { - DrawImgBoardBounded(globalData.garbageGML, j*bsize, bsize*12-i*bsize-pixels); + DrawImgBoardBounded(sprites.GetSprite(garbageSet.gml), j*bsize, bsize*12-i*bsize-pixels); } else if (j==5) { - DrawImgBoardBounded(globalData.garbageGMR, j*bsize, bsize*12-i*bsize-pixels); + DrawImgBoardBounded(sprites.GetSprite(garbageSet.gmr), j*bsize, bsize*12-i*bsize-pixels); } else { - DrawImgBoardBounded(globalData.garbageGM, j*bsize, bsize*12-i*bsize-pixels); + DrawImgBoardBounded(sprites.GetSprite(garbageSet.gm), j*bsize, bsize*12-i*bsize-pixels); } } } diff --git a/source/code/global.hpp b/source/code/global.hpp index 0c07caf8..cc41b963 100644 --- a/source/code/global.hpp +++ b/source/code/global.hpp @@ -124,21 +124,6 @@ struct GlobalData { sago::SoundHandler counterChunk; //When counting down sago::SoundHandler counterFinalChunk; - sago::SagoSprite garbageTL; //the Garbage Blocks - sago::SagoSprite garbageT; - sago::SagoSprite garbageTR; - sago::SagoSprite garbageR; - sago::SagoSprite garbageBR; - sago::SagoSprite garbageB; - sago::SagoSprite garbageBL; - sago::SagoSprite garbageL; - sago::SagoSprite garbageFill; - sago::SagoSprite garbageM; - sago::SagoSprite garbageML; - sago::SagoSprite garbageMR; - sago::SagoSprite garbageGM; - sago::SagoSprite garbageGML; - sago::SagoSprite garbageGMR; sago::SagoSprite transCover; //The transperant block, covers the upcomming sago::SagoSprite bSkip; sago::SagoSprite bRetry; diff --git a/source/code/main.cpp b/source/code/main.cpp index 9f3f2d18..e011fb17 100644 --- a/source/code/main.cpp +++ b/source/code/main.cpp @@ -165,21 +165,6 @@ static int InitImages(sago::SagoSpriteHolder& holder) { globalData.iLevelCheckBoxMarked = holder.GetSprite("i_level_check_box_marked"); globalData.iCheckBoxArea = holder.GetSprite("i_check_box_area"); globalData.boardBackBack = holder.GetSprite("board_back_back"); - globalData.garbageTL = holder.GetSprite("garbage_tl"); - globalData.garbageT = holder.GetSprite("garbage_t"); - globalData.garbageTR = holder.GetSprite("garbage_tr"); - globalData.garbageR = holder.GetSprite("garbage_r"); - globalData.garbageBR = holder.GetSprite("garbage_br"); - globalData.garbageB = holder.GetSprite("garbage_b"); - globalData.garbageBL = holder.GetSprite("garbage_bl"); - globalData.garbageL = holder.GetSprite("garbage_l"); - globalData.garbageFill = holder.GetSprite("garbage_fill"); - globalData.garbageML = holder.GetSprite("garbage_ml"); - globalData.garbageM = holder.GetSprite("garbage_m"); - globalData.garbageMR = holder.GetSprite("garbage_mr"); - globalData.garbageGM = holder.GetSprite("garbage_gm"); - globalData.garbageGML = holder.GetSprite("garbage_gml"); - globalData.garbageGMR = holder.GetSprite("garbage_gmr"); globalData.transCover = holder.GetSprite("trans_cover"); bExit = holder.GetSprite("b_exit"); globalData.bSkip = holder.GetSprite("b_blank"); diff --git a/source/code/menudef_themes.cpp b/source/code/menudef_themes.cpp index 7678ca4a..8537c98e 100644 --- a/source/code/menudef_themes.cpp +++ b/source/code/menudef_themes.cpp @@ -96,6 +96,11 @@ static void themesEditSwitchBorder() { globalData.theme = themeToEdit; } +static void themesEditSwitchGarbage() { + themeToEdit.garbage = ThemesGetNextGarbage(themeToEdit.garbage.name); + globalData.theme = themeToEdit; +} + static void themesEditSlot(int slot) { ThemesMenu tem(globalData.screen, fmt::format(_("Edit custom theme {}"), slot), true); size_t theme_index = ThemesGetNumber(fmt::format("custom_slot_{}", slot)); @@ -115,6 +120,10 @@ static void themesEditSlot(int slot) { bSwitchBorder.setLabel(_("Switch border")); bSwitchBorder.setAction(&themesEditSwitchBorder); tem.addButton(&bSwitchBorder); + Button bSwitchGarbage; + bSwitchGarbage.setLabel(_("Switch garbage")); + bSwitchGarbage.setAction(&themesEditSwitchGarbage); + tem.addButton(&bSwitchGarbage); themeBackup = globalData.theme; themeToEdit = ThemesGet(theme_index); globalData.theme = themeToEdit; diff --git a/source/code/themes.cpp b/source/code/themes.cpp index 2c532228..e7800fa5 100644 --- a/source/code/themes.cpp +++ b/source/code/themes.cpp @@ -100,15 +100,26 @@ void to_json(json& j, const ThemeBorderData& p) { } void to_json(json& j, const Theme& p) { - j = json{ {"theme_name", p.theme_name}, {"back_board", p.back_board}, {"background_name", p.background.name}, {"decoration_name", p.decoration.name}, {"border_name", p.border.name} }; + j = json{ {"theme_name", p.theme_name}, {"back_board", p.back_board}, {"background_name", p.background.name}, {"decoration_name", p.decoration.name}, {"border_name", p.border.name}, {"garbage_name", p.garbage.name} }; } void to_json(json& j, const DecorationData& p) { j = json{ {"name", p.name}, {"decoration_sprites", p.decoration_sprites} }; } +void to_json(json& j, const GarbageData& p) { + j = json{ + {"garbage_name", p.name}, + {"tl", p.tl}, {"t", p.t}, {"tr", p.tr}, + {"l", p.l}, {"fill", p.fill}, {"r", p.r}, + {"bl", p.bl}, {"b", p.b}, {"br", p.br}, + {"ml", p.ml}, {"m", p.m}, {"mr", p.mr}, + {"gml", p.gml}, {"gm", p.gm}, {"gmr", p.gmr} + }; +} + void to_json(json& j, const ThemeFileData& p) { - j = json{ {"background_data", p.background_data}, {"decoration_data", p.decoration_data}, {"border_data", p.border_data}, {"themes", p.themes} }; + j = json{ {"background_data", p.background_data}, {"decoration_data", p.decoration_data}, {"border_data", p.border_data}, {"garbage_data", p.garbage_data}, {"themes", p.themes} }; } void from_json(const json& j, BackGroundData& p) { @@ -169,6 +180,9 @@ void from_json(const json& j, Theme& p) { if (j.contains("border_name")) { j.at("border_name").get_to(p.border.name); } + if (j.contains("garbage_name")) { + j.at("garbage_name").get_to(p.garbage.name); + } } void from_json(const json& j, DecorationData& p) { @@ -176,6 +190,22 @@ void from_json(const json& j, DecorationData& p) { j.at("decoration_sprites").get_to(p.decoration_sprites); } +void from_json(const json& j, GarbageData& p) { + j.at("garbage_name").get_to(p.name); + const std::pair sprites[] = { + {"tl", &p.tl}, {"t", &p.t}, {"tr", &p.tr}, + {"l", &p.l}, {"fill", &p.fill}, {"r", &p.r}, + {"bl", &p.bl}, {"b", &p.b}, {"br", &p.br}, + {"ml", &p.ml}, {"m", &p.m}, {"mr", &p.mr}, + {"gml", &p.gml}, {"gm", &p.gm}, {"gmr", &p.gmr} + }; + for (const auto& sprite : sprites) { + if (j.contains(sprite.first)) { + j.at(sprite.first).get_to(*sprite.second); + } + } +} + void from_json(const json& j, ThemeFileData& p) { if (j.contains("background_data")) { j.at("background_data").get_to(p.background_data); @@ -186,6 +216,9 @@ void from_json(const json& j, ThemeFileData& p) { if (j.contains("border_data")) { j.at("border_data").get_to(p.border_data); } + if (j.contains("garbage_data")) { + j.at("garbage_data").get_to(p.garbage_data); + } if (j.contains("themes")) { j.at("themes").get_to(p.themes); } @@ -197,6 +230,7 @@ static std::vector themes(1); static std::map background_data; static std::map decoration_data; static std::map border_data; +static std::map garbage_data; static bool initialized = false; static size_t current_theme = 0; @@ -217,6 +251,11 @@ void ThemesFillMissingFields(Theme& theme) { theme.decoration.name = "smilies"; } theme.decoration = decoration_data[theme.decoration.name]; + if (garbage_data.find(theme.garbage.name) == garbage_data.end()) { + //If the theme does not define a garbage set then use the standard. + theme.garbage.name = "standard"; + } + theme.garbage = garbage_data[theme.garbage.name]; } static void ThemesReadDataFromFile(const std::string& filename) { @@ -239,6 +278,9 @@ static void ThemesReadDataFromFile(const std::string& filename) { for (const auto& border : tfd.border_data) { border_data[border.name] = border; } + for (const auto& garbage : tfd.garbage_data) { + garbage_data[garbage.name] = garbage; + } for (const Theme& theme : tfd.themes) { if (globalData.verboseLevel) { std::cout << "Adding theme " << theme.theme_name << "\n"; @@ -281,6 +323,11 @@ static void ThemesInitBorderData() { border_data[mirror.name] = mirror; } +static void ThemesInitGarbageData() { + GarbageData standard; + garbage_data[standard.name] = standard; +} + static bool str_startswith(const std::string& s, const char* prefix) { return (s.rfind(prefix, 0) == 0); } @@ -297,6 +344,9 @@ static void ThemesDumpData() { for (auto& pair : border_data) { tfd.border_data.push_back(pair.second); } + for (auto& pair : garbage_data) { + tfd.garbage_data.push_back(pair.second); + } json j = tfd; std::string s = j.dump(4); sago::WriteFileContent("themes_dump.json", s); @@ -336,6 +386,7 @@ void ThemesInit() { ThemesInitBackGroundData(); ThemesInitCustomBackgrounds(); ThemesInitBorderData(); + ThemesInitGarbageData(); ThemesLoadCustomBorders(); ThemesLoadCustomBackgrounds(); themes.resize(1); //Add the default theme @@ -448,6 +499,20 @@ ThemeBorderData ThemesGetNextBorder(const std::string& current) { return ret; } +GarbageData ThemesGetNextGarbage(const std::string& current) { + ThemesInit(); + GarbageData ret = garbage_data["standard"]; + auto it = garbage_data.find(current); + if (it != garbage_data.end()) { + ++it; + if (it == garbage_data.end()) { + it = garbage_data.begin(); + } + ret = it->second; + } + return ret; +} + void ThemesInitCustomBackgrounds() { std::vector custom_backgrounds = sago::GetFileList("textures/backgrounds"); std::stringstream sprite_stream; diff --git a/source/code/themes.hpp b/source/code/themes.hpp index f2601b73..cd9d7ffd 100644 --- a/source/code/themes.hpp +++ b/source/code/themes.hpp @@ -63,18 +63,39 @@ struct DecorationData { std::vector decoration_sprites = {"smileys0", "smileys1", "smileys2", "smileys3"}; }; +struct GarbageData { + std::string name = "standard"; + std::string tl = "garbage_tl"; + std::string t = "garbage_t"; + std::string tr = "garbage_tr"; + std::string l = "garbage_l"; + std::string fill = "garbage_fill"; + std::string r = "garbage_r"; + std::string bl = "garbage_bl"; + std::string b = "garbage_b"; + std::string br = "garbage_br"; + std::string ml = "garbage_ml"; + std::string m = "garbage_m"; + std::string mr = "garbage_mr"; + std::string gml = "garbage_gml"; + std::string gm = "garbage_gm"; + std::string gmr = "garbage_gmr"; +}; + struct Theme { std::string theme_name = "standard"; std::string back_board = "back_board"; // Can also be "back_board_sample_snow" or "trans_cover" BackGroundData background; //Serialized as background.background_name DecorationData decoration; ThemeBorderData border; + GarbageData garbage; }; struct ThemeFileData { std::vector background_data; std::vector decoration_data; std::vector border_data; + std::vector garbage_data; std::vector themes; }; @@ -123,6 +144,8 @@ ThemeBorderData ThemesGetBorder(const std::string& name); ThemeBorderData ThemesGetNextBorder(const std::string& current); +GarbageData ThemesGetNextGarbage(const std::string& current); + void ThemesInitCustomBackgrounds(); /** From fcbafb8bad69de9a644700ba89a57940f7baf31a Mon Sep 17 00:00:00 2001 From: Poul Sander Date: Wed, 11 Nov 2026 21:20:41 +0100 Subject: [PATCH 7/7] Remove the theme editor. Nobody is going to adjust themes by hand in the future --- man/blockattack.man | 3 - source/code/main.cpp | 16 - source/code/mainVars.inc | 1 - source/code/theme_editor/ThemeEditorState.cpp | 619 ------------------ source/code/theme_editor/ThemeEditorState.hpp | 116 ---- source/code/themes.cpp | 76 --- source/code/themes.hpp | 48 +- 7 files changed, 1 insertion(+), 878 deletions(-) delete mode 100644 source/code/theme_editor/ThemeEditorState.cpp delete mode 100644 source/code/theme_editor/ThemeEditorState.hpp diff --git a/man/blockattack.man b/man/blockattack.man index 13cfa89b..258075f4 100644 --- a/man/blockattack.man +++ b/man/blockattack.man @@ -52,9 +52,6 @@ Start the sprite editor/browser \fB\-\-puzzle\-editor\fR Start the build in puzzle editor .TP -\fB\-\-theme\-editor\fR -Start the theme editor -.TP \fB\-\-theme\-menu\fR Go directly to the theme selection screen .TP diff --git a/source/code/main.cpp b/source/code/main.cpp index e011fb17..82710af1 100644 --- a/source/code/main.cpp +++ b/source/code/main.cpp @@ -70,7 +70,6 @@ Source information and contacts persons can be found at #include "editor/SagoTextureSelector.hpp" #include "puzzle_editor/PuzzleEditorState.hpp" -#include "theme_editor/ThemeEditorState.hpp" #include "menudef_themes.hpp" #include "SagoImGui.hpp" @@ -917,7 +916,6 @@ static void ParseArguments(int argc, char* argv[], globalConfig& conf) { ("always-sixteen-nine", "Use 16:9 format even in Window mode") ("editor", "Start the sprite editor/browser") ("puzzle-editor", "Start the build in puzzle editor") - ("theme-editor", "Start the theme editor") ("theme-menu", "Go directly to the theme selection screen") ("puzzle-level-file", boost::program_options::value(), "Sets the default puzzle file to load") ("puzzle-single-level", boost::program_options::value(), "Start the specific puzzle level directly") @@ -1020,9 +1018,6 @@ static void ParseArguments(int argc, char* argv[], globalConfig& conf) { if (vm.count("puzzle-editor")) { puzzleEditor = true; } - if (vm.count("theme-editor")) { - themeEditor = true; - } if (vm.count("theme-menu")) { themeMenu = true; } @@ -1330,17 +1325,6 @@ int main(int argc, char* argv[]) { RunImGuiGameState(s); ImGui::SaveIniSettingsToDisk(imgui_inifile.c_str()); } - else if (themeEditor) { - InitImGui(sdlWindow, renderer, globalData.xsize, globalData.ysize); - ImGuiIO& io = ImGui::GetIO(); - io.IniFilename = nullptr; - std::string imgui_inifile = getPathToSaveFiles() + "/imgui_theme_editor.ini"; - ImGui::LoadIniSettingsFromDisk(imgui_inifile.c_str()); - ThemeEditorState s; - s.Init(); - RunImGuiGameState(s); - ImGui::SaveIniSettingsToDisk(imgui_inifile.c_str()); - } else if (globalData.replayArgument.length()) { ReplayPlayer rp; RunGameState(rp); diff --git a/source/code/mainVars.inc b/source/code/mainVars.inc index a1aeb965..ce394d73 100644 --- a/source/code/mainVars.inc +++ b/source/code/mainVars.inc @@ -92,7 +92,6 @@ bool twoPlayers; //True if two playerImGui_ImplSDL2_ProcessEvent(&event);s ar static bool singlePuzzle = false; static bool editor = false; static bool puzzleEditor = false; -static bool themeEditor = false; static bool themeMenu = false; static int singlePuzzleNr = 0; static std::string singlePuzzleFile; diff --git a/source/code/theme_editor/ThemeEditorState.cpp b/source/code/theme_editor/ThemeEditorState.cpp deleted file mode 100644 index e4541ce7..00000000 --- a/source/code/theme_editor/ThemeEditorState.cpp +++ /dev/null @@ -1,619 +0,0 @@ -/* -=========================================================================== -blockattack - Block Attack - Rise of the Blocks -Copyright (C) 2005-2026 Poul Sander - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 2 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see http://www.gnu.org/licenses/ - -Source information and contacts persons can be found at -https://blockattack.net -=========================================================================== -*/ - -#include "ThemeEditorState.hpp" - -#include -#include - -#include "imgui.h" -#include "imgui_internal.h" -#include "backends/imgui_impl_sdl2.h" -#include "backends/imgui_impl_sdlrenderer2.h" -#include "../sago/SagoMisc.hpp" -#include "../BlockGameSdl.hpp" - -ThemeEditorState::ThemeEditorState() { - current_border.name = "standard"; - current_border.border_sprite = "board_back_back"; - current_border.border_sprite_offset = {-60, -68}; - current_border.score_label_offset = {310, 80}; - current_border.time_label_offset = {310, 129}; - current_border.chain_label_offset = {310, 178}; - current_border.speed_label_offset = {310, 227}; - current_border.score_field_offset = {310, 102}; - current_border.time_field_offset = {310, 151}; - current_border.chain_field_offset = {310, 200}; - current_border.speed_field_offset = {310, 249}; - - current_background.name = "standard"; - current_background.background_sprite = "background"; - current_background.background_sprite_16x9 = "background_sixteen_nine"; - current_background.background_scale = ImgScale::Stretch; -} - -ThemeEditorState::~ThemeEditorState() { - // Restore the original theme when editor exits - globalData.theme = saved_theme; -} - -bool ThemeEditorState::IsActive() { - return isActive; -} - -void ThemeEditorState::ProcessInput(const SDL_Event& event, bool& processed) { - (void)event; - (void)processed; - // Handle any custom input processing here if needed -} - -void ThemeEditorState::Init() { - // Initialize the theme system - ThemesInit(); - - // Populate lists - border_names = ThemesGetBorderNames(); - background_names = ThemesGetBackgroundNames(); - decoration_names = ThemesGetDecorationNames(); - - // Save current theme and set up preview - saved_theme = globalData.theme; - preview_theme = ThemesGet(0); - - // Create preview game instance positioned in the right 40% of screen - // Editor takes 60% of width, so preview starts at 60% + some padding - int preview_x = static_cast(globalData.xsize * 0.62f); - preview_game = std::make_shared(preview_x, 100, &globalData.spriteHolder->GetDataHolder()); - preview_game->name = "Preview"; - preview_game->putSampleBlocks(); - - // Initialize preview with current border/background - preview_theme.border = current_border; - preview_theme.background = current_background; - globalData.theme = preview_theme; -} - -void ThemeEditorState::Update() { - // Update preview if needed - if (preview_needs_update) { - preview_needs_update = false; - UpdatePreview(); - } -} - -void ThemeEditorState::UpdatePreview() { - // Update preview theme based on current tab - switch (current_tab) { - case 0: // Border Editor - preview_theme.border = current_border; - break; - case 1: // Background Editor - preview_theme.background = current_background; - break; - case 2: // Theme Composer - // Combine selected components - if (static_cast(selected_border_index) < border_names.size()) { - preview_theme.border = ThemesGetBorder(border_names[selected_border_index]); - } - if (static_cast(selected_background_index) < background_names.size()) { - preview_theme.background = ThemesGetBackground(background_names[selected_background_index]); - } - preview_theme.back_board = board_backgrounds[selected_board_background]; - break; - } - - // Apply theme to global for preview rendering - globalData.theme = preview_theme; -} - -void ThemeEditorState::Draw(SDL_Renderer* target) { - // Get actual physical renderer size and keep logicalResize in sync every frame - int display_w = 1; - int display_h = 1; - SDL_GetRendererOutputSize(target, &display_w, &display_h); - globalData.logicalResize.SetPhysicalSize(display_w, display_h); - - // Main window - take up left 60% of screen to leave room for preview - const float editor_width = display_w * 0.5f; - ImGui::SetNextWindowPos(ImVec2(0, 0), ImGuiCond_Always); - ImGui::SetNextWindowSize(ImVec2(editor_width, (float)display_h), ImGuiCond_Always); - - ImGui::Begin("Theme Editor", nullptr, ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoCollapse); - - // Tab bar - if (ImGui::BeginTabBar("EditorTabs")) { - if (ImGui::BeginTabItem("Border Editor")) { - current_tab = 0; - DrawBorderEditor(); - ImGui::EndTabItem(); - } - if (ImGui::BeginTabItem("Background Editor")) { - current_tab = 1; - DrawBackgroundEditor(); - ImGui::EndTabItem(); - } - if (ImGui::BeginTabItem("Theme Composer")) { - current_tab = 2; - DrawThemeComposer(); - ImGui::EndTabItem(); - } - ImGui::EndTabBar(); - } - - ImGui::Separator(); - if (ImGui::Button("Exit Editor")) { - isActive = false; - } - - ImGui::End(); - - // Draw the preview: convert physical position (ImGui space) to logical space for DoPaintJob - if (preview_game) { - const int preview_physical_x = static_cast(editor_width) + 250; - const int preview_physical_y = 150; - int preview_logical_x = 0; - int preview_logical_y = 0; - globalData.logicalResize.PhysicalToLogical(preview_physical_x, preview_physical_y, preview_logical_x, preview_logical_y); - preview_game->SetTopXY(preview_logical_x, preview_logical_y); - preview_game->DoPaintJob(); - } -} - -void ThemeEditorState::DrawBorderEditor() { - ImGui::Text("Create and edit custom borders"); - ImGui::Separator(); - - // Load existing border - ImGui::Text("Load existing border:"); - if (ImGui::BeginCombo("##BorderLoad", border_names.empty() ? "No borders" : current_border.name.c_str())) { - for (const auto& name : border_names) { - bool is_selected = (current_border.name == name); - if (ImGui::Selectable(name.c_str(), is_selected)) { - LoadBorder(name); - } - if (is_selected) { - ImGui::SetItemDefaultFocus(); - } - } - ImGui::EndCombo(); - } - - ImGui::Separator(); - ImGui::Text("Border Settings:"); - - // Border name - ImGui::InputText("Border Name", border_name_buffer, sizeof(border_name_buffer)); - - // Border sprite - if (ImGui::InputText("Border Sprite", border_sprite_buffer, sizeof(border_sprite_buffer))) { - current_border.border_sprite = border_sprite_buffer; - preview_needs_update = true; - } - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Examples: board_back_back, board_back_back_mirror"); - } - - // Validate sprite button - ImGui::SameLine(); - if (ImGui::Button("Validate##border")) { - if (ValidateSprite(border_sprite_buffer)) { - ImGui::OpenPopup("Valid Sprite"); - } else { - ImGui::OpenPopup("Invalid Sprite"); - } - } - - if (ImGui::BeginPopup("Valid Sprite")) { - ImGui::Text("Sprite exists!"); - ImGui::EndPopup(); - } - if (ImGui::BeginPopup("Invalid Sprite")) { - ImGui::Text("Warning: Sprite does not exist!"); - ImGui::EndPopup(); - } - - // Border sprite offset - if (ImGui::DragInt2("Border Sprite Offset", &border_offset_x)) { - current_border.border_sprite_offset = {border_offset_x, border_offset_y}; - preview_needs_update = true; - } - - // Label offsets - ImGui::Separator(); - ImGui::Text("Label Positions:"); - if (ImGui::DragInt2("Score Label Offset", &score_offset_x)) { - current_border.score_label_offset = {score_offset_x, score_offset_y}; - preview_needs_update = true; - } - if (ImGui::DragInt2("Time Label Offset", &time_offset_x)) { - current_border.time_label_offset = {time_offset_x, time_offset_y}; - preview_needs_update = true; - } - if (ImGui::DragInt2("Chain Label Offset", &chain_offset_x)) { - current_border.chain_label_offset = {chain_offset_x, chain_offset_y}; - preview_needs_update = true; - } - if (ImGui::DragInt2("Speed Label Offset", &speed_offset_x)) { - current_border.speed_label_offset = {speed_offset_x, speed_offset_y}; - preview_needs_update = true; - } - - // Field offsets - ImGui::Separator(); - ImGui::Text("Field Positions:"); - if (ImGui::DragInt2("Score Field Offset", &score_field_x)) { - current_border.score_field_offset = {score_field_x, score_field_y}; - preview_needs_update = true; - } - if (ImGui::DragInt2("Time Field Offset", &time_field_x)) { - current_border.time_field_offset = {time_field_x, time_field_y}; - preview_needs_update = true; - } - if (ImGui::DragInt2("Chain Field Offset", &chain_field_x)) { - current_border.chain_field_offset = {chain_field_x, chain_field_y}; - preview_needs_update = true; - } - if (ImGui::DragInt2("Speed Field Offset", &speed_field_x)) { - current_border.speed_field_offset = {speed_field_x, speed_field_y}; - preview_needs_update = true; - } - - ImGui::Separator(); - if (ImGui::Button("Save Border")) { - SaveCurrentBorder(); - } - ImGui::SameLine(); - ImGui::TextColored(ImVec4(0.5f, 0.5f, 0.5f, 1.0f), "Saved to: borders/.json"); -} - -void ThemeEditorState::DrawBackgroundEditor() { - ImGui::Text("Create and edit custom backgrounds"); - ImGui::Separator(); - - // Load existing background - ImGui::Text("Load existing background:"); - if (ImGui::BeginCombo("##BackgroundLoad", background_names.empty() ? "No backgrounds" : current_background.name.c_str())) { - for (const auto& name : background_names) { - bool is_selected = (current_background.name == name); - if (ImGui::Selectable(name.c_str(), is_selected)) { - LoadBackground(name); - } - if (is_selected) { - ImGui::SetItemDefaultFocus(); - } - } - ImGui::EndCombo(); - } - - ImGui::Separator(); - ImGui::Text("Background Settings:"); - - // Background name - ImGui::InputText("Background Name", background_name_buffer, sizeof(background_name_buffer)); - - // Background sprite - if (ImGui::InputText("Background Sprite", background_sprite_buffer, sizeof(background_sprite_buffer))) { - current_background.background_sprite = background_sprite_buffer; - preview_needs_update = true; - } - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Examples: background, background_autumn, custom_background_"); - } - - // Validate sprite button - ImGui::SameLine(); - if (ImGui::Button("Validate##background")) { - if (ValidateSprite(background_sprite_buffer)) { - ImGui::OpenPopup("Valid BG Sprite"); - } else { - ImGui::OpenPopup("Invalid BG Sprite"); - } - } - - if (ImGui::BeginPopup("Valid BG Sprite")) { - ImGui::Text("Sprite exists!"); - ImGui::EndPopup(); - } - if (ImGui::BeginPopup("Invalid BG Sprite")) { - ImGui::Text("Warning: Sprite does not exist!"); - ImGui::EndPopup(); - } - - // 16:9 background sprite (optional) - if (ImGui::InputText("Background Sprite 16:9 (optional)", background_sprite_16x9_buffer, sizeof(background_sprite_16x9_buffer))) { - current_background.background_sprite_16x9 = background_sprite_16x9_buffer; - preview_needs_update = true; - } - - // Scaling mode - ImGui::Separator(); - ImGui::Text("Scaling Mode:"); - const char* scale_modes[] = { "Stretch", "Tile", "Resize", "Cut" }; - if (ImGui::Combo("##ScaleMode", &background_scale_mode, scale_modes, 4)) { - switch (background_scale_mode) { - case 0: current_background.background_scale = ImgScale::Stretch; break; - case 1: current_background.background_scale = ImgScale::Tile; break; - case 2: current_background.background_scale = ImgScale::Resize; break; - case 3: current_background.background_scale = ImgScale::Cut; break; - } - preview_needs_update = true; - } - - // Tile animation speed - ImGui::Separator(); - ImGui::Text("Tile Animation (0 = disabled):"); - if (ImGui::DragInt("Move Speed X (ms)", &tile_move_speed_x, 1.0f, 0, 10000)) { - current_background.tileMoveSpeedX = tile_move_speed_x; - preview_needs_update = true; - } - if (ImGui::DragInt("Move Speed Y (ms)", &tile_move_speed_y, 1.0f, 0, 10000)) { - current_background.tileMoveSpeedY = tile_move_speed_y; - preview_needs_update = true; - } - - ImGui::Separator(); - if (ImGui::Button("Save Background")) { - SaveCurrentBackground(); - } - ImGui::SameLine(); - ImGui::TextColored(ImVec4(0.5f, 0.5f, 0.5f, 1.0f), "Saved to: backgrounds/.json"); -} - -void ThemeEditorState::DrawThemeComposer() { - ImGui::Text("Combine borders and backgrounds into a complete theme"); - ImGui::Separator(); - - // Theme name - ImGui::InputText("Theme Name", theme_name_buffer, sizeof(theme_name_buffer)); - - // Border selection - ImGui::Separator(); - ImGui::Text("Select Border:"); - if (ImGui::BeginCombo("##BorderSelect", static_cast(selected_border_index) < border_names.size() ? border_names[selected_border_index].c_str() : "")) { - for (size_t i = 0; i < border_names.size(); i++) { - bool is_selected = (static_cast(selected_border_index) == i); - if (ImGui::Selectable(border_names[i].c_str(), is_selected)) { - selected_border_index = i; - preview_needs_update = true; - } - if (is_selected) { - ImGui::SetItemDefaultFocus(); - } - } - ImGui::EndCombo(); - } - - // Background selection - ImGui::Text("Select Background:"); - if (ImGui::BeginCombo("##BackgroundSelect", static_cast(selected_background_index) < background_names.size() ? background_names[selected_background_index].c_str() : "")) { - for (size_t i = 0; i < background_names.size(); i++) { - bool is_selected = (static_cast(selected_background_index) == i); - if (ImGui::Selectable(background_names[i].c_str(), is_selected)) { - selected_background_index = i; - preview_needs_update = true; - } - if (is_selected) { - ImGui::SetItemDefaultFocus(); - } - } - ImGui::EndCombo(); - } - - // Decoration selection - ImGui::Text("Select Decoration:"); - if (ImGui::BeginCombo("##DecorationSelect", static_cast(selected_decoration_index) < decoration_names.size() ? decoration_names[selected_decoration_index].c_str() : "")) { - for (size_t i = 0; i < decoration_names.size(); i++) { - bool is_selected = (static_cast(selected_decoration_index) == i); - if (ImGui::Selectable(decoration_names[i].c_str(), is_selected)) { - selected_decoration_index = i; - preview_needs_update = true; - } - if (is_selected) { - ImGui::SetItemDefaultFocus(); - } - } - ImGui::EndCombo(); - } - - // Board background selection - ImGui::Text("Select Board Background:"); - if (ImGui::BeginCombo("##BoardBGSelect", board_backgrounds[selected_board_background].c_str())) { - for (size_t i = 0; i < board_backgrounds.size(); i++) { - bool is_selected = (static_cast(selected_board_background) == i); - if (ImGui::Selectable(board_backgrounds[i].c_str(), is_selected)) { - selected_board_background = i; - preview_needs_update = true; - } - if (is_selected) { - ImGui::SetItemDefaultFocus(); - } - } - ImGui::EndCombo(); - } - - ImGui::Separator(); - if (ImGui::Button("Save Theme")) { - SaveCurrentTheme(); - } - ImGui::SameLine(); - ImGui::TextColored(ImVec4(0.5f, 0.5f, 0.5f, 1.0f), "Saved to: custom_themes.json"); -} - -void ThemeEditorState::DrawPreview(SDL_Renderer* target) { - (void)target; - // TODO: Implement preview rendering using BlockGameSdl -} - -void ThemeEditorState::SaveCurrentBorder() { - // Update current_border from UI fields - std::string name = border_name_buffer; - if (name.empty()) { - std::cerr << "Error: Border name cannot be empty\n"; - return; - } - - current_border.name = name; - current_border.border_sprite = border_sprite_buffer; - current_border.border_sprite_offset = {border_offset_x, border_offset_y}; - current_border.score_label_offset = {score_offset_x, score_offset_y}; - current_border.time_label_offset = {time_offset_x, time_offset_y}; - current_border.chain_label_offset = {chain_offset_x, chain_offset_y}; - current_border.speed_label_offset = {speed_offset_x, speed_offset_y}; - current_border.score_field_offset = {score_field_x, score_field_y}; - current_border.time_field_offset = {time_field_x, time_field_y}; - current_border.chain_field_offset = {chain_field_x, chain_field_y}; - current_border.speed_field_offset = {speed_field_x, speed_field_y}; - - // Validate sprite - if (!ValidateSprite(current_border.border_sprite)) { - std::cerr << "Warning: Saving border with non-existent sprite: " << current_border.border_sprite << "\n"; - } - - // Save to file - ThemesSaveBorderData(name, current_border); - std::cout << "Saved border: " << name << "\n"; - - // Refresh border list - border_names = ThemesGetBorderNames(); -} - -void ThemeEditorState::SaveCurrentBackground() { - // Update current_background from UI fields - std::string name = background_name_buffer; - if (name.empty()) { - std::cerr << "Error: Background name cannot be empty\n"; - return; - } - - current_background.name = name; - current_background.background_sprite = background_sprite_buffer; - current_background.background_sprite_16x9 = background_sprite_16x9_buffer; - - // Convert scale mode - switch (background_scale_mode) { - case 0: current_background.background_scale = ImgScale::Stretch; break; - case 1: current_background.background_scale = ImgScale::Tile; break; - case 2: current_background.background_scale = ImgScale::Resize; break; - case 3: current_background.background_scale = ImgScale::Cut; break; - } - - current_background.tileMoveSpeedX = tile_move_speed_x; - current_background.tileMoveSpeedY = tile_move_speed_y; - - // Validate sprite - if (!ValidateSprite(current_background.background_sprite)) { - std::cerr << "Warning: Saving background with non-existent sprite: " << current_background.background_sprite << "\n"; - } - - // Save to file - ThemesSaveBackgroundData(name, current_background); - std::cout << "Saved background: " << name << "\n"; - - // Refresh background list - background_names = ThemesGetBackgroundNames(); -} - -void ThemeEditorState::SaveCurrentTheme() { - std::string name = theme_name_buffer; - if (name.empty()) { - std::cerr << "Error: Theme name cannot be empty\n"; - return; - } - - Theme theme; - theme.theme_name = name; - - // Set components - if (static_cast(selected_border_index) < border_names.size()) { - theme.border = ThemesGetBorder(border_names[selected_border_index]); - } - if (static_cast(selected_background_index) < background_names.size()) { - theme.background = ThemesGetBackground(background_names[selected_background_index]); - } - theme.back_board = board_backgrounds[selected_board_background]; - - // Add or replace the theme - ThemesAddOrReplace(theme); - - // Save to custom themes file - ThemesSaveCustomSlots(); - - std::cout << "Saved theme: " << name << "\n"; -} - -void ThemeEditorState::LoadBorder(const std::string& name) { - current_border = ThemesGetBorder(name); - - // Update UI fields - strncpy(border_name_buffer, current_border.name.c_str(), sizeof(border_name_buffer) - 1); - strncpy(border_sprite_buffer, current_border.border_sprite.c_str(), sizeof(border_sprite_buffer) - 1); - border_offset_x = current_border.border_sprite_offset.first; - border_offset_y = current_border.border_sprite_offset.second; - score_offset_x = current_border.score_label_offset.first; - score_offset_y = current_border.score_label_offset.second; - time_offset_x = current_border.time_label_offset.first; - time_offset_y = current_border.time_label_offset.second; - chain_offset_x = current_border.chain_label_offset.first; - chain_offset_y = current_border.chain_label_offset.second; - speed_offset_x = current_border.speed_label_offset.first; - speed_offset_y = current_border.speed_label_offset.second; - score_field_x = current_border.score_field_offset.first; - score_field_y = current_border.score_field_offset.second; - time_field_x = current_border.time_field_offset.first; - time_field_y = current_border.time_field_offset.second; - chain_field_x = current_border.chain_field_offset.first; - chain_field_y = current_border.chain_field_offset.second; - speed_field_x = current_border.speed_field_offset.first; - speed_field_y = current_border.speed_field_offset.second; - preview_needs_update = true; -} - -void ThemeEditorState::LoadBackground(const std::string& name) { - current_background = ThemesGetBackground(name); - - // Update UI fields - strncpy(background_name_buffer, current_background.name.c_str(), sizeof(background_name_buffer) - 1); - strncpy(background_sprite_buffer, current_background.background_sprite.c_str(), sizeof(background_sprite_buffer) - 1); - strncpy(background_sprite_16x9_buffer, current_background.background_sprite_16x9.c_str(), sizeof(background_sprite_16x9_buffer) - 1); - - // Convert scale mode - switch (current_background.background_scale) { - case ImgScale::Stretch: background_scale_mode = 0; break; - case ImgScale::Tile: background_scale_mode = 1; break; - case ImgScale::Resize: background_scale_mode = 2; break; - case ImgScale::Cut: background_scale_mode = 3; break; - } - - tile_move_speed_x = current_background.tileMoveSpeedX; - tile_move_speed_y = current_background.tileMoveSpeedY; - preview_needs_update = true; -} - -bool ThemeEditorState::ValidateSprite(const std::string& sprite_name) { - return ThemesValidateSpriteName(sprite_name); -} - -std::vector ThemeEditorState::GetAvailableSprites() { - // TODO: Implement getting available sprite names from globalData.spriteHolder - return std::vector(); -} diff --git a/source/code/theme_editor/ThemeEditorState.hpp b/source/code/theme_editor/ThemeEditorState.hpp deleted file mode 100644 index 9152dda4..00000000 --- a/source/code/theme_editor/ThemeEditorState.hpp +++ /dev/null @@ -1,116 +0,0 @@ -/* -=========================================================================== -blockattack - Block Attack - Rise of the Blocks -Copyright (C) 2005-2026 Poul Sander - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 2 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see http://www.gnu.org/licenses/ - -Source information and contacts persons can be found at -https://blockattack.net -=========================================================================== -*/ - -#pragma once - -#include "../global.hpp" -#include "../sago/GameStateInterface.hpp" -#include "../themes.hpp" -#include -#include -#include - -class BlockGameSdl; - -class ThemeEditorState : public sago::GameStateInterface { -public: - ThemeEditorState(); - ThemeEditorState(const ThemeEditorState& orig) = delete; - virtual ~ThemeEditorState(); - - void Init(); - - bool IsActive() override; - void ProcessInput(const SDL_Event& event, bool& processed) override; - void Draw(SDL_Renderer* target) override; - void Update() override; - -private: - void DrawBorderEditor(); - void DrawBackgroundEditor(); - void DrawThemeComposer(); - void DrawPreview(SDL_Renderer* target); - - void SaveCurrentBorder(); - void SaveCurrentBackground(); - void SaveCurrentTheme(); - - void LoadBorder(const std::string& name); - void LoadBackground(const std::string& name); - - bool ValidateSprite(const std::string& sprite_name); - std::vector GetAvailableSprites(); - - bool isActive = true; - int current_tab = 0; // 0=Border, 1=Background, 2=Theme - - // Border editor state - ThemeBorderData current_border; - char border_name_buffer[256] = ""; - char border_sprite_buffer[256] = "board_back_back"; - int border_offset_x = -60; - int border_offset_y = -68; - int score_offset_x = 310; - int score_offset_y = 80; - int time_offset_x = 310; - int time_offset_y = 129; - int chain_offset_x = 310; - int chain_offset_y = 178; - int speed_offset_x = 310; - int speed_offset_y = 227; - int score_field_x = 310; - int score_field_y = 102; - int time_field_x = 310; - int time_field_y = 151; - int chain_field_x = 310; - int chain_field_y = 200; - int speed_field_x = 310; - int speed_field_y = 249; - - // Background editor state - BackGroundData current_background; - char background_name_buffer[256] = ""; - char background_sprite_buffer[256] = "background"; - char background_sprite_16x9_buffer[256] = "background_sixteen_nine"; - int background_scale_mode = 0; // 0=Stretch, 1=Tile, 2=Resize, 3=Cut - int tile_move_speed_x = 0; - int tile_move_speed_y = 0; - - // Theme composer state - char theme_name_buffer[256] = "custom_theme"; - int selected_border_index = 0; - int selected_background_index = 0; - int selected_decoration_index = 0; - int selected_board_background = 0; // 0=back_board, 1=back_board_sample_snow, 2=trans_cover - std::vector border_names; - std::vector background_names; - std::vector decoration_names; - const std::vector board_backgrounds = {"back_board", "back_board_sample_snow", "trans_cover"}; - - // Preview - std::shared_ptr preview_game; - Theme preview_theme; - Theme saved_theme; // Backup of the global theme - bool preview_needs_update = true; - void UpdatePreview(); -}; diff --git a/source/code/themes.cpp b/source/code/themes.cpp index e7800fa5..871a9fd4 100644 --- a/source/code/themes.cpp +++ b/source/code/themes.cpp @@ -24,7 +24,6 @@ Source information and contacts persons can be found at #include "themes.hpp" #include "sago/SagoMisc.hpp" -#include "os.hpp" #include #include #include @@ -548,34 +547,6 @@ void ThemesInitCustomBackgrounds() { sago::WriteFileContent("sprites/custom_backgrounds.sprite", sprite_stream.str()); } -void ThemesSaveBorderData(const std::string& name, const ThemeBorderData& data) { - ThemesInit(); - // Create borders directory if it doesn't exist - OsCreateFolder("borders"); - - json j = data; - std::string s = j.dump(4); - std::string filename = fmt::format("borders/{}.json", name); - sago::WriteFileContent(filename.c_str(), s); - - // Add to the border_data map - border_data[name] = data; -} - -void ThemesSaveBackgroundData(const std::string& name, const BackGroundData& data) { - ThemesInit(); - // Create backgrounds directory if it doesn't exist - OsCreateFolder("backgrounds"); - - json j = data; - std::string s = j.dump(4); - std::string filename = fmt::format("backgrounds/{}.json", name); - sago::WriteFileContent(filename.c_str(), s); - - // Add to the background_data map - background_data[name] = data; -} - void ThemesLoadCustomBorders() { const std::vector& border_files = sago::GetFileList("borders"); for (const std::string& filename : border_files) { @@ -623,50 +594,3 @@ void ThemesLoadCustomBackgrounds() { } } } - -bool ThemesValidateSpriteName(const std::string& sprite_name) { - ThemesInit(); - if (sprite_name.empty()) { - return false; - } - // Note: GetSprite returns a default sprite if not found, so we can't easily validate - // The sprite will be validated when actually used in the game - // For now, just check that it's not empty - return true; -} - -std::vector ThemesGetBorderNames() { - ThemesInit(); - std::vector names; - for (const auto& pair : border_data) { - names.push_back(pair.first); - } - return names; -} - -std::vector ThemesGetBackgroundNames() { - ThemesInit(); - std::vector names; - for (const auto& pair : background_data) { - names.push_back(pair.first); - } - return names; -} - -std::vector ThemesGetDecorationNames() { - ThemesInit(); - std::vector names; - for (const auto& pair : decoration_data) { - names.push_back(pair.first); - } - return names; -} - -BackGroundData ThemesGetBackground(const std::string& name) { - ThemesInit(); - auto it = background_data.find(name); - if (it == background_data.end()) { - return background_data["standard"]; - } - return it->second; -} \ No newline at end of file diff --git a/source/code/themes.hpp b/source/code/themes.hpp index cd9d7ffd..a0fd0ff4 100644 --- a/source/code/themes.hpp +++ b/source/code/themes.hpp @@ -148,20 +148,6 @@ GarbageData ThemesGetNextGarbage(const std::string& current); void ThemesInitCustomBackgrounds(); -/** - * @brief Saves a border to the borders/ directory - * @param name The name of the border - * @param data The border data to save - */ -void ThemesSaveBorderData(const std::string& name, const ThemeBorderData& data); - -/** - * @brief Saves a background to the backgrounds/ directory - * @param name The name of the background - * @param data The background data to save - */ -void ThemesSaveBackgroundData(const std::string& name, const BackGroundData& data); - /** * @brief Loads custom borders from the borders/ directory */ @@ -170,36 +156,4 @@ void ThemesLoadCustomBorders(); /** * @brief Loads custom backgrounds from the backgrounds/ directory */ -void ThemesLoadCustomBackgrounds(); - -/** - * @brief Validates that a sprite name exists in the sprite holder - * @param sprite_name The sprite name to validate - * @return true if the sprite exists, false otherwise - */ -bool ThemesValidateSpriteName(const std::string& sprite_name); - -/** - * @brief Gets all available border names - * @return Vector of border names - */ -std::vector ThemesGetBorderNames(); - -/** - * @brief Gets all available background names - * @return Vector of background names - */ -std::vector ThemesGetBackgroundNames(); - -/** - * @brief Gets all available decoration names - * @return Vector of decoration names - */ -std::vector ThemesGetDecorationNames(); - -/** - * @brief Gets a background by name - * @param name The background name - * @return The background data - */ -BackGroundData ThemesGetBackground(const std::string& name); \ No newline at end of file +void ThemesLoadCustomBackgrounds(); \ No newline at end of file