18-LAB / Documentation

vosu extensions

vosu keeps the tosu-compatible API families while adding optional native features and deliberately removing the embedded in-game product. This comparison is based on the published vosu source and upstream tosu source, not the assumption that every field described in vosu's README is exclusive.

The README says the additive input fields are planned for a tosu port soon(tm). Treat all extensions as optional; do not make server-name guesses into required capabilities.

The selected-field subscription vosu only provides change-only delivery: after the initial baseline, each update is an event naming exactly which selected fields changed or disappeared. Nothing changed means no update frame. Overlays can react without client-side diffing, polling, or full-document parsing, unlike compatibility streams that republish the whole document every poll interval.

Added endpoints, parameters, and headers

vosu addition Upstream comparison Reference
/websocket/v2/subscribe vosu only No matching upgrade in tosu's socket router Subscription protocol
V2_SUBSCRIPTION_DEBOUNCE configuration binding vosu only No selected-field sampler/binding in tosu; vosu uses 1–60000 ms Sampling
POST /api/counters/open-root vosu only management No dedicated route in tosu; its old open handler special-cases static.exe/tosu.exe HTTP management
POST /api/counters/download/{url}, /api/counters/open/{name}, /api/counters/delete/{name} vosu only management tosu serves GET at these paths; vosu replaces those methods, with no GET shim HTTP management
POST /api/runUpdates, POST /api/generateReport vosu only management tosu serves GET; vosu uses same-origin POST Reports/updates
Counter-search query source=installed|available vosu only tosu infers catalog search from Referer path /available; vosu makes it explicit and rejects unknown values with 400 Search
Calculator query lazer=true|false vosu only Not read by tosu's calculator route Calculator
Calculator mods JSON object/array and acronym text vosu only tosu's route accepts a parsed array or numeric coercion; vosu explicitly parses JSON-encoded object/array and two-letter acronyms Calculator
Response header X-Vosu-Identity: vosu on /favicon.ico vosu only No such header in tosu's favicon handler Bounded identity hint, not a security credential

The path-based calculator path parameter is not new, but vosu honors the supplied canonical regular .osu file. The examined tosu route tests whether query.path exists and then reads the current beatmap path. Do not assume identical behavior just because the parameter name matches.

vosu's exact same-origin management provenance policy is a behavioral difference: POSTs normally require exact listener scheme/host/port and saveSettings uses provenance captured at upgrade (1008 on forbidden writes). Scoped API keys can authorize value/profile writes; the loopback broker token can authorize command-socket writes only. Origin and Referer themselves are standard browser headers, not custom vosu headers. See security; provenance checks are CSRF protection, not authentication.

Added document fields

Each field below is additive; original timing arrays and four-button shapes remain compatible.

Field paths Type / meaning Upstream comparison
v1 gameplay.hits.pressHitErrors, gameplay.hits.releaseHitErrors; tournament tourney.ipcClients[].gameplay.hits.* vosu only Number arrays separating presses and hold releases tosu v1 only has original hitErrorArray
streamCompanion pressHitErrors, releaseHitErrors vosu only Number arrays tosu token type only has original hitErrors
Regular v2 play.pressHitErrors, play.releaseHitErrors; tourney.clients[].play.* vosu only Number arrays Absent from tosu Play type/builder
Precise v2 pressHitErrors, releaseHitErrors; tourney[].pressHitErrors, tourney[].releaseHitErrors vosu only Number arrays tosu precise type only exposes hitErrors
Regular v2 root keys vosu only {k1,k2,m1,m2,all} tosu regular v2 root has no keys; its precise v2 does have the four buttons
keys.all and precise tourney[].keys.all vosu only All source-reported logical inputs in order tosu precise KeyOverlay has only k1/k2/m1/m2
keys.all[].name, .isPressed, .count, .pressedAt, .releasedAt (and precise tournament equivalents) vosu only String, boolean, integer, nullable number, nullable number; gameplay ms (stable frame edges, lazer sampled edges) No all-key element in tosu

Together the press and release arrays hold every value of the original array exactly once, in order. osu!lazer and native osu!stable mania maps (including Mirror) are classified; stable converted maps and maps without metadata stay press-only, and an unmatched error falls back to a press within 250 ms of replay time or one second. Added in vosu 1.1.0: stable keys.all counts retained replay-frame edges and timestamps them at frame time; lazer still timestamps sampled transitions only. See schema.

Gameplay extensions added in vosu 1.1.0

