A viewport zoom plugin for Command & Conquer: Yuri's Revenge. It magnifies the tactical view at the render level, keeps the point under the cursor fixed by driving the game camera, and un-magnifies coordinates exactly where the game turns screen positions into game space, so selection and clicks land on what is actually displayed.
Built on the Windows API with Syringe (the injection framework used by Ares, Phobos and other YR extensions) for injection and lifecycle management. Only code hooks — the game executable and its resources are never patched on disk.
- 🔍 Ctrl + mouse wheel zoom over the tactical view: gear 1.15 per step, range 1.0x–4.0x, smooth frame-independent interpolation
- 🎯 The point under the cursor stays fixed: the game camera moves with every zoom step so nothing slips away
- ↩️ Double press Ctrl (within 400 ms) resets the zoom to 1.0x; the accumulated camera offset is undone automatically whenever the zoom returns to 1.0x
- 🧭 Arrow keys pan the view while zoomed
- 🖼 Render-level magnification: the view rect is backed up and upscaled (nearest neighbour) over the frame the game has already drawn
- 🧰 Only the tactical view is magnified — sidebar, HUD and the physical cursor are never remapped
- 🧮 Coordinates are transformed only where the game converts them (tactical clicks, selection rubber band, right drag)
- 📦 No GScript dependency: the zoom factor lives entirely in ViewCtrl — and when a mod ships its own scaler, ViewCtrl yields to it instead of competing (see Status)
- ⚡ Minimal performance overhead
- 🧪 Unit tests: 89 test cases / 955 assertions —
tests\bin\ViewCtrlTests.exe - 📊 Line coverage: 100% of
src/— 1495 of 1495 lines (OpenCppCoverage 0.9.9 over the doctest suite; the production-only branches — TextDefer's real dispatch, clamp-state diagnostics, OS-failure defenses — are reached through test seams) ⚠️ Competing scalers auto-detected (see docs/conflict-detection.md): when the mod scales the view itself —GScript.ext(e.g. Tiberium Crisis), Telescope v1.4, Kaleidoscope, MOExperience or Ra2.Vampire.Zoom —GameIntnotices and keeps ViewCtrl inert. Detection is layered: L1 module file name, L2.syhks00hook-table fingerprint, L3 runtime byte sniff for foreignE9patches, L4 reactive view-size watch, L5 scaler-specific export probe — a renamed or runtime-patched competitor is still caught- 🔬 Static audit: every hook site was verified against the YR Hook Encyclopedia — Syringe-stub safe (no relative branches inside the stolen windows, resume addresses on instruction boundaries), no registry conflicts; the bootstrap hook
0x52CAE9is shared with Ares (_YR_PostGameInit) and composes with it - 🖥 Render order verified by disassembly: magnification runs after the game draws into
DSurface::Composite, before the cursor blit and beforeFrame::Present— the magnified pixels are what reaches the screen - 🛡 SEH-guarded hooks: every hook body is wrapped in
__try/__exceptso a fault on malformed game memory is swallowed and the original instruction bytes are replayed instead of crashing the game; guards are compiled out of the test build so tests still catch real bugs - 🎮 Verified in-game on Yuri's Revenge 1.001 (modded install, 1920x1080): zoom range and anchor, click/selection transforms, reset and undo
- OS: Windows 7 or later (32-bit process)
- Game version: Yuri's Revenge 1.001
- Runtime: Syringe injection framework to load the DLL
- Make sure the game runs through Syringe (
Syringe.exeor a mod launcher based on it) - Place the compiled DLL in the game root directory
- Register the DLL in your Syringe loader configuration
- Launch the game — Release builds run silently and write no files by default; to get a diagnostic log without rebuilding, set the environment variable
VIEWCTRL_LOG=1before launching (the log goes to the debugger output / DebugView). A Debug build always writesViewCtrl.lognext to the game executable
YRpp (the Syringe headers) is a git submodule, so clone recursively:
git clone --recurse-submodules https://github.com/YoVVassup/RA2YR-Zooming.gitIf you already have a clone, initialise the submodule once:
git submodule update --initThen build Release|Win32 (v143 toolset):
msbuild viewctrl.vcxproj /p:Configuration=Release /p:Platform=Win32Tests live in tests\test.vcxproj and build as Debug|Win32 only:
msbuild tests\test.vcxproj /p:Configuration=Debug /p:Platform=Win32
tests\bin\ViewCtrlTests.exe| Action | Effect |
|---|---|
| Ctrl + scroll wheel up/down | Zoom in / out (x1.15 per step, clamped to 1.0x–4.0x) |
| Double press Ctrl (within 400 ms) | Reset the zoom to 1.0x and undo the camera offset |
| Arrow keys (while zoomed) | Pan the view |
Without Ctrl the wheel is passed to the game untouched. All zoom parameters are compile-time constants (src\Zoomer.hpp). To adjust them, modify the source code and recompile.
- Hooks only, never rewrites the executable — co-loadable with other Syringe DLLs (Ares, Phobos, ...); Syringe chains multiple DLLs at shared sites
0x52CAE9shares its 5 bytes with Ares_YR_PostGameInit; ViewCtrl runs the original bytes afterwards, so both handlers compose- Earlier builds could delegate the zoom factor to GScript; ViewCtrl no longer reads or writes GScript — if GScript's own zoom feature is active, one of the two is auto-disabled (see docs/conflict-detection.md)
- Display-layer scalers (cnc-ddraw forks, Vampire.Zoom) change the window size, not the view rect — they co-load, but the combined magnification is usually not what you want
- If you encounter compatibility issues with a specific mod, please open an Issue with details and the
ViewCtrl.logfrom a Debug build (Release builds compile file logging out entirely; enable transient logging withVIEWCTRL_LOG=1)
- Injection framework: Syringe
DEFINE_HOOK— 17 hooks, all addresses centralized insrc\GameAddrs.hppwith source annotations (YRpp / ReSource / Encyclopedia)
| Site | Bytes | Purpose |
|---|---|---|
0x52CAE9 |
5 | bootstrap after game init (InitGame) — Ares-shared, composes |
0x4F44AF |
5 | pre-render: substitute the view dimensions so the engine draws the small view; tick the zoom lerp |
0x4F451B |
5 | post-render: stretch the small view over the frame and restore the vanilla view dimensions |
0x6D864E / 0x6D868A |
6 | ClampCoordMap — view width/height reads clamped to the magnified source rect (falls back to the last good size) |
0x692325 |
6 | DisplayClass::ProcessClickCoords — tactical click transform |
0x6D9F80 / 0x6D9FC0 |
6 | selection rubber band start/end transform |
0x693791 |
6 | ScrollMapEdge — right-drag scroll speed transform |
0x6A71D0 … 0x6A7439 |
6 | BlitSidebar — five ViewBounds.W reads pinned to the real view width so the sidebar holds its place while the view is substituted |
0x6D4B50 |
6 | TacticalClass::DrawTimer — HUD timer draw deferred and replayed at native size after the stretch |
0x6D4E20 |
5 | TacticalClass::DrawScreenText — screen text deferred and replayed the same way |
0x4F44F9 |
12 | GScreenClass::Render — the DrawUI virtual call skipped as one window; the whole UI pass replays after the stretch |
- Render path (verified by disassembly):
MainLoop→GScreenClass::Render(0x4F4480) draws the frame — the substituted small view while zoomed — intoDSurface::Composite→ ViewCtrl stretches it up to the full view →wwmouse->DrawCursorBufferedblits the unscaled cursor → virtualRenderFrame→Frame::Present - Input: subclasses the game window procedure (
SetWindowLongPtrW); the wheel zooms only while Ctrl is held and the cursor is over the tactical view, everything else is forwarded to the game; the cursor position itself is never remapped - Camera: every zoom step moves the game camera (
TacticalMapClass::SetCameraPosition,0x6D6000) so the focused point stays under the cursor; the offset is undone at 1.0x - View rect:
DSurface::ViewBounds, polled at most every 250 ms on any frame whose dimensions are currently vanilla — including frames of a magnified session, so the first zoom of a session never renders against the raw client rect; the rect itself excludes the sidebar - Coordinate transforms: the points where the game turns screen positions into game space are un-magnified;
Zoomer::UnMagnifyis the exact inverse of the upscale the renderer draws - Robustness: every hook body is wrapped in SEH (
src\Seh.h) so a fault is swallowed and the original bytes are replayed; Release logging is off by default and can be toggled at runtime withVIEWCTRL_LOG=1
- docs/conflict-detection.md — how competing scalers are detected (L1–L5) and what is not a conflict
- docs/research-landscape.md — survey of every known YR zoom implementation and how ViewCtrl relates to each
Fork lineage
- This repository is a fork of ChoyoTsumu/RA2-about — the original viewport zoom concept and core implementation by ChoyuTsumu, assisted by Sovietianqi
- This fork: YoVVassup/RA2YR-Zooming — reworked as a render-level Syringe plugin (camera driving, coordinate transforms, unit tests, static hook audit); maintained by YoWassup
Acknowledgements — research material used to verify the logic
- YRpp — Syringe / C&C headers (git submodule of this project)
- RA2YR_ReSource — reverse-engineered
gamemd.exesymbol and class database used to name and cross-check addresses - YR Hook Encyclopedia — registry of hook sites from Ares, Phobos, Kratos and others, used for conflict checking and the Syringe-safety audit
- Ares — the Syringe injection framework and reference extension; shares our bootstrap hook
0x52CAE9 - Phobos, Kratos — hook-site cross-checks
Contributions and pull requests are welcome.
This project is licensed under the GNU General Public License v3.0. See LICENSE.txt for details.