@utsp/runtime-client 0.1.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Thomas Piquet
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,36 @@
1
+ # @utsp/runtime-client
2
+
3
+ > ⚠️ **PROTOTYPE - NOT READY FOR PRODUCTION**
4
+ >
5
+ > This package is currently in early development and should **NOT** be used in production.
6
+ > The API is unstable and subject to breaking changes without notice.
7
+
8
+ Client-side runtime for UTSP (Universal Text Stream Protocol) applications.
9
+
10
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
11
+
12
+ ## ⚠️ Development Status
13
+
14
+ **This is a prototype package under active development.**
15
+
16
+ - ❌ No stable API
17
+ - ❌ No documentation available yet
18
+ - ❌ Breaking changes expected
19
+ - ❌ Not recommended for production use
20
+
21
+ **Please check back later for updates or watch the repository for release announcements.**
22
+
23
+ ## Installation
24
+
25
+ ```bash
26
+ npm install @utsp/runtime-client
27
+ ```
28
+
29
+ ## Repository
30
+
31
+ - [GitHub](https://github.com/thp-software/utsp)
32
+ - [Issues](https://github.com/thp-software/utsp/issues)
33
+
34
+ ## License
35
+
36
+ MIT © 2025 Thomas Piquet
package/dist/index.cjs ADDED
@@ -0,0 +1 @@
1
+ "use strict";var C=Object.defineProperty;var B=Object.getOwnPropertyDescriptor;var O=Object.getOwnPropertyNames;var G=Object.prototype.hasOwnProperty;var H=(u,e,t)=>e in u?C(u,e,{enumerable:!0,configurable:!0,writable:!0,value:t}):u[e]=t;var f=(u,e)=>C(u,"name",{value:e,configurable:!0});var N=(u,e)=>{for(var t in e)C(u,t,{get:e[t],enumerable:!0})},q=(u,e,t,n)=>{if(e&&typeof e=="object"||typeof e=="function")for(let i of O(e))!G.call(u,i)&&i!==t&&C(u,i,{get:()=>e[i],enumerable:!(n=B(e,i))||n.enumerable});return u};var W=u=>q(C({},"__esModule",{value:!0}),u);var s=(u,e,t)=>(H(u,typeof e!="symbol"?e+"":e,t),t);var j={};N(j,{ClientRuntime:()=>k,RendererType:()=>F});module.exports=W(j);var $=require("@utsp/core"),x=require("@utsp/render"),y=require("@utsp/input"),D=require("@utsp/network-client");var F=(t=>(t.TerminalGL="webgl",t.Terminal2D="terminal2d",t))(F||{});var P=class P{constructor(e=60){s(this,"frameCount",0);s(this,"fps",0);s(this,"fpsUpdateTime",0);s(this,"lastCoreTime",0);s(this,"lastRenderTime",0);s(this,"lastTotalTime",0);s(this,"coreTimeSamples",[]);s(this,"renderTimeSamples",[]);s(this,"totalTimeSamples",[]);s(this,"maxSamples");if(e<=0)throw new Error("maxSamples must be positive");this.maxSamples=e}startTracking(e){this.fpsUpdateTime=e,this.frameCount=0,this.fps=0}updateFPS(e){this.frameCount++,e-this.fpsUpdateTime>=1e3&&(this.fps=Math.round(this.frameCount*1e3/(e-this.fpsUpdateTime)),this.frameCount=0,this.fpsUpdateTime=e)}recordFrameTiming(e,t){let n=e+t;this.lastCoreTime=e,this.lastRenderTime=t,this.lastTotalTime=n,this.coreTimeSamples.push(e),this.renderTimeSamples.push(t),this.totalTimeSamples.push(n),this.coreTimeSamples.length>this.maxSamples&&(this.coreTimeSamples.shift(),this.renderTimeSamples.shift(),this.totalTimeSamples.shift())}getFPS(){return this.fps}getLastFrameTiming(){if(!(this.lastCoreTime===0&&this.lastRenderTime===0))return{coreTime:this.lastCoreTime,renderTime:this.lastRenderTime,totalTime:this.lastTotalTime}}getAverageFrameTiming(){if(this.coreTimeSamples.length!==0)return{coreTime:this.average(this.coreTimeSamples),renderTime:this.average(this.renderTimeSamples),totalTime:this.average(this.totalTimeSamples)}}getStats(){return{fps:this.fps,lastFrame:this.getLastFrameTiming(),avgFrame:this.getAverageFrameTiming(),sampleCount:this.coreTimeSamples.length,maxSamples:this.maxSamples}}reset(){this.frameCount=0,this.fps=0,this.fpsUpdateTime=0,this.lastCoreTime=0,this.lastRenderTime=0,this.lastTotalTime=0,this.coreTimeSamples=[],this.renderTimeSamples=[],this.totalTimeSamples=[]}average(e){return e.length===0?0:e.reduce((t,n)=>t+n,0)/e.length}};f(P,"PerformanceMonitor");var I=P;var L=require("@utsp/core");var A=class A{constructor(e,t={}){s(this,"renderer");s(this,"options");this.renderer=e,this.options={debug:t.debug??!1,useImageDataRendering:t.useImageDataRendering??!1}}async initialize(e){await this.loadDefaultFont(e),await this.waitForReady()}async waitForReady(e=100,t=50){this.log("Waiting for renderer to be ready");let n=0;return new Promise((i,r)=>{let o=f(()=>{if(n++,this.renderer.isReady())this.log(`Renderer ready after ${n*t}ms`),i();else if(n>=e){let l=`Renderer failed to be ready after ${e*t}ms`;this.log(l),r(new Error(l))}else setTimeout(o,t)},"check");o()})}async loadDefaultFont(e){this.log("Loading ASCII 8x8 font");let t=(0,L.createASCII8x8FontLoad)(1);e.hasBitmapFont(t.fontId)||e.loadBitmapFontById(t.fontId,{charWidth:t.width,charHeight:t.height,cellWidth:t.cellWidth,cellHeight:t.cellHeight,glyphs:new Map(t.characters.map(n=>[n.charCode,n.bitmap]))}),"setImageDataRendering"in this.renderer&&this.options.useImageDataRendering&&(this.renderer.setImageDataRendering(!0),this.log("ImageData rendering enabled")),this.log("Font loaded and event triggered")}render(e,t){if(!this.renderer.isReady())return console.warn("[RENDERER MANAGER] Renderer not ready"),!1;let n=e.getRenderState(t);if(!n||n.displays.length===0)return console.warn("[RENDERER MANAGER] No render state or no displays"),!1;let i=n.displays[0];return this.renderer.renderDisplayData(i),!0}getRenderer(){return this.renderer}getCanvas(){return this.renderer.getCanvas()}isReady(){return this.renderer.isReady()}destroy(){this.log("Destroying renderer"),this.renderer.destroy()}log(e){this.options.debug&&console.warn(`[RendererManager] ${e}`)}};f(A,"RendererManager");var M=A;var R=require("@utsp/input");var U=class U{constructor(e,t,n,i,r){s(this,"network");s(this,"core");s(this,"rendererManager");s(this,"performanceMonitor");s(this,"options");s(this,"userId","");s(this,"lastReceivedTick",-1);s(this,"connected",!1);this.network=e,this.core=t,this.rendererManager=n,this.performanceMonitor=i,this.options={serverUrl:r.serverUrl,username:r.username,token:r.token??"",debug:r.debug??!1,autoReconnect:r.autoReconnect??!0,canvasWidth:r.canvasWidth,canvasHeight:r.canvasHeight}}async connect(){this.log(`Connecting to ${this.options.serverUrl}`),await this.network.connect(),this.connected=!0,this.log("Connected to server"),this.setupNetworkHandlers()}async joinGame(e){return new Promise((t,n)=>{this.network.send("join",{username:this.options.username,token:this.options.token}),this.network.on("join_response",i=>{if(i.success){this.userId=i.userId,this.log(`Joined game as ${this.userId}`);let r=this.core.createUser(this.userId,this.options.username);e.initUser(this.core,r,{username:this.options.username,token:this.options.token}),t(this.userId)}else n(new Error(i.error||"Failed to join game"))}),setTimeout(()=>n(new Error("Join timeout")),5e3)})}sendInput(e){let t=this.core.getUser(this.userId);if(!t)return;let n=this.rendererManager.getCanvas();if(n){let p=this.rendererManager.getRenderer()?.getOffsets?.(),T={offsetX:p?.offsetX??0,offsetY:p?.offsetY??0},v=R.InputCollector.collectMousePosition(e,n,this.options.canvasWidth,this.options.canvasHeight,T);t.setMousePosition(v.x,v.y,v.over),R.InputCollector.collectTouchPositions(e,n,this.options.canvasWidth,this.options.canvasHeight,10,T).forEach(w=>{t.setTouchPosition(w.id,w.x,w.y,w.over)})}let i=t.getInputBindingRegistry(),r=i.getAllAxes(),o=i.getAllButtons(),d=R.InputCollector.collectAxisSources(r,e),l=R.InputCollector.collectButtonSources(o,e),a=R.InputCollector.collectTextInputs(e);a.length>0&&t.setTextInputs(a);let c={};r.forEach(h=>{let p=i.evaluateAxis(h.bindingId,d);c[h.name]=p,t.setAxis(h.name,p)});let m={};o.forEach(h=>{let p=i.evaluateButton(h.bindingId,l);m[h.name]=p,t.setButton(h.name,p)});let g=t.getMouseDisplayInfo(),b=g?{x:g.localX,y:g.localY}:null;this.network.send("input",{timestamp:Date.now(),axes:c,buttons:m,mousePosition:b,textInputs:a})}disconnect(){this.connected&&(this.network.send("leave",{}),this.network.disconnect(),this.connected=!1,this.log("Disconnected from server"))}getUserId(){return this.userId}isConnected(){return this.connected}destroy(){this.disconnect(),this.network.destroy(),this.log("NetworkSync destroyed")}setupNetworkHandlers(){this.network.on("update",e=>{try{let t=this.convertToUint8Array(e,"update");if(!t)return;this.core.applyUpdatePacketBuffer(this.userId,t)?this.rendererManager.render(this.core,this.userId):this.log("Failed to apply update packet")}catch(t){this.log(`Error applying update packet: ${t}`)}}),this.network.on("update-static",e=>{this.handleUpdatePacket(e,"update-static")}),this.network.on("update-dynamic",e=>{this.handleUpdatePacket(e,"update-dynamic")}),this.network.on("load",e=>{try{let t=this.convertToUint8Array(e,"load");if(!t)return;this.core.applyLoadPacket(t)?this.log("Load packet applied successfully"):this.log("Failed to apply load packet")}catch(t){this.log(`Error applying load packet: ${t}`)}}),this.network.on("input-bindings",e=>{this.core.applyInputBindingsLoadPacket(this.userId,e)?this.log("Input bindings configured"):this.log("Failed to apply input bindings")}),this.network.on("disconnect",()=>{this.connected=!1,this.log("Disconnected from server")})}handleUpdatePacket(e,t){try{let n=this.convertToUint8Array(e,t);if(!n)return;let i=new DataView(n.buffer,n.byteOffset,n.byteLength),r=Number(i.getBigUint64(0,!1));if(t==="update-dynamic"&&r<this.lastReceivedTick)return;if(r>this.lastReceivedTick&&(this.lastReceivedTick=r),this.core.applyUpdatePacketBuffer(this.userId,n)){console.warn(`[CLIENT] ${t}: ${n.length}B (tick ${r}) - APPLIED`),this.options.debug&&this.debugRenderState();let d=performance.now();this.rendererManager.render(this.core,this.userId);let l=performance.now()-d;this.performanceMonitor.recordFrameTiming(0,l),this.options.debug&&console.warn(`[CLIENT] render() completed for ${t} (${l.toFixed(2)}ms)`)}else console.warn(`[CLIENT] Failed to apply ${t} packet (tick ${r})`)}catch(n){this.log(`Error applying ${t} update packet: ${n}`),console.error(n)}}convertToUint8Array(e,t){return e instanceof Uint8Array?e:e instanceof ArrayBuffer?new Uint8Array(e):Array.isArray(e)?new Uint8Array(e):e.data&&Array.isArray(e.data)?new Uint8Array(e.data):typeof e=="object"&&e.type==="Buffer"?new Uint8Array(e.data):(this.log(`Unknown data type for ${t} packet: ${typeof e}`),null)}debugRenderState(){let e=this.core.getUser(this.userId);if(e){let n=e.getLayers();console.warn(`[CLIENT] User has ${n.length} layers`),n.forEach((i,r)=>{let o=i.getOrders(),d=i.getStatic();(o.length>0||d)&&console.warn(` Layer ${r}: ${o.length} orders, static=${d}, z=${i.getZOrder()}`)})}let t=this.rendererManager.isReady();console.warn(`[CLIENT] Renderer ready: ${t}`)}log(e){this.options.debug&&console.warn(`[NetworkSync] ${e}`)}};f(U,"NetworkSync");var S=U;var E=class E{constructor(e){s(this,"core");s(this,"rendererManager");s(this,"rendererType");s(this,"input");s(this,"networkSync",null);s(this,"options");s(this,"running",!1);s(this,"startTime",0);s(this,"lastTimestamp",0);s(this,"userId","");s(this,"mode");s(this,"visibilityChangeHandler");s(this,"tickRate",30);s(this,"accumulatedTime",0);s(this,"FRAME_TIME_MIN",1e3/60);s(this,"lastRenderTimestamp",0);s(this,"rafId",0);s(this,"performanceMonitor");s(this,"renderMode","continuous");s(this,"renderRequested",!1);this.mode=e.mode,e.mode==="local"?(this.options={mode:"local",application:e.application,container:e.container,debug:e.debug??!1,width:e.width??80,height:e.height??25,userId:e.userId??"local",username:e.username??"User",showGrid:e.showGrid??!1,renderer:e.renderer??"webgl",useImageDataRendering:e.useImageDataRendering??!0,mobileInputConfig:e.mobileInputConfig,captureInput:e.captureInput??!1},this.userId=e.userId??"local"):this.options={mode:"connected",application:e.application,container:e.container,serverUrl:e.serverUrl,username:e.username??"Player",debug:e.debug??!1,width:e.width??80,height:e.height??25,autoReconnect:e.autoReconnect??!0,token:e.token,showGrid:e.showGrid??!1,renderer:e.renderer??"webgl",useImageDataRendering:e.useImageDataRendering??!0,mobileInputConfig:e.mobileInputConfig,captureInput:e.captureInput??!1},this.log(`Initializing ClientRuntime (${this.mode} mode)`),this.core=new $.Core({mode:"client",maxUsers:this.mode==="local"?1:100}),this.core.onPaletteChanged(n=>{this.onCorePaletteChanged(n)}),this.core.onBitmapFontChanged(n=>{this.onCoreBitmapFontChanged(n)}),this.visibilityChangeHandler=()=>{!document.hidden&&this.running&&(this.lastTimestamp=performance.now(),this.accumulatedTime=0,this.log("Tab visible: Reset timing"))},document.addEventListener("visibilitychange",this.visibilityChangeHandler);let t=this.createRenderer();if(this.rendererType=this.options.renderer??"webgl",this.rendererManager=new M(t,{debug:this.options.debug,useImageDataRendering:this.options.useImageDataRendering}),this.input=new y.UnifiedInputRouter({enableKeyboardMouse:!0,enableGamepad:!0,enableMobile:!0,targetElement:window,mobileTargetElement:void 0,debug:this.options.debug,keyboardConfig:{preventDefault:this.options.captureInput??!1,stopPropagation:this.options.captureInput??!1},mouseConfig:{preventDefault:this.options.captureInput??!1,stopPropagation:this.options.captureInput??!1},mobileConfig:{preventDefault:this.options.mobileInputConfig?.preventDefault??!0,passive:this.options.mobileInputConfig?.passive??!1,maxTouches:this.options.mobileInputConfig?.maxTouches??10}}),this.performanceMonitor=new I(60),this.mode==="connected"){let n=this.options,i=new D.SocketIOClient({url:n.serverUrl,autoReconnect:n.autoReconnect,auth:{username:n.username,token:n.token},debug:this.options.debug});this.networkSync=new S(i,this.core,this.rendererManager,this.performanceMonitor,{serverUrl:n.serverUrl,username:n.username,token:n.token,debug:this.options.debug,autoReconnect:n.autoReconnect,canvasWidth:this.options.width,canvasHeight:this.options.height})}this.tickRate=e.tickRate??30,this.tickRate===0&&!e.renderMode?(this.renderMode="on-demand",this.log("tickRate is 0: automatically enabling on-demand render mode")):this.renderMode=e.renderMode??"continuous",this.log(`Configured: tickRate=${this.tickRate}, renderMode=${this.renderMode}`),this.log("ClientRuntime initialized")}getMode(){return this.mode}getRendererType(){return this.rendererType}isRunning(){return this.running}createRenderer(){if((this.options.renderer??"webgl")==="terminal2d"){this.log("Creating Terminal 2D renderer");let r=new x.Terminal2D(this.options.container,{fixedCols:this.options.width,fixedRows:this.options.height,cellAspectRatio:1,showDebugGrid:this.options.showGrid});return this.options.useImageDataRendering&&(this.log("Enabling ImageData rendering (pixel-perfect, optimized)"),r.setImageDataRendering(!0)),this.log("Canvas 2D renderer created successfully"),r}this.log("Creating TerminalGL renderer");let n=document.createElement("canvas").getContext("webgl");if(!n)throw new Error("WebGL not supported. TerminalGL requires WebGL 1.0.");if(!n.getExtension("OES_element_index_uint"))throw new Error("OES_element_index_uint extension not supported. TerminalGL requires this extension for large terminals (256x256). Supported on 97% of devices (Android 4.3+, iOS 8+, Desktop).");if(!(this.options.container instanceof HTMLDivElement))throw new Error(`TerminalGL requires container to be an HTMLDivElement. Received: ${this.options.container.tagName}`);let i=new x.TerminalGL(this.options.container,{cols:this.options.width,rows:this.options.height,charWidth:8,charHeight:8,showGrid:this.options.showGrid});return this.log("TerminalGL created successfully"),i}setTickRate(e){if(e<0||e>1e3)throw new Error(`Invalid tick rate: ${e}. Must be between 0 and 1000.`);if(this.mode==="connected"){this.log("setTickRate() has no effect in connected mode");return}this.tickRate=e,e===0&&this.renderMode==="continuous"?(this.renderMode="on-demand",this.log(`Tick rate set to ${e} TPS (update loop disabled, automatically switched to on-demand render mode)`)):this.log(`Tick rate set to ${e} TPS ${e===0?"(update loop disabled)":""}`)}setRenderMode(e){this.renderMode=e,this.log(`Render mode set to ${e}`)}setMaxFPS(e){if(e<=0||e>240)throw new Error(`Invalid FPS: ${e}. Must be between 1 and 240.`);this.FRAME_TIME_MIN=1e3/e,this.log(`Max FPS set to ${e}`)}getTickRate(){return this.tickRate}requestRender(){this.renderMode==="on-demand"?(this.renderRequested=!0,this.log("Render requested")):this.log("requestRender() has no effect in continuous mode")}async start(){if(this.running){this.log("Already running");return}this.log("Starting ClientRuntime"),await this.rendererManager.initialize(this.core),this.log("Renderer is ready"),this.onCorePaletteChanged(this.core.getPalette()),this.log("Initial palette sent to renderer"),this.log("Calling application.init()"),this.options.application.init(this.core,this);let e=this.rendererManager.getCanvas();e&&this.input.setMobileTarget(e),this.mode==="connected"&&this.networkSync?(this.log("Connecting to server"),await this.networkSync.connect(),this.userId=await this.networkSync.joinGame(this.options.application)):await this.createLocalUser();let t=this.core.getUser(this.userId);if(t){let n=t.getDisplays();if(n.length>0){let r=n[0].getSize(),o=r.x,d=r.y,l=this.rendererManager.getRenderer(),a,c;if(this.rendererType==="webgl"){let g=l.getGridSize();a=g.cols,c=g.rows}else{let m=l;a=m.getCols(),c=m.getRows()}(a!==o||c!==d)&&(this.log(`Adjusting renderer from ${a}\xD7${c} to match display ${o}\xD7${d}`),l.resize(o,d))}}this.input.start(),this.running=!0,this.startTime=performance.now(),this.lastTimestamp=this.startTime,this.lastRenderTimestamp=this.startTime,this.accumulatedTime=0,this.performanceMonitor.reset(),this.performanceMonitor.startTracking(this.startTime),this.log("Starting main loop"),this.mainLoop(this.lastTimestamp)}async stop(){if(!this.running)return;this.log("Stopping ClientRuntime"),this.running=!1,this.rafId&&(cancelAnimationFrame(this.rafId),this.rafId=0),this.input.stop(),this.mode==="connected"&&this.networkSync&&this.networkSync.disconnect();let e=this.core.getUser(this.userId);e&&this.options.application.destroyUser&&this.options.application.destroyUser(this.core,e,"Runtime stopped"),this.log("ClientRuntime stopped")}getStats(){let e=this.performanceMonitor.getStats();return{mode:this.mode,running:this.running,userCount:this.core.getUsers().length,fps:e.fps,uptime:this.running?performance.now()-this.startTime:0,totalFrames:0,latency:void 0,lastFrameTiming:e.lastFrame,avgFrameTiming:e.avgFrame}}async destroy(){await this.stop(),this.log("Destroying ClientRuntime"),this.visibilityChangeHandler&&(document.removeEventListener("visibilitychange",this.visibilityChangeHandler),this.visibilityChangeHandler=void 0),this.options.application.destroy&&this.options.application.destroy(),this.rendererManager.destroy(),this.input.destroy(),this.networkSync&&this.networkSync.destroy(),this.log("ClientRuntime destroyed")}async createLocalUser(){let e=this.options;this.log(`Creating local user: ${this.userId}`);let t=this.core.createUser(this.userId,e.username);this.log("Calling application.initUser()"),this.options.application.initUser(this.core,t,{username:e.username}),this.log("Performing initial render"),this.core.endTick(),this.render(),this.log("Initial render complete")}mainLoop(e){if(!this.running){this.rafId&&(cancelAnimationFrame(this.rafId),this.rafId=0);return}if(this.renderMode==="on-demand"){this.renderRequested&&(this.renderRequested=!1,this.performanceMonitor.updateFPS(e),this.render()),this.rafId=requestAnimationFrame(o=>this.mainLoop(o));return}let t=e-this.lastRenderTimestamp;if(this.lastRenderTimestamp>0&&t<this.FRAME_TIME_MIN-1){this.rafId=requestAnimationFrame(o=>this.mainLoop(o));return}this.lastRenderTimestamp=e;let n=(e-this.lastTimestamp)/1e3,i=Math.min(n,.1);this.lastTimestamp=e,this.performanceMonitor.updateFPS(e);let r=this.core.getUser(this.userId);if(!r){this.rafId=requestAnimationFrame(o=>this.mainLoop(o));return}if(this.mode==="local"){if(this.tickRate===0){let a=performance.now();this.render();let c=performance.now()-a;this.performanceMonitor.recordFrameTiming(0,c),this.rafId=requestAnimationFrame(m=>this.mainLoop(m));return}this.accumulatedTime+=i;let o=1/this.tickRate;this.accumulatedTime>.5&&(this.accumulatedTime=o);let d=0,l=0;for(;this.accumulatedTime>=o&&d<5;){let a=performance.now();this.collectAndApplyInput(r),this.options.application.update(this.core,o),this.options.application.updateUser(this.core,r,o),r.clearTextInputs(),this.core.endTickSplit(),l+=performance.now()-a,this.accumulatedTime-=o,d++}if(d>0){let a=performance.now();this.render();let c=performance.now()-a;this.performanceMonitor.recordFrameTiming(l,c)}}else this.collectAndSendInput();this.rafId=requestAnimationFrame(o=>this.mainLoop(o))}collectAndApplyInput(e){let t=this.rendererManager.getCanvas();if(t){let d=e.getDisplays(),l=d.length>0?d[0].size.x:this.options.width,a=d.length>0?d[0].size.y:this.options.height,m=this.rendererManager.getRenderer()?.getOffsets?.(),g={offsetX:m?.offsetX??0,offsetY:m?.offsetY??0},b=y.InputCollector.collectMousePosition(this.input,t,l,a,g);e.setMousePosition(b.x,b.y,b.over);let h=y.InputCollector.collectTouchPositions(this.input,t,l,a,10,g);this.options.debug&&h.length>0&&console.warn("\u{1F4F1} Collected touches:",h),h.forEach(p=>{e.setTouchPosition(p.id,p.x,p.y,p.over)})}let n=y.InputCollector.collectTextInputs(this.input);n.length>0&&e.setTextInputs(n);let i=e.getInputBindingRegistry(),r=i.getAllAxes(),o=i.getAllButtons();if(r.length>0||o.length>0){let d=y.InputCollector.collectAxisSources(r,this.input),l=y.InputCollector.collectButtonSourcesWithTransitions(o,this.input);r.forEach(a=>{let c=i.evaluateAxis(a.bindingId,d);e.setAxis(a.name,c)}),o.forEach(a=>{let c=new Map,m=new Map,g=new Map;for(let[T,v]of l)c.set(T,v.pressed),m.set(T,v.justPressed),g.set(T,v.justReleased);let b=i.evaluateButton(a.bindingId,c),h=i.evaluateButton(a.bindingId,m),p=i.evaluateButton(a.bindingId,g);e.setButton(a.name,b),e.setButton(`${a.name}_justPressed`,h),e.setButton(`${a.name}_justReleased`,p)})}this.input.poll?.()}collectAndSendInput(){this.networkSync&&this.networkSync.sendInput(this.input)}render(){this.rendererManager.render(this.core,this.userId)}onCorePaletteChanged(e){this.log(`Palette changed, updating renderer (${e.size} colors)`);let t=[];for(let i=0;i<256;i++){let r=e.get(i);r?t.push({r:r.r,g:r.g,b:r.b,a:r.a}):t.push({r:0,g:0,b:0,a:0})}let n=this.rendererManager.getRenderer();if(!n){console.warn("[ClientRuntime] Cannot update palette: renderer is null");return}"setPalette"in n&&typeof n.setPalette=="function"?(n.setPalette(t),this.log("\u2713 Renderer palette updated successfully")):console.warn("[ClientRuntime] Renderer does not have setPalette method")}onCoreBitmapFontChanged(e){this.log(`Bitmap font ${e} changed, updating renderer`);let t=this.core.getBitmapFont(e);if(!t){console.warn(`[ClientRuntime] Font ${e} not found in registry`);return}let n=new Map,i=0;for(let o=0;o<256;o++){let d=t.getGlyph(o);d&&(n.set(o,d),i++)}this.log(`Built bitmap font map with ${i} glyphs, dimensions: ${t.getCharWidth()}x${t.getCharHeight()} (cell: ${t.getCellWidth()}x${t.getCellHeight()})`);let r=this.rendererManager.getRenderer();if(!r){console.warn("[ClientRuntime] Cannot update font: renderer is null");return}"setBitmapFont"in r&&typeof r.setBitmapFont=="function"?(r.setBitmapFont(n,t.getCharWidth(),t.getCharHeight(),t.getCellWidth(),t.getCellHeight()),this.log("\u2713 Renderer bitmap font updated successfully")):console.warn("[ClientRuntime] Renderer does not have setBitmapFont method")}log(e){this.options.debug&&console.warn(`[ClientRuntime/${this.mode}] ${e}`)}};f(E,"ClientRuntime");var k=E;
@@ -0,0 +1,212 @@
1
+ import { IApplication } from '@utsp/types';
2
+ export { IApplication } from '@utsp/types';
3
+
4
+ /**
5
+ * Runtime Client Types and Options
6
+ */
7
+
8
+ /**
9
+ * Client runtime mode
10
+ */
11
+ type ClientRuntimeMode = 'local' | 'connected';
12
+ /**
13
+ * Renderer type to use
14
+ */
15
+ declare enum RendererType {
16
+ /** WebGL2 renderer - Optimized WebGL 1.0 with GPU palette, instanced rendering (default, recommended) */
17
+ TerminalGL = "webgl",
18
+ /** Canvas 2D renderer - CPU fallback with ImageData optimization for benchmarks */
19
+ Terminal2D = "terminal2d"
20
+ }
21
+ /**
22
+ * Render mode for ClientRuntime
23
+ */
24
+ type RenderMode =
25
+ /** Continuous rendering at max FPS (default) - requestAnimationFrame loop */
26
+ 'continuous'
27
+ /** On-demand rendering - only renders when explicitly requested via requestRender() */
28
+ | 'on-demand';
29
+ /**
30
+ * Base client runtime options
31
+ */
32
+ interface BaseClientRuntimeOptions {
33
+ /** Application instance */
34
+ application: IApplication;
35
+ /** Container HTML element for rendering */
36
+ container: HTMLElement;
37
+ /** Enable debug logging */
38
+ debug?: boolean;
39
+ /** Initial display width in columns (default: 80). Can be overridden by IApplication via Display */
40
+ width?: number;
41
+ /** Initial display height in rows (default: 25). Can be overridden by IApplication via Display */
42
+ height?: number;
43
+ /** Renderer type (default: Auto) */
44
+ renderer?: RendererType;
45
+ /** Mobile/Touch input configuration */
46
+ mobileInputConfig?: {
47
+ /** Prevent default touch behavior (scroll, zoom). Default: true for canvas, false for page */
48
+ preventDefault?: boolean;
49
+ /** Use passive event listeners for better scroll performance. Default: false */
50
+ passive?: boolean;
51
+ /** Maximum simultaneous touches. Default: 10 */
52
+ maxTouches?: number;
53
+ };
54
+ /** Show debug grid to visualize cell boundaries (default: false) */
55
+ showGrid?: boolean;
56
+ /** Enable ImageData rendering for Canvas 2D (default: true). Provides pixel-perfect rendering with no gaps between cells. 10-20× faster than fillRect. Only for Terminal2D renderer with bitmap fonts. */
57
+ useImageDataRendering?: boolean;
58
+ /** Render mode: 'continuous' (default) renders at max FPS, 'on-demand' only renders when explicitly requested. When tickRate is 0, automatically switches to 'on-demand'. */
59
+ renderMode?: RenderMode;
60
+ /** Tick rate in ticks per second (default: 30). Set to 0 to disable update loop (only init/initUser, no update/updateUser). When set to 0, automatically enables 'on-demand' render mode. */
61
+ tickRate?: number;
62
+ /** Capture input events to prevent default browser behavior (Tab, arrows, etc.). Default: false. When true, preventDefault() and stopPropagation() are called on keyboard and mouse events to keep focus in the terminal. All F keys (F1-F12) and Ctrl/Cmd shortcuts are automatically excluded. */
63
+ captureInput?: boolean;
64
+ }
65
+ /**
66
+ * Local mode options (standalone, no network)
67
+ */
68
+ interface LocalModeOptions extends BaseClientRuntimeOptions {
69
+ /** Runtime mode */
70
+ mode: 'local';
71
+ /** User ID (default: 'local') */
72
+ userId?: string;
73
+ /** User name (default: 'User') */
74
+ username?: string;
75
+ }
76
+ /**
77
+ * Connected mode options (multiplayer)
78
+ */
79
+ interface ConnectedModeOptions extends BaseClientRuntimeOptions {
80
+ /** Runtime mode */
81
+ mode: 'connected';
82
+ /** Server URL (e.g., 'ws://localhost:3000') */
83
+ serverUrl: string;
84
+ /** User name */
85
+ username?: string;
86
+ /** Auto-reconnect on disconnect */
87
+ autoReconnect?: boolean;
88
+ /** Authentication token */
89
+ token?: string;
90
+ }
91
+ /**
92
+ * Union type of client runtime options
93
+ */
94
+ type ClientRuntimeOptions = LocalModeOptions | ConnectedModeOptions;
95
+ /**
96
+ * Performance timing for a single frame
97
+ */
98
+ interface FrameTiming {
99
+ /** Time spent in Core tick (ms) */
100
+ coreTime: number;
101
+ /** Time spent in Renderer (ms) */
102
+ renderTime: number;
103
+ /** Total frame time (ms) */
104
+ totalTime: number;
105
+ }
106
+ /**
107
+ * Client runtime statistics
108
+ */
109
+ interface ClientRuntimeStats {
110
+ /** Runtime mode */
111
+ mode: ClientRuntimeMode;
112
+ /** Is runtime currently running */
113
+ running: boolean;
114
+ /** Number of users (1 for local, multiple for connected) */
115
+ userCount: number;
116
+ /** Current FPS */
117
+ fps: number;
118
+ /** Uptime in milliseconds */
119
+ uptime: number;
120
+ /** Total frames processed */
121
+ totalFrames: number;
122
+ /** Network latency in ms (connected mode only) */
123
+ latency?: number;
124
+ /** Last frame timing breakdown */
125
+ lastFrameTiming?: FrameTiming;
126
+ /** Average frame timing over last 60 frames */
127
+ avgFrameTiming?: FrameTiming;
128
+ }
129
+
130
+ /**
131
+ * Client Runtime
132
+ *
133
+ * Unified client runtime supporting both local and connected modes.
134
+ */
135
+
136
+ declare class ClientRuntime {
137
+ private core;
138
+ private rendererManager;
139
+ private rendererType;
140
+ private input;
141
+ private networkSync;
142
+ private options;
143
+ private running;
144
+ private startTime;
145
+ private lastTimestamp;
146
+ private userId;
147
+ private mode;
148
+ private visibilityChangeHandler?;
149
+ private tickRate;
150
+ private accumulatedTime;
151
+ private readonly FRAME_TIME_MIN;
152
+ private lastRenderTimestamp;
153
+ private rafId;
154
+ private performanceMonitor;
155
+ private renderMode;
156
+ private renderRequested;
157
+ constructor(options: ClientRuntimeOptions);
158
+ getMode(): ClientRuntimeMode;
159
+ getRendererType(): RendererType;
160
+ isRunning(): boolean;
161
+ private createRenderer;
162
+ setTickRate(tickRate: number): void;
163
+ setRenderMode(mode: 'continuous' | 'on-demand'): void;
164
+ setMaxFPS(fps: number): void;
165
+ getTickRate(): number;
166
+ /**
167
+ * Request a render in on-demand mode.
168
+ * Has no effect in continuous mode (renders automatically).
169
+ *
170
+ * @example
171
+ * ```typescript
172
+ * // On-demand mode: render only when needed
173
+ * const runtime = new ClientRuntime({
174
+ * mode: 'local',
175
+ * renderMode: 'on-demand',
176
+ * tickRate: 0, // Disable update loop
177
+ * application: myApp,
178
+ * container: document.getElementById('app')
179
+ * });
180
+ *
181
+ * await runtime.start();
182
+ *
183
+ * // Manually request render after changing something
184
+ * core.getUser('local').setCell(0, 0, '@', 1, 0);
185
+ * runtime.requestRender(); // Trigger render
186
+ * ```
187
+ */
188
+ requestRender(): void;
189
+ start(): Promise<void>;
190
+ stop(): Promise<void>;
191
+ getStats(): ClientRuntimeStats;
192
+ destroy(): Promise<void>;
193
+ private createLocalUser;
194
+ private mainLoop;
195
+ private collectAndApplyInput;
196
+ private collectAndSendInput;
197
+ private render;
198
+ /**
199
+ * Handle palette change from Core
200
+ * Converts Core palette Map to RGBColor array and updates renderer
201
+ */
202
+ private onCorePaletteChanged;
203
+ /**
204
+ * Handle bitmap font change from Core
205
+ * Loads the font from registry and updates renderer
206
+ */
207
+ private onCoreBitmapFontChanged;
208
+ private log;
209
+ }
210
+
211
+ export { ClientRuntime, RendererType };
212
+ export type { BaseClientRuntimeOptions, ClientRuntimeMode, ClientRuntimeOptions, ClientRuntimeStats, ConnectedModeOptions, FrameTiming, LocalModeOptions, RenderMode };
package/dist/index.mjs ADDED
@@ -0,0 +1 @@
1
+ var A=Object.defineProperty;var E=(f,e,t)=>e in f?A(f,e,{enumerable:!0,configurable:!0,writable:!0,value:t}):f[e]=t;var g=(f,e)=>A(f,"name",{value:e,configurable:!0});var i=(f,e,t)=>(E(f,typeof e!="symbol"?e+"":e,t),t);import{Core as $}from"@utsp/core";import{TerminalGL as D,Terminal2D as B}from"@utsp/render";import{UnifiedInputRouter as O,InputCollector as R}from"@utsp/input";import{SocketIOClient as G}from"@utsp/network-client";var U=(t=>(t.TerminalGL="webgl",t.Terminal2D="terminal2d",t))(U||{});var S=class S{constructor(e=60){i(this,"frameCount",0);i(this,"fps",0);i(this,"fpsUpdateTime",0);i(this,"lastCoreTime",0);i(this,"lastRenderTime",0);i(this,"lastTotalTime",0);i(this,"coreTimeSamples",[]);i(this,"renderTimeSamples",[]);i(this,"totalTimeSamples",[]);i(this,"maxSamples");if(e<=0)throw new Error("maxSamples must be positive");this.maxSamples=e}startTracking(e){this.fpsUpdateTime=e,this.frameCount=0,this.fps=0}updateFPS(e){this.frameCount++,e-this.fpsUpdateTime>=1e3&&(this.fps=Math.round(this.frameCount*1e3/(e-this.fpsUpdateTime)),this.frameCount=0,this.fpsUpdateTime=e)}recordFrameTiming(e,t){let n=e+t;this.lastCoreTime=e,this.lastRenderTime=t,this.lastTotalTime=n,this.coreTimeSamples.push(e),this.renderTimeSamples.push(t),this.totalTimeSamples.push(n),this.coreTimeSamples.length>this.maxSamples&&(this.coreTimeSamples.shift(),this.renderTimeSamples.shift(),this.totalTimeSamples.shift())}getFPS(){return this.fps}getLastFrameTiming(){if(!(this.lastCoreTime===0&&this.lastRenderTime===0))return{coreTime:this.lastCoreTime,renderTime:this.lastRenderTime,totalTime:this.lastTotalTime}}getAverageFrameTiming(){if(this.coreTimeSamples.length!==0)return{coreTime:this.average(this.coreTimeSamples),renderTime:this.average(this.renderTimeSamples),totalTime:this.average(this.totalTimeSamples)}}getStats(){return{fps:this.fps,lastFrame:this.getLastFrameTiming(),avgFrame:this.getAverageFrameTiming(),sampleCount:this.coreTimeSamples.length,maxSamples:this.maxSamples}}reset(){this.frameCount=0,this.fps=0,this.fpsUpdateTime=0,this.lastCoreTime=0,this.lastRenderTime=0,this.lastTotalTime=0,this.coreTimeSamples=[],this.renderTimeSamples=[],this.totalTimeSamples=[]}average(e){return e.length===0?0:e.reduce((t,n)=>t+n,0)/e.length}};g(S,"PerformanceMonitor");var w=S;import{createASCII8x8FontLoad as L}from"@utsp/core";var k=class k{constructor(e,t={}){i(this,"renderer");i(this,"options");this.renderer=e,this.options={debug:t.debug??!1,useImageDataRendering:t.useImageDataRendering??!1}}async initialize(e){await this.loadDefaultFont(e),await this.waitForReady()}async waitForReady(e=100,t=50){this.log("Waiting for renderer to be ready");let n=0;return new Promise((s,r)=>{let o=g(()=>{if(n++,this.renderer.isReady())this.log(`Renderer ready after ${n*t}ms`),s();else if(n>=e){let l=`Renderer failed to be ready after ${e*t}ms`;this.log(l),r(new Error(l))}else setTimeout(o,t)},"check");o()})}async loadDefaultFont(e){this.log("Loading ASCII 8x8 font");let t=L(1);e.hasBitmapFont(t.fontId)||e.loadBitmapFontById(t.fontId,{charWidth:t.width,charHeight:t.height,cellWidth:t.cellWidth,cellHeight:t.cellHeight,glyphs:new Map(t.characters.map(n=>[n.charCode,n.bitmap]))}),"setImageDataRendering"in this.renderer&&this.options.useImageDataRendering&&(this.renderer.setImageDataRendering(!0),this.log("ImageData rendering enabled")),this.log("Font loaded and event triggered")}render(e,t){if(!this.renderer.isReady())return console.warn("[RENDERER MANAGER] Renderer not ready"),!1;let n=e.getRenderState(t);if(!n||n.displays.length===0)return console.warn("[RENDERER MANAGER] No render state or no displays"),!1;let s=n.displays[0];return this.renderer.renderDisplayData(s),!0}getRenderer(){return this.renderer}getCanvas(){return this.renderer.getCanvas()}isReady(){return this.renderer.isReady()}destroy(){this.log("Destroying renderer"),this.renderer.destroy()}log(e){this.options.debug&&console.warn(`[RendererManager] ${e}`)}};g(k,"RendererManager");var I=k;import{InputCollector as T}from"@utsp/input";var x=class x{constructor(e,t,n,s,r){i(this,"network");i(this,"core");i(this,"rendererManager");i(this,"performanceMonitor");i(this,"options");i(this,"userId","");i(this,"lastReceivedTick",-1);i(this,"connected",!1);this.network=e,this.core=t,this.rendererManager=n,this.performanceMonitor=s,this.options={serverUrl:r.serverUrl,username:r.username,token:r.token??"",debug:r.debug??!1,autoReconnect:r.autoReconnect??!0,canvasWidth:r.canvasWidth,canvasHeight:r.canvasHeight}}async connect(){this.log(`Connecting to ${this.options.serverUrl}`),await this.network.connect(),this.connected=!0,this.log("Connected to server"),this.setupNetworkHandlers()}async joinGame(e){return new Promise((t,n)=>{this.network.send("join",{username:this.options.username,token:this.options.token}),this.network.on("join_response",s=>{if(s.success){this.userId=s.userId,this.log(`Joined game as ${this.userId}`);let r=this.core.createUser(this.userId,this.options.username);e.initUser(this.core,r,{username:this.options.username,token:this.options.token}),t(this.userId)}else n(new Error(s.error||"Failed to join game"))}),setTimeout(()=>n(new Error("Join timeout")),5e3)})}sendInput(e){let t=this.core.getUser(this.userId);if(!t)return;let n=this.rendererManager.getCanvas();if(n){let u=this.rendererManager.getRenderer()?.getOffsets?.(),v={offsetX:u?.offsetX??0,offsetY:u?.offsetY??0},b=T.collectMousePosition(e,n,this.options.canvasWidth,this.options.canvasHeight,v);t.setMousePosition(b.x,b.y,b.over),T.collectTouchPositions(e,n,this.options.canvasWidth,this.options.canvasHeight,10,v).forEach(C=>{t.setTouchPosition(C.id,C.x,C.y,C.over)})}let s=t.getInputBindingRegistry(),r=s.getAllAxes(),o=s.getAllButtons(),d=T.collectAxisSources(r,e),l=T.collectButtonSources(o,e),a=T.collectTextInputs(e);a.length>0&&t.setTextInputs(a);let c={};r.forEach(m=>{let u=s.evaluateAxis(m.bindingId,d);c[m.name]=u,t.setAxis(m.name,u)});let p={};o.forEach(m=>{let u=s.evaluateButton(m.bindingId,l);p[m.name]=u,t.setButton(m.name,u)});let h=t.getMouseDisplayInfo(),y=h?{x:h.localX,y:h.localY}:null;this.network.send("input",{timestamp:Date.now(),axes:c,buttons:p,mousePosition:y,textInputs:a})}disconnect(){this.connected&&(this.network.send("leave",{}),this.network.disconnect(),this.connected=!1,this.log("Disconnected from server"))}getUserId(){return this.userId}isConnected(){return this.connected}destroy(){this.disconnect(),this.network.destroy(),this.log("NetworkSync destroyed")}setupNetworkHandlers(){this.network.on("update",e=>{try{let t=this.convertToUint8Array(e,"update");if(!t)return;this.core.applyUpdatePacketBuffer(this.userId,t)?this.rendererManager.render(this.core,this.userId):this.log("Failed to apply update packet")}catch(t){this.log(`Error applying update packet: ${t}`)}}),this.network.on("update-static",e=>{this.handleUpdatePacket(e,"update-static")}),this.network.on("update-dynamic",e=>{this.handleUpdatePacket(e,"update-dynamic")}),this.network.on("load",e=>{try{let t=this.convertToUint8Array(e,"load");if(!t)return;this.core.applyLoadPacket(t)?this.log("Load packet applied successfully"):this.log("Failed to apply load packet")}catch(t){this.log(`Error applying load packet: ${t}`)}}),this.network.on("input-bindings",e=>{this.core.applyInputBindingsLoadPacket(this.userId,e)?this.log("Input bindings configured"):this.log("Failed to apply input bindings")}),this.network.on("disconnect",()=>{this.connected=!1,this.log("Disconnected from server")})}handleUpdatePacket(e,t){try{let n=this.convertToUint8Array(e,t);if(!n)return;let s=new DataView(n.buffer,n.byteOffset,n.byteLength),r=Number(s.getBigUint64(0,!1));if(t==="update-dynamic"&&r<this.lastReceivedTick)return;if(r>this.lastReceivedTick&&(this.lastReceivedTick=r),this.core.applyUpdatePacketBuffer(this.userId,n)){console.warn(`[CLIENT] ${t}: ${n.length}B (tick ${r}) - APPLIED`),this.options.debug&&this.debugRenderState();let d=performance.now();this.rendererManager.render(this.core,this.userId);let l=performance.now()-d;this.performanceMonitor.recordFrameTiming(0,l),this.options.debug&&console.warn(`[CLIENT] render() completed for ${t} (${l.toFixed(2)}ms)`)}else console.warn(`[CLIENT] Failed to apply ${t} packet (tick ${r})`)}catch(n){this.log(`Error applying ${t} update packet: ${n}`),console.error(n)}}convertToUint8Array(e,t){return e instanceof Uint8Array?e:e instanceof ArrayBuffer?new Uint8Array(e):Array.isArray(e)?new Uint8Array(e):e.data&&Array.isArray(e.data)?new Uint8Array(e.data):typeof e=="object"&&e.type==="Buffer"?new Uint8Array(e.data):(this.log(`Unknown data type for ${t} packet: ${typeof e}`),null)}debugRenderState(){let e=this.core.getUser(this.userId);if(e){let n=e.getLayers();console.warn(`[CLIENT] User has ${n.length} layers`),n.forEach((s,r)=>{let o=s.getOrders(),d=s.getStatic();(o.length>0||d)&&console.warn(` Layer ${r}: ${o.length} orders, static=${d}, z=${s.getZOrder()}`)})}let t=this.rendererManager.isReady();console.warn(`[CLIENT] Renderer ready: ${t}`)}log(e){this.options.debug&&console.warn(`[NetworkSync] ${e}`)}};g(x,"NetworkSync");var M=x;var P=class P{constructor(e){i(this,"core");i(this,"rendererManager");i(this,"rendererType");i(this,"input");i(this,"networkSync",null);i(this,"options");i(this,"running",!1);i(this,"startTime",0);i(this,"lastTimestamp",0);i(this,"userId","");i(this,"mode");i(this,"visibilityChangeHandler");i(this,"tickRate",30);i(this,"accumulatedTime",0);i(this,"FRAME_TIME_MIN",1e3/60);i(this,"lastRenderTimestamp",0);i(this,"rafId",0);i(this,"performanceMonitor");i(this,"renderMode","continuous");i(this,"renderRequested",!1);this.mode=e.mode,e.mode==="local"?(this.options={mode:"local",application:e.application,container:e.container,debug:e.debug??!1,width:e.width??80,height:e.height??25,userId:e.userId??"local",username:e.username??"User",showGrid:e.showGrid??!1,renderer:e.renderer??"webgl",useImageDataRendering:e.useImageDataRendering??!0,mobileInputConfig:e.mobileInputConfig,captureInput:e.captureInput??!1},this.userId=e.userId??"local"):this.options={mode:"connected",application:e.application,container:e.container,serverUrl:e.serverUrl,username:e.username??"Player",debug:e.debug??!1,width:e.width??80,height:e.height??25,autoReconnect:e.autoReconnect??!0,token:e.token,showGrid:e.showGrid??!1,renderer:e.renderer??"webgl",useImageDataRendering:e.useImageDataRendering??!0,mobileInputConfig:e.mobileInputConfig,captureInput:e.captureInput??!1},this.log(`Initializing ClientRuntime (${this.mode} mode)`),this.core=new $({mode:"client",maxUsers:this.mode==="local"?1:100}),this.core.onPaletteChanged(n=>{this.onCorePaletteChanged(n)}),this.core.onBitmapFontChanged(n=>{this.onCoreBitmapFontChanged(n)}),this.visibilityChangeHandler=()=>{!document.hidden&&this.running&&(this.lastTimestamp=performance.now(),this.accumulatedTime=0,this.log("Tab visible: Reset timing"))},document.addEventListener("visibilitychange",this.visibilityChangeHandler);let t=this.createRenderer();if(this.rendererType=this.options.renderer??"webgl",this.rendererManager=new I(t,{debug:this.options.debug,useImageDataRendering:this.options.useImageDataRendering}),this.input=new O({enableKeyboardMouse:!0,enableGamepad:!0,enableMobile:!0,targetElement:window,mobileTargetElement:void 0,debug:this.options.debug,keyboardConfig:{preventDefault:this.options.captureInput??!1,stopPropagation:this.options.captureInput??!1},mouseConfig:{preventDefault:this.options.captureInput??!1,stopPropagation:this.options.captureInput??!1},mobileConfig:{preventDefault:this.options.mobileInputConfig?.preventDefault??!0,passive:this.options.mobileInputConfig?.passive??!1,maxTouches:this.options.mobileInputConfig?.maxTouches??10}}),this.performanceMonitor=new w(60),this.mode==="connected"){let n=this.options,s=new G({url:n.serverUrl,autoReconnect:n.autoReconnect,auth:{username:n.username,token:n.token},debug:this.options.debug});this.networkSync=new M(s,this.core,this.rendererManager,this.performanceMonitor,{serverUrl:n.serverUrl,username:n.username,token:n.token,debug:this.options.debug,autoReconnect:n.autoReconnect,canvasWidth:this.options.width,canvasHeight:this.options.height})}this.tickRate=e.tickRate??30,this.tickRate===0&&!e.renderMode?(this.renderMode="on-demand",this.log("tickRate is 0: automatically enabling on-demand render mode")):this.renderMode=e.renderMode??"continuous",this.log(`Configured: tickRate=${this.tickRate}, renderMode=${this.renderMode}`),this.log("ClientRuntime initialized")}getMode(){return this.mode}getRendererType(){return this.rendererType}isRunning(){return this.running}createRenderer(){if((this.options.renderer??"webgl")==="terminal2d"){this.log("Creating Terminal 2D renderer");let r=new B(this.options.container,{fixedCols:this.options.width,fixedRows:this.options.height,cellAspectRatio:1,showDebugGrid:this.options.showGrid});return this.options.useImageDataRendering&&(this.log("Enabling ImageData rendering (pixel-perfect, optimized)"),r.setImageDataRendering(!0)),this.log("Canvas 2D renderer created successfully"),r}this.log("Creating TerminalGL renderer");let n=document.createElement("canvas").getContext("webgl");if(!n)throw new Error("WebGL not supported. TerminalGL requires WebGL 1.0.");if(!n.getExtension("OES_element_index_uint"))throw new Error("OES_element_index_uint extension not supported. TerminalGL requires this extension for large terminals (256x256). Supported on 97% of devices (Android 4.3+, iOS 8+, Desktop).");if(!(this.options.container instanceof HTMLDivElement))throw new Error(`TerminalGL requires container to be an HTMLDivElement. Received: ${this.options.container.tagName}`);let s=new D(this.options.container,{cols:this.options.width,rows:this.options.height,charWidth:8,charHeight:8,showGrid:this.options.showGrid});return this.log("TerminalGL created successfully"),s}setTickRate(e){if(e<0||e>1e3)throw new Error(`Invalid tick rate: ${e}. Must be between 0 and 1000.`);if(this.mode==="connected"){this.log("setTickRate() has no effect in connected mode");return}this.tickRate=e,e===0&&this.renderMode==="continuous"?(this.renderMode="on-demand",this.log(`Tick rate set to ${e} TPS (update loop disabled, automatically switched to on-demand render mode)`)):this.log(`Tick rate set to ${e} TPS ${e===0?"(update loop disabled)":""}`)}setRenderMode(e){this.renderMode=e,this.log(`Render mode set to ${e}`)}setMaxFPS(e){if(e<=0||e>240)throw new Error(`Invalid FPS: ${e}. Must be between 1 and 240.`);this.FRAME_TIME_MIN=1e3/e,this.log(`Max FPS set to ${e}`)}getTickRate(){return this.tickRate}requestRender(){this.renderMode==="on-demand"?(this.renderRequested=!0,this.log("Render requested")):this.log("requestRender() has no effect in continuous mode")}async start(){if(this.running){this.log("Already running");return}this.log("Starting ClientRuntime"),await this.rendererManager.initialize(this.core),this.log("Renderer is ready"),this.onCorePaletteChanged(this.core.getPalette()),this.log("Initial palette sent to renderer"),this.log("Calling application.init()"),this.options.application.init(this.core,this);let e=this.rendererManager.getCanvas();e&&this.input.setMobileTarget(e),this.mode==="connected"&&this.networkSync?(this.log("Connecting to server"),await this.networkSync.connect(),this.userId=await this.networkSync.joinGame(this.options.application)):await this.createLocalUser();let t=this.core.getUser(this.userId);if(t){let n=t.getDisplays();if(n.length>0){let r=n[0].getSize(),o=r.x,d=r.y,l=this.rendererManager.getRenderer(),a,c;if(this.rendererType==="webgl"){let h=l.getGridSize();a=h.cols,c=h.rows}else{let p=l;a=p.getCols(),c=p.getRows()}(a!==o||c!==d)&&(this.log(`Adjusting renderer from ${a}\xD7${c} to match display ${o}\xD7${d}`),l.resize(o,d))}}this.input.start(),this.running=!0,this.startTime=performance.now(),this.lastTimestamp=this.startTime,this.lastRenderTimestamp=this.startTime,this.accumulatedTime=0,this.performanceMonitor.reset(),this.performanceMonitor.startTracking(this.startTime),this.log("Starting main loop"),this.mainLoop(this.lastTimestamp)}async stop(){if(!this.running)return;this.log("Stopping ClientRuntime"),this.running=!1,this.rafId&&(cancelAnimationFrame(this.rafId),this.rafId=0),this.input.stop(),this.mode==="connected"&&this.networkSync&&this.networkSync.disconnect();let e=this.core.getUser(this.userId);e&&this.options.application.destroyUser&&this.options.application.destroyUser(this.core,e,"Runtime stopped"),this.log("ClientRuntime stopped")}getStats(){let e=this.performanceMonitor.getStats();return{mode:this.mode,running:this.running,userCount:this.core.getUsers().length,fps:e.fps,uptime:this.running?performance.now()-this.startTime:0,totalFrames:0,latency:void 0,lastFrameTiming:e.lastFrame,avgFrameTiming:e.avgFrame}}async destroy(){await this.stop(),this.log("Destroying ClientRuntime"),this.visibilityChangeHandler&&(document.removeEventListener("visibilitychange",this.visibilityChangeHandler),this.visibilityChangeHandler=void 0),this.options.application.destroy&&this.options.application.destroy(),this.rendererManager.destroy(),this.input.destroy(),this.networkSync&&this.networkSync.destroy(),this.log("ClientRuntime destroyed")}async createLocalUser(){let e=this.options;this.log(`Creating local user: ${this.userId}`);let t=this.core.createUser(this.userId,e.username);this.log("Calling application.initUser()"),this.options.application.initUser(this.core,t,{username:e.username}),this.log("Performing initial render"),this.core.endTick(),this.render(),this.log("Initial render complete")}mainLoop(e){if(!this.running){this.rafId&&(cancelAnimationFrame(this.rafId),this.rafId=0);return}if(this.renderMode==="on-demand"){this.renderRequested&&(this.renderRequested=!1,this.performanceMonitor.updateFPS(e),this.render()),this.rafId=requestAnimationFrame(o=>this.mainLoop(o));return}let t=e-this.lastRenderTimestamp;if(this.lastRenderTimestamp>0&&t<this.FRAME_TIME_MIN-1){this.rafId=requestAnimationFrame(o=>this.mainLoop(o));return}this.lastRenderTimestamp=e;let n=(e-this.lastTimestamp)/1e3,s=Math.min(n,.1);this.lastTimestamp=e,this.performanceMonitor.updateFPS(e);let r=this.core.getUser(this.userId);if(!r){this.rafId=requestAnimationFrame(o=>this.mainLoop(o));return}if(this.mode==="local"){if(this.tickRate===0){let a=performance.now();this.render();let c=performance.now()-a;this.performanceMonitor.recordFrameTiming(0,c),this.rafId=requestAnimationFrame(p=>this.mainLoop(p));return}this.accumulatedTime+=s;let o=1/this.tickRate;this.accumulatedTime>.5&&(this.accumulatedTime=o);let d=0,l=0;for(;this.accumulatedTime>=o&&d<5;){let a=performance.now();this.collectAndApplyInput(r),this.options.application.update(this.core,o),this.options.application.updateUser(this.core,r,o),r.clearTextInputs(),this.core.endTickSplit(),l+=performance.now()-a,this.accumulatedTime-=o,d++}if(d>0){let a=performance.now();this.render();let c=performance.now()-a;this.performanceMonitor.recordFrameTiming(l,c)}}else this.collectAndSendInput();this.rafId=requestAnimationFrame(o=>this.mainLoop(o))}collectAndApplyInput(e){let t=this.rendererManager.getCanvas();if(t){let d=e.getDisplays(),l=d.length>0?d[0].size.x:this.options.width,a=d.length>0?d[0].size.y:this.options.height,p=this.rendererManager.getRenderer()?.getOffsets?.(),h={offsetX:p?.offsetX??0,offsetY:p?.offsetY??0},y=R.collectMousePosition(this.input,t,l,a,h);e.setMousePosition(y.x,y.y,y.over);let m=R.collectTouchPositions(this.input,t,l,a,10,h);this.options.debug&&m.length>0&&console.warn("\u{1F4F1} Collected touches:",m),m.forEach(u=>{e.setTouchPosition(u.id,u.x,u.y,u.over)})}let n=R.collectTextInputs(this.input);n.length>0&&e.setTextInputs(n);let s=e.getInputBindingRegistry(),r=s.getAllAxes(),o=s.getAllButtons();if(r.length>0||o.length>0){let d=R.collectAxisSources(r,this.input),l=R.collectButtonSourcesWithTransitions(o,this.input);r.forEach(a=>{let c=s.evaluateAxis(a.bindingId,d);e.setAxis(a.name,c)}),o.forEach(a=>{let c=new Map,p=new Map,h=new Map;for(let[v,b]of l)c.set(v,b.pressed),p.set(v,b.justPressed),h.set(v,b.justReleased);let y=s.evaluateButton(a.bindingId,c),m=s.evaluateButton(a.bindingId,p),u=s.evaluateButton(a.bindingId,h);e.setButton(a.name,y),e.setButton(`${a.name}_justPressed`,m),e.setButton(`${a.name}_justReleased`,u)})}this.input.poll?.()}collectAndSendInput(){this.networkSync&&this.networkSync.sendInput(this.input)}render(){this.rendererManager.render(this.core,this.userId)}onCorePaletteChanged(e){this.log(`Palette changed, updating renderer (${e.size} colors)`);let t=[];for(let s=0;s<256;s++){let r=e.get(s);r?t.push({r:r.r,g:r.g,b:r.b,a:r.a}):t.push({r:0,g:0,b:0,a:0})}let n=this.rendererManager.getRenderer();if(!n){console.warn("[ClientRuntime] Cannot update palette: renderer is null");return}"setPalette"in n&&typeof n.setPalette=="function"?(n.setPalette(t),this.log("\u2713 Renderer palette updated successfully")):console.warn("[ClientRuntime] Renderer does not have setPalette method")}onCoreBitmapFontChanged(e){this.log(`Bitmap font ${e} changed, updating renderer`);let t=this.core.getBitmapFont(e);if(!t){console.warn(`[ClientRuntime] Font ${e} not found in registry`);return}let n=new Map,s=0;for(let o=0;o<256;o++){let d=t.getGlyph(o);d&&(n.set(o,d),s++)}this.log(`Built bitmap font map with ${s} glyphs, dimensions: ${t.getCharWidth()}x${t.getCharHeight()} (cell: ${t.getCellWidth()}x${t.getCellHeight()})`);let r=this.rendererManager.getRenderer();if(!r){console.warn("[ClientRuntime] Cannot update font: renderer is null");return}"setBitmapFont"in r&&typeof r.setBitmapFont=="function"?(r.setBitmapFont(n,t.getCharWidth(),t.getCharHeight(),t.getCellWidth(),t.getCellHeight()),this.log("\u2713 Renderer bitmap font updated successfully")):console.warn("[ClientRuntime] Renderer does not have setBitmapFont method")}log(e){this.options.debug&&console.warn(`[ClientRuntime/${this.mode}] ${e}`)}};g(P,"ClientRuntime");var F=P;export{F as ClientRuntime,U as RendererType};
package/package.json ADDED
@@ -0,0 +1,68 @@
1
+ {
2
+ "name": "@utsp/runtime-client",
3
+ "version": "0.1.1",
4
+ "description": "UTSP Runtime Client - Local and multi-user client runtime",
5
+ "author": "Thomas Piquet",
6
+ "license": "MIT",
7
+ "type": "module",
8
+ "main": "./dist/index.cjs",
9
+ "module": "./dist/index.mjs",
10
+ "types": "./dist/index.d.ts",
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/index.d.ts",
14
+ "import": "./dist/index.mjs",
15
+ "require": "./dist/index.cjs"
16
+ }
17
+ },
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "https://github.com/thp-software/utsp.git",
21
+ "directory": "packages/runtime-client"
22
+ },
23
+ "bugs": {
24
+ "url": "https://github.com/thp-software/utsp/issues"
25
+ },
26
+ "homepage": "https://github.com/thp-software/utsp/tree/master/packages/runtime-client#readme",
27
+ "keywords": [
28
+ "utsp",
29
+ "runtime",
30
+ "client",
31
+ "browser",
32
+ "multi-user",
33
+ "local",
34
+ "terminal",
35
+ "rendering",
36
+ "input"
37
+ ],
38
+ "engines": {
39
+ "node": ">=18.0.0"
40
+ },
41
+ "sideEffects": false,
42
+ "files": [
43
+ "dist",
44
+ "README.md",
45
+ "LICENSE"
46
+ ],
47
+ "publishConfig": {
48
+ "access": "public"
49
+ },
50
+ "dependencies": {
51
+ "@utsp/core": "0.1.1",
52
+ "@utsp/input": "0.1.1",
53
+ "@utsp/render": "0.1.1",
54
+ "@utsp/network-client": "0.1.1",
55
+ "@utsp/types": "0.1.1"
56
+ },
57
+ "devDependencies": {
58
+ "typescript": "^5.6.3"
59
+ },
60
+ "scripts": {
61
+ "build": "node ../../scripts/build-package.mjs packages/runtime-client",
62
+ "dev": "tsc --watch",
63
+ "clean": "rimraf dist",
64
+ "lint": "eslint \"src/**/*.ts\" --max-warnings 0",
65
+ "lint:fix": "eslint \"src/**/*.ts\" --fix",
66
+ "typecheck": "tsc --noEmit"
67
+ }
68
+ }