Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
e406fca
SDL3 Input: Complete backport with Gamepad support and hardening
githubawn Apr 20, 2026
17f1816
SDL3 Input: Complete backport with Gamepad support and hardening
githubawn Apr 20, 2026
bd9921f
added build guards
githubawn Apr 20, 2026
86f996b
greptile feedback
githubawn Apr 20, 2026
43beb41
greptile feedback
githubawn Apr 20, 2026
96d0b3d
fixed build error
githubawn Apr 20, 2026
47b934c
greptile feedback
githubawn Apr 20, 2026
8cfcef4
added vcpkg
githubawn Apr 20, 2026
ebab585
vcpkg fix
githubawn Apr 20, 2026
2774a78
changed dpad group numbers
githubawn Apr 20, 2026
186eaf7
switch back to static linking
githubawn Apr 20, 2026
6b3bec1
move SDL3_image out SDL3input
githubawn Apr 21, 2026
d775816
properly fail on SDL_INIT failure
githubawn Apr 21, 2026
40d480c
removed custom ICO decoder
githubawn Apr 21, 2026
a959ecb
fixes SDL_WarpMouseInWindow mismatch
githubawn Apr 21, 2026
ea63a09
removed some dead declarations
githubawn Apr 21, 2026
13f1f66
temporary re-added custom decoder
githubawn Apr 21, 2026
94d3f4b
dont early return for headless mode
githubawn Apr 22, 2026
c75d632
applied clang to files new in this PR
githubawn May 14, 2026
4b87f56
updated SDL to latest version
githubawn Jun 1, 2026
2a55d23
renamed SAGE_USE_SDL3 to RTS_SDL3_ENABLE
githubawn Jun 1, 2026
8533760
cleaned up comments
githubawn Jun 1, 2026
25d8a42
build: upgrade SDL_image and clean up SDL3Cursor loadANI
githubawn Jun 9, 2026
57bcdf9
style: remove legacy empty parameter list style (void) -> ()
githubawn Jun 9, 2026
5e4c4d7
build: update vcpkg dependencies and fix SDL3 input scancode translation
githubawn Jun 9, 2026
808f8d0
move cmake to RTS_BUILD_OPTION_SDL3, fix sorting
githubawn Jun 10, 2026
81bf8f9
Move SDL3GameEngine files to Common directory.
githubawn Jun 12, 2026
785d213
style/fix: address PR review feedback on SDL3 input and engine device
githubawn Jul 23, 2026
78f7e15
Refactor entry points: extract AppMain lifecycle and isolate SDL3Main
githubawn Jul 23, 2026
6640a47
fix(build): correct include ordering in AppMain and SDL3Main for VC6 …
githubawn Jul 23, 2026
a19098f
fix(sdl3): address PR review feedback on SDL3 input and app lifecycle
githubawn Jul 23, 2026
3d7a346
feat(sdl3): backport gamepad stick acceleration and camera modulation
githubawn Jul 23, 2026
826c951
fix(sdl3): use logical window points in scaleMouseCoordinates
githubawn Jul 23, 2026
3bd8573
feat(sdl3): gamepad camera scrolling and double-click Guard mode
githubawn Jul 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
5 changes: 5 additions & 0 deletions Core/GameEngine/Include/GameClient/Display.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 ); ///<sets screen resolution/mode
virtual Int getDisplayModeCount() {return 0;} ///<return number of display modes/resolutions supported by video card.
virtual void getDisplayModeDescription(Int modeIndex, Int *xres, Int *yres, Int *bitDepth) {} ///<return description of mode
Expand Down
3 changes: 3 additions & 0 deletions Core/GameEngine/Include/GameClient/LookAtXlat.h
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ class LookAtTranslator : public GameMessageTranslator
Bool hasMouseMovedRecently();
void setCurrentPos( const ICoord2D& pos );
void setScreenEdgeScrollMode(ScreenEdgeScrollMode mode);
void setControllerInputActive(Bool active);

void resetModes(); //Used when disabling input, so when we reenable it we aren't stuck in a mode.

Expand Down Expand Up @@ -82,11 +83,13 @@ class LookAtTranslator : public GameMessageTranslator
ViewLocation m_viewLocation[ MAX_VIEW_LOCS ];
ScrollType m_scrollType;
ScreenEdgeScrollMode m_screenEdgeScrollMode;
Bool m_controllerInputActive;
UnsignedInt m_lastMouseMoveTimeMsec; // real-time in milliseconds when mouse last moved

void setScrolling( ScrollType scrollType );
void stopScrolling();
Bool canScrollAtScreenEdge() const;
Real getControllerScrollScale() const;
};

extern LookAtTranslator *TheLookAtTranslator;
2 changes: 2 additions & 0 deletions Core/GameEngine/Include/GameClient/View.h
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,8 @@ class View : public Snapshot
virtual Real getZoom() { return m_zoom; }
virtual void setZoom(Real z) { m_zoom = z; }
virtual Real getHeightAboveGround() { return m_heightAboveGround; }
Real getMinHeightAboveGround() const { return m_minHeightAboveGround; }
Real getMaxHeightAboveGround() const { return m_maxHeightAboveGround; }
virtual void setHeightAboveGround(Real z);
virtual void zoom( Real height ); ///< Zoom in/out, closer to the ground, limit to min, or farther away from the ground, limit to max
virtual void setZoomToDefault() { m_zoom = 1.0f; } ///< Set zoom to default value
Expand Down
65 changes: 43 additions & 22 deletions Core/GameEngine/Source/GameClient/MessageStream/LookAtXlat.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -76,44 +76,39 @@ static Bool scrollDir[4] = { false, false, false, false };

constexpr const Real SCROLL_MULTIPLIER = 2.0f;
constexpr const Real SCROLL_AMT = 100.0f * SCROLL_MULTIPLIER;
constexpr const Real MIN_CONTROLLER_SCROLL_SCALE = 0.4f;

static const Int edgeScrollSize = 3;

static Mouse::MouseCursor prevCursor = Mouse::ARROW;

//-----------------------------------------------------------------------------
void LookAtTranslator::setScrolling(ScrollType scrollType)
//-----------------------------------------------------------------------------
void LookAtTranslator::setScrolling( ScrollType scrollType )
{
if (!TheInGameUI->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;

Expand All @@ -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),
Expand All @@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand Down
20 changes: 20 additions & 0 deletions Core/GameEngineDevice/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)

Expand Down
85 changes: 85 additions & 0 deletions Core/GameEngineDevice/Include/SDL3Device/Common/SDL3GameEngine.h
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.
*/

#pragma once

#include "Lib/BaseType.h"

#include <SDL3/SDL.h>

#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();
};
61 changes: 61 additions & 0 deletions Core/GameEngineDevice/Include/SDL3Device/GameClient/SDL3Cursor.h
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.
*/

#pragma once

#include "Lib/BaseType.h"

#include <array>
#include <SDL3/SDL.h>

#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];
};
Loading
Loading