Skip to content

About

Zooming for RA2YR

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

ViewCtrl — Zoom Plugin for C&C: Yuri's Revenge

English · Русский · 简体中文

License: GPL v3 Language Platform Game Framework Tests Fork of


🎮 Overview

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.


✨ Features

  • 🔍 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

✅ Status

  • 🧪 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 — GameInt notices and keeps ViewCtrl inert. Detection is layered: L1 module file name, L2 .syhks00 hook-table fingerprint, L3 runtime byte sniff for foreign E9 patches, 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 0x52CAE9 is 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 before Frame::Present — the magnified pixels are what reaches the screen
  • 🛡 SEH-guarded hooks: every hook body is wrapped in __try/__except so 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

💻 Requirements

  • OS: Windows 7 or later (32-bit process)
  • Game version: Yuri's Revenge 1.001
  • Runtime: Syringe injection framework to load the DLL

📥 Installation

  1. Make sure the game runs through Syringe (Syringe.exe or a mod launcher based on it)
  2. Place the compiled DLL in the game root directory
  3. Register the DLL in your Syringe loader configuration
  4. 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=1 before launching (the log goes to the debugger output / DebugView). A Debug build always writes ViewCtrl.log next to the game executable

🔨 Building

YRpp (the Syringe headers) is a git submodule, so clone recursively:

git clone --recurse-submodules https://github.com/YoVVassup/RA2YR-Zooming.git

If you already have a clone, initialise the submodule once:

git submodule update --init

Then build Release|Win32 (v143 toolset):

msbuild viewctrl.vcxproj /p:Configuration=Release /p:Platform=Win32

Tests 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

🎛 Controls

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.


🤝 Compatibility

  • Hooks only, never rewrites the executable — co-loadable with other Syringe DLLs (Ares, Phobos, ...); Syringe chains multiple DLLs at shared sites
  • 0x52CAE9 shares 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.log from a Debug build (Release builds compile file logging out entirely; enable transient logging with VIEWCTRL_LOG=1)

⚙️ Technical Implementation

  • Injection framework: Syringe DEFINE_HOOK — 17 hooks, all addresses centralized in src\GameAddrs.hpp with 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 — into DSurface::Composite → ViewCtrl stretches it up to the full view → wwmouse->DrawCursorBuffered blits the unscaled cursor → virtual RenderFrame → 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::UnMagnify is 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 with VIEWCTRL_LOG=1

📚 Documentation


🏅 Credits

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.exe symbol 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.


📜 License

This project is licensed under the GNU General Public License v3.0. See LICENSE.txt for details.

About

Zooming for RA2YR

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages