Skip to content

feat: handle Nexus resumable-download prompt in the Wabbajack flow#20

Open
sndnz wants to merge 14 commits into
jaylann:mainfrom
sndnz:feat/robust-detection-any-setup
Open

feat: handle Nexus resumable-download prompt in the Wabbajack flow#20
sndnz wants to merge 14 commits into
jaylann:mainfrom
sndnz:feat/robust-detection-any-setup

Conversation

@sndnz

@sndnz sndnz commented Jul 14, 2026

Copy link
Copy Markdown

Summary

Adds handling for Nexus Mods' beta resumable-downloads prompt in the non-Vortex (browser / Wabbajack) flow.

For files over 500MB, Nexus now interrupts a manual download with a modal offering Standard download vs Resumable download. The resumable option streams via the browser File System API (a save-file dialog) that Wabbajack and browser download watchers cannot intercept, which stalls automation. The scanner now detects this modal and clicks Standard download first — ahead of the Website/Wabbajack button probes — to clear it and keep the normal download flow working.

Changes

  • New ButtonType.STANDARD_DOWNLOAD wired through the detector (optional StandardDownloadButton.png template), the scanner state machine, validate.py, the synthetic fixture generator, and the test-harness gating.
  • The prompt is probed only in the non-Vortex path; the Vortex flow is unchanged (covered by a regression test).
  • Graceful degradation: with no template asset the feature is a no-op, following the repo's existing optional-asset pattern (*New.png). This PR ships a captured assets/StandardDownloadButton.png.
  • Regenerated the deterministic detection manifest so the new button is covered across scales, degradations, occlusion, busy-UI, clutter, and multi-instance scenes.
  • Fixed a fixture-generator filename parser that assumed single-token button values (it broke on the underscore in standard_download).

Testing

  • python validate.py → passes
  • pytest → 282 passed, 26 skipped
  • ruff check . and ruff format --check . → clean
  • python detection_report.py --fail-under 0.999 → score 1.000 (standard_download 19/19 present detected, 0 false positives)

Warp conversation

Co-Authored-By: Oz oz-agent@warp.dev

jaylann and others added 14 commits July 11, 2026 12:42
… gray images

Extend TemplateCandidate to carry template keypoints, grayscale image and
dimensions; build the per-button-type candidate list once at init instead
of rebuilding it on every detect() call. Drops the now-unused descriptor
fields from ButtonAssets. No behavior change.
Replace the keypoint-centroid click point with a geometrically verified
fit: estimateAffinePartial2D over template->scene keypoint pairs, gated
on inlier count, inlier ratio, plausible scale (0.5-2.0) and rotation
(<=5 deg). The click point is the template center projected through the
transform (sub-pixel accurate: mean fixture error drops 4.5px -> 0.1px).
Confidence now blends inlier count and inlier ratio. DetectionResult
gains scale/inliers/method fields; num_matches reports inliers.
When the SIFT path finds nothing (small or low-feature templates, sub-1.0
DPI scales that starve keypoint detection), fall back to grayscale
TM_CCOEFF_NORMED over a geometric scale ladder (0.60-2.00, ~9% steps),
accepting peaks >= 0.80. Debug frames now draw the detection box at the
recovered template scale.
A geometrically coherent fit can still be a shared text fragment inside a
different button (the 'Slow download' template matches the 'download' word
in 'Download manually'). Verify every accepted fit with NCC between the
predicted scene region and the template at the recovered scale: fragment
matches score ~0.5, true matches >=0.8, threshold 0.60. Also lower the
fallback TM threshold to 0.75 -- downscaled thin-text templates bottom out
around 0.78 from resampling blur while cross-template peaks stay below 0.5.
…d sub-1.0 scales

- scales 0.75/0.85 added (exercises the NCC fallback on SIFT-starved
  templates), tolerances tightened to 10px clean / 15px degraded
  (projected centers are sub-pixel; the old 0.5*max(w,h)+15 hid drift)
- cross-template negatives per scale-1.0 frame, including the documented
  CLICK->VORTEX false-positive case previously omitted because main fails it
- Gaussian-noise (seeded) and JPEG-70 degraded variants, clutter scenes
  with two distractor buttons
- WABBAJACK and WEBSITE templates depict the same 'Slow download' button
  at different sizes, so negatives never pair them

Manifest grows 19 -> 68 cases. Scores: this branch 1.000 (48/48, 0 FP,
mean error 0.4px), main 0.717 on the same suite (42/48, 9 FP, 4.3px).
utils/dpi.py declares per-monitor DPI awareness at startup (v2 -> shcore
-> system fallback chain) so win32 reports/accepts the same physical
pixels mss captures; without it clicks are off by the display scale
factor on any monitor scaled != 100%.

