Skip to content

TheWeion/007FirstLightUltrawide

Repository files navigation

007 First Light — Ultrawide Cutscene Patch (ASI)

Removes the 16:9 pillarboxing (black bars on the sides) during cutscenes on ultrawide / super-ultrawide monitors, without editing the executable via an hex editor.

It does the same thing as the manual HxD fix — it just does it in memory, at runtime, every launch. No file edits, so it should survive game updates.

How it works

The Glacier engine stores the cutscene aspect ratio as a raw 32-bit float. For 16:9 that value is 1.777778, which in memory is the bytes 39 8E E3 3F. On startup this ASI scans the game's loaded executable image, finds every copy of that constant, and overwrites it with the float for your aspect ratio (Width / Height). Identical result to the hex edit, computed automatically.

The scan runs on a short retry schedule (up to 20 attempts, 500 ms apart) so it still lands if the engine is slow to map the constant — it succeeds as soon as the value appears and stops, so there's no fixed startup delay to wait out.

What you need

  1. An ASI loader. The standard one is Ultimate ASI Loader (ThirteenAG, on GitHub). Download the x64 build.
  2. Rename the loader's dll to a name the game already loads, and drop it in the game's Retail folder (next to 007FirstLight.exe). Good choices for a Glacier/Bink title:
    • bink2w64.dll (Glacier uses Bink for video — natural fit), or
    • dinput8.dll, or version.dll, or winhttp.dll. Any one of these works; pick whichever the loader's docs recommend if one doesn't take.

Install

Place these next to 007FirstLight.exe (…\Steam\steamapps\common\007 First Light\Retail):

Retail\
├─ 007FirstLight.exe
├─ <asi-loader>.dll                  (e.g. bink2w64.dll / dinput8.dll)
├─ 007FirstLightUltrawide.asi        ← this patch
└─ 007FirstLightUltrawide.ini        ← your settings

Some loader configs look for .asi files in a scripts\ or plugins\ subfolder instead — if so, put the .asi (and .ini) there.

Configure

Out of the box it's zero-configAspectRatio = auto follows the game's live render resolution, so the ratio is always right even if you change resolution or resize the window. You only touch the ini to force a fixed ratio or override:

[General]
Enabled = 1          ; 0/false/no/off = ASI loads but does nothing

[Aspect]
AspectRatio = auto   ; (default) follow the game's live resolution automatically
;AspectRatio = 2.388889   ; or force an exact ratio (overrides everything)
;Width  = 3440            ; or an explicit resolution override (needs both)
;Height = 1440

[Advanced]
SearchAspect = 0     ; 0 = search for the built-in 16:9 value (normal)

[Video]
KbdToggleKey          = VK_F10               ; keyboard key — flip ultrawide <-> 16:9 for FMV
GamepadToggleKey      = XINPUT_GAMEPAD_BACK  ; Xbox-style pad button (XInput) — does the same
DualSenseToggleButton = CREATE               ; DualSense (PS5) button, read natively over HID

Auto mode reads the game window's current size every poll (through Windows — no game-memory addresses) and recomputes the ratio, so resizes and resolution changes are tracked live, and it keeps working across game updates with zero maintenance. If the window can't be read yet it falls back to the monitor's size; if neither is available it fails safe (renders the game unmodified). To force a fixed ratio instead, set AspectRatio to a number, or set both Width and Height.

Enabled = 0 is a quick way to turn the fix off without removing any files (handy for A/B testing or if a future update breaks it).

[Advanced] SearchAspect controls the value the patch searches for in memory (as opposed to [Aspect], which is the value it writes). Leave it at 0 for normal use — that means "look for the game's stock 16:9 constant". You'd only ever set it if a game update changed that stored value (see Troubleshooting).

Normal use: leave it at 0. Zero means "use the built-in 16:9 value (1.777778 → 39 8E E3 3F)." That's the value the game ships with, so you never touch this for a working patch.

When you'd change it: only if a game update relocates and changes the locked ratio so the scan reports Occurrences patched: 0. At that point the game is no longer storing 16:9, so searching for 16:9 finds nothing. You point SearchAspect at the new locked ratio instead — and because it's in the ini, you do it without recompiling.

SearchAspect float search bytes
0 (default) 1.777778 (16:9) 39 8E E3 3F
1.85 1.850000 CD CC EC 3F
2.0 2.000000 00 00 00 40
2.39 2.390000 C3 F5 18 40

Verify

Launch the game and play a cutscene — it should fill the screen. A log file 007FirstLightUltrawide.log is written next to the .asi; open it to confirm. A good run looks like:

Replace bytes: 8E E3 18 40 (2.388889)
Patched occurrence #1 at 00007FF6...
Patched occurrence #2 at 00007FF6...
Finished. Occurrences patched: 2

