Skip to content

About

Cyberpunk 2077 mixed input fix plugin for RED4ext

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

MixedInputFix

A RED4ext plugin that fixes the Cyberpunk 2077 (2.11+) freeze and input-hint flicker with mixed controls - any setup that sends both gamepad and mouse/keyboard input at the same time:

  • Steam Controller / Steam Deck with touchpad or gyro mapped to mouse
  • DualShock/DualSense or other pads with gyro-to-mouse
  • gamepad in one hand, mouse in the other
  • analog keyboards (e.g. Wooting) mapping movement keys to a virtual stick
  • any similar remapper setup (Steam Input, JoyShockMapper, reWASD, ...)

In vanilla, every such input flips the game's "current input device" back and forth, the on-screen button hints flicker between keyboard and gamepad variants, and after 20-60 minutes of play the game hard-freezes and is then killed by its own watchdog. CDPR has declined to fix it ("unsupported configuration"). Full story and investigation: bug.md, also published as a post on r/cyberpunkgame with further discussion and other players' reports.

With the plugin, the UI is pinned to the device of your choice; the flicker and the freeze are gone. The raw input layer is untouched - mouse, cursor and all inputs keep working exactly as before.

DISCLAIMER: this plugin and most of the content of this repository - the code, the reverse engineering documented in RE-notes.md, the analysis tooling and the documentation - were AI-generated by Anthropic's Claude (Fable 5), working under my direction, with every build tested in-game by me. I want to be upfront about this so you can judge for yourself; the notes document the evidence behind every decision.

Install

  1. Install RED4ext.
  2. Extract the archive into the game folder, or copy its red4ext/plugins/MixedInputFix/ folder (MixedInputFix.dll, optional MixedInputFix.ini, scripts/) there by hand.

The scripts/ folder holds the script API declarations and needs redscript; without redscript the fix itself works the same, only the API is unavailable.

Built and tested on game versions 2.31 and 2.5 (GOG). Expected to work on Steam and Epic builds of the same patches: all code addresses resolve through the game's address library at runtime.

Code addresses are patch-stable, struct offsets are not, so the plugin reads the game executable's file version at startup and picks its offsets from a per-build table. On a build it does not know, it falls back to the newest offsets it has and verifies them against the running game before changing anything - most hotfixes do not move these structs, so this usually just works. If verification fails, the offset-dependent layers stay disabled and the log says so, rather than writing to a wrong address. A code address that cannot be resolved at all still fails loudly at startup, with a RED4ext error naming the hash.

Config

MixedInputFix.ini next to the DLL. Missing file or entry = defaults.

  • enabled (default true) - false makes the plugin attach nothing; the game runs fully vanilla. The kill switch if a game patch misbehaves.

  • mode (default controller) - which device the UI is pinned to:

    • controller: UI locks to the gamepad after its first input. For gamepad-primary setups (Steam Controller/Deck, gyro aim). The game is 100% vanilla until a gamepad is used.
    • kbm: UI locks to keyboard/mouse from boot. For KBM-primary setups with auxiliary analog inputs (e.g. movement/driving on a stick).
    • inhibited: pins nothing (vanilla behaviour) until a script mod switches the mode through the script API.

    Script mods can change the mode at runtime; it returns to this value when a save is loaded or the game returns to the main menu.

  • verbose (default false) - per-event logging for debugging.

What "pinned" means (it goes beyond visuals)

Once engaged, every device-dependent choice the game makes follows the pinned device - not just how the UI looks:

  • UI visuals: button-hint icons, contextual HUD hint layouts (KBM and gamepad variants differ - some actions are one key on keyboard but a button combo on a pad), shard/codex popup layouts, gamepad-specific hint bars (world map etc.).
  • UI behavior: the CrystalCoat vehicle color editor keeps one navigation paradigm instead of flickering between its KBM and gamepad variants; the phone HUD and the settings menu's DualSense detection follow the pinned device too.
  • Gameplay branches that check the input device: the game scripts also consult "last used device" for behavior, and those checks follow the pin as well - e.g. crouch/dodge handling (on gamepad, dodge and crouch share a button, so whether a dodge leaves you crouched follows different rules than on keyboard) and body-carry interactions.

This is deliberate - the paradigm follows the device you actually think in. Worth knowing: in vanilla mixed input these checks were effectively random - each one read whichever device happened to send the last input event, so behavior like dodge-exit crouching flip-flopped mid-play. The plugin makes all of them deterministic and mutually consistent with the chosen mode.

