@utsp/runtime-server 0.14.2 → 0.15.0-nightly.20251230160137.3077b87

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ .uplot,.uplot *,.uplot *:before,.uplot *:after{box-sizing:border-box}.uplot{font-family:system-ui,-apple-system,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";line-height:1.5;width:min-content}.u-title{text-align:center;font-size:18px;font-weight:700}.u-wrap{position:relative;-webkit-user-select:none;user-select:none}.u-over,.u-under{position:absolute}.u-under{overflow:hidden}.uplot canvas{display:block;position:relative;width:100%;height:100%}.u-axis{position:absolute}.u-legend{font-size:14px;margin:auto;text-align:center}.u-inline{display:block}.u-inline *{display:inline-block}.u-inline tr{margin-right:16px}.u-legend th{font-weight:600}.u-legend th>*{vertical-align:middle;display:inline-block}.u-legend .u-marker{width:1em;height:1em;margin-right:4px;background-clip:padding-box!important}.u-inline.u-live th:after{content:":";vertical-align:middle}.u-inline:not(.u-live) .u-value{display:none}.u-series>*{padding:4px}.u-series th{cursor:pointer}.u-legend .u-off>*{opacity:.3}.u-select{background:#00000012;position:absolute;pointer-events:none}.u-cursor-x,.u-cursor-y{position:absolute;left:0;top:0;pointer-events:none;will-change:transform}.u-hz .u-cursor-x,.u-vt .u-cursor-y{height:100%;border-right:1px dashed #607D8B}.u-hz .u-cursor-y,.u-vt .u-cursor-x{width:100%;border-bottom:1px dashed #607D8B}.u-cursor-pt{position:absolute;top:0;left:0;border-radius:50%;border:0 solid;pointer-events:none;will-change:transform;background-clip:padding-box!important}.u-axis.u-off,.u-select.u-off,.u-cursor-x.u-off,.u-cursor-y.u-off,.u-cursor-pt.u-off{display:none}:root{scrollbar-color:#4b4b4b #1b1b1b;scrollbar-width:thin}*::-webkit-scrollbar{width:10px;height:10px}*::-webkit-scrollbar-track{background:#1b1b1b}*::-webkit-scrollbar-thumb{background-color:#3b3b3b;border-radius:8px;border:2px solid #1b1b1b}*::-webkit-scrollbar-thumb:hover{background-color:#4b4b4b}*::-webkit-scrollbar-corner{background:#1b1b1b}
@@ -0,0 +1,13 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>UTSP Visual Debugger</title>
7
+ <script type="module" crossorigin src="/assets/index-B7ydytdk.js"></script>
8
+ <link rel="stylesheet" crossorigin href="/assets/index-DuratEin.css">
9
+ </head>
10
+ <body>
11
+ <div id="root"></div>
12
+ </body>
13
+ </html>
Binary file
Binary file
package/dist/index.cjs CHANGED
@@ -1,4 +1,4 @@
1
- "use strict";var l=Object.defineProperty;var y=Object.getOwnPropertyDescriptor;var w=Object.getOwnPropertyNames;var S=Object.prototype.hasOwnProperty;var A=(o,t,e)=>t in o?l(o,t,{enumerable:!0,configurable:!0,writable:!0,value:e}):o[t]=e;var m=(o,t)=>l(o,"name",{value:t,configurable:!0});var C=(o,t)=>{for(var e in t)l(o,e,{get:t[e],enumerable:!0})},U=(o,t,e,i)=>{if(t&&typeof t=="object"||typeof t=="function")for(let s of w(t))!S.call(o,s)&&s!==e&&l(o,s,{get:()=>t[s],enumerable:!(i=y(t,s))||i.enumerable});return o};var $=o=>U(l({},"__esModule",{value:!0}),o);var r=(o,t,e)=>(A(o,typeof t!="symbol"?t+"":t,e),e);var T={};C(T,{ServerRuntime:()=>u});module.exports=$(T);var f=require("@utsp/core"),k=require("@utsp/network-server"),v=require("@utsp/types");var g=class g{constructor(t){r(this,"core");r(this,"network");r(this,"options");r(this,"running",!1);r(this,"startTime",0);r(this,"lastTimestamp",0);r(this,"tickCount",0);r(this,"tps",0);r(this,"tpsUpdateTime",0);r(this,"tickInterval",null);r(this,"totalBytesSent",0);this.validateApplication(t.application),this.options={application:t.application,port:t.port,host:t.host??"0.0.0.0",tickRate:t.tickRate??20,maxConnections:t.maxConnections??100,debug:t.debug??!1,width:t.width??80,height:t.height??25,cors:t.cors??{origin:"*"}},this.log("Initializing ServerRuntime..."),this.core=new f.Core({mode:"server",maxUsers:this.options.maxConnections}),this.network=new k.SocketIOServer({port:this.options.port,host:this.options.host,cors:this.options.cors,maxConnections:this.options.maxConnections,debug:this.options.debug}),this.log("ServerRuntime initialized")}getMode(){return"server"}isRunning(){return this.running}setTickRate(t){if(t<=0||t>1e3)throw new Error(`Invalid tick rate: ${t}. Must be between 1 and 1000.`);if(this.options.tickRate=t,this.log(`Tick rate changed to ${t} TPS`),this.core.getUsers().forEach(e=>{e.setBytesTickRate(t)}),this.running&&this.tickInterval){clearInterval(this.tickInterval);let e=1e3/t;this.log(`Restarting tick loop at ${t} TPS (${e}ms)...`),this.tickInterval=setInterval(()=>{this.tick()},e)}}getTickRate(){return this.options.tickRate}async start(){if(this.running){this.log("Already running");return}this.log("Starting ServerRuntime..."),await this.network.start(),this.log(`Server listening on ${this.options.host}:${this.options.port}`),this.setupNetworkHandlers(),this.log("Calling application.init()..."),await this.options.application.init(this.core,this),this.running=!0,this.startTime=Date.now(),this.lastTimestamp=this.startTime,this.tpsUpdateTime=this.startTime;let t=1e3/this.options.tickRate;this.log(`Starting tick loop at ${this.options.tickRate} TPS (${t}ms)...`),this.tickInterval=setInterval(()=>{this.tick()},t)}async stop(){if(!this.running)return;this.log("Stopping ServerRuntime..."),this.running=!1,this.tickInterval&&(clearInterval(this.tickInterval),this.tickInterval=null),this.network.getClients().forEach(e=>{this.network.disconnectClient(e,"Server stopped")}),await this.network.stop(),this.log("ServerRuntime stopped")}getStats(){return{mode:"server",running:this.running,userCount:this.core.getUsers().length,tps:this.tps,uptime:this.running?Date.now()-this.startTime:0,totalTicks:this.tickCount,totalBytesSent:this.totalBytesSent}}async destroy(){await this.stop(),this.log("Destroying ServerRuntime..."),this.options.application.destroy&&this.options.application.destroy(),await this.network.destroy(),this.log("ServerRuntime destroyed")}setupNetworkHandlers(){this.network.onConnect(t=>{this.log(`Client connected: ${t}`)}),this.network.onDisconnect((t,e)=>{this.log(`Client disconnected: ${t} (${e})`);let i=this.core.getUser(t);i&&(this.options.application.destroyUser&&this.options.application.destroyUser(this.core,i,e),this.core.removeUser(t))}),this.network.on("join",(t,e)=>{this.log(`Join request from ${t}: ${e.username}`);try{let i=e.username||`Player${t.substring(0,4)}`,s=this.core.createUser(t,i);s.setBytesTickRate(this.options.tickRate),this.options.application.initUser(this.core,s,{username:i,token:e.token}),this.sendAndCount(t,"join_response",{success:!0,userId:t,roomId:"main"});let n=this.core.generateAllLoadPackets();this.log(`Sending ${n.length} load packets to ${t}...`),n.forEach(c=>{this.sendAndCount(t,"load",c)});let a=this.core.generateMacroLoadPackets(t);a.length>0&&(this.log(`Sending ${a.length} macro load packets to ${t}...`),a.forEach(c=>{this.sendAndCount(t,"load",c)}));let h=s.getInputBindingsLoadPacket();h&&(this.sendAndCount(t,"input-bindings",h),this.log(`Sent input bindings to ${t}`));let p=this.core.generateInitialUpdatePacket(t);p&&(this.sendAndCount(t,"update",p),this.log(`Sent initial update packet to ${t}`)),this.log(`User ${i} (${t}) joined`)}catch(i){this.log(`Failed to join: ${i}`),this.sendAndCount(t,"join_response",{success:!1,error:"Failed to join game"})}}),this.network.on("leave",t=>{this.log(`Leave request from ${t}`),this.network.disconnectClient(t,"User left")}),this.network.on("input",(t,e)=>{let i=this.core.getUser(t);if(!i){this.log(`Input from unknown user: ${t}`);return}let s=e instanceof Uint8Array?e:new Uint8Array(e);i.decodeAndApplyCompressedInput(s),this.syncResponsiveDisplaySize(i)}),this.network.on("audio-ack",(t,e)=>{let i=this.core.getUser(t);if(!i){this.log(`Audio ACK from unknown user: ${t}`);return}i.handleAudioAck(e),this.options.application.onAudioAck&&this.options.application.onAudioAck(this.core,i,e),this.log(`Audio ACK from ${t}: ${e.type}`)}),this.network.onBridge("*",(t,e)=>{let i=this.core.getUser(t);if(!i){this.log(`Bridge message from unknown user: ${t}`);return}this.options.application.onBridgeMessage&&this.options.application.onBridgeMessage(this.core,i,e.channel,e.data),this.log(`Bridge from ${t} on channel '${e.channel}'`)})}tick(){if(!this.running)return;let t=Date.now(),e=(t-this.lastTimestamp)/1e3;this.lastTimestamp=t,this.tickCount++,t-this.tpsUpdateTime>=1e3&&(this.tps=Math.round(this.tickCount*1e3/(t-this.tpsUpdateTime)),this.tickCount=0,this.tpsUpdateTime=t),this.options.application.update&&this.options.application.update(this.core,e),this.core.getUsers().forEach(s=>{if(this.options.application.updateUser(this.core,s,e),s.clearTextInputs(),s.needsSendSounds()){let n=this.core.generateSoundLoadPackets();n.length>0&&(this.log(`Sending ${n.length} sound packets to ${s.id}...`),n.forEach(a=>{this.sendAndCount(s.id,"sound-load",a)})),s.clearSendSounds()}}),this.broadcastUpdates()}broadcastUpdates(){this.core.endTick().forEach(({static:e,dynamic:i},s)=>{e&&this.sendAndCount(s,"update-static",e),i&&this.sendVolatileAndCount(s,"update-dynamic",i);let n=this.core.getUser(s);n&&n.hasBridgeMessages()&&n.getBridgeMessages().forEach(h=>{this.network.sendBridge(s,h.channel,h.data)})})}sendAndCount(t,e,i){this.network.sendToClient(t,e,i);let s=this.getDataSize(i);this.totalBytesSent+=s;let n=this.core.getUser(t);n&&n.recordBytesSent(s)}sendVolatileAndCount(t,e,i){let s=this.network;s.sendToClientVolatile?s.sendToClientVolatile(t,e,i):this.network.sendToClient(t,e,i),this.totalBytesSent+=i.length;let n=this.core.getUser(t);n&&n.recordBytesSent(i.length)}getDataSize(t){return t instanceof Uint8Array||typeof t=="string"?t.length:JSON.stringify(t).length}syncResponsiveDisplaySize(t){let e=t.getDisplays();if(e.length!==0)for(let i of e){let s=i.getId();if(t.getScalingMode(s)!=="responsive")continue;let a=t.getDisplayViewport(s);if(!a)continue;let h=t.getCellSize(s);if(h.cellWidth<=0||h.cellHeight<=0)continue;let p=Math.floor(a.pixelWidth/h.cellWidth),c=Math.floor(a.pixelHeight/h.cellHeight);p=Math.min(256,Math.max(1,p)),c=Math.min(256,Math.max(1,c));let d=i.getSize();(d.x!==p||d.y!==c)&&i.setSize(new v.Vector2(p,c))}}log(t){this.options.debug&&console.warn(`[ServerRuntime] ${t}`)}validateApplication(t){if(!t||typeof t!="object")throw new Error("[ServerRuntime] Invalid application: must be an object implementing IApplication interface");let e=t,i=["init","initUser","updateUser"],s=[];for(let n of i)typeof e[n]!="function"&&s.push(n);if(s.length>0)throw new Error(`[ServerRuntime] Invalid application: missing required methods: ${s.join(", ")}.
1
+ "use strict";var C=Object.create;var d=Object.defineProperty;var A=Object.getOwnPropertyDescriptor;var T=Object.getOwnPropertyNames;var z=Object.getPrototypeOf,M=Object.prototype.hasOwnProperty;var $=(c,t,e)=>t in c?d(c,t,{enumerable:!0,configurable:!0,writable:!0,value:e}):c[t]=e;var k=(c,t)=>d(c,"name",{value:t,configurable:!0});var x=(c,t)=>{for(var e in t)d(c,e,{get:t[e],enumerable:!0})},b=(c,t,e,s)=>{if(t&&typeof t=="object"||typeof t=="function")for(let i of T(t))!M.call(c,i)&&i!==e&&d(c,i,{get:()=>t[i],enumerable:!(s=A(t,i))||s.enumerable});return c};var B=(c,t,e)=>(e=c!=null?C(z(c)):{},b(t||!c||!c.__esModule?d(e,"default",{value:c,enumerable:!0}):e,c)),R=c=>b(d({},"__esModule",{value:!0}),c);var h=(c,t,e)=>($(c,typeof t!="symbol"?t+"":t,e),e);var I={};x(I,{ServerRuntime:()=>y});module.exports=R(I);var v=require("@utsp/core"),w=require("@utsp/network-server"),g=require("@utsp/types"),S=require("fs/promises"),m=B(require("path"),1),U=require("url"),P={};var u=class u{constructor(t){h(this,"core");h(this,"network");h(this,"options");h(this,"running",!1);h(this,"startTime",0);h(this,"lastTimestamp",0);h(this,"tickCount",0);h(this,"tps",0);h(this,"tpsUpdateTime",0);h(this,"tickInterval",null);h(this,"totalBytesSent",0);h(this,"debugClients",new Set);h(this,"debugDisplayOverrides",new Map);this.validateApplication(t.application),this.options={application:t.application,port:t.port,host:t.host??"0.0.0.0",tickRate:t.tickRate??20,maxConnections:t.maxConnections??100,debug:t.debug??!1,debugUi:t.debugUi??!1,debugUiServePath:t.debugUiServePath??this.resolveDefaultDebugUiServePath(),width:t.width??80,height:t.height??25,cors:t.cors??{origin:"*"}},this.log("Initializing ServerRuntime..."),this.core=new v.Core({mode:"server",maxUsers:this.options.maxConnections}),this.network=new w.SocketIOServer({port:this.options.port,host:this.options.host,cors:this.options.cors,maxConnections:this.options.maxConnections,debug:this.options.debug}),this.log("ServerRuntime initialized")}getMode(){return"server"}isRunning(){return this.running}setTickRate(t){if(t<=0||t>1e3)throw new Error(`Invalid tick rate: ${t}. Must be between 1 and 1000.`);if(this.options.tickRate=t,this.log(`Tick rate changed to ${t} TPS`),this.core.getUsers().forEach(e=>{e.setBytesTickRate(t)}),this.running&&this.tickInterval){clearInterval(this.tickInterval);let e=1e3/t;this.log(`Restarting tick loop at ${t} TPS (${e}ms)...`),this.tickInterval=setInterval(()=>{this.tick()},e)}}getTickRate(){return this.options.tickRate}async start(){if(this.running){this.log("Already running");return}this.log("Starting ServerRuntime..."),await this.network.start(),this.log(`Server listening on ${this.options.host}:${this.options.port}`),this.setupDebugUiStaticHosting(),this.setupNetworkHandlers(),this.log("Calling application.init()..."),await this.options.application.init(this.core,this),this.running=!0,this.startTime=Date.now(),this.lastTimestamp=this.startTime,this.tpsUpdateTime=this.startTime;let t=1e3/this.options.tickRate;this.log(`Starting tick loop at ${this.options.tickRate} TPS (${t}ms)...`),this.tickInterval=setInterval(()=>{this.tick()},t)}async stop(){if(!this.running)return;this.log("Stopping ServerRuntime..."),this.running=!1,this.tickInterval&&(clearInterval(this.tickInterval),this.tickInterval=null),this.network.getClients().forEach(e=>{this.network.disconnectClient(e,"Server stopped")}),await this.network.stop(),this.log("ServerRuntime stopped")}getStats(){return{mode:"server",running:this.running,userCount:this.core.getUsers().length,tps:this.tps,uptime:this.running?Date.now()-this.startTime:0,totalTicks:this.tickCount,totalBytesSent:this.totalBytesSent}}async destroy(){await this.stop(),this.log("Destroying ServerRuntime..."),this.options.application.destroy&&this.options.application.destroy(),await this.network.destroy(),this.log("ServerRuntime destroyed")}setupNetworkHandlers(){this.network.onConnect(t=>{this.log(`Client connected: ${t}`)}),this.network.onDisconnect((t,e)=>{this.log(`Client disconnected: ${t} (${e})`),this.debugClients.delete(t),this.debugDisplayOverrides.delete(t);let s=this.core.getUser(t);s&&(this.options.application.destroyUser&&this.options.application.destroyUser(this.core,s,e),this.core.removeUser(t))}),this.network.on("join",(t,e)=>{this.log(`Join request from ${t}: ${e.username}`);try{let s=e.username||`Player${t.substring(0,4)}`,i=this.core.createUser(t,s);i.setBytesTickRate(this.options.tickRate),this.options.application.initUser(this.core,i,{username:s,token:e.token}),this.options.debugUi&&this.syncDebugDisplay(i),this.sendAndCount(t,"join_response",{success:!0,userId:t,roomId:"main"});let n=this.core.generateAllLoadPackets();this.log(`Sending ${n.length} load packets to ${t}...`),n.forEach(p=>{this.sendAndCount(t,"load",p)});let o=this.core.generateMacroLoadPackets(t);o.length>0&&(this.log(`Sending ${o.length} macro load packets to ${t}...`),o.forEach(p=>{this.sendAndCount(t,"load",p)}));let a=i.getInputBindingsLoadPacket();a&&(this.sendAndCount(t,"input-bindings",a),this.log(`Sent input bindings to ${t}`));let r=this.core.generateInitialUpdatePacket(t);r&&(this.sendAndCount(t,"update",r),this.log(`Sent initial update packet to ${t}`)),this.log(`User ${s} (${t}) joined`)}catch(s){this.log(`Failed to join: ${s}`),this.sendAndCount(t,"join_response",{success:!1,error:"Failed to join game"})}}),this.network.on("leave",t=>{this.log(`Leave request from ${t}`),this.network.disconnectClient(t,"User left")}),this.network.on("input",(t,e)=>{let s=this.core.getUser(t);if(!s){this.log(`Input from unknown user: ${t}`);return}let i=e instanceof Uint8Array?e:new Uint8Array(e);s.decodeAndApplyCompressedInput(i),this.syncResponsiveDisplaySize(s)}),this.network.on("audio-ack",(t,e)=>{let s=this.core.getUser(t);if(!s){this.log(`Audio ACK from unknown user: ${t}`);return}s.handleAudioAck(e),this.options.application.onAudioAck&&this.options.application.onAudioAck(this.core,s,e),this.log(`Audio ACK from ${t}: ${e.type}`)}),this.network.onBridge("*",(t,e)=>{let s=this.core.getUser(t);if(!s){this.log(`Bridge message from unknown user: ${t}`);return}if(e.channel==="__debug_auth"){this.options.debugUi&&(this.debugClients.add(t),this.log(`Debug client authenticated: ${t}`));return}if(e.channel==="__debug_display_255"){this.handleDebugDisplayBridge(s,e.data);return}e.channel.startsWith("__debug_")||(this.options.application.onBridgeMessage&&this.options.application.onBridgeMessage(this.core,s,e.channel,e.data),this.log(`Bridge from ${t} on channel '${e.channel}'`))})}tick(){if(!this.running)return;let t=Date.now(),e=(t-this.lastTimestamp)/1e3;this.lastTimestamp=t,this.tickCount++,t-this.tpsUpdateTime>=1e3&&(this.tps=Math.round(this.tickCount*1e3/(t-this.tpsUpdateTime)),this.tickCount=0,this.tpsUpdateTime=t),this.options.application.update&&this.options.application.update(this.core,e),this.core.getUsers().forEach(i=>{if(this.options.application.updateUser(this.core,i,e),i.clearTextInputs(),this.options.debugUi&&this.syncDebugDisplay(i),i.needsSendSounds()){let n=this.core.generateSoundLoadPackets();n.length>0&&(this.log(`Sending ${n.length} sound packets to ${i.id}...`),n.forEach(o=>{this.sendAndCount(i.id,"sound-load",o)})),i.clearSendSounds()}}),this.broadcastUpdates()}broadcastUpdates(){let t=this.core.endTick(),e=new Map;if(t.forEach(({static:s,dynamic:i},n)=>{s&&(this.sendAndCount(n,"update-static",s),e.set(n,{staticSize:s.length,dynamicSize:e.get(n)?.dynamicSize??0})),i&&(this.sendVolatileAndCount(n,"update-dynamic",i),e.set(n,{staticSize:e.get(n)?.staticSize??0,dynamicSize:i.length}));let o=this.core.getUser(n);o&&o.hasBridgeMessages()&&o.getBridgeMessages().forEach(r=>{this.network.sendBridge(n,r.channel,r.data)})}),this.options.debugUi&&this.debugClients.size>0){let s=this.core.getUsers().map(n=>{let o=e.get(n.id)??{staticSize:0,dynamicSize:0},a=this.core.getStats().displayCompositeLayerIds,r={};for(let[p,l]of a.entries())r[p]={layerIdsTopToBottom:l};return{...n.getDebugInfo(),displayComposite:r,packets:o}}),i={tick:{tickRate:this.options.tickRate,tickCount:this.tickCount,tps:this.tps,uptime:this.running?Date.now()-this.startTime:0},users:s,totals:{totalBytesSent:this.totalBytesSent}};this.debugClients.forEach(n=>{this.network.sendBridge(n,"__debug_meta",i)})}}sendAndCount(t,e,s){this.network.sendToClient(t,e,s);let i=this.getDataSize(s);this.totalBytesSent+=i;let n=this.core.getUser(t);n&&n.recordBytesSent(i)}sendVolatileAndCount(t,e,s){let i=this.network;i.sendToClientVolatile?i.sendToClientVolatile(t,e,s):this.network.sendToClient(t,e,s),this.totalBytesSent+=s.length;let n=this.core.getUser(t);n&&n.recordBytesSent(s.length)}getDataSize(t){return t instanceof Uint8Array||typeof t=="string"?t.length:JSON.stringify(t).length}syncResponsiveDisplaySize(t){let e=t.getDisplays();if(e.length!==0)for(let s of e){let i=s.getId();if(t.getScalingMode(i)!=="responsive")continue;let o=t.getDisplayViewport(i);if(!o)continue;let a=t.getCellSize(i);if(a.cellWidth<=0||a.cellHeight<=0)continue;let r=Math.floor(o.pixelWidth/a.cellWidth),p=Math.floor(o.pixelHeight/a.cellHeight);r=Math.min(256,Math.max(1,r)),p=Math.min(256,Math.max(1,p));let l=s.getSize();(l.x!==r||l.y!==p)&&s.setSize(new g.Vector2(r,p))}}handleDebugDisplayBridge(t,e){if(!this.options.debugUi)return;let s=e&&typeof e=="object"?e:null;if(!s)return;let n={...this.debugDisplayOverrides.get(t.id)??{}},o=!1,a=s.origin;a&&typeof a.x=="number"&&typeof a.y=="number"&&(n.origin=new g.Vector2(a.x,a.y),o=!0);let r=s.size;if(r&&typeof r.x=="number"&&typeof r.y=="number"){let p=Math.min(256,Math.max(1,Math.round(r.x))),l=Math.min(256,Math.max(1,Math.round(r.y)));n.size=new g.Vector2(p,l),o=!0}o&&(this.debugDisplayOverrides.set(t.id,n),this.syncDebugDisplay(t))}syncDebugDisplay(t){if(!this.options.debugUi)return;let e=this.ensureDebugDisplay(t),s=t.getDisplays().filter(f=>!!f),i=s.find(f=>f.getId()===0)??s[0]??null,n=i?.getId()??0,o=this.debugDisplayOverrides.get(t.id),a=o?.origin??i?.getOrigin();a&&e.setOrigin(new g.Vector2(a.x,a.y));let r=o?.size??i?.getSize();if(r){let f=Math.min(256,Math.max(1,Math.round(r.x))),D=Math.min(256,Math.max(1,Math.round(r.y)));e.setSize(new g.Vector2(f,D))}let p=t.getCurrentPaletteSlotId(n);p!==null&&t.switchPalette(u.DEBUG_DISPLAY_ID,p),t.setScalingMode(u.DEBUG_DISPLAY_ID,g.ScalingMode.None);let l=t.getCellSize(n);t.setCellSize(u.DEBUG_DISPLAY_ID,l.cellWidth,l.cellHeight)}ensureDebugDisplay(t){let e=t.getDisplays().filter(i=>!!i),s=e.find(i=>i.getId()===u.DEBUG_DISPLAY_ID);if(!s){let i=e.find(a=>a.getId()===0)??e[0]??null,n=i?.getSize()??new g.Vector2(this.options.width,this.options.height);s=new v.Display(u.DEBUG_DISPLAY_ID,n.x,n.y);let o=i?.getOrigin()??new g.Vector2(0,0);s.setOrigin(new g.Vector2(o.x,o.y)),t.addDisplay(s)}return s}log(t){this.options.debug&&console.warn(`[ServerRuntime] ${t}`)}validateApplication(t){if(!t||typeof t!="object")throw new Error("[ServerRuntime] Invalid application: must be an object implementing IApplication interface");let e=t,s=["init","initUser","updateUser"],i=[];for(let n of s)typeof e[n]!="function"&&i.push(n);if(i.length>0)throw new Error(`[ServerRuntime] Invalid application: missing required methods: ${i.join(", ")}.
2
2
  Your application must implement the IApplication interface from @utsp/types.
3
3
  Required methods:
4
4
  - init(core, runtime): Initialize application
@@ -9,4 +9,4 @@ Optional methods:
9
9
  - destroyUser(core, user, reason?): Cleanup when user disconnects
10
10
  - destroy(): Cleanup on shutdown
11
11
  - onAudioAck(core, user, ack): Handle audio acknowledgments
12
- - onBridgeMessage(core, user, channel, data): Handle bridge messages`)}};m(g,"ServerRuntime");var u=g;0&&(module.exports={ServerRuntime});
12
+ - onBridgeMessage(core, user, channel, data): Handle bridge messages`)}setupDebugUiStaticHosting(){if(!this.options.debugUi||!this.options.debugUiServePath)return;let t=this.network.getHttpServer?.();if(!t){this.log("Debug UI serving skipped: HTTP server not available");return}let e=m.default.resolve(this.options.debugUiServePath);t.on("request",(s,i)=>{let n=s.url||"/";if(n.startsWith("/socket.io"))return;let o=n.split("?")[0],a=o==="/"?"/index.html":o,r=m.default.join(e,a);(async()=>{try{if(!(await(0,S.stat)(r)).isFile()){i.statusCode=404,i.end("Not Found");return}let l=await(0,S.readFile)(r);i.statusCode=200,i.setHeader("Content-Type",this.getContentType(r)),i.end(l)}catch{i.statusCode=404,i.end("Not Found")}})().catch(()=>{i.statusCode=500,i.end("Internal Server Error")})}),this.log(`Debug UI static hosting enabled from ${e}`)}getContentType(t){switch(m.default.extname(t).toLowerCase()){case".html":return"text/html; charset=utf-8";case".js":return"application/javascript; charset=utf-8";case".css":return"text/css; charset=utf-8";case".json":return"application/json; charset=utf-8";case".map":return"application/json; charset=utf-8";case".svg":return"image/svg+xml";case".png":return"image/png";case".jpg":case".jpeg":return"image/jpeg";case".gif":return"image/gif";case".woff":return"font/woff";case".woff2":return"font/woff2";default:return"application/octet-stream"}}resolveDefaultDebugUiServePath(){let t=typeof __dirname<"u"?__dirname:m.default.dirname((0,U.fileURLToPath)(P.url));return m.default.resolve(t,"debug-ui")}};k(u,"ServerRuntime"),h(u,"DEBUG_DISPLAY_ID",255);var y=u;0&&(module.exports={ServerRuntime});
package/dist/index.d.ts CHANGED
@@ -52,6 +52,16 @@ interface ServerRuntimeOptions {
52
52
  * @default false
53
53
  */
54
54
  debug?: boolean;
55
+ /**
56
+ * Enable visual debugger metadata streaming
57
+ * @default false
58
+ */
59
+ debugUi?: boolean;
60
+ /**
61
+ * Optional path to serve the built visual debugger (static files)
62
+ * If set and debugUi is true, runtime-server will serve files from this directory.
63
+ */
64
+ debugUiServePath?: string;
55
65
  }
56
66
  /**
57
67
  * Server Runtime Statistics
@@ -98,6 +108,9 @@ declare class ServerRuntime {
98
108
  private tpsUpdateTime;
99
109
  private tickInterval;
100
110
  private totalBytesSent;
111
+ private debugClients;
112
+ private debugDisplayOverrides;
113
+ private static readonly DEBUG_DISPLAY_ID;
101
114
  constructor(options: ServerRuntimeOptions);
102
115
  getMode(): 'server';
103
116
  isRunning(): boolean;
@@ -162,6 +175,18 @@ declare class ServerRuntime {
162
175
  * Mirrors the standalone client behavior so connected clients get matching resolution.
163
176
  */
164
177
  private syncResponsiveDisplaySize;
178
+ /**
179
+ * Handle debug bridge payloads to control display 255 (position/size overrides).
180
+ */
181
+ private handleDebugDisplayBridge;
182
+ /**
183
+ * Ensure the debug display exists and mirrors primary display settings.
184
+ */
185
+ private syncDebugDisplay;
186
+ /**
187
+ * Create or return the debug display (ID 255) for a user.
188
+ */
189
+ private ensureDebugDisplay;
165
190
  /**
166
191
  * Debug logging
167
192
  */
@@ -171,6 +196,19 @@ declare class ServerRuntime {
171
196
  * @throws Error if application is invalid
172
197
  */
173
198
  private validateApplication;
199
+ /**
200
+ * Serve static assets for the visual debugger if configured.
201
+ * Uses the underlying HTTP server from SocketIOServer.
202
+ */
203
+ private setupDebugUiStaticHosting;
204
+ /**
205
+ * Basic content-type resolver for static hosting
206
+ */
207
+ private getContentType;
208
+ /**
209
+ * Resolve default path for serving the visual debugger build (monorepo layout).
210
+ */
211
+ private resolveDefaultDebugUiServePath;
174
212
  }
175
213
 
176
214
  export { ServerRuntime };
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- var d=Object.defineProperty;var f=(c,t,e)=>t in c?d(c,t,{enumerable:!0,configurable:!0,writable:!0,value:e}):c[t]=e;var m=(c,t)=>d(c,"name",{value:t,configurable:!0});var o=(c,t,e)=>(f(c,typeof t!="symbol"?t+"":t,e),e);import{Core as k}from"@utsp/core";import{SocketIOServer as v}from"@utsp/network-server";import{Vector2 as y}from"@utsp/types";var u=class u{constructor(t){o(this,"core");o(this,"network");o(this,"options");o(this,"running",!1);o(this,"startTime",0);o(this,"lastTimestamp",0);o(this,"tickCount",0);o(this,"tps",0);o(this,"tpsUpdateTime",0);o(this,"tickInterval",null);o(this,"totalBytesSent",0);this.validateApplication(t.application),this.options={application:t.application,port:t.port,host:t.host??"0.0.0.0",tickRate:t.tickRate??20,maxConnections:t.maxConnections??100,debug:t.debug??!1,width:t.width??80,height:t.height??25,cors:t.cors??{origin:"*"}},this.log("Initializing ServerRuntime..."),this.core=new k({mode:"server",maxUsers:this.options.maxConnections}),this.network=new v({port:this.options.port,host:this.options.host,cors:this.options.cors,maxConnections:this.options.maxConnections,debug:this.options.debug}),this.log("ServerRuntime initialized")}getMode(){return"server"}isRunning(){return this.running}setTickRate(t){if(t<=0||t>1e3)throw new Error(`Invalid tick rate: ${t}. Must be between 1 and 1000.`);if(this.options.tickRate=t,this.log(`Tick rate changed to ${t} TPS`),this.core.getUsers().forEach(e=>{e.setBytesTickRate(t)}),this.running&&this.tickInterval){clearInterval(this.tickInterval);let e=1e3/t;this.log(`Restarting tick loop at ${t} TPS (${e}ms)...`),this.tickInterval=setInterval(()=>{this.tick()},e)}}getTickRate(){return this.options.tickRate}async start(){if(this.running){this.log("Already running");return}this.log("Starting ServerRuntime..."),await this.network.start(),this.log(`Server listening on ${this.options.host}:${this.options.port}`),this.setupNetworkHandlers(),this.log("Calling application.init()..."),await this.options.application.init(this.core,this),this.running=!0,this.startTime=Date.now(),this.lastTimestamp=this.startTime,this.tpsUpdateTime=this.startTime;let t=1e3/this.options.tickRate;this.log(`Starting tick loop at ${this.options.tickRate} TPS (${t}ms)...`),this.tickInterval=setInterval(()=>{this.tick()},t)}async stop(){if(!this.running)return;this.log("Stopping ServerRuntime..."),this.running=!1,this.tickInterval&&(clearInterval(this.tickInterval),this.tickInterval=null),this.network.getClients().forEach(e=>{this.network.disconnectClient(e,"Server stopped")}),await this.network.stop(),this.log("ServerRuntime stopped")}getStats(){return{mode:"server",running:this.running,userCount:this.core.getUsers().length,tps:this.tps,uptime:this.running?Date.now()-this.startTime:0,totalTicks:this.tickCount,totalBytesSent:this.totalBytesSent}}async destroy(){await this.stop(),this.log("Destroying ServerRuntime..."),this.options.application.destroy&&this.options.application.destroy(),await this.network.destroy(),this.log("ServerRuntime destroyed")}setupNetworkHandlers(){this.network.onConnect(t=>{this.log(`Client connected: ${t}`)}),this.network.onDisconnect((t,e)=>{this.log(`Client disconnected: ${t} (${e})`);let i=this.core.getUser(t);i&&(this.options.application.destroyUser&&this.options.application.destroyUser(this.core,i,e),this.core.removeUser(t))}),this.network.on("join",(t,e)=>{this.log(`Join request from ${t}: ${e.username}`);try{let i=e.username||`Player${t.substring(0,4)}`,s=this.core.createUser(t,i);s.setBytesTickRate(this.options.tickRate),this.options.application.initUser(this.core,s,{username:i,token:e.token}),this.sendAndCount(t,"join_response",{success:!0,userId:t,roomId:"main"});let n=this.core.generateAllLoadPackets();this.log(`Sending ${n.length} load packets to ${t}...`),n.forEach(h=>{this.sendAndCount(t,"load",h)});let r=this.core.generateMacroLoadPackets(t);r.length>0&&(this.log(`Sending ${r.length} macro load packets to ${t}...`),r.forEach(h=>{this.sendAndCount(t,"load",h)}));let a=s.getInputBindingsLoadPacket();a&&(this.sendAndCount(t,"input-bindings",a),this.log(`Sent input bindings to ${t}`));let p=this.core.generateInitialUpdatePacket(t);p&&(this.sendAndCount(t,"update",p),this.log(`Sent initial update packet to ${t}`)),this.log(`User ${i} (${t}) joined`)}catch(i){this.log(`Failed to join: ${i}`),this.sendAndCount(t,"join_response",{success:!1,error:"Failed to join game"})}}),this.network.on("leave",t=>{this.log(`Leave request from ${t}`),this.network.disconnectClient(t,"User left")}),this.network.on("input",(t,e)=>{let i=this.core.getUser(t);if(!i){this.log(`Input from unknown user: ${t}`);return}let s=e instanceof Uint8Array?e:new Uint8Array(e);i.decodeAndApplyCompressedInput(s),this.syncResponsiveDisplaySize(i)}),this.network.on("audio-ack",(t,e)=>{let i=this.core.getUser(t);if(!i){this.log(`Audio ACK from unknown user: ${t}`);return}i.handleAudioAck(e),this.options.application.onAudioAck&&this.options.application.onAudioAck(this.core,i,e),this.log(`Audio ACK from ${t}: ${e.type}`)}),this.network.onBridge("*",(t,e)=>{let i=this.core.getUser(t);if(!i){this.log(`Bridge message from unknown user: ${t}`);return}this.options.application.onBridgeMessage&&this.options.application.onBridgeMessage(this.core,i,e.channel,e.data),this.log(`Bridge from ${t} on channel '${e.channel}'`)})}tick(){if(!this.running)return;let t=Date.now(),e=(t-this.lastTimestamp)/1e3;this.lastTimestamp=t,this.tickCount++,t-this.tpsUpdateTime>=1e3&&(this.tps=Math.round(this.tickCount*1e3/(t-this.tpsUpdateTime)),this.tickCount=0,this.tpsUpdateTime=t),this.options.application.update&&this.options.application.update(this.core,e),this.core.getUsers().forEach(s=>{if(this.options.application.updateUser(this.core,s,e),s.clearTextInputs(),s.needsSendSounds()){let n=this.core.generateSoundLoadPackets();n.length>0&&(this.log(`Sending ${n.length} sound packets to ${s.id}...`),n.forEach(r=>{this.sendAndCount(s.id,"sound-load",r)})),s.clearSendSounds()}}),this.broadcastUpdates()}broadcastUpdates(){this.core.endTick().forEach(({static:e,dynamic:i},s)=>{e&&this.sendAndCount(s,"update-static",e),i&&this.sendVolatileAndCount(s,"update-dynamic",i);let n=this.core.getUser(s);n&&n.hasBridgeMessages()&&n.getBridgeMessages().forEach(a=>{this.network.sendBridge(s,a.channel,a.data)})})}sendAndCount(t,e,i){this.network.sendToClient(t,e,i);let s=this.getDataSize(i);this.totalBytesSent+=s;let n=this.core.getUser(t);n&&n.recordBytesSent(s)}sendVolatileAndCount(t,e,i){let s=this.network;s.sendToClientVolatile?s.sendToClientVolatile(t,e,i):this.network.sendToClient(t,e,i),this.totalBytesSent+=i.length;let n=this.core.getUser(t);n&&n.recordBytesSent(i.length)}getDataSize(t){return t instanceof Uint8Array||typeof t=="string"?t.length:JSON.stringify(t).length}syncResponsiveDisplaySize(t){let e=t.getDisplays();if(e.length!==0)for(let i of e){let s=i.getId();if(t.getScalingMode(s)!=="responsive")continue;let r=t.getDisplayViewport(s);if(!r)continue;let a=t.getCellSize(s);if(a.cellWidth<=0||a.cellHeight<=0)continue;let p=Math.floor(r.pixelWidth/a.cellWidth),h=Math.floor(r.pixelHeight/a.cellHeight);p=Math.min(256,Math.max(1,p)),h=Math.min(256,Math.max(1,h));let g=i.getSize();(g.x!==p||g.y!==h)&&i.setSize(new y(p,h))}}log(t){this.options.debug&&console.warn(`[ServerRuntime] ${t}`)}validateApplication(t){if(!t||typeof t!="object")throw new Error("[ServerRuntime] Invalid application: must be an object implementing IApplication interface");let e=t,i=["init","initUser","updateUser"],s=[];for(let n of i)typeof e[n]!="function"&&s.push(n);if(s.length>0)throw new Error(`[ServerRuntime] Invalid application: missing required methods: ${s.join(", ")}.
1
+ var y=Object.defineProperty;var k=(u,t,e)=>t in u?y(u,t,{enumerable:!0,configurable:!0,writable:!0,value:e}):u[t]=e;var v=(u,t)=>y(u,"name",{value:t,configurable:!0});var p=(u,t,e)=>(k(u,typeof t!="symbol"?t+"":t,e),e);import{Core as b,Display as w}from"@utsp/core";import{SocketIOServer as U}from"@utsp/network-server";import{ScalingMode as D,Vector2 as g}from"@utsp/types";import{readFile as C,stat as A}from"fs/promises";import m from"path";import{fileURLToPath as T}from"url";var l=class l{constructor(t){p(this,"core");p(this,"network");p(this,"options");p(this,"running",!1);p(this,"startTime",0);p(this,"lastTimestamp",0);p(this,"tickCount",0);p(this,"tps",0);p(this,"tpsUpdateTime",0);p(this,"tickInterval",null);p(this,"totalBytesSent",0);p(this,"debugClients",new Set);p(this,"debugDisplayOverrides",new Map);this.validateApplication(t.application),this.options={application:t.application,port:t.port,host:t.host??"0.0.0.0",tickRate:t.tickRate??20,maxConnections:t.maxConnections??100,debug:t.debug??!1,debugUi:t.debugUi??!1,debugUiServePath:t.debugUiServePath??this.resolveDefaultDebugUiServePath(),width:t.width??80,height:t.height??25,cors:t.cors??{origin:"*"}},this.log("Initializing ServerRuntime..."),this.core=new b({mode:"server",maxUsers:this.options.maxConnections}),this.network=new U({port:this.options.port,host:this.options.host,cors:this.options.cors,maxConnections:this.options.maxConnections,debug:this.options.debug}),this.log("ServerRuntime initialized")}getMode(){return"server"}isRunning(){return this.running}setTickRate(t){if(t<=0||t>1e3)throw new Error(`Invalid tick rate: ${t}. Must be between 1 and 1000.`);if(this.options.tickRate=t,this.log(`Tick rate changed to ${t} TPS`),this.core.getUsers().forEach(e=>{e.setBytesTickRate(t)}),this.running&&this.tickInterval){clearInterval(this.tickInterval);let e=1e3/t;this.log(`Restarting tick loop at ${t} TPS (${e}ms)...`),this.tickInterval=setInterval(()=>{this.tick()},e)}}getTickRate(){return this.options.tickRate}async start(){if(this.running){this.log("Already running");return}this.log("Starting ServerRuntime..."),await this.network.start(),this.log(`Server listening on ${this.options.host}:${this.options.port}`),this.setupDebugUiStaticHosting(),this.setupNetworkHandlers(),this.log("Calling application.init()..."),await this.options.application.init(this.core,this),this.running=!0,this.startTime=Date.now(),this.lastTimestamp=this.startTime,this.tpsUpdateTime=this.startTime;let t=1e3/this.options.tickRate;this.log(`Starting tick loop at ${this.options.tickRate} TPS (${t}ms)...`),this.tickInterval=setInterval(()=>{this.tick()},t)}async stop(){if(!this.running)return;this.log("Stopping ServerRuntime..."),this.running=!1,this.tickInterval&&(clearInterval(this.tickInterval),this.tickInterval=null),this.network.getClients().forEach(e=>{this.network.disconnectClient(e,"Server stopped")}),await this.network.stop(),this.log("ServerRuntime stopped")}getStats(){return{mode:"server",running:this.running,userCount:this.core.getUsers().length,tps:this.tps,uptime:this.running?Date.now()-this.startTime:0,totalTicks:this.tickCount,totalBytesSent:this.totalBytesSent}}async destroy(){await this.stop(),this.log("Destroying ServerRuntime..."),this.options.application.destroy&&this.options.application.destroy(),await this.network.destroy(),this.log("ServerRuntime destroyed")}setupNetworkHandlers(){this.network.onConnect(t=>{this.log(`Client connected: ${t}`)}),this.network.onDisconnect((t,e)=>{this.log(`Client disconnected: ${t} (${e})`),this.debugClients.delete(t),this.debugDisplayOverrides.delete(t);let i=this.core.getUser(t);i&&(this.options.application.destroyUser&&this.options.application.destroyUser(this.core,i,e),this.core.removeUser(t))}),this.network.on("join",(t,e)=>{this.log(`Join request from ${t}: ${e.username}`);try{let i=e.username||`Player${t.substring(0,4)}`,s=this.core.createUser(t,i);s.setBytesTickRate(this.options.tickRate),this.options.application.initUser(this.core,s,{username:i,token:e.token}),this.options.debugUi&&this.syncDebugDisplay(s),this.sendAndCount(t,"join_response",{success:!0,userId:t,roomId:"main"});let n=this.core.generateAllLoadPackets();this.log(`Sending ${n.length} load packets to ${t}...`),n.forEach(c=>{this.sendAndCount(t,"load",c)});let o=this.core.generateMacroLoadPackets(t);o.length>0&&(this.log(`Sending ${o.length} macro load packets to ${t}...`),o.forEach(c=>{this.sendAndCount(t,"load",c)}));let a=s.getInputBindingsLoadPacket();a&&(this.sendAndCount(t,"input-bindings",a),this.log(`Sent input bindings to ${t}`));let r=this.core.generateInitialUpdatePacket(t);r&&(this.sendAndCount(t,"update",r),this.log(`Sent initial update packet to ${t}`)),this.log(`User ${i} (${t}) joined`)}catch(i){this.log(`Failed to join: ${i}`),this.sendAndCount(t,"join_response",{success:!1,error:"Failed to join game"})}}),this.network.on("leave",t=>{this.log(`Leave request from ${t}`),this.network.disconnectClient(t,"User left")}),this.network.on("input",(t,e)=>{let i=this.core.getUser(t);if(!i){this.log(`Input from unknown user: ${t}`);return}let s=e instanceof Uint8Array?e:new Uint8Array(e);i.decodeAndApplyCompressedInput(s),this.syncResponsiveDisplaySize(i)}),this.network.on("audio-ack",(t,e)=>{let i=this.core.getUser(t);if(!i){this.log(`Audio ACK from unknown user: ${t}`);return}i.handleAudioAck(e),this.options.application.onAudioAck&&this.options.application.onAudioAck(this.core,i,e),this.log(`Audio ACK from ${t}: ${e.type}`)}),this.network.onBridge("*",(t,e)=>{let i=this.core.getUser(t);if(!i){this.log(`Bridge message from unknown user: ${t}`);return}if(e.channel==="__debug_auth"){this.options.debugUi&&(this.debugClients.add(t),this.log(`Debug client authenticated: ${t}`));return}if(e.channel==="__debug_display_255"){this.handleDebugDisplayBridge(i,e.data);return}e.channel.startsWith("__debug_")||(this.options.application.onBridgeMessage&&this.options.application.onBridgeMessage(this.core,i,e.channel,e.data),this.log(`Bridge from ${t} on channel '${e.channel}'`))})}tick(){if(!this.running)return;let t=Date.now(),e=(t-this.lastTimestamp)/1e3;this.lastTimestamp=t,this.tickCount++,t-this.tpsUpdateTime>=1e3&&(this.tps=Math.round(this.tickCount*1e3/(t-this.tpsUpdateTime)),this.tickCount=0,this.tpsUpdateTime=t),this.options.application.update&&this.options.application.update(this.core,e),this.core.getUsers().forEach(s=>{if(this.options.application.updateUser(this.core,s,e),s.clearTextInputs(),this.options.debugUi&&this.syncDebugDisplay(s),s.needsSendSounds()){let n=this.core.generateSoundLoadPackets();n.length>0&&(this.log(`Sending ${n.length} sound packets to ${s.id}...`),n.forEach(o=>{this.sendAndCount(s.id,"sound-load",o)})),s.clearSendSounds()}}),this.broadcastUpdates()}broadcastUpdates(){let t=this.core.endTick(),e=new Map;if(t.forEach(({static:i,dynamic:s},n)=>{i&&(this.sendAndCount(n,"update-static",i),e.set(n,{staticSize:i.length,dynamicSize:e.get(n)?.dynamicSize??0})),s&&(this.sendVolatileAndCount(n,"update-dynamic",s),e.set(n,{staticSize:e.get(n)?.staticSize??0,dynamicSize:s.length}));let o=this.core.getUser(n);o&&o.hasBridgeMessages()&&o.getBridgeMessages().forEach(r=>{this.network.sendBridge(n,r.channel,r.data)})}),this.options.debugUi&&this.debugClients.size>0){let i=this.core.getUsers().map(n=>{let o=e.get(n.id)??{staticSize:0,dynamicSize:0},a=this.core.getStats().displayCompositeLayerIds,r={};for(let[c,h]of a.entries())r[c]={layerIdsTopToBottom:h};return{...n.getDebugInfo(),displayComposite:r,packets:o}}),s={tick:{tickRate:this.options.tickRate,tickCount:this.tickCount,tps:this.tps,uptime:this.running?Date.now()-this.startTime:0},users:i,totals:{totalBytesSent:this.totalBytesSent}};this.debugClients.forEach(n=>{this.network.sendBridge(n,"__debug_meta",s)})}}sendAndCount(t,e,i){this.network.sendToClient(t,e,i);let s=this.getDataSize(i);this.totalBytesSent+=s;let n=this.core.getUser(t);n&&n.recordBytesSent(s)}sendVolatileAndCount(t,e,i){let s=this.network;s.sendToClientVolatile?s.sendToClientVolatile(t,e,i):this.network.sendToClient(t,e,i),this.totalBytesSent+=i.length;let n=this.core.getUser(t);n&&n.recordBytesSent(i.length)}getDataSize(t){return t instanceof Uint8Array||typeof t=="string"?t.length:JSON.stringify(t).length}syncResponsiveDisplaySize(t){let e=t.getDisplays();if(e.length!==0)for(let i of e){let s=i.getId();if(t.getScalingMode(s)!=="responsive")continue;let o=t.getDisplayViewport(s);if(!o)continue;let a=t.getCellSize(s);if(a.cellWidth<=0||a.cellHeight<=0)continue;let r=Math.floor(o.pixelWidth/a.cellWidth),c=Math.floor(o.pixelHeight/a.cellHeight);r=Math.min(256,Math.max(1,r)),c=Math.min(256,Math.max(1,c));let h=i.getSize();(h.x!==r||h.y!==c)&&i.setSize(new g(r,c))}}handleDebugDisplayBridge(t,e){if(!this.options.debugUi)return;let i=e&&typeof e=="object"?e:null;if(!i)return;let n={...this.debugDisplayOverrides.get(t.id)??{}},o=!1,a=i.origin;a&&typeof a.x=="number"&&typeof a.y=="number"&&(n.origin=new g(a.x,a.y),o=!0);let r=i.size;if(r&&typeof r.x=="number"&&typeof r.y=="number"){let c=Math.min(256,Math.max(1,Math.round(r.x))),h=Math.min(256,Math.max(1,Math.round(r.y)));n.size=new g(c,h),o=!0}o&&(this.debugDisplayOverrides.set(t.id,n),this.syncDebugDisplay(t))}syncDebugDisplay(t){if(!this.options.debugUi)return;let e=this.ensureDebugDisplay(t),i=t.getDisplays().filter(d=>!!d),s=i.find(d=>d.getId()===0)??i[0]??null,n=s?.getId()??0,o=this.debugDisplayOverrides.get(t.id),a=o?.origin??s?.getOrigin();a&&e.setOrigin(new g(a.x,a.y));let r=o?.size??s?.getSize();if(r){let d=Math.min(256,Math.max(1,Math.round(r.x))),S=Math.min(256,Math.max(1,Math.round(r.y)));e.setSize(new g(d,S))}let c=t.getCurrentPaletteSlotId(n);c!==null&&t.switchPalette(l.DEBUG_DISPLAY_ID,c),t.setScalingMode(l.DEBUG_DISPLAY_ID,D.None);let h=t.getCellSize(n);t.setCellSize(l.DEBUG_DISPLAY_ID,h.cellWidth,h.cellHeight)}ensureDebugDisplay(t){let e=t.getDisplays().filter(s=>!!s),i=e.find(s=>s.getId()===l.DEBUG_DISPLAY_ID);if(!i){let s=e.find(a=>a.getId()===0)??e[0]??null,n=s?.getSize()??new g(this.options.width,this.options.height);i=new w(l.DEBUG_DISPLAY_ID,n.x,n.y);let o=s?.getOrigin()??new g(0,0);i.setOrigin(new g(o.x,o.y)),t.addDisplay(i)}return i}log(t){this.options.debug&&console.warn(`[ServerRuntime] ${t}`)}validateApplication(t){if(!t||typeof t!="object")throw new Error("[ServerRuntime] Invalid application: must be an object implementing IApplication interface");let e=t,i=["init","initUser","updateUser"],s=[];for(let n of i)typeof e[n]!="function"&&s.push(n);if(s.length>0)throw new Error(`[ServerRuntime] Invalid application: missing required methods: ${s.join(", ")}.
2
2
  Your application must implement the IApplication interface from @utsp/types.
3
3
  Required methods:
4
4
  - init(core, runtime): Initialize application
@@ -9,4 +9,4 @@ Optional methods:
9
9
  - destroyUser(core, user, reason?): Cleanup when user disconnects
10
10
  - destroy(): Cleanup on shutdown
11
11
  - onAudioAck(core, user, ack): Handle audio acknowledgments
12
- - onBridgeMessage(core, user, channel, data): Handle bridge messages`)}};m(u,"ServerRuntime");var l=u;export{l as ServerRuntime};
12
+ - onBridgeMessage(core, user, channel, data): Handle bridge messages`)}setupDebugUiStaticHosting(){if(!this.options.debugUi||!this.options.debugUiServePath)return;let t=this.network.getHttpServer?.();if(!t){this.log("Debug UI serving skipped: HTTP server not available");return}let e=m.resolve(this.options.debugUiServePath);t.on("request",(i,s)=>{let n=i.url||"/";if(n.startsWith("/socket.io"))return;let o=n.split("?")[0],a=o==="/"?"/index.html":o,r=m.join(e,a);(async()=>{try{if(!(await A(r)).isFile()){s.statusCode=404,s.end("Not Found");return}let h=await C(r);s.statusCode=200,s.setHeader("Content-Type",this.getContentType(r)),s.end(h)}catch{s.statusCode=404,s.end("Not Found")}})().catch(()=>{s.statusCode=500,s.end("Internal Server Error")})}),this.log(`Debug UI static hosting enabled from ${e}`)}getContentType(t){switch(m.extname(t).toLowerCase()){case".html":return"text/html; charset=utf-8";case".js":return"application/javascript; charset=utf-8";case".css":return"text/css; charset=utf-8";case".json":return"application/json; charset=utf-8";case".map":return"application/json; charset=utf-8";case".svg":return"image/svg+xml";case".png":return"image/png";case".jpg":case".jpeg":return"image/jpeg";case".gif":return"image/gif";case".woff":return"font/woff";case".woff2":return"font/woff2";default:return"application/octet-stream"}}resolveDefaultDebugUiServePath(){let t=typeof __dirname<"u"?__dirname:m.dirname(T(import.meta.url));return m.resolve(t,"debug-ui")}};v(l,"ServerRuntime"),p(l,"DEBUG_DISPLAY_ID",255);var f=l;export{f as ServerRuntime};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@utsp/runtime-server",
3
- "version": "0.14.2",
3
+ "version": "0.15.0-nightly.20251230160137.3077b87",
4
4
  "description": "Server-side runtime for UTSP applications (Node.js, headless)",
5
5
  "author": "THP Software",
6
6
  "license": "MIT",
@@ -46,16 +46,17 @@
46
46
  "access": "public"
47
47
  },
48
48
  "dependencies": {
49
- "@utsp/core": "0.14.2",
50
- "@utsp/network-server": "0.14.2",
51
- "@utsp/types": "0.14.2"
49
+ "@utsp/core": "0.15.0-nightly.20251230160137.3077b87",
50
+ "@utsp/network-server": "0.15.0-nightly.20251230160137.3077b87",
51
+ "@utsp/types": "0.15.0-nightly.20251230160137.3077b87"
52
52
  },
53
53
  "devDependencies": {
54
+ "@utsp/visual-debugger": "0.15.0-nightly.20251230160137.3077b87",
54
55
  "@types/node": "^20.0.0",
55
56
  "typescript": "^5.3.3"
56
57
  },
57
58
  "scripts": {
58
- "build": "node ../../scripts/build-package.mjs packages/runtime-server",
59
+ "build": "pnpm --filter @utsp/visual-debugger build && node ../../scripts/build-package.mjs packages/runtime-server && node ../../scripts/embed-visual-debugger.mjs",
59
60
  "dev": "tsc --watch",
60
61
  "clean": "rimraf dist",
61
62
  "lint": "eslint \"src/**/*.ts\" --max-warnings 0",