@utsp/runtime-server 0.14.0 → 0.14.2

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.
package/dist/index.cjs CHANGED
@@ -1 +1,12 @@
1
- "use strict";var c=Object.defineProperty;var k=Object.getOwnPropertyDescriptor;var f=Object.getOwnPropertyNames;var v=Object.prototype.hasOwnProperty;var y=(o,t,e)=>t in o?c(o,t,{enumerable:!0,configurable:!0,writable:!0,value:e}):o[t]=e;var l=(o,t)=>c(o,"name",{value:t,configurable:!0});var S=(o,t)=>{for(var e in t)c(o,e,{get:t[e],enumerable:!0})},w=(o,t,e,i)=>{if(t&&typeof t=="object"||typeof t=="function")for(let s of f(t))!v.call(o,s)&&s!==e&&c(o,s,{get:()=>t[s],enumerable:!(i=k(t,s))||i.enumerable});return o};var C=o=>w(c({},"__esModule",{value:!0}),o);var r=(o,t,e)=>(y(o,typeof t!="symbol"?t+"":t,e),e);var A={};S(A,{ServerRuntime:()=>p});module.exports=C(A);var d=require("@utsp/core"),m=require("@utsp/network-server");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.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 d.Core({mode:"server",maxUsers:this.options.maxConnections}),this.network=new m.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(h=>{this.sendAndCount(t,"load",h)});let a=this.core.generateMacroLoadPackets(t);a.length>0&&(this.log(`Sending ${a.length} macro load packets to ${t}...`),a.forEach(h=>{this.sendAndCount(t,"load",h)}));let u=s.getInputBindingsLoadPacket();u&&(this.sendAndCount(t,"input-bindings",u),this.log(`Sent input bindings 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.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.core.endTick(),this.broadcastUpdates()}broadcastUpdates(){let t=this.core.endTickSplit(),e=this.core.getCurrentTick();t.forEach(({static:i,dynamic:s},n)=>{i&&(this.sendAndCount(n,"update-static",i),console.warn(`[SERVER] update-static: ${i.length}B (tick ${e})`)),s&&(this.sendVolatileAndCount(n,"update-dynamic",s),console.warn(`[SERVER] update-dynamic: ${s.length}B (tick ${e})`));let a=this.core.getUser(n);a&&a.hasBridgeMessages()&&a.getBridgeMessages().forEach(h=>{this.network.sendBridge(n,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}log(t){this.options.debug&&console.warn(`[ServerRuntime] ${t}`)}};l(g,"ServerRuntime");var p=g;0&&(module.exports={ServerRuntime});
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(", ")}.
2
+ Your application must implement the IApplication interface from @utsp/types.
3
+ Required methods:
4
+ - init(core, runtime): Initialize application
5
+ - initUser(core, user, metadata?): Initialize a new user
6
+ - updateUser(core, user, deltaTime): Update user each tick
7
+ Optional methods:
8
+ - update(core, deltaTime): Global update each tick
9
+ - destroyUser(core, user, reason?): Cleanup when user disconnects
10
+ - destroy(): Cleanup on shutdown
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});
package/dist/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { IApplication } from '@utsp/types';
1
2
  export { IApplication } from '@utsp/types';
2
3
 
3
4
  /**
@@ -6,8 +7,9 @@ export { IApplication } from '@utsp/types';
6
7
  interface ServerRuntimeOptions {
7
8
  /**
8
9
  * The application implementation
10
+ * Must implement IApplication interface with at least: init, initUser, updateUser
9
11
  */
10
- application: any;
12
+ application: IApplication;
11
13
  /**
12
14
  * Server port
13
15
  */
@@ -155,10 +157,20 @@ declare class ServerRuntime {
155
157
  * Get size of data in bytes
156
158
  */
157
159
  private getDataSize;
160
+ /**
161
+ * Resize displays in responsive mode based on the viewport reported by the client.
162
+ * Mirrors the standalone client behavior so connected clients get matching resolution.
163
+ */
164
+ private syncResponsiveDisplaySize;
158
165
  /**
159
166
  * Debug logging
160
167
  */
161
168
  private log;
169
+ /**
170
+ * Validate that application implements required IApplication methods
171
+ * @throws Error if application is invalid
172
+ */
173
+ private validateApplication;
162
174
  }
163
175
 
164
176
  export { ServerRuntime };
package/dist/index.mjs CHANGED
@@ -1 +1,12 @@
1
- var g=Object.defineProperty;var d=(a,t,e)=>t in a?g(a,t,{enumerable:!0,configurable:!0,writable:!0,value:e}):a[t]=e;var l=(a,t)=>g(a,"name",{value:t,configurable:!0});var o=(a,t,e)=>(d(a,typeof t!="symbol"?t+"":t,e),e);import{Core as m}from"@utsp/core";import{SocketIOServer as k}from"@utsp/network-server";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.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 m({mode:"server",maxUsers:this.options.maxConnections}),this.network=new k({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 c=s.getInputBindingsLoadPacket();c&&(this.sendAndCount(t,"input-bindings",c),this.log(`Sent input bindings 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.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.core.endTick(),this.broadcastUpdates()}broadcastUpdates(){let t=this.core.endTickSplit(),e=this.core.getCurrentTick();t.forEach(({static:i,dynamic:s},n)=>{i&&(this.sendAndCount(n,"update-static",i),console.warn(`[SERVER] update-static: ${i.length}B (tick ${e})`)),s&&(this.sendVolatileAndCount(n,"update-dynamic",s),console.warn(`[SERVER] update-dynamic: ${s.length}B (tick ${e})`));let r=this.core.getUser(n);r&&r.hasBridgeMessages()&&r.getBridgeMessages().forEach(h=>{this.network.sendBridge(n,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}log(t){this.options.debug&&console.warn(`[ServerRuntime] ${t}`)}};l(u,"ServerRuntime");var p=u;export{p as ServerRuntime};
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(", ")}.
2
+ Your application must implement the IApplication interface from @utsp/types.
3
+ Required methods:
4
+ - init(core, runtime): Initialize application
5
+ - initUser(core, user, metadata?): Initialize a new user
6
+ - updateUser(core, user, deltaTime): Update user each tick
7
+ Optional methods:
8
+ - update(core, deltaTime): Global update each tick
9
+ - destroyUser(core, user, reason?): Cleanup when user disconnects
10
+ - destroy(): Cleanup on shutdown
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};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@utsp/runtime-server",
3
- "version": "0.14.0",
3
+ "version": "0.14.2",
4
4
  "description": "Server-side runtime for UTSP applications (Node.js, headless)",
5
5
  "author": "THP Software",
6
6
  "license": "MIT",
@@ -46,9 +46,9 @@
46
46
  "access": "public"
47
47
  },
48
48
  "dependencies": {
49
- "@utsp/core": "0.14.0",
50
- "@utsp/types": "0.14.0",
51
- "@utsp/network-server": "0.14.0"
49
+ "@utsp/core": "0.14.2",
50
+ "@utsp/network-server": "0.14.2",
51
+ "@utsp/types": "0.14.2"
52
52
  },
53
53
  "devDependencies": {
54
54
  "@types/node": "^20.0.0",