Skip to content

Latest commit

 

History

22 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ZoomHUD — Diablo III camera zoom controller

NOTHING IS INJECTED INTO DIABLO III

No DLL. No code of ours running inside the game. No hooks into the game process.

Verified, not claimed: Diablo III loads 120 modules with this running — the same count as before it existed. The only non-Windows, non-game modules present are Defender's AMSI provider and YoloMouse, both of which were already there.

What it uses, and nothing more:

OpenProcess a handle with read/write access
ReadProcessMemory walks a pointer chain from outside
WriteProcessMemory writes 4 bytes — one float — nothing else
PostMessage one synthetic Z press while locating the field

Explicitly not used: CreateRemoteThread, LoadLibrary, remote SetWindowsHookEx, thread hijacking, IAT/inline patching, or any modification of the game's code.

The one hook that exists is for YOUR HOTKEYS ONLY, and it is not in the game. WH_KEYBOARD_LL is the single hook type Windows does not inject — it is called back inside ZoomHUD's own process, unlike WH_KEYBOARD or WH_GETMESSAGE which map a DLL into every process. It exists solely so a bare - can be a hotkey without being swallowed system-wide. It never touches Diablo III.

Practical consequence: our code cannot crash your game, because none of it runs there.

Zooms the Diablo III camera further out than the game allows, via hotkeys.

Defaults — single keys, no modifier needed:

-              zoom further out
+              zoom back in
NumPad 0       reset to default
Ctrl+Alt+R     reload (re-find the zoom field)
Ctrl+Alt+End   quit

There is also a reload button on the window. Use it after a game reload or character swap if the zoom stops responding: the tool re-finds the field automatically when the value stops reading plausibly, but the old address can sometimes still hold a believable number, in which case that check does not trip. Reload forces it. No restart, no recalibration.

Run ZoomHUD.bat (or ZoomHUD.exe) any time — before or after the game. It waits for Diablo III, finds the zoom field on its own, and re-finds it whenever the game rebuilds it.

Changing the keys

Edit ZoomHUD.ini, created next to the exe on first run:

zoom_out   = -
zoom_in    = +
zoom_reset = num0
reload     = ctrl+alt+R
quit       = ctrl+alt+End

step = 0.5          # change per keypress
min  = -6.0         # clamp; past about -3 the camera clips terrain
max  = 0.5

swallow      = 1    # 1 = the game never sees the keypress
only_in_game = 1    # 1 = only react while Diablo III is focused

Names: - + [ ] , . num- num+ num0 F1–F12 PgUp PgDn Home End Insert Delete A–Z D0–D9, optionally prefixed with ctrl+, alt+, shift+.

Single keys are safe here. Input is read through a low-level keyboard hook rather than RegisterHotKey, which matters: a bare hotkey registered the normal way is swallowed system-wide, so binding - would stop you typing a minus sign in any other application. The hook only reacts while Diablo III is the focused window (only_in_game = 1) and passes every other keystroke straight through. swallow = 1 additionally hides the key from the game itself, so a binding that clashes with a game control causes no double action.

Rebuild from source with powershell -ExecutionPolicy Bypass -File Build.ps1. Requires only the .NET Framework, which is already on every Windows machine.


What the zoom actually is

A single 32-bit float inside a per-character object on the heap.

value meaning
0.0 default view (what you see normally)
~0.5 the zoomed-in view the Z key toggles to
negative further out than the game can reach

Two consequences follow, and both are easy to get backwards:

  • 0.5 is not "half zoomed out." Values between 0.0 and 0.5 sit between the two views the game already gives you. Anything in that range moves the camera in, not out.
  • Going further out means going below 0.0. The parameter extrapolates cleanly past its normal range: -1.0 puts the camera roughly twice as far beyond default as 0.5 puts it in the other direction. Useful values run from about -0.5 to -3.0; beyond that the camera can clip through terrain.

Z does not scale a number — it switches the camera between two states, and this float is the parameter that selects between them. Writing a value the keybind can never produce is what puts the camera somewhere the game has no control for.

