diff --git a/CMakeLists.txt b/CMakeLists.txt index 18b055ec2b4..e87baad867c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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:") diff --git a/CMakePresets.json b/CMakePresets.json index 8f6a2bc3fa6..58fa6b99c0c 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -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" diff --git a/Patches/SagePatch/CMakeLists.txt b/Patches/SagePatch/CMakeLists.txt new file mode 100644 index 00000000000..f127fea6554 --- /dev/null +++ b/Patches/SagePatch/CMakeLists.txt @@ -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)") diff --git a/Patches/SagePatch/include/SagePatch/Features.h b/Patches/SagePatch/include/SagePatch/Features.h new file mode 100644 index 00000000000..c891c9ffcac --- /dev/null +++ b/Patches/SagePatch/include/SagePatch/Features.h @@ -0,0 +1,20 @@ +#pragma once + +#include + +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); + +} diff --git a/Patches/SagePatch/include/SagePatch/Hooks.h b/Patches/SagePatch/include/SagePatch/Hooks.h new file mode 100644 index 00000000000..d4e1fea3ad8 --- /dev/null +++ b/Patches/SagePatch/include/SagePatch/Hooks.h @@ -0,0 +1,12 @@ +#pragma once + +#include + +namespace sagepatch { + +void init(); +void shutdown(); + +bool handleKeyDown(const SDL_KeyboardEvent& ev); + +} diff --git a/Patches/SagePatch/include/SagePatch/Logger.h b/Patches/SagePatch/include/SagePatch/Logger.h new file mode 100644 index 00000000000..b37ead9aa62 --- /dev/null +++ b/Patches/SagePatch/include/SagePatch/Logger.h @@ -0,0 +1,6 @@ +#pragma once + +#include + +#define SAGEPATCH_LOG(fmt, ...) \ + fprintf(stderr, "[sagepatch] " fmt "\n", ##__VA_ARGS__) diff --git a/Patches/SagePatch/resources/Override.ini b/Patches/SagePatch/resources/Override.ini new file mode 100644 index 00000000000..6b1bc9b0517 --- /dev/null +++ b/Patches/SagePatch/resources/Override.ini @@ -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 diff --git a/Patches/SagePatch/src/common/CursorLock.cpp b/Patches/SagePatch/src/common/CursorLock.cpp new file mode 100644 index 00000000000..562927f3630 --- /dev/null +++ b/Patches/SagePatch/src/common/CursorLock.cpp @@ -0,0 +1,18 @@ +#include "SagePatch/Features.h" +#include "SagePatch/Logger.h" + +#include + +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"); +} + +} diff --git a/Patches/SagePatch/src/common/Init.cpp b/Patches/SagePatch/src/common/Init.cpp new file mode 100644 index 00000000000..847740f1def --- /dev/null +++ b/Patches/SagePatch/src/common/Init.cpp @@ -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() {} + +} diff --git a/Patches/SagePatch/src/common/KeyHandler.cpp b/Patches/SagePatch/src/common/KeyHandler.cpp new file mode 100644 index 00000000000..8fe87adf470 --- /dev/null +++ b/Patches/SagePatch/src/common/KeyHandler.cpp @@ -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; +} + +} diff --git a/Patches/SagePatch/src/common/WindowPosition.cpp b/Patches/SagePatch/src/common/WindowPosition.cpp new file mode 100644 index 00000000000..95e915ed0a7 --- /dev/null +++ b/Patches/SagePatch/src/common/WindowPosition.cpp @@ -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 + +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); +} + +} diff --git a/Patches/SagePatch/src/linux/Brightness_linux.cpp b/Patches/SagePatch/src/linux/Brightness_linux.cpp new file mode 100644 index 00000000000..007e177189c --- /dev/null +++ b/Patches/SagePatch/src/linux/Brightness_linux.cpp @@ -0,0 +1,108 @@ +// Linux brightness via XF86VidMode (X11 only). Loaded lazily via dlopen so the +// patch links cleanly on systems without libXxf86vm or running under Wayland. +// Under Wayland we log a one-time warning and no-op. + +#include "SagePatch/Features.h" +#include "SagePatch/Logger.h" + +#include +#include +#include +#include +#include +#include + +namespace sagepatch { + +static int g_brightness = 0; // -128 .. +128 + +static int clampi(int v, int lo, int hi) { + if (v < lo) return lo; + if (v > hi) return hi; + return v; +} + +// Minimal X11 + XF86VidMode forward decls so we do not need their headers. +typedef struct _XDisplay Display; +typedef Display* (*XOpenDisplay_fn)(const char*); +typedef int (*XCloseDisplay_fn)(Display*); +typedef int (*XDefaultScreen_fn)(Display*); +typedef int (*XF86VidModeGetGammaRampSize_fn)(Display*, int, int*); +typedef int (*XF86VidModeSetGammaRamp_fn)(Display*, int, int, unsigned short*, unsigned short*, unsigned short*); + +static bool tryGamma(float gamma) { + void* libX11 = dlopen("libX11.so.6", RTLD_NOW | RTLD_GLOBAL); + if (!libX11) libX11 = dlopen("libX11.so", RTLD_NOW | RTLD_GLOBAL); + void* libXxf86vm = dlopen("libXxf86vm.so.1", RTLD_NOW); + if (!libXxf86vm) libXxf86vm = dlopen("libXxf86vm.so", RTLD_NOW); + + if (!libX11 || !libXxf86vm) { + if (libX11) dlclose(libX11); + if (libXxf86vm) dlclose(libXxf86vm); + return false; + } + + auto XOpenDisplay = (XOpenDisplay_fn)dlsym(libX11, "XOpenDisplay"); + auto XCloseDisplay = (XCloseDisplay_fn)dlsym(libX11, "XCloseDisplay"); + auto XDefaultScreen = (XDefaultScreen_fn)dlsym(libX11, "XDefaultScreen"); + auto XF86VidModeGetGammaRampSize = (XF86VidModeGetGammaRampSize_fn)dlsym(libXxf86vm, "XF86VidModeGetGammaRampSize"); + auto XF86VidModeSetGammaRamp = (XF86VidModeSetGammaRamp_fn)dlsym(libXxf86vm, "XF86VidModeSetGammaRamp"); + + if (!XOpenDisplay || !XCloseDisplay || !XDefaultScreen || + !XF86VidModeGetGammaRampSize || !XF86VidModeSetGammaRamp) { + dlclose(libX11); + dlclose(libXxf86vm); + return false; + } + + Display* dpy = XOpenDisplay(nullptr); + if (!dpy) { + dlclose(libX11); + dlclose(libXxf86vm); + return false; + } + + int screen = XDefaultScreen(dpy); + int rampSize = 0; + if (!XF86VidModeGetGammaRampSize(dpy, screen, &rampSize) || rampSize <= 0) { + XCloseDisplay(dpy); + dlclose(libX11); + dlclose(libXxf86vm); + return false; + } + + std::vector r(rampSize), g(rampSize), b(rampSize); + for (int i = 0; i < rampSize; ++i) { + double pos = static_cast(i) / (rampSize - 1); + double v = 65535.0 * pow(pos, 1.0 / gamma); + if (v < 0.0) v = 0.0; + if (v > 65535.0) v = 65535.0; + unsigned short val = static_cast(v); + r[i] = g[i] = b[i] = val; + } + XF86VidModeSetGammaRamp(dpy, screen, rampSize, r.data(), g.data(), b.data()); + + XCloseDisplay(dpy); + // Intentionally leak the dlopens — we want them to stay loaded for the + // process lifetime to avoid re-resolving on every keypress. + return true; +} + +void adjustBrightness(int delta) { + g_brightness = clampi(g_brightness + delta, -128, +128); + float gamma = 1.0f + static_cast(g_brightness) / 256.0f; + if (gamma < 0.5f) gamma = 0.5f; + if (gamma > 2.0f) gamma = 2.0f; + + static bool warned = false; + if (!tryGamma(gamma)) { + if (!warned) { + SAGEPATCH_LOG("Brightness: XF86VidMode unavailable (Wayland or missing libXxf86vm) — feature disabled."); + warned = true; + } + return; + } + SAGEPATCH_LOG("Brightness: %+d (gamma %.3f)", g_brightness, gamma); +} + +} diff --git a/Patches/SagePatch/src/linux/Screenshot_linux.cpp b/Patches/SagePatch/src/linux/Screenshot_linux.cpp new file mode 100644 index 00000000000..11ec947623a --- /dev/null +++ b/Patches/SagePatch/src/linux/Screenshot_linux.cpp @@ -0,0 +1,95 @@ +// Linux screenshot via ImageMagick `import` — captures by X11 window id. +// Wayland fallback: `grim -g ` if available. We do not depend on either +// at link time; if neither is installed, we log a hint and skip. + +#include "SagePatch/Features.h" +#include "SagePatch/Logger.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace sagepatch { + +static void ensureScreenshotDir(char* outPath, size_t outPathLen) { + const char* home = getenv("HOME"); + if (!home || !*home) { + const struct passwd* pw = getpwuid(getuid()); + home = pw ? pw->pw_dir : "."; + } + snprintf(outPath, outPathLen, "%s/Pictures/GeneralsX", home); + mkdir(outPath, 0755); +} + +static void timestampFilename(char* out, size_t outLen, const char* dir) { + auto now = std::chrono::system_clock::now(); + std::time_t t = std::chrono::system_clock::to_time_t(now); + std::tm tm{}; + localtime_r(&t, &tm); + auto ms = std::chrono::duration_cast( + now.time_since_epoch()) % 1000; + snprintf(out, outLen, "%s/generalsx_%04d-%02d-%02d_%02d-%02d-%02d-%03d.png", + dir, + tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, + tm.tm_hour, tm.tm_min, tm.tm_sec, + static_cast(ms.count())); +} + +static bool tryImport(unsigned long winId, const char* path) { + char cmd[4096]; + snprintf(cmd, sizeof(cmd), + "command -v import >/dev/null 2>&1 && import -window 0x%lx \"%s\" 2>/dev/null", + winId, path); + int rc = system(cmd); + if (rc != 0) return false; + struct stat st; + return stat(path, &st) == 0 && st.st_size > 0; +} + +static bool tryGnomeScreenshot(const char* path) { + char cmd[4096]; + snprintf(cmd, sizeof(cmd), + "command -v gnome-screenshot >/dev/null 2>&1 && gnome-screenshot -w -f \"%s\" 2>/dev/null", + path); + int rc = system(cmd); + if (rc != 0) return false; + struct stat st; + return stat(path, &st) == 0 && st.st_size > 0; +} + +void takeScreenshot(SDL_Window* window) { + if (!window) { + SAGEPATCH_LOG("Screenshot: no window"); + return; + } + + SDL_PropertiesID props = SDL_GetWindowProperties(window); + auto winIdLong = static_cast( + SDL_GetNumberProperty(props, SDL_PROP_WINDOW_X11_WINDOW_NUMBER, 0)); + + char dir[1024], path[2048]; + ensureScreenshotDir(dir, sizeof(dir)); + timestampFilename(path, sizeof(path), dir); + + bool ok = false; + if (winIdLong != 0) { + ok = tryImport(winIdLong, path); + } + if (!ok) { + ok = tryGnomeScreenshot(path); + } + + if (ok) { + SAGEPATCH_LOG("Screenshot saved: %s", path); + } else { + SAGEPATCH_LOG("Screenshot: install ImageMagick (`import`) or gnome-screenshot to enable."); + SAGEPATCH_LOG("Screenshot: target was %s", path); + } +} + +} diff --git a/Patches/SagePatch/src/linux/interposers_linux.cpp b/Patches/SagePatch/src/linux/interposers_linux.cpp new file mode 100644 index 00000000000..73c1e59d001 --- /dev/null +++ b/Patches/SagePatch/src/linux/interposers_linux.cpp @@ -0,0 +1,30 @@ +// Linux: LD_PRELOAD-style symbol override. Our SDL_PollEvent wins resolution +// (loaded first via LD_PRELOAD), so all callers — including the host +// libSDL3.so when re-entered — see our hook. We get the real one via +// dlsym(RTLD_NEXT, ...) on first call. + +#include +#include +#include "SagePatch/Hooks.h" +#include "SagePatch/Logger.h" + +extern "C" SDL_bool SDL_PollEvent(SDL_Event* event) { + using PollFn = SDL_bool (*)(SDL_Event*); + static PollFn real = nullptr; + if (!real) { + real = reinterpret_cast(dlsym(RTLD_NEXT, "SDL_PollEvent")); + if (!real) { + SAGEPATCH_LOG("FATAL: dlsym(RTLD_NEXT, \"SDL_PollEvent\") failed: %s", dlerror()); + return SDL_FALSE; + } + } + while (real(event)) { + if (event->type == SDL_EVENT_KEY_DOWN) { + if (sagepatch::handleKeyDown(event->key)) { + continue; + } + } + return SDL_TRUE; + } + return SDL_FALSE; +} diff --git a/Patches/SagePatch/src/macos/Brightness_macos.cpp b/Patches/SagePatch/src/macos/Brightness_macos.cpp new file mode 100644 index 00000000000..29c0ab632fa --- /dev/null +++ b/Patches/SagePatch/src/macos/Brightness_macos.cpp @@ -0,0 +1,51 @@ +// Brightness adjustment via macOS CGSetDisplayTransferByFormula. SDL3 deprecated +// the gamma APIs in 3.0; CoreGraphics still works for the active display. +// +// Range: 0.5 (very dark) .. 2.0 (very bright). Internal counter goes -128..+128 +// for parity with GenTool's brightness slider semantics, then maps to a gamma +// curve via gamma = 1.0 + (value / 256.0). + +#include "SagePatch/Features.h" +#include "SagePatch/Logger.h" + +#include + +namespace sagepatch { + +static int g_brightness = 0; // -128 .. +128 + +static int clampi(int v, int lo, int hi) { + if (v < lo) return lo; + if (v > hi) return hi; + return v; +} + +static void applyGamma() { + float gamma = 1.0f + static_cast(g_brightness) / 256.0f; + if (gamma < 0.5f) gamma = 0.5f; + if (gamma > 2.0f) gamma = 2.0f; + + CGDirectDisplayID displays[8]; + uint32_t count = 0; + if (CGGetActiveDisplayList(8, displays, &count) != kCGErrorSuccess) { + SAGEPATCH_LOG("Brightness: cannot enumerate displays"); + return; + } + + for (uint32_t i = 0; i < count; ++i) { + CGSetDisplayTransferByFormula( + displays[i], + 0.0f, 1.0f, 1.0f / gamma, + 0.0f, 1.0f, 1.0f / gamma, + 0.0f, 1.0f, 1.0f / gamma); + } +} + +void adjustBrightness(int delta) { + g_brightness = clampi(g_brightness + delta, -128, +128); + applyGamma(); + SAGEPATCH_LOG("Brightness: %+d (gamma %.3f)", g_brightness, + 1.0 + g_brightness / 256.0); +} + +} diff --git a/Patches/SagePatch/src/macos/Screenshot_macos.cpp b/Patches/SagePatch/src/macos/Screenshot_macos.cpp new file mode 100644 index 00000000000..e892c7e9a20 --- /dev/null +++ b/Patches/SagePatch/src/macos/Screenshot_macos.cpp @@ -0,0 +1,97 @@ +// F11 screenshot. We resolve the SDL3 window's Cocoa window number, then shell +// out to `/usr/sbin/screencapture -l -x ` — that binary has been on +// every macOS version since 10.2 and unlike CGWindowListCreateImageFromArray +// is not deprecated on macOS 15+. + +#include "SagePatch/Features.h" +#include "SagePatch/Logger.h" + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace sagepatch { + +static void ensureScreenshotDir(char* outPath, size_t outPathLen) { + const char* home = getenv("HOME"); + if (!home || !*home) { + const struct passwd* pw = getpwuid(getuid()); + home = pw ? pw->pw_dir : "."; + } + snprintf(outPath, outPathLen, "%s/Pictures/GeneralsX", home); + mkdir(outPath, 0755); +} + +static void timestampFilename(char* out, size_t outLen, const char* dir) { + auto now = std::chrono::system_clock::now(); + std::time_t t = std::chrono::system_clock::to_time_t(now); + std::tm tm{}; + localtime_r(&t, &tm); + auto ms = std::chrono::duration_cast( + now.time_since_epoch()) % 1000; + snprintf(out, outLen, "%s/generalsx_%04d-%02d-%02d_%02d-%02d-%02d-%03d.png", + dir, + tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, + tm.tm_hour, tm.tm_min, tm.tm_sec, + static_cast(ms.count())); +} + +static unsigned long cocoaWindowNumber(void* nsWindow) { + using SendLong = unsigned long (*)(void*, SEL); + SEL sel = sel_registerName("windowNumber"); + SendLong send = reinterpret_cast(objc_msgSend); + return send(nsWindow, sel); +} + +void takeScreenshot(SDL_Window* window) { + if (!window) { + SAGEPATCH_LOG("Screenshot: no window"); + return; + } + + SDL_PropertiesID props = SDL_GetWindowProperties(window); + void* nsWindow = SDL_GetPointerProperty(props, SDL_PROP_WINDOW_COCOA_WINDOW_POINTER, nullptr); + if (!nsWindow) { + SAGEPATCH_LOG("Screenshot: SDL3 window has no Cocoa NSWindow handle"); + return; + } + + unsigned long winId = cocoaWindowNumber(nsWindow); + if (winId == 0) { + SAGEPATCH_LOG("Screenshot: window number == 0"); + return; + } + + char dir[1024], path[2048], cmd[4096]; + ensureScreenshotDir(dir, sizeof(dir)); + timestampFilename(path, sizeof(path), dir); + + // -l : capture only the window with that ID + // -x : no shutter sound + // -o : do not capture the window's shadow (cleaner crop) + // -t png : png output + snprintf(cmd, sizeof(cmd), + "/usr/sbin/screencapture -l%lu -x -o -t png \"%s\" 2>/dev/null", + winId, path); + + int rc = system(cmd); + if (rc == 0) { + struct stat st; + if (stat(path, &st) == 0 && st.st_size > 0) { + SAGEPATCH_LOG("Screenshot saved: %s (%lld bytes)", path, (long long)st.st_size); + return; + } + } + SAGEPATCH_LOG("Screenshot: screencapture failed (rc=%d)", rc); +} + +} diff --git a/Patches/SagePatch/src/macos/interposers_macos.cpp b/Patches/SagePatch/src/macos/interposers_macos.cpp new file mode 100644 index 00000000000..d2493e71adb --- /dev/null +++ b/Patches/SagePatch/src/macos/interposers_macos.cpp @@ -0,0 +1,41 @@ +// macOS DYLD interpose table — replaces SDL3 functions at load time without +// modifying the host binary or the real libSDL3.0.dylib. +// +// Reference: dyld(1) man page, section "Interposing" + . +// +// We hook SDL_PollEvent so we see every input event the game processes. When +// our hot-keys (F11, Scroll Lock, Page Up/Down) appear, we eat the event so the +// game does not also receive it. + +#include +#include "SagePatch/Hooks.h" +#include "SagePatch/Logger.h" + +namespace sagepatch { + +extern bool handleKeyDown(const SDL_KeyboardEvent& ev); + +static bool sage_SDL_PollEvent(SDL_Event* event) { + while (SDL_PollEvent(event)) { + if (event->type == SDL_EVENT_KEY_DOWN) { + if (handleKeyDown(event->key)) { + continue; + } + } + return true; + } + return false; +} + +} + +extern "C" { + +__attribute__((used)) static struct interpose_t { + const void* replacement; + const void* original; +} _sage_interposers[] __attribute__((section("__DATA,__interpose"))) = { + { (const void*)&sagepatch::sage_SDL_PollEvent, (const void*)&SDL_PollEvent }, +}; + +} diff --git a/Patches/SagePatch/src/windows/Stubs_windows.cpp b/Patches/SagePatch/src/windows/Stubs_windows.cpp new file mode 100644 index 00000000000..0a7b19123ab --- /dev/null +++ b/Patches/SagePatch/src/windows/Stubs_windows.cpp @@ -0,0 +1,32 @@ +// Windows backend stubs. +// +// Windows has no LD_PRELOAD or __DATA,__interpose equivalent. To hook +// SDL_PollEvent on Windows we would need either: +// - a proxy DLL pattern (rebuild the host's expected DLL load order so the +// game opens our DLL first, like the original GenTool's d3d8.dll wrap), or +// - in-process inline hooking via Detours / MinHook / Polyhook. +// +// Both options are larger than the macOS/Linux implementations and are +// deferred to a follow-up. On Windows users still have the original GenTool +// available, so SagePatch is opt-in QoL only on the platforms that lack one. +// +// The build system therefore disables SAGE_PATCH on Windows by default. If +// someone forces it ON, the linker will succeed (these stubs satisfy the +// symbol references) but the dylib does nothing at runtime. + +#include "SagePatch/Features.h" +#include "SagePatch/Logger.h" + +#include + +namespace sagepatch { + +void takeScreenshot(SDL_Window*) { + SAGEPATCH_LOG("Screenshot: Windows backend not implemented yet."); +} + +void adjustBrightness(int /*delta*/) { + SAGEPATCH_LOG("Brightness: Windows backend not implemented yet."); +} + +} diff --git a/cmake/config-build.cmake b/cmake/config-build.cmake index ef3c4a131a9..c981481ead2 100644 --- a/cmake/config-build.cmake +++ b/cmake/config-build.cmake @@ -23,6 +23,11 @@ endif() # macOS port option (Phase 5) option(SAGE_USE_MOLTENVK "Use MoltenVK for Vulkan on macOS (Phase 5 macOS port)" OFF) +# SagePatch — optional QoL features for casual play (screenshot, cursor lock, +# brightness, camera/scroll INI overrides). Compiles to a separate dylib that +# is loaded via DYLD_INSERT_LIBRARIES at runtime. macOS-only for now. +option(RTS_BUILD_OPTION_SAGE_PATCH "Build SagePatch QoL extras (macOS, requires SDL3)" OFF) + if(NOT RTS_BUILD_ZEROHOUR AND NOT RTS_BUILD_GENERALS) set(RTS_BUILD_ZEROHOUR TRUE) message("You must select one project to build, building Zero Hour by default.") @@ -40,6 +45,7 @@ add_feature_info(FFmpegSupport RTS_BUILD_OPTION_FFMPEG "Building with FFmpeg sup add_feature_info(SDL3Windowing SAGE_USE_SDL3 "Using SDL3 for windowing (Linux)") add_feature_info(OpenALAudio SAGE_USE_OPENAL "Using OpenAL for audio (Linux)") add_feature_info(UpdateCheck SAGE_UPDATE_CHECK "In-game update check via GitHub Releases API") +add_feature_info(SagePatch RTS_BUILD_OPTION_SAGE_PATCH "Build SagePatch QoL extras (macOS)") set(RTS_BUILD_OUTPUT_SUFFIX "" CACHE STRING "Suffix appended to output names of installable targets") diff --git a/docs/PATCHES/SAGEPATCH.md b/docs/PATCHES/SAGEPATCH.md new file mode 100644 index 00000000000..27d50757ecf --- /dev/null +++ b/docs/PATCHES/SAGEPATCH.md @@ -0,0 +1,206 @@ +# SagePatch — Casual QoL Extras for GeneralsX + +SagePatch is an optional, drop-in patch that adds quality-of-life features to +GeneralsX without modifying the game source. Inspired by the casual subset of +GenTool, it ships as a separate shared library that is loaded via the platform's +preload mechanism plus a small `GameData` INI override that the engine picks up +at startup. + +## Platform support + +| Platform | Mechanism | Status | +|---|---|---| +| **macOS** | `DYLD_INSERT_LIBRARIES` + `__DATA,__interpose` | ✅ Phase 1 | +| **Linux** | `LD_PRELOAD` + `dlsym(RTLD_NEXT, …)` | ✅ Phase 1 (X11 only for brightness) | +| **Windows** | proxy DLL pattern (deferred) | ⚠️ stubs build but no-op at runtime | + +Windows is deferred because Win32 has no native preload mechanism — it would +need either a proxy `d3d8.dll` (the original GenTool's pattern) or in-process +inline hooking via Detours/MinHook. Both are larger than the macOS/Linux +implementations and were skipped to ship the rest. Windows users still have +the original GenTool available. + +## Features + +| Feature | Trigger | Notes | +|---|---|---| +| Screenshot | `F11` | PNG saved to `~/Pictures/GeneralsX/`. macOS: `screencapture`. Linux: ImageMagick `import` (X11) or `gnome-screenshot`. | +| Cursor lock toggle | `Scroll Lock` | Confines mouse to the game window. SDL3 — works on every platform. | +| Brightness up | `Ctrl + Page Up` | +8 step on the gamma curve, range −128…+128. | +| Brightness down | `Ctrl + Page Down` | −8 step. macOS: CoreGraphics. Linux: XF86VidMode (X11 only — no-op under Wayland). | +| Window snap: center | `Ctrl + 1` | SDL3 `SDL_SetWindowPosition` to display center. | +| Window snap: top-left | `Ctrl + 2` | | +| Window snap: top-right | `Ctrl + 3` | | +| Window snap: bottom-left | `Ctrl + 4` | | +| Window snap: bottom-right | `Ctrl + 5` | | +| Camera zoom range | (passive) | `MaxCameraHeight=800`, `MinCameraHeight=60`, `EnforceMaxCameraHeight=No`. | +| Camera pitch | (passive) | `CameraPitch=50` (vanilla ~63). | +| Keyboard scroll speed | (passive) | `KeyboardScrollSpeedFactor=1.0` (vanilla 0.5). | + +Hot-key collisions: SagePatch eats the events it handles, so they do not +also reach the game. + +### About FPS counters + +The engine already ships a native FPS overlay at the top-left +(`W3DDisplay::drawFPSStats()`), gated by `#ifdef RTS_DEBUG` plus the runtime +`-benchmark ` CLI flag. SagePatch does not duplicate it. An earlier +revision of this patch defaulted `DXVK_HUD=fps` in the run wrapper as a +release-build alternative, but on macOS 26 the current MoltenVK SPIRV-Cross +back-end cannot translate DXVK's HUD pipeline shader (uses `DrawIndex`, no +MSL equivalent), causing the swap-chain blit pipeline to fail and the game +to hang at the EA logo. The default is now `DXVK_HUD=0` again. Users who +want a frame counter on macOS should either build with `RTS_DEBUG=ON` and +launch with `-benchmark 9999`, or set `DXVK_HUD=fps` themselves once +upstream MoltenVK ships the DrawIndex emulation patch. + +## How to enable + +### macOS + +```bash +cmake --preset macos-vulkan -DRTS_BUILD_OPTION_SAGE_PATCH=ON +cmake --build build/macos-vulkan --target z_generals -j$(sysctl -n hw.logicalcpu) +./scripts/build/macos/deploy-macos-zh.sh +~/GeneralsX/GeneralsZH/run.sh -win +``` + +(The `macos-vulkan` preset already sets the flag `ON`.) + +### Linux + +```bash +cmake --preset linux64-deploy -DRTS_BUILD_OPTION_SAGE_PATCH=ON +cmake --build build/linux64-deploy --target z_generals -j$(nproc) +./scripts/build/linux/deploy-linux-zh.sh +~/GeneralsX/GeneralsZH/run.sh -win +``` + +(The `linux64-deploy` preset does **not** set the flag automatically — opt in +explicitly.) + +### Disabling at runtime (no rebuild) + +```bash +SAGE_PATCH_DISABLED=1 ~/GeneralsX/GeneralsZH/run.sh -win +``` + +This skips the preload step. The INI override remains active — delete +`Data/INI/Default/GameData/SagePatch.ini` to revert camera/scroll values. + +## Architecture + +``` +Game process (GeneralsXZH) + │ + ├── DYLD_INSERT_LIBRARIES (macOS) / LD_PRELOAD (Linux) → libsage_patch.{dylib,so} + │ │ + │ └── SDL_PollEvent gets replaced (interpose table on macOS, + │ symbol override + dlsym RTLD_NEXT on Linux) + │ │ + │ └── F11, Scroll Lock, Ctrl+PgUp/Dn, Ctrl+1..5 → SagePatch handlers + │ │ + │ └── Per-platform: screencapture / ImageMagick, + │ CoreGraphics gamma / XF86VidMode, SDL_SetWindowPosition + │ + └── Engine loads Data/INI/Default/GameData/SagePatch.ini → camera/scroll overrides +``` + +No D3D8 proxy, no Vulkan layer, no engine source modifications. + +## Why this approach + +The original GenTool had to be a `d3d8.dll` proxy because Windows games of that +era exposed no other plugin surface. On macOS and Linux we have: + +- **Symbol override at load time** — `__DATA,__interpose` (macOS) and + `LD_PRELOAD` (Linux) replace `SDL_PollEvent` with our version without + touching the host SDL3 library. +- **OS-native window capture** — both platforms expose tools that snapshot + a single window without graphics-pipeline hooks. +- **Engine INI overrides** — the GeneralsX INI loader merges files in + `Data/INI/Default//`, so parameter tweaks need zero code. + +This keeps SagePatch ~600 lines instead of the ~3000-line full COM proxy +that a Windows-style implementation would require. + +## Already in the engine — no patch needed + +These GenTool-era options are already first-class engine command-line flags +in GeneralsX/TheSuperHackers (see `Common/CommandLine.cpp`). Use them +directly with `run.sh`: + +| Flag | Effect | +|---|---| +| `-nologo` | Skip the EA / Westwood intro | +| `-noShellAnim` | Skip the animated main-menu camera | +| `-noshellmap` | Skip the animated background map | +| `-quickstart` | Combined fast-boot | +| `-xres N -yres N` | Any resolution (the engine no longer locks the list) | +| `-forcefullviewport` | Full viewport on UI mods like Control Bar Pro | +| `-win` / `-fullscreen` | Window mode | +| `-noaudio` / `-nomusic` / `-novideo` | Disable subsystems | + +Game-engine bug fixes from the GenTool changelog (Scud bug, Tunnel bug, +Building bug, multiplayer movement crash, etc.) are handled **upstream by +TheSuperHackers/GeneralsGameCode**, which is the parent of GeneralsX and +maintained by the same author who wrote GenTool (xezon). The codebase +already carries 170+ `@bugfix` annotations including the Tunnel System +fixes by xezon. SagePatch does **not** duplicate those — duplicating fixes +would only create merge conflicts when GeneralsX next syncs with upstream. + +## Limits / what's *not* in scope + +By design, SagePatch sticks to **casual** QoL only: + +- No replay tools (frame stepping, fog-of-war replay, controls bar) +- No competitive features (Money Display, Player Table, Random Balance) +- No anti-cheat (MDS, Game File Validator, version validation) +- No external server integrations (CNC Online, GameRanger, Upload Mode, ticker, + ranked maps, ladder, GenTool updater) +- No engine bug fixes — those live in core source, gated by the existing + `@bugfix` annotation system, contributed upstream where possible +- No in-game text overlay (clock, match timer, in-game settings menu) — these + need a graphics-pipeline hook (D3D8 proxy or Vulkan layer); see the FPS + counter via `DXVK_HUD` for an existing alternative + +## File layout + +``` +Patches/SagePatch/ + CMakeLists.txt + include/SagePatch/{Hooks.h, Features.h, Logger.h} + src/ + common/ # SDL3 — works on every platform + Init.cpp + KeyHandler.cpp + CursorLock.cpp + WindowPosition.cpp + macos/ # macOS-only: __DATA,__interpose, screencapture, CoreGraphics + interposers_macos.cpp + Screenshot_macos.cpp + Brightness_macos.cpp + linux/ # Linux-only: LD_PRELOAD, ImageMagick, XF86VidMode + interposers_linux.cpp + Screenshot_linux.cpp + Brightness_linux.cpp + windows/ # Windows: stubs only (Phase 2) + Stubs_windows.cpp + resources/Override.ini # engine-side INI override (deployed to Data/INI/GameData/SagePatch.ini) +docs/PATCHES/SAGEPATCH.md # this file +``` + +### A note on the INI override path + +The engine loads `GameData` INIs in two passes: +`Data/INI/Default/GameData/...` first, then `Data/INI/GameData/...`. Each +`GameData` block parsed overwrites the previous values in `TheWritableGlobalData` +(`INI_LOAD_OVERWRITE` semantics). The vanilla camera defaults +(`MaxCameraHeight = 310`, etc.) live in the BIG-archived `Data/INI/GameData.ini` +which is parsed in the **second** pass, so an override placed in +`Data/INI/Default/GameData/` is silently undone by the second pass. The deploy +scripts therefore drop our `SagePatch.ini` into `Data/INI/GameData/` so it is +loaded last and the values stick. + +```text +``` diff --git a/flatpak/com.fbraz3.GeneralsX.yml b/flatpak/com.fbraz3.GeneralsX.yml index 033dc64b084..497f3e99159 100644 --- a/flatpak/com.fbraz3.GeneralsX.yml +++ b/flatpak/com.fbraz3.GeneralsX.yml @@ -57,6 +57,7 @@ modules: -DSAGE_USE_OPENAL=ON \ -DSAGE_USE_GLM=ON \ -DSAGE_UPDATE_CHECK=ON \ + -DRTS_BUILD_OPTION_SAGE_PATCH=ON \ -DFETCHCONTENT_FULLY_DISCONNECTED=ON \ -DFETCHCONTENT_UPDATES_DISCONNECTED=ON \ -DFETCHCONTENT_SOURCE_DIR_DXVK=/run/build/generalsx/dxvk-src \ @@ -66,6 +67,7 @@ modules: -DFETCHCONTENT_SOURCE_DIR_GAMESPY=/run/build/generalsx/gamespy-src \ -DFETCHCONTENT_SOURCE_DIR_LZHL=/run/build/generalsx/lzhl-src/CompLibHeader - cmake --build build-flatpak --target g_generals -j "${FLATPAK_BUILDER_N_JOBS:-4}" + - cmake --build build-flatpak --target sage_patch -j "${FLATPAK_BUILDER_N_JOBS:-4}" || true - install -Dm755 build-flatpak/Generals/GeneralsX /app/bin/GeneralsX - install -Dm755 flatpak/run-flatpak-generals.sh /app/bin/generalsx-wrapper.sh - install -Dm644 flatpak/com.fbraz3.GeneralsX.desktop /app/share/applications/com.fbraz3.GeneralsX.desktop @@ -79,6 +81,9 @@ modules: - cp -a build-flatpak/_deps/sdl3_image-build/libSDL3_image.so* /app/lib/ - cp -a build-flatpak/_deps/openal_soft-build/libopenal.so* /app/lib/ - find build-flatpak -name "libgamespy.so*" -type f -exec cp -a {} /app/lib/ \; + # SagePatch (optional QoL, gated by RTS_BUILD_OPTION_SAGE_PATCH at build). + - if [ -f build-flatpak/Patches/SagePatch/libsage_patch.so ]; then cp -a build-flatpak/Patches/SagePatch/libsage_patch.so /app/lib/; fi + - if [ -f Patches/SagePatch/resources/Override.ini ]; then install -Dm644 Patches/SagePatch/resources/Override.ini /app/share/generalsx/SagePatch.ini; fi - | for f in /app/lib/lib*.so.*; do b="$(basename "$f")" @@ -101,7 +106,21 @@ modules: export DXVK_WSI_DRIVER="SDL3" export DXVK_LOG_LEVEL="${DXVK_LOG_LEVEL:-info}" - export DXVK_HUD="${DXVK_HUD:-0}" + + # SagePatch (optional QoL: F11 screenshot, Scroll Lock cursor lock, + # Ctrl+PgUp/Dn brightness, Ctrl+1..5 window snap). Loaded via LD_PRELOAD + # only if the .so is present in /app/lib. Default DXVK_HUD to "fps" when + # SagePatch is active so casual users see a frame counter. + if [[ -f /app/lib/libsage_patch.so && "${SAGE_PATCH_DISABLED:-0}" != "1" ]]; then + if [[ -n "${LD_PRELOAD:-}" ]]; then + export LD_PRELOAD="/app/lib/libsage_patch.so:${LD_PRELOAD}" + else + export LD_PRELOAD="/app/lib/libsage_patch.so" + fi + export DXVK_HUD="${DXVK_HUD:-fps}" + else + export DXVK_HUD="${DXVK_HUD:-0}" + fi # Keep Vulkan ICD selection to the runtime/driver defaults unless user explicitly overrides. @@ -129,6 +148,16 @@ modules: fi cd "${CNC_GENERALS_INSTALLPATH}" + + # SagePatch INI override: engine reads INIs from cwd. Seed the bundled + # override into the asset tree on first launch. + SAGE_INI_SRC="/app/share/generalsx/SagePatch.ini" + SAGE_INI_DST="${CNC_GENERALS_INSTALLPATH}Data/INI/Default/GameData/SagePatch.ini" + if [[ -f "${SAGE_INI_SRC}" && ! -f "${SAGE_INI_DST}" && "${SAGE_PATCH_DISABLED:-0}" != "1" ]]; then + mkdir -p "$(dirname "${SAGE_INI_DST}")" + cp "${SAGE_INI_SRC}" "${SAGE_INI_DST}" 2>/dev/null || true + fi + exec /app/bin/GeneralsX "$@" EOF - chmod +x /app/bin/run.sh diff --git a/flatpak/com.fbraz3.GeneralsXZH.yml b/flatpak/com.fbraz3.GeneralsXZH.yml index b0fd10b95a0..448ae4cb4e5 100644 --- a/flatpak/com.fbraz3.GeneralsXZH.yml +++ b/flatpak/com.fbraz3.GeneralsXZH.yml @@ -57,6 +57,7 @@ modules: -DSAGE_USE_OPENAL=ON \ -DSAGE_USE_GLM=ON \ -DSAGE_UPDATE_CHECK=ON \ + -DRTS_BUILD_OPTION_SAGE_PATCH=ON \ -DFETCHCONTENT_FULLY_DISCONNECTED=ON \ -DFETCHCONTENT_UPDATES_DISCONNECTED=ON \ -DFETCHCONTENT_SOURCE_DIR_DXVK=/run/build/generalsxzh/dxvk-src \ @@ -66,6 +67,7 @@ modules: -DFETCHCONTENT_SOURCE_DIR_GAMESPY=/run/build/generalsxzh/gamespy-src \ -DFETCHCONTENT_SOURCE_DIR_LZHL=/run/build/generalsxzh/lzhl-src/CompLibHeader - cmake --build build-flatpak --target z_generals -j "${FLATPAK_BUILDER_N_JOBS:-4}" + - cmake --build build-flatpak --target sage_patch -j "${FLATPAK_BUILDER_N_JOBS:-4}" || true - install -Dm755 build-flatpak/GeneralsMD/GeneralsXZH /app/bin/GeneralsXZH - install -Dm755 flatpak/run-flatpak.sh /app/bin/generalsxzh-wrapper.sh - install -Dm644 flatpak/com.fbraz3.GeneralsXZH.desktop /app/share/applications/com.fbraz3.GeneralsXZH.desktop @@ -79,6 +81,10 @@ modules: - cp -a build-flatpak/_deps/sdl3_image-build/libSDL3_image.so* /app/lib/ - cp -a build-flatpak/_deps/openal_soft-build/libopenal.so* /app/lib/ - find build-flatpak -name "libgamespy.so*" -type f -exec cp -a {} /app/lib/ \; + # SagePatch (optional QoL, gated by RTS_BUILD_OPTION_SAGE_PATCH at build). + # Only present when the option is ON; -f guard keeps the manifest robust. + - if [ -f build-flatpak/Patches/SagePatch/libsage_patch.so ]; then cp -a build-flatpak/Patches/SagePatch/libsage_patch.so /app/lib/; fi + - if [ -f Patches/SagePatch/resources/Override.ini ]; then install -Dm644 Patches/SagePatch/resources/Override.ini /app/share/generalsxzh/SagePatch.ini; fi - | for f in /app/lib/lib*.so.*; do b="$(basename "$f")" @@ -101,7 +107,21 @@ modules: export DXVK_WSI_DRIVER="SDL3" export DXVK_LOG_LEVEL="${DXVK_LOG_LEVEL:-info}" - export DXVK_HUD="${DXVK_HUD:-0}" + + # SagePatch (optional QoL: F11 screenshot, Scroll Lock cursor lock, + # Ctrl+PgUp/Dn brightness, Ctrl+1..5 window snap). Loaded via LD_PRELOAD + # only if the .so is present in /app/lib. Default DXVK_HUD to "fps" when + # SagePatch is active so casual users see a frame counter. + if [[ -f /app/lib/libsage_patch.so && "${SAGE_PATCH_DISABLED:-0}" != "1" ]]; then + if [[ -n "${LD_PRELOAD:-}" ]]; then + export LD_PRELOAD="/app/lib/libsage_patch.so:${LD_PRELOAD}" + else + export LD_PRELOAD="/app/lib/libsage_patch.so" + fi + export DXVK_HUD="${DXVK_HUD:-fps}" + else + export DXVK_HUD="${DXVK_HUD:-0}" + fi # Keep Vulkan ICD selection to the runtime/driver defaults unless user explicitly overrides. @@ -131,6 +151,17 @@ modules: fi cd "${CNC_GENERALS_INSTALLPATH}" + + # SagePatch INI override: engine reads INIs from cwd, not /app/share. + # On first launch, copy the bundled override into the asset tree so + # casual QoL settings (camera height, scroll speed, pitch) take effect. + SAGE_INI_SRC="/app/share/generalsxzh/SagePatch.ini" + SAGE_INI_DST="${CNC_GENERALS_INSTALLPATH}Data/INI/Default/GameData/SagePatch.ini" + if [[ -f "${SAGE_INI_SRC}" && ! -f "${SAGE_INI_DST}" && "${SAGE_PATCH_DISABLED:-0}" != "1" ]]; then + mkdir -p "$(dirname "${SAGE_INI_DST}")" + cp "${SAGE_INI_SRC}" "${SAGE_INI_DST}" 2>/dev/null || true + fi + exec /app/bin/GeneralsXZH "$@" EOF - chmod +x /app/bin/run.sh diff --git a/scripts/build/linux/bundle-linux-zh.sh b/scripts/build/linux/bundle-linux-zh.sh index 2aa556585e2..633165fbef4 100755 --- a/scripts/build/linux/bundle-linux-zh.sh +++ b/scripts/build/linux/bundle-linux-zh.sh @@ -83,6 +83,19 @@ cp "${SDL3_IMAGE_LIB_DIR}"/libSDL3_image.so* "${BUNDLE_DIR}/" echo " + GameSpy library" cp "${GAMESPY_LIB}" "${BUNDLE_DIR}/" +# SagePatch (optional, gated by RTS_BUILD_OPTION_SAGE_PATCH at configure time). +SAGE_PATCH_LIB="${BUILD_DIR}/Patches/SagePatch/libsage_patch.so" +SAGE_PATCH_OVERRIDE="${PROJECT_ROOT}/Patches/SagePatch/resources/Override.ini" +if [[ -f "${SAGE_PATCH_LIB}" ]]; then + echo " + libsage_patch (SagePatch QoL)" + cp "${SAGE_PATCH_LIB}" "${BUNDLE_DIR}/" + if [[ -f "${SAGE_PATCH_OVERRIDE}" ]]; then + mkdir -p "${BUNDLE_DIR}/Data/INI/Default/GameData" + cp "${SAGE_PATCH_OVERRIDE}" \ + "${BUNDLE_DIR}/Data/INI/Default/GameData/SagePatch.ini" + fi +fi + # DXVK config if [[ -f "${DXVK_CONF_SRC}" ]]; then echo " + dxvk.conf" @@ -104,7 +117,20 @@ export LD_LIBRARY_PATH="${SCRIPT_DIR}:${LD_LIBRARY_PATH:-}" # Set DXVK environment export DXVK_WSI_DRIVER="SDL3" export DXVK_LOG_LEVEL="${DXVK_LOG_LEVEL:-info}" -export DXVK_HUD="${DXVK_HUD:-0}" + +# SagePatch (optional QoL: F11 screenshot, Scroll Lock cursor lock, +# Ctrl+PgUp/Dn brightness, Ctrl+1..5 window snap). Loaded via LD_PRELOAD only +# if libsage_patch.so is bundled. DXVK_HUD defaults to "fps" when active. +if [[ -f "${SCRIPT_DIR}/libsage_patch.so" && "${SAGE_PATCH_DISABLED:-0}" != "1" ]]; then + if [[ -n "${LD_PRELOAD:-}" ]]; then + export LD_PRELOAD="${SCRIPT_DIR}/libsage_patch.so:${LD_PRELOAD}" + else + export LD_PRELOAD="${SCRIPT_DIR}/libsage_patch.so" + fi + export DXVK_HUD="${DXVK_HUD:-fps}" +else + export DXVK_HUD="${DXVK_HUD:-0}" +fi # Auto-detect base Generals install path if [[ -z "${CNC_GENERALS_INSTALLPATH:-}" && -d "${SCRIPT_DIR}/../Generals" ]]; then diff --git a/scripts/build/linux/bundle-linux.sh b/scripts/build/linux/bundle-linux.sh index cdf26e9995c..6312d2491a0 100755 --- a/scripts/build/linux/bundle-linux.sh +++ b/scripts/build/linux/bundle-linux.sh @@ -83,6 +83,19 @@ cp "${SDL3_IMAGE_LIB_DIR}"/libSDL3_image.so* "${BUNDLE_DIR}/" echo " + GameSpy library" cp "${GAMESPY_LIB}" "${BUNDLE_DIR}/" +# SagePatch (optional, gated by RTS_BUILD_OPTION_SAGE_PATCH at configure time). +SAGE_PATCH_LIB="${BUILD_DIR}/Patches/SagePatch/libsage_patch.so" +SAGE_PATCH_OVERRIDE="${PROJECT_ROOT}/Patches/SagePatch/resources/Override.ini" +if [[ -f "${SAGE_PATCH_LIB}" ]]; then + echo " + libsage_patch (SagePatch QoL)" + cp "${SAGE_PATCH_LIB}" "${BUNDLE_DIR}/" + if [[ -f "${SAGE_PATCH_OVERRIDE}" ]]; then + mkdir -p "${BUNDLE_DIR}/Data/INI/Default/GameData" + cp "${SAGE_PATCH_OVERRIDE}" \ + "${BUNDLE_DIR}/Data/INI/Default/GameData/SagePatch.ini" + fi +fi + # DXVK config if [[ -f "${DXVK_CONF_SRC}" ]]; then echo " + dxvk.conf" @@ -104,7 +117,19 @@ export LD_LIBRARY_PATH="${SCRIPT_DIR}:${LD_LIBRARY_PATH:-}" # Set DXVK environment export DXVK_WSI_DRIVER="SDL3" export DXVK_LOG_LEVEL="${DXVK_LOG_LEVEL:-info}" -export DXVK_HUD="${DXVK_HUD:-0}" + +# SagePatch (optional QoL: F11 screenshot, Scroll Lock cursor lock, +# Ctrl+PgUp/Dn brightness, Ctrl+1..5 window snap). LD_PRELOAD only when bundled. +if [[ -f "${SCRIPT_DIR}/libsage_patch.so" && "${SAGE_PATCH_DISABLED:-0}" != "1" ]]; then + if [[ -n "${LD_PRELOAD:-}" ]]; then + export LD_PRELOAD="${SCRIPT_DIR}/libsage_patch.so:${LD_PRELOAD}" + else + export LD_PRELOAD="${SCRIPT_DIR}/libsage_patch.so" + fi + export DXVK_HUD="${DXVK_HUD:-fps}" +else + export DXVK_HUD="${DXVK_HUD:-0}" +fi # Auto-detect base Generals install path if [[ -z "${CNC_GENERALS_INSTALLPATH:-}" && -d "${SCRIPT_DIR}/../Generals" ]]; then diff --git a/scripts/build/linux/deploy-linux-zh.sh b/scripts/build/linux/deploy-linux-zh.sh index 868c89d1dae..3452204a25e 100755 --- a/scripts/build/linux/deploy-linux-zh.sh +++ b/scripts/build/linux/deploy-linux-zh.sh @@ -103,6 +103,26 @@ patchelf --set-rpath '$ORIGIN' "${RUNTIME_DIR}/GeneralsXZH" 2>/dev/null || { echo " Libraries will need LD_LIBRARY_PATH or manual RPATH setting" } +# SagePatch (optional, gated by RTS_BUILD_OPTION_SAGE_PATCH at configure time). +# When the .so exists, deploy it + the Override.ini into Data/INI/Default/ so +# the engine picks up the casual QoL settings. The launcher wrapper sets +# LD_PRELOAD to load the .so at runtime. +SAGE_PATCH_LIB="${BUILD_DIR}/Patches/SagePatch/libsage_patch.so" +SAGE_PATCH_OVERRIDE="${PROJECT_ROOT}/Patches/SagePatch/resources/Override.ini" +if [[ -f "${SAGE_PATCH_LIB}" ]]; then + echo " Deploying SagePatch (libsage_patch.so)..." + cp -v "${SAGE_PATCH_LIB}" "${RUNTIME_DIR}/" + if [[ -f "${SAGE_PATCH_OVERRIDE}" ]]; then + # path2 (Data/INI/GameData) is loaded after path1 (Data/INI/Default/GameData) + # and the path2 BIG file is what carries the real camera/scroll defaults. + # Our override must live in path2's subdir to win the last-write race. + mkdir -p "${RUNTIME_DIR}/Data/INI/GameData" + cp -v "${SAGE_PATCH_OVERRIDE}" \ + "${RUNTIME_DIR}/Data/INI/GameData/SagePatch.ini" + rm -f "${RUNTIME_DIR}/Data/INI/Default/GameData/SagePatch.ini" + fi +fi + # Copy run wrapper script echo " Copying run.sh wrapper..." cat > "${RUNTIME_DIR}/run.sh" << 'EOF' @@ -118,7 +138,25 @@ export LD_LIBRARY_PATH="${SCRIPT_DIR}:${LD_LIBRARY_PATH:-}" # Set DXVK environment export DXVK_WSI_DRIVER="SDL3" export DXVK_LOG_LEVEL="${DXVK_LOG_LEVEL:-info}" -export DXVK_HUD="${DXVK_HUD:-0}" +# DXVK HUD: SagePatch builds default to "fps" so casual users see frame rate +# without extra config. Set DXVK_HUD=0 explicitly to disable, or anything else +# (e.g. fps,memory,version) to customize. See DXVK docs for full list. +if [[ -f "${SCRIPT_DIR}/libsage_patch.so" && "${SAGE_PATCH_DISABLED:-0}" != "1" ]]; then + export DXVK_HUD="${DXVK_HUD:-fps}" +else + export DXVK_HUD="${DXVK_HUD:-0}" +fi + +# SagePatch (optional QoL features). Loaded via LD_PRELOAD so it can interpose +# SDL3 functions for hot-keys (F11 screenshot, Scroll Lock cursor lock, +# Ctrl+PageUp/PageDown brightness, Ctrl+1..5 window snap). +if [[ -f "${SCRIPT_DIR}/libsage_patch.so" && "${SAGE_PATCH_DISABLED:-0}" != "1" ]]; then + if [[ -n "${LD_PRELOAD:-}" ]]; then + export LD_PRELOAD="${SCRIPT_DIR}/libsage_patch.so:${LD_PRELOAD}" + else + export LD_PRELOAD="${SCRIPT_DIR}/libsage_patch.so" + fi +fi # GeneralsX @feature felipebraz 25/02/2026 Auto-detect base Generals install path # Set CNC_GENERALS_INSTALLPATH if not already set and ../Generals/ exists @@ -173,8 +211,13 @@ if [[ -z "${ALSOFT_DRIVERS:-}" ]]; then echo "INFO: OpenAL: ALSOFT_DRIVERS=$ALSOFT_DRIVERS (pipewire excluded)" fi +# The engine resolves Local FS lookups (Data/INI/Default/... overrides, etc.) +# relative to the binary's cwd. Without this cd, anything launched via absolute +# path misses every loose INI / asset and only sees BIG-archived data. +cd "${SCRIPT_DIR}" + # Run game with all arguments -exec "${SCRIPT_DIR}/GeneralsXZH" "$@" +exec "./GeneralsXZH" "$@" EOF chmod +x "${RUNTIME_DIR}/run.sh" diff --git a/scripts/build/macos/bundle-macos-generals.sh b/scripts/build/macos/bundle-macos-generals.sh index 29197d49da1..b96892d75f1 100755 --- a/scripts/build/macos/bundle-macos-generals.sh +++ b/scripts/build/macos/bundle-macos-generals.sh @@ -283,6 +283,19 @@ echo " + libdxvk_d3d8" cp "${DXVK_D3D8_LIB}" "${LIB_DIR}/libdxvk_d3d8.0.dylib" ln -sf libdxvk_d3d8.0.dylib "${LIB_DIR}/libdxvk_d3d8.dylib" +# SagePatch (optional, gated by RTS_BUILD_OPTION_SAGE_PATCH at configure time). +SAGE_PATCH_LIB="${BUILD_DIR}/Patches/SagePatch/libsage_patch.dylib" +SAGE_PATCH_OVERRIDE="${PROJECT_ROOT}/Patches/SagePatch/resources/Override.ini" +if [[ -f "${SAGE_PATCH_LIB}" ]]; then + echo " + libsage_patch (SagePatch QoL)" + cp "${SAGE_PATCH_LIB}" "${LIB_DIR}/libsage_patch.dylib" + if [[ -f "${SAGE_PATCH_OVERRIDE}" ]]; then + mkdir -p "${RESOURCES_DIR}/Data/INI/Default/GameData" + cp "${SAGE_PATCH_OVERRIDE}" \ + "${RESOURCES_DIR}/Data/INI/Default/GameData/SagePatch.ini" + fi +fi + if [[ "${INCLUDE_EXTERNAL_DYLIBS}" == "1" ]]; then echo " + scanning for external dylibs (Homebrew/system extras)" collect_external_dylibs "${LIB_DIR}" \ @@ -347,9 +360,21 @@ LIB_DIR="${RESOURCES_DIR}/lib" export DYLD_LIBRARY_PATH="${LIB_DIR}:${BIN_DIR}:${DYLD_LIBRARY_PATH:-}" +# SagePatch — see notes in bundle-macos-zh.sh. +if [[ -f "${LIB_DIR}/libsage_patch.dylib" && "${SAGE_PATCH_DISABLED:-0}" != "1" ]]; then + if [[ -n "${DYLD_INSERT_LIBRARIES:-}" ]]; then + export DYLD_INSERT_LIBRARIES="${LIB_DIR}/libsage_patch.dylib:${DYLD_INSERT_LIBRARIES}" + else + export DYLD_INSERT_LIBRARIES="${LIB_DIR}/libsage_patch.dylib" + fi +fi + # GeneralsX @bugfix fbraz3 20/03/2026 DXVK requires this env var on non-Win32; SDL3 matches game windowing layer export DXVK_WSI_DRIVER="SDL3" +# DXVK HUD: kept opt-in. MoltenVK on macOS 26 can't compile DXVK's HUD shader. +export DXVK_HUD="${DXVK_HUD:-0}" + if [[ -f "${RESOURCES_DIR}/MoltenVK_icd.json" ]]; then export VK_ICD_FILENAMES="${RESOURCES_DIR}/MoltenVK_icd.json" # GeneralsX @bugfix fbraz3 20/03/2026 Vulkan Loader 1.3.236+ uses VK_DRIVER_FILES; keep VK_ICD_FILENAMES for older loaders @@ -371,6 +396,14 @@ fi # Run from the detected Generals asset root when available. if [[ -d "${CNC_GENERALS_PATH}" ]]; then cd "${CNC_GENERALS_PATH}" + + # SagePatch INI override seed — see notes in bundle-macos-zh.sh. + SAGE_INI_SRC="${RESOURCES_DIR}/Data/INI/Default/GameData/SagePatch.ini" + SAGE_INI_DST="${CNC_GENERALS_PATH}/Data/INI/Default/GameData/SagePatch.ini" + if [[ -f "${SAGE_INI_SRC}" && ! -f "${SAGE_INI_DST}" && "${SAGE_PATCH_DISABLED:-0}" != "1" ]]; then + mkdir -p "$(dirname "${SAGE_INI_DST}")" + cp "${SAGE_INI_SRC}" "${SAGE_INI_DST}" + fi fi exec "${BIN_DIR}/GeneralsX" "$@" diff --git a/scripts/build/macos/bundle-macos-zh.sh b/scripts/build/macos/bundle-macos-zh.sh index 2cd8455c340..d4333797189 100755 --- a/scripts/build/macos/bundle-macos-zh.sh +++ b/scripts/build/macos/bundle-macos-zh.sh @@ -285,6 +285,19 @@ echo " + libdxvk_d3d8" cp "${DXVK_D3D8_LIB}" "${LIB_DIR}/libdxvk_d3d8.0.dylib" ln -sf libdxvk_d3d8.0.dylib "${LIB_DIR}/libdxvk_d3d8.dylib" +# SagePatch (optional, gated by RTS_BUILD_OPTION_SAGE_PATCH at configure time). +SAGE_PATCH_LIB="${BUILD_DIR}/Patches/SagePatch/libsage_patch.dylib" +SAGE_PATCH_OVERRIDE="${PROJECT_ROOT}/Patches/SagePatch/resources/Override.ini" +if [[ -f "${SAGE_PATCH_LIB}" ]]; then + echo " + libsage_patch (SagePatch QoL)" + cp "${SAGE_PATCH_LIB}" "${LIB_DIR}/libsage_patch.dylib" + if [[ -f "${SAGE_PATCH_OVERRIDE}" ]]; then + mkdir -p "${RESOURCES_DIR}/Data/INI/Default/GameData" + cp "${SAGE_PATCH_OVERRIDE}" \ + "${RESOURCES_DIR}/Data/INI/Default/GameData/SagePatch.ini" + fi +fi + if [[ "${INCLUDE_EXTERNAL_DYLIBS}" == "1" ]]; then echo " + scanning for external dylibs (Homebrew/system extras)" collect_external_dylibs "${LIB_DIR}" \ @@ -365,9 +378,27 @@ LIB_DIR="${RESOURCES_DIR}/lib" export DYLD_LIBRARY_PATH="${LIB_DIR}:${BIN_DIR}:${DYLD_LIBRARY_PATH:-}" +# SagePatch (optional QoL: F11 screenshot, Scroll Lock cursor lock, Ctrl+PgUp/Dn +# brightness, Ctrl+1..5 window snap). Only loaded when the bundled dylib is +# present and SAGE_PATCH_DISABLED is not set. Also seeds the engine INI loader +# via Resources/Data/INI/Default/GameData/SagePatch.ini. +if [[ -f "${LIB_DIR}/libsage_patch.dylib" && "${SAGE_PATCH_DISABLED:-0}" != "1" ]]; then + if [[ -n "${DYLD_INSERT_LIBRARIES:-}" ]]; then + export DYLD_INSERT_LIBRARIES="${LIB_DIR}/libsage_patch.dylib:${DYLD_INSERT_LIBRARIES}" + else + export DYLD_INSERT_LIBRARIES="${LIB_DIR}/libsage_patch.dylib" + fi +fi + # GeneralsX @bugfix fbraz3 20/03/2026 DXVK requires this env var on non-Win32; SDL3 matches game windowing layer export DXVK_WSI_DRIVER="SDL3" +# DXVK HUD: kept opt-in. MoltenVK on macOS 26 cannot compile DXVK's HUD +# pipeline shader (gl_DrawID / SPIR-V DrawIndex has no MSL equivalent yet). +# Defaulting it on causes the swap chain blit pipeline to fail. Users wanting +# an FPS overlay set DXVK_HUD=fps themselves. +export DXVK_HUD="${DXVK_HUD:-0}" + if [[ -f "${RESOURCES_DIR}/MoltenVK_icd.json" ]]; then export VK_ICD_FILENAMES="${RESOURCES_DIR}/MoltenVK_icd.json" # GeneralsX @bugfix fbraz3 20/03/2026 Vulkan Loader 1.3.236+ uses VK_DRIVER_FILES; keep VK_ICD_FILENAMES for older loaders @@ -407,6 +438,16 @@ fi # Run from the detected Zero Hour asset root when available. if [[ -d "${CNC_GENERALS_ZH_PATH}" ]]; then cd "${CNC_GENERALS_ZH_PATH}" + + # SagePatch INI override: the engine reads INIs from the cwd, not from + # inside the .app bundle. On first launch (or if the user deleted it), + # seed the override into the asset directory so casual QoL takes effect. + SAGE_INI_SRC="${RESOURCES_DIR}/Data/INI/Default/GameData/SagePatch.ini" + SAGE_INI_DST="${CNC_GENERALS_ZH_PATH}/Data/INI/Default/GameData/SagePatch.ini" + if [[ -f "${SAGE_INI_SRC}" && ! -f "${SAGE_INI_DST}" && "${SAGE_PATCH_DISABLED:-0}" != "1" ]]; then + mkdir -p "$(dirname "${SAGE_INI_DST}")" + cp "${SAGE_INI_SRC}" "${SAGE_INI_DST}" + fi fi exec "${BIN_DIR}/GeneralsXZH" "$@" diff --git a/scripts/build/macos/deploy-macos-zh.sh b/scripts/build/macos/deploy-macos-zh.sh index e1b4db2383d..4047492a5d1 100755 --- a/scripts/build/macos/deploy-macos-zh.sh +++ b/scripts/build/macos/deploy-macos-zh.sh @@ -139,6 +139,29 @@ else exit 1 fi +# SagePatch (optional, gated by RTS_BUILD_OPTION_SAGE_PATCH at configure time). +# When the dylib exists, deploy it + the Override.ini into Data/INI/Default/ so the +# engine picks up the casual QoL settings. The launcher wrapper sets +# DYLD_INSERT_LIBRARIES to load the dylib at runtime. +SAGE_PATCH_LIB="${BUILD_DIR}/Patches/SagePatch/libsage_patch.dylib" +SAGE_PATCH_OVERRIDE="${PROJECT_ROOT}/Patches/SagePatch/resources/Override.ini" +if [[ -f "${SAGE_PATCH_LIB}" ]]; then + echo " Deploying SagePatch (libsage_patch.dylib)..." + cp -v "${SAGE_PATCH_LIB}" "${RUNTIME_DIR}/" + if [[ -f "${SAGE_PATCH_OVERRIDE}" ]]; then + # The engine loads GameData INIs in two passes: path1=Data/INI/Default/GameData + # then path2=Data/INI/GameData. Path2 is loaded LAST and contains the actual + # camera/scroll defaults (MaxCameraHeight, etc.) in BIG-archived GameData.ini. + # Our override must therefore go in path2's subdir to take effect, since each + # parse of a GameData block overwrites prior values in TheWritableGlobalData. + mkdir -p "${RUNTIME_DIR}/Data/INI/GameData" + cp -v "${SAGE_PATCH_OVERRIDE}" \ + "${RUNTIME_DIR}/Data/INI/GameData/SagePatch.ini" + # Clean up any prior misplaced copy from earlier deploy versions. + rm -f "${RUNTIME_DIR}/Data/INI/Default/GameData/SagePatch.ini" + fi +fi + # GeneralsX @bugfix Copilot 24/03/2026 Deploy Fontconfig config into runtime dir so FreeType/Fontconfig can resolve fonts on macOS. # GeneralsX @bugfix BenderAI 24/03/2026 Guard Fontconfig conf.d copy so missing directory does not abort deploy under set -e. echo " Deploying Fontconfig config..." @@ -166,9 +189,26 @@ SCRIPT_DIR="\$(cd "\$(dirname "\$0")" && pwd)" # SDL3 and gamespy dylibs are in same dir; Vulkan/MoltenVK stays in SDK export DYLD_LIBRARY_PATH="\${SCRIPT_DIR}:\${DYLD_LIBRARY_PATH:-}" +# SagePatch (optional QoL features). Loaded via DYLD_INSERT_LIBRARIES so it +# can interpose SDL3 functions for hot-keys (F11 screenshot, Scroll Lock cursor +# lock, Ctrl+PageUp/PageDown brightness, Ctrl+1..5 window snap). +if [[ -f "\${SCRIPT_DIR}/libsage_patch.dylib" && "\${SAGE_PATCH_DISABLED:-0}" != "1" ]]; then + if [[ -n "\${DYLD_INSERT_LIBRARIES:-}" ]]; then + export DYLD_INSERT_LIBRARIES="\${SCRIPT_DIR}/libsage_patch.dylib:\${DYLD_INSERT_LIBRARIES}" + else + export DYLD_INSERT_LIBRARIES="\${SCRIPT_DIR}/libsage_patch.dylib" + fi +fi + # GeneralsX @bugfix fbraz3 20/03/2026 DXVK requires DXVK_WSI_DRIVER on non-Win32; must match game windowing (SDL3) export DXVK_WSI_DRIVER="SDL3" +# DXVK HUD: kept opt-in. MoltenVK on macOS 26 cannot compile DXVK's HUD +# pipeline shader (uses gl_DrawID / SPIR-V DrawIndex which has no MSL +# equivalent yet), so defaulting it on breaks the swap chain blit pipeline. +# Users wanting an FPS overlay set DXVK_HUD=fps themselves. +export DXVK_HUD="\${DXVK_HUD:-0}" + # MoltenVK ICD manifest — deployed alongside the binary by deploy-macos-zh.sh if [[ -f "\${SCRIPT_DIR}/MoltenVK_icd.json" ]]; then export VK_ICD_FILENAMES="\${SCRIPT_DIR}/MoltenVK_icd.json" @@ -187,7 +227,13 @@ if [[ -z "\${CNC_GENERALS_INSTALLPATH:-}" && -d "\${SCRIPT_DIR}/../Generals" ]]; export CNC_GENERALS_INSTALLPATH="\${SCRIPT_DIR}/../Generals/" fi -exec "\${SCRIPT_DIR}/GeneralsXZH" "\$@" +# The engine resolves Local FS lookups (e.g. INI overrides under +# Data/INI/Default/...) relative to the binary's cwd. Without this cd, anything +# launched via absolute path (Finder, gtimeout, full-path invocation) misses +# every loose INI / asset and only sees what is bundled inside the BIG files. +cd "\${SCRIPT_DIR}" + +exec "./GeneralsXZH" "\$@" WRAPPER chmod +x "${RUNTIME_DIR}/run.sh"