Keyboard menu shortcuts (controller mode)

One consequence is worth calling out. While the UI is pinned to the gamepad, the keyboard menu keys stop responding to a tap: holding M still opens the map, but J/P/K/I do nothing at all. The game gates those five actions on the last-used device and picks the hold variant for a gamepad, because the pad's Start button opens the player menu on tap and the map on hold - and of the five, only the map declares a hold event, so the rest are left with no working path. Not a plugin bug; the derivation is in RE-notes.md.

The optional MixedInputMenuKeys mod restores them without altering the Start button - see Optional mods.

Crouch and dodge with a second analog device

Separate from the pin - and present in vanilla, reproducible with enabled=false - if a second analog device is active while you play (the mouse in controller mode, a stick/analog in kbm mode) the game's native input layer can drop crouch and dodge specifically while that device streams; every other action still taps fine. The button-to-action mapping is resolved in layers - the default layer alone for KBM, or the default layer plus one selected controller layout (Legacy/Agile/Alternative) for a gamepad - and the pin does not govern that resolver, so the non-pinned device's stream makes it re-layer and lose bindings, or their release edges, that live only in the momentarily inactive layer. The controller layouts also behave differently at this native level, by a mechanism we have not fully reverse-engineered.

Workaround for kbm mode: set the controller layout to Alternative (crouch and dodge become separate single-press buttons), then in r6/config/inputUserMappings.xml add your keyboard crouch key to ToggleCrouch_Button_Alternative and Dive_Button_Alternative, and your dodge key to Dodge_Button_Alternative. The controller-mode mirror is confirmed fixable the same way - give the pad's crouch/dodge separate single-press buttons in the default layer (mirror Alternative onto it) - though that is a heavier edit to the base controller layout. Full derivation in RE-notes.md.

Game 2.5 made it worse: even with the bindings fixed, dodge (and, less often, crouch) stops responding while the other device streams. The engine still delivers each press but drops its release, and the game only acts on a tap (press + quick release), so nothing happens until the stream stops. The optional, heavily experimental MixedInputCrouchDodgeWorkaround mod makes dodge and toggle crouch act on the press instead, and ships the binding edits above as an input_loader file - see Optional mods.

Script API

Redscript mods can switch the mode at runtime, e.g. to pin the controller only while driving, or to get the vanilla mouse cursor in photo mode. The declarations ship with the plugin (scripts/ next to the DLL):

MixedInputFix.SetMode(MixedInputFixMode.KBM);  // Inhibited / Controller / KBM
let saved = MixedInputFix.GetMode();           // Disabled if enabled=false
MixedInputFix.SetMode(saved);
MixedInputFix.ResetMode();                     // back to the ini mode
  • A new mode takes hold on the next input of its device (pad for Controller, keyboard/mouse for KBM) - immediately if that device is already in use. Until then the game runs vanilla, so every switch is a normal device change for the UI.
  • Inhibited pins nothing - vanilla behaviour, including the device flipping behind the freeze. Fine for occasional use (photo mode); prefer KBM for long stretches.
  • The runtime mode is never saved. It resets to the ini mode whenever the player detaches (loading a save, quitting to the main menu), so mods should re-apply theirs when their situation starts again.
  • Last call wins; there is no stacking between mods. Use GetMode() to save and restore around a temporary change.
  • With enabled=false every call is refused (SetMode/ResetMode return false, GetMode returns Disabled).
  • To compile without the plugin installed, wrap calls in @if(ModuleExists("MixedInputFix")).

From the Cyber Engine Tweaks console, call the underlying natives with the mode as a number (0 = inhibited, 1 = controller, 2 = kbm, 3 = disabled from GetMode only):

Game.MixedInputFix_SetMode(2)          -- kbm
print(Game.MixedInputFix_GetMode())    -- 2
Game.MixedInputFix_ResetMode()         -- back to the ini mode

The same calls can be bound to a CET hotkey or used from a CET mod.

Optional mods

