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