diff --git a/CMakeLists.txt b/CMakeLists.txt index 39062e3fbe6..9c42722fa09 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -67,6 +67,10 @@ include(cmake/gamespy.cmake) include(cmake/lzhl.cmake) include(cmake/stb.cmake) +if(RTS_BUILD_OPTION_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/GameEngine/Include/GameClient/Display.h b/Core/GameEngine/Include/GameClient/Display.h index 9fc937bfaa2..d025e9a73d4 100644 --- a/Core/GameEngine/Include/GameClient/Display.h +++ b/Core/GameEngine/Include/GameClient/Display.h @@ -90,6 +90,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 RTS_SDL3_ENABLE + 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 ); ///getInputEnabled()) - return; + if (!m_isScrolling && TheStatsCollector != nullptr) + TheStatsCollector->startScrollTime(); - prevCursor = TheMouse->getMouseCursor(); m_isScrolling = true; - TheInGameUI->setScrolling( TRUE ); - TheTacticalView->setMouseLock( TRUE ); m_scrollType = scrollType; - if(TheStatsCollector) - TheStatsCollector->startScrollTime(); } //----------------------------------------------------------------------------- void LookAtTranslator::stopScrolling() { - m_isScrolling = false; - TheInGameUI->setScrolling( FALSE ); - TheTacticalView->setMouseLock( FALSE ); - TheMouse->setCursor(prevCursor); - m_scrollType = SCROLL_NONE; - - // increment the stats if we have a stats collector - if(TheStatsCollector) + if (m_isScrolling && TheStatsCollector != nullptr) TheStatsCollector->endScrollTime(); + m_isScrolling = false; + m_scrollType = SCROLL_NONE; } //----------------------------------------------------------------------------- Bool LookAtTranslator::canScrollAtScreenEdge() const { + if (m_controllerInputActive) + return false; + if (!TheMouse->isCursorCaptured()) return false; @@ -131,6 +126,22 @@ Bool LookAtTranslator::canScrollAtScreenEdge() const return true; } +//----------------------------------------------------------------------------- +Real LookAtTranslator::getControllerScrollScale() const +{ + if (!m_controllerInputActive || !TheTacticalView) + return 1.0f; + + const Real minHeight = TheTacticalView->getMinHeightAboveGround(); + const Real maxHeight = TheTacticalView->getMaxHeightAboveGround(); + const Real heightRange = maxHeight - minHeight; + if (heightRange <= 0.0f) + return 1.0f; + + const Real zoomOutFraction = clamp(0.0f, (TheTacticalView->getHeightAboveGround() - minHeight) / heightRange, 1.0f); + return MIN_CONTROLLER_SCROLL_SCALE + zoomOutFraction * (1.0f - MIN_CONTROLLER_SCROLL_SCALE); +} + //----------------------------------------------------------------------------- LookAtTranslator::LookAtTranslator() : m_isScrolling(false), @@ -140,8 +151,9 @@ LookAtTranslator::LookAtTranslator() : m_isChangingFOV(false), m_middleButtonDownTimeMsec(0), m_lastPlaneID(INVALID_DRAWABLE_ID), - m_lastMouseMoveTimeMsec(0), - m_scrollType(SCROLL_NONE) + m_scrollType(SCROLL_NONE), + m_controllerInputActive(false), + m_lastMouseMoveTimeMsec(0) { m_anchor.x = m_anchor.y = 0; m_currentPos.x = m_currentPos.y = 0; @@ -190,6 +202,14 @@ void LookAtTranslator::setScreenEdgeScrollMode(ScreenEdgeScrollMode mode) m_screenEdgeScrollMode = mode; } +void LookAtTranslator::setControllerInputActive(Bool active) +{ + m_controllerInputActive = active; + + if (active && m_isScrolling && m_scrollType == SCROLL_SCREENEDGE) + stopScrolling(); +} + //----------------------------------------------------------------------------- /** * The LookAt Translator is responsible for camera movements. It is directly responsible for @@ -485,21 +505,22 @@ GameMessageDisposition LookAtTranslator::translateGameMessage(const GameMessage break; case SCROLL_KEY: { + const Real scrollAmount = SCROLL_AMT * getControllerScrollScale(); if (scrollDir[DIR_UP]) { - offset.y -= TheGlobalData->m_verticalScrollSpeedFactor * fpsRatio * SCROLL_AMT * TheGlobalData->m_keyboardScrollFactor; + offset.y -= TheGlobalData->m_verticalScrollSpeedFactor * fpsRatio * scrollAmount * TheGlobalData->m_keyboardScrollFactor; } if (scrollDir[DIR_DOWN]) { - offset.y += TheGlobalData->m_verticalScrollSpeedFactor * fpsRatio * SCROLL_AMT * TheGlobalData->m_keyboardScrollFactor; + offset.y += TheGlobalData->m_verticalScrollSpeedFactor * fpsRatio * scrollAmount * TheGlobalData->m_keyboardScrollFactor; } if (scrollDir[DIR_LEFT]) { - offset.x -= TheGlobalData->m_horizontalScrollSpeedFactor * fpsRatio * SCROLL_AMT * TheGlobalData->m_keyboardScrollFactor; + offset.x -= TheGlobalData->m_horizontalScrollSpeedFactor * fpsRatio * scrollAmount * TheGlobalData->m_keyboardScrollFactor; } if (scrollDir[DIR_RIGHT]) { - offset.x += TheGlobalData->m_horizontalScrollSpeedFactor * fpsRatio * SCROLL_AMT * TheGlobalData->m_keyboardScrollFactor; + offset.x += TheGlobalData->m_horizontalScrollSpeedFactor * fpsRatio * scrollAmount * TheGlobalData->m_keyboardScrollFactor; } } break; diff --git a/Core/GameEngineDevice/CMakeLists.txt b/Core/GameEngineDevice/CMakeLists.txt index cf7e2cae76d..ac041375d07 100644 --- a/Core/GameEngineDevice/CMakeLists.txt +++ b/Core/GameEngineDevice/CMakeLists.txt @@ -197,6 +197,18 @@ set(GAMEENGINEDEVICE_SRC Source/Win32Device/GameClient/Win32Mouse.cpp ) +# Add Core-level SDL3 implementation +if(RTS_BUILD_OPTION_SDL3 AND NOT IS_VS6_BUILD) + list(APPEND GAMEENGINEDEVICE_SRC + Include/SDL3Device/Common/SDL3GameEngine.h + Include/SDL3Device/GameClient/SDL3Input.h + Include/SDL3Device/GameClient/SDL3Cursor.h + Source/SDL3Device/Common/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 @@ -233,6 +245,14 @@ target_link_libraries(corei_gameenginedevice_public INTERFACE milesstub ) +# Export SDL3 dependencies for modern builds +if(RTS_BUILD_OPTION_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/Common/SDL3GameEngine.h b/Core/GameEngineDevice/Include/SDL3Device/Common/SDL3GameEngine.h new file mode 100644 index 00000000000..de76e23cf45 --- /dev/null +++ b/Core/GameEngineDevice/Include/SDL3Device/Common/SDL3GameEngine.h @@ -0,0 +1,85 @@ +/* +** 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 . +*/ + +#pragma once + +#include "Lib/BaseType.h" + +#include + +#include "Common/GameEngine.h" + +// 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; + +class SDL3GameEngine : public GameEngine +{ +public: + SDL3GameEngine(); + virtual ~SDL3GameEngine(); + + // GameEngine interface + virtual void init() override; + virtual void reset() override; + virtual void update() override; + virtual void serviceWindowsOS() override; + virtual Bool isActive() override; + virtual void setIsActive(Bool isActive) override; + + // Factory methods (override GameEngine) + virtual LocalFileSystem* createLocalFileSystem() override; + virtual ArchiveFileSystem* createArchiveFileSystem() override; + virtual GameLogic* createGameLogic() override; + virtual GameClient* createGameClient() override; + virtual ModuleFactory* createModuleFactory() override; + virtual ThingFactory* createThingFactory() override; + virtual FunctionLexicon* createFunctionLexicon() override; + virtual Radar* createRadar(Bool dummy) override; + virtual WebBrowser* createWebBrowser() override; + virtual ParticleSystemManager* createParticleSystemManager(Bool dummy) override; + virtual AudioManager* createAudioManager(Bool dummy) override; + + // SDL3 specific + virtual SDL_Window* getSDLWindow() 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 updateTextInputState(); +}; diff --git a/Core/GameEngineDevice/Include/SDL3Device/GameClient/SDL3Cursor.h b/Core/GameEngineDevice/Include/SDL3Device/GameClient/SDL3Cursor.h new file mode 100644 index 00000000000..e6be47868b4 --- /dev/null +++ b/Core/GameEngineDevice/Include/SDL3Device/GameClient/SDL3Cursor.h @@ -0,0 +1,61 @@ +/* +** 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 . +*/ + +#pragma once + +#include "Lib/BaseType.h" + +#include +#include + +#include "GameClient/Mouse.h" + +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; } +}; + +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..c1686eb390a --- /dev/null +++ b/Core/GameEngineDevice/Include/SDL3Device/GameClient/SDL3Input.h @@ -0,0 +1,188 @@ +/* +** 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 . +*/ + +#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; + +extern SDL3InputManager* TheSDL3InputManager; + +typedef KeyDefType KeyVal; + +class SDL3Mouse : public Mouse +{ +public: + SDL3Mouse(SDL_Window* window); + virtual ~SDL3Mouse(); + + // SubsystemInterface + virtual void init() override; + virtual void reset() override; + virtual void update() override; + virtual void initCursorResources() override; + static void freeCursorResources(); + + // 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() override; + virtual void releaseCapture() 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; +}; + +class SDL3Keyboard : public Keyboard +{ +public: + SDL3Keyboard(); + virtual ~SDL3Keyboard(); + + // SubsystemInterface + virtual void init() override; + virtual void reset() override; + virtual void update() override; + + // Keyboard interface + virtual Bool getCapsState() override; + + // SDL3-specific methods + void addSDLEvent(SDL_Event* event); + +protected: + virtual void getKey(KeyboardIO* key) override; + virtual KeyVal translateScanCodeToKeyVal(SDL_Scancode scan); + +private: + void translateKeyEvent(const SDL_KeyboardEvent& event); +}; + +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 DESIGNED_WINDOW_HEIGHT = 1440.0f; + static constexpr float DEFAULT_CURSOR_SPEED = 1226.6667f; + static constexpr float DEFAULT_CURSOR_ACCELERATION = 7200.0f; + static constexpr float DEFAULT_CURSOR_DECELERATION = 14400.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; + + Uint64 m_lastUpdateTime; + float m_cursorVelocityX; + float m_cursorVelocityY; + float m_cursorRemainderX; + float m_cursorRemainderY; + Bool m_isQuitting; +}; diff --git a/Core/GameEngineDevice/Source/SDL3Device/Common/SDL3GameEngine.cpp b/Core/GameEngineDevice/Source/SDL3Device/Common/SDL3GameEngine.cpp new file mode 100644 index 00000000000..a67c819dff9 --- /dev/null +++ b/Core/GameEngineDevice/Source/SDL3Device/Common/SDL3GameEngine.cpp @@ -0,0 +1,384 @@ +/* +** 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 . +*/ + +#include "Lib/BaseType.h" + +#include +#include +#include +#include + +#include "Common/GameEngine.h" +#include "GameClient/Gadget.h" +#include "GameClient/GameWindow.h" +#include "GameClient/GameWindowManager.h" +#include "GameClient/Keyboard.h" +#include "GameClient/Mouse.h" +#include "GameLogic/GameLogic.h" +#include "GameNetwork/LANAPICallbacks.h" +#include "GameNetwork/NetworkInterface.h" +#include "MilesAudioDevice/MilesAudioManager.h" +#include "SDL3Device/Common/SDL3GameEngine.h" +#include "SDL3Device/GameClient/SDL3Input.h" +#include "StdDevice/Common/StdBIGFileSystem.h" +#include "StdDevice/Common/StdLocalFileSystem.h" +#include "W3DDevice/Common/W3DFunctionLexicon.h" +#include "W3DDevice/Common/W3DModuleFactory.h" +#include "W3DDevice/Common/W3DRadar.h" +#include "W3DDevice/Common/W3DThingFactory.h" +#include "W3DDevice/GameClient/W3DGameClient.h" +#include "W3DDevice/GameClient/W3DParticleSys.h" +#include "W3DDevice/GameClient/W3DWebBrowser.h" +#include "W3DDevice/GameLogic/W3DGameLogic.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; +} + +} // namespace + +SDL3GameEngine::SDL3GameEngine() + : GameEngine() + , m_SDLWindow(nullptr) + , m_IsInitialized(false) + , m_IsActive(false) + , m_IsTextInputActive(false) + , m_TextInputFocusWindow(nullptr) +{ +} + +SDL3GameEngine::~SDL3GameEngine() +{ + if (m_SDLWindow && m_IsTextInputActive) + { + SDL_StopTextInput(m_SDLWindow); + m_IsTextInputActive = false; + m_TextInputFocusWindow = nullptr; + } + + if (TheSDL3InputManager) + { + delete TheSDL3InputManager; + } +} + +void SDL3GameEngine::init() +{ + // 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(); +} + +void SDL3GameEngine::reset() +{ + if (m_SDLWindow && m_IsTextInputActive) + { + SDL_StopTextInput(m_SDLWindow); + m_IsTextInputActive = false; + m_TextInputFocusWindow = nullptr; + } + GameEngine::reset(); +} + +void SDL3GameEngine::update() +{ + 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; + } + } + } +} + +void SDL3GameEngine::serviceWindowsOS() +{ + pollSDL3Events(); +} + +Bool SDL3GameEngine::isActive() +{ + return m_IsActive; +} + +void SDL3GameEngine::setIsActive(Bool isActive) +{ + m_IsActive = isActive; +} + +void SDL3GameEngine::pollSDL3Events() +{ + 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() +{ + 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); + } +} + +LocalFileSystem* SDL3GameEngine::createLocalFileSystem() +{ + return NEW StdLocalFileSystem; +} + +ArchiveFileSystem* SDL3GameEngine::createArchiveFileSystem() +{ + return NEW StdBIGFileSystem; +} + +GameLogic* SDL3GameEngine::createGameLogic() +{ + return NEW W3DGameLogic; +} + +GameClient* SDL3GameEngine::createGameClient() +{ + return NEW W3DGameClient; +} + +ModuleFactory* SDL3GameEngine::createModuleFactory() +{ + return NEW W3DModuleFactory; +} + +ThingFactory* SDL3GameEngine::createThingFactory() +{ + return NEW W3DThingFactory; +} + +FunctionLexicon* SDL3GameEngine::createFunctionLexicon() +{ + return NEW W3DFunctionLexicon; +} + +Radar* SDL3GameEngine::createRadar(Bool dummy) +{ + if (dummy) + return NEW RadarDummy; + return NEW W3DRadar; +} + +ParticleSystemManager* SDL3GameEngine::createParticleSystemManager(Bool dummy) +{ + if (dummy) + return NEW ParticleSystemManagerDummy; + return NEW W3DParticleSystemManager; +} + +WebBrowser* SDL3GameEngine::createWebBrowser() +{ + return nullptr; +} + +AudioManager* SDL3GameEngine::createAudioManager(Bool dummy) +{ + if (dummy) + return NEW MilesAudioManagerDummy; + return NEW MilesAudioManager; +} diff --git a/Core/GameEngineDevice/Source/SDL3Device/GameClient/SDL3Cursor.cpp b/Core/GameEngineDevice/Source/SDL3Device/GameClient/SDL3Cursor.cpp new file mode 100644 index 00000000000..b806d55a5ed --- /dev/null +++ b/Core/GameEngineDevice/Source/SDL3Device/GameClient/SDL3Cursor.cpp @@ -0,0 +1,149 @@ +/* +** 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 . +*/ + +#include +#include +#include +#include + +#include "Common/Debug.h" +#include "Common/file.h" +#include "Common/FileSystem.h" +#include "SDL3Device/GameClient/SDL3Cursor.h" + +AnimatedCursor* SDL3CursorManager::m_cursorResources[Mouse::NUM_MOUSE_CURSORS][MAX_2D_CURSOR_DIRECTIONS] = {nullptr}; + +void SDL3CursorManager::init() +{ + 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(); + + SDL_IOStream* io = SDL_IOFromConstMem(buf.data(), buf.size()); + if (!io) + { + return nullptr; + } + + IMG_Animation* anim = IMG_LoadAnimation_IO(io, true); + if (!anim) + { + return nullptr; + } + + if (anim->count <= 0) + { + IMG_FreeAnimation(anim); + return nullptr; + } + + int hot_spot_x = 0, hot_spot_y = 0; + if (anim->frames && anim->frames[0]) + { + SDL_PropertiesID pr = SDL_GetSurfaceProperties(anim->frames[0]); + 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); + } + + std::unique_ptr cursor(new AnimatedCursor()); + + if (anim->count > 1) + { + std::vector sdl_frames(anim->count); + for (int i = 0; i < anim->count; ++i) + { + sdl_frames[i].surface = anim->frames[i]; + sdl_frames[i].duration = anim->delays[i]; + } + cursor->m_cursor = SDL_CreateAnimatedCursor(sdl_frames.data(), anim->count, hot_spot_x, hot_spot_y); + } + else + { + cursor->m_cursor = SDL_CreateColorCursor(anim->frames[0], hot_spot_x, hot_spot_y); + } + + if (!cursor->m_cursor) + { + DEBUG_LOG(("loadANI: Failed to create cursor from %s. hot=(%d, %d), count=%d. Error: %s", filepath, hot_spot_x, hot_spot_y, anim->count, SDL_GetError())); + } + + IMG_FreeAnimation(anim); + 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..ec88bfdf869 --- /dev/null +++ b/Core/GameEngineDevice/Source/SDL3Device/GameClient/SDL3Input.cpp @@ -0,0 +1,1163 @@ +/* +** 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 . +*/ + +#include "Lib/BaseType.h" + +#define _USE_MATH_DEFINES +#include +#include +#include +#include +#include +#include +#include +#include + +#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 "GameClient/LookAtXlat.h" +#include "GameLogic/GameLogic.h" +#include "SDL3Device/Common/SDL3GameEngine.h" +#include "SDL3Device/GameClient/SDL3Cursor.h" + +SDL3InputManager* TheSDL3InputManager = nullptr; + +// SDL3Mouse implementation + +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) +{ +} + +SDL3Mouse::~SDL3Mouse() +{ + releaseCapture(); +} + +void SDL3Mouse::init() +{ + Mouse::init(); + + m_inputMovesAbsolute = true; + + // Show cursor by default + setVisibility(true); +} + +void SDL3Mouse::reset() +{ + Mouse::reset(); + + releaseCapture(); + setVisibility(true); +} + +void SDL3Mouse::update() +{ + 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; + } +} + +void SDL3Mouse::initCursorResources() +{ + SDL3CursorManager::initResources(this); +} + +void SDL3Mouse::freeCursorResources() +{ + SDL3CursorManager::shutdown(); +} + +void SDL3Mouse::setCursor(MouseCursor cursor) +{ + if (m_currentCursor == cursor) + { + return; + } + + Mouse::setCursor(cursor); + m_currentCursor = cursor; +} + +void SDL3Mouse::setVisibility(Bool visible) +{ + Mouse::setVisibility(visible); + + if (visible) + { + SDL_ShowCursor(); + } + else + { + SDL_HideCursor(); + } +} + +void SDL3Mouse::loseFocus() +{ + Mouse::loseFocus(); + releaseCapture(); +} + +void SDL3Mouse::regainFocus() +{ + Mouse::regainFocus(); +} + +void SDL3Mouse::capture() +{ + if (!m_Window || m_isCursorCaptured) + { + return; + } + + SDL_CaptureMouse(true); + SDL_SetWindowMouseGrab(m_Window, true); + onCursorCaptured(true); +} + +void SDL3Mouse::releaseCapture() +{ + if (!m_isCursorCaptured) + { + return; + } + + SDL_CaptureMouse(false); + if (m_Window) + { + SDL_SetWindowMouseGrab(m_Window, false); + } + + onCursorCaptured(false); +} + +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, engine 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 * 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; + // Mouse events are delivered in logical window points, so scale against logical window size + SDL_GetWindowSize(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 + +SDL3Keyboard::SDL3Keyboard() + : Keyboard() +{} +SDL3Keyboard::~SDL3Keyboard() {} + +void SDL3Keyboard::init() { Keyboard::init(); } +void SDL3Keyboard::reset() { Keyboard::reset(); } +void SDL3Keyboard::update() { Keyboard::update(); } + +Bool SDL3Keyboard::getCapsState() +{ + return (SDL_GetModState() & SDL_KMOD_CAPS) != 0; +} + +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(SDL_Scancode scan) +{ + switch (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 + +SDL3InputManager::SDL3InputManager(SDL_Window* window) + : m_window(window) + , m_mouseNextFree(0) + , m_mouseNextGet(0) + , m_keyNextFree(0) + , m_keyNextGet(0) + , m_gamepad(nullptr) + , m_lastUpdateTime(0) + , m_cursorVelocityX(0.0f) + , m_cursorVelocityY(0.0f) + , m_cursorRemainderX(0.0f) + , m_cursorRemainderY(0.0f) + , 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; +} + +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(); +} + +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; +} + +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_state.rtDown) + virtualPulseKey(SDL_SCANCODE_LCTRL, false); + + if (m_gamepad) + { + if (m_state.stickLeft) virtualPulseKey(SDL_SCANCODE_LEFT, false); + if (m_state.stickRight) virtualPulseKey(SDL_SCANCODE_RIGHT, false); + if (m_state.stickUp) virtualPulseKey(SDL_SCANCODE_UP, false); + if (m_state.stickDown) virtualPulseKey(SDL_SCANCODE_DOWN, false); + + if (m_state.buttonState[SDL_GAMEPAD_BUTTON_SOUTH]) virtualPulseKey(SDL_SCANCODE_RETURN, false); + if (m_state.buttonState[SDL_GAMEPAD_BUTTON_EAST]) virtualPulseKey(SDL_SCANCODE_ESCAPE, false); + if (m_state.buttonState[SDL_GAMEPAD_BUTTON_WEST]) virtualPulseKey(SDL_SCANCODE_X, false); + if (m_state.buttonState[SDL_GAMEPAD_BUTTON_NORTH]) virtualPulseKey(SDL_SCANCODE_E, false); + if (m_state.buttonState[SDL_GAMEPAD_BUTTON_LEFT_SHOULDER]) virtualPulseKey(SDL_SCANCODE_Q, false); + if (m_state.buttonState[SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER]) virtualPulseKey(SDL_SCANCODE_E, false); + if (m_state.buttonState[SDL_GAMEPAD_BUTTON_DPAD_UP]) virtualPulseKey(SDL_SCANCODE_1, false); + if (m_state.buttonState[SDL_GAMEPAD_BUTTON_DPAD_DOWN]) virtualPulseKey(SDL_SCANCODE_2, false); + if (m_state.buttonState[SDL_GAMEPAD_BUTTON_DPAD_LEFT]) virtualPulseKey(SDL_SCANCODE_3, false); + if (m_state.buttonState[SDL_GAMEPAD_BUTTON_DPAD_RIGHT]) virtualPulseKey(SDL_SCANCODE_4, false); + + m_state = GamepadState(); + m_lastUpdateTime = 0; + + SDL_CloseGamepad(m_gamepad); + m_gamepad = nullptr; + } + + m_cursorVelocityX = 0.0f; + m_cursorVelocityY = 0.0f; + m_cursorRemainderX = 0.0f; + m_cursorRemainderY = 0.0f; + + if (TheLookAtTranslator) + TheLookAtTranslator->setControllerInputActive(false); +} + +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) +{ + static Uint64 lastClickTime[3] = {0, 0, 0}; + Uint64 now = SDL_GetTicks(); + + 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; + + int buttonIdx = (button == SDL_BUTTON_LEFT) ? 0 : ((button == SDL_BUTTON_RIGHT) ? 1 : 2); + if (down) + { + if (now - lastClickTime[buttonIdx] < 350) + clickEvent.button.clicks = 2; + else + clickEvent.button.clicks = 1; + lastClickTime[buttonIdx] = now; + } + else + { + 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; + + if (TheLookAtTranslator) + TheLookAtTranslator->setControllerInputActive(true); + + Uint64 now = SDL_GetTicks(); + float deltaTime = (now - m_lastUpdateTime) / 1000.0f; + m_lastUpdateTime = now; + if (deltaTime > 0.1f) + deltaTime = 0.1f; + + const float DEADZONE = DEFAULT_DEADZONE; + float resolutionScale = 1.0f; + int windowWidth = 0; + int windowHeight = 0; + if (m_window && SDL_GetWindowSizeInPixels(m_window, &windowWidth, &windowHeight) && windowHeight > 0) + resolutionScale = windowHeight / DESIGNED_WINDOW_HEIGHT; + + const float CURSOR_SPEED = DEFAULT_CURSOR_SPEED * resolutionScale; + const float CURSOR_ACCELERATION = DEFAULT_CURSOR_ACCELERATION * resolutionScale; + const float CURSOR_DECELERATION = DEFAULT_CURSOR_DECELERATION * resolutionScale; + + // 1. TRIGGERS (Modifiers: RT = Force Attack / LCtrl, LT = Unmapped) + bool ltPressed = SDL_GetGamepadAxis(m_gamepad, SDL_GAMEPAD_AXIS_LEFT_TRIGGER) > TRIGGER_THRESHOLD; + if (ltPressed != m_state.ltDown) + { + m_state.ltDown = ltPressed; + } + + 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; + float stickMagnitude = sqrtf(lx * lx + ly * ly); + if (stickMagnitude > 1.0f) + stickMagnitude = 1.0f; + + float targetVelocityX = 0.0f; + float targetVelocityY = 0.0f; + if (stickMagnitude > DEADZONE) + { + float speed = CURSOR_SPEED; + + // Radially remove the deadzone, then use a smoothstep response curve. + float response = (stickMagnitude - DEADZONE) / (1.0f - DEADZONE); + response = response * response * (3.0f - 2.0f * response); + targetVelocityX = (lx / stickMagnitude) * speed * response; + targetVelocityY = (ly / stickMagnitude) * speed * response; + } + + float velocityDeltaX = targetVelocityX - m_cursorVelocityX; + float velocityDeltaY = targetVelocityY - m_cursorVelocityY; + float velocityDeltaLength = sqrtf(velocityDeltaX * velocityDeltaX + velocityDeltaY * velocityDeltaY); + float acceleration = stickMagnitude > DEADZONE ? CURSOR_ACCELERATION : CURSOR_DECELERATION; + float maxVelocityChange = acceleration * deltaTime; + if (velocityDeltaLength > maxVelocityChange && velocityDeltaLength > 0.0f) + { + float changeScale = maxVelocityChange / velocityDeltaLength; + velocityDeltaX *= changeScale; + velocityDeltaY *= changeScale; + } + m_cursorVelocityX += velocityDeltaX; + m_cursorVelocityY += velocityDeltaY; + + m_cursorRemainderX += m_cursorVelocityX * deltaTime; + m_cursorRemainderY += m_cursorVelocityY * deltaTime; + int cursorDeltaX = (int)m_cursorRemainderX; + int cursorDeltaY = (int)m_cursorRemainderY; + m_cursorRemainderX -= cursorDeltaX; + m_cursorRemainderY -= cursorDeltaY; + + if (cursorDeltaX != 0 || cursorDeltaY != 0) + { + SDL_Event motionEvent; + memset(&motionEvent, 0, sizeof(motionEvent)); + motionEvent.type = SDL_EVENT_MOUSE_MOTION; + motionEvent.motion.xrel = (float)cursorDeltaX; + motionEvent.motion.yrel = (float)cursorDeltaY; + + float mx, my; + SDL_GetMouseState(&mx, &my); + motionEvent.motion.x = mx + cursorDeltaX; + motionEvent.motion.y = my + cursorDeltaY; + + 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, + [this](bool d) { virtualPulseKey(SDL_SCANCODE_LEFT, d); } + ); + handleGamepadButton( + SDL_GAMEPAD_BUTTON_INVALID, + m_state.stickRight, + rx > DEADZONE, + [this](bool d) { virtualPulseKey(SDL_SCANCODE_RIGHT, d); } + ); + handleGamepadButton( + SDL_GAMEPAD_BUTTON_INVALID, + m_state.stickUp, + ry < -DEADZONE, + [this](bool d) { virtualPulseKey(SDL_SCANCODE_UP, d); } + ); + handleGamepadButton( + SDL_GAMEPAD_BUTTON_INVALID, + m_state.stickDown, + ry > DEADZONE, + [this](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), + [this](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), + [this](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), + [this](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), + [this](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), + [this](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), + [this](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), + [this](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), + [this](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), + [this](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), + [this](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), + [this](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), + [this](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), + [this](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), + [this](bool d) { if (d) TheMessageStream->appendMessage(GameMessage::MSG_META_VIEW_COMMAND_CENTER); } + ); +} diff --git a/Core/Main/AppMain.cpp b/Core/Main/AppMain.cpp new file mode 100644 index 00000000000..391bf5e979f --- /dev/null +++ b/Core/Main/AppMain.cpp @@ -0,0 +1,194 @@ +/* +** 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 . +*/ + +#include "Lib/BaseType.h" +#include "AppMain.h" + +#define WIN32_LEAN_AND_MEAN +#include +#include +#include "Common/CommandLine.h" +#include "Common/CriticalSection.h" +#include "Common/Debug.h" +#include "Common/GameEngine.h" +#include "Common/GameMemory.h" +#include "Common/GlobalData.h" +#include "Common/StackDump.h" +#include "Common/version.h" +#include "BuildVersion.h" +#include "GeneratedVersion.h" +#include "GameClient/ClientInstance.h" + +#include "Common/Registry.h" + +#ifdef RTS_ENABLE_CRASHDUMP +#include "Common/MiniDumper.h" +#endif + +// Global game data paths & prefixes required by GameText +const Char *g_strFile = "data\\Generals.str"; +const Char *g_csfFile = "data\\%s\\Generals.csf"; +const char *gAppPrefix = ""; + +class Win32Mouse; +Win32Mouse *TheWin32Mouse = nullptr; + +// Critical sections for memory manager and string systems +static CriticalSection critSec1, critSec2, critSec3, critSec4, critSec5; + +namespace AppMain +{ + +Bool initBeforeWindow() +{ + TheAsciiStringCriticalSection = &critSec1; + TheUnicodeStringCriticalSection = &critSec2; + TheDmaCriticalSection = &critSec3; + TheMemoryPoolCriticalSection = &critSec4; + TheDebugLogCriticalSection = &critSec5; + + // Initialize memory manager early + initMemoryManager(); + + // Set working directory to executable folder + Char buffer[_MAX_PATH]; + GetModuleFileName(nullptr, buffer, sizeof(buffer)); + if (Char* pEnd = strrchr(buffer, '\\')) + { + *pEnd = 0; + } + ::SetCurrentDirectory(buffer); + +#ifdef RTS_DEBUG + int tmpFlag = _CrtSetDbgFlag(_CRTDBG_REPORT_FLAG); + tmpFlag |= (_CRTDBG_LEAK_CHECK_DF | _CRTDBG_ALLOC_MEM_DF); + tmpFlag &= ~_CRTDBG_CHECK_CRT_DF; + _CrtSetDbgFlag(tmpFlag); +#endif + + CommandLine::parseCommandLineForStartup(); + +#ifdef RTS_ENABLE_CRASHDUMP + MiniDumper::initMiniDumper(TheGlobalData->getPath_UserData()); +#endif + + return true; +} + +Bool initAfterWindow() +{ + // Setup version info + TheVersion = NEW Version; + TheVersion->setVersion( + VERSION_MAJOR, VERSION_MINOR, VERSION_BUILDNUM, VERSION_LOCALBUILDNUM, + AsciiString(VERSION_BUILDUSER), AsciiString(VERSION_BUILDLOC), + AsciiString(__TIME__), AsciiString(__DATE__) + ); + + // Single instance mutex check + if (!rts::ClientInstance::initialize()) + { + HWND ccwindow = FindWindow(rts::ClientInstance::getFirstInstanceName(), nullptr); + if (ccwindow) + { + SetForegroundWindow(ccwindow); + ShowWindow(ccwindow, SW_RESTORE); + } + + DEBUG_LOG(("Generals is already running...Bail!")); + delete TheVersion; + TheVersion = nullptr; + shutdownMemoryManager(); + return false; + } + DEBUG_LOG(("Create Generals Mutex okay.")); + + return true; +} + +static Bool s_isAppActive = false; + +void setAppActive(Bool active) +{ + s_isAppActive = active; + if (TheGameEngine) + { + TheGameEngine->setIsActive(active); + } +} + +Bool isAppActive() +{ + return s_isAppActive; +} + +void getInitialWindowBounds(Int& outWidth, Int& outHeight) +{ + outWidth = (TheGlobalData && TheGlobalData->m_xResolution > 0) ? TheGlobalData->m_xResolution : 800; + outHeight = (TheGlobalData && TheGlobalData->m_yResolution > 0) ? TheGlobalData->m_yResolution : 600; +} + +Int run() +{ + return GameMain(); +} + +void getSplashFilePath(Char* outBuffer, UnsignedInt bufferSize) +{ + if (!outBuffer || bufferSize == 0) + return; + + const char* fileName = "Install_Final.bmp"; + sprintf(outBuffer, "Data/%s/%s", GetRegistryLanguage().str(), fileName); + FILE* fileImage = fopen(outBuffer, "rb"); + if (fileImage) + { + fclose(fileImage); + } + else + { + sprintf(outBuffer, "%s", fileName); + } +} + +void shutdown() +{ + delete TheVersion; + TheVersion = nullptr; + +#ifdef MEMORYPOOL_DEBUG + TheMemoryPoolFactory->debugMemoryReport(REPORT_POOLINFO | REPORT_POOL_OVERFLOW | REPORT_SIMPLE_LEAKS, 0, 0); +#endif +#if defined(RTS_DEBUG) + TheMemoryPoolFactory->memoryPoolUsageReport("AAAMemStats"); +#endif + + shutdownMemoryManager(); + +#ifdef RTS_ENABLE_CRASHDUMP + MiniDumper::shutdownMiniDumper(); +#endif + + TheAsciiStringCriticalSection = nullptr; + TheUnicodeStringCriticalSection = nullptr; + TheDmaCriticalSection = nullptr; + TheMemoryPoolCriticalSection = nullptr; + TheDebugLogCriticalSection = nullptr; +} + +} // namespace AppMain diff --git a/Core/Main/AppMain.h b/Core/Main/AppMain.h new file mode 100644 index 00000000000..3dcd4d214e9 --- /dev/null +++ b/Core/Main/AppMain.h @@ -0,0 +1,60 @@ +/* +** 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 . +*/ + +#pragma once + +#include "Lib/BaseType.h" + +// Forward declarations +class GameEngine; + +/** + * Shared application lifecycle management for both Win32 and SDL3 entry points. + */ +namespace AppMain +{ + /** Initialize core subsystems before window creation (memory, working dir, debug flags, command line) */ + Bool initBeforeWindow(); + + /** Initialize subsystems after window creation (version info, single-instance mutex check) */ + Bool initAfterWindow(); + + /** Execute the main game loop */ + Int run(); + + /** Get initial window dimensions from GlobalData or defaults (800x600) */ + void getInitialWindowBounds(Int& outWidth, Int& outHeight); + + /** Update application focus active state across backends */ + void setAppActive(Bool active); + + /** Get current application focus active state */ + Bool isAppActive(); + + /** Get the localized or fallback splash screen bitmap file path */ + void getSplashFilePath(Char* outBuffer, UnsignedInt bufferSize); + + /** Shutdown core subsystems (version, minidump, memory manager) */ + void shutdown(); +} + +/** Engine creation helper */ +GameEngine* CreateGameEngine(); + +/** Main engine loop */ +Int GameMain(); diff --git a/Core/Main/SDL3Main.cpp b/Core/Main/SDL3Main.cpp new file mode 100644 index 00000000000..563339b326d --- /dev/null +++ b/Core/Main/SDL3Main.cpp @@ -0,0 +1,149 @@ +/* +** 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 . +*/ + +#include "Lib/BaseType.h" +#include "SDL3Main.h" + +#include +#include + +#include "AppMain.h" +#include "Common/CommandLine.h" +#include "Common/Debug.h" +#include "Common/GlobalData.h" +#include "Common/Registry.h" +#include "SDL3Device/Common/SDL3GameEngine.h" + +SDL_Window* TheSDL3Window = nullptr; +#ifdef _WIN32 +HINSTANCE ApplicationHInstance = nullptr; +HWND ApplicationHWnd = nullptr; +#endif + +static SDL_Surface* gLoadScreenSurface = nullptr; + +GameEngine* CreateGameEngine() +{ + SDL3GameEngine* engine = NEW SDL3GameEngine; + engine->setIsActive(true); + return engine; +} + +int main(int argc, char* argv[]) +{ + (void)argc; + (void)argv; + + Int exitcode = 1; + + if (!AppMain::initBeforeWindow()) + { + return exitcode; + } + +#ifdef _WIN32 + ApplicationHInstance = GetModuleHandle(nullptr); +#endif + + // Load splash screen surface + char filePath[512]; + AppMain::getSplashFilePath(filePath, sizeof(filePath)); + gLoadScreenSurface = SDL_LoadBMP(filePath); + + 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())); + AppMain::shutdown(); + return exitcode; + } + + Uint32 flags = SDL_WINDOW_HIDDEN | SDL_WINDOW_RESIZABLE; + if (!TheGlobalData->m_windowed) + { + flags |= SDL_WINDOW_FULLSCREEN; + } + + Int startWidth, startHeight; + AppMain::getInitialWindowBounds(startWidth, startHeight); + + TheSDL3Window = SDL_CreateWindow("Command & Conquer Generals", startWidth, startHeight, flags); + if (!TheSDL3Window) + { + DEBUG_LOG(("SDL_CreateWindow failed: %s", SDL_GetError())); + SDL_Quit(); + AppMain::shutdown(); + return exitcode; + } + +#ifdef _WIN32 + SDL_PropertiesID props = SDL_GetWindowProperties(TheSDL3Window); + ApplicationHWnd = (HWND)SDL_GetPointerProperty(props, SDL_PROP_WINDOW_WIN32_HWND_POINTER, nullptr); +#endif + + SDL_ShowWindow(TheSDL3Window); + AppMain::setAppActive(true); + + // Render splash screen onto SDL window surface + 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, nullptr, screen, &destRect, SDL_SCALEMODE_LINEAR); + SDL_UpdateWindowSurface(TheSDL3Window); + } + } + } + + if (gLoadScreenSurface != nullptr) + { + SDL_DestroySurface(gLoadScreenSurface); + gLoadScreenSurface = nullptr; + } + + if (!AppMain::initAfterWindow()) + { + if (TheSDL3Window) + { + SDL_DestroyWindow(TheSDL3Window); + TheSDL3Window = nullptr; + } + SDL_Quit(); + return exitcode; + } + + exitcode = AppMain::run(); + + if (TheSDL3Window) + { + SDL_DestroyWindow(TheSDL3Window); + TheSDL3Window = nullptr; + } + SDL_Quit(); + + AppMain::shutdown(); + + return exitcode; +} diff --git a/Core/Main/SDL3Main.h b/Core/Main/SDL3Main.h new file mode 100644 index 00000000000..c349f9b3633 --- /dev/null +++ b/Core/Main/SDL3Main.h @@ -0,0 +1,33 @@ +/* +** 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 . +*/ + +#pragma once + +#include + +#ifdef _WIN32 +#include +#endif + +// Global SDL3 window pointer +extern SDL_Window* TheSDL3Window; + +#ifdef _WIN32 +// Native HWND export for DirectX compatibility +extern HWND ApplicationHWnd; +#endif diff --git a/Generals/Code/Main/CMakeLists.txt b/Generals/Code/Main/CMakeLists.txt index f40c7434bdf..45c7c0c5e36 100644 --- a/Generals/Code/Main/CMakeLists.txt +++ b/Generals/Code/Main/CMakeLists.txt @@ -10,6 +10,7 @@ endif() target_link_libraries(g_generals PRIVATE binkstub comctl32 + corei_main d3d8 d3dx8 dinput8 @@ -65,10 +66,22 @@ target_include_directories(g_generals PRIVATE ) target_sources(g_generals PRIVATE - WinMain.cpp - WinMain.h + ${CMAKE_SOURCE_DIR}/Core/Main/AppMain.cpp + ${CMAKE_SOURCE_DIR}/Core/Main/AppMain.h ) +if(RTS_BUILD_OPTION_SDL3) + target_sources(g_generals PRIVATE + ${CMAKE_SOURCE_DIR}/Core/Main/SDL3Main.cpp + ${CMAKE_SOURCE_DIR}/Core/Main/SDL3Main.h + ) +else() + target_sources(g_generals PRIVATE + WinMain.cpp + WinMain.h + ) +endif() + # RC files are optional for MinGW builds # Icon: LoadIcon() handles missing resource gracefully (uses default system icon) # Manifest: DPI awareness metadata (nice to have but not essential) diff --git a/Generals/Code/Main/WinMain.cpp b/Generals/Code/Main/WinMain.cpp index c8e9bb9961d..41c9f68d08e 100644 --- a/Generals/Code/Main/WinMain.cpp +++ b/Generals/Code/Main/WinMain.cpp @@ -40,6 +40,7 @@ #include // USER INCLUDES ////////////////////////////////////////////////////////////// +#include "AppMain.h" #include "WinMain.h" #include "Lib/BaseType.h" #include "Common/CommandLine.h" @@ -59,8 +60,8 @@ #include "GameLogic/GameLogic.h" ///< @todo for demo, remove #include "GameClient/Mouse.h" #include "GameClient/IMEManager.h" -#include "Win32Device/GameClient/Win32Mouse.h" #include "Win32Device/Common/Win32GameEngine.h" +#include "Win32Device/GameClient/Win32Mouse.h" #include "Common/version.h" #include "BuildVersion.h" #include "GeneratedVersion.h" @@ -73,12 +74,12 @@ // GLOBALS //////////////////////////////////////////////////////////////////// HINSTANCE ApplicationHInstance = nullptr; ///< our application instance HWND ApplicationHWnd = nullptr; ///< our application window handle -Win32Mouse *TheWin32Mouse = nullptr; ///< for the WndProc() only +extern Win32Mouse *TheWin32Mouse; ///< defined in AppMain.cpp DWORD TheMessageTime = 0; ///< For getting the time that a message was posted from Windows. -const Char *g_strFile = "data\\Generals.str"; -const Char *g_csfFile = "data\\%s\\Generals.csf"; -const char *gAppPrefix = ""; /// So WB can have a different debug log file name. +extern const Char *g_strFile; +extern const Char *g_csfFile; +extern const char *gAppPrefix; static Bool gInitializing = false; static Bool gDoPaint = true; @@ -469,9 +470,7 @@ LRESULT CALLBACK WndProc( HWND hWnd, UINT message, // paths that take care of that. isWinMainActive = (BOOL) wParam; - - if (TheGameEngine) - TheGameEngine->setIsActive(isWinMainActive); + AppMain::setAppActive(isWinMainActive); if (isWinMainActive) { @@ -841,19 +840,18 @@ Int APIENTRY WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, // WWDebug_Install_Message_Handler(WWDebug_Message_Callback); // WWDebug_Install_Assert_Handler(WWAssert_Callback); - // [SKB: Jun 24 2003 @ 1:50pm] : - // Force to be loaded from a file, not a resource so same exe can be used in germany and retail. - gLoadScreenBitmap = (HBITMAP)LoadImage(hInstance, "Install_Final.bmp", IMAGE_BITMAP, 0, 0, LR_SHARED|LR_LOADFROMFILE); + if (!AppMain::initBeforeWindow()) + { + return exitcode; + } - CommandLine::parseCommandLineForStartup(); + char filePath[_MAX_PATH]; + AppMain::getSplashFilePath(filePath, sizeof(filePath)); + gLoadScreenBitmap = (HBITMAP)LoadImage(hInstance, filePath, IMAGE_BITMAP, 0, 0, LR_SHARED | LR_LOADFROMFILE); -#ifdef RTS_ENABLE_CRASHDUMP - // Initialize minidump facilities - requires TheGlobalData so performed after parseCommandLineForStartup - MiniDumper::initMiniDumper(TheGlobalData->getPath_UserData()); -#endif - // register windows class and create application window if(!TheGlobalData->m_headless && initializeAppWindows(hInstance, nCmdShow, TheGlobalData->m_windowed) == false) { + AppMain::shutdown(); return exitcode; } @@ -865,55 +863,16 @@ Int APIENTRY WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, gLoadScreenBitmap = nullptr; } - - // BGC - initialize COM - // OleInitialize(nullptr); - - - - // Set up version info - TheVersion = NEW Version; - TheVersion->setVersion(VERSION_MAJOR, VERSION_MINOR, VERSION_BUILDNUM, VERSION_LOCALBUILDNUM, - AsciiString(VERSION_BUILDUSER), AsciiString(VERSION_BUILDLOC), - AsciiString(__TIME__), AsciiString(__DATE__)); - - // TheSuperHackers @refactor The instance mutex now lives in its own class. - - if (!rts::ClientInstance::initialize()) + if (!AppMain::initAfterWindow()) { - HWND ccwindow = FindWindow(rts::ClientInstance::getFirstInstanceName(), nullptr); - if (ccwindow) - { - SetForegroundWindow(ccwindow); - ShowWindow(ccwindow, SW_RESTORE); - } - - DEBUG_LOG(("Generals is already running...Bail!")); - delete TheVersion; - TheVersion = nullptr; - shutdownMemoryManager(); + AppMain::shutdown(); return exitcode; } - DEBUG_LOG(("Create Generals Mutex okay.")); - DEBUG_LOG(("CRC message is %d", GameMessage::MSG_LOGIC_CRC)); // run the game main loop - exitcode = GameMain(); - - delete TheVersion; - TheVersion = nullptr; - - #ifdef MEMORYPOOL_DEBUG - TheMemoryPoolFactory->debugMemoryReport(REPORT_POOLINFO | REPORT_POOL_OVERFLOW | REPORT_SIMPLE_LEAKS, 0, 0); - #endif - #if defined(RTS_DEBUG) - TheMemoryPoolFactory->memoryPoolUsageReport("AAAMemStats"); - #endif - - shutdownMemoryManager(); + exitcode = AppMain::run(); - // BGC - shut down COM - // OleUninitialize(); + AppMain::shutdown(); } catch (...) { @@ -942,7 +901,7 @@ GameEngine *CreateGameEngine() engine = NEW Win32GameEngine; //game engine may not have existed when app got focus so make sure it //knows about current focus state. - engine->setIsActive(isWinMainActive); + engine->setIsActive(AppMain::isAppActive()); return engine; diff --git a/GeneralsMD/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DGameClient.h b/GeneralsMD/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DGameClient.h index fa06cf092f7..7e032d37023 100644 --- a/GeneralsMD/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DGameClient.h +++ b/GeneralsMD/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DGameClient.h @@ -46,18 +46,26 @@ #include "W3DDevice/GameClient/W3DGameFont.h" #include "W3DDevice/GameClient/W3DDisplayStringManager.h" #include "VideoDevice/Bink/BinkVideoPlayer.h" +#include "W3DDevice/GameClient/W3DSnow.h" + #ifdef RTS_HAS_FFMPEG #include "VideoDevice/FFmpeg/FFmpegVideoPlayer.h" #endif + +#if RTS_SDL3_ENABLE +#include "SDL3Device/GameClient/SDL3Input.h" +extern SDL_Window* TheSDL3Window; +#else +#include "W3DDevice/GameClient/W3DMouse.h" #include "Win32Device/GameClient/Win32DIKeyboard.h" #include "Win32Device/GameClient/Win32DIMouse.h" #include "Win32Device/GameClient/Win32Mouse.h" -#include "W3DDevice/GameClient/W3DMouse.h" -#include "W3DDevice/GameClient/W3DSnow.h" +extern Win32Mouse *TheWin32Mouse; +#endif -class ThingTemplate; -extern Win32Mouse *TheWin32Mouse; + +class ThingTemplate; /////////////////////////////////////////////////////////////////////////////// // PROTOTYPES ///////////////////////////////////////////////////////////////// @@ -126,11 +134,23 @@ class W3DGameClient : public GameClient }; -inline Keyboard *W3DGameClient::createKeyboard() { return NEW DirectInputKeyboard; } +inline Keyboard *W3DGameClient::createKeyboard() +{ +#if RTS_SDL3_ENABLE + return NEW SDL3Keyboard; +#else + return NEW DirectInputKeyboard; +#endif +} + inline Mouse *W3DGameClient::createMouse() { +#if RTS_SDL3_ENABLE + return NEW SDL3Mouse(TheSDL3Window); +#else //return new DirectInputMouse; Win32Mouse * mouse = NEW W3DMouse; TheWin32Mouse = mouse; ///< global cheat for the WndProc() return mouse; +#endif } diff --git a/GeneralsMD/Code/Main/CMakeLists.txt b/GeneralsMD/Code/Main/CMakeLists.txt index 0aa9b6df91d..29c1673409c 100644 --- a/GeneralsMD/Code/Main/CMakeLists.txt +++ b/GeneralsMD/Code/Main/CMakeLists.txt @@ -12,6 +12,7 @@ target_link_libraries(z_generals PRIVATE comctl32 core_debug core_profile_legacy + corei_main d3d8 d3dx8 dinput8 @@ -55,10 +56,22 @@ target_include_directories(z_generals PRIVATE ) target_sources(z_generals PRIVATE - WinMain.cpp - WinMain.h + ${CMAKE_SOURCE_DIR}/Core/Main/AppMain.cpp + ${CMAKE_SOURCE_DIR}/Core/Main/AppMain.h ) +if(RTS_BUILD_OPTION_SDL3) + target_sources(z_generals PRIVATE + ${CMAKE_SOURCE_DIR}/Core/Main/SDL3Main.cpp + ${CMAKE_SOURCE_DIR}/Core/Main/SDL3Main.h + ) +else() + target_sources(z_generals PRIVATE + WinMain.cpp + WinMain.h + ) +endif() + # RC files optional for MinGW builds # Icon: LoadIcon() handles missing resource gracefully (uses default system icon) # Manifest: DPI awareness metadata (nice to have but not essential) diff --git a/GeneralsMD/Code/Main/WinMain.cpp b/GeneralsMD/Code/Main/WinMain.cpp index 0d37cab5933..7bcb64a6c3d 100644 --- a/GeneralsMD/Code/Main/WinMain.cpp +++ b/GeneralsMD/Code/Main/WinMain.cpp @@ -40,6 +40,7 @@ #include // USER INCLUDES ////////////////////////////////////////////////////////////// +#include "AppMain.h" #include "WinMain.h" #include "Lib/BaseType.h" #include "Common/CommandLine.h" @@ -60,10 +61,11 @@ #include "GameLogic/GameLogic.h" ///< @todo for demo, remove #include "GameClient/Mouse.h" #include "GameClient/IMEManager.h" -#include "Win32Device/GameClient/Win32Mouse.h" -#include "Win32Device/Common/Win32GameEngine.h" #include "Common/version.h" #include "BuildVersion.h" +#include "Win32Device/Common/Win32GameEngine.h" +#include "Win32Device/GameClient/Win32Mouse.h" + #include "GeneratedVersion.h" #include "resource.h" @@ -75,18 +77,21 @@ // GLOBALS //////////////////////////////////////////////////////////////////// HINSTANCE ApplicationHInstance = nullptr; ///< our application instance HWND ApplicationHWnd = nullptr; ///< our application window handle -Win32Mouse *TheWin32Mouse = nullptr; ///< for the WndProc() only +extern Win32Mouse *TheWin32Mouse; ///< defined in AppMain.cpp DWORD TheMessageTime = 0; ///< For getting the time that a message was posted from Windows. -const Char *g_strFile = "data\\Generals.str"; -const Char *g_csfFile = "data\\%s\\Generals.csf"; -const char *gAppPrefix = ""; /// So WB can have a different debug log file name. +extern const Char *g_strFile; +extern const Char *g_csfFile; +extern const char *gAppPrefix; static Bool gInitializing = false; static Bool gDoPaint = true; static Bool isWinMainActive = false; static HBITMAP gLoadScreenBitmap = nullptr; +#if RTS_SDL3_ENABLE +static SDL_Surface* gLoadScreenSurface = nullptr; +#endif //#define DEBUG_WINDOWS_MESSAGES @@ -471,9 +476,7 @@ LRESULT CALLBACK WndProc( HWND hWnd, UINT message, // paths that take care of that. isWinMainActive = (BOOL) wParam; - - if (TheGameEngine) - TheGameEngine->setIsActive(isWinMainActive); + AppMain::setAppActive(isWinMainActive); if (isWinMainActive) { @@ -848,100 +851,40 @@ Int APIENTRY WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, // WWDebug_Install_Message_Handler(WWDebug_Message_Callback); // WWDebug_Install_Assert_Handler(WWAssert_Callback); + if (!AppMain::initBeforeWindow()) + { + return exitcode; + } // Force "splash image" to be loaded from a file, not a resource so same exe can be used in different localizations. -#if defined(RTS_DEBUG) || defined RTS_PROFILE_LEGACY - - // check both localized directory and root dir char filePath[_MAX_PATH]; - const char *fileName = "Install_Final.bmp"; - static const char *localizedPathFormat = "Data/%s/"; - sprintf(filePath,localizedPathFormat, GetRegistryLanguage().str()); - strlcat(filePath, fileName, ARRAY_SIZE(filePath)); - FILE *fileImage = fopen(filePath, "r"); - if (fileImage) { - fclose(fileImage); - gLoadScreenBitmap = (HBITMAP)LoadImage(hInstance, filePath, IMAGE_BITMAP, 0, 0, LR_SHARED|LR_LOADFROMFILE); - } - else { - gLoadScreenBitmap = (HBITMAP)LoadImage(hInstance, fileName, IMAGE_BITMAP, 0, 0, LR_SHARED|LR_LOADFROMFILE); - } -#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); -#endif - - CommandLine::parseCommandLineForStartup(); -#ifdef RTS_ENABLE_CRASHDUMP - // Initialize minidump facilities - requires TheGlobalData so performed after parseCommandLineForStartup - MiniDumper::initMiniDumper(TheGlobalData->getPath_UserData()); -#endif + AppMain::getSplashFilePath(filePath, sizeof(filePath)); + gLoadScreenBitmap = (HBITMAP)LoadImage(hInstance, filePath, IMAGE_BITMAP, 0, 0, LR_SHARED | LR_LOADFROMFILE); - // register windows class and create application window if(!TheGlobalData->m_headless && initializeAppWindows(hInstance, nCmdShow, TheGlobalData->m_windowed) == false) { + AppMain::shutdown(); return exitcode; } // save our application instance for future use ApplicationHInstance = hInstance; - if (gLoadScreenBitmap!=nullptr) { + if (gLoadScreenBitmap != nullptr) { ::DeleteObject(gLoadScreenBitmap); gLoadScreenBitmap = nullptr; } - - // BGC - initialize COM - // OleInitialize(nullptr); - - - - // Set up version info - TheVersion = NEW Version; - TheVersion->setVersion(VERSION_MAJOR, VERSION_MINOR, VERSION_BUILDNUM, VERSION_LOCALBUILDNUM, - AsciiString(VERSION_BUILDUSER), AsciiString(VERSION_BUILDLOC), - AsciiString(__TIME__), AsciiString(__DATE__)); - - // TheSuperHackers @refactor The instance mutex now lives in its own class. - - if (!rts::ClientInstance::initialize()) + if (!AppMain::initAfterWindow()) { - HWND ccwindow = FindWindow(rts::ClientInstance::getFirstInstanceName(), nullptr); - if (ccwindow) - { - SetForegroundWindow(ccwindow); - ShowWindow(ccwindow, SW_RESTORE); - } - - DEBUG_LOG(("Generals is already running...Bail!")); - delete TheVersion; - TheVersion = nullptr; - shutdownMemoryManager(); + AppMain::shutdown(); return exitcode; } - DEBUG_LOG(("Create Generals Mutex okay.")); - - DEBUG_LOG(("CRC message is %d", GameMessage::MSG_LOGIC_CRC)); // run the game main loop - exitcode = GameMain(); + exitcode = AppMain::run(); - delete TheVersion; - TheVersion = nullptr; - - #ifdef MEMORYPOOL_DEBUG - TheMemoryPoolFactory->debugMemoryReport(REPORT_POOLINFO | REPORT_POOL_OVERFLOW | REPORT_SIMPLE_LEAKS, 0, 0); - #endif - #if defined(RTS_DEBUG) - TheMemoryPoolFactory->memoryPoolUsageReport("AAAMemStats"); - #endif - - shutdownMemoryManager(); - - // BGC - shut down COM - // OleUninitialize(); + AppMain::shutdown(); } catch (...) { @@ -969,8 +912,7 @@ GameEngine *CreateGameEngine() engine = NEW Win32GameEngine; //game engine may not have existed when app got focus so make sure it //knows about current focus state. - engine->setIsActive(isWinMainActive); + engine->setIsActive(AppMain::isAppActive()); return engine; - } diff --git a/cmake/config-build.cmake b/cmake/config-build.cmake index 7973f5a1f03..f9e339118b7 100644 --- a/cmake/config-build.cmake +++ b/cmake/config-build.cmake @@ -9,6 +9,15 @@ option(RTS_BUILD_OPTION_DEBUG "Build code with the \"Debug\" configuration." OFF 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) +option(RTS_BUILD_OPTION_SDL3 "Enable SDL3 input/window backend" ON) + +if(IS_VS6_BUILD) + set(RTS_BUILD_OPTION_SDL3 OFF CACHE BOOL "Enable SDL3 input/window backend" FORCE) +endif() + +if(RTS_BUILD_OPTION_SDL3) + target_compile_definitions(core_config INTERFACE RTS_SDL3_ENABLE=1) +endif() if(NOT RTS_BUILD_ZEROHOUR AND NOT RTS_BUILD_GENERALS) set(RTS_BUILD_ZEROHOUR TRUE) @@ -24,6 +33,7 @@ add_feature_info(DebugBuild RTS_BUILD_OPTION_DEBUG "Building as a \"Debug\" buil add_feature_info(AddressSanitizer RTS_BUILD_OPTION_ASAN "Building with address sanitizer") add_feature_info(Vc6FullDebug RTS_BUILD_OPTION_VC6_FULL_DEBUG "Building VC6 with full debug info") add_feature_info(FFmpegSupport RTS_BUILD_OPTION_FFMPEG "Building with FFmpeg support") +add_feature_info(Sdl3Backend RTS_BUILD_OPTION_SDL3 "Building with SDL3 input/window backend") set(RTS_BUILD_OUTPUT_SUFFIX "" CACHE STRING "Suffix appended to output names of installable targets") diff --git a/cmake/sdl3.cmake b/cmake/sdl3.cmake new file mode 100644 index 00000000000..9231c9115e7 --- /dev/null +++ b/cmake/sdl3.cmake @@ -0,0 +1,59 @@ +# 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.10/SDL3-3.4.10.tar.gz + URL_HASH SHA256=12b34280415ec8418c864408b93d008a20a6530687ee613d60bfbd20411f2785 + OVERRIDE_FIND_PACKAGE + ) + + FetchContent_Declare( + SDL3_image + # Pin to commit with the ANI loader RIFF word-alignment fix and legacy parsing relaxation. + # NOTE: The legacy asset parsing relaxation fix (commit 67da91c / 0e2eaa9) is not yet in the + # official SDL_image 3.4.4 release. vcpkg builds will inherit this fix once 3.4.5+ is packaged. + GIT_REPOSITORY https://github.com/libsdl-org/SDL_image.git + GIT_TAG 0e2eaa923ddea285dfa35c4bf0c0092d3799e2ee + ) + + # 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..73657ba677e --- /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": "f9ffbaa46ad8e284b2b74919f7e0ba259564d424", + "packages": [ + "sdl3", + "sdl3-image" + ] + } + ] +} \ No newline at end of file diff --git a/vcpkg-lock.json b/vcpkg-lock.json index 422998f0378..f77c128f168 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.10", + "port-version": 0, + "git-tree": "ca8f0f2fec751398b72e40cb25af47f13bf22d63" + }, + { + "name": "sdl3-image", + "version-string": "3.4.4", + "port-version": 0, + "git-tree": "7d64d0dd26d1a025b99b83429838551fde4a65cf" + }, + { + "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 9ce3c6667c3..d84f17cf0f7 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -1,9 +1,21 @@ { - "$schema": "https://raw.githubusercontent.com/microsoft/vcpkg-tool/main/docs/vcpkg.schema.json", - "builtin-baseline": "b02e341c927f16d991edbd915d8ea43eac52096c", - "dependencies": [ - "zlib", - "ffmpeg", - "stb" - ] - } \ No newline at end of file + "$schema": "https://raw.githubusercontent.com/microsoft/vcpkg-tool/main/docs/vcpkg.schema.json", + "builtin-baseline": "b02e341c927f16d991edbd915d8ea43eac52096c", + "dependencies": [ + "ffmpeg", + "sdl3", + "sdl3-image", + "stb", + "zlib" + ], + "overrides": [ + { + "name": "sdl3", + "version": "3.4.10" + }, + { + "name": "sdl3-image", + "version": "3.4.4" + } + ] +}