Small redscript mods in mods/, each a separate download with its own version. Copy <Mod>.reds to <game>/r6/scripts/<Mod>/ (or extract its archive into the game folder). All need redscript. Mods with custom bindings also ship input/<Mod>.xml, which goes to <game>/r6/input/ and needs input_loader.

  • MixedInputMenuKeys - restores the keyboard menu shortcuts in controller mode (see above). Inert unless something pins the device, so it is safe to leave installed.

  • MixedInputInhibitOnPhotoMode - inhibits the plugin while photo mode is open (vanilla photo mode with the mouse cursor) and restores the previous mode on exit. Uses the script API.

  • MixedInputVehicleControllerFpsKbm - controller mode in vehicles, kbm mode on foot: drive with the pad, fight with the mouse. Uses the script API. Deliberately minimal: until you first leave a vehicle, the ini mode applies.

  • MixedInputBindings - two bindings that pin a device: pad A pins the controller, Space pins kbm - i.e. jump on foot, handbrake in vehicles, so you switch schemes by pressing that button on the device you pick up. Rebind them in MixedInputBindings.xml. Uses the script API and needs input_loader.

  • MixedInputCrouchDodgeWorkaround - heavily experimental. Dodge and toggle crouch (crouch, stand up, slide, air dash) act on the button press instead of a tap, so they keep working while the other device streams (see above). Requires the Alternative controller layout - with any other layout it changes nothing - and its MixedInputCrouchDodgeWorkaround.xml bindings (needs input_loader). Before 0.2 it was named MixedInputOnPress and shipped an edited r6/config/inputUserMappings.xml: delete r6/scripts/MixedInputOnPress/ and restore the .orig. Unlike the others it is not inert without the pin: with Alternative, dodge and crouch act on press for everyone. It replaces several vanilla locomotion methods, so it must be updated for each game patch and conflicts with other mods that replace the same methods.

  • MixedInputQuickMeleeWorkaround - experimental. Quick melee and the dual-pistol Valentino's Frenzy (same button) act on the button press instead of a tap, for the same reason: while the other device streams, the release is lost and quick melee never fires. With the Alternative layout quick melee (tap) and the scanner (hold) share LB, and only the release tells them apart, so one of the two has to leave the controller. The archive ships two variants of MixedInputQuickMeleeWorkaround.xml (needs input_loader). Copy exactly one into r6/input/. input_loader also reads subfolders of r6/input/, so the variants ship outside it:

    • r6/input-lb-does-only-quick-melee/ - LB is quick melee, and the scanner is removed from the controller, in every layout. Bind it to a keyboard key: Tab by default, or set "Scanner (hold)" in the keybinding settings.
    • r6/input-lb-does-only-scanner/ - LB is the scanner, and quick melee is removed from the controller: Q by default, or set "Quick Melee". The engine still delays the scanner on Alternative's LB by about 0.3 s, which the mod works around.

    Either way, a Steam Input / Deckhand back button mapped to that key works too, and the scanner comes on as soon as it is pressed (the XMLs drop its short hold delay). Not inert without the pin: quick melee acts on press for everyone. It replaces several vanilla quick melee and Frenzy methods, so it must be re-checked for each game patch.

MixedInputInhibitOnPhotoMode and MixedInputVehicleControllerFpsKbm are also meant as examples of the script API.

Verify

Check <game>/red4ext/logs/mixedinputfix-*.log:

  • startup: MixedInputFix 1.1.0 active (mode:controller game:3.0.88.46235 offsets:2.5/exact writer:on scriptKBM:on scriptPad:on scriptPS5:on latch:on+on) - offsets:<patch>/exact means the plugin recognises this game build; /fallback means it is running on an unlisted one and is verifying the offsets it guessed (see Install)
  • first gamepad input: gamepad detected (device 5) (5 = XInput; other pad types 2..9), and in controller mode controller mode engaged: UI device locked to gamepad (device 5)
  • mode changes through the script API: mode kbm -> controller (script), then the ... mode engaged line once that device is used
  • on exit: session summary: suppressed=... scriptKBM=... fallbackRestores=...
  • [fallback:...] warnings should be rare (session init at most); frequent ones mean a game patch added a new writer path - please report them.

Build from source

powershell -ExecutionPolicy Bypass -File build.ps1

Requires MSVC (vcvars64.bat path at the top of build.ps1) and the header-only RED4ext.SDK submodule (git submodule update --init). Output and all intermediates land in build/.

Note: the submodule deliberately tracks the psiberx fork of RED4ext.SDK (the variant used by Codeware) rather than upstream WopsS - it is what this plugin was developed and tested against; upstream has minor macro differences.

How it works

