diff --git a/CMakeLists.txt b/CMakeLists.txt index 28ce09560e9..56d559d7d83 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -66,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/Core/GameEngineDevice/CMakeLists.txt b/Core/GameEngineDevice/CMakeLists.txt index 74b040200ae..7d630f42230 100644 --- a/Core/GameEngineDevice/CMakeLists.txt +++ b/Core/GameEngineDevice/CMakeLists.txt @@ -192,6 +192,18 @@ 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 + Include/SDL3Device/GameClient/SDL3Cursor.h + Source/SDL3GameEngine.cpp + Source/SDL3Device/GameClient/SDL3Input.cpp + Source/SDL3Device/GameClient/SDL3Cursor.cpp + ) +endif() + # Add C++ 17 FileSystem implementation for non-VS6 builds if(NOT IS_VS6_BUILD) list(APPEND GAMEENGINEDEVICE_SRC @@ -227,6 +239,14 @@ 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::SDL3 + SDL3_image::SDL3_image + ) +endif() + if(RTS_BUILD_OPTION_FFMPEG) find_package(FFMPEG REQUIRED) diff --git a/Core/GameEngineDevice/Include/SDL3Device/GameClient/SDL3Cursor.h b/Core/GameEngineDevice/Include/SDL3Device/GameClient/SDL3Cursor.h new file mode 100644 index 00000000000..fe0e4b738ea --- /dev/null +++ b/Core/GameEngineDevice/Include/SDL3Device/GameClient/SDL3Cursor.h @@ -0,0 +1,68 @@ +/* +** 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* 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 new file mode 100644 index 00000000000..14ce24203f1 --- /dev/null +++ b/Core/GameEngineDevice/Include/SDL3Device/GameClient/SDL3Input.h @@ -0,0 +1,195 @@ +/* +** 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" + +// SYSTEM INCLUDES +#include +#include +#include + +// USER INCLUDES +#include "GameClient/Mouse.h" +#include "GameClient/Keyboard.h" +#include "GameClient/KeyDefs.h" + +// FORWARD REFERENCES +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; + static void freeCursorResources(void); + + // 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); + + SDL_Window* m_Window; + Bool m_IsCaptured; + Bool m_IsVisible; + Bool m_LostFocus; + + Int m_directionFrame; + + float m_accumulatedDeltaX; + float m_accumulatedDeltaY; + + SDL_Cursor* m_activeSDLCursor; +}; + +// 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(SDL_Window* window); + 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(); + + SDL_Window* m_window; + SDL_Gamepad* m_gamepad; + 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 + 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..8b854886993 --- /dev/null +++ b/Core/GameEngineDevice/Include/SDL3GameEngine.h @@ -0,0 +1,95 @@ +/* +** 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 "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/SDL3Cursor.cpp b/Core/GameEngineDevice/Source/SDL3Device/GameClient/SDL3Cursor.cpp new file mode 100644 index 00000000000..b58793b75ff --- /dev/null +++ b/Core/GameEngineDevice/Source/SDL3Device/GameClient/SDL3Cursor.cpp @@ -0,0 +1,191 @@ +/* +** 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 }; + +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) + { + return nullptr; + } + + Int size = file->size(); + std::vector buf(size); + file->read(buf.data(), size); + file->close(); + + std::vector frames; + int hot_spot_x = 0, hot_spot_y = 0; + Uint32 rate = 1; + + // Detect RIFF/ACON container + if (buf.size() >= 12 && memcmp(buf.data(), "RIFF", 4) == 0 && memcmp(buf.data() + 8, "ACON", 4) == 0) + { + 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 (frames.empty()) + { + return nullptr; + } + + std::unique_ptr cursor(new AnimatedCursor()); + if (frames.size() > 1) + { + cursor->m_cursor = SDL_CreateAnimatedCursor(frames.data(), (int)frames.size(), hot_spot_x, hot_spot_y); + } + else + { + cursor->m_cursor = SDL_CreateColorCursor(frames[0].surface, hot_spot_x, hot_spot_y); + } + + 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 new file mode 100644 index 00000000000..cffbcbb8fbe --- /dev/null +++ b/Core/GameEngineDevice/Source/SDL3Device/GameClient/SDL3Input.cpp @@ -0,0 +1,880 @@ +/* +** 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 "Lib/BaseType.h" + +#define _USE_MATH_DEFINES +#include +#include +#include +#include +#include +#include +#include +#include +#include "SDL3Device/GameClient/SDL3Cursor.h" + +#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 +// ============================================================================ + +/** + * 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_directionFrame(0), + m_accumulatedDeltaX(0.0f), + m_accumulatedDeltaY(0.0f), + m_activeSDLCursor(nullptr) +{ +} + +/** + * 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(); + + 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 + { + requestedHandle = SDL3CursorManager::getCursor(cursor, m_directionFrame); + if (!requestedHandle) + { + bUseDefaultCursor = true; + } + } + + if (bUseDefaultCursor) + { + requestedHandle = SDL3CursorManager::getCursor(NORMAL, 0); + if (!requestedHandle) + { + requestedHandle = SDL_GetDefaultCursor(); + } + } + + if (requestedHandle != m_activeSDLCursor) + { + SDL_SetCursor(requestedHandle); + m_activeSDLCursor = requestedHandle; + } +} + +/** + * Initialize cursor resources (load cursor images from ANI files) + */ +void SDL3Mouse::initCursorResources(void) +{ + SDL3CursorManager::initResources(this); +} + +void SDL3Mouse::freeCursorResources(void) +{ + SDL3CursorManager::shutdown(); +} + +/** + * Set mouse cursor type + */ +void SDL3Mouse::setCursor(MouseCursor cursor) +{ + if (m_currentCursor == cursor) + { + return; + } + + Mouse::setCursor( cursor ); + m_currentCursor = cursor; +} + +/** + * 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 ? (Uint32)SDL_GetTicks() : 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(SDL_Window* window) + : m_window(window), + 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(); + SDL3Mouse::freeCursorResources(); + 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; + + if (m_window) + { + clickEvent.button.windowID = SDL_GetWindowID(m_window); + } + + 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; + + if (m_window) + { + motionEvent.motion.windowID = SDL_GetWindowID(m_window); + } + + addMouseSDLEvent(motionEvent); + SDL_WarpMouseInWindow(m_window, 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_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); }); +} diff --git a/Core/GameEngineDevice/Source/SDL3GameEngine.cpp b/Core/GameEngineDevice/Source/SDL3GameEngine.cpp new file mode 100644 index 00000000000..b8b7b2c2dc7 --- /dev/null +++ b/Core/GameEngineDevice/Source/SDL3GameEngine.cpp @@ -0,0 +1,385 @@ +/* +** 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 "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) + { + // 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 + 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)) + { + 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..dfc5d3026f1 --- /dev/null +++ b/cmake/sdl3.cmake @@ -0,0 +1,56 @@ +# 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(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_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) + + # Populate SDL3 and SDL3_image + FetchContent_MakeAvailable(SDL3) + FetchContent_MakeAvailable(SDL3_image) +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(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. 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-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..c6e9380319a 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -1,8 +1,9 @@ { - "$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 + "$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