commit 8ac2b8542b8e2bf1928cfd90949b8c1704d6d4d1 Author: Claude Date: Sat Sep 26 21:22:49 2026 +0200 Publish built kitchen-sink and hello-world example plugins diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..efb9808 --- /dev/null +++ b/LICENSE @@ -0,0 +1,24 @@ +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to diff --git a/README.md b/README.md new file mode 100644 index 0000000..a6753ec --- /dev/null +++ b/README.md @@ -0,0 +1,8 @@ +# VRCNext example plugins + +Built bundles of the example plugins from +[vrcnext-plugin-system](https://github.com/vrcnext-plugins/vrcnext-plugin-system/tree/main/examples), +published so they can be installed from the Plugins tab by pasting this repository's URL. + +Sources live in the plugin-system repository; this repository only carries `vrcnext-plugins.json` +and the `dist/` output of `./scripts/build.sh`. diff --git a/dist/hello-world.js b/dist/hello-world.js new file mode 100644 index 0000000..832e56b --- /dev/null +++ b/dist/hello-world.js @@ -0,0 +1,2 @@ +var s={greetOnFriendOnline:{kind:"boolean",label:"Log a line when a friend comes online",description:"Writes to the plugin log, not to VRChat.",default:!0},tone:{kind:"select",label:"Greeting tone",default:"friendly",options:[{value:"friendly",label:"Friendly"},{value:"terse",label:"Terse"}]}},z={id:"hello-world",settings:s,activate(e){e.logger.info(`Hello from v${e.version}.`);let t=e.settings.get("tone")==="terse"?"Online:":"Welcome back,";e.events.on("friendTimelineEvent",n=>{e.settings.get("greetOnFriendOnline")&&n.type==="online"&&e.logger.info(`${t} ${n.friendName}`)}),e.ui.addSettingsCard({title:"Hello World",icon:"back_hand",render:n=>{n.appendChild(e.ui.createToggleRow("Show a toast right now",!1,i=>{i&&e.ui.toast({message:"Hello from a VRCNext plugin."})}))}})},deactivate(){}};export{z as default}; +//# sourceMappingURL=hello-world.js.map diff --git a/dist/hello-world.js.map b/dist/hello-world.js.map new file mode 100644 index 0000000..f4745ae --- /dev/null +++ b/dist/hello-world.js.map @@ -0,0 +1,7 @@ +{ + "version": 3, + "sources": ["../src/index.ts"], + "sourcesContent": ["/**\n * Minimal VRCNext plugin.\n *\n * Demonstrates the four things every plugin has available: typed settings, the host event\n * stream, UI injection using VRCNext's own markup, and disposal.\n */\n\nimport { definePlugin, type PluginId, type SettingsSchema } from '@vrcnext/plugin-api';\n\nconst settings = {\n greetOnFriendOnline: {\n kind: 'boolean',\n label: 'Log a line when a friend comes online',\n description: 'Writes to the plugin log, not to VRChat.',\n default: true,\n },\n tone: {\n kind: 'select',\n label: 'Greeting tone',\n default: 'friendly',\n options: [\n { value: 'friendly', label: 'Friendly' },\n { value: 'terse', label: 'Terse' },\n ],\n },\n} as const satisfies SettingsSchema;\n\nexport default definePlugin({\n id: 'hello-world' as PluginId,\n settings,\n\n activate(ctx) {\n ctx.logger.info(`Hello from v${ctx.version}.`);\n\n // `tone` is the literal union 'friendly' | 'terse', not a bare string.\n const greeting = ctx.settings.get('tone') === 'terse' ? 'Online:' : 'Welcome back,';\n\n ctx.events.on('friendTimelineEvent', (payload) => {\n if (!ctx.settings.get('greetOnFriendOnline')) return;\n if (payload.type !== 'online') return;\n ctx.logger.info(`${greeting} ${payload.friendName}`);\n });\n\n ctx.ui.addSettingsCard({\n title: 'Hello World',\n icon: 'back_hand',\n render: (card) => {\n card.appendChild(\n ctx.ui.createToggleRow(\n 'Show a toast right now',\n false,\n (checked) => {\n if (checked) ctx.ui.toast({ message: 'Hello from a VRCNext plugin.' });\n },\n ),\n );\n },\n });\n },\n\n deactivate() {\n // Listeners and panels registered through `ctx` are torn down by the host; only\n // plugin-owned resources created outside it need cleaning up here.\n },\n});\n"], + "mappings": "AASA,IAAMA,EAAW,CACf,oBAAqB,CACnB,KAAM,UACN,MAAO,wCACP,YAAa,2CACb,QAAS,EACX,EACA,KAAM,CACJ,KAAM,SACN,MAAO,gBACP,QAAS,WACT,QAAS,CACP,CAAE,MAAO,WAAY,MAAO,UAAW,EACvC,CAAE,MAAO,QAAS,MAAO,OAAQ,CACnC,CACF,CACF,EAEOC,EAAqB,CAC1B,GAAI,cACJ,SAAAD,EAEA,SAASE,EAAK,CACZA,EAAI,OAAO,KAAK,eAAeA,EAAI,OAAO,GAAG,EAG7C,IAAMC,EAAWD,EAAI,SAAS,IAAI,MAAM,IAAM,QAAU,UAAY,gBAEpEA,EAAI,OAAO,GAAG,sBAAwBE,GAAY,CAC3CF,EAAI,SAAS,IAAI,qBAAqB,GACvCE,EAAQ,OAAS,UACrBF,EAAI,OAAO,KAAK,GAAGC,CAAQ,IAAIC,EAAQ,UAAU,EAAE,CACrD,CAAC,EAEDF,EAAI,GAAG,gBAAgB,CACrB,MAAO,cACP,KAAM,YACN,OAASG,GAAS,CAChBA,EAAK,YACHH,EAAI,GAAG,gBACL,yBACA,GACCI,GAAY,CACPA,GAASJ,EAAI,GAAG,MAAM,CAAE,QAAS,8BAA+B,CAAC,CACvE,CACF,CACF,CACF,CACF,CAAC,CACH,EAEA,YAAa,CAGb,CACF", + "names": ["settings", "index_default", "ctx", "greeting", "payload", "card", "checked"] +} diff --git a/dist/kitchen-sink.js b/dist/kitchen-sink.js new file mode 100644 index 0000000..69f1fde --- /dev/null +++ b/dist/kitchen-sink.js @@ -0,0 +1,41 @@ +var a=` +.ks-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); + gap: 10px; + padding: 10px 0; +} + +.ks-stat { + background: var(--bg-input); + border-radius: 10px; + padding: 12px 14px; +} + +.ks-stat-label { + font-size: calc(11px + var(--fs-off, 0px)); + color: var(--tx3); +} + +.ks-stat-value { + font-size: calc(20px + var(--fs-off, 0px)); + font-weight: 600; + color: var(--tx0); + font-variant-numeric: tabular-nums; +} + +.ks-log { + max-height: 220px; + overflow-y: auto; + font-family: ui-monospace, monospace; + font-size: calc(11px + var(--fs-off, 0px)); + color: var(--tx2); + background: var(--bg-input); + border-radius: 8px; + padding: 8px 10px; +} + +.ks-log-line { white-space: pre-wrap; word-break: break-word; } +.ks-log-line + .ks-log-line { margin-top: 3px; } +`;function d(e){let t=document.createElement("div");return t.className="ks-log",e.appendChild(t),{append(n){let i=document.createElement("div");i.className="ks-log-line";let o=new Date().toLocaleTimeString();for(i.textContent=`${o} ${n}`,t.appendChild(i);t.childElementCount>200;)t.firstElementChild?.remove();t.scrollHeight-t.scrollTop-t.clientHeight<40&&(t.scrollTop=t.scrollHeight)},clear(){t.replaceChildren()}}}var u={watchGameLog:{kind:"boolean",label:"Follow the VRChat game log",description:"Streams player joins and world changes into the panel below.",default:!0},oscParameter:{kind:"string",label:"OSC parameter to pulse",description:"Sent to /avatar/parameters/ by the dashboard button.",default:"VRCEmote",placeholder:"VRCEmote"},oscValue:{kind:"number",label:"OSC value",default:1,min:0,max:255,step:1},verbosity:{kind:"select",label:"Log verbosity",default:"normal",options:[{value:"quiet",label:"Quiet"},{value:"normal",label:"Normal"},{value:"loud",label:"Everything"}]}},pe={id:"kitchen-sink",settings:u,activate(e){let t={gameLogCount:0,oscParamCount:0,panel:void 0,log:n=>{e.settings.get("verbosity")!=="quiet"&&(e.logger.info(n),t.panel?.append(n))}};e.ui.injectCss(a),p(e),h(e,t),v(e,t),b(e,t),y(e,t),S(e,t),C(e,t),w(e,t),k(e,t),N(e,t),e.logger.info(`Kitchen Sink v${e.version} ready.`)},deactivate(){}};function p(e){e.disposables.add(e.settings.onChange(t=>{e.logger.debug(`Settings changed; verbosity is now ${t.verbosity}.`)}))}function h(e,t){e.events.on("friendTimelineEvent",n=>{t.log(`[friend] ${n.friendName} \u2192 ${n.type}`)}),e.events.on("vrcCurrentInstance",n=>{if(typeof n!="object"||n===null)return;let i=n.worldName;typeof i=="string"&&t.log(`[instance] now in ${i}`)}),e.events.onAny(n=>{e.settings.get("verbosity")==="loud"&&e.logger.debug(`event ${n.type}`)})}function v(e,t){e.gameLog.on(n=>{e.settings.get("watchGameLog")&&(t.gameLogCount+=1,t.log(`[gamelog] ${n.type}: ${n.message}`))}),e.gameLog.onType("OnPlayerJoined",n=>{t.log(`[join] ${n.detail||n.message}`)}),e.gameLog.history(e.signal).then(n=>{e.logger.info(`Game log backlog: ${String(n.length)} entries.`)}).catch(n=>{e.signal.aborted||e.logger.warn(`Could not read the game log: ${String(n)}`)})}function b(e,t){if(!e.osc.available){t.log("[osc] OSC is Windows-only in VRCNext; skipping.");return}e.osc.connect(),e.osc.onParam(n=>{t.oscParamCount+=1,e.settings.get("verbosity")==="loud"&&t.log(`[osc] ${n.name} = ${String(n.value)}`)}),e.osc.onAvatarChange(n=>{t.log(`[osc] avatar ${n.avatarId} with ${String(n.parameters.length)} parameters`)})}function c(e){if(!e.osc.available)return"OSC unavailable (Windows only)";let t=e.settings.get("oscParameter"),n=e.settings.get("oscValue");return e.osc.send(t,"int",n),`${t} = ${String(n)}`}function y(e,t){e.deepLinks.onPrefix("wrld",n=>{t.log(`[link] world ${n.id}`)}),e.deepLinks.on(n=>{t.log(`[link] ${n.prefix}:${n.id}${n.action?` (${n.action})`:""}`)})}function S(e,t){e.router.get("stats",()=>Response.json({gameLogEvents:t.gameLogCount,oscParams:t.oscParamCount,verbosity:e.settings.get("verbosity")})),e.router.get("greet/:name",n=>Response.json({hello:n.params.name??"world"})),e.router.post("osc/pulse",()=>Response.json({sent:c(e)})),e.logger.info(`Routes mounted at ${e.router.base.pathname}`)}function C(e,t){e.contextMenu.contribute(n=>{let i=[{kind:"divider"},{kind:"item",icon:"auto_awesome",label:"Kitchen Sink: log this element",onSelect:()=>{t.log(`[ctx] ${n.element.tagName.toLowerCase()}`)}},{kind:"submenu",icon:"tune",label:"Kitchen Sink: verbosity",items:()=>u.verbosity.options.map(o=>({kind:"item",icon:"adjust",label:o.label,checked:e.settings.get("verbosity")===o.value,onSelect:()=>{e.settings.set("verbosity",o.value)}}))}];if(n.entity!==void 0){let o=n.entity;i.push({kind:"item",icon:"content_copy",label:`Copy ${o.type} id`,onSelect:()=>{navigator.clipboard.writeText(o.id)}})}return i})}function w(e,t){e.gameLog.onType("OnPlayerJoined",n=>{let i=n.detail||n.message;e.notifications.toast({message:`${i} joined.`}),e.notifications.desktopAvailable?e.notifications.desktop({title:"Player joined",subtitle:i,accent:"info"}):t.log("[notify] desktop/VR notifications are Windows-only.")}),e.events.on("friendTimelineEvent",n=>{n.type==="online"&&e.notifications.notifToast({kind:"notification",sender:n.friendName,message:"came online"})})}async function k(e,t){if(!await e.native.ready){t.log(`[native] no companion at ${e.native.endpoint}; VR targets unavailable.`);return}let n=await e.native.targets();t.log(`[native] targets: ${n.map(o=>o.name).join(", ")||"none"}`),e.gameLog.onType("OnPlayerJoined",o=>{let s=o.detail||o.message;e.native.notify({title:"Player joined",content:s,timeoutSecs:4,icon:"user-available",overrides:{wayvr:{content:`${s} joined the instance`,height:200,opacity:.85,alwaysShow:!0}}})}),e.events.on("friendTimelineEvent",o=>{o.type==="online"&&e.native.notify({title:o.friendName,content:"came online",sinks:["wayvr"],timeoutSecs:3,opacity:.7})});let i=await e.native.describe();i!==void 0&&t.log(`[native] companion ${i.version}, services: ${Object.keys(i.services).join(", ")}`)}function N(e,t){e.ui.addDashboardCard({title:"Kitchen Sink",icon:"auto_awesome",order:10,render:n=>{let i=document.createElement("div");i.className="ks-grid",i.append(l("Game log events",()=>t.gameLogCount),l("OSC parameters",()=>t.oscParamCount)),n.appendChild(i);let o=document.createElement("button");o.textContent="Send OSC pulse",o.addEventListener("click",()=>{e.ui.toast({message:`Sent ${c(e)}.`})}),n.appendChild(o)}}),L(e,t),e.ui.addSettingsCard({title:"Kitchen Sink",icon:"auto_awesome",render:n=>{n.appendChild(e.ui.createToggleRow("Follow the VRChat game log",e.settings.get("watchGameLog"),o=>{e.settings.set("watchGameLog",o)}));let i=document.createElement("button");i.textContent="Reset settings",i.addEventListener("click",()=>{e.notifications.confirm({title:"Reset Kitchen Sink",message:"Restore every Kitchen Sink setting to its default?",confirmLabel:"Reset",icon:"restart_alt"}).then(async o=>{o&&(await e.settings.reset(),e.notifications.toast({message:"Kitchen Sink settings reset."}))})}),n.appendChild(i)}})}function L(e,t){e.ui.addNavTab({label:"Kitchen Sink",icon:"auto_awesome",render:n=>{let i=e.ui.kit,o=i.card({title:"Live activity",icon:"radar"});t.panel=d(o);let s=i.emptyState("No request yet.");n.append(i.layout(i.grid([i.card({title:"Counters",icon:"bolt",children:[i.stat({label:"Game log events",value:String(t.gameLogCount)}),i.stat({label:"OSC parameters",value:String(t.oscParamCount)})]}),i.card({title:"Capabilities",icon:"tune",children:[i.row({label:"OSC",value:e.osc.available?i.badge("ok","Ready"):i.badge("warn","Windows only"),detail:"Sent through VRCNext\u2019s own sockets."}),i.row({label:"Native companion",value:e.native.available?i.badge("ok","Connected"):i.badge("neutral","Not running"),detail:e.native.endpoint}),i.toggleRow({label:"Follow the game log",detail:"Mirrors VRChat log lines into the panel below.",value:e.settings.get("watchGameLog"),onChange:r=>{e.settings.set("watchGameLog",r)}})]}),i.card({title:"In-page routes",icon:"account_tree",children:[i.description("Plugin routes are reachable from this page only, never from curl."),i.buttonRow(i.button({label:"GET stats",icon:"download",onClick:()=>{e.router.fetch("stats").then(async r=>{s.textContent=await r.text()}).catch(r=>{s.textContent=String(r)})}})),s]})]),o))}})}function l(e,t){let n=document.createElement("div");n.className="ks-stat";let i=document.createElement("div");i.className="ks-stat-label",i.textContent=e;let o=document.createElement("div");return o.className="ks-stat-value",o.textContent=String(t()),new IntersectionObserver(r=>{for(let g of r)g.isIntersecting&&(o.textContent=String(t()))}).observe(n),n.append(i,o),n}export{pe as default}; +//# sourceMappingURL=kitchen-sink.js.map diff --git a/dist/kitchen-sink.js.map b/dist/kitchen-sink.js.map new file mode 100644 index 0000000..2039aa1 --- /dev/null +++ b/dist/kitchen-sink.js.map @@ -0,0 +1,7 @@ +{ + "version": 3, + "sources": ["../src/styles.ts", "../src/log-panel.ts", "../src/index.ts"], + "sourcesContent": ["/**\n * Stylesheet injected by the plugin.\n *\n * Colours come from VRCNext's CSS custom properties rather than literals, so the plugin follows\n * the user's theme \u2014 including custom themes \u2014 instead of fighting it.\n */\n\nexport const PLUGIN_CSS = `\n.ks-grid {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));\n gap: 10px;\n padding: 10px 0;\n}\n\n.ks-stat {\n background: var(--bg-input);\n border-radius: 10px;\n padding: 12px 14px;\n}\n\n.ks-stat-label {\n font-size: calc(11px + var(--fs-off, 0px));\n color: var(--tx3);\n}\n\n.ks-stat-value {\n font-size: calc(20px + var(--fs-off, 0px));\n font-weight: 600;\n color: var(--tx0);\n font-variant-numeric: tabular-nums;\n}\n\n.ks-log {\n max-height: 220px;\n overflow-y: auto;\n font-family: ui-monospace, monospace;\n font-size: calc(11px + var(--fs-off, 0px));\n color: var(--tx2);\n background: var(--bg-input);\n border-radius: 8px;\n padding: 8px 10px;\n}\n\n.ks-log-line { white-space: pre-wrap; word-break: break-word; }\n.ks-log-line + .ks-log-line { margin-top: 3px; }\n`;\n", "/**\n * Small scrolling log view used by the Kitchen Sink tab.\n *\n * Kept in its own module to show the normal pattern: UI helpers separate from the plugin's\n * lifecycle wiring, so `index.ts` stays readable.\n */\n\nconst MAX_LINES = 200;\n\nexport interface LogPanel {\n append(message: string): void;\n clear(): void;\n}\n\nexport function createLogPanel(parent: HTMLElement): LogPanel {\n const list = document.createElement('div');\n list.className = 'ks-log';\n parent.appendChild(list);\n\n return {\n append(message: string): void {\n const line = document.createElement('div');\n line.className = 'ks-log-line';\n const time = new Date().toLocaleTimeString();\n line.textContent = `${time} ${message}`;\n list.appendChild(line);\n\n while (list.childElementCount > MAX_LINES) list.firstElementChild?.remove();\n\n // Only chase the tail when the user has not scrolled up to read history.\n const atBottom = list.scrollHeight - list.scrollTop - list.clientHeight < 40;\n if (atBottom) list.scrollTop = list.scrollHeight;\n },\n\n clear(): void {\n list.replaceChildren();\n },\n };\n}\n", "/**\n * Kitchen Sink \u2014 the reference VRCNext plugin.\n *\n * Exercises every capability the host provides, in the order a plugin author is likely to need\n * them. Each section is small and commented so it can be copied out on its own.\n */\n\nimport {\n definePlugin,\n type ContextMenuEntry,\n type PluginContext,\n type PluginId,\n type SettingsSchema,\n} from '@vrcnext/plugin-api';\n\nimport { PLUGIN_CSS } from './styles.js';\nimport { createLogPanel, type LogPanel } from './log-panel.js';\n\nconst settings = {\n watchGameLog: {\n kind: 'boolean',\n label: 'Follow the VRChat game log',\n description: 'Streams player joins and world changes into the panel below.',\n default: true,\n },\n oscParameter: {\n kind: 'string',\n label: 'OSC parameter to pulse',\n description: 'Sent to /avatar/parameters/ by the dashboard button.',\n default: 'VRCEmote',\n placeholder: 'VRCEmote',\n },\n oscValue: {\n kind: 'number',\n label: 'OSC value',\n default: 1,\n min: 0,\n max: 255,\n step: 1,\n },\n verbosity: {\n kind: 'select',\n label: 'Log verbosity',\n default: 'normal',\n options: [\n { value: 'quiet', label: 'Quiet' },\n { value: 'normal', label: 'Normal' },\n { value: 'loud', label: 'Everything' },\n ],\n },\n} as const satisfies SettingsSchema;\n\ntype Ctx = PluginContext;\n\n/** Shared mutable state for the session. Reset each activation. */\ninterface State {\n gameLogCount: number;\n oscParamCount: number;\n panel: LogPanel | undefined;\n /** Writes to both the plugin logger and the on-screen panel, honouring `verbosity`. */\n log(message: string): void;\n}\n\nexport default definePlugin({\n id: 'kitchen-sink' as PluginId,\n settings,\n\n activate(ctx) {\n const state: State = {\n gameLogCount: 0,\n oscParamCount: 0,\n panel: undefined,\n log: (message) => {\n if (ctx.settings.get('verbosity') === 'quiet') return;\n ctx.logger.info(message);\n state.panel?.append(message);\n },\n };\n\n // 1. Custom CSS, removed automatically on deactivate.\n ctx.ui.injectCss(PLUGIN_CSS);\n\n installSettingsWatch(ctx);\n installEvents(ctx, state);\n installGameLog(ctx, state);\n installOsc(ctx, state);\n installDeepLinks(ctx, state);\n installRoutes(ctx, state);\n installContextMenu(ctx, state);\n installNotifications(ctx, state);\n // Fire-and-forget: the companion probe must not hold up the rest of activation.\n void installNative(ctx, state);\n installUi(ctx, state);\n\n ctx.logger.info(`Kitchen Sink v${ctx.version} ready.`);\n },\n\n deactivate() {\n // Everything above registered through `ctx`, so the host tears it all down. Only resources\n // created outside the context would need explicit cleanup here.\n },\n});\n\n/** 2. React to settings changes. `values` keeps its literal types. */\nfunction installSettingsWatch(ctx: Ctx): void {\n ctx.disposables.add(\n ctx.settings.onChange((values) => {\n ctx.logger.debug(`Settings changed; verbosity is now ${values.verbosity}.`);\n }),\n );\n}\n\n/** 3. Typed host events, plus the untyped escape hatch. */\nfunction installEvents(ctx: Ctx, state: State): void {\n // Fully typed: `payload.friendName` is a string.\n ctx.events.on('friendTimelineEvent', (payload) => {\n state.log(`[friend] ${payload.friendName} \u2192 ${payload.type}`);\n });\n\n // Not in the verified map, so the payload is `unknown` and must be narrowed.\n ctx.events.on('vrcCurrentInstance', (payload) => {\n if (typeof payload !== 'object' || payload === null) return;\n const worldName = (payload as { worldName?: unknown }).worldName;\n if (typeof worldName === 'string') state.log(`[instance] now in ${worldName}`);\n });\n\n // Diagnostics: every event flowing from VRCNext.\n ctx.events.onAny((envelope) => {\n if (ctx.settings.get('verbosity') === 'loud') {\n ctx.logger.debug(`event ${envelope.type}`);\n }\n });\n}\n\n/** 4. VRChat game log \u2014 the lowest-latency source of in-game activity. */\nfunction installGameLog(ctx: Ctx, state: State): void {\n ctx.gameLog.on((entry) => {\n if (!ctx.settings.get('watchGameLog')) return;\n state.gameLogCount += 1;\n state.log(`[gamelog] ${entry.type}: ${entry.message}`);\n });\n\n ctx.gameLog.onType('OnPlayerJoined', (entry) => {\n state.log(`[join] ${entry.detail || entry.message}`);\n });\n\n // Backlog is fetched once, aborted if the plugin is disabled mid-flight.\n void ctx.gameLog\n .history(ctx.signal)\n .then((entries) => { ctx.logger.info(`Game log backlog: ${String(entries.length)} entries.`); })\n .catch((error: unknown) => {\n if (ctx.signal.aborted) return;\n ctx.logger.warn(`Could not read the game log: ${String(error)}`);\n });\n}\n\n/** 5. OSC in and out. Windows-only in VRCNext \u2014 always guard on `available`. */\nfunction installOsc(ctx: Ctx, state: State): void {\n if (!ctx.osc.available) {\n state.log('[osc] OSC is Windows-only in VRCNext; skipping.');\n return;\n }\n ctx.osc.connect();\n\n ctx.osc.onParam((event) => {\n state.oscParamCount += 1;\n if (ctx.settings.get('verbosity') === 'loud') {\n state.log(`[osc] ${event.name} = ${String(event.value)}`);\n }\n });\n\n ctx.osc.onAvatarChange((event) => {\n state.log(`[osc] avatar ${event.avatarId} with ${String(event.parameters.length)} parameters`);\n });\n}\n\n/** Sends the configured parameter. Shared by the dashboard button and the HTTP route. */\nfunction pulseOsc(ctx: Ctx): string {\n if (!ctx.osc.available) return 'OSC unavailable (Windows only)';\n const name = ctx.settings.get('oscParameter');\n const value = ctx.settings.get('oscValue');\n ctx.osc.send(name, 'int', value);\n return `${name} = ${String(value)}`;\n}\n\n/** 6. Deep links VRCNext delivers. Custom prefixes are not possible \u2014 see the docs. */\nfunction installDeepLinks(ctx: Ctx, state: State): void {\n ctx.deepLinks.onPrefix('wrld', (event) => {\n state.log(`[link] world ${event.id}`);\n return undefined; // Not handled \u2014 let other plugins see it too.\n });\n\n ctx.deepLinks.on((event) => {\n state.log(`[link] ${event.prefix}:${event.id}${event.action ? ` (${event.action})` : ''}`);\n return undefined;\n });\n}\n\n/** 7. In-page HTTP routes. Reachable from the page, not from outside it. */\nfunction installRoutes(ctx: Ctx, state: State): void {\n ctx.router.get('stats', () =>\n Response.json({\n gameLogEvents: state.gameLogCount,\n oscParams: state.oscParamCount,\n verbosity: ctx.settings.get('verbosity'),\n }),\n );\n\n ctx.router.get('greet/:name', (request) =>\n Response.json({ hello: request.params['name'] ?? 'world' }),\n );\n\n ctx.router.post('osc/pulse', () => Response.json({ sent: pulseOsc(ctx) }));\n\n ctx.logger.info(`Routes mounted at ${ctx.router.base.pathname}`);\n}\n\n/** 8. Context-menu items, dividers and submenus. */\nfunction installContextMenu(ctx: Ctx, state: State): void {\n ctx.contextMenu.contribute((target) => {\n const entries: ContextMenuEntry[] = [\n { kind: 'divider' },\n {\n kind: 'item',\n icon: 'auto_awesome',\n label: 'Kitchen Sink: log this element',\n onSelect: () => { state.log(`[ctx] ${target.element.tagName.toLowerCase()}`); },\n },\n {\n kind: 'submenu',\n icon: 'tune',\n label: 'Kitchen Sink: verbosity',\n items: () =>\n settings.verbosity.options.map(\n (option): ContextMenuEntry => ({\n kind: 'item',\n icon: 'adjust',\n label: option.label,\n checked: ctx.settings.get('verbosity') === option.value,\n onSelect: () => { void ctx.settings.set('verbosity', option.value); },\n }),\n ),\n },\n ];\n\n if (target.entity !== undefined) {\n const entity = target.entity;\n entries.push({\n kind: 'item',\n icon: 'content_copy',\n label: `Copy ${entity.type} id`,\n onSelect: () => { void navigator.clipboard.writeText(entity.id); },\n });\n }\n return entries;\n });\n}\n\n/** 9. Every notification surface VRCNext exposes. */\nfunction installNotifications(ctx: Ctx, state: State): void {\n ctx.gameLog.onType('OnPlayerJoined', (entry) => {\n const who = entry.detail || entry.message;\n\n // In-app toast \u2014 always available.\n ctx.notifications.toast({ message: `${who} joined.` });\n\n // OS tray toast + SteamVR wrist overlay, in one call. Windows only.\n if (ctx.notifications.desktopAvailable) {\n ctx.notifications.desktop({\n title: 'Player joined',\n subtitle: who,\n accent: 'info',\n });\n } else {\n state.log('[notify] desktop/VR notifications are Windows-only.');\n }\n });\n\n // Styled like one of VRCNext's own notification kinds.\n ctx.events.on('friendTimelineEvent', (payload) => {\n if (payload.type !== 'online') return;\n ctx.notifications.notifToast({\n kind: 'notification',\n sender: payload.friendName,\n message: 'came online',\n });\n });\n}\n\n/**\n * 10. The optional native companion \u2014 VR overlay and desktop notification targets.\n *\n * Everything here is written for the companion being absent, because for most users it is. Note\n * the two-step: ask what targets exist, then address them by name. Hard-coding `'wayvr'` would\n * work today and break the moment someone runs a different overlay.\n */\nasync function installNative(ctx: Ctx, state: State): Promise {\n // `await ctx.native.ready`, not `if (ctx.native.available)`. The host's probe is still in flight\n // when plugins activate, so reading the snapshot here is a race: on a fast answer it is true, on\n // a slow one false, and the plugin silently loses its VR notifications.\n if (!(await ctx.native.ready)) {\n state.log(`[native] no companion at ${ctx.native.endpoint}; VR targets unavailable.`);\n return;\n }\n\n const targets = await ctx.native.targets();\n state.log(`[native] targets: ${targets.map((t) => t.name).join(', ') || 'none'}`);\n\n ctx.gameLog.onType('OnPlayerJoined', (entry) => {\n const who = entry.detail || entry.message;\n\n // One call, presented differently in each place: a tall translucent panel in VR, an ordinary\n // toast on the monitor. Omitting `sinks` means \"every target the companion has\".\n void ctx.native.notify({\n title: 'Player joined',\n content: who,\n timeoutSecs: 4,\n icon: 'user-available',\n overrides: {\n wayvr: {\n content: `${who} joined the instance`,\n height: 200,\n opacity: 0.85,\n alwaysShow: true,\n },\n },\n });\n });\n\n // VR only \u2014 deliberately nothing on the monitor, because this fires often.\n ctx.events.on('friendTimelineEvent', (payload) => {\n if (payload.type !== 'online') return;\n void ctx.native.notify({\n title: payload.friendName,\n content: 'came online',\n sinks: ['wayvr'],\n timeoutSecs: 3,\n opacity: 0.7,\n });\n });\n\n // The forward-compatible path: call a service this example predates.\n const description = await ctx.native.describe();\n if (description !== undefined) {\n state.log(\n `[native] companion ${description.version}, services: ${Object.keys(description.services).join(', ')}`,\n );\n }\n}\n\n/** 11. UI: a dashboard card, a sidebar tab and a settings card. */\nfunction installUi(ctx: Ctx, state: State): void {\n ctx.ui.addDashboardCard({\n title: 'Kitchen Sink',\n icon: 'auto_awesome',\n order: 10,\n render: (card) => {\n const grid = document.createElement('div');\n grid.className = 'ks-grid';\n grid.append(\n statTile('Game log events', () => state.gameLogCount),\n statTile('OSC parameters', () => state.oscParamCount),\n );\n card.appendChild(grid);\n\n const pulse = document.createElement('button');\n pulse.textContent = 'Send OSC pulse';\n pulse.addEventListener('click', () => {\n ctx.ui.toast({ message: `Sent ${pulseOsc(ctx)}.` });\n });\n card.appendChild(pulse);\n },\n });\n\n installNavTab(ctx, state);\n\n ctx.ui.addSettingsCard({\n title: 'Kitchen Sink',\n icon: 'auto_awesome',\n render: (card) => {\n card.appendChild(\n ctx.ui.createToggleRow(\n 'Follow the VRChat game log',\n ctx.settings.get('watchGameLog'),\n (checked) => { void ctx.settings.set('watchGameLog', checked); },\n ),\n );\n\n // Blocking confirmation modal; resolves false on cancel, backdrop click or dismissal.\n const reset = document.createElement('button');\n reset.textContent = 'Reset settings';\n reset.addEventListener('click', () => {\n void ctx.notifications\n .confirm({\n title: 'Reset Kitchen Sink',\n message: 'Restore every Kitchen Sink setting to its default?',\n confirmLabel: 'Reset',\n icon: 'restart_alt',\n })\n .then(async (confirmed) => {\n if (!confirmed) return;\n await ctx.settings.reset();\n ctx.notifications.toast({ message: 'Kitchen Sink settings reset.' });\n });\n });\n card.appendChild(reset);\n },\n });\n}\n\n/**\n * The plugin's tab, declaratively, from `ctx.ui.kit`.\n *\n * No class names, no `createElement`, no stylesheet \u2014 and the result is VRCNext's own markup, so\n * it inherits the theme, the font-size offset and every future restyle of the app.\n */\nfunction installNavTab(ctx: Ctx, state: State): void {\n ctx.ui.addNavTab({\n label: 'Kitchen Sink',\n icon: 'auto_awesome',\n render: (tab) => {\n const k = ctx.ui.kit;\n const activity = k.card({ title: 'Live activity', icon: 'radar' });\n state.panel = createLogPanel(activity);\n\n const output = k.emptyState('No request yet.');\n\n tab.append(\n k.layout(\n // A grid rather than full-width cards: two short cards side by side beats two wide ones.\n k.grid([\n k.card({\n title: 'Counters',\n icon: 'bolt',\n children: [\n k.stat({ label: 'Game log events', value: String(state.gameLogCount) }),\n k.stat({ label: 'OSC parameters', value: String(state.oscParamCount) }),\n ],\n }),\n k.card({\n title: 'Capabilities',\n icon: 'tune',\n children: [\n k.row({\n label: 'OSC',\n value: ctx.osc.available ? k.badge('ok', 'Ready') : k.badge('warn', 'Windows only'),\n detail: 'Sent through VRCNext\u2019s own sockets.',\n }),\n k.row({\n label: 'Native companion',\n value: ctx.native.available\n ? k.badge('ok', 'Connected')\n : k.badge('neutral', 'Not running'),\n detail: ctx.native.endpoint,\n }),\n k.toggleRow({\n label: 'Follow the game log',\n detail: 'Mirrors VRChat log lines into the panel below.',\n value: ctx.settings.get('watchGameLog'),\n onChange: (next) => { void ctx.settings.set('watchGameLog', next); },\n }),\n ],\n }),\n k.card({\n title: 'In-page routes',\n icon: 'account_tree',\n children: [\n k.description('Plugin routes are reachable from this page only, never from curl.'),\n k.buttonRow(\n k.button({\n label: 'GET stats',\n icon: 'download',\n onClick: () => {\n void ctx.router\n .fetch('stats')\n .then(async (response) => {\n output.textContent = await response.text();\n })\n .catch((error: unknown) => { output.textContent = String(error); });\n },\n }),\n ),\n output,\n ],\n }),\n ]),\n activity,\n ),\n );\n },\n });\n}\n\n/** Stat tile that refreshes itself from a getter while the dashboard is visible. */\nfunction statTile(label: string, read: () => number): HTMLElement {\n const tile = document.createElement('div');\n tile.className = 'ks-stat';\n\n const labelEl = document.createElement('div');\n labelEl.className = 'ks-stat-label';\n labelEl.textContent = label;\n\n const valueEl = document.createElement('div');\n valueEl.className = 'ks-stat-value';\n valueEl.textContent = String(read());\n\n // Repaint only while the tile is actually on screen.\n const observer = new IntersectionObserver((entries) => {\n for (const entry of entries) {\n if (entry.isIntersecting) valueEl.textContent = String(read());\n }\n });\n observer.observe(tile);\n\n tile.append(labelEl, valueEl);\n return tile;\n}\n"], + "mappings": "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", + "names": ["PLUGIN_CSS", "createLogPanel", "parent", "list", "message", "line", "time", "settings", "index_default", "ctx", "state", "message", "PLUGIN_CSS", "installSettingsWatch", "installEvents", "installGameLog", "installOsc", "installDeepLinks", "installRoutes", "installContextMenu", "installNotifications", "installNative", "installUi", "values", "payload", "worldName", "envelope", "entry", "entries", "error", "event", "pulseOsc", "name", "value", "request", "target", "option", "entity", "who", "targets", "t", "description", "card", "grid", "statTile", "pulse", "installNavTab", "checked", "reset", "confirmed", "tab", "k", "activity", "createLogPanel", "output", "next", "response", "label", "read", "tile", "labelEl", "valueEl"] +} diff --git a/vrcnext-plugins.json b/vrcnext-plugins.json new file mode 100644 index 0000000..b8bc997 --- /dev/null +++ b/vrcnext-plugins.json @@ -0,0 +1,26 @@ +{ + "formatVersion": 1, + "name": "VRCNext Example Plugins", + "plugins": [ + { + "id": "kitchen-sink", + "name": "Kitchen Sink", + "version": "1.0.0", + "description": "Reference plugin exercising every host capability.", + "entry": "dist/kitchen-sink.js", + "apiVersion": "^0.1.0", + "author": "vrcnext-plugins", + "homepage": "https://github.com/vrcnext-plugins/vrcnext-plugin-system" + }, + { + "id": "hello-world", + "name": "Hello World", + "version": "1.0.0", + "description": "Minimal plugin showing settings, events and UI injection.", + "entry": "dist/hello-world.js", + "apiVersion": "^0.1.0", + "author": "vrcnext-plugins", + "homepage": "https://github.com/vrcnext-plugins/vrcnext-plugin-system" + } + ] +}