# Prism (PVNG): context for writing a TypeScript mod # Single file to paste into an AI assistant. API version 2. Source: docs/modding/*.md and mods/sdk/modding.d.ts. ## ABSOLUTE RULE DO NOT INVENT ANY API. Use only the functions, events and fields listed below (or in mods/sdk/modding.d.ts if the author provides it to you). If the request requires something the API does not offer, SAY SO clearly ("the mod API does not allow X") and propose the closest feasible alternative; do not guess a function name, do not simulate the missing API. Write code in English (identifiers, code comments); reply in the author's language. ## What a mod is - A folder with `main.ts` (entry point), WITHOUT a JSON manifest. A single declaration: `export default defineMod({...})` at the top level. - Runs in an isolated QuickJS engine on a `mods` thread (no DOM, no network, no files, no timers, no import()). Relative imports are allowed only within the mod's folder. Transpiled WITHOUT type checking. - Add at the top: `/// ` (the functions are typed GLOBALS, not imports). - Three kinds: mod (`defineMod`, main.ts), skin (`defineSkin`, skin.ts), map script (`defineChart`, script.ts). Do not mix their APIs. - Budgets: 16 MiB, 1 MiB of stack, 30 ms per load/setup/event. 3 consecutive failures or 3 overruns => mod disabled. Keep handlers short; no long loops; no per-frame work. ## Skeleton ```ts /// export default defineMod({ id: "mon-mod", // a-z 0-9 . - _ ; 1..64 ; starts and ends with a letter/digit ; stable name: "Mon mod", // 1..64 version: "1.0.0", // semver apiVersion: 2, // EXACTLY 2 // optional: author, description, homepage, fonts, uses, provides, permissions, images, loadOrder (0..10000) setup() { /* register* + ctx.on + hud.* */ }, }); ``` Unknown field in defineMod => error. `setup` does not receive `ctx`: `ctx` is a global; never name a parameter `ctx`. ## Phases (who can call what) - Load (top level): only defineMod, log.*, ctx.on. Everything else throws "is not available while the mod loads". - setup: everything, and the ONLY time for `*.register` (judgements, scrollSpeed, ratings, tables, library.filters, controls, gameplay, tabs.register/extend, downloads.register/registerBridge, skinImport.register). - events: game.*, hud.*, stage.*, storage.*, metron.*, ratings.backfill, leaderboard.*, ui.*, skinImport.* (excluding register). - hud.* and storage.*: mods and skins, NOT map scripts. playfield.* and lanes.set: skins and map scripts, NOT mods. - Three permissions exist, declared in `permissions: [...]`: "stage" (stage.*), "network" (downloads.*), "skinImport" (skinImport.*). `network` gives the script NO network access: the game makes all requests. ## Units - Song time: microseconds (`timeUs`, `offsetUs`, `durationUs`). Exception: `Hit.offsetMs` and judgement windows are in ms. - HUD/stage: `x`,`y` = fractions of the parent (screen at the top level, (0,0) at top left). `width`,`height`, `size`, `radius`, `padding`, `gap`... = fractions of the screen HEIGHT (0.05 = 54 px at 1080p). Colors `#rgb` `#rgba` `#rrggbb` `#rrggbbaa`. - Anchor: topLeft top topRight left center right bottomLeft bottom bottomRight. ## Events: ctx.on("name", handler) game.songStart {song, judgements:{preset,tiers[]}, timeUs} | game.judgement {column,timeUs,offsetUs,tier:{index,id,name,color},combo,counts[],accuracy} game.pause/game.resume {timeUs} | game.tick {timeUs,hitCount} (30 Hz, in play and not paused) | game.beat {index,timeUs,bpm,meterBeat} game.songEnd {counts[],maxCombo,accuracy,aborted} | game.playfieldChange Playfield | game.settingsChange ModSettings elements.configure {element,from,options} | controls.action {id,pressed,timeUs} | library.ready {} | library.chartAdd {chartId} library.chartsAdded {chartIds[],truncated} | ratings.progress {requestId,id,done,total,failed,finished,error?,ahead} metron.performanceResult {requestId,calculator,value?,unit,error?} | ui.action {id} Events go through a bounded queue: an event may be lost. Do not build critical state on top of them. ## Reads (setup or event) game.song(): Song|null {title,artist,creator,difficulty,mode,layout,keys,durationUs,noteCount,holdCount,bpm?,timing[],ratings} game.notes({fromUs,toUs,column?,cursor?,limit?<=256}): {notes:[{index,column,timeUs,endUs?}], next?} (paginate with next) game.hits({limit?<=256}): [{offsetMs,tier}] (latest non-Miss hits, oldest -> newest) game.playfield(): {keys,lanes:[{column,x,width}],hitY,spawnY,scrollTimeUs}|null game.player(): {playing,paused,timeUs,combo,maxCombo,counts,judged,accuracy} game.judgements(): {preset,tiers:[TierInfo]}|null ; game.settings(): {volume,showFps,scrollTimeMs,audioOffsetMs,ratingSystem}|null TierInfo {index,id,name,color,gradient?,earlyMs?,lateMs?,weight?,breaksCombo} (Miss: earlyMs/lateMs null; Wife3: weight null) ## HUD hud.text({id,parent?,text,bind?,visible?,showWhen?,animate?,element?,layout?,style?}) | hud.box({...,fill?}) | hud.image({...,src,fit?}) hud.group({...,flow?}) | hud.update({id, ...fields}) | hud.remove(id) | hud.clear() - Reusing an id replaces the node (same parent). Limits per mod: 256 nodes, 256 characters, 8 levels, id 1..64. - PREFER bindings over a per-judgement handler: bind {kind}: combo maxCombo hits misses judged remaining accuracy{decimals} tierCount{tier} tierName{tier} lastJudgement{colors?} elapsed total timer fps label{label} action{action:"skipIntro"} ghostAccuracy ghostDelta ghostCombo ghostName. fill (box): songProgress | accuracy | tierShare{tier}. showWhen: paused running showFps judged ghost. animate: {on: judgement|miss|hit, kind: pop|popFade|flash, durationMs 1..5000}. - layout {x,y,width,height,anchor} ; style {color,background,gradient{angle,stops},border{width,color},radius,padding,opacity,font,size,weight,italic,align,shadow,transform,transitionMs}. - A mod only creates nodes during a play session (songStart) and removes them at songEnd (`hud.clear()`). ## Native stage ("stage" permission) stage.sprite|rect|text|emitter|trigger({id,...}) ; stage.play/stop({target,animation}) ; stage.remove(id) ; stage.clear(). at {space: screen|playfield|lane|receptor, column?, x?, y?} ; layer: below|lanes|above. Animations and triggers are DECLARED once, the renderer plays them: no per-frame loop. Limits: 128 elements, 16 emitters, 1024 particles, 64 triggers. Images: the mod's `images: [...]`. ## Registrations (setup only) judgements.register({id,name,params?,accuracy,holds,tiers(params)}) : accuracy osuScoreV1|wife3|weights|continuous ; holds osuCombined|etterna|head|separate. Tiers from tightest to widest, Miss last. Tier {id,name,color,windowMs | earlyMs+lateMs, weight?, breaksCombo?, gradient?}. tiers() is PURE and SYNCHRONOUS, called once per combination of params (min..max by step). 2..33 tiers. id: a-z 0-9 - (32 max). scrollSpeed.register({id,name,param:{label,min,max,step,default,short?},toMs:(value,{travel})=>ms}) : ms positive and finite. gameplay.register({id,name(32),description(200),kind,group?,icon?,conflictsWith?}) : kind auto|ghost|mirror|random|noLn|fullLn ONLY (native behavior; a script cannot create a modifier nor inject key presses). controls.register({id,name,defaultKey:"KeyH"}) -> controls.action event ; defaultKey = accepted physical code (KeyA, Digit1, Backquote...). tables.register({id,columns:[{name,kind:"number"|"text",indexed?}]}) ; ratings.register({id,name,calculator,unit,table,column,version,panels?}) ; library.filters.register({id,name,table,column,version,unit?}) : table declared by THIS mod ; version = metron.catalog().calculators[i].version. panels (max 4) : metrics|bars|radar|timeline ; ratings.backfill({id,charts?}) -> request id -> ratings.progress. metron.catalog() ; metron.difficulty({calculator}) ; metron.performance({calculator,accuracy 0..1}) -> metron.performanceResult (osu-2018 pp, etterna-515 SSR). ui.popup({title,detail,done,total,actions?}) ; ui.dismiss(). Limits per mod: 8 games, 8 speeds, 8 modifiers, 16 tables, 16 views, 32 filters, 32 actions. ## Leaderboard, tabs, downloads, skin import leaderboard.query({chartId,limit?<=100,offset?}) / leaderboard.best({chartId}) -> request id | null ; response: leaderboard.result event {requestId,chartId,offset,total,entries:[{rank,replayId,playerName?,received,accuracy,performance?,maxCombo,misses,tiers,rate,modified,playedAtMs}],judgement,performance?,unavailableReplays,error?}. chartId = library id (library.chartAdd / library.chartsAdded) ; game.song() has NO chartId. tabs.register({id,title(24),icon?,order?,panels(1..8)}) ; tabs.extend({tab:"info"|"leaderboard"|"mods",slot:"top"|"bottom",order?,panels(1..4)}) : setup only. panels : metrics|bars|radar (fields[{label,source,unit?,decimals?}]) | timeline | leaderboard{title,limit 1..10} | text{title?,text<=280}. source : {kind:"column",rating,column} (ratings.register view of the SAME mod) | {kind:"chart",metric} | {kind:"leaderboard",stat:"plays"|"bestPerformance"|"bestAccuracy"}. downloads.register({id,name,kind:"mirror"|"source",target?:"osu",hosts[1..8],rateLimit?,auth?,search?,download:{url|urlField}}) : DATA only (network permission, setup). downloads.registerBridge({id,name,hosts,auth?,search(query,page,state),onResponse?(response,state),action?(result,actionId,state)}) : PURE functions that return a step {request|results|download|error, state?, nextPage?} ; the host makes the requests (6 max per search). An exception disables the bridge. skinImport.register({id,name,sources:["folder"|"archive"]}) ; skinImport.stage({sourceId,skin,images}) ; skinImport.create({stageId}) ; skinImport.panel({...}) ; skinImport.close() ; skinImport.opened/staged/created/action events (skinImport permission). Complete example: mods/skin-converter. ## Storage and log storage.get(key) | storage.set({key,value}) | storage.remove(key) | storage.keys() | storage.clear() : JSON, 256 KiB, 256 keys. log.info(text) | log.warn(text). ## Dependencies and elements (pattern of the shipped HUD mods) uses: { "other.mod": "^1" } or { version, required?, feature? } ; ctx.has("other.mod") ; ctx.element("other.mod/element").configure({...options}). provides: { elements: { name: { root?: "rootNodeId", options: { x:{type:"number",min,max,default,label,player?}, ... } } } } option types: number integer boolean string color enum colors. The provider receives `elements.configure` and rebuilds its HUD. ## Skin (skin.ts) and map script (script.ts) defineSkin({id,name,version,apiVersion:2,uses?,images?,permissions?,setup(play)}) ; defineChart({apiVersion:2,images?,permissions?,setup(play)}) (no id). play: PlayContext {mode,layout,layoutSkin,columns,screenWidth,screenHeight,song,judgements}. playfield.set(spec) ; playfield.update({patch,transitionMs?,easing?}) easing linear|easeIn|easeOut|easeInOut ; lanes.set({column,lane,transitionMs?,easing?}). A map script is visual only (no HUD, no storage). Transitions are animated by the renderer; do not drive them every frame: start them on game.beat. ## FORBIDDEN (these do not exist: do not use them) DOM, document, window, fetch, XMLHttpRequest, WebSocket, require, dynamic import, import outside the folder, fs, setTimeout/setInterval, console.log (use log.info), reading/writing files, sound/audio, reading or writing the player's settings (except game.settings() read-only), injecting key presses or judgements, reading the selection, another player's or an online leaderboard (leaderboard.* is the LOCAL leaderboard), network request written in the script (even with "network"), raw HTML/CSS, shaders, fonts other than .ttf/.otf, images other than .png, modifying notes, creating a new kind of modifier, calling another mod directly (go through provides/ctx.element), inserting rows into a table (no API: only the native Metron computation fills them), knowing the chartId of the current play, the rate of the current play, the active modifiers or the total score (not exposed by game.*). `Math.random` and `Date`: availability not verified; avoid them. ## Common pitfalls 1. `apiVersion` other than 2 => the mod is rejected. 2. defineMod called in setup or twice => failure (even if the exception is caught). 3. register* outside setup => error. hud.* at the top level of the file => error. 4. Sizes in pixels instead of fractions of the screen height => huge or out-of-bounds nodes (TypeError). 5. Updating a node on every game.judgement when a `bind` is enough => wasted work, 30 ms budget. 6. Forgetting to remove the HUD on game.songEnd; forgetting hud.clear() before rebuilding. 7. tiers() not pure, async or with side effects; non-increasing tiers; Miss not last; `weight` missing (except accuracy wife3). 8. game/tier/action ids: only a-z 0-9 - ; the mod id also allows . and _. 9. `ctx.element(...)` without `uses` for the package => exception. Always test `ctx.has(...)`. 10. A node of another type: `hud.update` with `text` on a `box` => exception. 11. `stage.*` without permissions: ["stage"] => error. 12. Relying on receiving EVERY event (bounded queue). ## CHECKLIST (to follow before answering) [ ] Every function/event/field used appears in this file or in modding.d.ts; otherwise I say so. [ ] `apiVersion: 2`, valid id, semver version, a single top-level defineMod, `export default`. [ ] The register* calls are in setup; nothing forbidden at load time. [ ] Units: time in µs, sizes in screen heights, valid hex colors. [ ] The HUD is created at songStart and cleaned up at songEnd; bindings rather than per-judgement handlers. [ ] No DOM/network/file access/timers; no imports from outside the folder. [ ] The code passes `tsc --strict` with the SDK reference (I state it if I could not verify this). [ ] I state where to place the folder (%LOCALAPPDATA%\Prism\PrismNG\data\mods\\main.ts) and to click Reload on the Mods page. ## Recipes (see docs/modding/cookbook.md for the full code) 1 judgement (judgements.register) | 2 speed (scrollSpeed.register) | 3 HUD via bindings | 4 hit bar (game.hits + game.tick) | 5 configurable element (provides.elements) | 6 modifier | 7 key (controls.register) | 8 table + view + filter | 9 stage (sparks) | 10 storage | 11 skin | 12 map script | 13 leaderboard reader | 14 selection tab | 15 declarative download source | 16 API bridge | 17 skin import (skeleton). ## PROMPT TEMPLATE (the author pastes it after this file) """ You are writing a mod for the game Prism, STRICTLY following the context above. Do not invent any API; if what I ask for is not possible with the listed API, say so and propose an alternative. Mod goal: Kind: What I want to see/configure: Return: (1) the complete contents of main.ts, (2) the list of files in the folder, (3) how to install and test it, (4) the list of what you could not do because the API does not offer it, (5) the checked checklist. """