Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -130,5 +130,10 @@ if(RTS_BUILD_GENERALS)
add_subdirectory(Generals)
endif()

# SagePatch (optional QoL extras, macOS-only for now)
if(RTS_BUILD_OPTION_SAGE_PATCH)
add_subdirectory(Patches/SagePatch)
endif()

feature_summary(WHAT ENABLED_FEATURES DESCRIPTION "Enabled features:")
feature_summary(WHAT DISABLED_FEATURES DESCRIPTION "Disabled features:")
3 changes: 2 additions & 1 deletion CMakePresets.json
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,8 @@
"SAGE_USE_MOLTENVK": "ON",
"SAGE_UPDATE_CHECK": "ON",
"RTS_CRASHDUMP_ENABLE": "OFF",
"RTS_BUILD_OPTION_FFMPEG": "ON"
"RTS_BUILD_OPTION_FFMPEG": "ON",
"RTS_BUILD_OPTION_SAGE_PATCH": "ON"
},
"environment": {
"PKG_CONFIG_PATH": "/opt/homebrew/lib/pkgconfig"
Expand Down
92 changes: 92 additions & 0 deletions Patches/SagePatch/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# SagePatch — cross-platform QoL extras for GeneralsX.
#
# Layout: shared C++ in src/common/, platform-specific implementations in
# src/{macos,linux,windows}/. The common code calls a single sagepatch::
# namespace; per-platform sources provide screenshot, brightness, and the
# event-hook mechanism (DYLD __interpose on macOS, LD_PRELOAD + dlsym on Linux).
#
# Engine-side INI overrides (camera height, scroll speed, camera pitch) are
# picked up automatically by the engine — see resources/Override.ini.

if(NOT SAGE_USE_SDL3)
message(FATAL_ERROR "SagePatch requires SAGE_USE_SDL3=ON.")
endif()

set(SAGE_PATCH_SOURCES
src/common/Init.cpp
src/common/KeyHandler.cpp
src/common/CursorLock.cpp
src/common/WindowPosition.cpp
)

if(APPLE)
list(APPEND SAGE_PATCH_SOURCES
src/macos/interposers_macos.cpp
src/macos/Screenshot_macos.cpp
src/macos/Brightness_macos.cpp
)
elseif(UNIX AND NOT APPLE)
list(APPEND SAGE_PATCH_SOURCES
src/linux/interposers_linux.cpp
src/linux/Screenshot_linux.cpp
src/linux/Brightness_linux.cpp
)
elseif(WIN32)
message(WARNING "SagePatch on Windows: no preload mechanism; the dylib will build but is a no-op until a proxy DLL pattern is implemented.")
list(APPEND SAGE_PATCH_SOURCES
src/windows/Stubs_windows.cpp
)
else()
message(FATAL_ERROR "SagePatch: unsupported platform.")
endif()

add_library(sage_patch SHARED ${SAGE_PATCH_SOURCES})

target_include_directories(sage_patch PRIVATE
include
${CMAKE_BINARY_DIR}/_deps/sdl3-src/include
)

# SDL3 symbols resolve at load time from the host process — do not link the
# dylib at build time so a wrong RPATH cannot pull in a stale SDL3.
if(APPLE)
target_link_libraries(sage_patch PRIVATE
"-undefined dynamic_lookup"
"-framework CoreGraphics"
"-framework ApplicationServices"
"-framework Foundation"
)
elseif(UNIX)
target_link_libraries(sage_patch PRIVATE
${CMAKE_DL_LIBS}
)
# Linux equivalent of `-undefined dynamic_lookup`. SDL_PollEvent etc. are
# supplied by the host's libSDL3.so at preload time.
target_link_options(sage_patch PRIVATE "LINKER:--unresolved-symbols=ignore-in-shared-libs")
endif()

set_target_properties(sage_patch PROPERTIES
OUTPUT_NAME "sage_patch"
PREFIX "lib"
)
if(APPLE)
set_target_properties(sage_patch PROPERTIES
SUFFIX ".dylib"
INSTALL_NAME_DIR "@rpath"
BUILD_WITH_INSTALL_RPATH TRUE
)
endif()

target_compile_options(sage_patch PRIVATE
-fvisibility=hidden
-fno-exceptions
-Wall
-Wextra
-Wno-unused-parameter
)

target_compile_definitions(sage_patch PRIVATE
SAGE_PATCH_VERSION="0.1.0-alpha"
)

add_feature_info(SagePatch ON "QoL patch (screenshot, cursor lock, brightness, window snaps, camera/scroll INI overrides)")
20 changes: 20 additions & 0 deletions Patches/SagePatch/include/SagePatch/Features.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#pragma once

#include <SDL3/SDL.h>

namespace sagepatch {

enum class WindowPosition {
Center,
TopLeft,
TopRight,
BottomLeft,
BottomRight,
};

void takeScreenshot(SDL_Window* window);
void toggleCursorLock(SDL_Window* window);
void adjustBrightness(int delta);
void moveWindow(SDL_Window* window, WindowPosition where);

}
12 changes: 12 additions & 0 deletions Patches/SagePatch/include/SagePatch/Hooks.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#pragma once

#include <SDL3/SDL.h>

namespace sagepatch {

void init();
void shutdown();

bool handleKeyDown(const SDL_KeyboardEvent& ev);

}
6 changes: 6 additions & 0 deletions Patches/SagePatch/include/SagePatch/Logger.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#pragma once

#include <stdio.h>

#define SAGEPATCH_LOG(fmt, ...) \
fprintf(stderr, "[sagepatch] " fmt "\n", ##__VA_ARGS__)
26 changes: 26 additions & 0 deletions Patches/SagePatch/resources/Override.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
; -----------------------------------------------------------------------------
; SagePatch - Casual QoL overrides for GeneralsX
;
; Conservative camera/scroll bumps so casual players get a slightly wider
; field of view without breaking small maps (orthographic cull at the edge).
;
; Loaded by the engine after the BIG-archived Data/INI/GameData.ini, so values
; here override (not append to) the originals.
;
; Vanilla values (from BIG-archived Data/INI/GameData.ini):
; MaxCameraHeight = 310
; MinCameraHeight = 120
; KeyboardScrollSpeedFactor = 0.5
; CameraPitch = ~37.5
; -----------------------------------------------------------------------------

GameData
; ~1.6x vanilla; further out without seeing past the map border on small maps.
MaxCameraHeight = 500.0
; Slightly closer than vanilla so casual zoom-in feels useful.
MinCameraHeight = 80.0
; Still soft-disabled so the user can push past max without a hard clamp.
EnforceMaxCameraHeight = No
; Keyboard scroll - vanilla 0.5 is sluggish, double it.
KeyboardScrollSpeedFactor = 1.0
End
18 changes: 18 additions & 0 deletions Patches/SagePatch/src/common/CursorLock.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#include "SagePatch/Features.h"
#include "SagePatch/Logger.h"

#include <SDL3/SDL.h>

namespace sagepatch {

static bool g_cursorLocked = false;

void toggleCursorLock(SDL_Window* window) {
if (!window) return;
g_cursorLocked = !g_cursorLocked;
SDL_SetWindowMouseGrab(window, g_cursorLocked);
SDL_SetWindowRelativeMouseMode(window, g_cursorLocked);
SAGEPATCH_LOG("Cursor lock: %s", g_cursorLocked ? "ON" : "OFF");
}

}
36 changes: 36 additions & 0 deletions Patches/SagePatch/src/common/Init.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
#include "SagePatch/Hooks.h"
#include "SagePatch/Logger.h"

namespace sagepatch {

#if defined(__APPLE__)
# define SAGE_LOAD_MECHANISM "DYLD_INSERT_LIBRARIES"
#elif defined(__linux__)
# define SAGE_LOAD_MECHANISM "LD_PRELOAD"
#else
# define SAGE_LOAD_MECHANISM "preload"
#endif

__attribute__((constructor))
static void onLoad() {
SAGEPATCH_LOG("Loaded (version %s) via %s", SAGE_PATCH_VERSION, SAGE_LOAD_MECHANISM);
init();
}

__attribute__((destructor))
static void onUnload() {
shutdown();
SAGEPATCH_LOG("Unloaded");
}

void init() {
SAGEPATCH_LOG("Hot-keys:");
SAGEPATCH_LOG(" F11 screenshot (PNG to ~/Pictures/GeneralsX)");
SAGEPATCH_LOG(" Scroll Lock toggle cursor lock");
SAGEPATCH_LOG(" Ctrl+PageUp/Dn brightness +/-");
SAGEPATCH_LOG(" Ctrl+1..5 window position (center / TL / TR / BL / BR)");
}

void shutdown() {}

}
53 changes: 53 additions & 0 deletions Patches/SagePatch/src/common/KeyHandler.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
#include "SagePatch/Hooks.h"
#include "SagePatch/Features.h"
#include "SagePatch/Logger.h"

namespace sagepatch {

bool handleKeyDown(const SDL_KeyboardEvent& ev) {
if (ev.repeat) return false;

SDL_Window* window = SDL_GetWindowFromID(ev.windowID);
if (!window) return false;

const bool ctrl = (ev.mod & SDL_KMOD_CTRL) != 0;

switch (ev.key) {
case SDLK_F11:
takeScreenshot(window);
return true;

case SDLK_SCROLLLOCK:
toggleCursorLock(window);
return true;

case SDLK_PAGEUP:
if (ctrl) { adjustBrightness(+8); return true; }
break;
case SDLK_PAGEDOWN:
if (ctrl) { adjustBrightness(-8); return true; }
break;

case SDLK_1:
if (ctrl) { moveWindow(window, WindowPosition::Center); return true; }
break;
case SDLK_2:
if (ctrl) { moveWindow(window, WindowPosition::TopLeft); return true; }
break;
case SDLK_3:
if (ctrl) { moveWindow(window, WindowPosition::TopRight); return true; }
break;
case SDLK_4:
if (ctrl) { moveWindow(window, WindowPosition::BottomLeft); return true; }
break;
case SDLK_5:
if (ctrl) { moveWindow(window, WindowPosition::BottomRight); return true; }
break;

default:
break;
}
return false;
}

}
59 changes: 59 additions & 0 deletions Patches/SagePatch/src/common/WindowPosition.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// Window position presets — Ctrl+1..5 to snap the window to common positions
// on the active display. SDL3 does the work; works on every platform.

#include "SagePatch/Features.h"
#include "SagePatch/Logger.h"

#include <SDL3/SDL.h>

namespace sagepatch {

void moveWindow(SDL_Window* window, WindowPosition where) {
if (!window) return;

SDL_DisplayID display = SDL_GetDisplayForWindow(window);
SDL_Rect bounds{};
if (display == 0 || !SDL_GetDisplayUsableBounds(display, &bounds)) {
SAGEPATCH_LOG("WindowPosition: cannot resolve display bounds");
return;
}

int w = 0, h = 0;
SDL_GetWindowSize(window, &w, &h);

int x = bounds.x, y = bounds.y;
const char* name = "?";

switch (where) {
case WindowPosition::Center:
x = bounds.x + (bounds.w - w) / 2;
y = bounds.y + (bounds.h - h) / 2;
name = "center";
break;
case WindowPosition::TopLeft:
x = bounds.x;
y = bounds.y;
name = "top-left";
break;
case WindowPosition::TopRight:
x = bounds.x + bounds.w - w;
y = bounds.y;
name = "top-right";
break;
case WindowPosition::BottomLeft:
x = bounds.x;
y = bounds.y + bounds.h - h;
name = "bottom-left";
break;
case WindowPosition::BottomRight:
x = bounds.x + bounds.w - w;
y = bounds.y + bounds.h - h;
name = "bottom-right";
break;
}

SDL_SetWindowPosition(window, x, y);
SAGEPATCH_LOG("Window position: %s (%d,%d)", name, x, y);
}

}
Loading
Loading