Short version - three cooperating layers, all bound via RED4ext address-library hashes (full reverse-engineering record with the derivations, evidence and failed approaches: RE-notes.md):

  1. Writer suppression (the core fix). The ink UI resolves all button-hint icons from a single device field ([[InkSystem]+0x210]+0x1F8 in 2.5, input::EInputDevice). That field has exactly one steady-state writer, an InkSystem method that reads the current device and, on change, rewrites the field and broadcasts device-change notifications - the flicker, and the per-event UI churn whose accumulation causes the freeze (reproduced and measured, see RE-notes). The plugin hooks the writer, derives the would-be value the same way the writer does, and skips it entirely when it would switch away from the pinned device: no write, no broadcasts, no churn.
  2. Script-side stickiness. Game scripts read the device through the natives PlayerLastUsedKBM/Pad/PS5Pad (a separate mechanism - it drives the contextual HUD hint layouts, popup layouts etc.). All script calls funnel through their registered native handlers; the plugin hooks those and forces the answers to match the pinned device.
  3. Fallback latch (watchdog). Independent hooks observe the field and restore the pinned value if anything outside the suppressed writer ever changes it (e.g. session-init code), logging a warning - so hook coverage is verifiable from any user's log. If the writer hook ever failed to attach on some game build, this latch alone still provides the fix at v0.6 level (proven in testing).
hook addr-lib hash purpose
device writer (InkSystem method) 3719303744 core fix: suppress switches off the pinned device
PlayerLastUsedKBM native handler 1037639234 script-side device answer
PlayerLastUsedPad native handler 1291656829 script-side device answer
PlayerLastUsedPS5Pad native handler 2647269205 script-side device answer (DualSense detection)
hint icon refresh (inkInputDisplayController) 4042789608 fallback latch + coverage watchdog
inkSystem::ProcessInputEvents 673454961 fallback latch, once per UI input pump
InkSystem instance pointer 2635337807 (data, not a hook)

The hashes survive game patches; the four struct offsets involved (2.5: 0x210/0x1F8/0x300/vtbl 0x228; 2.31 had 0x1E0 and 0x218 for the second and fourth) are per-patch and may need re-derivation - the procedure is documented in RE-notes.md ("Porting to a new game patch") and the tools/ directory contains the analysis scripts that make it a ~10 minute job.

Writer diagnostic (opt-in, for porting)

Create an empty file MixedInputFix.diag next to the installed DLL and start the game: the plugin arms a hardware write-watchpoint on the device field and logs every writer's instruction address as exe+0x... (the RIP is the instruction after the write; the plugin's own writes are filtered; auto-disarms after ~400 events). One short mixed-input session is enough; delete the marker afterwards. The offsets feed directly into tools/dumpfunc.py. This is how the writer was originally found.

Version history

  • 0.1-0.5 - failed approaches (kept honest in RE-notes.md so nobody retries them: device-activation hook fires on the wrong layer; pointer-mode suppression breaks the cursor).

  • 0.6 - read-side latch on the hint device field. First working version: flicker gone, no freeze in long sessions. Regression: gamepad hint bars on some screens (world map) could hide and never re-show.

  • 0.7 - added the opt-in hardware-watchpoint diagnostic; located the field's single writer.

  • 0.8 - writer-level suppression: switches off the pinned device are skipped inside the game's own writer - no hide broadcasts, no churn. Fixed the world-map regression at the root.

  • 0.9 / 0.9.1 - script-side stickiness via the PlayerLastUsed* native handlers: fixed the contextual HUD hint widget flickering between its KBM and gamepad layout variants; all script device checks made consistent.

  • 1.0.0 - release: MixedInputFix.ini (enabled, mode, verbose) with the new kbm mode (mirror-image stickiness for KBM-primary players), quiet-by-default logging with a session summary on exit, fail-soft hook attachment. Validated: vanilla freeze reproduced in ~25 min of intensive mixed input; with the fix, multi-hour sessions run clean at well past the vanilla freeze threshold with zero fallback events.

  • 1.1.0 - game 2.5 support, plus:

    • game-build awareness: struct offsets are selected from a per-version table keyed on the executable's file version (2.31 and 2.5), and an unlisted build falls back to the newest known offsets, verified against the running game before the fix engages (if they fail, the plugin stays fully vanilla);
    • script API for redscript mods to switch the mode at runtime, and a new inhibited mode;
    • two new optional mods built on it: MixedInputInhibitOnPhotoMode and MixedInputVehicleControllerFpsKbm.

License

MIT.

About

Cyberpunk 2077 mixed input fix plugin for RED4ext

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages