Releases: maschine34675/WebOverlay
Release list
v1.11.0 - answers the library used to keep to itself
Two consumer wishes from CombatLog, and two things found underneath them
while answering. Nothing changes for a mod that does not use the new members.
A mod can ask whether its message was taken. TryPost mirrors the three
Post overloads and answers whether the command entered the library's queue:
false for a disposed handle or a refusal, true otherwise - and true means the
queue, not delivery; the outbox, the target document and the browser still
stand between it and the page. One-shot state a page must not miss - a
finished report - can now be retried when the library itself declined it,
instead of being recorded as sent.
A mod can ask how a Show or Hide finished. Show(cb) and Hide(cb)
answer exactly once with a VisibilityOutcome: Applied, AlreadyThere,
RefusedFullscreen, Superseded, Failed, Disposed or QueueRefused.
The answer travels like a script result, so it survives a dispose and goes
quiet only during shutdown. Every consumer's first request waits for the
browser view and is answered before Ready, and there is deliberately no
order promise between the answer and the VisibilityChanged it caused.
VisibilityChanged itself is untouched; the deeper fix that suggests itself
- un-droppable visibility - is refuted on the record in the wishlist answers.
A flooding mod now costs itself. The 1.10.0 bound was one queue for every
mod in the process, so the first mod to fill it had every other mod's next
Show, Hide, LoadHtml or Post refused - and the comment beside it
claimed otherwise. Each overlay now has a share of 1,024 under the kept
ceiling of 4,096, obligations never count against a share, and the warning
names the overlay and its mod, at most once a minute. What a neighbour cannot
be spared is the wait behind the flooder's backlog: one queue, one thread,
about a tenth of a second at full rate. Latent rather than observed - every
shipping consumer throttles.
The exclusive-fullscreen refusal is no longer dead by construction. The
plugin read Screen.fullScreenMode from the overlay thread, where Unity's
binding is not thread-safe (get_width is marked thread-safe;
get_fullScreenMode is not), and the throw was swallowed as "supported". The
plugin's Update now caches the answer on the main thread. Reasoned from the
binding metadata rather than measured in the game, which apparently never
enters the mode.
Quieter on the way out. Closed no longer fires from a shutdown
teardown, Show/Hide and answers stay silent once shutdown began, and
main-thread dispatch tests shutdown before it tests for a pump. Also: a
Show in the moment between the window and its browser view no longer shows
a bare popup, and a Show on a failed overlay no longer arms the next
Toggle to hide a dead window.
Rows 79-92 in docs/FAULT-TESTS.md prove it (row 78 re-proven, row 21
extended), five of them by putting the fault back and watching the row fail;
the two limits - the fullscreen path and the bare-popup moment - are stated
in the rows rather than papered over. Consumers gate on 1.11.0 for the new
members; examples/WebOverlayGate.cs shows the shape.
v1.10.0 - what happens when something misbehaves
A hardening release: the five findings that had been carried through every
review since 1.8, closed. Nothing changes for a well-behaved mod - what
changes is what happens when something misbehaves.
Downloads are blocked by default. A page-initiated download is cancelled,
its UI suppressed, with one warning naming the URL. This is a deliberate
tightening of a security default: every page here is the mod's own, and a
page over a game has no business writing files to the player's disk. A mod
that genuinely wants files sets OverlayOptions.AllowDownloads. (On a
runtime from before 2021 there is no download control; downloads then stay
browser-managed, and the log says so.)
A mod finally learns when its channels are dead. If the channel shim
cannot be installed, ChannelsFailed fires - latched like Failed, so a
late subscription still hears it - and ChannelsAvailable answers false.
The overlay itself keeps working: the window, raw Post/MessageReceived
and scripts are untouched; what is dead is everything built on
window.overlay. Until now the only trace was a log line the mod never saw.
Navigation completions carry identity. They are matched by WebView2's
own NavigationId, on top of the positional guard rather than instead of it.
The defect this closes: a page-initiated navigation that the origin filter
refuses used to have its cancellation reported as the page still on screen
having failed to load - a false failure report about a healthy document.
A misbehaving consumer pays for itself. The overlay command queue is
bounded like the main-thread event queue; a mod posting in a hot loop gets
dropped commands and one warning, not every mod in the process an unbounded
heap. Obligations are never dropped: a Request or ExecuteScript refused
by a full queue is answered null immediately - "answered exactly once"
holds under flood - and disposals always run.
The bounds store honors its lock. A two-second timeout used to mean
writing without the mutex - the torn file it exists to prevent - and then
releasing a lock never taken. Now: one warning, no write, and the next save
works.
Rows 72-78 in docs/FAULT-TESTS.md prove all of it, each by putting the
fault back and watching the row fail - with the two limits stated in the
rows themselves rather than papered over.
Extract over the game root. Everything is additive; consumers gating on a
version want 1.10.0 for the three new members, and nothing existing moved.
v1.9.1 - a front door worth walking through
A documentation release. Nothing in the library's behaviour changed - the DLL
differs from 1.9.0 only in its version stamp and doc-comment metadata.
An outside assessment put it plainly: the hard parts of this documentation were
unusually deep and the easy parts were full of holes. The landing page read
like an API reference merged with an audit report, and the prominent example
produced six compiler errors as pasted. Both are fixed at the root.
The README is a landing page again - what the library is, the demo in
motion, a compatibility box, installation, a quickstart, and a map. The
reference moved next door and is better for it:
docs/API.md-
members, options, events, failure causes, channels, threading, orderingdocs/RECIPES.md-
HUDs and transparency, shaping, real files and web fonts, previewing without
the game, performance, security defaultsdocs/TROUBLESHOOTING.md-
by symptom, ending in what a useful bug report containsdocs/INTERNALS.md-
how it works and why it looks like this, plus the review history
The quickstart compiles as pasted, and stays that way. It is a complete
plugin - reference block, hotkey, failure handling, Dispose on shutdown -
and it is not trusted but extracted: tools/Check-Quickstart.ps1 compiles the
two marked README blocks verbatim against a real SPT installation, and the
release packaging refuses to package when they do not build.
IntelliSense. Anvil-WebOverlay.xml now ships next to the DLL, so
referencing the installed library gives the documentation comments in the
editor rather than on a website.
Every dated review report under docs/ now carries its own
historical-snapshot banner, because a reader deep-linked into one never passes
through the index that says so.
Extract over the game root, as always. Nothing a consumer gates on has moved:
1.8.8 remains the floor for click-through, 1.9.0 for VirtualHost.Access.
v1.9.0 - a host you can open, a page that speaks up
The fourth round of consumer requests, answered in code. Two additions, and a
handful of things that were true but had never been written down.
VirtualHost.Access decides how much of a mapped folder other origins in
the same overlay may read: DenyCors (the default, and what every mapping has
always had), Deny, or Allow.
It exists for web fonts. They are fetched in CORS mode by specification, so the
default refuses a face served from another host of yours - the page renders in a
fallback and nothing reports it. The only way out was one host for everything,
which for one mod meant mapping its whole plugin folder, DLL and settings
included, because splitting it would have cost the fonts.
The same trap catches a single-host mod too: a page handed over with LoadHtml
has an opaque origin, so it is cross-origin to every mapped host, including
its only one. Allow is not free - it is the folder answering
Access-Control-Allow-Origin: * to every origin in the overlay - so it belongs
on a folder holding only what the page may read.
Page diagnostics. A new setting, Diagnostics / Log page problems, reports
what a mod's page says went wrong inside itself: a script error, an unhandled
rejection, a console error, or a font that would not load. Nothing in there
reached the log before - the window could be rendering in the wrong font, having
thrown on every frame, and the only report was a player saying it looked wrong.
Off by default and behind Advanced, like its neighbour, and it reaches windows
opened after it is switched on.
Written down at last:
- Channel ordering. Messages arrive at the page in the order they were
posted, across channels and not merely within one, because every send goes
through a single queue.Retainreplays first;LatestOnlykeeps the
position of the message it replaced. Measured, not assumed. - The default window geometry - 80% by 85%, centred - is documented on
WidthandHeightthemselves, where someone deciding not to set a size will
read it. That is exactly where a first-person game reads the mouse, and it
cost the 1.8.6-1.8.8 series to find. docs/SOFT-DEPENDENCY.mdnames the pattern three consumers had each
arrived at independently, and states the half of the rule that is easier to
miss: a version gate answers whether a member behaves, not whether it
exists.tools/Audit-SoftDependency.ps1ships here now rather than in a consumer.
It is the build-time check for that rule, and it gained the missing half.
Also settled: two questions this repository had been carrying as comments -
that the ordering guarantee is what is now promised, and that DENY_CORS rather
than the stricter DENY is the right default, because an inline page really
cannot reach a Deny folder.
Extract over the game root. Everything is additive; nothing a consumer gates on
has moved, so 1.8.8 remains the floor for click-through.
v1.8.10 - opacity and the page that stays
Two fixes on top of 1.8.8, both in code the mouse series itself added, and
both found by review rather than by anything going wrong in play.
A half-transparent panel stopped being half-transparent. Handing the mouse
through to the game means making the window layered, and a layered window
paints nothing until its attributes are set - so they were set, to fully
opaque. Nothing writes the alpha again after a window is created, so the first
time the mouse went back to the game the fade was gone for good. Opacity now
survives the round trip.
A blocked navigation threw away the page that was still on screen. When the
browser accepts a navigation and only this library's origin filter turns it
down, the old document stays up - but the overlay reported IsPageLoaded false
about a window that was showing a page, buffered every send into nothing, and
counted the next LoadHtml as a first page rather than a retarget, which let
that buffer run into it. It now restores the previous target, exactly as it
already did when the browser refused outright.
Also: the diagnostic line that names the window in front asked what that window
wanted rather than which window it was, so a panel that had only asked for
click-through was reported as some other application's - in the one line
written to identify it.
Rolls up 1.8.9, which put the cursor diagnostic behind the settings menu's
Advanced switch. That is the only setting this library binds, so an ordinary
player now sees nothing from it at all.
Both fixes were checked by putting the fault back and watching the new rows
fail. Extract over the game root; nothing a consumer gates on has moved, so
1.8.8 remains the floor for click-through.
v1.8.8 - the mouse goes where the player is looking
One long fault, found and fixed in stages. A mod's panel covering the middle
of the screen could stop the mouse from turning the player, with nothing
anywhere to say why - and every early guess about the cause was wrong.
The cause is geometry, not the cursor. The game locks the pointer to the centre
of its own window while the player looks around, and Windows delivers mouse
movement to whatever window sits under it, no matter who holds the foreground.
A panel over that point receives the movement. Nothing is in error, which is
exactly why nothing reported one. Holding a mouse button restored it, because
that gives the game window a capture - the detail that made it look like a
cursor problem for an evening.
OverlayOptions.ClickThroughWhenUnfocused is the fix: while the game is
the window in front and is actually holding the mouse, the mouse passes through
the overlay to the game. Off by default. While it is engaged the panel cannot
be clicked back to the front - a click is mouse input like any other - so a
consumer that switches it on needs a hotkey, which is how it was opened.
It engages only while the game really holds the mouse. In a menu the cursor is
free, the game has no use for the centre of the screen, and the panel behaves
like any other window. And when the answer is contested - a configuration menu
showing the pointer while the game hides it again in the same frame - the last
settled answer stands, rather than the window's style being rewritten a hundred
times a second.
Also fixed on the way: FreeCursorWhileShown never fired for a panel that was
shown and focused in one go, which is what opening one normally does; a refused
navigation no longer disturbs a working overlay; and answers and completions
are bound to the document that asked for them.
Rolls up 1.8.4 through 1.8.8, which were development steps rather than
releases. CHANGELOG.md has each one separately.
Extract over the game root. Consumers gating on a version want 1.8.7 or newer
for the click-through behaviour; 1.7.0 is enough for FreeCursorWhileShown.
v1.8.3 - a cursor that stays put
The mouse cursor no longer flickers while an overlay that frees it is open.
What was happening
The game does not blindly re-hide the cursor every frame - it decides once per frame what the cursor state should be, and writes it only when the live state disagrees. A mod setting Cursor.visible = true is therefore exactly what creates the disagreement: the game corrects it, the mod sets it again, and the two alternate at frame rate. Setting it from LateUpdate as well does not help, because the correction also re-locks the cursor and lock mode acts natively.
There is a second half to it that is easy to miss. The same correction swaps the cursor bitmap for a fully transparent one. A mod that forces only the property gets a cursor it cannot see, and never restores the image.
What changed
FreeCursorWhileShown now sets the game's own "show the cursor" flag rather than overruling it - once per change of state instead of once per frame. The game then wants the cursor visible too, so there is nothing to disagree about and it stops writing; the single write it did perform restored visibility, lock mode and bitmap together. The flag is released when no overlay wants it and when the plugin shuts down.
This was read out of the game's own code, which uses the same flag where its world needs a cursor mid-raid. It is reached by reflection, so this library still references nothing but BepInEx and Unity: a game without those types falls back to setting the properties directly, which flickers, and a flickering cursor still beats an unreachable window.
For mod authors doing this themselves: the flag is global and has no counter, so two mods using it will take the cursor from each other. If more than one of your mods needs it, register an input node whose ShouldLockCursor() returns ECursorResult.ShowCursor instead - the game's input tree takes the maximum across nodes, so those compose properly.
Also fixed
- A
Readyhandler that asks for its page immediately - the usual shape - could load that page twice, because the first navigation now waits for the channel shim and the mod's own call arrived in between.PageLoadedfired twice and the page's own scripts ran twice. - A page's buffered and retained state is dropped when a navigation actually starts, not when it is requested. The browser can accept a request that this library's own origin filter then refuses - a URL with no origin to trust, a host whose folder mapping failed - and the page on screen stays exactly where it was. Discarding its state at the moment of the request threw it away anyway. The synchronous rejection, the filter's refusal and a navigation to the page already showing now all follow from one rule.
- Retargeting means the browser had actually taken the previous page, not merely that a target had been recorded, so a page named while the browser is still starting no longer costs the state set up before it.
- If the game refuses to take the cursor back, the library stops believing it is holding it and falls back, instead of leaving the player without one.
Verification
Probe modes ready-load and generation, fault matrix rows 50-52, full matrix green across 38 modes. Both new checks were verified by watching them fail before the fix rather than only pass after it. Confirmed in game: the cursor stays still, with the game's own cursor image.
Installation
Extract Anvil-WebOverlay-v1.8.3.zip over the SPT folder. The demo plugin is optional and separate.
v1.8.2 - answers that know which page asked
Nothing changes for players - this release is for the mods that use the library.
A patch release from a full external code review of 1.8.1. Every finding was independently verified before it was acted on: all of them real, none of them a release blocker, and three of the review's recommended corrections turned out to be wrong and were not taken.
What players might notice
- An answer the game was too busy to deliver could be dropped instead of arriving late.
- A page that reloads no longer receives an answer meant for the page before it.
For mod authors
The theme is identity: a question belongs to the document that asked it, and a navigation completion belongs to the navigation that started it. Neither was tracked, because until now nothing needed them to be.
Requests. A page numbers its questions from 1 again in every new document and matches an answer on that number alone. So a reply the mod took its time over - the deferred OnRequest form, or any reply at all under main-thread dispatch - could resolve whichever question the next document happened to number the same. Replies now carry the generation that asked them, both while the mod holds them and while they wait in the outbox, since an answer to a question asked from a parse-time script can still be buffered when the document changes. The mod-to-page direction was never affected; those ids never restart.
Navigation. A NavigationCompleted arriving before its own navigation has started belongs to the one it replaced. Accepting it marked the page the mod was waiting for as loaded and flushed the outbox into a document already on its way out.
Startup. The channel shim is installed asynchronously, and the browser only promises it is in place once its completion has run. The first navigation now waits for that instead of racing it, so the mod's own first page cannot come up without the window.overlay its first script uses. Only the navigation waits - Ready and the window itself are unchanged, since Ready has never meant "the page is loaded"; PageLoaded does.
Answers under EventDispatch.MainThread could be dropped when the queue filled, exactly as they could under Manual before 1.8.1 closed it there. The queue reported a full queue as delivered and the result path believed it. Answers now bypass that limit - their number is bounded by the calls that asked for them, so they cannot run away.
Smaller, same family: a request that timed out while waiting for the page was still put to it afterwards, running a page handler for an answer nobody was listening for; a closed window buffered sends into an outbox nobody would flush; creating a windowed overlay may wait for a second browser, and that wait pumps messages - so it can run the overlay's own close, which creation now checks for afterwards; and Navigate to the page already showing counted as a retarget and discarded the retained state, while the same page reloading itself kept it.
Documentation. The virtual-host isolation wording was imprecise in all four places it appeared: DENY_CORS denies fetch and XHR from another origin but not ordinary sub-resource loads. The access kind itself is unchanged and deliberate - the stricter DENY would break inline pages, whose opaque origin makes even the mod's own markup cross-origin to the mapped folder. The README's links are now absolute, because it ships inside the release zip where none of its targets do.
Verification
Probe mode generation, fault matrix rows 48-49, full matrix green across 37 modes. The matrix now states how each row is evidenced rather than marking everything PASS, after one row was found to claim a proof the automation does not perform.
Deferred to a separate round, deliberately: the unbounded host queue (not reachable by normal mod code), download policy, the two-process user-data-folder question, headless detection, and the bounds-store lock timeout. Each needs a design decision or a two-process test that the probe cannot stand in for.
Installation
Extract Anvil-WebOverlay-v1.8.2.zip over the SPT folder. The demo plugin is optional and separate.
v1.8.1 - failure paths that no longer fail quietly
Nothing changes for players - this release is for the mods that use the library.
A patch release: every change fixes a failure path, so normal use looks exactly as it did in 1.8.0. Mods on 1.8.0 should move up, as two of the fixes restore promises the API documentation makes.
What players might notice
- A mod's settings could still be lost after a page reload, in the one case 1.8.0 did not cover - when a page the mod asked for was refused by the browser.
- A rare crash when the second browser had to be started more than once.
- A page that simply is not there - a typo in a file name - now says so in the log instead of leaving the overlay to sit there looking slow.
- An overlay that has died no longer reports itself as showing a page.
For mod authors
Delivery. Navigate and LoadHtml used to drop the buffered sends and the retained state before the call that turned out to be rejected, so the page that stayed on screen lost the state belonging to it - and the next reload, whether the library's own after a renderer crash or the page calling location.reload(), handed it its defaults while the mod still believed its configuration was up. A refused navigation now leaves the overlay exactly as it was. A successful retarget still forgets, as documented.
A page named before the browser exists is navigated to once the view is created, and that attempt could be refused too. Its result was never looked at, so the refused page stayed the overlay's target: every send buffered into nothing, IsPageLoaded stayed false for good, and the mod's next LoadHtml looked like a retarget away from it and threw out state that had never belonged to any page.
Answers. EventDispatch.Manual could swallow one. Results travelled the event queue, and that queue is dropped when the handle is disposed: correct for events, which are documented as droppable, and a broken promise for an answer, which is documented as always arriving. Answers now have a queue of their own - never dropped on overflow, drained first by PumpEvents(), and handed over on the spot when the handle is disposed, since nobody pumps a handle they have thrown away.
Failures stop being silent. A navigation that fails outright now logs the page and the browser's error status. fail() retires the page instead of leaving IsPageLoaded true, answers every script caller still waiting, and refuses later sends rather than buffering them into a page that no longer exists. A renderer crash settles the scripts that were running in it, and a reload the browser refuses afterwards ends the overlay with RendererCrashed instead of leaving it quietly blank.
Also: a Create and a Dispose posted in that order could arrive the other way round and build a window nothing would ever destroy; a second browser that can never start was retried by every windowed overlay, each time holding the creation queue for the full timeout (now at most three attempts, and a ten-second wait rather than thirty); and the documentation for the latched Ready/Failed claimed a late subscription runs inside the +=, which is not true outside the default dispatch mode - it matters to a soft-dependency gate deciding whether to fall back.
Verification
Fault matrix rows 40-45 added, 36 probe modes green, including the pixel and click checks. Verified in game: renderer crash with recovery and the terminal third failure, the browser process killed outright, two mods colliding over hosting modes, and the cursor being handed back mid-raid.
The probe host itself now lives in the repository at tools/Probe, along with preview - a mode that shows your page in a real overlay so a HUD can be built without launching a raid. docs/SOFT-DEPENDENCY.md collects the rules for depending on this library without requiring it.
Installation
Extract Anvil-WebOverlay-v1.8.1.zip over the SPT folder. The demo plugin is optional and separate.
v1.8.0
Nothing changes for players; this release is for the mods that use the library.
A mod's page can be told to keep its state: a payload sent with PostOptions.Retain is remembered per channel and handed to every page that loads afterwards. That matters because the library reloads a page by itself after a renderer hiccup, and the fresh document would otherwise start from its own defaults while the mod believes it already sent its configuration.
Overlays that stream live data can keep up instead of falling behind: PostOptions.LatestOnly drops a payload the library is still holding when a newer one on the same channel arrives, and a page can ask for the same on its side with overlay.on(channel, fn, { latest: true }), which hands over the newest payload once per frame rather than a backlog. Once a message has gone to the browser there is no queue left here to collapse, which is why the page has the other half.
Events can now be taken on the consumer's own terms: Dispatch = EventDispatch.Manual plus PumpEvents() delivers them inside the mod's own Update, at the point it chooses and on its own frame budget - the answer to dispatched handlers being billed to this library by a profiler.
Fixed: messages and scripts queued before the first LoadHtml or Navigate are no longer discarded by it. Clearing exists so leftovers for a page being left behind do not reach its replacement; naming the first page is not that.