vosu only Added in vosu 1.1.0. Treat these fields as optional on other servers. They are additive to regular and precise v2, not v1 or streamCompanion.

Regular v2 Precise v2 Shape
play.hitOffsets hitOffsets Optional number[], flat stride-3 [x, y, error, …]
play.keyHitErrors keyHitErrors Optional { press: number[][], release: number[][] }
play.catcher catcher Optional { x: number, dashing: boolean }

Standard hit offsets

Enable ENABLE_HIT_OFFSETS = true (Game data → Standard hit offsets in the dashboard) to expose play.hitOffsets in regular v2 and top-level hitOffsets in precise v2; the field is omitted entirely by default (false) and whenever the setting is off.

Each triple describes an osu!standard circle or slider-head hit: X/Y are cursor minus stacked center, in displayed 512×384 playfield pixels, and error is the original matching timing error in milliseconds. The array is empty outside standard gameplay. Missing spatial evidence omits a triple rather than inventing a position.

While disabled, stable skips spatial replay/map matching and lazer skips judgement-position and object-property reads. Enabling mid-attempt records subsequent hits only; it does not backfill history. keys.all and catcher are independent of this setting.

Stable matches replay down-edges to unjudged map heads using press time - error (±1 ms), with calculator stacking and HR reflection: HR reflects the raw position, and the stack offset is then applied unreflected, as stable's own HR autoplay cursor shows. The replay cursor is in the displayed (HR-flipped) frame. Pending evidence expires after 250 ms without blocking later matched judgements. Mirror and other positional transformations are unsupported on stable; Mirror produces no triples.

Lazer reads the actual judgement cursor and object's position/stack/scale, so applied positional mods need no map reconstruction. It needs the measured spatial offsets (shipped for lazer 2026.921.0, or generated at runtime for other versions); without them it emits no triples rather than reading unverified memory. Autoplay on a stacked test map gives (0, 0) for every hit on both clients, with and without HR.

JSON delivers a number array, not a binary typed-array buffer. Convert it when needed:

function hitOffsetTriples(regular, precise) {
  const values = regular?.play?.hitOffsets ?? precise?.hitOffsets;
  return Array.isArray(values) ? Float32Array.from(values) : null;
}

const offsets = hitOffsetTriples(regularState, preciseState);
if (offsets) {
  for (let i = 0; i < offsets.length; i += 3) {
    const [x, y, errorMs] = offsets.subarray(i, i + 3);
    console.log({ x, y, errorMs });
  }
}

The 3D hit-offsets demo overlay requires ENABLE_HIT_OFFSETS = true; regular v2 and precise v2 omit the field when the setting is off.

Mania per-key errors

Enable ENABLE_MANIA_KEY_HIT_ERRORS = true to expose keyHitErrors; it is omitted entirely by default. The outer index is the zero-based displayed lane, including Mirror. Each inner array contains timing errors in milliseconds; untouched lanes before the highest observed lane are empty. Outside mania attempts both outer arrays are empty.

History follows the existing aggregate hit-error reset/retention rules: pause/results retain it, exiting or retrying resets it, and enabling mid-attempt starts with subsequent classifications without rebuilding earlier events.

Stable includes only errors matched to native mania map judgements and replay transitions. Unmatched errors published by the 250 ms/one-second fallback, Random plays, and converted maps have unknown columns and are excluded here, but remain in aggregate arrays. Lazer reads each judged object's column; when a hold aggregate arrives later it moves the tail's timing from that column's press array to its release array. Missing/unmeasured column offsets omit events rather than inventing lanes.

function laneTiming(regular, precise, lane) {
  const errors = regular?.play?.keyHitErrors ?? precise?.keyHitErrors;
  if (!errors) return null; // Disabled or unsupported, not an empty history.
  return {
    press: errors.press[lane] ?? [],
    release: errors.release[lane] ?? [],
  };
}

Catcher position and dash

catcher appears only during catch gameplay when evidence is available; it is omitted in menus, results and other rulesets. x is the client's stored position in osu! playfield pixels (0–512), not screen space. dashing is ordinary dash, not hyperdash.

Stable selects the replay frame at current play time, including preloaded watched replays, independently of new judgements. The legacy X slot and dash value 1 follow lazer's legacy catch replay conversion; their live stable memory mapping still needs measurement. Lazer reads native X and Dashing during precise polling; the new catch offset template entries must be measured before that client's field can be emitted.

Logical keys and remaining parity limits

Stable now publishes replay-frame logical actions with lazer-compatible B1-onwards names/order, independently of in-game overlay visibility. Lazer accepts up to 20 mania actions (dual-stage, stage one then stage two). See the ruleset mapping for each name and stable replay bit.

ENABLE_KEY_OVERLAY still gates updates after the opening 150 ms, and legacy k1, k2, m1, m2 sources are unchanged. Edge timestamps are nullable gameplay milliseconds. Stable counts every retained down-edge with exact frame timestamps, including complete taps between polls, stopping at the play clock even for preloaded replays/spectating. Retry/rewind resets the attempt. Rolling eviction preserves accumulated counts, but missing more than 8192 frames establishes a new baseline without inventing a press; evicted history is not recoverable when attaching midway either.

Lazer records sampled-edge timestamps; count jumps may reveal missed complete taps without fabricating timestamps. Stable catch left/right remain unpressed with zero counts and null timestamps because stable records position instead; dash is available. These and unrecoverable history remain parity differences, not physical-input measurements. Live osu!stable b20260924.1 confirmed the in-memory button byte in all four rulesets and counting with the in-game key overlay hidden.

Package-level differences

vosu behavior Upstream comparison
Package settings.schema.toml and central <name>.settings.values.toml vosu only tosu's parser uses package settings.json and JSON values; vosu migrates these once, then keeps TOML authoritative
Explicit top-level alias mount confinement; delete unlinks mount, install rejects symlink destinations vosu only tosu uses generic path join/stat/read and recursive delete; it does not implement this explicit confined-mount contract. Basic filesystem symlinks are not themselves a new concept

Descriptors, setting types, ?update=yes, ?update=true, and window.COUNTER_PATH are shared compatibility mechanisms, not exclusive additions. See packages.

Added in vosu 1.1.0 vosu only: settings profiles, per-instance ?vosuProfile=, qualified COUNTER_PATH, and window.VOSU_PROFILE. Schemas remain shared and the reserved default profile keeps legacy values paths. No override preserves the existing served bytes/metadata; a qualified identity is not an asset URL. See profile-aware packages.

tosu surfaces vosu does not serve

tosu route/surface vosu behavior / replacement
GET /api/ingame deprecated 404; embedded in-game product removed
__ingame__ dedicated settings deprecated Removed; use ordinary per-counter settings, not this namespace
getOverlays / getCounters catalog commands Removed as operations; authorized unknown commands only receive a command-name reply
GET /api/counters/download/{url}, GET /api/counters/open/{name}, GET /api/counters/delete/{name} 404; dashboard uses POST
GET /api/runUpdates, GET /api/generateReport 404; dashboard uses POST
Built-in /local-overlays instruction page No dedicated page in vosu; it reaches ordinary counter-static fallback and is normally missing unless a matching static resource is supplied
Special counter-open names tosu.exe, static.exe No magic aliases; use the normal name or dedicated /api/counters/open-root

Overlays must not depend on /api/ingame or __ingame__, and should never have used that in-game endpoint even to manage their own settings/configuration. In-game rendering is vosulay's job. ADR 0008 is the clean-cutover rationale; the router confirms /api/ingame cannot fall through to counter serving.

Feature detection

  1. Try the selected-field upgrade and send selectors immediately. If the upgrade fails or the initial selection is rejected, fall back to the compatibility v2 stream. Do not probe management operations.
  2. Use regular v2 for map/play state and precise v2 for fallback keys. Check properties, not an assumed server version.
  3. For press history, fall back to the original array. For release history, an absent field means unsupported—do not reinterpret every event as a hold release.
  4. Use the four-button shape when keys.all is absent. A favicon identity hint can identify vosu but is not proof that a particular future extension exists.
function inputCapabilities(regular, precise) {
  const keys = regular.keys ?? precise?.keys;
  const original = regular.play?.hitErrorArray ?? precise?.hitErrors ?? [];
  return {
    keys,
    allKeys: Array.isArray(keys?.all) ? keys.all : null,
    presses: regular.play?.pressHitErrors ?? precise?.pressHitErrors ?? original,
    releases: regular.play?.releaseHitErrors ?? precise?.releaseHitErrors ?? null,
  };
}

null here means unsupported, not an observed empty history. The tutorial shows subscription-first fallback handling. Keepalive uses WebSocket control Ping/Pong only; browser clients send no heartbeat.

Comparison sources

Edit this page on GitHub