You may also see one or more No matches yet (attempt N/20); retrying... lines before the patch succeeds — that's normal when the engine hasn't finished mapping the constant yet, and the retry loop simply waits for it.

Pre-rendered (FMV) video cutscenes

The main patch fixes real-time, engine-rendered cinematics — the engine just renders more width. Some scenes, though, are pre-rendered video files baked at 16:9 (in this game they're packed inside the Glacier .rpkg archives and played by CRI MovieCRI Movie/PCx64 Ver.4.13.18, statically linked into 007FirstLight.exe, so there is no separate video-codec DLL). A 16:9 video has no pixels for the ultrawide sides, so nothing can fill them with correct image — the only clean option is to show the video centered at 16:9 while it plays, then return to ultrawide.

Reliably auto-detecting when one of these is playing turned out to need a disassembler — the CRI Movie player is statically linked with no usable hook point, and the memory flags an earlier version polled were brittle and broke on game updates. So instead of guessing, this patch gives you a manual hotkey.

The hotkey

Press F10 (keyboard), your Xbox-style pad's View/Back button, or your DualSense's Create button (the defaults) once when an FMV starts — the aspect flips to the engine default 16:9 so the video is centered with side bars instead of stretched. Press it again when the video ends to snap back to ultrawide. The switch is instant: the engine re-reads the aspect every frame, so flipping the patched value takes effect immediately (the same reason live AspectRatio = auto resolution tracking works). Launch state is ultrawide.

[Video]
KbdToggleKey          = VK_F10               ; keyboard key (read via GetAsyncKeyState)
GamepadToggleKey      = XINPUT_GAMEPAD_BACK  ; Xbox-style pad button (read via XInput)
DualSenseToggleButton = CREATE               ; DualSense (PS5) button (read natively over HID)
PollIntervalMs        = 50                   ; how often the toggles + live resolution are polled

All three inputs work at once — any one toggles. The keyboard key is a Windows virtual-key name: e.g. VK_F10, VK_PAUSE, VK_HOME (the VK_ prefix is optional; a numeric code like 0x79 / 121 also works) — see Microsoft's virtual-key codes. The Xbox-style button is named by its XInput identifier: XINPUT_GAMEPAD_BACK (the default — the "View"/Back button), XINPUT_GAMEPAD_START, XINPUT_GAMEPAD_A, XINPUT_GAMEPAD_LEFT_SHOULDER, … (the XINPUT_GAMEPAD_ prefix is optional, so BACK works too; the two analog triggers are XINPUT_GAMEPAD_LEFT_TRIGGER / _RIGHT_TRIGGER). The DualSense button is named the PlayStation way: CREATE (the default), OPTIONS, PS, CROSS, CIRCLE, SQUARE, TRIANGLE, L1/R1/L2/R2, L3/R3, TOUCHPAD, MUTE (SHARE is an alias for CREATE). Names are case-insensitive, and 0 disables that input.

The toggle only fires while the game window is focused, so the same key/button pressed in another app after alt-tabbing (or while the Steam overlay is up) won't flip the aspect underneath the game.

How DualSense and Xbox pads are read

A DualSense is read directly over HID (USB or Bluetooth), so its Create button works even when the pad is connected natively — where XInput, an Xbox-only API, can't see it. The DualSense and DualSense Edge are both recognized, and the pad is detected on hot-plug. An Xbox-style pad — or a DualSense routed through Steam Input / DS4Windows, which makes it look like one — is read through XInput instead, and Steam Input maps the DualSense's Create button to XINPUT_GAMEPAD_BACK. So Create is the default toggle whether the pad is native or routed through Steam. The log records every toggle (Hotkey: aspect toggled -> …), each apply (Aspect applied -> …), the configured inputs, and when a DualSense connects, so you can confirm it's working.

Why not automatic? Found two memory addresses to detect playback, but those addresses move on game updates and the signal was ambiguous. A frame-accurate automatic detector means locating the CRI player's functions (criMvPly_Start/_Stop/_GetStatus) in Ghidra/IDA and hooking them with MinHook — which is a much larger job. The manual toggle is deterministic, needs no addresses, and can't break on an update. That reverse-engineering design and insane ramblings are preserved in docs/cri-movie-detection.md, which I may work on in time.

Troubleshooting

  • Occurrences patched: 0 — the scan locates the value wherever an update moves it, so this almost always means the value itself changed (a different baked constant, or stored/computed differently). Re-derive the 16:9 search bytes in HxD, convert them to a decimal aspect (or just use the new ratio), and set [Advanced] SearchAspect in the ini — no recompile needed. Timing is rarely the cause now: the patch re-scans automatically (20 attempts, 500 ms apart, ~10 s total), so it still lands even if the engine is slow to map the constant.
  • Log file never appears — the ASI isn't being loaded. Check that the loader proxy dll name is one the game actually imports, and that you used the x64 loader. Some titles need the proxy in the same folder as the exe.
  • Cutscenes stretch instead of crop — that's the engine's choice for how it fits the wider ratio; this fix changes the ratio it targets, not the fit mode.
  • Want to disable it — set Enabled = 0 in the ini, or just remove the .asi (or the loader dll). Nothing on disk in the game files was changed.

Rebuilding from source

Requires CMake (3.15+) and a C++ compiler — either Visual Studio Build Tools (MSVC) or MinGW/GCC. The CMakeLists.txt works with both, and it generates 007FirstLightUltrawide.ini automatically next to the built .asi (the template lives inside CMakeLists.txt, so there's no separate file to copy). The project targets 64-bit only.

On Windows with Visual Studio Build Tools

Do not pass a toolchain file (those are for Linux cross-compiling).

cmake -B build
cmake --build build --config Release

Output: build\Release\007FirstLightUltrawide.asi (with the .ini beside it). MSVC is multi-config, so the --config Release is what selects the optimized build — there's no -DCMAKE_BUILD_TYPE step.

On Windows with MinGW (e.g. an MSYS2 MINGW64 shell)

Pick a non-Visual-Studio generator so CMake uses your MinGW gcc. No toolchain file is needed — your shell's environment already provides the right compiler.

cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release    # or -G "MinGW Makefiles"
cmake --build build

Output: build/007FirstLightUltrawide.asi (with the .ini beside it).

Cross-compiling from Linux

This is the only situation where the toolchain file applies:

cmake -B build -DCMAKE_TOOLCHAIN_FILE=cmake/toolchain-mingw-x64.cmake
cmake --build build

Versioning a local build

The build accepts the semantic version as a cache variable; it is compiled into the .asi (file-version resource + the .log header). Omit it and you get 0.0.0 / dev:

cmake -B build -DPROJECT_VERSION=1.4.0

Releases (CI)

Pushing to main runs .github/workflows/release.yml, which builds an MSVC x64 Release and publishes a GitHub Release. The version is derived automatically from the commit history using Conventional Commits:

Commit prefix Example Bump
fix: fix: handle 0-width ini value patch — 1.2.3 → 1.2.4
feat: feat: ini-configurable retry budget minor — 1.2.3 → 1.3.0
feat!: / BREAKING CHANGE: feat!: drop 32-bit support major — 1.2.3 → 2.0.0

Commits that aren't fix/feat/breaking (e.g. docs:, chore:) build but publish no release. Each release attaches 007FirstLightUltrawide-vX.Y.Z.zip (the .asi + generated .ini), and that version is the one embedded in the binary.

Seeding or forcing a version. Commit-based bumping needs a prior version tag to count from, so the first release (and any time you want a specific version) is cut by pushing a tag:

git tag -a v1.0.0 -m "Release v1.0.0"
git push origin v1.0.0

The workflow builds that exact version and publishes it. After a version tag exists, pushes to main take over and auto-increment from it.

Release notes are generated automatically by git-cliff (cliff.toml), which groups the Conventional Commits in the release into sections — Features, Bug Fixes, Documentation, CI/CD, and so on — so every release ships a detailed, categorized changelog.

For releases to publish, the repo's Settings → Actions → General → Workflow permissions must be set to Read and write (lets the default GITHUB_TOKEN create tags and releases).

Contributing / linting

Two layers keep the code consistent:

  • Locally — pre-commit. Fast format + hygiene checks, plus a commit-message check that enforces the Conventional Commits the release workflow depends on. One-time setup:

    pip install pre-commit
    pre-commit install            # installs the pre-commit and commit-msg hooks
    pre-commit run --all-files    # optional: lint the whole tree now

    Hooks: clang-format (shares .clang-format), whitespace/EOF/line-ending fixers, and conventional-pre-commit on the commit message. clang-tidy is not run here (it needs a build database) — CI handles it.

  • In CI — cpp-linter. .github/workflows/cpp-linter.yml runs clang-format and clang-tidy (using .clang-format / .clang-tidy) on every PR and push to main, annotating findings inline. It configures an MSVC build first so clang-tidy can resolve windows.h.

Style/check config lives in .clang-format and .clang-tidy at the repo root; both the local hooks and CI read the same files, so results match.

Notes

  • This patches every occurrence of the 39 8E E3 3F byte pattern in the main module. If the engine happens to use that same float for something unrelated, it would be changed too.

About

Ultrawide cutscene fix for 007 First Light — an ASI that rewrites the 16:9 aspect ratio in memory at runtime.

Topics

Resources

License

Stars

Watchers

Forks

Contributors