On this page

← All documentation

Mod API reference (version 2)

Mod API reference (version 2), namespace by namespace, checked against the SDK.

Source of truth: mods/sdk/modding.d.ts (generated from the Rust contracts by cargo run -p modding --example write_sdk). This page is checked against this file by node scripts/check-modding-docs.mjs: every symbol in a heading (###) must exist in the SDK, and every function or event in the SDK must appear here. For the exact fields of a type, the .d.ts is authoritative; for the detailed rules and limits, see also TypeScript mods: the detailed guide.

Conventions: song times are in microseconds (timeUs), except Hit.offsetMs (real milliseconds). Layout and style lengths are fractions of the screen height. Any out-of-range value throws an exception in the script (see concepts).

Availability by script kind

Three kinds of scripts share the same API: a mod (defineMod, main.ts), a skin (defineSkin, skin.ts) and a map script (defineChart, script.ts). Phases: load (top level of the file), setup, event.

Function mod skin map script Phase
defineMod / defineSkin / defineChart defineMod defineSkin defineChart load, once only
ctx.on yes yes yes load, setup; afterwards
ctx.has, ctx.element yes yes [TO VERIFY] setup, event
game.* (reads) yes yes yes setup, event
hud.*, storage.* yes yes no setup, event
stage.* stage permission stage permission stage permission setup, event
playfield.*, lanes.set no yes yes setup, event during a play session
judgements.register, scrollSpeed.register, ratings.register, tables.register, library.filters.register, controls.register, gameplay.register yes no no setup only
ratings.backfill, metron.performance, ui.popup, ui.dismiss yes no no setup, event
leaderboard.query, leaderboard.best yes no no setup, event (response: leaderboard.result)
tabs.register, tabs.extend yes no no setup only
downloads.register, downloads.registerBridge network permission no no setup only
skinImport.register skinImport permission no no setup only
skinImport.stage, skinImport.create, skinImport.panel, skinImport.close skinImport permission no no setup, event
metron.catalog, metron.difficulty yes yes yes setup, event
log.info, log.warn yes yes yes always (even at load)

Sources: crates/modding/src/api.rs (for_running, for_mods_and_skins, playfield_call, *Register functions).

Declaration

defineMod

defineMod(input: ModDefinition): ModManifest. Called once, at the top level of main.ts, with export default. Fields: id, name, version, apiVersion (must be 2), and optional author, description, homepage, fonts, uses, provides, permissions, images, loadOrder, setup(mod). Any unknown field is rejected. Rules for each field: Declaring the mod: defineMod.

defineSkin

defineSkin(input: SkinDefinition): SkinManifest, in skin.ts. Same fields as a mod except loadOrder; setup(play: PlayContext). See Skins.

defineChart

defineChart(input: ChartDefinition): ChartManifest, in script.ts next to a chart. Fields: apiVersion, permissions?, images?, setup(play). No id. See Map scripts.

ctx

Typed global ctx (not a parameter: name the setup parameter play or mod, never ctx).

  • ctx.on(event, handler): subscribes a handler to an event from the list below; allowed at the top level and in setup.
  • ctx.has(id): true if the package id, declared in uses, is present, compatible and running.
  • ctx.element("<package>/<element>").configure(options): configures an element that another package provides (provides.elements). The package must be in uses, otherwise an exception add `<package>` to `uses` to configure its elements is thrown.

Events

Subscription: ctx.on("<name>", handler). game.* events are only delivered to scripts running during the play session; queues are bounded (1024) and an event may be dropped if the mods thread is running late.

game.songStart

SongStartEvent: song, judgements (JudgementConfig), timeUs (negative during a pre-roll). The chart and the playfield become readable.

game.judgement

JudgementEvent: column, timeUs, offsetUs (input − note; 0 for a miss), tier (TierRef), combo, counts (per tier), accuracy. The totals are those of the engine after this note.

game.pause

SongTime: { timeUs }.

game.resume

SongTime: { timeUs }.

game.tick

TickEvent: timeUs, hitCount (native non-Miss hits, reset to zero at the start of each play session). 30 Hz, only while a chart is playing without pause.

game.beat

Beat: index, timeUs, bpm, meterBeat (0 on the first beat of the measure). Delivered at the time of each beat of the chart; if late, only the last one.

game.songEnd

SongEndEvent: counts, maxCombo, accuracy, aborted.

game.playfieldChange

Payload: Playfield (resize).

game.settingsChange

Payload: ModSettings.

elements.configure

ElementConfigure: element, from ("player" or the id of the calling package), options (all options, defaults filled in). Received by the provider.

controls.action

ModActionEvent: id (declared local id), pressed, timeUs.

library.ready

LibraryReady ({}): the library is loaded (after the interface and the mods).

library.chartAdd

ChartAdded: { chartId }, one per chart added by a scan.

library.chartsAdded

ChartsAdded: { chartIds, truncated } (8192 at most): the same ids in a single event.

ratings.progress

RatingProgress (to the requesting mod only): requestId, id, done, total, failed, finished, error?, ahead.

metron.performanceResult

PerformanceResult (to the requesting mod only): requestId, calculator, value?, unit (pp or SSR), error?.

ui.action

PopupActionEvent: { id } of a popup action, or "dismiss".

leaderboard.result

LeaderboardResult (to the requesting mod only): requestId, chartId, offset, total, entries (LeaderboardEntry[]), judgement, performance? (LeaderboardPerformance), unavailableReplays, error?.

skinImport.opened

SkinImportOpened (permission skinImport): importerId, locale (en, fr, or zh), source? (SkinSource), error?.

skinImport.staged

SkinImportStaged: requestId, stageId?, report? (Report), error?.

skinImport.created

SkinImportCreated: requestId, id? (id of the written skin), error?.

skinImport.action

SkinImportAction: the { id } of a card action (skinImport.panel) or "dismiss".

Reads: game.*

On-demand reads from setup or a handler. Never while the mod loads (is not available while the mod loads).

game.song

game.song(): Song | null: the last song started (title, artist, creator, difficulty, mode, layout, keys, durationUs, noteCount, holdCount, bpm, timing, ratings indexed by calculator id).

game.notes

game.notes(query: NoteQuery): NotePage. fromUs, toUs (head in [fromUs, toUs)), column?, cursor?, limit? (default 64, 1 to 256). Pagination via next. endUs is null for a tap note; mines are not notes.

game.hits

game.hits(query: HitQuery): Hit[]: the latest native non-Miss hits, from the oldest to the most recent (limit 1 to 256, default 50). Hit = { offsetMs, tier } (offsetMs is negative when early).

game.playfield

game.playfield(): Playfield | null: keys, lanes ({ column, x, width }), hitY, spawnY, scrollTimeUs.

game.player

game.player(): PlayerState: playing, paused, timeUs, combo, maxCombo, counts, judged, accuracy.

game.judgements

game.judgements(): JudgementConfig | null: preset and tiers (TierInfo).

game.settings

game.settings(): ModSettings | null: volume, showFps, scrollTimeMs, audioOffsetMs, ratingSystem.

HUD: hud.*

A node tree retained per mod; a node stays until hud.remove, hud.clear, or the mod is disabled. Per-mod limits: 256 nodes, 256 characters per text, id from 1 to 64 characters, 8 levels of groups. Reusing an id replaces the node (same parent). Fields common to every node: id, parent?, visible?, layout?, style?, showWhen?, animate?, element?.

hud.text

hud.text(node: TextNode): string. Own fields: text, bind? (TextBinding). Bindings: combo, maxCombo, hits, misses, judged, remaining, accuracy (decimals?), ghostAccuracy, ghostDelta, ghostCombo, ghostName, tierCount (tier), tierName (tier), lastJudgement (colors?), elapsed, total, timer, fps, label (label: HudLabel), action (action: "skipIntro"). The overlay updates them without going back through the script.

hud.box

hud.box(node: BoxNode): string. Own field: fill? (Fill: songProgress, accuracy, tierShare + tier) which crops the box horizontally from the left.

hud.image

hud.image(node: ImageNode): string. src: a PNG from the mod's folder (8 MiB at most, checked at call time); fit?: contain (default), cover, fill.

hud.group

hud.group(node: GroupNode): string. flow? (Flow: direction row | column, gap?, align?, justify?) lays out the children; without flow, they are placed as fractions of the group (so give it width and height).

hud.update

hud.update(patch: NodePatch): void: modifies an existing node; the given fields of layout and style replace those of the node, the others remain. text/bind, src/fit, flow, fill only apply to the corresponding node type.

hud.remove

hud.remove(id: string): void: removes the node and its descendants.

hud.clear

hud.clear(): void: removes all of the mod's nodes.

Layout (Layout): x, y (fractions of the parent, [-10, 10]), width, height ([0, 4], screen heights), anchor (Anchor). Style (Style): color, background, gradient, border, radius, padding, opacity, font, size, weight, italic, align, shadow, transform, transitionMs. Details on the bounds: HUD. Colors: #rgb, #rgba, #rrggbb, #rrggbbaa.

Native rendering: stage.*

Requires permissions: ["stage"] (Permission is "stage", "skinImport", or "network"); the player can revoke it per mod. Budgets per script: 128 elements, 16 emitters, 1024 particles, 64 triggers. Elements are declarative: animations and triggers are evaluated by the renderer on every frame, never by the script.

stage.sprite

stage.sprite(input: StageSprite): string: a PNG image declared in images.

stage.rect

stage.rect(input: StageRect): string: a solid rectangle, radius in screen heights.

stage.text

stage.text(input: StageText): string: a single line (128 characters) in the game's font.

stage.emitter

stage.emitter(input: StageEmitter): string: a particle emitter (burst, rate, maxParticles, lifetimeMs…).

stage.trigger

stage.trigger(input: StageTrigger): string: on (StageEvent: judgement, press, release, holdStart, holdEnd), target, play? or stop?.

stage.play

stage.play(input: StageCue): void: starts an animation from a handler.

stage.stop

stage.stop(input: StageCue): void: stops it.

stage.remove

stage.remove(id: string): boolean: removes an element or a trigger.

stage.clear

stage.clear(): void: removes everything.

Position at (StagePoint): space (screen default, playfield, lane, receptor), column?, x?, y?. Layers (StageLayer): below, lanes, above.

Playfield: playfield.* and lanes.set (skins and map scripts)

playfield.set

playfield.set(spec: PlayfieldSpec): void: merges spec into the playfield (at setup). The fields are listed in the SDK's PlayfieldSpec; see Playfield reference.

playfield.update

playfield.update(update: PlayfieldUpdate): void: patch (PlayfieldSpec), transitionMs? (0 to 10000), easing? (Easing). Animated field by field by the renderer; no per-frame calls.

lanes.set

lanes.set(update: LaneUpdate): void: column (starting from 0), lane (LaneSpec), transitionMs?, easing?.

Judgement: judgements.register

judgements.register

judgements.register(definition: JudgementSetDefinition): void, setup only, 8 sets per mod. Fields: id (a-z, 0-9, -, 32 at most), name, params? (JudgementParam: label, min, max, step, default, short?), accuracy (SetAccuracy), holds (SetHolds), tiers(params) (JudgementTiers, synchronous, resolved once per parameter combination right after setup). A tier (JudgementTier): id, name, color, gradient?, windowMs? or earlyMs + lateMs, weight?, breaksCombo?. From the tightest to the widest, with Miss last. Rules: Judgement sets, docs/judgement.md.

Scroll speed: scrollSpeed.register

scrollSpeed.register

scrollSpeed.register(definition: ScrollSpeedDefinition): void, setup only, 8 systems per mod. id, name, param (ScrollSpeedParam, like JudgementParam), toMs(value, context) (ScrollSpeedToMs) which returns the scroll time in milliseconds; context.travel (ScrollSpeedContext) is the reference distance in screen heights. Rules: Scroll speed.

Gameplay modifiers: gameplay.register

gameplay.register

gameplay.register(input: GameplayModifierDeclaration): void, setup only, 8 per mod. id, name (32), description (200), kind (GameplayModifierKind: auto, ghost, mirror, random, noLn, fullLn), group?, icon?, conflictsWith? (8 <modId>/<id> keys). The behavior is native: the script only declares.

Keys: controls.register

controls.register

controls.register(input: ModActionDeclaration): void, setup only, 32 per mod. id, name (64), defaultKey: a physical key code accepted by the game (KeyH, Digit1, Backquote…, list: crates/core/src/mode/key-codes.json). Received through controls.action. Escape and F2 remain reserved.

Library, tables, and difficulty ratings

tables.register

tables.register(input: TableDefinition): void, setup only. id, columns (ColumnDefinition: name, kind number | text, indexed?). 16 tables per mod, isolated per mod; their rows disappear along with their chart.

ratings.register

ratings.register(input: RatingDeclaration): void, setup only, 16 views per mod. id, name, calculator, unit, table, column, version, panels? (RatingPanel: metrics, bars, radar, timeline, at most 4). The table must have been declared by this mod, the column must be numeric, and version must equal the one from metron.catalog().

ratings.backfill

ratings.backfill(input: BackfillRequest): number | null: requests the calculation of missing or stale charts (charts? restricts to a set of ids). Returns a request id (null when the mod already has 2 requests in progress); progress arrives through ratings.progress.

library.filters.register

library.filters.register(input: LibraryFilterDeclaration): void, setup only, 32 per mod. id, name, table, column, version, unit?. The table must already be declared by this mod.

metron.catalog

metron.catalog(): MetronCatalog: calculators (MetronCalculator: id, performance, version).

metron.difficulty

metron.difficulty(input: CalculatorRequest): number | null: preloaded native value for the current chart, null if absent.

metron.performance

metron.performance(input: PerformanceRequest): number | null: calculator, accuracy (fraction [0, 1]). Returns a request id (null if the service is busy); the result arrives through metron.performanceResult. Only osu-2018 (pp) and etterna-515 (SSR) have a performance.

ui.popup

ui.popup(input: PopupDefinition): void: menu progress window (title, detail, done, total, actions? up to 4 PopupAction { id, label }). One popup open per mod, 8 in total; the text is never HTML.

ui.dismiss

ui.dismiss(): void: closes the mod's popup.

Local leaderboard: leaderboard.*

Read-only access to a chart's local leaderboard, no permission required: typed, bounded, asynchronous (sources: Local leaderboard: leaderboard.*, crates/modding/src/leaderboard.rs, api.rs). chartId is the library's chart identifier (library.chartAdd, library.chartsAdded). Reserved for mods; never a path, a file, or a replay entry.

leaderboard.query

leaderboard.query(input: LeaderboardQuery): number | null: chartId, limit? (1 to 100, 20 by default), offset? (0 to 100,000). Returns a request number, or null when they pile up (2 pending per mod, 16 in total); the response arrives through the leaderboard.result event. Each request rejudges the chart: read one page at a time.

leaderboard.best

leaderboard.best(input: LeaderboardBestQuery): number | null: { chartId }. Equivalent to query with limit: 1: rank 1 of the entire leaderboard (imported replays included, received).

A LeaderboardEntry: rank (starting at 1), replayId, playerName? (null for an old replay without a name), received, accuracy (percent), performance? (null if not computable), performanceNonstandard, maxCombo, misses, tiers (TierCount[]: { name, count }), rate, modified, playedAtMs. The leaderboard is the one shown in the interface: replays rejudged with the player's current judgement, ranked by performance.

Selection tabs: tabs.*

Declarative: a mod adds a tab to the selected chart's panel, or sections to existing tabs; Rust validates everything, and the interface renders generically (no markup from the mod). setup only, mods only (sources: Tabs in the selection screen: tabs.*, tabs.rs).

tabs.register

tabs.register(input: TabDeclaration): void: id, title (1 to 24 characters), icon? (closed list of 16 lucide icons: layers, info, trophy, star, gauge, activity, chart-bar, list, flame, music, clock, target, sparkles, bookmark, heart, users), order? (0 to 1000), panels (1 to 8 TabPanel). 4 tabs per mod, 12 in total.

tabs.extend

tabs.extend(input: TabExtensionDeclaration): void: tab (ExtendableTab: info, leaderboard, mods), slot (TabSlot: top | bottom), order?, panels (1 to 4). Adds content before or after the host's content, never removing it. 4 extensions per mod, 24 in total.

Panels (TabPanel): metrics, bars (max?), radar (max?) with TabField entries (label, source, unit?, decimals?), timeline (like the notes), leaderboard (title, limit 1 to 10: the already-evaluated ranking of the Leaderboard tab) and text (title?, text 1 to 280 characters, no markup). Field sources (TabFieldSource): { kind: "column", rating, column } (a numeric column from the table of a difficulty view declared by the same mod with ratings.register, earlier), { kind: "chart", metric } (ChartMetric), { kind: "leaderboard", stat } (LeaderboardStat: plays, bestPerformance, bestAccuracy). Tabs follow the mod live.

Downloads: downloads.*

network permission. Requires permissions: ["network"]; setup only; the player can revoke the permission mod by mod and sees the declared hosts. The script never touches the network: the host (crates/downloader) sends all requests, over HTTPS and to declared hosts only, and keeps the player's token out of the script's reach. Sources: Downloads: downloads.register and Mods and downloads on the feature/mod-download-sources branch, crates/modding/src/downloads.rs (4 declarations per mod, 64 in total).

downloads.register

downloads.register(input: DownloadDeclaration): void: a declarative source (data only). Fields: id, name, description?, site?, kind (DownloadKind: mirror = one more mirror for osu! beatmapsets, target: "osu"; source = one more tab in Download), hosts (1 to 8 exact lowercase DNS names), rateLimit? (requests per minute, 1 to 120, 30 by default), auth? (DownloadAuth: kind none | token, header?, scheme?, scope? download | all; the token is entered by the player), search? (SearchSpec: url, params, sorts, statuses, paging, response) and download (DownloadSpec: url with {id}, or urlField = JSON path of the URL). URL templates: closed set of variables {query}, {status}, {sort}, {offset}, {page}, {limit}, {cursor}, {keysMin}, {keysMax}, {starsMin}, {starsMax}, {bpmMin}, {bpmMax}, {lengthMin}, {lengthMax}, {genre}, {language}; a [ …] group is only written if all of its variables have a value. Responses: simple JSON paths (ItemSpec, DifficultySpec, OnlySpec, ResponseSpec with format osu | mapped, PagingSpec offset | page | cursor).

downloads.registerBridge

downloads.registerBridge(input: BridgeDefinition): void: for an API that the data cannot describe (POST, XML/HTML, search → detail → file chaining). Pure, synchronous functions compute the next step (BridgeStep), the host makes the request, then calls the mod back:

  • search(query, page, state) (BridgeSearch): first page, page is 1;
  • onResponse(response, state) (BridgeOnResponse): response is a BridgeResponse (status, headers, text);
  • action?(result, actionId, state) (BridgeAction): click on a button of a result (BridgeActionResult).

A step (BridgeStep) contains one of: request (BridgeRequest: url, method GET | POST, headers?, body?, form?, json?, expect? json | text | xml | html), results (BridgeResult[]: id, title, artist, creator?, coverUrl?, tags?, size?, keyCount?, details? (ResultDetail), actions? (ResultAction), data?), download (BridgeDownload: url, method?, headers?, body?, form?, json?, filename?, format BridgeFormat: zip | osz | qp) or error; plus nextPage? and state? (JSON passed back as-is on the next call). Limits: 6 requests per search or action, 60 s, 2 MiB of text per response. An exception, a non-JSON result or a budget overrun disables the bridge. BridgeDeclaration is the form without the functions.

Skin import: skinImport.*

skinImport permission. A mod with permissions: ["skinImport"] offers to convert an osu! skin (folder or .osk) into a Prism skin. It touches no file and no pixel: the host reads, transforms the images, and writes the folder; the mod only supplies values. Mods only; register in setup, the rest in a handler. Without the permission: `skinImport` needs the `skinImport` permission. Full example: mods/skin-converter (pvng.skin-converter). Source: Converting an osu! skin: the skinImport permission, docs/convertisseur-skin-osu.md, crates/modding/src/importers.rs.

skinImport.register

skinImport.register(input: ImporterDeclaration): void: id, name, description?, localized? (ImporterText per language), sources (SourceKind[]: folder, archive). Adds a card to the Skins page; 4 importers per mod. The button opens the game's file dialog.

skinImport.stage

skinImport.stage(input: StageRequest): number | null: sourceId, skin (SkinDescription: id, name, version?, author?, description?, playfield?, layouts (SkinLayout: keys, playfield?, lanes), hud? (SkinHud)), images (ImageOp[]: source, dest, animation?, frame?, padTop?, padBottom?, fit? (ImageBox), stretch? (ImageSize)). The host validates, transforms and replies with skinImport.staged (Report: files StagedFile, totalBytes, budgetBytes, overBudget, problems).

skinImport.create

skinImport.create(input: SkinImportCreate): number | null: { stageId }; the host writes a readable skin.ts into <data>/skins/ (never replacing an existing skin) and replies with skinImport.created.

skinImport.panel

skinImport.panel(input: ImportPanelDefinition): void: a declarative card (text only): title, status (PanelStatus: working, ready, done, error), detail?, sections? (PanelSection: heading, rows of PanelRow { label, value }), notes? (PanelNote: level NoteLevel info | warning | error, text), actions? (PanelAction: id, label, primary?, editSkin? = id of a skin created by this mod, opens the editor). Limits: 6 sections of 24 rows, 48 notes, 4 actions.

skinImport.close

skinImport.close(): void: removes the card.

The source read (SkinSource): sourceId, label, kind, ini? (SkinIni: general IniGeneral, mania ManiaSection[] of ManiaColumn, fontsUsed, problems), images (SourceImage: normalized name, frames, hasStill, scale, dimensions, bytes, blank?, peakColor?, peakWidth?), truncated, totalBytes, problems.

Storage: storage.*

JSON file specific to the mod; 256 KiB, 256 keys of 1 to 128 characters; written at most once per second.

storage.get

storage.get(key: string): unknown | null.

storage.set

storage.set(entry: StorageEntry): void: { key, value }, JSON value.

storage.remove

storage.remove(key: string): void.

storage.keys

storage.keys(): string[]: sorted keys.

storage.clear

storage.clear(): void.

Log: log.*

log.info

log.info(text: string): void: message displayed under the mod in the Mods page (1024 characters at most, 256 messages queued).

log.warn

log.warn(text: string): void: same, warning level.

Secondary types

SDK types that the sections above mention without detailing them (the .d.ts gives every field):

Type Meaning
Animate { on: AnimateOn, kind: AnimateKind, durationMs }: HUD node animation; AnimateOn = judgement | miss | hit, AnimateKind = pop | popFade | flash; duration 1 to 5000 ms
Border { width, color } (solid border of a node)
Gradient, GradientStop { angle, stops } (2 to 8 stops { color, at }, at in [0, 1])
Shadow { x, y, blur, color }
Transform { x?, y?, scale?, rotate? } applied after placement
TextAlign start | center | end
ShowWhen paused | running | showFps | judged | ghost
HudElement name of a standard element whose font the player changes: fps, accuracy, hits, misses, combo, timer, remaining, status, judgement, judgementCounts
HudAction skipIntro (action binding)
ImageFit contain | cover | fill
FlowDirection, FlowAlign, FlowJustify fields of Flow: row | column; start | center | end | stretch; start | center | end | spaceBetween
Lane { column, x, width } of a Playfield
Note { index, column, timeUs, endUs? } of a NotePage
TimingPoint, BpmRange { timeUs, bpm, beatUs, meter }; { min, max, main } (in Song)
HitList Hit[]
HostEvents event name → payload table that types ctx.on
Dependency, DependencySpec value of uses: semver range, or { version, required?, feature? }
Provides, ElementDeclaration, OptionKind provides.elements; { root?, options? }; number | integer | boolean | string | color | enum | colors
ModSetup, SkinSetup, ChartSetup setup signatures (ModManifest / PlayContext)
SetDeclaration, ScrollSpeedDeclaration declaration shapes without the function (tiers, toMs)
ColumnKind number | text (table column)
ChartMetric notes, holds, holdPercent, averageNps, peakNps, bpmMin, bpmMax, duration
RatingField, RatingFieldSource, RatingSeries, RatingSeriesSource fields and series of a RatingPanel: { label, source, unit?, decimals? }, { kind: "column" } or { kind: "chart" }, density | bpm
SpriteSize { width, height } (screen heights)
PlayfieldAnchor, ScrollDirection, HitLightOn, TextureFilter, JudgementLineAt, MeasureLineLength, HoldMissedStyle enumerations of PlayfieldSpec and LaneSpec
StageAlign, StageSpace, StageSize, StageProps, StageAnimation, StageJudgementFilter, StageColumn pieces of stage.*: alignment, coordinate space, size, animated properties (x, y, scale, rotation, opacity, color), animation { durationMs, easing?, repeat?, from?, to? }, trigger filters
AuthKind, AuthScope DownloadAuth.kind: none | token; scope: download | all
DownloadTarget, PagingKind, ResponseFormat osu (the only mirror target); offset | page | cursor; osu | mapped
BridgeMethod, BridgeExpect GET | POST; json | text | xml | html

Source in the game repository: docs/modding/reference.md