From 1a69378971f5d5dbf5099a6a3fc851fc684bb2d5 Mon Sep 17 00:00:00 2001 From: githubawn <115191165+githubawn@users.noreply.github.com> Date: Mon, 20 Apr 2026 16:11:02 +0200 Subject: [PATCH 01/18] SDL3 Input: Complete backport with Gamepad support and hardening Consolidated all work from the test/sdl3-backport branch into a single atomic commit: - Centralized input management via SDL3InputManager. - Hardened Ani/RIFF cursor loading with robust bounds checking. - Native Gamepad support with analogue stick-to-mouse emulation and custom RTS mappings. - Modernized focus and capture handling for better stability during Alt-Tab. - Standardized and secured string operations throughout the SDL3 path. --- CMakeLists.txt | 1 + Core/GameEngineDevice/CMakeLists.txt | 19 + .../Include/SDL3Device/GameClient/SDL3Input.h | 204 +++ .../GameEngineDevice/Include/SDL3GameEngine.h | 91 ++ .../SDL3Device/GameClient/SDL3Input.cpp | 1109 +++++++++++++++++ .../Source/SDL3GameEngine.cpp | 382 ++++++ .../GameEngine/Include/GameClient/Display.h | 5 + .../W3DDevice/GameClient/W3DGameClient.h | 30 +- GeneralsMD/Code/Main/WinMain.cpp | 94 +- cmake/config-build.cmake | 10 + cmake/sdl3.cmake | 95 ++ 11 files changed, 2031 insertions(+), 9 deletions(-) create mode 100644 Core/GameEngineDevice/Include/SDL3Device/GameClient/SDL3Input.h create mode 100644 Core/GameEngineDevice/Include/SDL3GameEngine.h create mode 100644 Core/GameEngineDevice/Source/SDL3Device/GameClient/SDL3Input.cpp create mode 100644 Core/GameEngineDevice/Source/SDL3GameEngine.cpp create mode 100644 cmake/sdl3.cmake diff --git a/CMakeLists.txt b/CMakeLists.txt index 28ce09560e9..0c7dfa916b0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -53,6 +53,7 @@ if((WIN32 OR "${CMAKE_SYSTEM}" MATCHES "Windows") AND ${CMAKE_SIZEOF_VOID_P} EQU include(cmake/miles.cmake) include(cmake/bink.cmake) include(cmake/dx8.cmake) + include(cmake/sdl3.cmake) endif() # Define a dummy stlport target when not on VC6. diff --git a/Core/GameEngineDevice/CMakeLists.txt b/Core/GameEngineDevice/CMakeLists.txt index 74b040200ae..a98ccadde9b 100644 --- a/Core/GameEngineDevice/CMakeLists.txt +++ b/Core/GameEngineDevice/CMakeLists.txt @@ -192,6 +192,16 @@ set(GAMEENGINEDEVICE_SRC Source/Win32Device/GameClient/Win32Mouse.cpp ) +# Add Core-level SDL3 implementation +if(SAGE_USE_SDL3 AND NOT IS_VS6_BUILD) + list(APPEND GAMEENGINEDEVICE_SRC + Include/SDL3GameEngine.h + Include/SDL3Device/GameClient/SDL3Input.h + Source/SDL3GameEngine.cpp + Source/SDL3Device/GameClient/SDL3Input.cpp + ) +endif() + # Add C++ 17 FileSystem implementation for non-VS6 builds if(NOT IS_VS6_BUILD) list(APPEND GAMEENGINEDEVICE_SRC @@ -227,6 +237,15 @@ target_link_libraries(corei_gameenginedevice_public INTERFACE milesstub ) +# Export SDL3 dependencies for modern builds +if(SAGE_USE_SDL3 AND NOT IS_VS6_BUILD) + target_link_libraries(corei_gameenginedevice_public INTERFACE + SDL3::Headers + SDL3::SDL3-static + SDL3_image::SDL3_image-static + ) +endif() + if(RTS_BUILD_OPTION_FFMPEG) find_package(FFMPEG REQUIRED) diff --git a/Core/GameEngineDevice/Include/SDL3Device/GameClient/SDL3Input.h b/Core/GameEngineDevice/Include/SDL3Device/GameClient/SDL3Input.h new file mode 100644 index 00000000000..29f8fdcf17f --- /dev/null +++ b/Core/GameEngineDevice/Include/SDL3Device/GameClient/SDL3Input.h @@ -0,0 +1,204 @@ +/* +** Command & Conquer Generals Zero Hour(tm) +** Copyright 2025 TheSuperHackers +** +** 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 3 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 . +*/ + +#pragma once + +#include "Lib/BaseType.h" + +// SYSTEM INCLUDES +#include +#include +#include +#include + +// USER INCLUDES +#include "GameClient/Mouse.h" +#include "GameClient/Keyboard.h" +#include "GameClient/KeyDefs.h" + +// FORWARD REFERENCES +struct AnimatedCursor; +class SDL3InputManager; + +// GLOBALS --------------------------------------------------------------------- +extern SDL3InputManager* TheSDL3InputManager; + +// TYPE DEFINES ---------------------------------------------------------------- +typedef KeyDefType KeyVal; + +// SDL3Mouse ------------------------------------------------------------------ +/** Mouse interface using SDL3 APIs */ +//----------------------------------------------------------------------------- +class SDL3Mouse : public Mouse +{ +public: + SDL3Mouse(SDL_Window* window); + virtual ~SDL3Mouse(void); + + // SubsystemInterface + virtual void init(void) override; + virtual void reset(void) override; + virtual void update(void) override; + virtual void initCursorResources(void) override; + + // Mouse interface + virtual void setCursor(MouseCursor cursor) override; + virtual void setVisibility(Bool visible) override; + virtual void loseFocus() override; + virtual void regainFocus() override; + + // SDL3-specific methods + void addSDLEvent(SDL_Event *event); + +protected: + virtual void capture(void) override; + virtual void releaseCapture(void) override; + virtual UnsignedByte getMouseEvent(MouseIO *result, Bool flush) override; + +private: + // Event translation from SDL_Event (Clean Slate implementation) + void translateEvent(const SDL_Event& event, MouseIO *result); + + // Scale raw SDL window coordinates to game internal resolution + void scaleMouseCoordinates(int rawX, int rawY, Uint32 windowID, int& scaledX, int& scaledY); + + // Load cursor from ANI file (fighter19 pattern) + AnimatedCursor* loadCursorFromFile(const char* filepath); + + SDL_Window* m_Window; + Bool m_IsCaptured; + Bool m_IsVisible; + Bool m_LostFocus; + + Uint32 m_LeftButtonDownTime; + Uint32 m_RightButtonDownTime; + Uint32 m_MiddleButtonDownTime; + UnsignedInt m_LastFrameNumber; + + ICoord2D m_LeftButtonDownPos; + ICoord2D m_RightButtonDownPos; + ICoord2D m_MiddleButtonDownPos; + + Int m_directionFrame; + UnsignedInt m_inputFrame; + + float m_accumulatedDeltaX; + float m_accumulatedDeltaY; + + SDL_Cursor* m_activeSDLCursor; + Bool m_cursorDirty; +}; + +// SDL3Keyboard --------------------------------------------------------------- +/** Keyboard interface using SDL3 APIs */ +//----------------------------------------------------------------------------- +class SDL3Keyboard : public Keyboard +{ +public: + SDL3Keyboard(void); + virtual ~SDL3Keyboard(void); + + // SubsystemInterface + virtual void init(void) override; + virtual void reset(void) override; + virtual void update(void) override; + + // Keyboard interface + virtual Bool getCapsState(void) override; + + // SDL3-specific methods + void addSDLEvent(SDL_Event *event); + +protected: + virtual void getKey(KeyboardIO *key) override; + virtual KeyVal translateScanCodeToKeyVal(unsigned char scan); + +private: + void translateKeyEvent(const SDL_KeyboardEvent& event); +}; + +// SDL3InputManager ----------------------------------------------------------- +/** Unified manager for SDL3 input events */ +//----------------------------------------------------------------------------- +class SDL3InputManager +{ +public: + SDL3InputManager(); + virtual ~SDL3InputManager(); + + void update(); + + // Buffer access + Bool getNextMouseEvent(SDL_Event& outEvent); + Bool getNextKeyboardEvent(SDL_Event& outEvent); + + void addMouseSDLEvent(const SDL_Event& event); + void addKeyboardSDLEvent(const SDL_Event& event); + + Bool isQuitting() const { return m_isQuitting; } + + // Constants + static constexpr float AXIS_MAX = 32767.0f; + static constexpr int TRIGGER_THRESHOLD = 16384; + static constexpr float DEFAULT_DEADZONE = 0.15f; + static constexpr float DEFAULT_CURSOR_SPEED = 800.0f; + +private: + struct GamepadState { + bool buttonState[SDL_GAMEPAD_BUTTON_COUNT]; + bool stickLeft, stickRight, stickUp, stickDown; + bool ltDown, rtDown; + + GamepadState() { + memset(buttonState, 0, sizeof(buttonState)); + stickLeft = stickRight = stickUp = stickDown = false; + ltDown = rtDown = false; + } + }; + +private: + // Gamepad management + void openFirstGamepad(); + void closeGamepad(); + void processGamepadInput(); + void handleGamepadButton(SDL_GamepadButton button, bool& currentState, bool isDown, std::function action); + + // Virtual event injection + void virtualPulseKey(SDL_Scancode scancode, bool down); + void virtualPulseMouse(Uint8 button, bool down); + + // Event buffers + static const UnsignedInt MAX_MOUSE_EVENTS = 256; + static const UnsignedInt MAX_KEY_EVENTS = 256; + + SDL_Event m_mouseEvents[MAX_MOUSE_EVENTS]; + UnsignedInt m_mouseNextFree; + UnsignedInt m_mouseNextGet; + + SDL_Event m_keyEvents[MAX_KEY_EVENTS]; + UnsignedInt m_keyNextFree; + UnsignedInt m_keyNextGet; + + // Gamepad state + SDL_Gamepad* m_gamepad; + GamepadState m_state; + + Bool m_precisionMode; + Uint64 m_lastUpdateTime; + Bool m_isQuitting; +}; diff --git a/Core/GameEngineDevice/Include/SDL3GameEngine.h b/Core/GameEngineDevice/Include/SDL3GameEngine.h new file mode 100644 index 00000000000..a85a6b575c2 --- /dev/null +++ b/Core/GameEngineDevice/Include/SDL3GameEngine.h @@ -0,0 +1,91 @@ +/* +** Command & Conquer Generals Zero Hour(tm) +** Copyright 2025 TheSuperHackers +** +** 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 3 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 . +*/ + +#pragma once + +#include "Lib/BaseType.h" + +#include "Common/GameEngine.h" +#include + +// EXTERNALS +// SDL3 window typically provided by WinMain integration +extern SDL_Window* TheSDL3Window; + +// Forward declarations for base classes +class AudioManager; +class Mouse; +class Keyboard; +class GameWindow; +class LocalFileSystem; +class ArchiveFileSystem; +class ThingFactory; +class ModuleFactory; +class FunctionLexicon; +class Radar; +class WebBrowser; +class ParticleSystemManager; + +/** + * SDL3GameEngine + * + * GameEngine subclass that uses SDL3 for windowing and input. + * Replaces or supplements Win32-specific window handling with SDL3. + */ +class SDL3GameEngine : public GameEngine +{ +public: + SDL3GameEngine(); + virtual ~SDL3GameEngine(); + + // GameEngine interface + virtual void init(void) override; + virtual void reset(void) override; + virtual void update(void) override; + virtual void serviceWindowsOS(void) override; + virtual Bool isActive(void) override; + virtual void setIsActive(Bool isActive) override; + + // Factory methods (override GameEngine) + virtual LocalFileSystem *createLocalFileSystem(void) override; + virtual ArchiveFileSystem *createArchiveFileSystem(void) override; + virtual GameLogic *createGameLogic(void) override; + virtual GameClient *createGameClient(void) override; + virtual ModuleFactory *createModuleFactory(void) override; + virtual ThingFactory *createThingFactory(void) override; + virtual FunctionLexicon *createFunctionLexicon(void) override; + virtual Radar *createRadar(Bool dummy) override; + virtual WebBrowser *createWebBrowser(void) override; + virtual ParticleSystemManager* createParticleSystemManager(Bool dummy) override; + virtual AudioManager *createAudioManager(Bool dummy) override; + + // SDL3 specific + virtual SDL_Window* getSDLWindow(void) const { return m_SDLWindow; } + virtual void forwardTextInputEvent(const char* utf8Text); + +protected: + SDL_Window* m_SDLWindow; + Bool m_IsInitialized; + Bool m_IsActive; + Bool m_IsTextInputActive; + GameWindow* m_TextInputFocusWindow; + + // Event processing + void pollSDL3Events(void); + void updateTextInputState(void); +}; diff --git a/Core/GameEngineDevice/Source/SDL3Device/GameClient/SDL3Input.cpp b/Core/GameEngineDevice/Source/SDL3Device/GameClient/SDL3Input.cpp new file mode 100644 index 00000000000..44291b80076 --- /dev/null +++ b/Core/GameEngineDevice/Source/SDL3Device/GameClient/SDL3Input.cpp @@ -0,0 +1,1109 @@ +/* +** Command & Conquer Generals Zero Hour(tm) +** Copyright 2025 TheSuperHackers +** +** 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 3 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 . +*/ + +#include "Lib/BaseType.h" + +#define _USE_MATH_DEFINES +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include // For timeGetTime() + +#include "SDL3Device/GameClient/SDL3Input.h" +#include "Common/Debug.h" +#include "Common/file.h" +#include "Common/FileSystem.h" +#include "Common/GameEngine.h" +#include "Common/MessageStream.h" +#include "GameClient/Display.h" +#include "GameClient/InGameUI.h" +#include "GameLogic/GameLogic.h" +#include "SDL3GameEngine.h" + +// GLOBALS --------------------------------------------------------------------- +SDL3InputManager* TheSDL3InputManager = nullptr; + +// ============================================================================ +// SDL3MOUSE IMPLEMENTATION +// ============================================================================ + +/** + * AnimatedCursor - Helper struct for cursor animation + */ +struct AnimatedCursor { + std::array m_frameCursors; + std::array m_frameSurfaces; + int m_currentFrame = 0; + int m_frameCount = 0; + int m_frameRate = 0; // the time a frame is displayed in 1/60th of a second + + AnimatedCursor() + { + m_frameCursors.fill(nullptr); + m_frameSurfaces.fill(nullptr); + } + + ~AnimatedCursor() + { + for (int i = 0; i < MAX_2D_CURSOR_ANIM_FRAMES; i++) + { + if (m_frameCursors[i]) + { + SDL_DestroyCursor(m_frameCursors[i]); + m_frameCursors[i] = nullptr; + } + if (m_frameSurfaces[i]) + { + SDL_DestroySurface(m_frameSurfaces[i]); + m_frameSurfaces[i] = nullptr; + } + } + } + + /** + * Get the active frame cursor based on current system time + */ + SDL_Cursor* getActiveFrame() const + { + if (m_frameCount <= 0) return nullptr; + if (m_frameCount == 1) return m_frameCursors[0]; + + Uint64 now = SDL_GetTicks(); + size_t index = (m_frameRate > 0) + ? (size_t)((now * 60 / 1000) / m_frameRate) % m_frameCount + : 0; + return m_frameCursors[index]; + } +}; + +// Global cursor resources array +static AnimatedCursor* cursorResources[Mouse::NUM_MOUSE_CURSORS][MAX_2D_CURSOR_DIRECTIONS]; + +// RIFF/ANI parsing helpers +typedef std::array FourCC; +constexpr FourCC riff_id = {'R', 'I', 'F', 'F'}; +constexpr FourCC acon_id = {'A', 'C', 'O', 'N'}; +constexpr FourCC anih_id = {'a', 'n', 'i', 'h'}; +constexpr FourCC fram_id = {'f', 'r', 'a', 'm'}; +constexpr FourCC icon_id = {'i', 'c', 'o', 'n'}; +constexpr FourCC list_id = {'L', 'I', 'S', 'T'}; + +struct ANIHeader +{ + uint32_t size; + uint32_t frames; + uint32_t steps; + uint32_t width; + uint32_t height; + uint32_t bitsPerPixel; + uint32_t planes; + uint32_t displayRate; + uint32_t flags; +}; + +struct RIFFChunk +{ + FourCC id; + uint32_t size; + FourCC type; +}; + +static RIFFChunk* getNextChunk(RIFFChunk* chunk, const char* buffer_end) +{ + if (!chunk) return nullptr; + + // Size check: Chunk header is at least 8 bytes (ID + Size). + char* next = (char*)chunk + 8 + chunk->size; + + // RIFF chunks are padded to 2 bytes + if (chunk->size % 2 != 0) next++; + + if (next >= buffer_end) return nullptr; + return (RIFFChunk*)next; +} + +static void* getChunkData(RIFFChunk* chunk) +{ + // For LIST and RIFF, type is at +8, data starts at +12 + if (chunk->id == list_id || chunk->id == riff_id) + return (char*)chunk + 12; + + // For others, data starts at +8 + return (char*)chunk + 8; +} + +/** + * loadANI - Dedicated standalone RIFF/ANI parser (Hardened) + */ +static AnimatedCursor* loadANI(const char* filepath) +{ + File* file = TheFileSystem->openFile(filepath, File::READ | File::BINARY); + if (!file) + { + DEBUG_LOG(("loadANI: Failed to open ANI cursor [%s]", filepath)); + return nullptr; + } + + Int size = file->size(); + if (size < (Int)sizeof(RIFFChunk)) + { + DEBUG_LOG(("loadANI: File too small [%s]", filepath)); + file->close(); + return nullptr; + } + + std::unique_ptr file_buffer(new char[size]); + if (file->read(file_buffer.get(), size) != size) + { + DEBUG_LOG(("loadANI: Failed to read ANI cursor [%s]", filepath)); + file->close(); + return nullptr; + } + file->close(); + + char* buffer_start = file_buffer.get(); + char* buffer_end = buffer_start + size; + + RIFFChunk *riff_header = (RIFFChunk*)buffer_start; + if (riff_header->id != riff_id || riff_header->type != acon_id) + { + DEBUG_LOG(("loadANI: Not a valid RIFF/ACON file [%s]", filepath)); + return nullptr; + } + + DEBUG_LOG(("loadANI: Loading %s", filepath)); + std::unique_ptr cursor(new AnimatedCursor()); + + // Top level chunks start after the RIFF header (8 bytes + 'ACON' = 12 bytes) + RIFFChunk* chunk = (RIFFChunk*)(buffer_start + 12); + + while (chunk != nullptr && (char *)chunk + 8 <= buffer_end) + { + if (chunk->id == anih_id) + { + if (chunk->size < sizeof(ANIHeader)) + { + DEBUG_LOG(("loadANI: Invalid ANI header size")); + return nullptr; + } + + ANIHeader *ani_header = (ANIHeader*)getChunkData(chunk); + cursor->m_frameCount = ani_header->frames; + cursor->m_frameRate = ani_header->displayRate; + } + else if (chunk->id == list_id && chunk->type == fram_id) + { + int frame_index = 0; + // Sub-chunks in LIST start after the header + type (12 bytes) + RIFFChunk *frame = (RIFFChunk*)((char *)chunk + 12); + char* list_end = (char*)chunk + 8 + chunk->size; + if (list_end > buffer_end) list_end = buffer_end; + + while (frame != nullptr && (char *)frame + 8 <= list_end) + { + if (frame->id == icon_id) + { + if ((char*)frame + 8 + frame->size <= list_end) + { + const void *frame_buffer = getChunkData(frame); + SDL_IOStream *io_stream = SDL_IOFromConstMem(frame_buffer, frame->size); + if (io_stream) + { + SDL_Surface *surface = cursor->m_frameSurfaces[frame_index] = IMG_LoadTyped_IO(io_stream, true, "ico"); + if (surface) + { + SDL_PropertiesID props = SDL_GetSurfaceProperties(surface); + int hot_spot_x = (int)SDL_GetNumberProperty(props, SDL_PROP_SURFACE_HOTSPOT_X_NUMBER, 0); + int hot_spot_y = (int)SDL_GetNumberProperty(props, SDL_PROP_SURFACE_HOTSPOT_Y_NUMBER, 0); + + cursor->m_frameCursors[frame_index++] = SDL_CreateColorCursor(surface, hot_spot_x, hot_spot_y); + } + } + } + } + + if (frame_index >= MAX_2D_CURSOR_ANIM_FRAMES) break; + frame = getNextChunk(frame, list_end); + } + } + + chunk = getNextChunk(chunk, buffer_end); + } + + return cursor.release(); +} + +/** + * Constructor - Initialize SDL3Mouse with window handle + */ +SDL3Mouse::SDL3Mouse(SDL_Window* window) + : Mouse(), + m_Window(window), + m_IsCaptured(false), + m_IsVisible(true), + m_LostFocus(false), + m_LeftButtonDownTime(0), + m_RightButtonDownTime(0), + m_MiddleButtonDownTime(0), + m_LastFrameNumber(0), + m_directionFrame(0), + m_inputFrame(0), + m_accumulatedDeltaX(0.0f), + m_accumulatedDeltaY(0.0f), + m_activeSDLCursor(nullptr), + m_cursorDirty(false) +{ + m_LeftButtonDownPos.x = 0; + m_LeftButtonDownPos.y = 0; + m_RightButtonDownPos.x = 0; + m_RightButtonDownPos.y = 0; + m_MiddleButtonDownPos.x = 0; + m_MiddleButtonDownPos.y = 0; +} + +/** + * Destructor + */ +SDL3Mouse::~SDL3Mouse(void) +{ + releaseCapture(); +} + +/** + * Initialize mouse subsystem + */ +void SDL3Mouse::init(void) +{ + Mouse::init(); + + m_inputMovesAbsolute = TRUE; + + // Show cursor by default + setVisibility(TRUE); +} + +/** + * Reset mouse to default state + */ +void SDL3Mouse::reset(void) +{ + Mouse::reset(); + + releaseCapture(); + setVisibility(TRUE); +} + +/** + * Update mouse state (called per-frame) + */ +void SDL3Mouse::update(void) +{ + Mouse::update(); + + m_inputFrame++; + + if (m_LostFocus) + { + return; + } + + MouseCursor cursor = m_currentCursor; + + if (cursor != NONE && cursor != INVALID_MOUSE_CURSOR && m_cursorInfo[cursor].numDirections > 1) + { + float dx = 0.0f; + float dy = 0.0f; + bool hasMovement = false; + + if (cursor == SCROLL && TheInGameUI && TheInGameUI->isScrolling()) + { + Coord2D scroll = TheInGameUI->getScrollAmount(); + if (scroll.x != 0.0f || scroll.y != 0.0f) + { + dx = scroll.x; + dy = scroll.y; + hasMovement = true; + } + } + + if (!hasMovement) + { + if (SDL_fabsf(m_accumulatedDeltaX) > 0.01f || SDL_fabsf(m_accumulatedDeltaY) > 0.01f) + { + dx = m_accumulatedDeltaX; + dy = m_accumulatedDeltaY; + hasMovement = true; + + m_accumulatedDeltaX = 0.0f; + m_accumulatedDeltaY = 0.0f; + } + } + + if (hasMovement) + { + float angle = atan2f(dy, dx); + if (angle < 0) angle += 2.0f * (float)M_PI; + float segmentAngle = 2.0f * (float)M_PI / (float)m_cursorInfo[cursor].numDirections; + m_directionFrame = (int)((angle + (segmentAngle / 2.0f)) / segmentAngle) % m_cursorInfo[cursor].numDirections; + } + } + else + { + m_directionFrame = 0; + } + + SDL_Cursor* requestedHandle = nullptr; + bool bUseDefaultCursor = false; + + if (cursor == NONE || cursor == INVALID_MOUSE_CURSOR || !m_IsVisible) + { + bUseDefaultCursor = true; + } + else + { + AnimatedCursor* animated = cursorResources[cursor][m_directionFrame]; + if (animated) + { + requestedHandle = animated->getActiveFrame(); + } + else + { + bUseDefaultCursor = true; + } + } + + if (bUseDefaultCursor) + { + if (cursorResources[NORMAL][0]) + { + requestedHandle = cursorResources[NORMAL][0]->m_frameCursors[0]; + } + else + { + requestedHandle = SDL_GetDefaultCursor(); + } + } + + if (requestedHandle != m_activeSDLCursor) + { + SDL_SetCursor(requestedHandle); + m_activeSDLCursor = requestedHandle; + } + + m_cursorDirty = false; +} + +/** + * Initialize cursor resources (load cursor images from ANI files) + */ +void SDL3Mouse::initCursorResources(void) +{ + for (Int cursor=FIRST_CURSOR; cursor 1) + snprintf(resourcePath, sizeof(resourcePath), "Data/Cursors/%s%d.ani", m_cursorInfo[cursor].textureName.str(), direction); + else + snprintf(resourcePath, sizeof(resourcePath), "Data/Cursors/%s.ani", m_cursorInfo[cursor].textureName.str()); + + cursorResources[cursor][direction]=loadCursorFromFile(resourcePath); + DEBUG_ASSERTCRASH(cursorResources[cursor][direction], ("MissingCursor %s\n",resourcePath)); + } + } + } +} + +AnimatedCursor* SDL3Mouse::loadCursorFromFile(const char* filepath) +{ + return loadANI(filepath); +} + +/** + * Set mouse cursor type + */ +void SDL3Mouse::setCursor(MouseCursor cursor) +{ + if (m_currentCursor == cursor) + { + return; + } + + Mouse::setCursor( cursor ); + m_currentCursor = cursor; + m_cursorDirty = true; +} + +/** + * Set cursor visibility + */ +void SDL3Mouse::setVisibility(Bool visible) +{ + Mouse::setVisibility(visible); + + if (visible) { + SDL_ShowCursor(); + } else { + SDL_HideCursor(); + } +} + +/** + * Handle window losing focus + */ +void SDL3Mouse::loseFocus() +{ + Mouse::loseFocus(); + releaseCapture(); +} + +/** + * Handle window regaining focus + */ +void SDL3Mouse::regainFocus() +{ + Mouse::regainFocus(); +} + +/** + * Capture mouse (confine to window) + */ +void SDL3Mouse::capture(void) +{ + if (!m_Window || m_isCursorCaptured) { + return; + } + + SDL_CaptureMouse(true); + SDL_SetWindowMouseGrab(m_Window, true); + onCursorCaptured(true); +} + +/** + * Release mouse capture + */ +void SDL3Mouse::releaseCapture(void) +{ + if (!m_isCursorCaptured) { + return; + } + + SDL_CaptureMouse(false); + if (m_Window) { + SDL_SetWindowMouseGrab(m_Window, false); + } + + onCursorCaptured(false); +} + +/** + * Get next mouse event from the centralized input manager + */ +UnsignedByte SDL3Mouse::getMouseEvent(MouseIO *result, Bool flush) +{ + if (!TheSDL3InputManager) { + return MOUSE_NONE; + } + + SDL_Event nextEvent; + if (!TheSDL3InputManager->getNextMouseEvent(nextEvent)) { + return MOUSE_NONE; + } + + translateEvent(nextEvent, result); + + return MOUSE_OK; +} + +void SDL3Mouse::addSDLEvent(SDL_Event *event) +{ + if (TheSDL3InputManager && event) { + TheSDL3InputManager->addMouseSDLEvent(*event); + } +} + +//----------------------------------------------------------------------------- +/** Unified event translation (Clean Slate Rewrite) */ +//----------------------------------------------------------------------------- +void SDL3Mouse::translateEvent(const SDL_Event& event, MouseIO *result) +{ + if (!result) return; + + // Reset state + result->leftState = result->rightState = result->middleState = MBS_None; + result->wheelPos = 0; + result->deltaPos.x = result->deltaPos.y = 0; + + // Common timestamp (SDL3 uses nanoseconds, SAGE usually wants ms) + result->time = (Uint32)(event.common.timestamp / 1000000); + + int rawX = 0; + int rawY = 0; + Uint32 windowID = 0; + + switch (event.type) { + case SDL_EVENT_MOUSE_MOTION: + rawX = (int)event.motion.x; + rawY = (int)event.motion.y; + windowID = event.motion.windowID; + result->deltaPos.x = (Int)event.motion.xrel; + result->deltaPos.y = (Int)event.motion.yrel; + + m_accumulatedDeltaX += event.motion.xrel; + m_accumulatedDeltaY += event.motion.yrel; + break; + + case SDL_EVENT_MOUSE_BUTTON_DOWN: + case SDL_EVENT_MOUSE_BUTTON_UP: + { + rawX = (int)event.button.x; + rawY = (int)event.button.y; + windowID = event.button.windowID; + + MouseButtonState state = event.button.down ? MBS_Down : MBS_Up; + if (event.button.down && event.button.clicks >= 2) state = MBS_DoubleClick; + + if (event.button.button == SDL_BUTTON_LEFT) result->leftState = state; + else if (event.button.button == SDL_BUTTON_RIGHT) result->rightState = state; + else if (event.button.button == SDL_BUTTON_MIDDLE) result->middleState = state; + break; + } + + case SDL_EVENT_MOUSE_WHEEL: + { + // For wheel events, use current mouse position + float mx, my; + SDL_GetMouseState(&mx, &my); + rawX = (int)mx; + rawY = (int)my; + windowID = event.wheel.windowID; + result->wheelPos = (Int)(event.wheel.y * 120); // MOUSE_WHEEL_DELTA + break; + } + + default: + return; + } + + // Dynamic Scaling Guard + int scaledX, scaledY; + scaleMouseCoordinates(rawX, rawY, windowID, scaledX, scaledY); + result->pos.x = scaledX; + result->pos.y = scaledY; +} + +void SDL3Mouse::scaleMouseCoordinates(int rawX, int rawY, Uint32 windowID, int& scaledX, int& scaledY) +{ + SDL_Window* window = SDL_GetWindowFromID(windowID); + if (!window || !TheDisplay) { + scaledX = rawX; + scaledY = rawY; + return; + } + + int winW = 0, winH = 0; + SDL_GetWindowSizeInPixels(window, &winW, &winH); + + int intW = TheDisplay->getWidth(); + int intH = TheDisplay->getHeight(); + + // Guard: If we are at native resolution, bypass all math + if (winW == intW && winH == intH) { + scaledX = rawX; + scaledY = rawY; + return; + } + + // Handle Viewport/Letterboxing if active + int pbX, pbY, pbW, pbH; + if (TheDisplay->getViewportRect(pbX, pbY, pbW, pbH)) { + int cx = std::max(0, std::min(pbW, rawX - pbX)); + int cy = std::max(0, std::min(pbH, rawY - pbY)); + scaledX = (int)(cx * (float)intW / pbW); + scaledY = (int)(cy * (float)intH / pbH); + } else { + scaledX = (int)(rawX * (float)intW / winW); + scaledY = (int)(rawY * (float)intH / winH); + } +} + +// ============================================================================ +// SDL3KEYBOARD IMPLEMENTATION +// ============================================================================ + +/** + * Lifecycle + */ +SDL3Keyboard::SDL3Keyboard(void) : Keyboard() {} +SDL3Keyboard::~SDL3Keyboard(void) {} + +/** + * SubsystemInterface + */ +void SDL3Keyboard::init(void) { Keyboard::init(); } +void SDL3Keyboard::reset(void) { Keyboard::reset(); } +void SDL3Keyboard::update(void) { Keyboard::update(); } + +/** + * Keyboard Interface + */ +Bool SDL3Keyboard::getCapsState(void) { return FALSE; } + +/** + * SDL3-specific internal methods + */ +void SDL3Keyboard::getKey(KeyboardIO *key) +{ + if (!TheSDL3InputManager) { + key->key = KEY_NONE; + key->status = KeyboardIO::STATUS_UNUSED; + return; + } + + SDL_Event nextEvent; + if (!TheSDL3InputManager->getNextKeyboardEvent(nextEvent)) { + key->key = KEY_NONE; + key->status = KeyboardIO::STATUS_UNUSED; + return; + } + + const SDL_KeyboardEvent& keyEvent = nextEvent.key; + KeyDefType keyDef = translateScanCodeToKeyVal(keyEvent.scancode); + + key->key = keyDef; + key->status = KeyboardIO::STATUS_UNUSED; + key->state = keyEvent.down ? KEY_STATE_DOWN : KEY_STATE_UP; + key->keyDownTimeMsec = keyEvent.down ? timeGetTime() : 0; + + SDL_Keymod mod = keyEvent.mod; + if (mod & SDL_KMOD_LSHIFT) key->state |= KEY_STATE_LSHIFT; + if (mod & SDL_KMOD_RSHIFT) key->state |= KEY_STATE_RSHIFT; + if (mod & SDL_KMOD_LCTRL) key->state |= KEY_STATE_LCONTROL; + if (mod & SDL_KMOD_RCTRL) key->state |= KEY_STATE_RCONTROL; + if (mod & SDL_KMOD_LALT) key->state |= KEY_STATE_LALT; + if (mod & SDL_KMOD_RALT) key->state |= KEY_STATE_RALT; + if (mod & SDL_KMOD_CAPS) key->state |= KEY_STATE_CAPSLOCK; + + if (keyDef == KEY_LSHIFT) key->state &= ~KEY_STATE_LSHIFT; + if (keyDef == KEY_RSHIFT) key->state &= ~KEY_STATE_RSHIFT; + if (keyDef == KEY_LCTRL) key->state &= ~KEY_STATE_LCONTROL; + if (keyDef == KEY_RCTRL) key->state &= ~KEY_STATE_RCONTROL; + if (keyDef == KEY_LALT) key->state &= ~KEY_STATE_LALT; + if (keyDef == KEY_RALT) key->state &= ~KEY_STATE_RALT; +} + +void SDL3Keyboard::addSDLEvent(SDL_Event *event) +{ + if (TheSDL3InputManager && event) { + TheSDL3InputManager->addKeyboardSDLEvent(*event); + } +} + +KeyVal SDL3Keyboard::translateScanCodeToKeyVal(unsigned char scan) +{ + switch ((SDL_Scancode)scan) { + case SDL_SCANCODE_ESCAPE: return KEY_ESC; + case SDL_SCANCODE_RETURN: return KEY_ENTER; + case SDL_SCANCODE_KP_ENTER: return KEY_KPENTER; + case SDL_SCANCODE_SPACE: return KEY_SPACE; + case SDL_SCANCODE_TAB: return KEY_TAB; + case SDL_SCANCODE_BACKSPACE: return KEY_BACKSPACE; + case SDL_SCANCODE_DELETE: return KEY_DEL; + case SDL_SCANCODE_HOME: return KEY_HOME; + case SDL_SCANCODE_END: return KEY_END; + case SDL_SCANCODE_PAGEUP: return KEY_PGUP; + case SDL_SCANCODE_PAGEDOWN: return KEY_PGDN; + case SDL_SCANCODE_INSERT: return KEY_INS; + case SDL_SCANCODE_LSHIFT: return KEY_LSHIFT; + case SDL_SCANCODE_RSHIFT: return KEY_RSHIFT; + case SDL_SCANCODE_LCTRL: return KEY_LCTRL; + case SDL_SCANCODE_RCTRL: return KEY_RCTRL; + case SDL_SCANCODE_LALT: return KEY_LALT; + case SDL_SCANCODE_RALT: return KEY_RALT; + case SDL_SCANCODE_UP: return KEY_UP; + case SDL_SCANCODE_DOWN: return KEY_DOWN; + case SDL_SCANCODE_LEFT: return KEY_LEFT; + case SDL_SCANCODE_RIGHT: return KEY_RIGHT; + case SDL_SCANCODE_F1: return KEY_F1; + case SDL_SCANCODE_F2: return KEY_F2; + case SDL_SCANCODE_F3: return KEY_F3; + case SDL_SCANCODE_F4: return KEY_F4; + case SDL_SCANCODE_F5: return KEY_F5; + case SDL_SCANCODE_F6: return KEY_F6; + case SDL_SCANCODE_F7: return KEY_F7; + case SDL_SCANCODE_F8: return KEY_F8; + case SDL_SCANCODE_F9: return KEY_F9; + case SDL_SCANCODE_F10: return KEY_F10; + case SDL_SCANCODE_F11: return KEY_F11; + case SDL_SCANCODE_F12: return KEY_F12; + case SDL_SCANCODE_1: return KEY_1; + case SDL_SCANCODE_2: return KEY_2; + case SDL_SCANCODE_3: return KEY_3; + case SDL_SCANCODE_4: return KEY_4; + case SDL_SCANCODE_5: return KEY_5; + case SDL_SCANCODE_6: return KEY_6; + case SDL_SCANCODE_7: return KEY_7; + case SDL_SCANCODE_8: return KEY_8; + case SDL_SCANCODE_9: return KEY_9; + case SDL_SCANCODE_0: return KEY_0; + case SDL_SCANCODE_A: return KEY_A; + case SDL_SCANCODE_B: return KEY_B; + case SDL_SCANCODE_C: return KEY_C; + case SDL_SCANCODE_D: return KEY_D; + case SDL_SCANCODE_E: return KEY_E; + case SDL_SCANCODE_F: return KEY_F; + case SDL_SCANCODE_G: return KEY_G; + case SDL_SCANCODE_H: return KEY_H; + case SDL_SCANCODE_I: return KEY_I; + case SDL_SCANCODE_J: return KEY_J; + case SDL_SCANCODE_K: return KEY_K; + case SDL_SCANCODE_L: return KEY_L; + case SDL_SCANCODE_M: return KEY_M; + case SDL_SCANCODE_N: return KEY_N; + case SDL_SCANCODE_O: return KEY_O; + case SDL_SCANCODE_P: return KEY_P; + case SDL_SCANCODE_Q: return KEY_Q; + case SDL_SCANCODE_R: return KEY_R; + case SDL_SCANCODE_S: return KEY_S; + case SDL_SCANCODE_T: return KEY_T; + case SDL_SCANCODE_U: return KEY_U; + case SDL_SCANCODE_V: return KEY_V; + case SDL_SCANCODE_W: return KEY_W; + case SDL_SCANCODE_X: return KEY_X; + case SDL_SCANCODE_Y: return KEY_Y; + case SDL_SCANCODE_Z: return KEY_Z; + case SDL_SCANCODE_MINUS: return KEY_MINUS; + case SDL_SCANCODE_EQUALS: return KEY_EQUAL; + case SDL_SCANCODE_LEFTBRACKET: return KEY_LBRACKET; + case SDL_SCANCODE_RIGHTBRACKET: return KEY_RBRACKET; + case SDL_SCANCODE_SEMICOLON: return KEY_SEMICOLON; + case SDL_SCANCODE_APOSTROPHE: return KEY_APOSTROPHE; + case SDL_SCANCODE_GRAVE: return KEY_TICK; + case SDL_SCANCODE_COMMA: return KEY_COMMA; + case SDL_SCANCODE_PERIOD: return KEY_PERIOD; + case SDL_SCANCODE_SLASH: return KEY_SLASH; + case SDL_SCANCODE_BACKSLASH: return KEY_BACKSLASH; + case SDL_SCANCODE_KP_1: return KEY_KP1; + case SDL_SCANCODE_KP_2: return KEY_KP2; + case SDL_SCANCODE_KP_3: return KEY_KP3; + case SDL_SCANCODE_KP_4: return KEY_KP4; + case SDL_SCANCODE_KP_5: return KEY_KP5; + case SDL_SCANCODE_KP_6: return KEY_KP6; + case SDL_SCANCODE_KP_7: return KEY_KP7; + case SDL_SCANCODE_KP_8: return KEY_KP8; + case SDL_SCANCODE_KP_9: return KEY_KP9; + case SDL_SCANCODE_KP_0: return KEY_KP0; + case SDL_SCANCODE_KP_PLUS: return KEY_KPPLUS; + case SDL_SCANCODE_KP_MINUS: return KEY_KPMINUS; + case SDL_SCANCODE_KP_MULTIPLY: return KEY_KPSTAR; + case SDL_SCANCODE_KP_DIVIDE: return KEY_KPSLASH; + case SDL_SCANCODE_KP_PERIOD: return KEY_KPDEL; + case SDL_SCANCODE_CAPSLOCK: return KEY_CAPS; + case SDL_SCANCODE_NUMLOCKCLEAR: return KEY_NUM; + case SDL_SCANCODE_SCROLLLOCK: return KEY_SCROLL; + case SDL_SCANCODE_PRINTSCREEN: return KEY_SYSREQ; + default: return KEY_NONE; + } +} + +// ============================================================================ +// SDL3INPUTMANAGER IMPLEMENTATION +// ============================================================================ + +/** + * Lifecycle + */ +SDL3InputManager::SDL3InputManager() + : m_mouseNextFree(0), + m_mouseNextGet(0), + m_keyNextFree(0), + m_keyNextGet(0), + m_gamepad(nullptr), + m_precisionMode(FALSE), + m_lastUpdateTime(0), + m_isQuitting(FALSE) +{ + memset(m_mouseEvents, 0, sizeof(m_mouseEvents)); + memset(m_keyEvents, 0, sizeof(m_keyEvents)); + TheSDL3InputManager = this; + + openFirstGamepad(); + m_lastUpdateTime = SDL_GetTicks(); +} + +SDL3InputManager::~SDL3InputManager() +{ + closeGamepad(); + TheSDL3InputManager = nullptr; +} + +/** + * Unified Event Loop + */ +void SDL3InputManager::update() +{ + SDL_Event event; + while (SDL_PollEvent(&event)) { + switch (event.type) { + case SDL_EVENT_QUIT: + case SDL_EVENT_WINDOW_CLOSE_REQUESTED: + m_isQuitting = true; + break; + + case SDL_EVENT_GAMEPAD_ADDED: + if (!m_gamepad) openFirstGamepad(); + break; + + case SDL_EVENT_GAMEPAD_REMOVED: + if (m_gamepad && event.gdevice.which == SDL_GetGamepadID(m_gamepad)) closeGamepad(); + break; + + case SDL_EVENT_WINDOW_FOCUS_GAINED: + if (TheMouse) { + TheMouse->regainFocus(); + TheMouse->refreshCursorCapture(); + } + break; + + case SDL_EVENT_WINDOW_FOCUS_LOST: + if (TheMouse) TheMouse->loseFocus(); + break; + + case SDL_EVENT_WINDOW_MOUSE_ENTER: + if (TheMouse) TheMouse->onCursorMovedInside(); + break; + + case SDL_EVENT_WINDOW_MOUSE_LEAVE: + if (TheMouse) TheMouse->onCursorMovedOutside(); + break; + + case SDL_EVENT_MOUSE_MOTION: + case SDL_EVENT_MOUSE_BUTTON_DOWN: + case SDL_EVENT_MOUSE_BUTTON_UP: + case SDL_EVENT_MOUSE_WHEEL: + addMouseSDLEvent(event); + break; + + case SDL_EVENT_KEY_DOWN: + case SDL_EVENT_KEY_UP: + if (!event.key.repeat) addKeyboardSDLEvent(event); + break; + + case SDL_EVENT_TEXT_INPUT: + if (TheGameEngine) { + SDL3GameEngine* engine = dynamic_cast(TheGameEngine); + if (engine) engine->forwardTextInputEvent(event.text.text); + } + break; + + default: + break; + } + } + + processGamepadInput(); +} + +/** + * Buffer Management + */ +Bool SDL3InputManager::getNextMouseEvent(SDL_Event& outEvent) +{ + if (m_mouseEvents[m_mouseNextGet].type == SDL_EVENT_FIRST) return FALSE; + + SDL_Event* event = &m_mouseEvents[m_mouseNextGet]; + m_mouseNextGet = (m_mouseNextGet + 1) % MAX_MOUSE_EVENTS; + + outEvent = *event; + event->type = SDL_EVENT_FIRST; + return TRUE; +} + +Bool SDL3InputManager::getNextKeyboardEvent(SDL_Event& outEvent) +{ + if (m_keyEvents[m_keyNextGet].type == SDL_EVENT_FIRST) return FALSE; + + SDL_Event* event = &m_keyEvents[m_keyNextGet]; + m_keyNextGet = (m_keyNextGet + 1) % MAX_KEY_EVENTS; + + outEvent = *event; + event->type = SDL_EVENT_FIRST; + return TRUE; +} + +void SDL3InputManager::addMouseSDLEvent(const SDL_Event& event) +{ + UnsignedInt nextFree = (m_mouseNextFree + 1) % MAX_MOUSE_EVENTS; + if (nextFree == m_mouseNextGet) return; + m_mouseEvents[m_mouseNextFree] = event; + m_mouseNextFree = nextFree; +} + +void SDL3InputManager::addKeyboardSDLEvent(const SDL_Event& event) +{ + UnsignedInt nextFree = (m_keyNextFree + 1) % MAX_KEY_EVENTS; + if (nextFree == m_keyNextGet) return; + m_keyEvents[m_keyNextFree] = event; + m_keyNextFree = nextFree; +} + +/** + * Gamepad Logic + */ +void SDL3InputManager::openFirstGamepad() +{ + int count = 0; + SDL_JoystickID* joysticks = SDL_GetGamepads(&count); + if (joysticks) { + for (int i = 0; i < count; ++i) { + m_gamepad = SDL_OpenGamepad(joysticks[i]); + if (m_gamepad) { + DEBUG_LOG(("SDL3InputManager: Opened gamepad: %s", SDL_GetGamepadName(m_gamepad))); + break; + } + } + SDL_free(joysticks); + } +} + +void SDL3InputManager::closeGamepad() +{ + if (m_gamepad) { + SDL_CloseGamepad(m_gamepad); + m_gamepad = nullptr; + } +} + +void SDL3InputManager::virtualPulseKey(SDL_Scancode scancode, bool down) +{ + SDL_Event keyEvent; + memset(&keyEvent, 0, sizeof(keyEvent)); + keyEvent.type = down ? SDL_EVENT_KEY_DOWN : SDL_EVENT_KEY_UP; + keyEvent.key.scancode = scancode; + keyEvent.key.down = down; + + if (scancode == SDL_SCANCODE_LCTRL) keyEvent.key.mod = SDL_KMOD_LCTRL; + else if (scancode == SDL_SCANCODE_LSHIFT) keyEvent.key.mod = SDL_KMOD_LSHIFT; + else if (scancode == SDL_SCANCODE_LALT) keyEvent.key.mod = SDL_KMOD_LALT; + + addKeyboardSDLEvent(keyEvent); +} + +void SDL3InputManager::virtualPulseMouse(Uint8 button, bool down) +{ + SDL_Event clickEvent; + memset(&clickEvent, 0, sizeof(clickEvent)); + clickEvent.type = down ? SDL_EVENT_MOUSE_BUTTON_DOWN : SDL_EVENT_MOUSE_BUTTON_UP; + clickEvent.button.button = button; + clickEvent.button.clicks = 1; + clickEvent.button.down = down; + + float mx, my; + SDL_GetMouseState(&mx, &my); + clickEvent.button.x = mx; + clickEvent.button.y = my; + + addMouseSDLEvent(clickEvent); +} + +void SDL3InputManager::handleGamepadButton(SDL_GamepadButton button, bool& currentState, bool isDown, std::function action) +{ + if (isDown != currentState) { + action(isDown); + currentState = isDown; + } +} + +void SDL3InputManager::processGamepadInput() +{ + if (!m_gamepad) return; + + Uint64 now = SDL_GetTicks(); + float deltaTime = (now - m_lastUpdateTime) / 1000.0f; + m_lastUpdateTime = now; + + const float DEADZONE = DEFAULT_DEADZONE; + const float CURSOR_SPEED = DEFAULT_CURSOR_SPEED; + + // 1. TRIGGERS (Modifiers & Precision) + bool ltPressed = SDL_GetGamepadAxis(m_gamepad, SDL_GAMEPAD_AXIS_LEFT_TRIGGER) > TRIGGER_THRESHOLD; + if (ltPressed != m_state.ltDown) { + m_state.ltDown = ltPressed; + m_precisionMode = m_state.ltDown; + } + + bool rtPressed = SDL_GetGamepadAxis(m_gamepad, SDL_GAMEPAD_AXIS_RIGHT_TRIGGER) > TRIGGER_THRESHOLD; + if (rtPressed != m_state.rtDown) { + m_state.rtDown = rtPressed; + virtualPulseKey(SDL_SCANCODE_LCTRL, m_state.rtDown); + } + + // 2. STICKS (Movement & Panning) + float lx = SDL_GetGamepadAxis(m_gamepad, SDL_GAMEPAD_AXIS_LEFTX) / AXIS_MAX; + float ly = SDL_GetGamepadAxis(m_gamepad, SDL_GAMEPAD_AXIS_LEFTY) / AXIS_MAX; + + if (SDL_fabsf(lx) > DEADZONE || SDL_fabsf(ly) > DEADZONE) { + float speed = CURSOR_SPEED; + if (m_precisionMode) speed *= 0.3f; + + SDL_Event motionEvent; + memset(&motionEvent, 0, sizeof(motionEvent)); + motionEvent.type = SDL_EVENT_MOUSE_MOTION; + motionEvent.motion.xrel = lx * speed * deltaTime; + motionEvent.motion.yrel = ly * speed * deltaTime; + + float mx, my; + SDL_GetMouseState(&mx, &my); + motionEvent.motion.x = mx + motionEvent.motion.xrel; + motionEvent.motion.y = my + motionEvent.motion.yrel; + + addMouseSDLEvent(motionEvent); + SDL_WarpMouseInWindow(NULL, motionEvent.motion.x, motionEvent.motion.y); + } + + float rx = SDL_GetGamepadAxis(m_gamepad, SDL_GAMEPAD_AXIS_RIGHTX) / AXIS_MAX; + float ry = SDL_GetGamepadAxis(m_gamepad, SDL_GAMEPAD_AXIS_RIGHTY) / AXIS_MAX; + + handleGamepadButton(SDL_GAMEPAD_BUTTON_INVALID, m_state.stickLeft, rx < -DEADZONE, [&](bool d){ virtualPulseKey(SDL_SCANCODE_LEFT, d); }); + handleGamepadButton(SDL_GAMEPAD_BUTTON_INVALID, m_state.stickRight, rx > DEADZONE, [&](bool d){ virtualPulseKey(SDL_SCANCODE_RIGHT, d); }); + handleGamepadButton(SDL_GAMEPAD_BUTTON_INVALID, m_state.stickUp, ry < -DEADZONE, [&](bool d){ virtualPulseKey(SDL_SCANCODE_UP, d); }); + handleGamepadButton(SDL_GAMEPAD_BUTTON_INVALID, m_state.stickDown, ry > DEADZONE, [&](bool d){ virtualPulseKey(SDL_SCANCODE_DOWN, d); }); + + // 3. BUTTONS & D-PAD (Actions & Hotkeys) + handleGamepadButton(SDL_GAMEPAD_BUTTON_SOUTH, m_state.buttonState[SDL_GAMEPAD_BUTTON_SOUTH], SDL_GetGamepadButton(m_gamepad, SDL_GAMEPAD_BUTTON_SOUTH), [&](bool d){ virtualPulseMouse(SDL_BUTTON_LEFT, d); }); + handleGamepadButton(SDL_GAMEPAD_BUTTON_EAST, m_state.buttonState[SDL_GAMEPAD_BUTTON_EAST], SDL_GetGamepadButton(m_gamepad, SDL_GAMEPAD_BUTTON_EAST), [&](bool d){ virtualPulseMouse(SDL_BUTTON_RIGHT, d); }); + handleGamepadButton(SDL_GAMEPAD_BUTTON_WEST, m_state.buttonState[SDL_GAMEPAD_BUTTON_WEST], SDL_GetGamepadButton(m_gamepad, SDL_GAMEPAD_BUTTON_WEST), [&](bool d){ virtualPulseKey(SDL_SCANCODE_A, d); }); + handleGamepadButton(SDL_GAMEPAD_BUTTON_NORTH, m_state.buttonState[SDL_GAMEPAD_BUTTON_NORTH], SDL_GetGamepadButton(m_gamepad, SDL_GAMEPAD_BUTTON_NORTH), [&](bool d){ if (d) TheMessageStream->appendMessage(GameMessage::MSG_META_STOP); }); + handleGamepadButton(SDL_GAMEPAD_BUTTON_LEFT_SHOULDER, m_state.buttonState[SDL_GAMEPAD_BUTTON_LEFT_SHOULDER], SDL_GetGamepadButton(m_gamepad, SDL_GAMEPAD_BUTTON_LEFT_SHOULDER), [&](bool d){ virtualPulseKey(SDL_SCANCODE_Q, d); }); + handleGamepadButton(SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER, m_state.buttonState[SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER], SDL_GetGamepadButton(m_gamepad, SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER), [&](bool d){ virtualPulseKey(SDL_SCANCODE_LSHIFT, d); }); + handleGamepadButton(SDL_GAMEPAD_BUTTON_START, m_state.buttonState[SDL_GAMEPAD_BUTTON_START], SDL_GetGamepadButton(m_gamepad, SDL_GAMEPAD_BUTTON_START), [&](bool d){ virtualPulseKey(SDL_SCANCODE_ESCAPE, d); }); + handleGamepadButton(SDL_GAMEPAD_BUTTON_BACK, m_state.buttonState[SDL_GAMEPAD_BUTTON_BACK], SDL_GetGamepadButton(m_gamepad, SDL_GAMEPAD_BUTTON_BACK), [&](bool d){ virtualPulseKey(SDL_SCANCODE_SPACE, d); }); + handleGamepadButton(SDL_GAMEPAD_BUTTON_DPAD_UP, m_state.buttonState[SDL_GAMEPAD_BUTTON_DPAD_UP], SDL_GetGamepadButton(m_gamepad, SDL_GAMEPAD_BUTTON_DPAD_UP), [&](bool d){ virtualPulseKey(SDL_SCANCODE_1, d); }); + handleGamepadButton(SDL_GAMEPAD_BUTTON_DPAD_DOWN, m_state.buttonState[SDL_GAMEPAD_BUTTON_DPAD_DOWN], SDL_GetGamepadButton(m_gamepad, SDL_GAMEPAD_BUTTON_DPAD_DOWN), [&](bool d){ virtualPulseKey(SDL_SCANCODE_2, d); }); + handleGamepadButton(SDL_GAMEPAD_BUTTON_DPAD_LEFT, m_state.buttonState[SDL_GAMEPAD_BUTTON_DPAD_LEFT], SDL_GetGamepadButton(m_gamepad, SDL_GAMEPAD_BUTTON_DPAD_LEFT), [&](bool d){ virtualPulseKey(SDL_SCANCODE_3, d); }); + handleGamepadButton(SDL_GAMEPAD_BUTTON_DPAD_RIGHT, m_state.buttonState[SDL_GAMEPAD_BUTTON_DPAD_RIGHT], SDL_GetGamepadButton(m_gamepad, SDL_GAMEPAD_BUTTON_DPAD_RIGHT), [&](bool d){ virtualPulseKey(SDL_SCANCODE_4, d); }); + handleGamepadButton(SDL_GAMEPAD_BUTTON_LEFT_STICK, m_state.buttonState[SDL_GAMEPAD_BUTTON_LEFT_STICK], SDL_GetGamepadButton(m_gamepad, SDL_GAMEPAD_BUTTON_LEFT_STICK), [&](bool d){ if (d) TheMessageStream->appendMessage(GameMessage::MSG_META_SELECT_NEXT_IDLE_WORKER); }); + handleGamepadButton(SDL_GAMEPAD_BUTTON_RIGHT_STICK, m_state.buttonState[SDL_GAMEPAD_BUTTON_RIGHT_STICK], SDL_GetGamepadButton(m_gamepad, SDL_GAMEPAD_BUTTON_RIGHT_STICK), [&](bool d){ if (d) TheMessageStream->appendMessage(GameMessage::MSG_META_VIEW_COMMAND_CENTER); }); +} diff --git a/Core/GameEngineDevice/Source/SDL3GameEngine.cpp b/Core/GameEngineDevice/Source/SDL3GameEngine.cpp new file mode 100644 index 00000000000..fe9dd205247 --- /dev/null +++ b/Core/GameEngineDevice/Source/SDL3GameEngine.cpp @@ -0,0 +1,382 @@ +/* +** Command & Conquer Generals Zero Hour(tm) +** Copyright 2025 TheSuperHackers +** +** 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 3 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 . +*/ + +#include "Lib/BaseType.h" + +#include +#include +#include +#include + +#include "Common/GameEngine.h" +#include "SDL3GameEngine.h" +#include "SDL3Device/GameClient/SDL3Input.h" +#include "MilesAudioDevice/MilesAudioManager.h" +#include "GameClient/Mouse.h" +#include "GameClient/Keyboard.h" +#include "GameClient/GameWindow.h" +#include "GameClient/GameWindowManager.h" +#include "GameClient/Gadget.h" +#include "GameNetwork/LANAPICallbacks.h" +#include "GameNetwork/NetworkInterface.h" +#include "GameLogic/GameLogic.h" +#include "W3DDevice/GameLogic/W3DGameLogic.h" +#include "W3DDevice/GameClient/W3DGameClient.h" +#include "W3DDevice/Common/W3DModuleFactory.h" +#include "W3DDevice/Common/W3DThingFactory.h" +#include "W3DDevice/Common/W3DFunctionLexicon.h" +#include "W3DDevice/Common/W3DRadar.h" +#include "W3DDevice/GameClient/W3DParticleSys.h" +#include "W3DDevice/GameClient/W3DWebBrowser.h" +#include "StdDevice/Common/StdLocalFileSystem.h" +#include "StdDevice/Common/StdBIGFileSystem.h" + +// Extern globals for input devices (set by GameClient) +extern Mouse *TheMouse; +extern Keyboard *TheKeyboard; +extern GameWindowManager *TheWindowManager; + +namespace { + +Bool DecodeNextUtf8Codepoint(const char* text, size_t length, size_t& offset, UnsignedInt& outCodepoint) +{ + outCodepoint = 0; + if (!text || offset >= length) { + return false; + } + + const unsigned char first = static_cast(text[offset]); + if (first == 0) { + return false; + } + + if (first < 0x80) { + outCodepoint = first; + offset += 1; + return true; + } + + if ((first & 0xE0) == 0xC0 && offset + 1 < length) { + const unsigned char second = static_cast(text[offset + 1]); + if ((second & 0xC0) == 0x80) { + outCodepoint = ((first & 0x1F) << 6) | (second & 0x3F); + offset += 2; + return true; + } + } + + if ((first & 0xF0) == 0xE0 && offset + 2 < length) { + const unsigned char second = static_cast(text[offset + 1]); + const unsigned char third = static_cast(text[offset + 2]); + if ((second & 0xC0) == 0x80 && (third & 0xC0) == 0x80) { + outCodepoint = ((first & 0x0F) << 12) | ((second & 0x3F) << 6) | (third & 0x3F); + offset += 3; + return true; + } + } + + if ((first & 0xF8) == 0xF0 && offset + 3 < length) { + const unsigned char second = static_cast(text[offset + 1]); + const unsigned char third = static_cast(text[offset + 2]); + const unsigned char fourth = static_cast(text[offset + 3]); + if ((second & 0xC0) == 0x80 && (third & 0xC0) == 0x80 && (fourth & 0xC0) == 0x80) { + outCodepoint = ((first & 0x07) << 18) | ((second & 0x3F) << 12) | ((third & 0x3F) << 6) | (fourth & 0x3F); + offset += 4; + return true; + } + } + + // Invalid UTF-8 sequence: skip one byte and keep processing. + offset += 1; + return false; +} + +} + +/** + * Constructor: Initialize SDL3 game engine state + */ +SDL3GameEngine::SDL3GameEngine() + : GameEngine(), + m_SDLWindow(nullptr), + m_IsInitialized(false), + m_IsActive(false), + m_IsTextInputActive(false), + m_TextInputFocusWindow(nullptr) +{ +} + +/** + * Destructor: Cleanup SDL3 resources + */ +SDL3GameEngine::~SDL3GameEngine() +{ + if (m_SDLWindow && m_IsTextInputActive) { + SDL_StopTextInput(m_SDLWindow); + m_IsTextInputActive = false; + m_TextInputFocusWindow = nullptr; + } + + if (TheSDL3InputManager) { + delete TheSDL3InputManager; + } +} + +/** + * From GameEngine: init() - initialize subsystems + */ +void SDL3GameEngine::init(void) +{ + // Verify window was created by SDL3Main integration + extern SDL_Window* TheSDL3Window; + extern HWND ApplicationHWnd; + + if (!TheSDL3Window || !ApplicationHWnd) { + return; + } + + // Store window reference locally + m_SDLWindow = TheSDL3Window; + m_IsInitialized = true; + m_IsActive = true; + + // Initialize the unified input manager + if (!TheSDL3InputManager) { + NEW SDL3InputManager(); + } + + // Call parent init to initialize game subsystems + GameEngine::init(); +} + +/** + * From GameEngine: reset() - reset system to starting state + */ +void SDL3GameEngine::reset(void) +{ + if (m_SDLWindow && m_IsTextInputActive) { + SDL_StopTextInput(m_SDLWindow); + m_IsTextInputActive = false; + m_TextInputFocusWindow = nullptr; + } + GameEngine::reset(); +} + +/** + * From GameEngine: update() - per-frame update + */ +void SDL3GameEngine::update(void) +{ + pollSDL3Events(); + GameEngine::update(); + + // If the window is minimized, enter a throttled loop to save resources + // while keeping the network connection alive, matching legacy Win32 behavior. + if (m_SDLWindow && (SDL_GetWindowFlags(m_SDLWindow) & SDL_WINDOW_MINIMIZED)) + { + while (m_SDLWindow && (SDL_GetWindowFlags(m_SDLWindow) & SDL_WINDOW_MINIMIZED)) + { + // Prevent CPU/GPU pinning while alt-tabbed + SDL_Delay(5); + + // Stay responsive to events (so we can see when we're un-minimized) + pollSDL3Events(); + + // Keep the LAN subsystem alive to prevent multiplayer disconnects + if (TheLAN != nullptr) { + TheLAN->setIsActive(isActive()); + TheLAN->update(); + } + + // If we are in a network game, we must NOT stay in this loop, + // as the engine needs to keep pumping logic frames to avoid desyncs. + if (getQuitting() || (TheGameLogic && (TheGameLogic->isInInternetGame() || TheGameLogic->isInLanGame()))) { + break; + } + } + } +} + +/** + * From GameEngine: serviceWindowsOS() - native OS service + */ +void SDL3GameEngine::serviceWindowsOS(void) +{ + pollSDL3Events(); +} + +/** + * Check if game has OS focus + */ +Bool SDL3GameEngine::isActive(void) +{ + return m_IsActive; +} + +/** + * Set OS focus status + */ +void SDL3GameEngine::setIsActive(Bool isActive) +{ + m_IsActive = isActive; +} + +/** + * Poll and process SDL3 events + */ +void SDL3GameEngine::pollSDL3Events(void) +{ + if (!m_SDLWindow || !TheSDL3InputManager) { + return; + } + + updateTextInputState(); + + // Process all events via the dedicated manager + TheSDL3InputManager->update(); + + // Check if we should quit + if (TheSDL3InputManager->isQuitting()) { + m_quitting = true; + } +} + +void SDL3GameEngine::updateTextInputState(void) +{ + if (!m_SDLWindow || !TheWindowManager) { + return; + } + + GameWindow* focusedWindow = TheWindowManager->winGetFocus(); + const Bool wantsTextInput = + focusedWindow != nullptr && BitIsSet(focusedWindow->winGetStyle(), GWS_ENTRY_FIELD); + + if (wantsTextInput) { + if (!m_IsTextInputActive) { + if (SDL_StartTextInput(m_SDLWindow)) { + m_IsTextInputActive = true; + } + } + m_TextInputFocusWindow = focusedWindow; + } else { + if (m_IsTextInputActive) { + SDL_StopTextInput(m_SDLWindow); + m_IsTextInputActive = false; + } + m_TextInputFocusWindow = nullptr; + } +} + +void SDL3GameEngine::forwardTextInputEvent(const char* utf8Text) +{ + if (!utf8Text || !TheWindowManager) { + return; + } + + GameWindow* targetWindow = m_TextInputFocusWindow; + if (!targetWindow || !BitIsSet(targetWindow->winGetStyle(), GWS_ENTRY_FIELD)) { + return; + } + + const size_t textLength = strlen(utf8Text); + size_t offset = 0; + while (offset < textLength) { + UnsignedInt codepoint = 0; + if (!DecodeNextUtf8Codepoint(utf8Text, textLength, offset, codepoint)) { + continue; + } + + if (codepoint == 0 || codepoint > 0x10FFFFU) { + continue; + } + + if (codepoint >= 0xD800U && codepoint <= 0xDFFFU) { + continue; + } + + if (codepoint > 0xFFFFU) { + continue; + } + + const WideChar wideCharacter = static_cast(codepoint); + TheWindowManager->winSendInputMsg(targetWindow, GWM_IME_CHAR, static_cast(wideCharacter), 0); + } +} + +/** + * Factory Methods for GameEngine subsystems + */ + +LocalFileSystem *SDL3GameEngine::createLocalFileSystem(void) +{ + return NEW StdLocalFileSystem; +} + +ArchiveFileSystem *SDL3GameEngine::createArchiveFileSystem(void) +{ + return NEW StdBIGFileSystem; +} + +GameLogic *SDL3GameEngine::createGameLogic(void) +{ + return NEW W3DGameLogic; +} + +GameClient *SDL3GameEngine::createGameClient(void) +{ + return NEW W3DGameClient; +} + +ModuleFactory *SDL3GameEngine::createModuleFactory(void) +{ + return NEW W3DModuleFactory; +} + +ThingFactory *SDL3GameEngine::createThingFactory(void) +{ + return NEW W3DThingFactory; +} + +FunctionLexicon *SDL3GameEngine::createFunctionLexicon(void) +{ + return NEW W3DFunctionLexicon; +} + +Radar *SDL3GameEngine::createRadar(Bool dummy) +{ + (void)dummy; + return NEW W3DRadar; +} + +ParticleSystemManager* SDL3GameEngine::createParticleSystemManager(Bool dummy) +{ + (void)dummy; + return NEW W3DParticleSystemManager; +} + +WebBrowser *SDL3GameEngine::createWebBrowser(void) +{ + return nullptr; +} + +AudioManager *SDL3GameEngine::createAudioManager(Bool dummy) +{ + if (dummy) + return NEW MilesAudioManagerDummy; + return NEW MilesAudioManager; +} diff --git a/GeneralsMD/Code/GameEngine/Include/GameClient/Display.h b/GeneralsMD/Code/GameEngine/Include/GameClient/Display.h index 8c022244206..038d84b1260 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameClient/Display.h +++ b/GeneralsMD/Code/GameEngine/Include/GameClient/Display.h @@ -80,6 +80,11 @@ class Display : public SubsystemInterface virtual UnsignedInt getBitDepth() { return m_bitDepth; } virtual void setWindowed( Bool windowed ) { m_windowed = windowed; } ///< set windowed/fullscreen flag virtual Bool getWindowed() { return m_windowed; } ///< return widowed/fullscreen flag + +#if SAGE_USE_SDL3 + virtual Bool getViewportRect( Int& x, Int& y, Int& width, Int& height ) const { return FALSE; } +#endif + virtual Bool setDisplayMode( UnsignedInt xres, UnsignedInt yres, UnsignedInt bitdepth, Bool windowed ); /// + #include "SDL3GameEngine.h" + #include "GameClient/Keyboard.h" + SDL_Window* TheSDL3Window = nullptr; +#else + #include "Win32Device/Common/Win32GameEngine.h" + #include "Win32Device/GameClient/Win32Mouse.h" +#endif + #include "GeneratedVersion.h" #include "resource.h" @@ -87,6 +96,9 @@ static Bool gDoPaint = true; static Bool isWinMainActive = false; static HBITMAP gLoadScreenBitmap = nullptr; +#if SAGE_USE_SDL3 +static SDL_Surface* gLoadScreenSurface = nullptr; +#endif //#define DEBUG_WINDOWS_MESSAGES @@ -855,14 +867,23 @@ Int APIENTRY WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, if (fileImage) { fclose(fileImage); gLoadScreenBitmap = (HBITMAP)LoadImage(hInstance, filePath, IMAGE_BITMAP, 0, 0, LR_SHARED|LR_LOADFROMFILE); +#if SAGE_USE_SDL3 + gLoadScreenSurface = SDL_LoadBMP(filePath); +#endif } else { gLoadScreenBitmap = (HBITMAP)LoadImage(hInstance, fileName, IMAGE_BITMAP, 0, 0, LR_SHARED|LR_LOADFROMFILE); +#if SAGE_USE_SDL3 + gLoadScreenSurface = SDL_LoadBMP(fileName); +#endif } #else // in release, the file only ever lives in the root dir gLoadScreenBitmap = (HBITMAP)LoadImage(hInstance, "Install_Final.bmp", IMAGE_BITMAP, 0, 0, LR_SHARED|LR_LOADFROMFILE); +#if SAGE_USE_SDL3 + gLoadScreenSurface = SDL_LoadBMP("Install_Final.bmp"); +#endif #endif CommandLine::parseCommandLineForStartup(); @@ -872,18 +893,78 @@ Int APIENTRY WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, #endif // register windows class and create application window +#if SAGE_USE_SDL3 + if (!TheGlobalData->m_headless) + { + if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_EVENTS | SDL_INIT_GAMEPAD) == 0) + { + DEBUG_LOG(("SDL_Init failed: %s", SDL_GetError())); + return exitcode; + } + + Uint32 flags = SDL_WINDOW_HIDDEN | SDL_WINDOW_RESIZABLE; + if (!TheGlobalData->m_windowed) flags |= SDL_WINDOW_FULLSCREEN; + + TheSDL3Window = SDL_CreateWindow("Command & Conquer Generals", 800, 600, flags); + if (!TheSDL3Window) + { + DEBUG_LOG(("SDL_CreateWindow failed: %s", SDL_GetError())); + return exitcode; + } + + // Retrieve the native HWND from the SDL window for D3D compatibility + SDL_PropertiesID props = SDL_GetWindowProperties(TheSDL3Window); + ApplicationHWnd = (HWND)SDL_GetPointerProperty(props, SDL_PROP_WINDOW_WIN32_HWND_POINTER, NULL); + + // Set initial window size from global data if available + Int startWidth = TheGlobalData->m_xResolution; + Int startHeight = TheGlobalData->m_yResolution; + if (startWidth > 0 && startHeight > 0) + { + SDL_SetWindowSize(TheSDL3Window, startWidth, startHeight); + } + + SDL_ShowWindow(TheSDL3Window); + isWinMainActive = true; + + // Draw the splash screen immediately for SDL3 using safe software surface (8-line modernization) + if (gLoadScreenSurface != nullptr) + { + SDL_Surface* screen = SDL_GetWindowSurface(TheSDL3Window); + if (screen) + { + SDL_ClearSurface(screen, 0.0f, 0.0f, 0.0f, 1.0f); + float bitmapAspect = 800.0f / 600.0f; + int drawWidth = (float)screen->w / screen->h > bitmapAspect ? (int)(screen->h * bitmapAspect) : screen->w; + int drawHeight = (float)screen->w / screen->h > bitmapAspect ? screen->h : (int)(screen->w / bitmapAspect); + SDL_Rect destRect = { (screen->w - drawWidth) / 2, (screen->h - drawHeight) / 2, drawWidth, drawHeight }; + SDL_BlitSurfaceScaled(gLoadScreenSurface, NULL, screen, &destRect, SDL_SCALEMODE_LINEAR); + SDL_UpdateWindowSurface(TheSDL3Window); + } + } + + } +#else if(!TheGlobalData->m_headless && initializeAppWindows(hInstance, nCmdShow, TheGlobalData->m_windowed) == false) { return exitcode; } +#endif // save our application instance for future use ApplicationHInstance = hInstance; - if (gLoadScreenBitmap!=nullptr) { +#if SAGE_USE_SDL3 + if (gLoadScreenSurface != nullptr) { + SDL_DestroySurface(gLoadScreenSurface); + gLoadScreenSurface = nullptr; + } +#else + if (gLoadScreenBitmap != nullptr) { ::DeleteObject(gLoadScreenBitmap); gLoadScreenBitmap = nullptr; } +#endif // BGC - initialize COM @@ -957,6 +1038,11 @@ Int APIENTRY WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, //============================================================================= GameEngine *CreateGameEngine() { +#if SAGE_USE_SDL3 + SDL3GameEngine *engine = NEW SDL3GameEngine; + engine->setIsActive(isWinMainActive); + return engine; +#else Win32GameEngine *engine; engine = NEW Win32GameEngine; @@ -965,5 +1051,5 @@ GameEngine *CreateGameEngine() engine->setIsActive(isWinMainActive); return engine; - +#endif } diff --git a/cmake/config-build.cmake b/cmake/config-build.cmake index b28f5c0b760..923be92abe2 100644 --- a/cmake/config-build.cmake +++ b/cmake/config-build.cmake @@ -9,6 +9,16 @@ option(RTS_BUILD_OPTION_ASAN "Build code with Address Sanitizer." OFF) option(RTS_BUILD_OPTION_VC6_FULL_DEBUG "Build VC6 with full debug info." OFF) option(RTS_BUILD_OPTION_FFMPEG "Enable FFmpeg support" OFF) +# Enable SDL3 by default for modern builds +if(NOT IS_VS6_BUILD) + option(SAGE_USE_SDL3 "Enable SDL3 input/window backend" ON) + if(SAGE_USE_SDL3) + target_compile_definitions(core_config INTERFACE SAGE_USE_SDL3=1) + endif() +else() + set(SAGE_USE_SDL3 OFF CACHE BOOL "Enable SDL3 input/window backend" FORCE) +endif() + if(NOT RTS_BUILD_ZEROHOUR AND NOT RTS_BUILD_GENERALS) set(RTS_BUILD_ZEROHOUR TRUE) message("You must select one project to build, building Zero Hour by default.") diff --git a/cmake/sdl3.cmake b/cmake/sdl3.cmake new file mode 100644 index 00000000000..8a5ed3728b8 --- /dev/null +++ b/cmake/sdl3.cmake @@ -0,0 +1,95 @@ +include(FetchContent) + +# GeneralsX @build felipebraz 17/04/2026 SDL3 Dependency +# Download and build SDL3 from source as a static library. +# This avoids manual installation and keeps the repository clean. + +set(SDL_TESTS OFF CACHE BOOL "" FORCE) +set(SDL_EXAMPLES OFF CACHE BOOL "" FORCE) +set(SDL_INSTALL OFF CACHE BOOL "" FORCE) +set(SDL_STATIC ON CACHE BOOL "" FORCE) +set(SDL_SHARED OFF CACHE BOOL "" FORCE) + +# Minimal build for Generals/Zero Hour engine integration. + +# Disable Subsystems +set(SDL_RENDER OFF CACHE BOOL "" FORCE) # Disables all hardware renderers (D3D11, D3D12, Vulkan, GL) +set(SDL_HAPTIC OFF CACHE BOOL "" FORCE) +set(SDL_POWER OFF CACHE BOOL "" FORCE) +set(SDL_SENSOR OFF CACHE BOOL "" FORCE) +set(SDL_HIDAPI OFF CACHE BOOL "" FORCE) + +# Disable External Platform Support +set(SDL_X11 OFF CACHE BOOL "" FORCE) +set(SDL_WAYLAND OFF CACHE BOOL "" FORCE) +set(SDL_VULKAN OFF CACHE BOOL "" FORCE) +set(SDL_METAL OFF CACHE BOOL "" FORCE) + +# Disable Misc Features +set(SDL_CAMERA OFF CACHE BOOL "" FORCE) +set(SDL_DIALOG OFF CACHE BOOL "" FORCE) +set(SDL_LOCALE OFF CACHE BOOL "" FORCE) +set(SDL_MISC OFF CACHE BOOL "" FORCE) +set(SDL_OFFSCREEN OFF CACHE BOOL "" FORCE) +set(SDL_VIRTUAL_JOYSTICK OFF CACHE BOOL "" FORCE) + +# SDL3 - Core library (v3.4.2) +FetchContent_Declare( + SDL3 + URL https://github.com/libsdl-org/SDL/releases/download/release-3.4.2/SDL3-3.4.2.tar.gz + URL_HASH SHA256=ef39a2e3f9a8a78296c40da701967dd1b0d0d6e267e483863ce70f8a03b4050c +) + +# SDL3_image - Image loading support (v3.4.0) +# --- SDL3_IMAGE CATEGORIES --- + +# Disable Metadata/Packaging +set(SDLIMAGE_SAMPLES OFF CACHE BOOL "" FORCE) +set(SDLIMAGE_TESTS OFF CACHE BOOL "" FORCE) +set(SDLIMAGE_INSTALL OFF CACHE BOOL "" FORCE) +set(SDLIMAGE_VENDORED OFF CACHE BOOL "" FORCE) +set(SDLIMAGE_BACKEND_WIC OFF CACHE BOOL "" FORCE) # Avoid LNK2005 + +# Disable Codecs (minimal set) +set(SDLIMAGE_BACKEND_STB OFF CACHE BOOL "" FORCE) +set(SDLIMAGE_JPG OFF CACHE BOOL "" FORCE) +set(SDLIMAGE_PNG OFF CACHE BOOL "" FORCE) +set(SDLIMAGE_APNG OFF CACHE BOOL "" FORCE) # Fixes 'APNG_ENABLED not defined' warning +set(SDLIMAGE_WEBP OFF CACHE BOOL "" FORCE) +set(SDLIMAGE_TIF OFF CACHE BOOL "" FORCE) +set(SDLIMAGE_AVIF OFF CACHE BOOL "" FORCE) +set(SDLIMAGE_JXL OFF CACHE BOOL "" FORCE) +set(SDLIMAGE_QOI OFF CACHE BOOL "" FORCE) +set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE) # Ensure static for SDL3_image + +FetchContent_Declare( + SDL3_image + URL https://github.com/libsdl-org/SDL_image/releases/download/release-3.4.0/SDL3_image-3.4.0.tar.gz + URL_HASH SHA256=2ceb75eab4235c2c7e93dafc3ef3268ad368ca5de40892bf8cffdd510f29d9d8 +) + +FetchContent_MakeAvailable(SDL3) + +# Trick SDL3_image into thinking SDL3 is already found to avoid the broken find_package() in the build tree. +# SDL3_image specifically checks for SDL3::Headers and SDL3::SDL3 (for static builds). +if(NOT TARGET SDL3::Headers) + if(TARGET SDL3_Headers) + add_library(SDL3::Headers ALIAS SDL3_Headers) + else() + add_library(SDL3::Headers INTERFACE IMPORTED GLOBAL) + target_include_directories(SDL3::Headers INTERFACE "${sdl3_SOURCE_DIR}/include") + endif() +endif() + +if(NOT TARGET SDL3::SDL3) + if(TARGET SDL3-static) + add_library(SDL3::SDL3 ALIAS SDL3-static) + elseif(TARGET SDL3-shared) + add_library(SDL3::SDL3 ALIAS SDL3-shared) + endif() +endif() + +set(SDL3_FOUND TRUE CACHE BOOL "" FORCE) +set(SDL3_VERSION "3.4.2" CACHE STRING "" FORCE) + +FetchContent_MakeAvailable(SDL3_image) From 8df9b8678160a474547c56bbee5315c174bd0a12 Mon Sep 17 00:00:00 2001 From: githubawn <115191165+githubawn@users.noreply.github.com> Date: Mon, 20 Apr 2026 17:00:36 +0200 Subject: [PATCH 02/18] SDL3 Input: Complete backport with Gamepad support and hardening Consolidated all work from the test/sdl3-backport branch into a single atomic commit: - Centralized input management via SDL3InputManager. - Hardened Ani/RIFF cursor loading with robust bounds checking. - Native Gamepad support with analogue stick-to-mouse emulation and custom RTS mappings. - Modernized focus and capture handling for better stability during Alt-Tab. - Standardized and secured string operations throughout the SDL3 path. - Updated credit attribution (fbraz3). --- .../Include/SDL3Device/GameClient/SDL3Input.h | 4 ++++ Core/GameEngineDevice/Include/SDL3GameEngine.h | 4 ++++ .../Source/SDL3Device/GameClient/SDL3Input.cpp | 4 ++++ Core/GameEngineDevice/Source/SDL3GameEngine.cpp | 4 ++++ 4 files changed, 16 insertions(+) diff --git a/Core/GameEngineDevice/Include/SDL3Device/GameClient/SDL3Input.h b/Core/GameEngineDevice/Include/SDL3Device/GameClient/SDL3Input.h index 29f8fdcf17f..74d7cb8ac03 100644 --- a/Core/GameEngineDevice/Include/SDL3Device/GameClient/SDL3Input.h +++ b/Core/GameEngineDevice/Include/SDL3Device/GameClient/SDL3Input.h @@ -16,6 +16,10 @@ ** along with this program. If not, see . */ +/* +** Derived from the GeneralsX branch by fbraz3 +*/ + #pragma once #include "Lib/BaseType.h" diff --git a/Core/GameEngineDevice/Include/SDL3GameEngine.h b/Core/GameEngineDevice/Include/SDL3GameEngine.h index a85a6b575c2..1c86d77b0a9 100644 --- a/Core/GameEngineDevice/Include/SDL3GameEngine.h +++ b/Core/GameEngineDevice/Include/SDL3GameEngine.h @@ -16,6 +16,10 @@ ** along with this program. If not, see . */ +/* +** Derived from the GeneralsX branch by fbraz3 +*/ + #pragma once #include "Lib/BaseType.h" diff --git a/Core/GameEngineDevice/Source/SDL3Device/GameClient/SDL3Input.cpp b/Core/GameEngineDevice/Source/SDL3Device/GameClient/SDL3Input.cpp index 44291b80076..7889d343e0b 100644 --- a/Core/GameEngineDevice/Source/SDL3Device/GameClient/SDL3Input.cpp +++ b/Core/GameEngineDevice/Source/SDL3Device/GameClient/SDL3Input.cpp @@ -16,6 +16,10 @@ ** along with this program. If not, see . */ +/* +** Derived from the GeneralsX branch by fbraz3 +*/ + #include "Lib/BaseType.h" #define _USE_MATH_DEFINES diff --git a/Core/GameEngineDevice/Source/SDL3GameEngine.cpp b/Core/GameEngineDevice/Source/SDL3GameEngine.cpp index fe9dd205247..f6c96c4904d 100644 --- a/Core/GameEngineDevice/Source/SDL3GameEngine.cpp +++ b/Core/GameEngineDevice/Source/SDL3GameEngine.cpp @@ -16,6 +16,10 @@ ** along with this program. If not, see . */ +/* +** Derived from the GeneralsX branch by fbraz3 +*/ + #include "Lib/BaseType.h" #include From e3b674b2aed78cb060e0c1c2107427818bb0701e Mon Sep 17 00:00:00 2001 From: githubawn <115191165+githubawn@users.noreply.github.com> Date: Mon, 20 Apr 2026 17:09:32 +0200 Subject: [PATCH 03/18] added build guards --- CMakeLists.txt | 5 ++++- cmake/sdl3.cmake | 4 ++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 0c7dfa916b0..56d559d7d83 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -53,7 +53,6 @@ if((WIN32 OR "${CMAKE_SYSTEM}" MATCHES "Windows") AND ${CMAKE_SIZEOF_VOID_P} EQU include(cmake/miles.cmake) include(cmake/bink.cmake) include(cmake/dx8.cmake) - include(cmake/sdl3.cmake) endif() # Define a dummy stlport target when not on VC6. @@ -67,6 +66,10 @@ include(cmake/config.cmake) include(cmake/gamespy.cmake) include(cmake/lzhl.cmake) +if(SAGE_USE_SDL3 AND NOT IS_VS6_BUILD) + include(cmake/sdl3.cmake) +endif() + if (IS_VS6_BUILD) # The original max sdk does not compile against a modern compiler. # If there is a desire to make this work, then a fixed max sdk needs to be created. diff --git a/cmake/sdl3.cmake b/cmake/sdl3.cmake index 8a5ed3728b8..ebb4e16bc25 100644 --- a/cmake/sdl3.cmake +++ b/cmake/sdl3.cmake @@ -1,5 +1,9 @@ include(FetchContent) +if(NOT SAGE_USE_SDL3 OR IS_VS6_BUILD) + return() +endif() + # GeneralsX @build felipebraz 17/04/2026 SDL3 Dependency # Download and build SDL3 from source as a static library. # This avoids manual installation and keeps the repository clean. From 2499e0b8e7e51f210bd078cb8958a8f99f0ceddd Mon Sep 17 00:00:00 2001 From: githubawn <115191165+githubawn@users.noreply.github.com> Date: Mon, 20 Apr 2026 17:18:08 +0200 Subject: [PATCH 04/18] greptile feedback --- .../Include/SDL3Device/GameClient/SDL3Input.h | 2 +- Core/GameEngineDevice/Include/SDL3GameEngine.h | 2 +- .../Source/SDL3Device/GameClient/SDL3Input.cpp | 9 ++++----- Core/GameEngineDevice/Source/SDL3GameEngine.cpp | 2 +- 4 files changed, 7 insertions(+), 8 deletions(-) diff --git a/Core/GameEngineDevice/Include/SDL3Device/GameClient/SDL3Input.h b/Core/GameEngineDevice/Include/SDL3Device/GameClient/SDL3Input.h index 74d7cb8ac03..57e3eca463a 100644 --- a/Core/GameEngineDevice/Include/SDL3Device/GameClient/SDL3Input.h +++ b/Core/GameEngineDevice/Include/SDL3Device/GameClient/SDL3Input.h @@ -1,6 +1,6 @@ /* ** Command & Conquer Generals Zero Hour(tm) -** Copyright 2025 TheSuperHackers +** Copyright 2026 TheSuperHackers ** ** 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 diff --git a/Core/GameEngineDevice/Include/SDL3GameEngine.h b/Core/GameEngineDevice/Include/SDL3GameEngine.h index 1c86d77b0a9..8b854886993 100644 --- a/Core/GameEngineDevice/Include/SDL3GameEngine.h +++ b/Core/GameEngineDevice/Include/SDL3GameEngine.h @@ -1,6 +1,6 @@ /* ** Command & Conquer Generals Zero Hour(tm) -** Copyright 2025 TheSuperHackers +** Copyright 2026 TheSuperHackers ** ** 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 diff --git a/Core/GameEngineDevice/Source/SDL3Device/GameClient/SDL3Input.cpp b/Core/GameEngineDevice/Source/SDL3Device/GameClient/SDL3Input.cpp index 7889d343e0b..889b4d81128 100644 --- a/Core/GameEngineDevice/Source/SDL3Device/GameClient/SDL3Input.cpp +++ b/Core/GameEngineDevice/Source/SDL3Device/GameClient/SDL3Input.cpp @@ -1,6 +1,6 @@ /* ** Command & Conquer Generals Zero Hour(tm) -** Copyright 2025 TheSuperHackers +** Copyright 2026 TheSuperHackers ** ** 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 @@ -58,7 +58,6 @@ SDL3InputManager* TheSDL3InputManager = nullptr; struct AnimatedCursor { std::array m_frameCursors; std::array m_frameSurfaces; - int m_currentFrame = 0; int m_frameCount = 0; int m_frameRate = 0; // the time a frame is displayed in 1/60th of a second @@ -95,7 +94,7 @@ struct AnimatedCursor { Uint64 now = SDL_GetTicks(); size_t index = (m_frameRate > 0) - ? (size_t)((now * 60 / 1000) / m_frameRate) % m_frameCount + ? (size_t)((now * 60 / 1000) / m_frameRate) % (size_t)std::min((int)m_frameCount, MAX_2D_CURSOR_ANIM_FRAMES) : 0; return m_frameCursors[index]; } @@ -213,7 +212,7 @@ static AnimatedCursor* loadANI(const char* filepath) } ANIHeader *ani_header = (ANIHeader*)getChunkData(chunk); - cursor->m_frameCount = ani_header->frames; + cursor->m_frameCount = (int)std::min((unsigned int)ani_header->frames, (unsigned int)MAX_2D_CURSOR_ANIM_FRAMES); cursor->m_frameRate = ani_header->displayRate; } else if (chunk->id == list_id && chunk->type == fram_id) @@ -1084,7 +1083,7 @@ void SDL3InputManager::processGamepadInput() motionEvent.motion.y = my + motionEvent.motion.yrel; addMouseSDLEvent(motionEvent); - SDL_WarpMouseInWindow(NULL, motionEvent.motion.x, motionEvent.motion.y); + SDL_WarpMouseInWindow(nullptr, motionEvent.motion.x, motionEvent.motion.y); } float rx = SDL_GetGamepadAxis(m_gamepad, SDL_GAMEPAD_AXIS_RIGHTX) / AXIS_MAX; diff --git a/Core/GameEngineDevice/Source/SDL3GameEngine.cpp b/Core/GameEngineDevice/Source/SDL3GameEngine.cpp index f6c96c4904d..03025768e05 100644 --- a/Core/GameEngineDevice/Source/SDL3GameEngine.cpp +++ b/Core/GameEngineDevice/Source/SDL3GameEngine.cpp @@ -1,6 +1,6 @@ /* ** Command & Conquer Generals Zero Hour(tm) -** Copyright 2025 TheSuperHackers +** Copyright 2026 TheSuperHackers ** ** 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 From f8fb43f6a604cb14574c3851dfb19888724ef2ae Mon Sep 17 00:00:00 2001 From: githubawn <115191165+githubawn@users.noreply.github.com> Date: Mon, 20 Apr 2026 17:29:45 +0200 Subject: [PATCH 05/18] greptile feedback --- .../Include/SDL3Device/GameClient/SDL3Input.h | 1 + .../SDL3Device/GameClient/SDL3Input.cpp | 21 +++++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/Core/GameEngineDevice/Include/SDL3Device/GameClient/SDL3Input.h b/Core/GameEngineDevice/Include/SDL3Device/GameClient/SDL3Input.h index 57e3eca463a..66a583ec9f6 100644 --- a/Core/GameEngineDevice/Include/SDL3Device/GameClient/SDL3Input.h +++ b/Core/GameEngineDevice/Include/SDL3Device/GameClient/SDL3Input.h @@ -59,6 +59,7 @@ class SDL3Mouse : public Mouse virtual void reset(void) override; virtual void update(void) override; virtual void initCursorResources(void) override; + static void freeCursorResources(void); // Mouse interface virtual void setCursor(MouseCursor cursor) override; diff --git a/Core/GameEngineDevice/Source/SDL3Device/GameClient/SDL3Input.cpp b/Core/GameEngineDevice/Source/SDL3Device/GameClient/SDL3Input.cpp index 889b4d81128..33837ac80d9 100644 --- a/Core/GameEngineDevice/Source/SDL3Device/GameClient/SDL3Input.cpp +++ b/Core/GameEngineDevice/Source/SDL3Device/GameClient/SDL3Input.cpp @@ -439,6 +439,21 @@ void SDL3Mouse::initCursorResources(void) } } +void SDL3Mouse::freeCursorResources(void) +{ + for (Int cursor = 0; cursor < Mouse::NUM_MOUSE_CURSORS; cursor++) + { + for (Int direction = 0; direction < MAX_2D_CURSOR_DIRECTIONS; direction++) + { + if (cursorResources[cursor][direction]) + { + delete cursorResources[cursor][direction]; + cursorResources[cursor][direction] = nullptr; + } + } + } +} + AnimatedCursor* SDL3Mouse::loadCursorFromFile(const char* filepath) { return loadANI(filepath); @@ -859,6 +874,7 @@ SDL3InputManager::SDL3InputManager() SDL3InputManager::~SDL3InputManager() { closeGamepad(); + SDL3Mouse::freeCursorResources(); TheSDL3InputManager = nullptr; } @@ -1081,6 +1097,11 @@ void SDL3InputManager::processGamepadInput() SDL_GetMouseState(&mx, &my); motionEvent.motion.x = mx + motionEvent.motion.xrel; motionEvent.motion.y = my + motionEvent.motion.yrel; + + if (m_SDLWindow) + { + motionEvent.motion.windowID = SDL_GetWindowID(m_SDLWindow); + } addMouseSDLEvent(motionEvent); SDL_WarpMouseInWindow(nullptr, motionEvent.motion.x, motionEvent.motion.y); From 4ac91e0d79056222fce381cadc0a41f791b7a52d Mon Sep 17 00:00:00 2001 From: githubawn <115191165+githubawn@users.noreply.github.com> Date: Mon, 20 Apr 2026 17:54:18 +0200 Subject: [PATCH 06/18] fixed build error --- .../Include/SDL3Device/GameClient/SDL3Input.h | 6 ++++-- .../Source/SDL3Device/GameClient/SDL3Input.cpp | 9 +++++---- Core/GameEngineDevice/Source/SDL3GameEngine.cpp | 2 +- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/Core/GameEngineDevice/Include/SDL3Device/GameClient/SDL3Input.h b/Core/GameEngineDevice/Include/SDL3Device/GameClient/SDL3Input.h index 66a583ec9f6..5d574178115 100644 --- a/Core/GameEngineDevice/Include/SDL3Device/GameClient/SDL3Input.h +++ b/Core/GameEngineDevice/Include/SDL3Device/GameClient/SDL3Input.h @@ -143,7 +143,7 @@ class SDL3Keyboard : public Keyboard class SDL3InputManager { public: - SDL3InputManager(); + SDL3InputManager(SDL_Window* window); virtual ~SDL3InputManager(); void update(); @@ -180,6 +180,9 @@ class SDL3InputManager // Gamepad management void openFirstGamepad(); void closeGamepad(); + + SDL_Window* m_window; + SDL_Gamepad* m_gamepad; void processGamepadInput(); void handleGamepadButton(SDL_GamepadButton button, bool& currentState, bool isDown, std::function action); @@ -200,7 +203,6 @@ class SDL3InputManager UnsignedInt m_keyNextGet; // Gamepad state - SDL_Gamepad* m_gamepad; GamepadState m_state; Bool m_precisionMode; diff --git a/Core/GameEngineDevice/Source/SDL3Device/GameClient/SDL3Input.cpp b/Core/GameEngineDevice/Source/SDL3Device/GameClient/SDL3Input.cpp index 33837ac80d9..7d8e45a9b26 100644 --- a/Core/GameEngineDevice/Source/SDL3Device/GameClient/SDL3Input.cpp +++ b/Core/GameEngineDevice/Source/SDL3Device/GameClient/SDL3Input.cpp @@ -853,8 +853,9 @@ KeyVal SDL3Keyboard::translateScanCodeToKeyVal(unsigned char scan) /** * Lifecycle */ -SDL3InputManager::SDL3InputManager() - : m_mouseNextFree(0), +SDL3InputManager::SDL3InputManager(SDL_Window* window) + : m_window(window), + m_mouseNextFree(0), m_mouseNextGet(0), m_keyNextFree(0), m_keyNextGet(0), @@ -1098,9 +1099,9 @@ void SDL3InputManager::processGamepadInput() motionEvent.motion.x = mx + motionEvent.motion.xrel; motionEvent.motion.y = my + motionEvent.motion.yrel; - if (m_SDLWindow) + if (m_window) { - motionEvent.motion.windowID = SDL_GetWindowID(m_SDLWindow); + motionEvent.motion.windowID = SDL_GetWindowID(m_window); } addMouseSDLEvent(motionEvent); diff --git a/Core/GameEngineDevice/Source/SDL3GameEngine.cpp b/Core/GameEngineDevice/Source/SDL3GameEngine.cpp index 03025768e05..c719c43b19d 100644 --- a/Core/GameEngineDevice/Source/SDL3GameEngine.cpp +++ b/Core/GameEngineDevice/Source/SDL3GameEngine.cpp @@ -161,7 +161,7 @@ void SDL3GameEngine::init(void) // Initialize the unified input manager if (!TheSDL3InputManager) { - NEW SDL3InputManager(); + TheSDL3InputManager = new SDL3InputManager(m_SDLWindow); } // Call parent init to initialize game subsystems From a0ac8f4c067550a2c0c3e12fd257e7fcdcaa0ebb Mon Sep 17 00:00:00 2001 From: githubawn <115191165+githubawn@users.noreply.github.com> Date: Mon, 20 Apr 2026 18:05:18 +0200 Subject: [PATCH 07/18] greptile feedback --- .../Source/SDL3Device/GameClient/SDL3Input.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Core/GameEngineDevice/Source/SDL3Device/GameClient/SDL3Input.cpp b/Core/GameEngineDevice/Source/SDL3Device/GameClient/SDL3Input.cpp index 7d8e45a9b26..be1daa3bce4 100644 --- a/Core/GameEngineDevice/Source/SDL3Device/GameClient/SDL3Input.cpp +++ b/Core/GameEngineDevice/Source/SDL3Device/GameClient/SDL3Input.cpp @@ -1044,6 +1044,11 @@ void SDL3InputManager::virtualPulseMouse(Uint8 button, bool down) SDL_GetMouseState(&mx, &my); clickEvent.button.x = mx; clickEvent.button.y = my; + + if (m_window) + { + clickEvent.button.windowID = SDL_GetWindowID(m_window); + } addMouseSDLEvent(clickEvent); } From d9d9ff0c2a486cfd915b1031b04ecf315ec091c7 Mon Sep 17 00:00:00 2001 From: githubawn <115191165+githubawn@users.noreply.github.com> Date: Mon, 20 Apr 2026 20:24:35 +0200 Subject: [PATCH 08/18] added vcpkg --- Core/GameEngineDevice/CMakeLists.txt | 5 +- cmake/sdl3.cmake | 131 ++++++++------------------- vcpkg-lock.json | 109 ++++++++++++---------- vcpkg.json | 29 +++++- 4 files changed, 125 insertions(+), 149 deletions(-) diff --git a/Core/GameEngineDevice/CMakeLists.txt b/Core/GameEngineDevice/CMakeLists.txt index a98ccadde9b..69ae180a1de 100644 --- a/Core/GameEngineDevice/CMakeLists.txt +++ b/Core/GameEngineDevice/CMakeLists.txt @@ -240,9 +240,8 @@ target_link_libraries(corei_gameenginedevice_public INTERFACE # Export SDL3 dependencies for modern builds if(SAGE_USE_SDL3 AND NOT IS_VS6_BUILD) target_link_libraries(corei_gameenginedevice_public INTERFACE - SDL3::Headers - SDL3::SDL3-static - SDL3_image::SDL3_image-static + SDL3::SDL3 + SDL3_image::SDL3_image ) endif() diff --git a/cmake/sdl3.cmake b/cmake/sdl3.cmake index ebb4e16bc25..ec34f4b1466 100644 --- a/cmake/sdl3.cmake +++ b/cmake/sdl3.cmake @@ -1,99 +1,40 @@ -include(FetchContent) - -if(NOT SAGE_USE_SDL3 OR IS_VS6_BUILD) - return() +# Standardized vcpkg integration: Try find_package first, fallback to source build if not found. +find_package(SDL3 CONFIG QUIET) +find_package(SDL3_image CONFIG QUIET) + +if(NOT SDL3_FOUND OR NOT SDL3_image_FOUND) + message(STATUS "SDL3 not found via vcpkg/find_package, falling back to source build (FetchContent)...") + include(FetchContent) + + FetchContent_Declare( + SDL3 + URL https://github.com/libsdl-org/SDL/releases/download/release-3.4.4/SDL3-3.4.4.tar.gz + URL_HASH SHA256=EE712DBE6A89BB140BBFC2CE72358FB5EE5CC2240ABEABD54855012DB30B3864 + OVERRIDE_FIND_PACKAGE + ) + + FetchContent_Declare( + SDL3_image + URL https://github.com/libsdl-org/SDL_image/releases/download/release-3.4.2/SDL3_image-3.4.2.tar.gz + URL_HASH SHA256=82fdb88cf1a9cbdc1c77797aaa3292e6d22ce12586be718c8ea43530df1536b4 + ) + + # Official SDL configuration for a unified build tree + set(SDL_SHARED ON CACHE BOOL "" FORCE) + set(SDL_STATIC OFF CACHE BOOL "" FORCE) + set(SDLIMAGE_VENDORED OFF CACHE BOOL "" FORCE) + set(SDLIMAGE_ZLIB ON CACHE BOOL "" FORCE) + set(SDLIMAGE_PNG ON CACHE BOOL "" FORCE) + set(SDLIMAGE_APNG ON CACHE BOOL "" FORCE) + + # Making them available in order ensures SDL3_image can see the SDL3 targets + FetchContent_MakeAvailable(SDL3 SDL3_image) endif() -# GeneralsX @build felipebraz 17/04/2026 SDL3 Dependency -# Download and build SDL3 from source as a static library. -# This avoids manual installation and keeps the repository clean. - -set(SDL_TESTS OFF CACHE BOOL "" FORCE) -set(SDL_EXAMPLES OFF CACHE BOOL "" FORCE) -set(SDL_INSTALL OFF CACHE BOOL "" FORCE) -set(SDL_STATIC ON CACHE BOOL "" FORCE) -set(SDL_SHARED OFF CACHE BOOL "" FORCE) - -# Minimal build for Generals/Zero Hour engine integration. - -# Disable Subsystems -set(SDL_RENDER OFF CACHE BOOL "" FORCE) # Disables all hardware renderers (D3D11, D3D12, Vulkan, GL) -set(SDL_HAPTIC OFF CACHE BOOL "" FORCE) -set(SDL_POWER OFF CACHE BOOL "" FORCE) -set(SDL_SENSOR OFF CACHE BOOL "" FORCE) -set(SDL_HIDAPI OFF CACHE BOOL "" FORCE) - -# Disable External Platform Support -set(SDL_X11 OFF CACHE BOOL "" FORCE) -set(SDL_WAYLAND OFF CACHE BOOL "" FORCE) -set(SDL_VULKAN OFF CACHE BOOL "" FORCE) -set(SDL_METAL OFF CACHE BOOL "" FORCE) - -# Disable Misc Features -set(SDL_CAMERA OFF CACHE BOOL "" FORCE) -set(SDL_DIALOG OFF CACHE BOOL "" FORCE) -set(SDL_LOCALE OFF CACHE BOOL "" FORCE) -set(SDL_MISC OFF CACHE BOOL "" FORCE) -set(SDL_OFFSCREEN OFF CACHE BOOL "" FORCE) -set(SDL_VIRTUAL_JOYSTICK OFF CACHE BOOL "" FORCE) - -# SDL3 - Core library (v3.4.2) -FetchContent_Declare( - SDL3 - URL https://github.com/libsdl-org/SDL/releases/download/release-3.4.2/SDL3-3.4.2.tar.gz - URL_HASH SHA256=ef39a2e3f9a8a78296c40da701967dd1b0d0d6e267e483863ce70f8a03b4050c -) - -# SDL3_image - Image loading support (v3.4.0) -# --- SDL3_IMAGE CATEGORIES --- - -# Disable Metadata/Packaging -set(SDLIMAGE_SAMPLES OFF CACHE BOOL "" FORCE) -set(SDLIMAGE_TESTS OFF CACHE BOOL "" FORCE) -set(SDLIMAGE_INSTALL OFF CACHE BOOL "" FORCE) -set(SDLIMAGE_VENDORED OFF CACHE BOOL "" FORCE) -set(SDLIMAGE_BACKEND_WIC OFF CACHE BOOL "" FORCE) # Avoid LNK2005 - -# Disable Codecs (minimal set) -set(SDLIMAGE_BACKEND_STB OFF CACHE BOOL "" FORCE) -set(SDLIMAGE_JPG OFF CACHE BOOL "" FORCE) -set(SDLIMAGE_PNG OFF CACHE BOOL "" FORCE) -set(SDLIMAGE_APNG OFF CACHE BOOL "" FORCE) # Fixes 'APNG_ENABLED not defined' warning -set(SDLIMAGE_WEBP OFF CACHE BOOL "" FORCE) -set(SDLIMAGE_TIF OFF CACHE BOOL "" FORCE) -set(SDLIMAGE_AVIF OFF CACHE BOOL "" FORCE) -set(SDLIMAGE_JXL OFF CACHE BOOL "" FORCE) -set(SDLIMAGE_QOI OFF CACHE BOOL "" FORCE) -set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE) # Ensure static for SDL3_image - -FetchContent_Declare( - SDL3_image - URL https://github.com/libsdl-org/SDL_image/releases/download/release-3.4.0/SDL3_image-3.4.0.tar.gz - URL_HASH SHA256=2ceb75eab4235c2c7e93dafc3ef3268ad368ca5de40892bf8cffdd510f29d9d8 -) - -FetchContent_MakeAvailable(SDL3) - -# Trick SDL3_image into thinking SDL3 is already found to avoid the broken find_package() in the build tree. -# SDL3_image specifically checks for SDL3::Headers and SDL3::SDL3 (for static builds). -if(NOT TARGET SDL3::Headers) - if(TARGET SDL3_Headers) - add_library(SDL3::Headers ALIAS SDL3_Headers) - else() - add_library(SDL3::Headers INTERFACE IMPORTED GLOBAL) - target_include_directories(SDL3::Headers INTERFACE "${sdl3_SOURCE_DIR}/include") - endif() +# Uniform aliases to ensure linking works across both discovery methods +if(TARGET SDL3::SDL3-shared AND NOT TARGET SDL3::SDL3) + add_library(SDL3::SDL3 ALIAS SDL3::SDL3-shared) endif() - -if(NOT TARGET SDL3::SDL3) - if(TARGET SDL3-static) - add_library(SDL3::SDL3 ALIAS SDL3-static) - elseif(TARGET SDL3-shared) - add_library(SDL3::SDL3 ALIAS SDL3-shared) - endif() +if(TARGET SDL3::SDL3-static AND NOT TARGET SDL3::SDL3) + add_library(SDL3::SDL3 ALIAS SDL3::SDL3-static) endif() - -set(SDL3_FOUND TRUE CACHE BOOL "" FORCE) -set(SDL3_VERSION "3.4.2" CACHE STRING "" FORCE) - -FetchContent_MakeAvailable(SDL3_image) diff --git a/vcpkg-lock.json b/vcpkg-lock.json index 422998f0378..c6ef82b1101 100644 --- a/vcpkg-lock.json +++ b/vcpkg-lock.json @@ -1,48 +1,65 @@ { - "version": 1, - "dependencies": [ - { - "name": "ffmpeg", - "version-string": "7.1.1", - "port-version": 1, - "git-tree": "6ff75f1f596ada519241989f44077cda442480b2" - }, - { - "name": "pkgconf", - "version-string": "2.3.0", - "port-version": 0, - "git-tree": "ae3886d8a627ec99dd18890389b6d5d331e29799" - }, - { - "name": "vcpkg-cmake", - "version-string": "2024-04-23", - "port-version": 0, - "git-tree": "e74aa1e8f93278a8e71372f1fa08c3df420eb840" - }, - { - "name": "vcpkg-cmake-get-vars", - "version-string": "2024-09-22", - "port-version": 0, - "git-tree": "f23148add155147f3d95ae622d3b0031beb25acf" - }, - { - "name": "vcpkg-pkgconfig-get-modules", - "version-string": "2024-04-03", - "port-version": 0, - "git-tree": "6845369c8cb7d3c318e8e3ae92fd2b7570a756ca" - }, - { - "name": "vcpkg-tool-meson", - "version-string": "1.6.1", - "port-version": 0, - "git-tree": "dc948c67d7f1359319f801078422e996b0a89fd0" - }, - { - "name": "zlib", - "version-string": "1.3.1", - "port-version": 0, - "git-tree": "3f05e04b9aededb96786a911a16193cdb711f0c9" - } - ] + "version": 1, + "dependencies": [ + { + "name": "ffmpeg", + "version-string": "7.1.1", + "port-version": 1, + "git-tree": "6ff75f1f596ada519241989f44077cda442480b2" + }, + { + "name": "pkgconf", + "version-string": "2.3.0", + "port-version": 0, + "git-tree": "ae3886d8a627ec99dd18890389b6d5d331e29799" + }, + { + "name": "sdl3", + "version-string": "3.4.4", + "port-version": 0, + "git-tree": "c3ea8e6cf352b01ab5bf9035850e72f674af2433" + }, + { + "name": "sdl3-image", + "version-string": "3.4.2", + "port-version": 0, + "git-tree": "2621596cc09e39b1ab98298f4f9e126c1764a6c4" + }, + { + "name": "vcpkg-cmake", + "version-string": "2024-04-23", + "port-version": 0, + "git-tree": "e74aa1e8f93278a8e71372f1fa08c3df420eb840" + }, + { + "name": "vcpkg-cmake-config", + "version-string": "2024-05-23", + "port-version": 0, + "git-tree": "97a63e4bc1a17422ffe4eff71da53b4b561a7841" + }, + { + "name": "vcpkg-cmake-get-vars", + "version-string": "2024-09-22", + "port-version": 0, + "git-tree": "f23148add155147f3d95ae622d3b0031beb25acf" + }, + { + "name": "vcpkg-pkgconfig-get-modules", + "version-string": "2024-04-03", + "port-version": 0, + "git-tree": "6845369c8cb7d3c318e8e3ae92fd2b7570a756ca" + }, + { + "name": "vcpkg-tool-meson", + "version-string": "1.6.1", + "port-version": 0, + "git-tree": "dc948c67d7f1359319f801078422e996b0a89fd0" + }, + { + "name": "zlib", + "version-string": "1.3.1", + "port-version": 0, + "git-tree": "3f05e04b9aededb96786a911a16193cdb711f0c9" + } + ] } - diff --git a/vcpkg.json b/vcpkg.json index 011b913c8aa..b3543b926b8 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -1,8 +1,27 @@ { "$schema": "https://raw.githubusercontent.com/microsoft/vcpkg-tool/main/docs/vcpkg.schema.json", - "builtin-baseline": "b02e341c927f16d991edbd915d8ea43eac52096c", "dependencies": [ - "zlib", - "ffmpeg" - ] - } \ No newline at end of file + "zlib", + "ffmpeg", + "sdl3", + "sdl3-image" + ], + "configuration": { + "default-registry": { + "kind": "git", + "repository": "https://github.com/microsoft/vcpkg", + "baseline": "b02e341c927f16d991edbd915d8ea43eac52096c" + }, + "registries": [ + { + "kind": "git", + "repository": "https://github.com/microsoft/vcpkg", + "baseline": "256acc64012b23a13041d8705805e1f23b43a024", + "packages": [ + "sdl3", + "sdl3-image" + ] + } + ] + } +} \ No newline at end of file From d048867a7c0ebc6258b04803e25a3b5bd076ad60 Mon Sep 17 00:00:00 2001 From: githubawn <115191165+githubawn@users.noreply.github.com> Date: Mon, 20 Apr 2026 21:08:38 +0200 Subject: [PATCH 09/18] vcpkg fix --- vcpkg-configuration.json | 19 +++++++++++++++++++ vcpkg.json | 32 +++++++------------------------- 2 files changed, 26 insertions(+), 25 deletions(-) create mode 100644 vcpkg-configuration.json diff --git a/vcpkg-configuration.json b/vcpkg-configuration.json new file mode 100644 index 00000000000..1561529b76b --- /dev/null +++ b/vcpkg-configuration.json @@ -0,0 +1,19 @@ +{ + "$schema": "https://raw.githubusercontent.com/microsoft/vcpkg-tool/main/docs/vcpkg-configuration.schema.json", + "default-registry": { + "kind": "git", + "repository": "https://github.com/microsoft/vcpkg", + "baseline": "b02e341c927f16d991edbd915d8ea43eac52096c" + }, + "registries": [ + { + "kind": "git", + "repository": "https://github.com/microsoft/vcpkg", + "baseline": "256acc64012b23a13041d8705805e1f23b43a024", + "packages": [ + "sdl3", + "sdl3-image" + ] + } + ] +} \ No newline at end of file diff --git a/vcpkg.json b/vcpkg.json index b3543b926b8..c6e9380319a 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -1,27 +1,9 @@ { - "$schema": "https://raw.githubusercontent.com/microsoft/vcpkg-tool/main/docs/vcpkg.schema.json", - "dependencies": [ - "zlib", - "ffmpeg", - "sdl3", - "sdl3-image" - ], - "configuration": { - "default-registry": { - "kind": "git", - "repository": "https://github.com/microsoft/vcpkg", - "baseline": "b02e341c927f16d991edbd915d8ea43eac52096c" - }, - "registries": [ - { - "kind": "git", - "repository": "https://github.com/microsoft/vcpkg", - "baseline": "256acc64012b23a13041d8705805e1f23b43a024", - "packages": [ - "sdl3", - "sdl3-image" - ] - } - ] - } + "$schema": "https://raw.githubusercontent.com/microsoft/vcpkg-tool/main/docs/vcpkg.schema.json", + "dependencies": [ + "zlib", + "ffmpeg", + "sdl3", + "sdl3-image" + ] } \ No newline at end of file From 534e694154153bed081dca5eef29564435e88c55 Mon Sep 17 00:00:00 2001 From: githubawn <115191165+githubawn@users.noreply.github.com> Date: Mon, 20 Apr 2026 21:49:58 +0200 Subject: [PATCH 10/18] changed dpad group numbers --- .../Source/SDL3Device/GameClient/SDL3Input.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Core/GameEngineDevice/Source/SDL3Device/GameClient/SDL3Input.cpp b/Core/GameEngineDevice/Source/SDL3Device/GameClient/SDL3Input.cpp index be1daa3bce4..b8732ab6c99 100644 --- a/Core/GameEngineDevice/Source/SDL3Device/GameClient/SDL3Input.cpp +++ b/Core/GameEngineDevice/Source/SDL3Device/GameClient/SDL3Input.cpp @@ -1130,10 +1130,10 @@ void SDL3InputManager::processGamepadInput() handleGamepadButton(SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER, m_state.buttonState[SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER], SDL_GetGamepadButton(m_gamepad, SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER), [&](bool d){ virtualPulseKey(SDL_SCANCODE_LSHIFT, d); }); handleGamepadButton(SDL_GAMEPAD_BUTTON_START, m_state.buttonState[SDL_GAMEPAD_BUTTON_START], SDL_GetGamepadButton(m_gamepad, SDL_GAMEPAD_BUTTON_START), [&](bool d){ virtualPulseKey(SDL_SCANCODE_ESCAPE, d); }); handleGamepadButton(SDL_GAMEPAD_BUTTON_BACK, m_state.buttonState[SDL_GAMEPAD_BUTTON_BACK], SDL_GetGamepadButton(m_gamepad, SDL_GAMEPAD_BUTTON_BACK), [&](bool d){ virtualPulseKey(SDL_SCANCODE_SPACE, d); }); - handleGamepadButton(SDL_GAMEPAD_BUTTON_DPAD_UP, m_state.buttonState[SDL_GAMEPAD_BUTTON_DPAD_UP], SDL_GetGamepadButton(m_gamepad, SDL_GAMEPAD_BUTTON_DPAD_UP), [&](bool d){ virtualPulseKey(SDL_SCANCODE_1, d); }); - handleGamepadButton(SDL_GAMEPAD_BUTTON_DPAD_DOWN, m_state.buttonState[SDL_GAMEPAD_BUTTON_DPAD_DOWN], SDL_GetGamepadButton(m_gamepad, SDL_GAMEPAD_BUTTON_DPAD_DOWN), [&](bool d){ virtualPulseKey(SDL_SCANCODE_2, d); }); - handleGamepadButton(SDL_GAMEPAD_BUTTON_DPAD_LEFT, m_state.buttonState[SDL_GAMEPAD_BUTTON_DPAD_LEFT], SDL_GetGamepadButton(m_gamepad, SDL_GAMEPAD_BUTTON_DPAD_LEFT), [&](bool d){ virtualPulseKey(SDL_SCANCODE_3, d); }); - handleGamepadButton(SDL_GAMEPAD_BUTTON_DPAD_RIGHT, m_state.buttonState[SDL_GAMEPAD_BUTTON_DPAD_RIGHT], SDL_GetGamepadButton(m_gamepad, SDL_GAMEPAD_BUTTON_DPAD_RIGHT), [&](bool d){ virtualPulseKey(SDL_SCANCODE_4, d); }); + handleGamepadButton(SDL_GAMEPAD_BUTTON_DPAD_LEFT, m_state.buttonState[SDL_GAMEPAD_BUTTON_DPAD_LEFT], SDL_GetGamepadButton(m_gamepad, SDL_GAMEPAD_BUTTON_DPAD_LEFT), [&](bool d){ virtualPulseKey(SDL_SCANCODE_1, d); }); + handleGamepadButton(SDL_GAMEPAD_BUTTON_DPAD_UP, m_state.buttonState[SDL_GAMEPAD_BUTTON_DPAD_UP], SDL_GetGamepadButton(m_gamepad, SDL_GAMEPAD_BUTTON_DPAD_UP), [&](bool d){ virtualPulseKey(SDL_SCANCODE_2, d); }); + handleGamepadButton(SDL_GAMEPAD_BUTTON_DPAD_RIGHT, m_state.buttonState[SDL_GAMEPAD_BUTTON_DPAD_RIGHT], SDL_GetGamepadButton(m_gamepad, SDL_GAMEPAD_BUTTON_DPAD_RIGHT), [&](bool d){ virtualPulseKey(SDL_SCANCODE_3, d); }); + handleGamepadButton(SDL_GAMEPAD_BUTTON_DPAD_DOWN, m_state.buttonState[SDL_GAMEPAD_BUTTON_DPAD_DOWN], SDL_GetGamepadButton(m_gamepad, SDL_GAMEPAD_BUTTON_DPAD_DOWN), [&](bool d){ virtualPulseKey(SDL_SCANCODE_4, d); }); handleGamepadButton(SDL_GAMEPAD_BUTTON_LEFT_STICK, m_state.buttonState[SDL_GAMEPAD_BUTTON_LEFT_STICK], SDL_GetGamepadButton(m_gamepad, SDL_GAMEPAD_BUTTON_LEFT_STICK), [&](bool d){ if (d) TheMessageStream->appendMessage(GameMessage::MSG_META_SELECT_NEXT_IDLE_WORKER); }); handleGamepadButton(SDL_GAMEPAD_BUTTON_RIGHT_STICK, m_state.buttonState[SDL_GAMEPAD_BUTTON_RIGHT_STICK], SDL_GetGamepadButton(m_gamepad, SDL_GAMEPAD_BUTTON_RIGHT_STICK), [&](bool d){ if (d) TheMessageStream->appendMessage(GameMessage::MSG_META_VIEW_COMMAND_CENTER); }); } From 09fd4e68ac2e47a220d87a2eeefa892f45411f22 Mon Sep 17 00:00:00 2001 From: githubawn <115191165+githubawn@users.noreply.github.com> Date: Mon, 20 Apr 2026 23:06:25 +0200 Subject: [PATCH 11/18] switch back to static linking --- cmake/sdl3.cmake | 30 +++++++++++++++++++++++------- triplets/x86-windows.cmake | 4 ++++ 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/cmake/sdl3.cmake b/cmake/sdl3.cmake index ec34f4b1466..dfc5d3026f1 100644 --- a/cmake/sdl3.cmake +++ b/cmake/sdl3.cmake @@ -20,15 +20,19 @@ if(NOT SDL3_FOUND OR NOT SDL3_image_FOUND) ) # Official SDL configuration for a unified build tree - set(SDL_SHARED ON CACHE BOOL "" FORCE) - set(SDL_STATIC OFF CACHE BOOL "" FORCE) + set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE) + set(SDL_SHARED OFF CACHE BOOL "" FORCE) + set(SDL_STATIC ON CACHE BOOL "" FORCE) set(SDLIMAGE_VENDORED OFF CACHE BOOL "" FORCE) - set(SDLIMAGE_ZLIB ON CACHE BOOL "" FORCE) - set(SDLIMAGE_PNG ON CACHE BOOL "" FORCE) - set(SDLIMAGE_APNG ON CACHE BOOL "" FORCE) + set(SDLIMAGE_SHARED OFF CACHE BOOL "" FORCE) + set(SDLIMAGE_STATIC ON CACHE BOOL "" FORCE) + set(SDLIMAGE_ZLIB OFF CACHE BOOL "" FORCE) + set(SDLIMAGE_PNG OFF CACHE BOOL "" FORCE) + set(SDLIMAGE_APNG OFF CACHE BOOL "" FORCE) - # Making them available in order ensures SDL3_image can see the SDL3 targets - FetchContent_MakeAvailable(SDL3 SDL3_image) + # Populate SDL3 and SDL3_image + FetchContent_MakeAvailable(SDL3) + FetchContent_MakeAvailable(SDL3_image) endif() # Uniform aliases to ensure linking works across both discovery methods @@ -38,3 +42,15 @@ endif() if(TARGET SDL3::SDL3-static AND NOT TARGET SDL3::SDL3) add_library(SDL3::SDL3 ALIAS SDL3::SDL3-static) endif() + +# Centralized dependency restoration for SDL3 static builds. +# We apply these directly to the SDL3-static target so it correctly handles its own needs. +if(TARGET SDL3-static) + target_link_libraries(SDL3-static INTERFACE + ws2_32.lib + winmm.lib + imm32.lib + version.lib + setupapi.lib + ) +endif() diff --git a/triplets/x86-windows.cmake b/triplets/x86-windows.cmake index 106900a72d7..ab214b8bf09 100644 --- a/triplets/x86-windows.cmake +++ b/triplets/x86-windows.cmake @@ -3,6 +3,10 @@ set(VCPKG_TARGET_ARCHITECTURE x86) set(VCPKG_CRT_LINKAGE dynamic) set(VCPKG_LIBRARY_LINKAGE dynamic) +if(PORT MATCHES "sdl3") + set(VCPKG_LIBRARY_LINKAGE static) +endif() + # Exclude compiler version from ABI hash so that weekly GitHub runner image # updates don't invalidate the binary cache. Minor MSVC version bumps do not # cause ABI incompatibilities for this project. From 9b0262c9ec41e3b123d200f9941bcba62c33f606 Mon Sep 17 00:00:00 2001 From: githubawn <115191165+githubawn@users.noreply.github.com> Date: Tue, 21 Apr 2026 17:40:52 +0200 Subject: [PATCH 12/18] move SDL3_image out SDL3input --- Core/GameEngineDevice/CMakeLists.txt | 2 + .../SDL3Device/GameClient/SDL3Cursor.h | 70 +++++ .../Include/SDL3Device/GameClient/SDL3Input.h | 8 - .../SDL3Device/GameClient/SDL3Cursor.cpp | 254 +++++++++++++++++ .../SDL3Device/GameClient/SDL3Input.cpp | 267 +----------------- 5 files changed, 335 insertions(+), 266 deletions(-) create mode 100644 Core/GameEngineDevice/Include/SDL3Device/GameClient/SDL3Cursor.h create mode 100644 Core/GameEngineDevice/Source/SDL3Device/GameClient/SDL3Cursor.cpp diff --git a/Core/GameEngineDevice/CMakeLists.txt b/Core/GameEngineDevice/CMakeLists.txt index 69ae180a1de..7d630f42230 100644 --- a/Core/GameEngineDevice/CMakeLists.txt +++ b/Core/GameEngineDevice/CMakeLists.txt @@ -197,8 +197,10 @@ if(SAGE_USE_SDL3 AND NOT IS_VS6_BUILD) list(APPEND GAMEENGINEDEVICE_SRC Include/SDL3GameEngine.h Include/SDL3Device/GameClient/SDL3Input.h + Include/SDL3Device/GameClient/SDL3Cursor.h Source/SDL3GameEngine.cpp Source/SDL3Device/GameClient/SDL3Input.cpp + Source/SDL3Device/GameClient/SDL3Cursor.cpp ) endif() diff --git a/Core/GameEngineDevice/Include/SDL3Device/GameClient/SDL3Cursor.h b/Core/GameEngineDevice/Include/SDL3Device/GameClient/SDL3Cursor.h new file mode 100644 index 00000000000..0e41b3855f8 --- /dev/null +++ b/Core/GameEngineDevice/Include/SDL3Device/GameClient/SDL3Cursor.h @@ -0,0 +1,70 @@ +/* +** Command & Conquer Generals Zero Hour(tm) +** Copyright 2026 TheSuperHackers +** +** 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 3 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 . +*/ + +/* +** Derived from the GeneralsX branch by fbraz3 +*/ + +#pragma once + +#include "Lib/BaseType.h" +#include +#include + +// USER INCLUDES +#include "GameClient/Mouse.h" + +/** + * AnimatedCursor - Wrapper for SDL3 native animated cursors + */ +struct AnimatedCursor { + SDL_Cursor* m_cursor; + + AnimatedCursor() : m_cursor(nullptr) {} + ~AnimatedCursor() + { + if (m_cursor) + { + SDL_DestroyCursor(m_cursor); + m_cursor = nullptr; + } + } + + SDL_Cursor* getCursor() const { return m_cursor; } +}; + +/** + * SDL3CursorManager - Manages loading and lifecycle of cursors + */ +class SDL3CursorManager +{ +public: + static void init(); + static void shutdown(); + + static SDL_Cursor* getCursor(Mouse::MouseCursor cursor, int direction); + + // Internal loader used by Mouse implementation + static void initResources(Mouse* mouse); + +private: + static AnimatedCursor* loadCursorFromFile(const char* filepath); + static AnimatedCursor* loadANI(const char* filepath); + + static AnimatedCursor* m_cursorResources[Mouse::NUM_MOUSE_CURSORS][MAX_2D_CURSOR_DIRECTIONS]; +}; diff --git a/Core/GameEngineDevice/Include/SDL3Device/GameClient/SDL3Input.h b/Core/GameEngineDevice/Include/SDL3Device/GameClient/SDL3Input.h index 5d574178115..43b46609f17 100644 --- a/Core/GameEngineDevice/Include/SDL3Device/GameClient/SDL3Input.h +++ b/Core/GameEngineDevice/Include/SDL3Device/GameClient/SDL3Input.h @@ -26,7 +26,6 @@ // SYSTEM INCLUDES #include -#include #include #include @@ -36,7 +35,6 @@ #include "GameClient/KeyDefs.h" // FORWARD REFERENCES -struct AnimatedCursor; class SDL3InputManager; // GLOBALS --------------------------------------------------------------------- @@ -82,9 +80,6 @@ class SDL3Mouse : public Mouse // Scale raw SDL window coordinates to game internal resolution void scaleMouseCoordinates(int rawX, int rawY, Uint32 windowID, int& scaledX, int& scaledY); - // Load cursor from ANI file (fighter19 pattern) - AnimatedCursor* loadCursorFromFile(const char* filepath); - SDL_Window* m_Window; Bool m_IsCaptured; Bool m_IsVisible; @@ -93,20 +88,17 @@ class SDL3Mouse : public Mouse Uint32 m_LeftButtonDownTime; Uint32 m_RightButtonDownTime; Uint32 m_MiddleButtonDownTime; - UnsignedInt m_LastFrameNumber; ICoord2D m_LeftButtonDownPos; ICoord2D m_RightButtonDownPos; ICoord2D m_MiddleButtonDownPos; Int m_directionFrame; - UnsignedInt m_inputFrame; float m_accumulatedDeltaX; float m_accumulatedDeltaY; SDL_Cursor* m_activeSDLCursor; - Bool m_cursorDirty; }; // SDL3Keyboard --------------------------------------------------------------- diff --git a/Core/GameEngineDevice/Source/SDL3Device/GameClient/SDL3Cursor.cpp b/Core/GameEngineDevice/Source/SDL3Device/GameClient/SDL3Cursor.cpp new file mode 100644 index 00000000000..d114205ad14 --- /dev/null +++ b/Core/GameEngineDevice/Source/SDL3Device/GameClient/SDL3Cursor.cpp @@ -0,0 +1,254 @@ +/* +** Command & Conquer Generals Zero Hour(tm) +** Copyright 2026 TheSuperHackers +** +** 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 3 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 . +*/ + +/* +** Derived from the GeneralsX branch by fbraz3 +*/ + +#include "SDL3Device/GameClient/SDL3Cursor.h" +#include +#include +#include +#include +#include +#include + +#include "Common/Debug.h" +#include "Common/file.h" +#include "Common/FileSystem.h" + +// Initialize static member +AnimatedCursor* SDL3CursorManager::m_cursorResources[Mouse::NUM_MOUSE_CURSORS][MAX_2D_CURSOR_DIRECTIONS] = { nullptr }; + +// RIFF/ANI parsing helpers (moved from SDL3Input.cpp) +typedef std::array FourCC; +constexpr FourCC riff_id = {'R', 'I', 'F', 'F'}; +constexpr FourCC acon_id = {'A', 'C', 'O', 'N'}; +constexpr FourCC anih_id = {'a', 'n', 'i', 'h'}; +constexpr FourCC fram_id = {'f', 'r', 'a', 'm'}; +constexpr FourCC icon_id = {'i', 'c', 'o', 'n'}; +constexpr FourCC list_id = {'L', 'I', 'S', 'T'}; + +struct ANIHeader +{ + uint32_t size; + uint32_t frames; + uint32_t steps; + uint32_t width; + uint32_t height; + uint32_t bitsPerPixel; + uint32_t planes; + uint32_t displayRate; + uint32_t flags; +}; + +struct RIFFChunk +{ + FourCC id; + uint32_t size; + FourCC type; +}; + +static RIFFChunk* getNextChunk(RIFFChunk* chunk, const char* buffer_end) +{ + if (!chunk) return nullptr; + char* next = (char*)chunk + 8 + chunk->size; + if (chunk->size % 2 != 0) next++; + if (next >= buffer_end) return nullptr; + return (RIFFChunk*)next; +} + +static void* getChunkData(RIFFChunk* chunk) +{ + if (chunk->id == list_id || chunk->id == riff_id) + return (char*)chunk + 12; + return (char*)chunk + 8; +} + +void SDL3CursorManager::init() +{ + // Cursors are typically initialized via initResources when the Mouse device is ready + shutdown(); +} + +void SDL3CursorManager::shutdown() +{ + for (int i = 0; i < Mouse::NUM_MOUSE_CURSORS; ++i) + { + for (int j = 0; j < MAX_2D_CURSOR_DIRECTIONS; ++j) + { + if (m_cursorResources[i][j]) + { + delete m_cursorResources[i][j]; + m_cursorResources[i][j] = nullptr; + } + } + } +} + +SDL_Cursor* SDL3CursorManager::getCursor(Mouse::MouseCursor cursor, int direction) +{ + if (cursor < 0 || cursor >= Mouse::NUM_MOUSE_CURSORS) return nullptr; + if (direction < 0 || direction >= MAX_2D_CURSOR_DIRECTIONS) direction = 0; + + AnimatedCursor* anim = m_cursorResources[cursor][direction]; + return anim ? anim->getCursor() : nullptr; +} + +void SDL3CursorManager::initResources(Mouse* mouse) +{ + if (!mouse) return; + + for (Int cursor = Mouse::FIRST_CURSOR; cursor < Mouse::NUM_MOUSE_CURSORS; cursor++) + { + for (Int direction = 0; direction < mouse->m_cursorInfo[cursor].numDirections; direction++) + { + if (!m_cursorResources[cursor][direction] && !mouse->m_cursorInfo[cursor].textureName.isEmpty()) + { + char resourcePath[256]; + if (mouse->m_cursorInfo[cursor].numDirections > 1) + snprintf(resourcePath, sizeof(resourcePath), "Data/Cursors/%s%d.ani", mouse->m_cursorInfo[cursor].textureName.str(), direction); + else + snprintf(resourcePath, sizeof(resourcePath), "Data/Cursors/%s.ani", mouse->m_cursorInfo[cursor].textureName.str()); + + m_cursorResources[cursor][direction] = loadANI(resourcePath); + DEBUG_ASSERTCRASH(m_cursorResources[cursor][direction], ("MissingCursor %s\n", resourcePath)); + } + } + } +} + +AnimatedCursor* SDL3CursorManager::loadANI(const char* filepath) +{ + File* file = TheFileSystem->openFile(filepath, File::READ | File::BINARY); + if (!file) + { + DEBUG_LOG(("loadANI: Failed to open ANI cursor [%s]", filepath)); + return nullptr; + } + + Int size = file->size(); + if (size < (Int)sizeof(RIFFChunk)) + { + DEBUG_LOG(("loadANI: File too small [%s]", filepath)); + file->close(); + return nullptr; + } + + std::unique_ptr file_buffer(new char[size]); + if (file->read(file_buffer.get(), size) != size) + { + DEBUG_LOG(("loadANI: Failed to read ANI cursor [%s]", filepath)); + file->close(); + return nullptr; + } + file->close(); + + char* buffer_start = file_buffer.get(); + char* buffer_end = buffer_start + size; + + RIFFChunk *riff_header = (RIFFChunk*)buffer_start; + if (riff_header->id != riff_id || riff_header->type != acon_id) + { + DEBUG_LOG(("loadANI: Not a valid RIFF/ACON file [%s]", filepath)); + return nullptr; + } + + DEBUG_LOG(("loadANI: Loading %s", filepath)); + + std::vector frames; + int frameRate = 0; + int hot_spot_x = 0; + int hot_spot_y = 0; + bool hot_spot_set = false; + + // Top level chunks start after the RIFF header (8 bytes + 'ACON' = 12 bytes) + RIFFChunk* chunk = (RIFFChunk*)(buffer_start + 12); + + while (chunk != nullptr && (char *)chunk + 8 <= buffer_end) + { + if (chunk->id == anih_id) + { + if (chunk->size >= sizeof(ANIHeader)) + { + ANIHeader *ani_header = (ANIHeader*)getChunkData(chunk); + frameRate = ani_header->displayRate; // Display rate in 1/60th of a second + } + } + else if (chunk->id == list_id && chunk->type == fram_id) + { + RIFFChunk *frame = (RIFFChunk*)((char *)chunk + 12); + char* list_end = (char*)chunk + 8 + chunk->size; + if (list_end > buffer_end) list_end = buffer_end; + + while (frame != nullptr && (char *)frame + 8 <= list_end) + { + if (frame->id == icon_id) + { + if ((char*)frame + 8 + frame->size <= list_end) + { + const void *frame_buffer = getChunkData(frame); + SDL_IOStream *io_stream = SDL_IOFromConstMem(frame_buffer, frame->size); + if (io_stream) + { + SDL_Surface *surface = IMG_LoadTyped_IO(io_stream, true, "ico"); + if (surface) + { + if (!hot_spot_set) + { + SDL_PropertiesID props = SDL_GetSurfaceProperties(surface); + hot_spot_x = (int)SDL_GetNumberProperty(props, SDL_PROP_SURFACE_HOTSPOT_X_NUMBER, 0); + hot_spot_y = (int)SDL_GetNumberProperty(props, SDL_PROP_SURFACE_HOTSPOT_Y_NUMBER, 0); + hot_spot_set = true; + } + + SDL_CursorFrameInfo info; + info.surface = surface; + // SAGE's displayRate is in 1/60th of a second. SDL3 wants milliseconds. + info.duration = (frameRate * 1000) / 60; + frames.push_back(info); + } + } + } + } + frame = getNextChunk(frame, list_end); + } + } + chunk = getNextChunk(chunk, buffer_end); + } + + if (frames.empty()) return nullptr; + + std::unique_ptr cursor(new AnimatedCursor()); + if (frames.size() == 1) + { + cursor->m_cursor = SDL_CreateColorCursor(frames[0].surface, hot_spot_x, hot_spot_y); + } + else + { + cursor->m_cursor = SDL_CreateAnimatedCursor(frames.data(), (int)frames.size(), hot_spot_x, hot_spot_y); + } + + // Clean up all surfaces + for (auto& f : frames) + { + SDL_DestroySurface(f.surface); + } + + return cursor.release(); +} diff --git a/Core/GameEngineDevice/Source/SDL3Device/GameClient/SDL3Input.cpp b/Core/GameEngineDevice/Source/SDL3Device/GameClient/SDL3Input.cpp index b8732ab6c99..4dda8ee30d5 100644 --- a/Core/GameEngineDevice/Source/SDL3Device/GameClient/SDL3Input.cpp +++ b/Core/GameEngineDevice/Source/SDL3Device/GameClient/SDL3Input.cpp @@ -31,7 +31,7 @@ #include #include #include -#include +#include "SDL3Device/GameClient/SDL3Cursor.h" #include // For timeGetTime() #include "SDL3Device/GameClient/SDL3Input.h" @@ -48,215 +48,10 @@ // GLOBALS --------------------------------------------------------------------- SDL3InputManager* TheSDL3InputManager = nullptr; -// ============================================================================ +/// ============================================================================ // SDL3MOUSE IMPLEMENTATION // ============================================================================ -/** - * AnimatedCursor - Helper struct for cursor animation - */ -struct AnimatedCursor { - std::array m_frameCursors; - std::array m_frameSurfaces; - int m_frameCount = 0; - int m_frameRate = 0; // the time a frame is displayed in 1/60th of a second - - AnimatedCursor() - { - m_frameCursors.fill(nullptr); - m_frameSurfaces.fill(nullptr); - } - - ~AnimatedCursor() - { - for (int i = 0; i < MAX_2D_CURSOR_ANIM_FRAMES; i++) - { - if (m_frameCursors[i]) - { - SDL_DestroyCursor(m_frameCursors[i]); - m_frameCursors[i] = nullptr; - } - if (m_frameSurfaces[i]) - { - SDL_DestroySurface(m_frameSurfaces[i]); - m_frameSurfaces[i] = nullptr; - } - } - } - - /** - * Get the active frame cursor based on current system time - */ - SDL_Cursor* getActiveFrame() const - { - if (m_frameCount <= 0) return nullptr; - if (m_frameCount == 1) return m_frameCursors[0]; - - Uint64 now = SDL_GetTicks(); - size_t index = (m_frameRate > 0) - ? (size_t)((now * 60 / 1000) / m_frameRate) % (size_t)std::min((int)m_frameCount, MAX_2D_CURSOR_ANIM_FRAMES) - : 0; - return m_frameCursors[index]; - } -}; - -// Global cursor resources array -static AnimatedCursor* cursorResources[Mouse::NUM_MOUSE_CURSORS][MAX_2D_CURSOR_DIRECTIONS]; - -// RIFF/ANI parsing helpers -typedef std::array FourCC; -constexpr FourCC riff_id = {'R', 'I', 'F', 'F'}; -constexpr FourCC acon_id = {'A', 'C', 'O', 'N'}; -constexpr FourCC anih_id = {'a', 'n', 'i', 'h'}; -constexpr FourCC fram_id = {'f', 'r', 'a', 'm'}; -constexpr FourCC icon_id = {'i', 'c', 'o', 'n'}; -constexpr FourCC list_id = {'L', 'I', 'S', 'T'}; - -struct ANIHeader -{ - uint32_t size; - uint32_t frames; - uint32_t steps; - uint32_t width; - uint32_t height; - uint32_t bitsPerPixel; - uint32_t planes; - uint32_t displayRate; - uint32_t flags; -}; - -struct RIFFChunk -{ - FourCC id; - uint32_t size; - FourCC type; -}; - -static RIFFChunk* getNextChunk(RIFFChunk* chunk, const char* buffer_end) -{ - if (!chunk) return nullptr; - - // Size check: Chunk header is at least 8 bytes (ID + Size). - char* next = (char*)chunk + 8 + chunk->size; - - // RIFF chunks are padded to 2 bytes - if (chunk->size % 2 != 0) next++; - - if (next >= buffer_end) return nullptr; - return (RIFFChunk*)next; -} - -static void* getChunkData(RIFFChunk* chunk) -{ - // For LIST and RIFF, type is at +8, data starts at +12 - if (chunk->id == list_id || chunk->id == riff_id) - return (char*)chunk + 12; - - // For others, data starts at +8 - return (char*)chunk + 8; -} - -/** - * loadANI - Dedicated standalone RIFF/ANI parser (Hardened) - */ -static AnimatedCursor* loadANI(const char* filepath) -{ - File* file = TheFileSystem->openFile(filepath, File::READ | File::BINARY); - if (!file) - { - DEBUG_LOG(("loadANI: Failed to open ANI cursor [%s]", filepath)); - return nullptr; - } - - Int size = file->size(); - if (size < (Int)sizeof(RIFFChunk)) - { - DEBUG_LOG(("loadANI: File too small [%s]", filepath)); - file->close(); - return nullptr; - } - - std::unique_ptr file_buffer(new char[size]); - if (file->read(file_buffer.get(), size) != size) - { - DEBUG_LOG(("loadANI: Failed to read ANI cursor [%s]", filepath)); - file->close(); - return nullptr; - } - file->close(); - - char* buffer_start = file_buffer.get(); - char* buffer_end = buffer_start + size; - - RIFFChunk *riff_header = (RIFFChunk*)buffer_start; - if (riff_header->id != riff_id || riff_header->type != acon_id) - { - DEBUG_LOG(("loadANI: Not a valid RIFF/ACON file [%s]", filepath)); - return nullptr; - } - - DEBUG_LOG(("loadANI: Loading %s", filepath)); - std::unique_ptr cursor(new AnimatedCursor()); - - // Top level chunks start after the RIFF header (8 bytes + 'ACON' = 12 bytes) - RIFFChunk* chunk = (RIFFChunk*)(buffer_start + 12); - - while (chunk != nullptr && (char *)chunk + 8 <= buffer_end) - { - if (chunk->id == anih_id) - { - if (chunk->size < sizeof(ANIHeader)) - { - DEBUG_LOG(("loadANI: Invalid ANI header size")); - return nullptr; - } - - ANIHeader *ani_header = (ANIHeader*)getChunkData(chunk); - cursor->m_frameCount = (int)std::min((unsigned int)ani_header->frames, (unsigned int)MAX_2D_CURSOR_ANIM_FRAMES); - cursor->m_frameRate = ani_header->displayRate; - } - else if (chunk->id == list_id && chunk->type == fram_id) - { - int frame_index = 0; - // Sub-chunks in LIST start after the header + type (12 bytes) - RIFFChunk *frame = (RIFFChunk*)((char *)chunk + 12); - char* list_end = (char*)chunk + 8 + chunk->size; - if (list_end > buffer_end) list_end = buffer_end; - - while (frame != nullptr && (char *)frame + 8 <= list_end) - { - if (frame->id == icon_id) - { - if ((char*)frame + 8 + frame->size <= list_end) - { - const void *frame_buffer = getChunkData(frame); - SDL_IOStream *io_stream = SDL_IOFromConstMem(frame_buffer, frame->size); - if (io_stream) - { - SDL_Surface *surface = cursor->m_frameSurfaces[frame_index] = IMG_LoadTyped_IO(io_stream, true, "ico"); - if (surface) - { - SDL_PropertiesID props = SDL_GetSurfaceProperties(surface); - int hot_spot_x = (int)SDL_GetNumberProperty(props, SDL_PROP_SURFACE_HOTSPOT_X_NUMBER, 0); - int hot_spot_y = (int)SDL_GetNumberProperty(props, SDL_PROP_SURFACE_HOTSPOT_Y_NUMBER, 0); - - cursor->m_frameCursors[frame_index++] = SDL_CreateColorCursor(surface, hot_spot_x, hot_spot_y); - } - } - } - } - - if (frame_index >= MAX_2D_CURSOR_ANIM_FRAMES) break; - frame = getNextChunk(frame, list_end); - } - } - - chunk = getNextChunk(chunk, buffer_end); - } - - return cursor.release(); -} - /** * Constructor - Initialize SDL3Mouse with window handle */ @@ -269,13 +64,10 @@ SDL3Mouse::SDL3Mouse(SDL_Window* window) m_LeftButtonDownTime(0), m_RightButtonDownTime(0), m_MiddleButtonDownTime(0), - m_LastFrameNumber(0), m_directionFrame(0), - m_inputFrame(0), m_accumulatedDeltaX(0.0f), m_accumulatedDeltaY(0.0f), - m_activeSDLCursor(nullptr), - m_cursorDirty(false) + m_activeSDLCursor(nullptr) { m_LeftButtonDownPos.x = 0; m_LeftButtonDownPos.y = 0; @@ -324,8 +116,6 @@ void SDL3Mouse::update(void) { Mouse::update(); - m_inputFrame++; - if (m_LostFocus) { return; @@ -385,12 +175,8 @@ void SDL3Mouse::update(void) } else { - AnimatedCursor* animated = cursorResources[cursor][m_directionFrame]; - if (animated) - { - requestedHandle = animated->getActiveFrame(); - } - else + requestedHandle = SDL3CursorManager::getCursor(cursor, m_directionFrame); + if (!requestedHandle) { bUseDefaultCursor = true; } @@ -398,11 +184,8 @@ void SDL3Mouse::update(void) if (bUseDefaultCursor) { - if (cursorResources[NORMAL][0]) - { - requestedHandle = cursorResources[NORMAL][0]->m_frameCursors[0]; - } - else + requestedHandle = SDL3CursorManager::getCursor(NORMAL, 0); + if (!requestedHandle) { requestedHandle = SDL_GetDefaultCursor(); } @@ -413,8 +196,6 @@ void SDL3Mouse::update(void) SDL_SetCursor(requestedHandle); m_activeSDLCursor = requestedHandle; } - - m_cursorDirty = false; } /** @@ -422,41 +203,12 @@ void SDL3Mouse::update(void) */ void SDL3Mouse::initCursorResources(void) { - for (Int cursor=FIRST_CURSOR; cursor 1) - snprintf(resourcePath, sizeof(resourcePath), "Data/Cursors/%s%d.ani", m_cursorInfo[cursor].textureName.str(), direction); - else - snprintf(resourcePath, sizeof(resourcePath), "Data/Cursors/%s.ani", m_cursorInfo[cursor].textureName.str()); - - cursorResources[cursor][direction]=loadCursorFromFile(resourcePath); - DEBUG_ASSERTCRASH(cursorResources[cursor][direction], ("MissingCursor %s\n",resourcePath)); - } - } - } + SDL3CursorManager::initResources(this); } void SDL3Mouse::freeCursorResources(void) { - for (Int cursor = 0; cursor < Mouse::NUM_MOUSE_CURSORS; cursor++) - { - for (Int direction = 0; direction < MAX_2D_CURSOR_DIRECTIONS; direction++) - { - if (cursorResources[cursor][direction]) - { - delete cursorResources[cursor][direction]; - cursorResources[cursor][direction] = nullptr; - } - } - } -} - -AnimatedCursor* SDL3Mouse::loadCursorFromFile(const char* filepath) -{ - return loadANI(filepath); + SDL3CursorManager::shutdown(); } /** @@ -471,7 +223,6 @@ void SDL3Mouse::setCursor(MouseCursor cursor) Mouse::setCursor( cursor ); m_currentCursor = cursor; - m_cursorDirty = true; } /** From 3a40b11f623639128add22c0e82c522933d69477 Mon Sep 17 00:00:00 2001 From: githubawn <115191165+githubawn@users.noreply.github.com> Date: Tue, 21 Apr 2026 17:50:28 +0200 Subject: [PATCH 13/18] properly fail on SDL_INIT failure --- GeneralsMD/Code/Main/WinMain.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GeneralsMD/Code/Main/WinMain.cpp b/GeneralsMD/Code/Main/WinMain.cpp index cce81e16c8e..6cb6d57cca6 100644 --- a/GeneralsMD/Code/Main/WinMain.cpp +++ b/GeneralsMD/Code/Main/WinMain.cpp @@ -896,7 +896,7 @@ Int APIENTRY WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, #if SAGE_USE_SDL3 if (!TheGlobalData->m_headless) { - if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_EVENTS | SDL_INIT_GAMEPAD) == 0) + if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_EVENTS | SDL_INIT_GAMEPAD)) { DEBUG_LOG(("SDL_Init failed: %s", SDL_GetError())); return exitcode; From 4d82b8137b89716cc511f38d8bb11bb99e79ac3d Mon Sep 17 00:00:00 2001 From: githubawn <115191165+githubawn@users.noreply.github.com> Date: Tue, 21 Apr 2026 18:09:29 +0200 Subject: [PATCH 14/18] removed custom ICO decoder --- .../SDL3Device/GameClient/SDL3Cursor.cpp | 156 ++++-------------- 1 file changed, 31 insertions(+), 125 deletions(-) diff --git a/Core/GameEngineDevice/Source/SDL3Device/GameClient/SDL3Cursor.cpp b/Core/GameEngineDevice/Source/SDL3Device/GameClient/SDL3Cursor.cpp index d114205ad14..45cd031df3a 100644 --- a/Core/GameEngineDevice/Source/SDL3Device/GameClient/SDL3Cursor.cpp +++ b/Core/GameEngineDevice/Source/SDL3Device/GameClient/SDL3Cursor.cpp @@ -35,51 +35,6 @@ // Initialize static member AnimatedCursor* SDL3CursorManager::m_cursorResources[Mouse::NUM_MOUSE_CURSORS][MAX_2D_CURSOR_DIRECTIONS] = { nullptr }; -// RIFF/ANI parsing helpers (moved from SDL3Input.cpp) -typedef std::array FourCC; -constexpr FourCC riff_id = {'R', 'I', 'F', 'F'}; -constexpr FourCC acon_id = {'A', 'C', 'O', 'N'}; -constexpr FourCC anih_id = {'a', 'n', 'i', 'h'}; -constexpr FourCC fram_id = {'f', 'r', 'a', 'm'}; -constexpr FourCC icon_id = {'i', 'c', 'o', 'n'}; -constexpr FourCC list_id = {'L', 'I', 'S', 'T'}; - -struct ANIHeader -{ - uint32_t size; - uint32_t frames; - uint32_t steps; - uint32_t width; - uint32_t height; - uint32_t bitsPerPixel; - uint32_t planes; - uint32_t displayRate; - uint32_t flags; -}; - -struct RIFFChunk -{ - FourCC id; - uint32_t size; - FourCC type; -}; - -static RIFFChunk* getNextChunk(RIFFChunk* chunk, const char* buffer_end) -{ - if (!chunk) return nullptr; - char* next = (char*)chunk + 8 + chunk->size; - if (chunk->size % 2 != 0) next++; - if (next >= buffer_end) return nullptr; - return (RIFFChunk*)next; -} - -static void* getChunkData(RIFFChunk* chunk) -{ - if (chunk->id == list_id || chunk->id == riff_id) - return (char*)chunk + 12; - return (char*)chunk + 8; -} - void SDL3CursorManager::init() { // Cursors are typically initialized via initResources when the Mouse device is ready @@ -143,9 +98,9 @@ AnimatedCursor* SDL3CursorManager::loadANI(const char* filepath) } Int size = file->size(); - if (size < (Int)sizeof(RIFFChunk)) + if (size <= 0) { - DEBUG_LOG(("loadANI: File too small [%s]", filepath)); + DEBUG_LOG(("loadANI: File is empty [%s]", filepath)); file->close(); return nullptr; } @@ -159,96 +114,47 @@ AnimatedCursor* SDL3CursorManager::loadANI(const char* filepath) } file->close(); - char* buffer_start = file_buffer.get(); - char* buffer_end = buffer_start + size; - - RIFFChunk *riff_header = (RIFFChunk*)buffer_start; - if (riff_header->id != riff_id || riff_header->type != acon_id) - { - DEBUG_LOG(("loadANI: Not a valid RIFF/ACON file [%s]", filepath)); - return nullptr; - } - DEBUG_LOG(("loadANI: Loading %s", filepath)); - std::vector frames; - int frameRate = 0; - int hot_spot_x = 0; - int hot_spot_y = 0; - bool hot_spot_set = false; + SDL_IOStream *io = SDL_IOFromConstMem(file_buffer.get(), (size_t)size); + if (!io) return nullptr; - // Top level chunks start after the RIFF header (8 bytes + 'ACON' = 12 bytes) - RIFFChunk* chunk = (RIFFChunk*)(buffer_start + 12); + // Use SDL3_image to load the animation (handles RIFF/ANI container and frame decoding) + IMG_Animation *anim = IMG_LoadAnimation_IO(io, true); + if (!anim) + { + DEBUG_LOG(("loadANI: IMG_LoadAnimation_IO failed for [%s]: %s", filepath, SDL_GetError())); + return nullptr; + } - while (chunk != nullptr && (char *)chunk + 8 <= buffer_end) - { - if (chunk->id == anih_id) - { - if (chunk->size >= sizeof(ANIHeader)) - { - ANIHeader *ani_header = (ANIHeader*)getChunkData(chunk); - frameRate = ani_header->displayRate; // Display rate in 1/60th of a second - } - } - else if (chunk->id == list_id && chunk->type == fram_id) - { - RIFFChunk *frame = (RIFFChunk*)((char *)chunk + 12); - char* list_end = (char*)chunk + 8 + chunk->size; - if (list_end > buffer_end) list_end = buffer_end; - - while (frame != nullptr && (char *)frame + 8 <= list_end) - { - if (frame->id == icon_id) - { - if ((char*)frame + 8 + frame->size <= list_end) - { - const void *frame_buffer = getChunkData(frame); - SDL_IOStream *io_stream = SDL_IOFromConstMem(frame_buffer, frame->size); - if (io_stream) - { - SDL_Surface *surface = IMG_LoadTyped_IO(io_stream, true, "ico"); - if (surface) - { - if (!hot_spot_set) - { - SDL_PropertiesID props = SDL_GetSurfaceProperties(surface); - hot_spot_x = (int)SDL_GetNumberProperty(props, SDL_PROP_SURFACE_HOTSPOT_X_NUMBER, 0); - hot_spot_y = (int)SDL_GetNumberProperty(props, SDL_PROP_SURFACE_HOTSPOT_Y_NUMBER, 0); - hot_spot_set = true; - } - - SDL_CursorFrameInfo info; - info.surface = surface; - // SAGE's displayRate is in 1/60th of a second. SDL3 wants milliseconds. - info.duration = (frameRate * 1000) / 60; - frames.push_back(info); - } - } - } - } - frame = getNextChunk(frame, list_end); - } - } - chunk = getNextChunk(chunk, buffer_end); - } + if (anim->count == 0) + { + IMG_FreeAnimation(anim); + return nullptr; + } - if (frames.empty()) return nullptr; + // Get hotspots from the first frame's properties (SDL3_image sets these for ICO/CUR) + SDL_PropertiesID props = SDL_GetSurfaceProperties(anim->frames[0]); + int hot_spot_x = (int)SDL_GetNumberProperty(props, SDL_PROP_SURFACE_HOTSPOT_X_NUMBER, 0); + int hot_spot_y = (int)SDL_GetNumberProperty(props, SDL_PROP_SURFACE_HOTSPOT_Y_NUMBER, 0); + // Create the animated cursor resource std::unique_ptr cursor(new AnimatedCursor()); - if (frames.size() == 1) + if (anim->count == 1) { - cursor->m_cursor = SDL_CreateColorCursor(frames[0].surface, hot_spot_x, hot_spot_y); + cursor->m_cursor = SDL_CreateColorCursor(anim->frames[0], hot_spot_x, hot_spot_y); } else { - cursor->m_cursor = SDL_CreateAnimatedCursor(frames.data(), (int)frames.size(), hot_spot_x, hot_spot_y); - } - - // Clean up all surfaces - for (auto& f : frames) - { - SDL_DestroySurface(f.surface); + std::vector frames(anim->count); + for (int i = 0; i < anim->count; i++) + { + frames[i].surface = anim->frames[i]; + frames[i].duration = (Uint32)anim->delays[i]; + } + cursor->m_cursor = SDL_CreateAnimatedCursor(frames.data(), anim->count, hot_spot_x, hot_spot_y); } + IMG_FreeAnimation(anim); return cursor.release(); } From 884218bc36f97724dc445eeb4c3c07367277a852 Mon Sep 17 00:00:00 2001 From: githubawn <115191165+githubawn@users.noreply.github.com> Date: Tue, 21 Apr 2026 18:48:17 +0200 Subject: [PATCH 15/18] fixes SDL_WarpMouseInWindow mismatch --- .../GameEngineDevice/Source/SDL3Device/GameClient/SDL3Input.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Core/GameEngineDevice/Source/SDL3Device/GameClient/SDL3Input.cpp b/Core/GameEngineDevice/Source/SDL3Device/GameClient/SDL3Input.cpp index 4dda8ee30d5..608219e695e 100644 --- a/Core/GameEngineDevice/Source/SDL3Device/GameClient/SDL3Input.cpp +++ b/Core/GameEngineDevice/Source/SDL3Device/GameClient/SDL3Input.cpp @@ -861,7 +861,7 @@ void SDL3InputManager::processGamepadInput() } addMouseSDLEvent(motionEvent); - SDL_WarpMouseInWindow(nullptr, motionEvent.motion.x, motionEvent.motion.y); + SDL_WarpMouseInWindow(m_window, motionEvent.motion.x, motionEvent.motion.y); } float rx = SDL_GetGamepadAxis(m_gamepad, SDL_GAMEPAD_AXIS_RIGHTX) / AXIS_MAX; From b268cdae9bed0ef800644bf28c1617a06de849d9 Mon Sep 17 00:00:00 2001 From: githubawn <115191165+githubawn@users.noreply.github.com> Date: Tue, 21 Apr 2026 19:09:48 +0200 Subject: [PATCH 16/18] removed some dead declarations --- .../Include/SDL3Device/GameClient/SDL3Cursor.h | 2 -- .../Include/SDL3Device/GameClient/SDL3Input.h | 8 -------- .../Source/SDL3Device/GameClient/SDL3Input.cpp | 12 +----------- 3 files changed, 1 insertion(+), 21 deletions(-) diff --git a/Core/GameEngineDevice/Include/SDL3Device/GameClient/SDL3Cursor.h b/Core/GameEngineDevice/Include/SDL3Device/GameClient/SDL3Cursor.h index 0e41b3855f8..fe0e4b738ea 100644 --- a/Core/GameEngineDevice/Include/SDL3Device/GameClient/SDL3Cursor.h +++ b/Core/GameEngineDevice/Include/SDL3Device/GameClient/SDL3Cursor.h @@ -63,8 +63,6 @@ class SDL3CursorManager static void initResources(Mouse* mouse); private: - static AnimatedCursor* loadCursorFromFile(const char* filepath); static AnimatedCursor* loadANI(const char* filepath); - static AnimatedCursor* m_cursorResources[Mouse::NUM_MOUSE_CURSORS][MAX_2D_CURSOR_DIRECTIONS]; }; diff --git a/Core/GameEngineDevice/Include/SDL3Device/GameClient/SDL3Input.h b/Core/GameEngineDevice/Include/SDL3Device/GameClient/SDL3Input.h index 43b46609f17..14ce24203f1 100644 --- a/Core/GameEngineDevice/Include/SDL3Device/GameClient/SDL3Input.h +++ b/Core/GameEngineDevice/Include/SDL3Device/GameClient/SDL3Input.h @@ -85,14 +85,6 @@ class SDL3Mouse : public Mouse Bool m_IsVisible; Bool m_LostFocus; - Uint32 m_LeftButtonDownTime; - Uint32 m_RightButtonDownTime; - Uint32 m_MiddleButtonDownTime; - - ICoord2D m_LeftButtonDownPos; - ICoord2D m_RightButtonDownPos; - ICoord2D m_MiddleButtonDownPos; - Int m_directionFrame; float m_accumulatedDeltaX; diff --git a/Core/GameEngineDevice/Source/SDL3Device/GameClient/SDL3Input.cpp b/Core/GameEngineDevice/Source/SDL3Device/GameClient/SDL3Input.cpp index 608219e695e..cffbcbb8fbe 100644 --- a/Core/GameEngineDevice/Source/SDL3Device/GameClient/SDL3Input.cpp +++ b/Core/GameEngineDevice/Source/SDL3Device/GameClient/SDL3Input.cpp @@ -32,7 +32,6 @@ #include #include #include "SDL3Device/GameClient/SDL3Cursor.h" -#include // For timeGetTime() #include "SDL3Device/GameClient/SDL3Input.h" #include "Common/Debug.h" @@ -61,20 +60,11 @@ SDL3Mouse::SDL3Mouse(SDL_Window* window) m_IsCaptured(false), m_IsVisible(true), m_LostFocus(false), - m_LeftButtonDownTime(0), - m_RightButtonDownTime(0), - m_MiddleButtonDownTime(0), m_directionFrame(0), m_accumulatedDeltaX(0.0f), m_accumulatedDeltaY(0.0f), m_activeSDLCursor(nullptr) { - m_LeftButtonDownPos.x = 0; - m_LeftButtonDownPos.y = 0; - m_RightButtonDownPos.x = 0; - m_RightButtonDownPos.y = 0; - m_MiddleButtonDownPos.x = 0; - m_MiddleButtonDownPos.y = 0; } /** @@ -464,7 +454,7 @@ void SDL3Keyboard::getKey(KeyboardIO *key) key->key = keyDef; key->status = KeyboardIO::STATUS_UNUSED; key->state = keyEvent.down ? KEY_STATE_DOWN : KEY_STATE_UP; - key->keyDownTimeMsec = keyEvent.down ? timeGetTime() : 0; + key->keyDownTimeMsec = keyEvent.down ? (Uint32)SDL_GetTicks() : 0; SDL_Keymod mod = keyEvent.mod; if (mod & SDL_KMOD_LSHIFT) key->state |= KEY_STATE_LSHIFT; From a7883e8291a3bc5fed939cdf917d9f8ccbe77a26 Mon Sep 17 00:00:00 2001 From: githubawn <115191165+githubawn@users.noreply.github.com> Date: Tue, 21 Apr 2026 22:55:55 +0200 Subject: [PATCH 17/18] temporary re-added custom decoder --- .../SDL3Device/GameClient/SDL3Cursor.cpp | 117 +++++++++++------- 1 file changed, 74 insertions(+), 43 deletions(-) diff --git a/Core/GameEngineDevice/Source/SDL3Device/GameClient/SDL3Cursor.cpp b/Core/GameEngineDevice/Source/SDL3Device/GameClient/SDL3Cursor.cpp index 45cd031df3a..b58793b75ff 100644 --- a/Core/GameEngineDevice/Source/SDL3Device/GameClient/SDL3Cursor.cpp +++ b/Core/GameEngineDevice/Source/SDL3Device/GameClient/SDL3Cursor.cpp @@ -93,68 +93,99 @@ AnimatedCursor* SDL3CursorManager::loadANI(const char* filepath) File* file = TheFileSystem->openFile(filepath, File::READ | File::BINARY); if (!file) { - DEBUG_LOG(("loadANI: Failed to open ANI cursor [%s]", filepath)); return nullptr; } Int size = file->size(); - if (size <= 0) - { - DEBUG_LOG(("loadANI: File is empty [%s]", filepath)); - file->close(); - return nullptr; - } - - std::unique_ptr file_buffer(new char[size]); - if (file->read(file_buffer.get(), size) != size) - { - DEBUG_LOG(("loadANI: Failed to read ANI cursor [%s]", filepath)); - file->close(); - return nullptr; - } + std::vector buf(size); + file->read(buf.data(), size); file->close(); - DEBUG_LOG(("loadANI: Loading %s", filepath)); - - SDL_IOStream *io = SDL_IOFromConstMem(file_buffer.get(), (size_t)size); - if (!io) return nullptr; + std::vector frames; + int hot_spot_x = 0, hot_spot_y = 0; + Uint32 rate = 1; - // Use SDL3_image to load the animation (handles RIFF/ANI container and frame decoding) - IMG_Animation *anim = IMG_LoadAnimation_IO(io, true); - if (!anim) + // Detect RIFF/ACON container + if (buf.size() >= 12 && memcmp(buf.data(), "RIFF", 4) == 0 && memcmp(buf.data() + 8, "ACON", 4) == 0) { - DEBUG_LOG(("loadANI: IMG_LoadAnimation_IO failed for [%s]: %s", filepath, SDL_GetError())); - return nullptr; + char* p = buf.data() + 12; + char* end = buf.data() + buf.size(); + while (p + 8 <= end) + { + Uint32 id, sz; + memcpy(&id, p, 4); + memcpy(&sz, p + 4, 4); + p += 8; + + if (id == *(Uint32*)"anih" && sz >= 36) + { + memcpy(&rate, p + 28, 4); + } + else if (id == *(Uint32*)"LIST" && sz >= 4 && memcmp(p, "fram", 4) == 0) + { + char* lp = p + 4; + char* le = p + sz; + while (lp + 8 <= le) + { + Uint32 fid, fsz; + memcpy(&fid, lp, 4); + memcpy(&fsz, lp + 4, 4); + lp += 8; + + if (fid == *(Uint32*)"icon") + { + SDL_IOStream* io = SDL_IOFromConstMem(lp, fsz); + SDL_Surface* s = IMG_LoadTyped_IO(io, true, "ico"); + if (s) + { + if (frames.empty()) + { + SDL_PropertiesID pr = SDL_GetSurfaceProperties(s); + hot_spot_x = (int)SDL_GetNumberProperty(pr, SDL_PROP_SURFACE_HOTSPOT_X_NUMBER, 0); + hot_spot_y = (int)SDL_GetNumberProperty(pr, SDL_PROP_SURFACE_HOTSPOT_Y_NUMBER, 0); + } + frames.push_back({ s, (Uint32)(rate * 1000 / 60) }); + } + } + lp += (fsz + (fsz & 1)); + } + } + p += (sz + (sz & 1)); + } + } + else + { + // Fallback for direct ICO/CUR files + SDL_IOStream* io = SDL_IOFromConstMem(buf.data(), buf.size()); + SDL_Surface* s = IMG_LoadTyped_IO(io, true, "ico"); + if (s) + { + SDL_PropertiesID pr = SDL_GetSurfaceProperties(s); + hot_spot_x = (int)SDL_GetNumberProperty(pr, SDL_PROP_SURFACE_HOTSPOT_X_NUMBER, 0); + hot_spot_y = (int)SDL_GetNumberProperty(pr, SDL_PROP_SURFACE_HOTSPOT_Y_NUMBER, 0); + frames.push_back({ s, 16 }); + } } - if (anim->count == 0) + if (frames.empty()) { - IMG_FreeAnimation(anim); return nullptr; } - // Get hotspots from the first frame's properties (SDL3_image sets these for ICO/CUR) - SDL_PropertiesID props = SDL_GetSurfaceProperties(anim->frames[0]); - int hot_spot_x = (int)SDL_GetNumberProperty(props, SDL_PROP_SURFACE_HOTSPOT_X_NUMBER, 0); - int hot_spot_y = (int)SDL_GetNumberProperty(props, SDL_PROP_SURFACE_HOTSPOT_Y_NUMBER, 0); - - // Create the animated cursor resource std::unique_ptr cursor(new AnimatedCursor()); - if (anim->count == 1) + if (frames.size() > 1) { - cursor->m_cursor = SDL_CreateColorCursor(anim->frames[0], hot_spot_x, hot_spot_y); + cursor->m_cursor = SDL_CreateAnimatedCursor(frames.data(), (int)frames.size(), hot_spot_x, hot_spot_y); } else { - std::vector frames(anim->count); - for (int i = 0; i < anim->count; i++) - { - frames[i].surface = anim->frames[i]; - frames[i].duration = (Uint32)anim->delays[i]; - } - cursor->m_cursor = SDL_CreateAnimatedCursor(frames.data(), anim->count, hot_spot_x, hot_spot_y); + cursor->m_cursor = SDL_CreateColorCursor(frames[0].surface, hot_spot_x, hot_spot_y); + } + + for (auto& f : frames) + { + SDL_DestroySurface(f.surface); } - IMG_FreeAnimation(anim); - return cursor.release(); + return cursor.release(); } From 038aea09581dd8f6d4b78edc3066e7141eda5b0e Mon Sep 17 00:00:00 2001 From: githubawn <115191165+githubawn@users.noreply.github.com> Date: Wed, 22 Apr 2026 20:15:54 +0200 Subject: [PATCH 18/18] dont early return for headless mode --- .../Source/SDL3GameEngine.cpp | 23 +++++++++---------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/Core/GameEngineDevice/Source/SDL3GameEngine.cpp b/Core/GameEngineDevice/Source/SDL3GameEngine.cpp index c719c43b19d..b8b7b2c2dc7 100644 --- a/Core/GameEngineDevice/Source/SDL3GameEngine.cpp +++ b/Core/GameEngineDevice/Source/SDL3GameEngine.cpp @@ -149,19 +149,18 @@ void SDL3GameEngine::init(void) // Verify window was created by SDL3Main integration extern SDL_Window* TheSDL3Window; extern HWND ApplicationHWnd; - - if (!TheSDL3Window || !ApplicationHWnd) { - return; - } - - // Store window reference locally - m_SDLWindow = TheSDL3Window; - m_IsInitialized = true; - m_IsActive = true; - // Initialize the unified input manager - if (!TheSDL3InputManager) { - TheSDL3InputManager = new SDL3InputManager(m_SDLWindow); + if (TheSDL3Window && ApplicationHWnd) + { + // Store window reference locally + m_SDLWindow = TheSDL3Window; + m_IsInitialized = true; + m_IsActive = true; + + // Initialize the unified input manager + if (!TheSDL3InputManager) { + TheSDL3InputManager = new SDL3InputManager(m_SDLWindow); + } } // Call parent init to initialize game subsystems