From fcd6fd7c232103fa9a5eb3cbe8a521c055ac5126 Mon Sep 17 00:00:00 2001 From: buzz Date: Sat, 18 Apr 2026 01:33:19 +0200 Subject: [PATCH] Fix memory leaks - Smart Pointer Usage - std::unique_ptr is used consistently for exclusive ownership - std::shared_ptr is used appropriately for shared resources (Font) - All new expressions are wrapped in std::make_unique or std::make_shared - RAII Wrappers - SDLWindow and SDLRenderer properly wrap SDL resources - Font::FontResource wraps SDL_RWops with proper cleanup - Image destructor calls SDL_FreeSurface - FileStream destructor calls close() - Static Resources - FontStorage::fontsMap uses std::shared_ptr - auto-cleanup on program exit - RecordStore::opened uses std::unique_ptr - auto-cleanup on program exit - Font::FontResource singleton uses std::shared_ptr - auto-cleanup --- src/GameCanvas.cpp | 13 +- src/GameCanvas.h | 11 +- src/GameLevel.h | 1 - src/GameMenu.cpp | 5 + src/GameMenu.h | 6 +- src/GamePhysics.h | 4 +- src/LevelLoader.cpp | 20 +- src/LevelLoader.h | 5 +- src/MenuManager.cpp | 454 +++++++++++++++++++---------------- src/MenuManager.h | 132 +++++----- src/Micro.cpp | 19 +- src/Micro.h | 10 +- src/RecordManager.h | 1 + src/SettingsStringRender.cpp | 30 ++- src/SettingsStringRender.h | 13 +- src/TextRender.cpp | 6 +- src/TextRender.h | 2 +- src/lcdui/Canvas.cpp | 5 + src/lcdui/Canvas.h | 1 + src/lcdui/CanvasImpl.cpp | 33 +-- src/lcdui/CanvasImpl.h | 6 +- src/lcdui/Font.cpp | 46 +++- src/lcdui/Font.h | 19 +- src/main.cpp | 4 + src/utils/SDLWrappers.h | 115 +++++++++ src/utils/Time.cpp | 3 +- 26 files changed, 594 insertions(+), 370 deletions(-) create mode 100644 src/utils/SDLWrappers.h diff --git a/src/GameCanvas.cpp b/src/GameCanvas.cpp index ff3686e..123db02 100644 --- a/src/GameCanvas.cpp +++ b/src/GameCanvas.cpp @@ -29,7 +29,7 @@ GameCanvas::GameCanvas(Micro* micro) dx = 0; dy = height2; - commandMenu = new Command("Menu", 1, 1); + commandMenu = std::make_unique("Menu", 1, 1); defaultFontWidth00 = defaultFont->stringWidth("00") + 3; } @@ -589,7 +589,7 @@ void GameCanvas::setMenuManager(MenuManager* menuManager) void GameCanvas::handleMenuCommand(Command* command, Displayable* displayable) { (void)displayable; - if (command == commandMenu) { + if (command == commandMenu.get()) { menuManager->isMenuRenderingBlocked = true; // Signal menu manager to show menu micro->gameToMenu(); // Transition from game to menu state } @@ -620,10 +620,15 @@ void GameCanvas::commandAction(Command* command, Displayable* displayable) void GameCanvas::removeMenuCommand() { - removeCommand(commandMenu); + removeCommand(commandMenu.get()); } void GameCanvas::addMenuCommand() { - addCommand(commandMenu); + addCommand(commandMenu.get()); +} + +void GameCanvas::requestExit() +{ + Micro::isGameLoopRunning = false; } diff --git a/src/GameCanvas.h b/src/GameCanvas.h index cfb0eeb..0649d7f 100644 --- a/src/GameCanvas.h +++ b/src/GameCanvas.h @@ -21,19 +21,19 @@ class GameCanvas : public Canvas, public CommandListener { void handleUpdatedInput(); void processTimers(); - Graphics* graphics = nullptr; + Graphics* graphics = nullptr; // Non-owning reference (owned by Canvas base class) int dx; int dy; int engineSpriteWidth; int engineSpriteHeight; int fenderSpriteWidth; int fenderSpriteHeight; - GamePhysics* gamePhysics = nullptr; - MenuManager* menuManager = nullptr; + GamePhysics* gamePhysics = nullptr; // Non-owning reference (owned by Micro) + MenuManager* menuManager = nullptr; // Non-owning reference (owned by Micro) // Additional offsets for camera view adjustment int cameraOffsetX = 0; int cameraOffsetY = 0; - Micro* micro = nullptr; + Micro* micro = nullptr; // Non-owning reference (root owner) std::shared_ptr font; bool timerTriggered = false; // 0=gameplay, 1=logo screen, 2=splash screen @@ -54,7 +54,7 @@ class GameCanvas : public Canvas, public CommandListener { std::string timerMessage = ""; int timerId = 0; std::vector timers; - Command* commandMenu; + std::unique_ptr commandMenu; // Owned by GameCanvas inline static std::string stringWithTime = ""; std::vector time10MsToStringCache = std::vector(100); int timeInSeconds = -1; @@ -120,6 +120,7 @@ class GameCanvas : public Canvas, public CommandListener { void commandAction(Command* command, Displayable* displayable); void removeMenuCommand(); void addMenuCommand(); + void requestExit() override; int width; int height2; diff --git a/src/GameLevel.h b/src/GameLevel.h index 65ac310..04b55dc 100644 --- a/src/GameLevel.h +++ b/src/GameLevel.h @@ -33,7 +33,6 @@ class GameLevel { std::vector> pointPositions; GameLevel(); - ~GameLevel(); void init(); void setStartFinishPositions(int startX, int startY, int finishX, int finishY); int getStartPosX(); diff --git a/src/GameMenu.cpp b/src/GameMenu.cpp index d45ef5c..9da0bb0 100644 --- a/src/GameMenu.cpp +++ b/src/GameMenu.cpp @@ -422,6 +422,11 @@ void GameMenu::navigateToItem(int targetIndex) { resetToFirstItem(); + // Bounds check: don't navigate beyond the last element + if (targetIndex >= static_cast(vector.size())) { + targetIndex = vector.size() - 1; + } + while (selectedItemIndex < targetIndex) { ++selectedItemIndex; if (selectedItemIndex > scrollOffsetLast) { diff --git a/src/GameMenu.h b/src/GameMenu.h index c96f244..3c03805 100644 --- a/src/GameMenu.h +++ b/src/GameMenu.h @@ -12,11 +12,12 @@ class Graphics; class GameMenu { private: - GameMenu* gameMenu; + GameMenu* gameMenu; // Non-owning reference (parent menu, owned by MenuManager) std::string menuTitle; int selectedItemIndex; + // Non-owning references to menu elements. Elements are owned by MenuManager. std::vector vector; - Micro* micro; + Micro* micro; // Non-owning reference (owned by root) std::shared_ptr font; std::shared_ptr font2; std::shared_ptr font3; @@ -49,6 +50,7 @@ class GameMenu { void setMenuTitle(std::string title); void resetToFirstItem(); void resetToLastItem(); + // Adds element (non-owning reference). Element must be owned elsewhere (e.g., MenuManager). void addMenuElement(IGameMenuElement* element); void processGameActionDown(); void processGameActionUp(); diff --git a/src/GamePhysics.h b/src/GamePhysics.h index c4188b6..01be0c3 100644 --- a/src/GamePhysics.h +++ b/src/GamePhysics.h @@ -3,9 +3,9 @@ #include #include #include "PhysicsElemOrMenuItem.h" +#include "MotoComponent.h" class LevelLoader; -class MotoComponent; class GameCanvas; /** @@ -50,7 +50,7 @@ class GamePhysics { // Engine momentum / throttle value (drives wheel torque) int engineMomentumF16 = 0; - LevelLoader* levelLoader; + LevelLoader* levelLoader; // Non-owning reference (owned by Micro) // Collision normal X - from LevelLoader int collisionNormalXF16 = 0; // Collision normal Y - from LevelLoader diff --git a/src/LevelLoader.cpp b/src/LevelLoader.cpp index 1c5491e..ba50c30 100644 --- a/src/LevelLoader.cpp +++ b/src/LevelLoader.cpp @@ -21,14 +21,12 @@ LevelLoader::LevelLoader(const std::filesystem::path& mrgFilePath) } if (!mrgFilePath.string().empty()) { - FileStream* fileStream = new FileStream(mrgFilePath, std::ios::in | std::ios::binary); - if (!fileStream->isOpen()) { + levelFileStream = std::make_unique(mrgFilePath, std::ios::in | std::ios::binary); + if (!levelFileStream->isOpen()) { throw std::system_error(errno, std::system_category(), "Failed to open " + mrgFilePath.string()); } - levelFileStream = fileStream; } else { - EmbedFileStream* embedFileStream = new EmbedFileStream("levels.mrg"); - levelFileStream = static_cast(embedFileStream); + levelFileStream = std::make_unique("levels.mrg"); } loadLevels(); @@ -37,7 +35,7 @@ LevelLoader::LevelLoader(const std::filesystem::path& mrgFilePath) LevelLoader::~LevelLoader() { - delete levelFileStream; + // unique_ptr members (levelFileStream and gameLevel) are automatically cleaned up } void LevelLoader::loadLevels() @@ -101,11 +99,11 @@ void LevelLoader::seekAndLoadTrackData(int league, int track) { // Seek to track byte offset in MRG file (1-based indices) levelFileStream->setPos(trackOffsetInFile[league - 1][track - 1]); - if (gameLevel == nullptr) { - gameLevel = new GameLevel(); + if (!gameLevel) { + gameLevel = std::make_unique(); } - gameLevel->load(levelFileStream); - precomputeTrackGeometry(gameLevel); + gameLevel->load(levelFileStream.get()); + precomputeTrackGeometry(gameLevel.get()); } void LevelLoader::cacheStartPosition() @@ -144,7 +142,7 @@ int LevelLoader::getTrackProgressRatio(int xF16) void LevelLoader::precomputeTrackGeometry(GameLevel* level) { trackMinX = INT_MIN; - this->gameLevel = level; + // Note: level is the same as gameLevel.get(), passed for convenience int pointsCount = level->pointsCount; // Resize trackSegmentNormals array if needed (min capacity: 100) diff --git a/src/LevelLoader.h b/src/LevelLoader.h index e75374c..844c718 100644 --- a/src/LevelLoader.h +++ b/src/LevelLoader.h @@ -5,6 +5,7 @@ #include #include #include +#include #include "GamePhysics.h" #include "GameCanvas.h" @@ -33,13 +34,13 @@ class LevelLoader { // Cached X position of visibleSegmentEndIdx static int visibleSegmentEndX; - FileStream* levelFileStream; + std::unique_ptr levelFileStream; void loadLevels(); public: static bool isEnabledPerspective; static bool isEnabledShadows; - GameLevel* gameLevel = nullptr; + std::unique_ptr gameLevel; int currentLevel = 0; int currentTrack = -1; std::vector> trackNames = std::vector>(3); diff --git a/src/MenuManager.cpp b/src/MenuManager.cpp index b915d46..e6ed3a0 100644 --- a/src/MenuManager.cpp +++ b/src/MenuManager.cpp @@ -17,16 +17,22 @@ MenuManager::MenuManager(Micro* micro) helpSeparatorText = std::make_unique("", micro); // Empty separator for help menus } +MenuManager::~MenuManager() +{ + // recordManager is automatically deleted by unique_ptr + // All TextRender vectors are automatically cleaned up by unique_ptr +} + void MenuManager::initializePhase(int phase) { int levelIndex; switch (phase) { case 1: // Initialize basic state and open record store - playerName = defaultPlayerName; + playerName = defaultPlayerName; // std::array copy onOffOptions = { "On", "Off" }; inputMethodNames = { "Keyset 1", "Keyset 2", "Keyset 3" }; - recordManager = new RecordManager(); + recordManager = std::make_unique(); finishTime = -1L; finishTimeSeconds = -1; finishTimeCentiseconds = -1; @@ -81,7 +87,7 @@ void MenuManager::initializePhase(int phase) recordData = loadPlayerName(16, (int8_t)-1); if (!recordData.empty() && recordData[0] != -1) { for (levelIndex = 0; levelIndex < 3; ++levelIndex) { - playerName[levelIndex] = recordData[levelIndex]; + playerName[levelIndex] = static_cast(recordData[levelIndex]); } } @@ -154,47 +160,47 @@ void MenuManager::initializePhase(int phase) return; case 4: { // Create main menu screens - mainMenu = new GameMenu("Main", micro, nullptr); - playMenu = new GameMenu("Play", micro, mainMenu); - optionsMenu = new GameMenu("Options", micro, mainMenu); - aboutMenu = new GameMenu("About", micro, mainMenu); - helpMenu = new GameMenu("Help", micro, mainMenu); - backSetting = new SettingsStringRender("Back", 0, this, std::vector(), false, micro, mainMenu, true); - goToMainSetting = new SettingsStringRender("Go to Main", 0, this, std::vector(), false, micro, mainMenu, true); - continueSetting = new SettingsStringRender("Continue", 0, this, std::vector(), false, micro, mainMenu, true); - playMenuSetting = new SettingsStringRender("Play Menu", 0, this, std::vector(), false, micro, mainMenu, true); + mainMenu = std::make_unique("Main", micro, nullptr); + playMenu = std::make_unique("Play", micro, mainMenu.get()); + optionsMenu = std::make_unique("Options", micro, mainMenu.get()); + aboutMenu = std::make_unique("About", micro, mainMenu.get()); + helpMenu = std::make_unique("Help", micro, mainMenu.get()); + backSetting = std::make_unique("Back", 0, this, std::vector(), false, micro, mainMenu.get(), true); + goToMainSetting = std::make_unique("Go to Main", 0, this, std::vector(), false, micro, mainMenu.get(), true); + continueSetting = std::make_unique("Continue", 0, this, std::vector(), false, micro, mainMenu.get(), true); + playMenuSetting = std::make_unique("Play Menu", 0, this, std::vector(), false, micro, mainMenu.get(), true); std::shared_ptr boldSmallFont = FontStorage::getFont(Font::STYLE_BOLD, Font::SIZE_SMALL); if (aboutMenu->xPos + boldSmallFont->stringWidth("http://www.codebrew.se/") >= getCanvasWidth()) { - codebrewLinkText = new TextRender("www.codebrew.se", micro); + codebrewLinkText = std::make_unique("www.codebrew.se", micro); } else { - codebrewLinkText = new TextRender("http://www.codebrew.se/", micro); + codebrewLinkText = std::make_unique("http://www.codebrew.se/", micro); } codebrewLinkText->setFont(boldSmallFont); - highscoreMenu = new GameMenu("Highscore", micro, playMenu); - finishedTrackMenu = new GameMenu("Finished!", micro, playMenu); + highscoreMenu = std::make_unique("Highscore", micro, playMenu.get()); + finishedTrackMenu = std::make_unique("Finished!", micro, playMenu.get()); } return; case 5: // Create ingame and confirmation menus - ingameMenu = new GameMenu("Ingame", micro, playMenu); - enterNameMenu = new GameMenu("Enter Name", micro, finishedTrackMenu, playerName); - confirmClearHighscoresMenu = new GameMenu("Confirm Clear", micro, optionsMenu); - confirmFullResetMenu = new GameMenu("Confirm Reset", micro, confirmClearHighscoresMenu); - playMenuTask = new PhysicsElemOrMenuItem("Play Menu", playMenu, this); - optionsTask = new PhysicsElemOrMenuItem("Options", optionsMenu, this); - helpTask = new PhysicsElemOrMenuItem("Help", helpMenu, this); - aboutTask = new PhysicsElemOrMenuItem("About", aboutMenu, this); - exitGameSetting = new SettingsStringRender("Exit Game", 0, this, std::vector(), false, micro, mainMenu, true); - mainMenu->addMenuElement(playMenuTask); - mainMenu->addMenuElement(optionsTask); - mainMenu->addMenuElement(helpTask); - mainMenu->addMenuElement(aboutTask); - mainMenu->addMenuElement(exitGameSetting); - levelSetting = new SettingsStringRender("Level", lastSelectedLevel, this, levelNames, false, micro, playMenu, false); - trackSetting = new SettingsStringRender("Track", lastSelectedTrackPerLevel[lastSelectedLevel], this, trackNamesByLevel[lastSelectedLevel], false, micro, playMenu, false); - leagueSetting = new SettingsStringRender("League", lastSelectedLeague, this, leagueNames, false, micro, playMenu, false); + ingameMenu = std::make_unique("Ingame", micro, playMenu.get()); + enterNameMenu = std::make_unique("Enter Name", micro, finishedTrackMenu.get(), playerName.data()); + confirmClearHighscoresMenu = std::make_unique("Confirm Clear", micro, optionsMenu.get()); + confirmFullResetMenu = std::make_unique("Confirm Reset", micro, confirmClearHighscoresMenu.get()); + playMenuTask = std::make_unique("Play Menu", playMenu.get(), this); + optionsTask = std::make_unique("Options", optionsMenu.get(), this); + helpTask = std::make_unique("Help", helpMenu.get(), this); + aboutTask = std::make_unique("About", aboutMenu.get(), this); + exitGameSetting = std::make_unique("Exit Game", 0, this, std::vector(), false, micro, mainMenu.get(), true); + mainMenu->addMenuElement(playMenuTask.get()); + mainMenu->addMenuElement(optionsTask.get()); + mainMenu->addMenuElement(helpTask.get()); + mainMenu->addMenuElement(aboutTask.get()); + mainMenu->addMenuElement(exitGameSetting.get()); + levelSetting = std::make_unique("Level", lastSelectedLevel, this, levelNames, false, micro, playMenu.get(), false); + trackSetting = std::make_unique("Track", lastSelectedTrackPerLevel[lastSelectedLevel], this, trackNamesByLevel[lastSelectedLevel], false, micro, playMenu.get(), false); + leagueSetting = std::make_unique("League", lastSelectedLeague, this, leagueNames, false, micro, playMenu.get(), false); try { trackSetting->setAvailableOptions(maxUnlockedTracksPerLevel[lastSelectedLevel]); @@ -204,119 +210,119 @@ void MenuManager::initializePhase(int phase) levelSetting->setAvailableOptions(maxAvailableLevels); leagueSetting->setAvailableOptions(maxAvailableLeagues); - highscoreTask = new PhysicsElemOrMenuItem("Highscore", highscoreMenu, this); - highscoreMenu->addMenuElement(backSetting); - startTask = new SettingsStringRender("Start>", 0, this, std::vector(), false, micro, mainMenu, true); - playMenu->addMenuElement(startTask); - playMenu->addMenuElement(levelSetting); - playMenu->addMenuElement(trackSetting); - playMenu->addMenuElement(leagueSetting); - playMenu->addMenuElement(highscoreTask); - playMenu->addMenuElement(goToMainSetting); - - perspectiveSetting = new SettingsStringRender("Perspective", perspectiveDisabled, this, onOffOptions, true, micro, optionsMenu, false); - shadowsSetting = new SettingsStringRender("Shadows", shadowsDisabled, this, onOffOptions, true, micro, optionsMenu, false); - driverSpriteSetting = new SettingsStringRender("Driver sprite", driverSpriteDisabled, this, onOffOptions, true, micro, optionsMenu, false); - bikeSpriteSetting = new SettingsStringRender("Bike sprite", bikeSpriteDisabled, this, onOffOptions, true, micro, optionsMenu, false); - inputSetting = new SettingsStringRender("Input", inputMethod, this, inputMethodNames, false, micro, optionsMenu, false); - lookAheadSetting = new SettingsStringRender("Look ahead", lookAheadDisabled, this, onOffOptions, true, micro, optionsMenu, false); - clearHighscoreTask = new PhysicsElemOrMenuItem("Clear highscore", confirmClearHighscoresMenu, this); + highscoreTask = std::make_unique("Highscore", highscoreMenu.get(), this); + highscoreMenu->addMenuElement(backSetting.get()); + startTask = std::make_unique("Start>", 0, this, std::vector(), false, micro, mainMenu.get(), true); + playMenu->addMenuElement(startTask.get()); + playMenu->addMenuElement(levelSetting.get()); + playMenu->addMenuElement(trackSetting.get()); + playMenu->addMenuElement(leagueSetting.get()); + playMenu->addMenuElement(highscoreTask.get()); + playMenu->addMenuElement(goToMainSetting.get()); + + perspectiveSetting = std::make_unique("Perspective", perspectiveDisabled, this, onOffOptions, true, micro, optionsMenu.get(), false); + shadowsSetting = std::make_unique("Shadows", shadowsDisabled, this, onOffOptions, true, micro, optionsMenu.get(), false); + driverSpriteSetting = std::make_unique("Driver sprite", driverSpriteDisabled, this, onOffOptions, true, micro, optionsMenu.get(), false); + bikeSpriteSetting = std::make_unique("Bike sprite", bikeSpriteDisabled, this, onOffOptions, true, micro, optionsMenu.get(), false); + inputSetting = std::make_unique("Input", inputMethod, this, inputMethodNames, false, micro, optionsMenu.get(), false); + lookAheadSetting = std::make_unique("Look ahead", lookAheadDisabled, this, onOffOptions, true, micro, optionsMenu.get(), false); + clearHighscoreTask = std::make_unique("Clear highscore", confirmClearHighscoresMenu.get(), this); return; case 6: // Populate options menu and create help menus - optionsMenu->addMenuElement(perspectiveSetting); - optionsMenu->addMenuElement(shadowsSetting); - optionsMenu->addMenuElement(driverSpriteSetting); - optionsMenu->addMenuElement(bikeSpriteSetting); - optionsMenu->addMenuElement(inputSetting); - optionsMenu->addMenuElement(lookAheadSetting); - optionsMenu->addMenuElement(clearHighscoreTask); - optionsMenu->addMenuElement(backSetting); - confirmNoSetting = new SettingsStringRender("No", 0, this, std::vector(), false, micro, mainMenu, true); - confirmYesSetting = new SettingsStringRender("Yes", 0, this, std::vector(), false, micro, mainMenu, true); - fullResetTask = new PhysicsElemOrMenuItem("Full Reset", confirmFullResetMenu, this); - addMultilineTextToMenu(confirmClearHighscoresMenu, "Clearing the highscores can not be undone. It will remove all the registered times on all tracks."); - addMultilineTextToMenu(confirmClearHighscoresMenu, "Would you like to clear the highscores?"); - confirmClearHighscoresMenu->addMenuElement(confirmNoSetting); - confirmClearHighscoresMenu->addMenuElement(confirmYesSetting); - confirmClearHighscoresMenu->addMenuElement(fullResetTask); - addMultilineTextToMenu(confirmFullResetMenu, "A full reset can not be undone. It will relock all tracks and leagues and clear back all settings to default. A full reset will exit the application."); - addMultilineTextToMenu(confirmFullResetMenu, "Would you like to do a full reset?"); - confirmFullResetMenu->addMenuElement(confirmNoSetting); - confirmFullResetMenu->addMenuElement(confirmYesSetting); - helpObjectiveMenu = new GameMenu("Objective", micro, helpMenu); - helpObjectiveTask = new PhysicsElemOrMenuItem("Objective", helpObjectiveMenu, this); - addMultilineTextToMenu(helpObjectiveMenu, "Race to the finish line as fast as you can without crashing. By leaning forward and backward you can adjust the rotation of your bike. By landing on both wheels after jumping, your bike won't crash as easily. Beware, the levels tend to get harder and harder..."); - helpObjectiveMenu->addMenuElement(backSetting); - helpMenu->addMenuElement(helpObjectiveTask); - helpKeysMenu = new GameMenu("Keys", micro, helpMenu); - helpKeysTask = new PhysicsElemOrMenuItem("Keys", helpKeysMenu, this); - addMultilineTextToMenu(helpKeysMenu, "- " + inputMethodNames[0] + " -"); - addMultilineTextToMenu(helpKeysMenu, "UP accelerates, DOWN brakes, RIGHT leans forward and LEFT leans backward. 1 accelerates and leans backward. 3 accelerates and leans forward. 7 brakes and leans backward. 9 brakes and leans forward."); + optionsMenu->addMenuElement(perspectiveSetting.get()); + optionsMenu->addMenuElement(shadowsSetting.get()); + optionsMenu->addMenuElement(driverSpriteSetting.get()); + optionsMenu->addMenuElement(bikeSpriteSetting.get()); + optionsMenu->addMenuElement(inputSetting.get()); + optionsMenu->addMenuElement(lookAheadSetting.get()); + optionsMenu->addMenuElement(clearHighscoreTask.get()); + optionsMenu->addMenuElement(backSetting.get()); + confirmNoSetting = std::make_unique("No", 0, this, std::vector(), false, micro, mainMenu.get(), true); + confirmYesSetting = std::make_unique("Yes", 0, this, std::vector(), false, micro, mainMenu.get(), true); + fullResetTask = std::make_unique("Full Reset", confirmFullResetMenu.get(), this); + addMultilineTextToMenu(confirmClearHighscoresMenu.get(), "Clearing the highscores can not be undone. It will remove all the registered times on all tracks."); + addMultilineTextToMenu(confirmClearHighscoresMenu.get(), "Would you like to clear the highscores?"); + confirmClearHighscoresMenu->addMenuElement(confirmNoSetting.get()); + confirmClearHighscoresMenu->addMenuElement(confirmYesSetting.get()); + confirmClearHighscoresMenu->addMenuElement(fullResetTask.get()); + addMultilineTextToMenu(confirmFullResetMenu.get(), "A full reset can not be undone. It will relock all tracks and leagues and clear back all settings to default. A full reset will exit the application."); + addMultilineTextToMenu(confirmFullResetMenu.get(), "Would you like to do a full reset?"); + confirmFullResetMenu->addMenuElement(confirmNoSetting.get()); + confirmFullResetMenu->addMenuElement(confirmYesSetting.get()); + helpObjectiveMenu = std::make_unique("Objective", micro, helpMenu.get()); + helpObjectiveTask = std::make_unique("Objective", helpObjectiveMenu.get(), this); + addMultilineTextToMenu(helpObjectiveMenu.get(), "Race to the finish line as fast as you can without crashing. By leaning forward and backward you can adjust the rotation of your bike. By landing on both wheels after jumping, your bike won't crash as easily. Beware, the levels tend to get harder and harder..."); + helpObjectiveMenu->addMenuElement(backSetting.get()); + helpMenu->addMenuElement(helpObjectiveTask.get()); + helpKeysMenu = std::make_unique("Keys", micro, helpMenu.get()); + helpKeysTask = std::make_unique("Keys", helpKeysMenu.get(), this); + addMultilineTextToMenu(helpKeysMenu.get(), "- " + inputMethodNames[0] + " -"); + addMultilineTextToMenu(helpKeysMenu.get(), "UP accelerates, DOWN brakes, RIGHT leans forward and LEFT leans backward. 1 accelerates and leans backward. 3 accelerates and leans forward. 7 brakes and leans backward. 9 brakes and leans forward."); helpKeysMenu->addMenuElement(helpSeparatorText.get()); - addMultilineTextToMenu(helpKeysMenu, "- " + inputMethodNames[1] + " -"); - addMultilineTextToMenu(helpKeysMenu, "1 accelerates, 4 brakes, 6 leans forward and 5 leans backward."); + addMultilineTextToMenu(helpKeysMenu.get(), "- " + inputMethodNames[1] + " -"); + addMultilineTextToMenu(helpKeysMenu.get(), "1 accelerates, 4 brakes, 6 leans forward and 5 leans backward."); helpKeysMenu->addMenuElement(helpSeparatorText.get()); - addMultilineTextToMenu(helpKeysMenu, "- " + inputMethodNames[2] + " -"); - addMultilineTextToMenu(helpKeysMenu, "3 accelerates, 6 brakes, 5 leans forward and 4 leans backward."); - helpKeysMenu->addMenuElement(backSetting); - helpMenu->addMenuElement(helpKeysTask); - helpUnlockingMenu = new GameMenu("Unlocking", micro, helpMenu); - helpUnlockingTask = new PhysicsElemOrMenuItem("Unlocking", helpUnlockingMenu, this); - addMultilineTextToMenu(helpUnlockingMenu, "By completing the easier levels, new levels will be unlocked. You will also gain access to higher leagues where more advanced bikes with different characteristics are available."); - helpUnlockingMenu->addMenuElement(backSetting); - helpMenu->addMenuElement(helpUnlockingTask); - helpHighscoreDescriptionMenu = new GameMenu("Highscore", micro, helpMenu); - helpHighscoreTask = new PhysicsElemOrMenuItem("Highscore", helpHighscoreDescriptionMenu, this); - addMultilineTextToMenu(helpHighscoreDescriptionMenu, "The three best times on every track are saved for each league. When beating a time on a track you will be asked to enter your name. The highscores can be viewed from the Play Menu. By pressing left and right in the highscore view you can view the highscore for a specific league. The highscore can be cleared from the options menu."); - helpHighscoreDescriptionMenu->addMenuElement(backSetting); - helpMenu->addMenuElement(helpHighscoreTask); + addMultilineTextToMenu(helpKeysMenu.get(), "- " + inputMethodNames[2] + " -"); + addMultilineTextToMenu(helpKeysMenu.get(), "3 accelerates, 6 brakes, 5 leans forward and 4 leans backward."); + helpKeysMenu->addMenuElement(backSetting.get()); + helpMenu->addMenuElement(helpKeysTask.get()); + helpUnlockingMenu = std::make_unique("Unlocking", micro, helpMenu.get()); + helpUnlockingTask = std::make_unique("Unlocking", helpUnlockingMenu.get(), this); + addMultilineTextToMenu(helpUnlockingMenu.get(), "By completing the easier levels, new levels will be unlocked. You will also gain access to higher leagues where more advanced bikes with different characteristics are available."); + helpUnlockingMenu->addMenuElement(backSetting.get()); + helpMenu->addMenuElement(helpUnlockingTask.get()); + helpHighscoreDescriptionMenu = std::make_unique("Highscore", micro, helpMenu.get()); + helpHighscoreTask = std::make_unique("Highscore", helpHighscoreDescriptionMenu.get(), this); + addMultilineTextToMenu(helpHighscoreDescriptionMenu.get(), "The three best times on every track are saved for each league. When beating a time on a track you will be asked to enter your name. The highscores can be viewed from the Play Menu. By pressing left and right in the highscore view you can view the highscore for a specific league. The highscore can be cleared from the options menu."); + helpHighscoreDescriptionMenu->addMenuElement(backSetting.get()); + helpMenu->addMenuElement(helpHighscoreTask.get()); return; case 7: // Create help options description menu - helpOptionsDescriptionMenu = new GameMenu("Options", micro, helpMenu); - helpOptionsTask = new PhysicsElemOrMenuItem("Options", helpOptionsDescriptionMenu, this); + helpOptionsDescriptionMenu = std::make_unique("Options", micro, helpMenu.get()); + helpOptionsTask = std::make_unique("Options", helpOptionsDescriptionMenu.get(), this); - addMultilineTextToMenu(helpOptionsDescriptionMenu, "Perspective: On/Off"); - addMultilineTextToMenu(helpOptionsDescriptionMenu, "Default: . Turns on and off the perspective view of the tracks."); + addMultilineTextToMenu(helpOptionsDescriptionMenu.get(), "Perspective: On/Off"); + addMultilineTextToMenu(helpOptionsDescriptionMenu.get(), "Default: . Turns on and off the perspective view of the tracks."); helpOptionsDescriptionMenu->addMenuElement(helpSeparatorText.get()); - addMultilineTextToMenu(helpOptionsDescriptionMenu, "Shadows: On/Off"); - addMultilineTextToMenu(helpOptionsDescriptionMenu, "Default: . Turns on and off the shadows."); + addMultilineTextToMenu(helpOptionsDescriptionMenu.get(), "Shadows: On/Off"); + addMultilineTextToMenu(helpOptionsDescriptionMenu.get(), "Default: . Turns on and off the shadows."); helpOptionsDescriptionMenu->addMenuElement(helpSeparatorText.get()); - addMultilineTextToMenu(helpOptionsDescriptionMenu, "Driver Sprite: On / Off"); - addMultilineTextToMenu(helpOptionsDescriptionMenu, "Default: . uses a texture for the driver. uses line graphics."); + addMultilineTextToMenu(helpOptionsDescriptionMenu.get(), "Driver Sprite: On / Off"); + addMultilineTextToMenu(helpOptionsDescriptionMenu.get(), "Default: . uses a texture for the driver. uses line graphics."); helpOptionsDescriptionMenu->addMenuElement(helpSeparatorText.get()); - addMultilineTextToMenu(helpOptionsDescriptionMenu, "Bike Sprite: On / Off"); - addMultilineTextToMenu(helpOptionsDescriptionMenu, "Default: . uses a texture for the bike. uses line graphics."); + addMultilineTextToMenu(helpOptionsDescriptionMenu.get(), "Bike Sprite: On / Off"); + addMultilineTextToMenu(helpOptionsDescriptionMenu.get(), "Default: . uses a texture for the bike. uses line graphics."); helpOptionsDescriptionMenu->addMenuElement(helpSeparatorText.get()); - addMultilineTextToMenu(helpOptionsDescriptionMenu, "Input: Keyset 1,2,3 "); - addMultilineTextToMenu(helpOptionsDescriptionMenu, "Default: <1>. Determines which type of input should be used when playing. See \"Keys\" in the help menu for more info."); + addMultilineTextToMenu(helpOptionsDescriptionMenu.get(), "Input: Keyset 1,2,3 "); + addMultilineTextToMenu(helpOptionsDescriptionMenu.get(), "Default: <1>. Determines which type of input should be used when playing. See \"Keys\" in the help menu for more info."); helpOptionsDescriptionMenu->addMenuElement(helpSeparatorText.get()); - addMultilineTextToMenu(helpOptionsDescriptionMenu, "Look ahead: On/Off"); - addMultilineTextToMenu(helpOptionsDescriptionMenu, "Default: . Turns on and off smart camera movement."); + addMultilineTextToMenu(helpOptionsDescriptionMenu.get(), "Look ahead: On/Off"); + addMultilineTextToMenu(helpOptionsDescriptionMenu.get(), "Default: . Turns on and off smart camera movement."); helpOptionsDescriptionMenu->addMenuElement(helpSeparatorText.get()); - addMultilineTextToMenu(helpOptionsDescriptionMenu, "Clear highscore"); - addMultilineTextToMenu(helpOptionsDescriptionMenu, "Lets you clear the highscores. Here you can also do a \"Full Reset\" which will reset the game to original state (clear settings, highscores, unlocked levels and leagues)."); + addMultilineTextToMenu(helpOptionsDescriptionMenu.get(), "Clear highscore"); + addMultilineTextToMenu(helpOptionsDescriptionMenu.get(), "Lets you clear the highscores. Here you can also do a \"Full Reset\" which will reset the game to original state (clear settings, highscores, unlocked levels and leagues)."); helpOptionsDescriptionMenu->addMenuElement(helpSeparatorText.get()); - helpOptionsDescriptionMenu->addMenuElement(backSetting); - helpMenu->addMenuElement(helpOptionsTask); - helpMenu->addMenuElement(backSetting); - addMultilineTextToMenu(aboutMenu, "\"Gravity Defied - Trial Racing\" v1.0 by Codebrew Software © 2004."); - addMultilineTextToMenu(aboutMenu, "brought 2 you by pascha. For information visit:"); - aboutMenu->addMenuElement(codebrewLinkText); - aboutMenu->addMenuElement(backSetting); - nextTrackSetting = new SettingsStringRender("Track: " + micro->levelLoader->getName(0, 1), 0, this, std::vector(), false, micro, mainMenu, true); - restartTrackSetting = new SettingsStringRender("Restart: " + micro->levelLoader->getName(0, 0), 0, this, std::vector(), false, micro, mainMenu, true); - ingameMenu->addMenuElement(continueSetting); - ingameMenu->addMenuElement(restartTrackSetting); - ingameMenu->addMenuElement(optionsTask); - ingameMenu->addMenuElement(helpTask); - ingameMenu->addMenuElement(playMenuSetting); - okSetting = new SettingsStringRender("Ok", 0, this, std::vector(), false, micro, mainMenu, true); - nameEntrySetting = new SettingsStringRender("Name - " + std::string(playerName), 0, this, std::vector(), false, micro, mainMenu, true); - okCommand = new Command("Ok", 4, 1); - backCommand = new Command("Back", 2, 1); - switchToMenu(mainMenu, false); + helpOptionsDescriptionMenu->addMenuElement(backSetting.get()); + helpMenu->addMenuElement(helpOptionsTask.get()); + helpMenu->addMenuElement(backSetting.get()); + addMultilineTextToMenu(aboutMenu.get(), "\"Gravity Defied - Trial Racing\" v1.0 by Codebrew Software © 2004."); + addMultilineTextToMenu(aboutMenu.get(), "brought 2 you by pascha. For information visit:"); + aboutMenu->addMenuElement(codebrewLinkText.get()); + aboutMenu->addMenuElement(backSetting.get()); + nextTrackSetting = std::make_unique("Track: " + micro->levelLoader->getName(0, 1), 0, this, std::vector(), false, micro, mainMenu.get(), true); + restartTrackSetting = std::make_unique("Restart: " + micro->levelLoader->getName(0, 0), 0, this, std::vector(), false, micro, mainMenu.get(), true); + ingameMenu->addMenuElement(continueSetting.get()); + ingameMenu->addMenuElement(restartTrackSetting.get()); + ingameMenu->addMenuElement(optionsTask.get()); + ingameMenu->addMenuElement(helpTask.get()); + ingameMenu->addMenuElement(playMenuSetting.get()); + okSetting = std::make_unique("Ok", 0, this, std::vector(), false, micro, mainMenu.get(), true); + nameEntrySetting = std::make_unique("Name - " + std::string(playerName.data()), 0, this, std::vector(), false, micro, mainMenu.get(), true); + okCommand = std::make_unique("Ok", 4, 1); + backCommand = std::make_unique("Back", 2, 1); + switchToMenu(mainMenu.get(), false); menuBackgroundImage = std::make_unique("raster.png"); @@ -327,10 +333,11 @@ void MenuManager::initializePhase(int phase) void MenuManager::addMultilineTextToMenu(GameMenu* menu, const std::string& text) { - std::vector textRenders = TextRender::makeMultilineTextRenders(text, micro); + auto textRenders = TextRender::makeMultilineTextRenders(text, micro); - for (std::size_t i = 0; i < textRenders.size(); ++i) { - menu->addMenuElement(textRenders[i]); + for (auto& textRender : textRenders) { + menu->addMenuElement(textRender.get()); + multilineTextRenders.push_back(std::move(textRender)); } } @@ -347,16 +354,19 @@ bool MenuManager::consumeShouldStartRaceFlag() void MenuManager::saveHighscoreAndShowResults() { // Save the record and update progression - recordManager->addRecord(leagueSetting->getCurrentOptionPos(), playerName, finishTime); + recordManager->addRecord(leagueSetting->getCurrentOptionPos(), playerName.data(), finishTime); recordManager->writeRecordInfo(); isAllTracksCompletedAtLevel = false; finishedTrackMenu->clearVector(); - finishedTrackMenu->addMenuElement(new TextRender("Time: " + finishTimeFormatted, micro)); + finishedTrackMenuTextRenders.clear(); + finishedTrackMenuTextRenders.push_back(std::make_unique("Time: " + finishTimeFormatted, micro)); + finishedTrackMenu->addMenuElement(finishedTrackMenuTextRenders.back().get()); std::vector recordDescriptions = recordManager->getRecordDescription(leagueSetting->getCurrentOptionPos()); for (std::size_t i = 0; i < recordDescriptions.size(); ++i) { if (recordDescriptions[i] != "") { - finishedTrackMenu->addMenuElement(new TextRender(std::to_string(i + 1) + "." + recordDescriptions[i], micro)); + finishedTrackMenuTextRenders.push_back(std::make_unique(std::to_string(i + 1) + "." + recordDescriptions[i], micro)); + finishedTrackMenu->addMenuElement(finishedTrackMenuTextRenders.back().get()); } } @@ -402,7 +412,7 @@ void MenuManager::saveHighscoreAndShowResults() } int completedTrackCount = countRecordStoresForLevel(levelSetting->getCurrentOptionPos()); - addMultilineTextToMenu(finishedTrackMenu, completedTrackCount + " of " + std::to_string(trackNamesByLevel[levelSetting->getCurrentOptionPos()].size()) + " tracks in " + levelNames[levelSetting->getCurrentOptionPos()] + " completed."); + addMultilineTextToMenu(finishedTrackMenu.get(), completedTrackCount + " of " + std::to_string(trackNamesByLevel[levelSetting->getCurrentOptionPos()].size()) + " tracks in " + levelNames[levelSetting->getCurrentOptionPos()] + " completed."); if (!isAllTracksCompletedAtLevel) { restartTrackSetting->setText("Restart: " + micro->levelLoader->getName(levelSetting->getCurrentOptionPos(), trackSetting->getCurrentOptionPos())); nextTrackSetting->setText("Next: " + micro->levelLoader->getName(savedLevelBeforeMenu, savedTrackBeforeMenu + 1)); @@ -414,9 +424,10 @@ void MenuManager::saveHighscoreAndShowResults() } if (newlyUnlockedLeague != -1) { - addMultilineTextToMenu(finishedTrackMenu, "Congratultions! You have successfully unlocked a new league: " + leagueNames[newlyUnlockedLeague]); + addMultilineTextToMenu(finishedTrackMenu.get(), "Congratultions! You have successfully unlocked a new league: " + leagueNames[newlyUnlockedLeague]); if (newlyUnlockedLeague == 3) { - finishedTrackMenu->addMenuElement(new TextRender("Enjoy...", micro)); + finishedTrackMenuTextRenders.push_back(std::make_unique("Enjoy...", micro)); + finishedTrackMenu->addMenuElement(finishedTrackMenuTextRenders.back().get()); } showAlert("League unlocked", "You have successfully unlocked a new league: " + leagueNames[newlyUnlockedLeague], nullptr); @@ -430,19 +441,19 @@ void MenuManager::saveHighscoreAndShowResults() } if (!allTracksCompleted) { - addMultilineTextToMenu(finishedTrackMenu, "You have completed all tracks at this level."); + addMultilineTextToMenu(finishedTrackMenu.get(), "You have completed all tracks at this level."); } } } if (!isAllTracksCompletedAtLevel) { - finishedTrackMenu->addMenuElement(nextTrackSetting); + finishedTrackMenu->addMenuElement(nextTrackSetting.get()); } restartTrackSetting->setText("Restart: " + micro->levelLoader->getName(savedLevelBeforeMenu, savedTrackBeforeMenu)); - finishedTrackMenu->addMenuElement(restartTrackSetting); - finishedTrackMenu->addMenuElement(playMenuSetting); - switchToMenu(finishedTrackMenu, false); + finishedTrackMenu->addMenuElement(restartTrackSetting.get()); + finishedTrackMenu->addMenuElement(playMenuSetting.get()); + switchToMenu(finishedTrackMenu.get(), false); } void MenuManager::requestRepaint() @@ -466,7 +477,7 @@ void MenuManager::runMenuLoop(int menuType) switch (menuType) { // main menu case 0: - switchToMenu(mainMenu, false); + switchToMenu(mainMenu.get(), false); micro->gamePhysics->enableGenerateInputAI(); shouldStartRaceImmediately = true; break; @@ -476,7 +487,7 @@ void MenuManager::runMenuLoop(int menuType) savedTrackBeforeMenu = trackSetting->getCurrentOptionPos(); restartTrackSetting->setText("Restart: " + micro->levelLoader->getName(savedLevelBeforeMenu, savedTrackBeforeMenu)); shouldStartRaceImmediately = false; - switchToMenu(ingameMenu, false); + switchToMenu(ingameMenu.get(), false); break; // finished track menu case 2: { @@ -489,7 +500,9 @@ void MenuManager::runMenuLoop(int menuType) finishTimeFormatted = formatTime(finishTime); if (newRecordPosition >= 0 && newRecordPosition <= 2) { // New record is in top 3 - show name entry - TextRender* firstPlaceText = new TextRender("", micro); + finishedTrackMenuTextRenders.clear(); + finishedTrackMenuTextRenders.push_back(std::make_unique("", micro)); + TextRender* firstPlaceText = finishedTrackMenuTextRenders.back().get(); firstPlaceText->setDx(GameCanvas::spriteSizeX[5] + 1); switch (newRecordPosition) { case 0: @@ -506,19 +519,20 @@ void MenuManager::runMenuLoop(int menuType) } finishedTrackMenu->addMenuElement(firstPlaceText); - TextRender* timeText = new TextRender("" + finishTimeFormatted, micro); + finishedTrackMenuTextRenders.push_back(std::make_unique("" + finishTimeFormatted, micro)); + TextRender* timeText = finishedTrackMenuTextRenders.back().get(); timeText->setDx(GameCanvas::spriteSizeX[5] + 1); finishedTrackMenu->addMenuElement(timeText); - finishedTrackMenu->addMenuElement(okSetting); - finishedTrackMenu->addMenuElement(nameEntrySetting); - switchToMenu(finishedTrackMenu, false); + finishedTrackMenu->addMenuElement(okSetting.get()); + finishedTrackMenu->addMenuElement(nameEntrySetting.get()); + switchToMenu(finishedTrackMenu.get(), false); isMenuRenderingBlocked = false; } else { saveHighscoreAndShowResults(); } } break; default: - switchToMenu(mainMenu, false); + switchToMenu(mainMenu.get(), false); break; } @@ -607,7 +621,7 @@ void MenuManager::processKey(int keyCode) return; case 2: // LEFT currentGameMenu->processGameActionUpd(3); - if (currentGameMenu == highscoreMenu) { + if (currentGameMenu == highscoreMenu.get()) { --highscoreLeagueViewIndex; if (highscoreLeagueViewIndex < 0) { highscoreLeagueViewIndex = 0; @@ -622,7 +636,7 @@ void MenuManager::processKey(int keyCode) break; case 5: // RIGHT currentGameMenu->processGameActionUpd(2); - if (currentGameMenu == highscoreMenu) { + if (currentGameMenu == highscoreMenu.get()) { ++highscoreLeagueViewIndex; if (highscoreLeagueViewIndex > leagueSetting->getMaxAvailableOptionPos()) { highscoreLeagueViewIndex = leagueSetting->getMaxAvailableOptionPos(); @@ -645,18 +659,24 @@ void MenuManager::processKey(int keyCode) void MenuManager::handleCommand(Command* command, Displayable* displayable) { (void)displayable; - if (command == okCommand) { + if (command == okCommand.get()) { if (currentGameMenu != nullptr) { currentGameMenu->processGameActionUpd(1); return; } - } else if (command == backCommand && currentGameMenu != nullptr) { - if (currentGameMenu == ingameMenu) { + } else if (command == backCommand.get() && currentGameMenu != nullptr) { + if (currentGameMenu == ingameMenu.get()) { micro->menuToGame(); return; } - switchToMenu(currentGameMenu->getGameMenu(), true); + GameMenu* parentMenu = currentGameMenu->getGameMenu(); + // If there's no parent menu, user is at root - exit the game + if (parentMenu == nullptr) { + Micro::isGameLoopRunning = false; + } + + switchToMenu(parentMenu, true); } } @@ -667,28 +687,33 @@ GameMenu* MenuManager::getCurrentMenu() void MenuManager::switchToMenu(GameMenu* menu, bool skipSelectionReset) { - micro->gameCanvas->removeCommand(backCommand); - if (menu != mainMenu && menu != finishedTrackMenu && menu != nullptr) { - micro->gameCanvas->addCommand(backCommand); + micro->gameCanvas->removeCommand(backCommand.get()); + if (menu != mainMenu.get() && menu != finishedTrackMenu.get() && menu != nullptr) { + micro->gameCanvas->addCommand(backCommand.get()); } - if (menu == highscoreMenu) { + if (menu == highscoreMenu.get()) { highscoreLeagueViewIndex = leagueSetting->getCurrentOptionPos(); refreshHighscoreDisplay(highscoreLeagueViewIndex); - } else if (menu == finishedTrackMenu) { - playerName = enterNameMenu->getStrArr(); - nameEntrySetting->setText("Name - " + std::string(playerName)); - } else if (menu == playMenu) { - trackSetting->setOptionsList(micro->levelLoader->trackNames[levelSetting->getCurrentOptionPos()]); - if (currentGameMenu == trackSelectionParentMenu) { + } else if (menu == finishedTrackMenu.get()) { + char* newName = enterNameMenu->getStrArr(); + for (int i = 0; i < 3; ++i) { + playerName[i] = newName[i]; + } + playerName[3] = '\0'; + nameEntrySetting->setText("Name - " + std::string(playerName.data())); + } else if (menu == playMenu.get()) { + // Save track selection if coming from track selection menu + if (currentGameMenu == trackSetting->getCurrentMenu()) { lastSelectedTrackPerLevel[levelSetting->getCurrentOptionPos()] = trackSetting->getCurrentOptionPos(); } + trackSetting->setOptionsList(micro->levelLoader->trackNames[levelSetting->getCurrentOptionPos()]); trackSetting->setAvailableOptions(maxUnlockedTracksPerLevel[levelSetting->getCurrentOptionPos()]); trackSetting->setCurentOptionPos(lastSelectedTrackPerLevel[levelSetting->getCurrentOptionPos()]); } - if (menu == mainMenu || menu == playMenu) { + if (menu == mainMenu.get() || menu == playMenu.get()) { micro->gamePhysics->enableGenerateInputAI(); } @@ -703,14 +728,18 @@ void MenuManager::switchToMenu(GameMenu* menu, bool skipSelectionReset) void MenuManager::refreshHighscoreDisplay(int leagueIndex) { highscoreMenu->clearVector(); + highscoreMenuTextRenders.clear(); recordManager->openRecordStore(levelSetting->getCurrentOptionPos(), trackSetting->getCurrentOptionPos()); - highscoreMenu->addMenuElement(new TextRender(micro->levelLoader->getName(levelSetting->getCurrentOptionPos(), trackSetting->getCurrentOptionPos()), micro)); - highscoreMenu->addMenuElement(new TextRender("LEAGUE: " + leagueSetting->getOptionsList()[leagueIndex], micro)); + highscoreMenuTextRenders.push_back(std::make_unique(micro->levelLoader->getName(levelSetting->getCurrentOptionPos(), trackSetting->getCurrentOptionPos()), micro)); + highscoreMenu->addMenuElement(highscoreMenuTextRenders.back().get()); + highscoreMenuTextRenders.push_back(std::make_unique("LEAGUE: " + leagueSetting->getOptionsList()[leagueIndex], micro)); + highscoreMenu->addMenuElement(highscoreMenuTextRenders.back().get()); std::vector recordDescriptions = recordManager->getRecordDescription(leagueIndex); for (std::size_t i = 0; i < recordDescriptions.size(); ++i) { if (recordDescriptions[i] != "") { - TextRender* recordText = new TextRender(std::to_string(i + 1) + "." + recordDescriptions[i], micro); + highscoreMenuTextRenders.push_back(std::make_unique(std::to_string(i + 1) + "." + recordDescriptions[i], micro)); + TextRender* recordText = highscoreMenuTextRenders.back().get(); recordText->setDx(GameCanvas::spriteSizeX[5] + 1); if (i == 0) { recordText->setDrawSprite(true, 5); @@ -726,10 +755,11 @@ void MenuManager::refreshHighscoreDisplay(int leagueIndex) recordManager->closeRecordStore(); if (recordDescriptions[0] == "") { - highscoreMenu->addMenuElement(new TextRender("No Highscores", micro)); + highscoreMenuTextRenders.push_back(std::make_unique("No Highscores", micro)); + highscoreMenu->addMenuElement(highscoreMenuTextRenders.back().get()); } - highscoreMenu->addMenuElement(backSetting); + highscoreMenu->addMenuElement(backSetting.get()); } /* synchronized */ void MenuManager::saveStateAndCloseRecordStore() @@ -750,7 +780,7 @@ void MenuManager::refreshHighscoreDisplay(int leagueIndex) void MenuManager::saveSettingsToBuffer() { // Copy player name to buffer (indices 16-18) - savePlayerNameToBuffer(16, playerName); + savePlayerNameToBuffer(16, playerName.data()); // Save all settings to the 19-byte buffer setSavedStateValue(0, (int8_t)perspectiveSetting->getCurrentOptionPos()); @@ -805,7 +835,7 @@ void MenuManager::showAlert(const std::string& title, const std::string& message void MenuManager::handleMenuSelection(IGameMenuElement* element) { - if (element == startTask) { + if (element == startTask.get()) { if (levelSetting->getCurrentOptionPos() <= levelSetting->getMaxAvailableOptionPos() && trackSetting->getCurrentOptionPos() <= trackSetting->getMaxAvailableOptionPos() && leagueSetting->getCurrentOptionPos() <= leagueSetting->getMaxAvailableOptionPos()) { micro->gamePhysics->disableGenerateInputAI(); micro->levelLoader->loadTrack(levelSetting->getCurrentOptionPos(), trackSetting->getCurrentOptionPos()); @@ -815,24 +845,24 @@ void MenuManager::handleMenuSelection(IGameMenuElement* element) } else { showAlert("GDTR", "Complete more tracks to unlock this track/league combo.", nullptr); } - } else if (element == perspectiveSetting) { + } else if (element == perspectiveSetting.get()) { micro->gamePhysics->invertYPositions(perspectiveSetting->getCurrentOptionPos() == 0); LevelLoader::isEnabledPerspective = perspectiveSetting->getCurrentOptionPos() == 0; - } else if (element == shadowsSetting) { + } else if (element == shadowsSetting.get()) { LevelLoader::isEnabledShadows = shadowsSetting->getCurrentOptionPos() == 0; } else { - if (element == driverSpriteSetting) { + if (element == driverSpriteSetting.get()) { if (driverSpriteSetting->checkAndClearSelectionFlag()) { driverSpriteSetting->setCurentOptionPos(driverSpriteSetting->getCurrentOptionPos() + 1); return; } - } else if (element == bikeSpriteSetting) { + } else if (element == bikeSpriteSetting.get()) { if (bikeSpriteSetting->checkAndClearSelectionFlag()) { bikeSpriteSetting->setCurentOptionPos(bikeSpriteSetting->getCurrentOptionPos() + 1); return; } } else { - if (element == inputSetting) { + if (element == inputSetting.get()) { if (inputSetting->checkAndClearSelectionFlag()) { inputSetting->setCurentOptionPos(inputSetting->getCurrentOptionPos() + 1); } @@ -841,16 +871,16 @@ void MenuManager::handleMenuSelection(IGameMenuElement* element) return; } - if (element == lookAheadSetting) { + if (element == lookAheadSetting.get()) { micro->gamePhysics->setEnableLookAhead(lookAheadSetting->getCurrentOptionPos() == 0); return; } - if (element == confirmYesSetting) { - if (currentGameMenu == confirmClearHighscoresMenu) { + if (element == confirmYesSetting.get()) { + if (currentGameMenu == confirmClearHighscoresMenu.get()) { recordManager->deleteRecordStores(); showAlert("Cleared", "Highscores have been cleared", nullptr); - } else if (currentGameMenu == confirmFullResetMenu) { + } else if (currentGameMenu == confirmFullResetMenu.get()) { performFullReset(); showAlert("Reset", "Master reset. Application will be closed.", nullptr); } @@ -859,17 +889,17 @@ void MenuManager::handleMenuSelection(IGameMenuElement* element) return; } - if (element == confirmNoSetting) { + if (element == confirmNoSetting.get()) { switchToMenu(currentGameMenu->getGameMenu(), false); return; } - if (element == backSetting) { + if (element == backSetting.get()) { switchToMenu(currentGameMenu->getGameMenu(), true); return; } - if (element == playMenuSetting) { + if (element == playMenuSetting.get()) { levelSetting->setCurentOptionPos(savedLevelBeforeMenu); trackSetting->setAvailableOptions(maxUnlockedTracksPerLevel[savedLevelBeforeMenu]); trackSetting->setCurentOptionPos(savedTrackBeforeMenu); @@ -877,17 +907,19 @@ void MenuManager::handleMenuSelection(IGameMenuElement* element) return; } - if (element == goToMainSetting) { - switchToMenu(mainMenu, false); + if (element == goToMainSetting.get()) { + switchToMenu(mainMenu.get(), false); return; } - if (element == exitGameSetting) { + if (element == exitGameSetting.get()) { + // User wants to exit the game - signal the game loop to stop + Micro::isGameLoopRunning = false; switchToMenu(currentGameMenu->getGameMenu(), false); return; } - if (element == restartTrackSetting) { + if (element == restartTrackSetting.get()) { if (leagueSetting->getCurrentOptionPos() <= leagueSetting->getMaxAvailableOptionPos()) { levelSetting->setCurentOptionPos(savedLevelBeforeMenu); trackSetting->setAvailableOptions(maxUnlockedTracksPerLevel[savedLevelBeforeMenu]); @@ -898,7 +930,7 @@ void MenuManager::handleMenuSelection(IGameMenuElement* element) return; } } else { - if (element == nextTrackSetting) { + if (element == nextTrackSetting.get()) { if (!isAllTracksCompletedAtLevel) { trackSetting->menuElemMethod(2); } @@ -911,41 +943,41 @@ void MenuManager::handleMenuSelection(IGameMenuElement* element) return; } - if (element == continueSetting) { + if (element == continueSetting.get()) { requestRepaint(); micro->menuToGame(); return; } - if (element == nameEntrySetting) { + if (element == nameEntrySetting.get()) { enterNameMenu->resetToFirstItem(); - switchToMenu(enterNameMenu, false); + switchToMenu(enterNameMenu.get(), false); return; } - if (element == okSetting) { + if (element == okSetting.get()) { saveHighscoreAndShowResults(); return; } - if (element == trackSetting) { + if (element == trackSetting.get()) { if (trackSetting->checkAndClearSelectionFlag()) { trackSetting->setAvailableOptions(maxUnlockedTracksPerLevel[levelSetting->getCurrentOptionPos()]); trackSetting->init(); trackSelectionParentMenu = trackSetting->getParentGameMenu(); - switchToMenu(trackSelectionParentMenu, false); - trackSelectionParentMenu->navigateToItem(trackSetting->getCurrentOptionPos()); + switchToMenu(trackSetting->getCurrentMenu(), false); + trackSetting->getCurrentMenu()->navigateToItem(trackSetting->getCurrentOptionPos()); } lastSelectedTrackPerLevel[levelSetting->getCurrentOptionPos()] = trackSetting->getCurrentOptionPos(); return; } - if (element == levelSetting) { + if (element == levelSetting.get()) { if (levelSetting->checkAndClearSelectionFlag()) { levelSelectionMenu = levelSetting->getParentGameMenu(); - switchToMenu(levelSelectionMenu, false); - levelSelectionMenu->navigateToItem(levelSetting->getCurrentOptionPos()); + switchToMenu(levelSetting->getCurrentMenu(), false); + levelSetting->getCurrentMenu()->navigateToItem(levelSetting->getCurrentOptionPos()); } trackSetting->setOptionsList(micro->levelLoader->trackNames[levelSetting->getCurrentOptionPos()]); @@ -955,11 +987,11 @@ void MenuManager::handleMenuSelection(IGameMenuElement* element) return; } - if (element == leagueSetting && leagueSetting->checkAndClearSelectionFlag()) { + if (element == leagueSetting.get() && leagueSetting->checkAndClearSelectionFlag()) { leagueSelectionMenu = leagueSetting->getParentGameMenu(); leagueSetting->setParentGameMenu(currentGameMenu); - switchToMenu(leagueSelectionMenu, false); - leagueSelectionMenu->navigateToItem(leagueSetting->getCurrentOptionPos()); + switchToMenu(leagueSetting->getCurrentMenu(), false); + leagueSetting->getCurrentMenu()->navigateToItem(leagueSetting->getCurrentOptionPos()); } } } @@ -1113,17 +1145,17 @@ void MenuManager::performFullReset() void MenuManager::removeCommands() { - micro->gameCanvas->removeCommand(okCommand); - micro->gameCanvas->removeCommand(backCommand); + micro->gameCanvas->removeCommand(okCommand.get()); + micro->gameCanvas->removeCommand(backCommand.get()); } void MenuManager::addCommands() { - if (currentGameMenu != mainMenu && currentGameMenu != finishedTrackMenu && currentGameMenu != nullptr) { - micro->gameCanvas->addCommand(backCommand); + if (currentGameMenu != mainMenu.get() && currentGameMenu != finishedTrackMenu.get() && currentGameMenu != nullptr) { + micro->gameCanvas->addCommand(backCommand.get()); } - micro->gameCanvas->addCommand(okCommand); + micro->gameCanvas->addCommand(okCommand.get()); } int MenuManager::countRecordStoresForLevel(int levelIndex) diff --git a/src/MenuManager.h b/src/MenuManager.h index ad166d3..38e6fcc 100644 --- a/src/MenuManager.h +++ b/src/MenuManager.h @@ -3,8 +3,11 @@ #include #include #include +#include #include "IMenuManager.h" +#include "TextRender.h" +#include "SettingsStringRender.h" class Micro; class RecordManager; @@ -14,7 +17,6 @@ class PhysicsElemOrMenuItem; class SettingsStringRender; class RecordStore; class Image; -class TextRender; class Graphics; class Displayable; class IGameMenuElement; @@ -23,69 +25,75 @@ class MenuManager : public IMenuManager { private: // Saved state buffer (19 bytes) - see initPart case 2 for layout std::vector savedStateBuffer; - Micro* micro; - RecordManager* recordManager; - Command* okCommand; - Command* backCommand; + Micro* micro; // Non-owning reference + std::unique_ptr recordManager; // Owned by MenuManager + std::unique_ptr okCommand; + std::unique_ptr backCommand; // Main menu screens - GameMenu* mainMenu; - GameMenu* playMenu; - GameMenu* optionsMenu; - GameMenu* aboutMenu; - GameMenu* helpMenu; - GameMenu* confirmClearHighscoresMenu; - GameMenu* confirmFullResetMenu; - GameMenu* finishedTrackMenu; - GameMenu* ingameMenu; + std::unique_ptr mainMenu; + std::unique_ptr playMenu; + std::unique_ptr optionsMenu; + std::unique_ptr aboutMenu; + std::unique_ptr helpMenu; + std::unique_ptr confirmClearHighscoresMenu; + std::unique_ptr confirmFullResetMenu; + std::unique_ptr finishedTrackMenu; + std::unique_ptr ingameMenu; // Menu navigation elements - PhysicsElemOrMenuItem* playMenuTask; - PhysicsElemOrMenuItem* optionsTask; - PhysicsElemOrMenuItem* helpTask; - SettingsStringRender* levelSetting; - GameMenu* levelSelectionMenu; - SettingsStringRender* trackSetting; + std::unique_ptr playMenuTask; + std::unique_ptr optionsTask; + std::unique_ptr helpTask; + std::unique_ptr levelSetting; + GameMenu* levelSelectionMenu; // Non-owning reference (temporary) + std::unique_ptr trackSetting; // Parent menu when track selection is active - GameMenu* trackSelectionParentMenu; - SettingsStringRender* leagueSetting; - GameMenu* leagueSelectionMenu; - GameMenu* highscoreMenu; - PhysicsElemOrMenuItem* highscoreTask; - SettingsStringRender* startTask; - SettingsStringRender* perspectiveSetting; - SettingsStringRender* shadowsSetting; - SettingsStringRender* driverSpriteSetting; - SettingsStringRender* bikeSpriteSetting; - SettingsStringRender* inputSetting; - SettingsStringRender* lookAheadSetting; - PhysicsElemOrMenuItem* clearHighscoreTask; - PhysicsElemOrMenuItem* fullResetTask; - SettingsStringRender* confirmYesSetting; - SettingsStringRender* confirmNoSetting; - PhysicsElemOrMenuItem* aboutTask; - GameMenu* helpObjectiveMenu; - PhysicsElemOrMenuItem* helpObjectiveTask; - GameMenu* helpKeysMenu; - PhysicsElemOrMenuItem* helpKeysTask; - GameMenu* helpUnlockingMenu; - PhysicsElemOrMenuItem* helpUnlockingTask; - GameMenu* helpHighscoreDescriptionMenu; - PhysicsElemOrMenuItem* helpHighscoreTask; - GameMenu* helpOptionsDescriptionMenu; - PhysicsElemOrMenuItem* helpOptionsTask; - GameMenu* enterNameMenu; - SettingsStringRender* backSetting; - SettingsStringRender* playMenuSetting; - SettingsStringRender* continueSetting; - SettingsStringRender* goToMainSetting; - SettingsStringRender* exitGameSetting; + GameMenu* trackSelectionParentMenu; // Non-owning reference + std::unique_ptr leagueSetting; + GameMenu* leagueSelectionMenu; // Non-owning reference (temporary) + std::unique_ptr highscoreMenu; + std::unique_ptr highscoreTask; + std::unique_ptr startTask; + std::unique_ptr perspectiveSetting; + std::unique_ptr shadowsSetting; + std::unique_ptr driverSpriteSetting; + std::unique_ptr bikeSpriteSetting; + std::unique_ptr inputSetting; + std::unique_ptr lookAheadSetting; + std::unique_ptr clearHighscoreTask; + std::unique_ptr fullResetTask; + std::unique_ptr confirmYesSetting; + std::unique_ptr confirmNoSetting; + std::unique_ptr aboutTask; + std::unique_ptr helpObjectiveMenu; + std::unique_ptr helpObjectiveTask; + std::unique_ptr helpKeysMenu; + std::unique_ptr helpKeysTask; + std::unique_ptr helpUnlockingMenu; + std::unique_ptr helpUnlockingTask; + std::unique_ptr helpHighscoreDescriptionMenu; + std::unique_ptr helpHighscoreTask; + std::unique_ptr helpOptionsDescriptionMenu; + std::unique_ptr helpOptionsTask; + std::unique_ptr enterNameMenu; + std::unique_ptr backSetting; + std::unique_ptr playMenuSetting; + std::unique_ptr continueSetting; + std::unique_ptr goToMainSetting; + std::unique_ptr exitGameSetting; // "Restart: [track name]" - SettingsStringRender* restartTrackSetting; + std::unique_ptr restartTrackSetting; // "Next: [track name]" - SettingsStringRender* nextTrackSetting; + std::unique_ptr nextTrackSetting; // "Ok" for highscore entry - SettingsStringRender* okSetting; + std::unique_ptr okSetting; // "Name - [player name]" - SettingsStringRender* nameEntrySetting; + std::unique_ptr nameEntrySetting; + // Storage for multiline text renders (owned by MenuManager) + std::vector> multilineTextRenders; + // Storage for finished track menu text renders (owned by MenuManager) + std::vector> finishedTrackMenuTextRenders; + // Storage for highscore menu text renders (owned by MenuManager) + std::vector> highscoreMenuTextRenders; int64_t finishTime; // For display formatting int finishTimeSeconds; @@ -94,10 +102,10 @@ class MenuManager : public IMenuManager { // Formatted time string (MM:SS.cc) std::string finishTimeFormatted; // 3-character player name for highscores - char* playerName; + std::array playerName; // Array[3]: max unlocked track per level char maxUnlockedTracksPerLevel[4]; - char defaultPlayerName[4] = "AAA"; + std::array defaultPlayerName = { 'A', 'A', 'A', '\0' }; // Number of unlocked leagues (0-3) int8_t maxAvailableLeagues = 0; // Number of unlocked levels (1-3) @@ -110,6 +118,7 @@ class MenuManager : public IMenuManager { std::vector leagueNames = std::vector(3); // All 4 league names (including 325cc) std::vector allLeagueNames; + // Non-owning reference (owned by RecordStore::opened static map) RecordStore* recordStore; // Record ID for saved state (typo in original: "recorc") int savedStateRecordId = -1; @@ -117,7 +126,7 @@ class MenuManager : public IMenuManager { // raster.png std::unique_ptr menuBackgroundImage; // Clickable link to codebrew.se - TextRender* codebrewLinkText; + std::unique_ptr codebrewLinkText; int savedLevelBeforeMenu = 0; int savedTrackBeforeMenu = 0; bool isAllTracksCompletedAtLevel = false; @@ -158,13 +167,14 @@ class MenuManager : public IMenuManager { int countRecordStoresForLevel(int levelIndex); public: - GameMenu* currentGameMenu; + GameMenu* currentGameMenu; // Non-owning pointer to currently active menu // Currently viewed league in highscore screen int highscoreLeagueViewIndex = 0; // Prevents menu rendering over game bool isMenuRenderingBlocked = false; MenuManager(Micro* micro); + ~MenuManager(); // Initialize in phases 1-7 void initializePhase(int phase); // Returns and clears shouldStartRaceImmediately diff --git a/src/Micro.cpp b/src/Micro.cpp index 7ce2e4a..6b85a0d 100644 --- a/src/Micro.cpp +++ b/src/Micro.cpp @@ -11,12 +11,9 @@ bool Micro::isGameLoopRunning = false; int Micro::gameLoadingStateStage = 0; -Micro::Micro() -{ -} - Micro::~Micro() { + // destroy unique_ptr members } void Micro::setNumPhysicsLoops(int value) @@ -45,14 +42,14 @@ int64_t Micro::goLoadingStep() int64_t startTimeMillis = Time::currentTimeMillis(); switch (gameLoadingStateStage) { case 1: - levelLoader = new LevelLoader(mrgFilePath); + levelLoader = std::make_unique(mrgFilePath); break; case 2: - gamePhysics = new GamePhysics(levelLoader); - gameCanvas->init(gamePhysics); + gamePhysics = std::make_unique(levelLoader.get()); + gameCanvas->init(gamePhysics.get()); break; case 3: - menuManager = new MenuManager(this); + menuManager = std::make_unique(this); menuManager->initializePhase(1); break; case 4: @@ -74,7 +71,7 @@ int64_t Micro::goLoadingStep() menuManager->initializePhase(7); break; case 10: - gameCanvas->setMenuManager(menuManager); + gameCanvas->setMenuManager(menuManager.get()); gameCanvas->setViewPosition(-50, 150); setMode(1); break; @@ -95,7 +92,7 @@ void Micro::init() { int64_t timeToLoading = 3000L; // Thread.yield(); - gameCanvas = new GameCanvas(this); + gameCanvas = std::make_unique(this); gameCanvas->requestRepaint(1); while (!gameCanvas->isShown()) { @@ -173,7 +170,7 @@ void Micro::run() init(); } - gameCanvas->setCommandListener(gameCanvas); + gameCanvas->setCommandListener(gameCanvas.get()); restart(false); menuManager->runMenuLoop(0); if (menuManager->consumeShouldStartRaceFlag()) { diff --git a/src/Micro.h b/src/Micro.h index 76a25d1..f096c3a 100644 --- a/src/Micro.h +++ b/src/Micro.h @@ -1,6 +1,7 @@ #pragma once #include #include +#include #include class GameCanvas; @@ -15,10 +16,10 @@ class Micro { std::filesystem::path mrgFilePath; public: - GameCanvas* gameCanvas; - LevelLoader* levelLoader; - GamePhysics* gamePhysics; - MenuManager* menuManager; + std::unique_ptr gameCanvas; + std::unique_ptr levelLoader; + std::unique_ptr gamePhysics; + std::unique_ptr menuManager; bool isAboutToExit = false; int numPhysicsLoops = 2; int64_t timeMs = 0; @@ -30,7 +31,6 @@ class Micro { inline static bool isInGameMenu; static int gameLoadingStateStage; - Micro(); ~Micro(); void startApp(int argc, char** argv); diff --git a/src/RecordManager.h b/src/RecordManager.h index deb2fed..bbc88d1 100644 --- a/src/RecordManager.h +++ b/src/RecordManager.h @@ -30,6 +30,7 @@ class RecordManager { // 4: league, 100, 175, 225, 350, // 3: three best times char recordName[LEAGUES_MAX][RECORD_NO_MAX][PLAYER_NAME_MAX + 1]; + // Non-owning reference (owned by RecordStore::opened static map) RecordStore* recordStore = nullptr; int packedRecordInfoRecordId = -1; std::vector packedRecordInfo = std::vector(96); diff --git a/src/SettingsStringRender.cpp b/src/SettingsStringRender.cpp index 2ba0381..1b012b0 100644 --- a/src/SettingsStringRender.cpp +++ b/src/SettingsStringRender.cpp @@ -64,18 +64,21 @@ void SettingsStringRender::setOptionsList(std::vector options) void SettingsStringRender::init() { - currentGameMenu = new GameMenu(text, micro, parentGameMenu); - settingsStringRenders = std::vector(optionsList.size()); + currentGameMenu = std::make_unique(text, micro, parentGameMenu); + settingsStringRenders.clear(); + settingsStringRenders.reserve(optionsList.size()); - for (int i = 0; i < static_cast(settingsStringRenders.size()); ++i) { + for (int i = 0; i < static_cast(optionsList.size()); ++i) { if (i > maxAvailableOption) { - settingsStringRenders[i] = new SettingsStringRender(optionsList[i], 0, this, std::vector(), false, micro, parentGameMenu, true); - settingsStringRenders[i]->setFlags(true, true); + auto child = std::make_unique(optionsList[i], 0, this, std::vector(), false, micro, parentGameMenu, true); + child->setFlags(true, true); + currentGameMenu->addMenuElement(child.get()); + settingsStringRenders.push_back(std::move(child)); } else { - settingsStringRenders[i] = new SettingsStringRender(optionsList[i], 0, this, std::vector(), false, micro, parentGameMenu, true); + auto child = std::make_unique(optionsList[i], 0, this, std::vector(), false, micro, parentGameMenu, true); + currentGameMenu->addMenuElement(child.get()); + settingsStringRenders.push_back(std::move(child)); } - - currentGameMenu->addMenuElement(settingsStringRenders[i]); } } @@ -256,7 +259,7 @@ int SettingsStringRender::getCurrentOptionPos() GameMenu* SettingsStringRender::getCurrentMenu() { - return currentGameMenu; + return currentGameMenu.get(); } GameMenu* SettingsStringRender::getParentGameMenu() @@ -277,7 +280,7 @@ void SettingsStringRender::saveStateAndCloseRecordStore() void SettingsStringRender::handleMenuSelection(IGameMenuElement* element) { for (int i = 0; i < static_cast(settingsStringRenders.size()); ++i) { - if (element == settingsStringRenders[i]) { + if (element == settingsStringRenders[i].get()) { currentOptionPos = i; selectCurrentOptionName(); break; @@ -290,7 +293,12 @@ void SettingsStringRender::handleMenuSelection(IGameMenuElement* element) std::vector SettingsStringRender::getSettingsStringRenders() { - return settingsStringRenders; + std::vector result; + result.reserve(settingsStringRenders.size()); + for (const auto& render : settingsStringRenders) { + result.push_back(render.get()); + } + return result; } bool SettingsStringRender::checkAndClearSelectionFlag() diff --git a/src/SettingsStringRender.h b/src/SettingsStringRender.h index 2f63ee1..cd6a86a 100644 --- a/src/SettingsStringRender.h +++ b/src/SettingsStringRender.h @@ -2,6 +2,7 @@ #include #include +#include #include "IMenuManager.h" #include "IGameMenuElement.h" @@ -16,16 +17,16 @@ class SettingsStringRender : public IMenuManager, public IGameMenuElement { int currentOptionPos; int maxAvailableOption; std::string text; - IMenuManager* menuManager; - GameMenu* currentGameMenu = nullptr; - GameMenu* parentGameMenu = nullptr; + IMenuManager* menuManager; // Non-owning reference (parent manager) + std::unique_ptr currentGameMenu; // Owned by SettingsStringRender + GameMenu* parentGameMenu = nullptr; // Non-owning reference (owned by MenuManager) // Is On/Off toggle mode (vs multi-option) bool isToggleMode; // Selection confirmed flag bool isSelectionConfirmed = false; std::string selectedOptionName; - Micro* micro = nullptr; - std::vector settingsStringRenders; + Micro* micro = nullptr; // Non-owning reference (owned by root) + std::vector> settingsStringRenders; // Owned by this SettingsStringRender bool hasSprite; bool isDrawSprite8; bool useColon; @@ -53,7 +54,7 @@ class SettingsStringRender : public IMenuManager, public IGameMenuElement { void switchToMenu(GameMenu* menu, bool skipSelectionReset); void saveStateAndCloseRecordStore(); void handleMenuSelection(IGameMenuElement* element); - std::vector getSettingsStringRenders(); + std::vector getSettingsStringRenders(); // Returns non-owning raw pointers for iteration // Returns true if selection was confirmed, then clears flag bool checkAndClearSelectionFlag(); }; diff --git a/src/TextRender.cpp b/src/TextRender.cpp index d8f38bf..b4c7891 100644 --- a/src/TextRender.cpp +++ b/src/TextRender.cpp @@ -66,13 +66,13 @@ void TextRender::render(Graphics* graphics, int y, int x) graphics->setFont(preservedFont); } -std::vector TextRender::makeMultilineTextRenders(std::string text, Micro* micro) +std::vector> TextRender::makeMultilineTextRenders(std::string text, Micro* micro) { std::size_t startPos = 0; std::size_t endPos = 0; int8_t padding = 25; - std::vector vector; + std::vector> vector; for (; endPos < text.length(); startPos = ++endPos - 1) { std::size_t spacePos; if ((spacePos = text.find(" ", startPos)) == std::string::npos) { @@ -89,7 +89,7 @@ std::vector TextRender::makeMultilineTextRenders(std::string text, } } - vector.push_back(new TextRender(text.substr(startPos, endPos - startPos), micro)); + vector.push_back(std::make_unique(text.substr(startPos, endPos - startPos), micro)); } return vector; diff --git a/src/TextRender.h b/src/TextRender.h index 9b678df..29cac72 100644 --- a/src/TextRender.h +++ b/src/TextRender.h @@ -32,7 +32,7 @@ class TextRender : public IGameMenuElement { bool isNotTextRender(); void menuElemMethod(int index); void render(Graphics* graphics, int y, int x); - static std::vector makeMultilineTextRenders(std::string text, Micro* micro); + static std::vector> makeMultilineTextRenders(std::string text, Micro* micro); void setDx(int dx); void setDrawSprite(bool isDrawSprite, int spriteNo); }; diff --git a/src/lcdui/Canvas.cpp b/src/lcdui/Canvas.cpp index db42151..fe73c71 100644 --- a/src/lcdui/Canvas.cpp +++ b/src/lcdui/Canvas.cpp @@ -103,4 +103,9 @@ void Canvas::pressedEsc() return; } } +} + +void Canvas::requestExit() +{ + // Default implementation - subclasses can override } \ No newline at end of file diff --git a/src/lcdui/Canvas.h b/src/lcdui/Canvas.h index 3ec7ec1..4363bf7 100644 --- a/src/lcdui/Canvas.h +++ b/src/lcdui/Canvas.h @@ -43,6 +43,7 @@ class Canvas : public Displayable { void publicKeyPressed(int keyCode); void publicKeyReleased(int keyCode); void pressedEsc(); + virtual void requestExit() = 0; virtual void paint(Graphics* g) = 0; virtual void keyPressed(int keyCode) = 0; virtual void keyReleased(int keyCode) = 0; diff --git a/src/lcdui/CanvasImpl.cpp b/src/lcdui/CanvasImpl.cpp index 7f1c807..cd5847e 100644 --- a/src/lcdui/CanvasImpl.cpp +++ b/src/lcdui/CanvasImpl.cpp @@ -7,6 +7,7 @@ #include #include "Canvas.h" +#include "../utils/SDLWrappers.h" CanvasImpl::CanvasImpl(Canvas* canvas) { @@ -24,40 +25,30 @@ CanvasImpl::CanvasImpl(Canvas* canvas) throw std::runtime_error(TTF_GetError()); } - window = SDL_CreateWindow( + window = SDLWindow( 0, SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, width, height, SDL_WINDOW_SHOWN); - if (!window) { - throw std::runtime_error(SDL_GetError()); - } - - renderer = SDL_CreateRenderer( - window, -1, SDL_RENDERER_ACCELERATED); - - if (!renderer) { - throw std::runtime_error(SDL_GetError()); - } + renderer = SDLRenderer( + window.get(), -1, SDL_RENDERER_ACCELERATED); - SDL_SetRenderDrawColor(renderer, 255, 255, 255, 255); - SDL_RenderClear(renderer); + SDL_SetRenderDrawColor(renderer.get(), 255, 255, 255, 255); + SDL_RenderClear(renderer.get()); } CanvasImpl::~CanvasImpl() { - SDL_DestroyRenderer(renderer); - SDL_DestroyWindow(window); + // RAII handles cleanup: SDLRenderer and SDLWindow destructors will be called automatically SDL_Quit(); IMG_Quit(); - TTF_Quit(); } void CanvasImpl::repaint() { - SDL_RenderPresent(renderer); + SDL_RenderPresent(renderer.get()); } int CanvasImpl::getWidth() @@ -72,7 +63,7 @@ int CanvasImpl::getHeight() SDL_Renderer* CanvasImpl::getRenderer() { - return renderer; + return renderer.get(); } void CanvasImpl::processEvents() @@ -82,7 +73,9 @@ void CanvasImpl::processEvents() while (SDL_PollEvent(&e) != 0) { switch (e.type) { case SDL_QUIT: - exit(0); // IMPROVE This is a super dumb way to finish the game, but it works + // Signal the game to exit naturally instead of calling exit() + // This ensures proper C++ destructor order (Micro -> Canvas -> CanvasImpl) + canvas->requestExit(); break; case SDL_KEYDOWN: { int keyCode = convertKeyCharToKeyCode(e.key.keysym.sym); @@ -131,5 +124,5 @@ int CanvasImpl::convertKeyCharToKeyCode(SDL_Keycode keyCode) void CanvasImpl::setWindowTitle(const std::string& title) { - SDL_SetWindowTitle(window, title.c_str()); + SDL_SetWindowTitle(window.get(), title.c_str()); } \ No newline at end of file diff --git a/src/lcdui/CanvasImpl.h b/src/lcdui/CanvasImpl.h index 90c4763..7f91728 100644 --- a/src/lcdui/CanvasImpl.h +++ b/src/lcdui/CanvasImpl.h @@ -6,14 +6,16 @@ #include #include +#include "../utils/SDLWrappers.h" + class Canvas; class CanvasImpl { private: Canvas* canvas; - SDL_Window* window; - SDL_Renderer* renderer; + SDLWindow window; + SDLRenderer renderer; const int width = 640; const int height = 480; diff --git a/src/lcdui/Font.cpp b/src/lcdui/Font.cpp index 547c4ee..a267790 100644 --- a/src/lcdui/Font.cpp +++ b/src/lcdui/Font.cpp @@ -1,24 +1,47 @@ #include "Font.h" #include +#include CMRC_DECLARE(assets); -Font::Font(FontStyle style, FontSize pointSize) +Font::FontResource::FontResource() { - if (!ttfRwOps) { - cmrc::embedded_filesystem internalFs = cmrc::assets::get_filesystem(); - cmrc::file fileData = internalFs.open("FontSansSerif.ttf"); - SDL_RWops* raw = SDL_RWFromConstMem(fileData.begin(), fileData.size()); - if (!raw) { - throw std::runtime_error(SDL_GetError()); - } + cmrc::embedded_filesystem internalFs = cmrc::assets::get_filesystem(); + cmrc::file fileData = internalFs.open("FontSansSerif.ttf"); + SDL_RWops* raw = SDL_RWFromConstMem(fileData.begin(), fileData.size()); + if (!raw) { + throw std::runtime_error(SDL_GetError()); + } + rwOps = raw; +} - ttfRwOps = raw; +Font::FontResource::~FontResource() +{ + if (rwOps) { + SDL_FreeRW(rwOps); + rwOps = nullptr; } +} + +std::shared_ptr& Font::getFontResource() +{ + // Thread-safe static local initialization (Meyers singleton) + static std::shared_ptr instance = std::make_shared(); + return instance; +} + +Font::Font(FontStyle style, FontSize pointSize) +{ + fontResource = getFontResource(); int realSize = getRealFontSize(pointSize); - TTF_Font* font = TTF_OpenFontRW(ttfRwOps, SDL_TRUE, realSize); + // Use SDL_FALSE so TTF_CloseFont doesn't free the RWops + // The RWops is managed by FontResource shared_ptr + TTF_Font* font = TTF_OpenFontRW(fontResource->get(), SDL_FALSE, realSize); + if (!font) { + throw std::runtime_error(TTF_GetError()); + } TTF_SetFontStyle(font, style); this->ttfFont = font; this->height = realSize; @@ -27,6 +50,9 @@ Font::Font(FontStyle style, FontSize pointSize) Font::~Font() { TTF_CloseFont(ttfFont); + // fontResource shared_ptr automatically manages cleanup + // When last Font is destroyed, fontResourceInstance will be freed + // and FontResource destructor will clean up rwOps } int Font::getBaselinePosition() const diff --git a/src/lcdui/Font.h b/src/lcdui/Font.h index 8d138ba..cab777a 100644 --- a/src/lcdui/Font.h +++ b/src/lcdui/Font.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include @@ -35,7 +36,23 @@ class Font { int substringWidth(const std::string& string, int offset, int len); private: - static inline SDL_RWops* ttfRwOps = nullptr; + // RAII wrapper for static SDL_RWops resource + struct FontResource { + SDL_RWops* rwOps = nullptr; + + FontResource(); + ~FontResource(); + + // Non-copyable + FontResource(const FontResource&) = delete; + FontResource& operator=(const FontResource&) = delete; + + SDL_RWops* get() const { return rwOps; } + }; + + static std::shared_ptr& getFontResource(); + + std::shared_ptr fontResource; // Shared ownership of static resource TTF_Font* ttfFont; int height; diff --git a/src/main.cpp b/src/main.cpp index ff84b9b..7301348 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -4,6 +4,10 @@ #include #include "Micro.h" +#include "GameCanvas.h" +#include "LevelLoader.h" +#include "GamePhysics.h" +#include "MenuManager.h" int main(int argc, char** argv) { diff --git a/src/utils/SDLWrappers.h b/src/utils/SDLWrappers.h new file mode 100644 index 0000000..293110c --- /dev/null +++ b/src/utils/SDLWrappers.h @@ -0,0 +1,115 @@ +#pragma once + +#include + +#include +#include +#include + +/** + * RAII wrapper for SDL_Window. + * Automatically destroys the window on destruction. + * Non-copyable, movable. + */ +class SDLWindow { +private: + SDL_Window* window = nullptr; + +public: + SDLWindow() = default; + + SDLWindow(const char* title, int x, int y, int w, int h, Uint32 flags) + : window(SDL_CreateWindow(title, x, y, w, h, flags)) + { + if (!window) { + throw std::runtime_error(SDL_GetError()); + } + } + + ~SDLWindow() + { + if (window) { + SDL_DestroyWindow(window); + } + } + + // Non-copyable + SDLWindow(const SDLWindow&) = delete; + SDLWindow& operator=(const SDLWindow&) = delete; + + // Movable + SDLWindow(SDLWindow&& other) noexcept + : window(other.window) + { + other.window = nullptr; + } + + SDLWindow& operator=(SDLWindow&& other) noexcept + { + if (this != &other) { + if (window) { + SDL_DestroyWindow(window); + } + window = other.window; + other.window = nullptr; + } + return *this; + } + + SDL_Window* get() const { return window; } + explicit operator bool() const { return window != nullptr; } +}; + +/** + * RAII wrapper for SDL_Renderer. + * Automatically destroys the renderer on destruction. + * Non-copyable, movable. + */ +class SDLRenderer { +private: + SDL_Renderer* renderer = nullptr; + +public: + SDLRenderer() = default; + + SDLRenderer(SDL_Window* window, int index, Uint32 flags) + : renderer(SDL_CreateRenderer(window, index, flags)) + { + if (!renderer) { + throw std::runtime_error(SDL_GetError()); + } + } + + ~SDLRenderer() + { + if (renderer) { + SDL_DestroyRenderer(renderer); + } + } + + // Non-copyable + SDLRenderer(const SDLRenderer&) = delete; + SDLRenderer& operator=(const SDLRenderer&) = delete; + + // Movable + SDLRenderer(SDLRenderer&& other) noexcept + : renderer(other.renderer) + { + other.renderer = nullptr; + } + + SDLRenderer& operator=(SDLRenderer&& other) noexcept + { + if (this != &other) { + if (renderer) { + SDL_DestroyRenderer(renderer); + } + renderer = other.renderer; + other.renderer = nullptr; + } + return *this; + } + + SDL_Renderer* get() const { return renderer; } + explicit operator bool() const { return renderer != nullptr; } +}; \ No newline at end of file diff --git a/src/utils/Time.cpp b/src/utils/Time.cpp index d8c2c4d..52dcbfe 100644 --- a/src/utils/Time.cpp +++ b/src/utils/Time.cpp @@ -7,7 +7,8 @@ namespace Time { int64_t currentTimeMillis() { return std::chrono::duration_cast( - std::chrono::system_clock::now().time_since_epoch()).count(); + std::chrono::system_clock::now().time_since_epoch()) + .count(); } void sleep(int64_t ms)