feat(sagepatch): casual QoL patch for macOS + Linux + user-config-dir (Phase 1.5)#156
Merged
Conversation
Introduces an optional patch (Patches/SagePatch/) that adds quality-of-life
features for casual play without modifying the game source. Gated by the
new RTS_BUILD_OPTION_SAGE_PATCH cmake flag (default OFF, ON in macos-vulkan
preset).
Architecture: a separate dylib loaded via DYLD_INSERT_LIBRARIES that
replaces SDL_PollEvent at the dyld level (__DATA,__interpose) to capture
hot-keys, plus an engine-side INI override picked up automatically from
Data/INI/Default/GameData/SagePatch.ini. No D3D8 proxy, no Vulkan layer,
no engine source changes.
Features in this initial drop:
F11 PNG screenshot via /usr/sbin/screencapture (window-only)
Scroll Lock cursor lock toggle (SDL_SetWindowMouseGrab)
Ctrl+PgUp/Dn display gamma adjustment, range -128..+128
Override.ini MaxCameraHeight=800, MinCameraHeight=60, ScrollSpeed=1.0,
EnforceMaxCameraHeight=No
Anti-cheat features (MDS, Game File Validator, ergc, version validation)
and competitive/networking features (Money Display, Player Table, Random
Balance, replay tools, CNC Online, ticker, Upload Mode, Auto Updater) are
intentionally excluded — focus is casual QoL only.
Smoke test: dylib loads cleanly via DYLD_INSERT_LIBRARIES, engine reads
the INI override, GameMain returns code 0.
…s + FPS
GeneralsX is a multi-platform project; the Phase-1 SagePatch was macOS-only,
which broke the rule. Restructured src/ tree and added Linux backend.
Source tree:
src/common/ SDL3-only code that works everywhere — Init, KeyHandler,
CursorLock, WindowPosition.
src/macos/ __DATA,__interpose hook, screencapture, CoreGraphics gamma.
src/linux/ LD_PRELOAD + dlsym(RTLD_NEXT), ImageMagick `import` /
gnome-screenshot, XF86VidMode (lazy dlopen, X11 only —
no-op under Wayland).
src/windows/ stubs only (Phase 2 — would need a proxy DLL).
New features in this iteration:
* Window snap presets — Ctrl+1..5 (center / TL / TR / BL / BR), via
SDL_SetWindowPosition. Cross-platform.
* FPS counter — both macOS and Linux deploy scripts default DXVK_HUD=fps
when SagePatch is active, so casual users get a frame counter without
extra config. Override with DXVK_HUD=0 or any custom value.
* Camera pitch override added to resources/Override.ini.
Linux deploy script (scripts/build/linux/deploy-linux-zh.sh) now copies
libsage_patch.so + Override.ini, and the generated run.sh sets LD_PRELOAD
when the library is present. SAGE_PATCH_DISABLED=1 still skips the preload.
Phase 1 complete. Phase 2 items intentionally deferred:
- Engine bug fixes (scud / tunnel / building / multiplayer crash) require
edits inside Generals*/Code, defeating SagePatch's "no source change"
contract.
- Windows preload is structurally different — needs a d3d8.dll proxy.
- In-game overlay text (clock, match timer, in-game menu) needs a
graphics hook (D3D8 proxy or Vulkan layer).
… fixes Two important clarifications after a code-base inventory: 1. Engine already supports GenTool-style CLI flags natively. Document -nologo, -noShellAnim, -noshellmap, -quickstart, -xres/-yres, -forcefullviewport, -noaudio/-nomusic/-novideo so users do not assume they need SagePatch to get those. They have always been part of the engine's CommandLine.cpp; no patch required. 2. Engine bug fixes (scud / tunnel / building / multiplayer movement crash) are handled by the upstream TheSuperHackers/GeneralsGameCode project which is maintained by the same author who wrote GenTool (xezon). The codebase already carries 170+ @BugFix annotations including the Tunnel System fixes by xezon himself. SagePatch does not duplicate them — that would create merge conflicts on the next upstream sync. This explicitly scopes SagePatch to QoL features that live outside the engine source tree.
Local deploy already shipped SagePatch on the previous commit. This wires
the same artifacts (libsage_patch.{dylib,so} + Override.ini) into every
release bundle so users actually get the QoL features when they download
the official zip / tar / flatpak — not just when they build from source.
Touches:
- scripts/build/macos/bundle-macos-zh.sh
- scripts/build/macos/bundle-macos-generals.sh
- scripts/build/linux/bundle-linux-zh.sh
- scripts/build/linux/bundle-linux.sh
- flatpak/com.fbraz3.GeneralsXZH.yml
- flatpak/com.fbraz3.GeneralsX.yml
Each one now:
1. Copies libsage_patch.{dylib,so} into the bundle's lib path (guarded by
`-f` so the build still succeeds when SAGE_PATCH=OFF).
2. Ships resources/Override.ini into the bundle's data path
(Resources/Data/... on macOS, /app/share/... in flatpak,
Data/... in the standalone Linux tar).
3. Generated wrapper scripts now set DYLD_INSERT_LIBRARIES / LD_PRELOAD
when the lib is present, with a `:` guard so empty existing values do
not break dyld.
4. Wrapper seeds the INI override into `${CNC_GENERALS_*_PATH}/Data/INI/Default/GameData/SagePatch.ini`
on first launch (engine reads INIs from cwd, not from inside the bundle).
5. DXVK_HUD defaults to "fps" when SagePatch is active (was: "0").
Flatpak manifests also pick up `-DRTS_BUILD_OPTION_SAGE_PATCH=ON` and run
the `sage_patch` build target after `z_generals` / `g_generals`.
Verified on macOS by running bundle-macos-zh.sh end-to-end:
GeneralsXZH.app/Contents/Resources/lib/libsage_patch.dylib (55 KB, arm64)
GeneralsXZH.app/Contents/Resources/Data/INI/Default/GameData/SagePatch.ini
…awIndex The wrapper scripts shipped DXVK_HUD=fps when SagePatch was active. On macOS 26 (MoltenVK 1.4.1, current SDK), DXVK's HUD pipeline shader uses gl_DrawID, which lowers to SPIR-V DrawIndex. SPIRV-Cross to MSL has no equivalent for that decoration and aborts conversion: [mvk-error] SPIR-V to MSL conversion error: DrawIndex is not supported in MSL. err: Failed to create swap chain blit pipeline: VK_ERROR_INITIALIZATION_FAILED The blit pipeline failure means DXVK can never present a frame; the game hangs at the EA Games logo (last thing the engine drew before DXVK started needing the blit pipeline). Revert the default to DXVK_HUD=0 on the three macOS wrapper scripts. Users who want the FPS overlay can still opt in with DXVK_HUD=fps. Linux and Flatpak wrappers are unchanged: native Vulkan drivers handle DrawIndex correctly there.
The engine resolves Local FS lookups (Data/INI/Default/<subdir>/*.ini overrides, loose Data/ assets, etc.) relative to the binary's cwd, never the binary's location. Without an explicit cd, launching the wrapper via absolute path, Finder, gtimeout, or any other invocation that does not happen to start in the asset dir caused the engine to miss every loose INI on disk — including SagePatch.ini — while still loading the BIG-archived defaults via the archive file system. Symptom: the game runs but the override apparently does nothing. Wrapper now cds to the script's own directory (which deploy puts the binary, the dylibs, the override INI, and the .big assets into) and execs the binary relative to that. Matches the bundle-script wrappers which already did this.
The engine ships a native FPS overlay at the top-left via W3DDisplay::drawFPSStats(), gated by #ifdef RTS_DEBUG plus the runtime -benchmark <seconds> CLI flag. The previous SagePatch revision defaulted DXVK_HUD=fps in the run wrapper as a release-build alternative, but that default was already reverted in 55f06b7 because MoltenVK on macOS 26 cannot compile DXVK's HUD pipeline shader (DrawIndex has no MSL equivalent) and the resulting blit-pipeline failure hangs the game at the EA logo. Removing the FPS row from the feature table and replacing it with a small 'About FPS counters' section that explains the native option and why our DXVK_HUD shortcut is parked.
The engine subsystem init for TheWritableGlobalData scans two parent dirs: path1 = Data/INI/Default/GameData (loaded first) path2 = Data/INI/GameData (loaded second) Each parsed GameData block overwrites prior values in TheWritableGlobalData (INI_LOAD_OVERWRITE semantics). The vanilla camera defaults (MaxCameraHeight = 310, MinCameraHeight = 120, CameraPitch = 37.5) live in the BIG-archived Data/INI/GameData.ini and are parsed in the SECOND pass, so an override placed under Data/INI/Default/GameData/ — which is parsed in the FIRST pass — is silently undone right after. Verified via temporary instrumentation in parseGameDataDefinition: pass 1: file=Data/INI/Default/GameData.ini max=300/min=100 (debug-only block) pass 2: file=Data/INI/GameData.ini line=464 max=310/min=120 (vanilla, was last-write) pass 3: file=Data/INI/GameData/SagePatch.ini max=800/min=60 (now winning) Deploy scripts on both platforms now write to Data/INI/GameData/SagePatch.ini (path2 subdir, parsed last) instead of Data/INI/Default/GameData/SagePatch.ini, and clean up any prior misplaced copy. SAGEPATCH.md gains a short section explaining the load order so future contributors do not repeat the mistake.
MaxCameraHeight=800 was too aggressive — on small maps it pushed the orthographic frustum past the playable border, exposing void/cull at the screen edges. Drop to 500 (~1.6x vanilla 310) so the extra range is useful without breaking small skirmish maps. Also raise MinCameraHeight back to 80 (slightly closer than vanilla 120 but not as tight as the previous 60), and remove the CameraPitch override entirely — letting the engine keep its vanilla ~37.5 pitch instead of forcing 50, which was an arbitrary choice and not a published GenTool default. Scroll factor stays at 1.0 (2x vanilla). Reaffirms that no published GenTool tuning numbers are public; these are conservative casual-friendly bumps.
8 tasks
Resolved conflicts in bundle scripts to keep both SagePatch and FFmpeg runtime libs
c36326f to
adcdff4
Compare
Owner
Author
|
@ebellumat FYI |
- Change RTS_BUILD_OPTION_SAGE_PATCH default from OFF to ON - Update comment to reflect Linux support (LD_PRELOAD) alongside macOS - SagePatch is now built by default on feature/sagepatch-qol-v2 branch GeneralsX @feature fbraz3 07/06/2026 Enable SagePatch by default
- Bundle scripts (macOS/Linux) now deploy SagePatch.ini to Data/INI/GameData/ instead of Data/INI/Default/GameData/ - Flatpak run.sh wrappers seed override to correct path - run.sh wrapper comments updated - Docs updated to reflect correct path - Deploy scripts keep cleanup of old misplaced path GeneralsX @BugFix fbraz3 07/06/2026 Fix SagePatch INI path
- Move add_subdirectory(Patches/SagePatch) before game targets - Add add_dependencies(z_generals sage_patch) so CI builds --target z_generals also triggers sage_patch build - Same for g_generals (Generals Base) Without this, CI 'cmake --build --target z_generals' skipped the standalone sage_patch library, so libsage_patch.dylib never existed in the build directory and the bundle script skipped it. GeneralsX @BugFix fbraz3 07/06/2026 SagePatch dependency on game targets
…alse SDL3 3.4.2 removed SDL_bool, SDL_TRUE, and SDL_FALSE. By default (without SDL_ENABLE_OLD_NAMES), these names are #defined to SDL_bool_renamed_bool / SDL_TRUE_renamed_true / SDL_FALSE_renamed_false which cause intentional compile errors. Replace with plain C++ bool/true/false in the interposer file. macOS interposer already used bool/true/false correctly. GeneralsX @BugFix fbraz3 07/06/2026 Fix SDL3 3.4.2 compat
BFD ld on Flatpak build ignores --unresolved-symbols=ignore-in-shared-libs, causing undefined reference errors for SDL_GetWindowFromID, SDL_SetWindowMouseGrab, SDL_GetDisplayForWindow, SDL_GetDisplayUsableBounds, SDL_GetWindowSize, SDL_SetWindowPosition, SDL_GetWindowProperties, SDL_GetNumberProperty. Link against SDL3::SDL3 on Linux (same as macOS uses -undefined dynamic_lookup). At runtime sage_patch loads via LD_PRELOAD into a process that already has SDL3, so symbols resolve from the host's libSDL3.so. GeneralsX @BugFix fbraz3 07/06/2026 Fix Linux sage_patch linker error
Adds a new GlobalData INI parameter TerrainDrawDistanceScale (default 1.0) that scales terrain draw dimensions at runtime. SagePatch Override.ini sets it to 1.05 to fix terrain pop-in at max zoom out. Changes: - GlobalData.h: Add m_terrainDrawDistanceScale Real field - GlobalData.cpp: Add INI parse entry and default init (1.0) - W3DView.cpp: Apply scale factor to drawWidth/drawHeight before rendering - Override.ini: Set TerrainDrawDistanceScale = 1.05 for ~5% increase
Replaces the run.sh/bundle script copy approach with engine-managed defaults. On first launch, the engine checks if SagePatch.ini exists in the user data directory and creates it with default QoL values if missing. Changes: - GameEngine.cpp: Auto-create SagePatch.ini with defaults if missing - deploy-*.sh: Remove Override.ini copy (engine handles it now) - bundle-*.sh: Remove Override.ini copy into bundle resources - flatpak/*.yml: Remove SagePatch.ini install + run.sh seeding User data dirs: - macOS: ~/Library/Application Support/GeneralsX/GeneralsZH/ - Linux: ~/.local/share/GeneralsX/GeneralsZH/
New in-game Extras menu with sliders for: - Max/Min Camera Height (100-1000 / 50-300) - Camera Pitch (20-60 degrees) - Keyboard Scroll Speed (1-200%) - Terrain Draw Distance Scale (100-200%) Files: - ExtrasMenu.cpp: 5 callbacks (Init/Update/Shutdown/System/Input) - ExtrasMenu.wnd: Basic layout with sliders and buttons - OptionPreferences.h/.cpp: Getters for SagePatch fields - FunctionLexicon.cpp: Registered in all 5 callback tables - GUICallbacks.h: Declared externs
Adds ButtonExtras to OptionsMenu.wnd handler that pushes Menus/ExtrasMenu.wnd onto the shell stack.
The engine's winCreateFromScript prepends 'Window\' to the path and opens via TheFileSystem which checks local files before BIG archives. Copying the .wnd to Window/Menus/ in the runtime dir makes it loadable at runtime.
….wnd - Added ExtrasMenu.cpp to CMakeLists.txt Menus section - Fixed includes: GameClient/GameClient.h, GameClient/Shell.h, etc - Replaced MSG_NOT_HANDLED with MSG_IGNORED, GIM_KEYBOARD_KEY with GWM_CHAR - Created ExtrasMenu.wnd with 5 sliders and 3 buttons matching the .wnd format - Deploy scripts now copy the .wnd to runtime dir
OptionsMenu.wnd in WindowZH.big has no ButtonExtras. Create it at runtime using gogoGadgetPushButton, positioned to the left of ButtonBack. The button's system callback is set to OptionsMenuSystem so it matches the GBM_SELECTED handler.
getDataTemplate only recognizes VERTSLIDER/HORZSLIDER, not SLIDER. Using SLIDER left data pointer null causing segfault in parseSliderData.
- docs/HOWTO/README.md: Index of available tutorials - docs/HOWTO/SAGEPATCH_CONFIGURATION.md: Guide for configuring QoL settings - README.md: Added HOWTO section with link to SagePatch tutorial
- AGENTS.md: Added docs/HOWTO/ to Docs Workflow and Directories sections - docs.instructions.md: Added docs/HOWTO/ section with naming conventions and guidelines
User-facing tutorials moved from docs/BUILD/ to docs/HOWTO/: - GETTING_THE_GAME_FILES.md: How to obtain original game assets - INSTALLATION.md: How to install GeneralsX on Linux/macOS Updated cross-references: - README.md: Links point to docs/HOWTO/ - RELEASE_PIPELINE.md: Updated URL - release.yml: Updated GitHub URLs - HOWTO/README.md: Added both tutorials to index
- HOWTO Tutorials section moved right after downloads (more prominent) - Name origin section moved after HOWTO for better flow - Removed duplicate HOWTO section from bottom of file
…lobalData The Generals base game has its own GlobalData.h/cpp that didn't include the new TerrainDrawDistanceScale field, causing OptionPreferences.cpp (compiled from Core/) to fail with missing member error. Changes: - Generals/GlobalData.h: Add m_terrainDrawDistanceScale field - Generals/GlobalData.cpp: Add INI parse entry + default init (1.0) - Generals/GameEngine.cpp: Add SagePatch auto-creation (same as GeneralsMD)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Supersedes #110
This PR carries forward ebellumat's feature/sagepatch-qol work and adds Phase 1.5: a per-user config dir + symlink pattern so the SagePatch override is editable outside the game data dir and the .app / install dir. The original PR's 9 commits are preserved intact at the bottom of the branch history; the 4 follow-up commits are mine.
What's in
Phase 1 (ebellumat, 9 commits, untouched)
Patches/SagePatch/— optional, drop-in patch that adds quality-of-lifefeatures for casual play. Gated by the new CMake flag
RTS_BUILD_OPTION_SAGE_PATCH(defaultOFF,ONin themacos-vulkanpreset). Independently implemented from public GenTool docs + SDL3 /
CoreGraphics / X11 docs. No code reverse-engineered from the original
closed-source
d3d8.dll.F11screencapture -l <id>import/gnome-screenshotScroll LockCtrl + PageUp/DownCtrl + 1..5DXVK_HUD=fpsdefault inrun.shPhase 1.5 (this PR, 4 commits)
User-config-dir + symlink pattern — SagePatch
Override.iniisseeded once to a per-user location on first launch, then symlinked
from the engine's cwd path2 slot (
Data/INI/GameData/SagePatch.ini)to that user file. Users can edit QoL settings (MaxCameraHeight,
MinCameraHeight, KeyboardScrollSpeed, etc.) without touching the
.app / install dir or the game data dir, and changes take effect on
next launch.
~/Library/Application Support/GeneralsX/SagePatch/SagePatch.ini$XDG_CONFIG_HOME/GeneralsX/SagePatch/SagePatch.ini(fallback
~/.config/GeneralsX/SagePatch/SagePatch.ini)d3d8.dll)path1 → path2 fix — Engine INI load order is
path1first(
Data/INI/Default/GameData/, parsed first), then overwritten by theBIG-archived
Data/INI/GameData.inidefaults. A user override inpath1 is silently lost. ebellumat's PR commit 598d8fd fixed this on
the deploy side; the bundle side still wrote to path1. Both sides
now write to path2 (
Data/INI/GameData/, parsed last, wins).Base-game deploy parity —
deploy-linux.shanddeploy-macos-generals.shhad no SagePatch wiring at all (onlythe bundles did). A user running
cmake --preset macos-vulkan && cmake --build && ./scripts/build/macos/deploy-macos-generals.shgot a vanilla game; a user running the bundle got SagePatch. Both
base-game deploys now copy the dylib, set the preload env var,
default
DXVK_HUD=fps,cdto${SCRIPT_DIR}, and apply theuser-config-dir pattern — same behavior as the ZH deploys.
Launchers self-heal on every run — Stale
path1copies fromolder deploys are removed, stale
path2files (left over from aprevious non-symlink deploy) are removed before the symlink is
recreated. Idempotent; safe to re-run.
Architecture
No D3D8 proxy, no Vulkan layer, no engine source modifications. Phase 1
patch source is ~600 lines C++ across
common/,macos/,linux/,windows/(stubs).What's intentionally not in scope
TheSuperHackers/GeneralsGameCode, which is the parent of GeneralsX and is maintained by the same author who wrote GenTool. The codebase already carries 170+@bugfixannotations including Tunnel System fixes by xezon. Duplicating them here would create merge conflicts on the next upstream sync.DXVK_HUDprovides a basic FPS counter as a substitute.LD_PRELOAD/__interposeequivalent; needs a proxyd3d8.dlllike the original GenTool. Stubs are in place so the build still succeeds withRTS_BUILD_OPTION_SAGE_PATCH=ONon Windows, but the runtime is no-op until the proxy is implemented.Already in the engine, no patch needed
These GenTool-era options are first-class engine flags in
Common/CommandLine.cpp. Documented in the SAGEPATCH.md so users know they can call them directly viarun.sh:-nologo-noShellAnim-noshellmap-quickstart-xres N -yres N(resolution unlock — engine no longer locks the list)-forcefullviewport-noaudio,-nomusic,-novideoTest plan
macos-vulkanpreset,RTS_BUILD_OPTION_SAGE_PATCH=ON)libsage_patch.dylibproduced — 55 KB, arm64 native, dynamic SDL3 lookupOverride.iniinto the runtime dirrun.shwrapper setsDYLD_INSERT_LIBRARIES/LD_PRELOADcorrectly (no trailing:)bash -nclean, generatedrun.shblocksbash -ncleanbash -non every deploy and bundle scriptSAGE_PATCH_DISABLED=1 ./run.shskips the preload as expectedCommits
Phase 1.5 — follow-up (this PR adds)
c36326f58—docs(blog): 2026-06-06 SagePatch deploy/bundle user-config-dir continuation1e1beded9—refactor(sagepatch): base game deploy parity with Zero Hour192b44643—refactor(sagepatch): route deploy-zh run.sh through user-config-dir9023734e3—refactor(sagepatch): user-config-dir + symlink pattern, fix path1→path27328e0ab3—merge(main): sync sagepatch-qol with origin/main (force-push + upstream 06-05-2026)Phase 1 — original (ebellumat, preserved intact)
508839d62—tune(sagepatch): tone down camera defaults (500/80, drop pitch override)598d8fd57—fix(sagepatch,deploy): drop Override.ini in path2 subdir so values stick535c8a12a—docs(sagepatch): drop FPS-counter feature claim; engine already has one22d9696e1—fix(deploy): cd to script dir in run.sh wrapper before exec55f06b77c—fix(sagepatch,macos): default DXVK_HUD off; MoltenVK can't compile DrawIndex1cf944fdc—feat(sagepatch): wire SagePatch through every release bundle path3a87fa2cd—docs(sagepatch): clarify scope vs. existing engine flags and upstream fixes3efaac65c—feat(sagepatch): make cross-platform (macOS + Linux), add window snaps + FPSd42ef875c—feat(sagepatch): add casual QoL patch for macOS via DYLD interposeNotes on naming
SagePatchis a placeholder picked at scaffolding time (the SAGE engine being what Generals runs on). Trivial to rename via:Reference
feature/sagepatch-qol→main)docs/PATCHES/SAGEPATCH.mddocs/DEV_BLOG/2026-06-DIARY.md