The value is not saved anywhere. It is not in D3Prefs.txt (which has no zoom key at all) and does not survive a client restart. It is pure runtime state, which is why a resident tool is needed rather than a config edit.


Finding it: why the obvious approaches fail

The field cannot be found by scanning for values, and understanding why is what makes the working method reliable.

Scanning for a value that changes when you press Z yields millions of hits, and narrowing it produces the wrong survivors. A running game mutates enormous amounts of memory every frame, and among the survivors are matrix elements, animation timers, and UI fade alphas. Several of those are also floats that sit at exactly 0.0 and 1.0, alternate in lockstep with any UI transition, and are indistinguishable from the zoom by value alone.

Two specific traps worth naming, because both look like success:

  • Identity-matrix elements. A 4×4 transform ends 0,0,0,1. That trailing 1 matches a "0 or 1 float" filter perfectly and rides every toggle, because the matrix is cleared and rebuilt during transitions. It is not a zoom value and writing to it does nothing.
  • UI alphas. Interface fades are normalised 0.0 → 1.0 floats that animate on exactly the same schedule as the camera transition.

Anything found by value also goes stale. The object lives on a heap the game recycles. Addresses die on client restart, on character swap, on zone change — and in practice within minutes of ordinary play. A remembered address is worthless; it must be re-derived every time.


Finding it: the method that works

Two filters, applied in order. Neither depends on any address.

1. Structural — find it by pointer shape, not by value

The field is reached by a fixed chain of pointers rooted in the game module's static writable data:

objMgr = *(an 8-byte-aligned pointer in the module's writable .data)
unk    = *(objMgr + BASE_OFF)
node   = *(unk    + 0x48)
zoom   =  (float)(node + 0x18)

Walking that shape is a far stronger filter than any value test, and the search space is tiny — the module's writable .data is about 4.6 MB, versus the 2.2 GB of heap a value scan has to cover. The whole walk takes ~1.5 seconds.

BASE_OFF is the only part that moves between game versions (the containing structure grows as fields are added), so it is swept over 0x800…0xE00 in 8-byte steps rather than hardcoded. The +0x48 and +0x18 hops have been stable and are used directly.

2. Behavioural — prove it by reproducibility

The structural filter still returns thousands of chains. The one that matters is identified by a property nothing else has:

Press Z five times, reading every candidate each time. Keep only addresses that return to the exact same value every time a zoom state repeats, and hold a different exact value in the other state.

This is decisive because it tests reproducibility rather than change. Heap churn and animation can change on cue by coincidence; they cannot reproduce a specific value on command, repeatedly, in alternation. A free-running counter fails immediately — it never returns to a previous value.

The surviving pair should read 0.0 in one state and a small positive value in the other. That is the zoom.

3. Confirming without a human — the screen test

When even that is ambiguous (or after a patch, when the offsets above may be wrong), there is a filter that depends on nothing version-specific:

Write an extrapolated value, screenshot the game window, and compare against the frame-to-frame noise measured while idle. Only the real zoom changes essentially every pixel.

Ambient animation — monsters, spell effects, torchlight — moves a few percent of the frame. A camera move changes all of it. Measuring the idle noise floor first and requiring ~2.5× that separates them cleanly. In practice the zoom scores around 48 against a noise floor near 8.

This is what makes the tool patch-proof in principle: the test is literally "did the picture change", which no game update can invalidate.


Beating version changes

When a patch breaks it, the failure will be BASE_OFF moving. In order:

  1. Widen the sweep. 0x600…0x1200 instead of 0x800…0xE00. Cheap; usually enough, since the structure only grows.
  2. Sweep the other hops. If that fails, +0x48 and +0x18 have moved too. Sweep +0x00…0x90 and +0x00…0x40 respectively. Slower, still bounded.
  3. Drop the chain entirely. Use the value-alternation filter across all of memory to get candidates, then confirm each with the screen test. Slower (minutes rather than seconds) but assumes nothing — no offsets, no addresses, no expected values. This is the fallback that always works.
  4. Read the new offsets back out. Once step 3 finds the address, walk pointers backwards to recover the new BASE_OFF and restore the fast path.

The two assumptions that would genuinely break the design: the zoom ceasing to be a 4-byte float, or ceasing to have two discrete states. Neither has any reason to change.


Version-specific constants

Everything here is derived at runtime — nothing below is compiled in as an absolute address.

module            Diablo III64.exe        base resolved at runtime (ASLR)
BASE_OFF          swept 0x800..0xE00      the offset that moves between patches
hop 1             +0x48                   stable so far
hop 2             +0x18                   stable so far
default state      0.0
toggled state     ~0.5
useful range      -0.5 .. -3.0            beyond that the camera clips terrain
verified against  2.8.0.99920

Design notes

Nothing is injected into Diablo III. The tool reads and writes the game's memory from outside via ReadProcessMemory / WriteProcessMemory. No DLL is loaded into the game, and its module list is unchanged while this runs. Beyond being less conspicuous, it means a bug here crashes this tool rather than the game.

The address is never remembered. It is re-derived on every launch and again whenever the current one stops reading a plausible value. That is not a fallback path — it is the normal mode of operation, because the object is rebuilt constantly.

The value usually sticks once written. On this build the game does not continuously reset the field, so a single write holds. The tool re-checks every 2 seconds anyway and rewrites if something has reset it, which also covers the case where the object was rebuilt underneath.

Locating presses Z a few times. That is the verification step toggling the zoom to observe both states. It restores the level afterwards. If the camera flicks in and out for a few seconds on startup, that is the tool identifying itself, not a fault.


Folder layout

ZoomHUD\
  ZoomHUD.exe          the app
  ZoomHUD.bat          launcher
  ZoomHUD.ini          your key bindings (created on first run, fully commented)
  README.md            this file
  Build.ps1            rebuilds the exe from source
  Clean.ps1            removes run artefacts; -Scrub also redacts identifiers

  src\
    ZoomHUD.cs         complete source, ~700 lines, single file

  tools\               PowerShell versions and the research instruments
    Zoom.ps1             locate, apply, hold
    Zoom-Auto.ps1        version-independent fallback: no hardcoded offsets
    Zoom-Daemon.ps1      background watcher, survives restarts
    Scan-Chain.ps1       the pointer-chain walk on its own
    Screen-Probe.ps1     the screenshot confirmation test
    Scan-Value.ps1       general differential memory scanner
    Input.ps1            sends keys to the game window
    Freeze-Many.ps1      hold a set of addresses at a value
    Find-Camera.ps1      locate camera structs by signature
    Context.ps1          classify an address by what surrounds it
    Find-Strings.ps1     search the module for strings
    Scan-Rdata.ps1       search read-only constants
    Peek.ps1             read/write/watch fixed addresses
    Watch-Attach.ps1     which processes hold handles to the game
    Trace-Write.ps1      hardware write-breakpoint via cdb

  docs\
    AUDIT.md           full investigation record
    ARCHITECTURE.md    the two builds compared: stacks, footprint, hook latency
    APPROACH.md        hardcoded vs derived: why nothing here stores an address
    GR-TOOLS.md        the TurboHUD automation plugins: how they act, what breaks them

Everything in tools\ must stay in the same directory — the scripts invoke each other by relative path.

If the app stops working after a patch

cd tools
.\Zoom-Auto.ps1          # assumes nothing; finds it from scratch

That path hardcodes no offsets and no values — it learns the states by observation and confirms by screenshot. Slower (minutes rather than seconds), but it is the one that keeps working when the fast path breaks. Once it reports an address, the new offsets can be read back out to repair the fast path.

docs\AUDIT.md records ten phases of investigation, most of it negative results — the approaches that looked right and were not. Worth reading before changing the method, since it is built specifically to avoid repeating them.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages