v3.3.0
Ultrawide renders correctly in Syndicate, and Rogue's frame rate no longer slows the game down or throws the camera around.
What's New
Syndicate ultrawide (AspectRatioFix)
The engine computes the correct aspect ratio for the selected mode and stores it at +0x95C/+0x960, but the viewport code reads +0x92C/+0x930. Only the AMD SLS paths write that second block, and both are gated off on a single monitor, so it keeps the 16:9 value it was initialised with and every ultrawide render is stretched -- in fullscreen and in borderless alike.
The new AspectRatioFix hook mirrors the computed ratio and its gate bytes into the block the consumer actually reads. Confirmed at 3440x1440: fullscreen and borderless both render correctly.
[Video]
; Aspect ratio override. Auto(0) = derive from the resolution the game applies.
; Accepts ratio like 21:9, 32:9, or decimal like 2.389
AspectRatio=0
[Hooks]
AspectRatioFix=trueLeaving AspectRatio=0 derives the ratio from whatever resolution the game applies, which is what you want unless the display reports something the game misreads.
Rogue frame pacing (FPSUnlock rework)
Setting a cap above the display's own ceiling ran the game in slow motion: at Target=120 against a 60 Hz present, the simulation advanced at half speed.
Two separate causes, both in how the plugin drove the engine's timing mode:
- Mode 3 was never an uncap.
UpdateFrameTimingre-arms the frame deadline for modes 0, 1 and 3, so mode 3 paced each frame to its own 15-frame trailing average and then fed that lagging average back as the frame delta. - Mode 0 ignored wall time. It stepped the simulation by
1/fixed_rateregardless of how long the frame actually took, so any external limiter below the cap slowed the game in proportion.
Both uncapped and capped now go through mode 2, which keeps the measured delta and the engine's 1/15 s clamp. Its deadline period is a float with a single reference in the binary: 0 leaves the deadline at the current time so the wait loop never runs, 1000/target paces to the cap.
While a cap is set the plugin also raises the process timer resolution to 1 ms. The game never calls timeBeginPeriod itself, so Sleep(1) rounds up to roughly 15.6 ms and nothing above about 64 FPS is reachable. Uncapped there is no wait at all, so the resolution is left alone.
Measured under Proton with DXVK holding Present at 60, simulation time taken from FrameTiming.accumulated_ticks against QPC:
| Build | Target | Present | Sim speed |
|---|---|---|---|
| v3.2.0 | 120 | 60 | 0.50x |
| v3.2.0 | 0 | 60 | 1.00x |
| v3.3.0 | 120 | 60 | 1.00x |
Refs #9.
Rogue camera lean (CameraLean)
The sprint camera sway is an additive sequence track on the CameraFXInterface roll angle, applied once per frame with no time step, so it scales directly with frame rate: a gentle lean at 30 fps becomes a 30-40 degree roll at 200 that snaps back when the sequence ends. Every camera modifier upstream of the FX pass is level, which is why this took a hardware watchpoint on the roll field to find.
The new CameraLean hook scales the track weight by frame_dt * ReferenceFPS at the sequence dispatcher, and only for additive tracks bound to that object. Measured at about 215 fps over 30 s of sprint-tapping: peak roll 24.7 deg without the hook, 6.0 deg with it at ReferenceFPS=60.
Fixes #8.
Rogue mouse smoothing (MouseSmoothing)
The counterpart to Syndicate's CameraSmoothing, but it cannot work the same way. Syndicate has a boolean gate choosing between a direct copy and a damped lerp of the look axes, so its hook flips one jnz to jmp. Rogue has no such gate -- tracing the look input end to end, the mouse delta is written raw into the virtual right stick and reaches all five consumers of that axis unfiltered. The lag that keeps the camera drifting after the mouse stops is applied further downstream, in the camera's own damping, which in this engine is data-driven. There is no branch to flip and no constant in the exe to change.
So MouseSmoothing hooks the input update where the delta is live -- after the cursor-mode filter, before the event consumers -- and pre-emphasises it to invert a first-order lag. Feeding estimate + (raw - estimate) / factor makes the downstream filter land on raw.
It is gated on is_in_game(), because menus drive the hardware cursor from the same DIMOUSESTATE buffer and amplifying deltas there makes the cursor unusable.
Debuggers can attach to Syndicate
VMProtect's debugger check redirects ntdll!DbgUiRemoteBreakin to a stub whose entire body is TerminateProcess(0xDEADC0DE), so the thread the OS injects on DebugActiveProcess killed the game instead of raising the breakpoint a debugger expects.
The plugin now restores the function's 5-byte prologue from the copy of ntdll on disk, putting it back to what the loader mapped. Verified live: Cheat Engine attaches, the game keeps running, and the bytes still read clean afterwards, so the packer does not reapply its patch. Useful if you want to poke at the game yourself.
Codebase audit
A full audit against the real binaries through the IDA databases, rather than inferred from source. The parts that affect you:
- ResolutionFix could crash. Its early return used
trampoline_rsp = rsp + 8, which points at shadow space rather than the return address, so filtering a mode jumped to whatever that slot held. Only reachable on a display that publishes a non-standard mode. - A VMProtect race. The
CreateThreadhook was armed before its result was stored, so a thread entering that window called through an unset hook and never reached the realCreateThread. - Config parsing was too permissive.
AspectRatio = 1.7abcparsed as1.7, andnanandinfwere accepted. Values are now trimmed, fully consumed and checked finite, falling back to the declared default. - Deleting an INI key did nothing until restart -- the field kept its old value. It now restores its default on reload.
- Silent partial hooks. An optional signature that stopped matching left part of a hook inactive with no way to tell that from a hook working as intended. The registry now warns per unresolved signature, by name, after install.
Builds are warnings-as-errors in CI, and the test suite runs under CTest with 37 passing tests in both configurations.
Configuration
New keys, both games. An existing INI works unchanged -- missing keys fall back to the defaults -- but copy the shipped INI if you want the new entries documented in place.
AC.Rogue.PatchFix.ini:
[CameraLean]
; Frame rate the lean was tuned for. At this rate the fix changes nothing; the
; game shipped capped at 30, so 30 reproduces the sway as designed and 60 keeps
; the stronger sway an unlocked 60 fps machine had. 0 removes the lean entirely.
ReferenceFPS=30
[Input]
; The engine's blend coefficient, measured by feel. 1.0 disables the
; compensation entirely. Lower it until the camera stops when the mouse stops;
; too low overshoots and the camera snaps back. Values below 0.02 are treated
; as off and logged. This is a per-frame coefficient: if the feel changes after
; changing [FPS] Target, retune it.
SmoothingFactor=0.1
[Hooks]
CameraLean=true
MouseSmoothing=trueAC.Syndicate.PatchFix.ini:
[Video]
; Aspect ratio override. Auto(0) = derive from the resolution the game applies.
; Accepts ratio like 21:9, 32:9, or decimal like 2.389
AspectRatio=0
[Hooks]
AspectRatioFix=trueAssets
| File | Description |
|---|---|
AC.Rogue.PatchFix.asi |
Rogue plugin (Windows) |
AC.Rogue.PatchFix-wine.asi |
Rogue plugin (Wine/Proton cross-compiled) |
AC.Rogue.PatchFix.ini |
Rogue default configuration |
AC.Syndicate.PatchFix.asi |
Syndicate plugin (Windows) |
AC.Syndicate.PatchFix-wine.asi |
Syndicate plugin (Wine/Proton cross-compiled) |
AC.Syndicate.PatchFix.ini |
Syndicate default configuration |
What's Changed
- fix(rogue): scale camera sway by frame time by @playday3008 in #14
- fix(rogue): pace FPS through the engine's vsync mode by @playday3008 in #10
- feat(rogue): add MouseSmoothing hook by @playday3008 in #13
- fix: audit the codebase and fix what it turned up by @playday3008 in #16
- feat(vmp): restore DbgUiRemoteBreakin so debuggers can attach by @playday3008 in #17
- feat(syndicate): add AspectRatioFix hook by @playday3008 in #19
Full Changelog: v3.2.0...v3.3.0