services/geometry.py adds pure, win32-free layout math: MonitorFrame
(per-monitor image<->virtual conversion, primary = origin invariant),
VirtualDesktop (frame lookup, click clamping with dead-zone refusal,
win32-vs-mss layout agreement check that catches failed DPI awareness)
and compute_search_bbox (clips window rects to the frame and applies a
fixed fractional inset). Drops the unused Monitor.is_negative.
Replace random cursor/window-title mocks with a configurable
MockDisplayState (monitor layout, window rects, cursor, recorded events)
so coordinate tests can assert exact behaviour. utils/platform gains a
send_input_mouse shim: real ctypes SendInput on Windows (raising on
blocked input so callers can fall back to mouse_event), state-recording
mock elsewhere.
ScreenCapture now grabs each monitor separately, pairing every image with
its MonitorFrame geometry, so coordinate conversion uses the correct
per-monitor offset (exact on negative origins, mixed DPI, portrait and
non-contiguous layouts). Deletes the single-offset img<->monitor helpers
and the L1-nearest win32<->mss matching heuristic; mss geometry is
authoritative and win32 rects are only cross-checked for DPI sanity.
mss_factory injection + FakeMSS enable layout tests on any platform.
- ClickController takes the monitor layout, clamps click targets onto the
  nearest monitor (refusing dead-zone targets) and sends button events via
  SendInput with a mouse_event fallback; click() reports success so the
  scanner only counts real clicks
- WindowManager places windows by geometry (primary = origin invariant,
  Vortex on the first secondary) instead of assuming EnumDisplayMonitors
  ordering; get_all_monitors ensures DPI awareness first and sorts primary
  first
- Scanner iterates per-monitor frames: Vortex search runs only on the
  frame hosting the window (via compute_search_bbox, replacing the
  window-position-dependent 'fac' padding formula), web/dialog handlers
  sweep every frame, detections convert to virtual-desktop coordinates
  with the owning frame's offset
- main() declares DPI awareness before anything touches win32/mss

Adds scanner-level coordinate integration tests over a monitor left of
the primary.
…line

Both Windows fixture tools now declare DPI awareness first and work per
monitor frame: ground truth is frame-relative (fx*width, fy*height) --
trivially exact since capture is per-frame -- and case meta records the
frame geometry, full virtual layout, DPI awareness level and orientation.
Drops the win32<->mss nearest-bounds heuristic copy. README's DPI
compatibility-override workaround is obsolete (the app sets per-monitor
awareness itself) and is replaced by a note on the logged awareness level.
Fixture families now cover: scale sweep 0.65-2.0 at varied frame positions,
five degradations (noise/jpeg/brightness/contrast/blur), cursor occlusion,
feature-rich backgrounds (synthetic UI panels, block noise), clutter,
multi-instance frames (nearest ground-truth point wins via new 'points'
support), a mega frame with all templates, and absent guards: empty, busy
button-free scenes, cross-template negatives, and lookalike buttons whose
text differs.

Detector hardening driven by the new adversarial scenes:
- NCC fallback ladder capped at 1.1x (it exists to rescue SIFT-starved
  DOWN-scales; wide ladders let generic rect+text UI widgets impersonate
  small templates at inflated scales)
- layered fallback acceptance: intensity peaks >=0.90 pass; 0.80-0.90
  band peaks must also match the template's blurred-gradient signature
  (>=0.80) at the peak -- band FPs score <=0.71, true positives >=0.91
- SIFT scale gate widened to 0.4-2.2 so 2.0x buttons stay on the SIFT path

detection_report.py grows per-variant breakdown, per-detect latency and a
--fail-under gate; synthetic PNGs (15MB) are no longer committed -- both
pytest and the report regenerate them deterministically when absent.

Scores on the 188-case suite: this branch 1.000 (120/120, 0 FP across 68
negatives, 0.3px mean error), main 0.665 (97/120, 20 FP).
…32 services

- ButtonDetector: transform validation windows (scale/rotation bounds,
  inclusive edges), projected-center math, bbox crop offset round-trip,
  method/inliers/scale reporting on both paths, legacy-template mode,
  detect_multiple, hallucination guard
- Scanner state machine with a stubbed detector: full vortex->web->dialog
  cycle, web retry-limit fallback to vortex search, missing-window wait
  (detector never consulted), legacy popup precedence, wabbajack-mode
  reset semantics, website-before-wabbajack ordering
- WindowManager against mock win32: vortex to first secondary, single
  monitor no-op, missing window no-op, title-substring placement, primary
  first in get_all_monitors regardless of enumeration order, off-Windows
  guard
- ClickController: SendInput-blocked fallback to mouse_event, double click
- DebugRecorder: annotated frame output, scaled box near frame edge
- Fixture generator invariants: deterministic records + byte-stable
  images, ground-truth points inside bounds, >=5 variant families per
  type, equivalence rule enforced on cross-template negatives, background
  keypoint-count contracts, position pool never clips the widest template

275 tests total (was 106 before this pass, 20 before the PR).
New detection-benchmark job (ubuntu + windows) regenerates the
deterministic fixture suite, runs detection_report.py --fail-under 0.999
(any single miss or false positive among the 188 cases drops the score
below the bar) and publishes the per-type/per-variant breakdown to the
job summary.
Nexus beta resumable-downloads interposes a Standard download vs Resumable download modal for files over 500MB. The resumable option streams via the File System API save dialog, which Wabbajack and browser download watchers cannot intercept, stalling automation. The scanner now detects the modal and clicks Standard download first in the non-Vortex (browser/Wabbajack) path to clear it.

Driven by an optional StandardDownloadButton.png template so it degrades gracefully when the asset is absent. Adds the STANDARD_DOWNLOAD button type through the detector, scanner, validation, synthetic fixtures and tests; regenerates the detection manifest (score holds at 1.000).

Co-Authored-By: Oz <oz-agent@warp.dev>
@sndnz
sndnz requested a review from jaylann as a code owner July 14, 2026 22:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants