commit 88d231e3671951db60bec1f06a7bde8cc4a434c9 Author: usernames122 Date: Sun Oct 12 18:03:20 2025 +0200 init commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c2658d7 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules/ diff --git a/index.html b/index.html new file mode 100644 index 0000000..5dd7e95 --- /dev/null +++ b/index.html @@ -0,0 +1,14 @@ + + + + + RobloxEngine + + + + + + + \ No newline at end of file diff --git a/js/clientmain.js b/js/clientmain.js new file mode 100644 index 0000000..c0065d5 --- /dev/null +++ b/js/clientmain.js @@ -0,0 +1,30 @@ +import { DataModel } from "./core/DataModel.js"; +import * as THREE from "three"; +import { NetworkService } from "./instances/NetworkService.js"; +import { ReplicatorService } from "./instances/ReplicatorService.js"; +import { RenderService } from "./instances/RenderService.js"; +import { Workspace } from "./instances/Workspace.js"; +import { DebugTextService } from "./instances/DebugTextService.js"; // Client-only debug overlay manager + +const dm = new DataModel(); +dm.SetParent(null); // root + +// Create workspace +const ws = new Workspace(); +ws.SetParent(dm); + +const net = new NetworkService(); +net.SetParent(dm); +net.isServer = false; // client mode +await net.connect("ws://localhost:8080"); // connect to server + +const render = new RenderService(dm); +render.SetParent(dm); +render.start(); + +// Start replicator +const replication = new ReplicatorService(dm, net); // Automatically parents to dm + +// Create DebugTextService +const debugText = new DebugTextService(); +debugText.SetParent(dm); \ No newline at end of file diff --git a/js/core/BO3ScriptSignal.js b/js/core/BO3ScriptSignal.js new file mode 100644 index 0000000..1869150 --- /dev/null +++ b/js/core/BO3ScriptSignal.js @@ -0,0 +1,40 @@ +// A simple event system that mimics Roblox's RBXScriptSignal. + +export class BO3ScriptSignal { + constructor() { + this._connections = []; + } + + // Connect a function to the signal. + Connect(fn) { + const connection = { fn, connected: true }; + this._connections.push(connection); + return { + Disconnect: () => { + connection.connected = false; + this._connections = this._connections.filter(c => c !== connection); + }, + }; + } + + // Fire all connected callbacks with arguments. + Fire(...args) { + for (const c of this._connections) { + if (c.connected) { + try { + c.fn(...args); + } catch (err) { + console.error("[BO3ScriptSignal] Error in connected function:", err); + } + } + } + } + + // Disconnect all listeners. + DisconnectAll() { + this._connections = []; + } + get [Symbol.toStringTag]() { + return "BO3ScriptSignal"; + } +} diff --git a/js/core/DataModel.js b/js/core/DataModel.js new file mode 100644 index 0000000..9bf50a1 --- /dev/null +++ b/js/core/DataModel.js @@ -0,0 +1,15 @@ +import {Instance} from "../instances/Instance.js"; + +export class DataModel extends Instance { + constructor() { + super("DataModel"); + this.InstanceId = "DataModelRoot"; // Fixed ID for root + } + GetService(name) { + // Loop through children by class name + return this.Children.find(c => c.ClassName === name) || null; + } + GetDataModel() {return this;} // Override to return self +} // Just a container for the whole instance tree +// cuz this is an Entity Component System, we need a root entity +// Assume all children of DataModel are services or top-level game objects \ No newline at end of file diff --git a/js/core/util.js b/js/core/util.js new file mode 100644 index 0000000..0df4624 --- /dev/null +++ b/js/core/util.js @@ -0,0 +1,140 @@ +import * as THREE from "three"; + + +function guidGenerator() { + var S4 = function() { + return (((1+Math.random())*0x10000)|0).toString(16).substring(1); + }; + return (S4()+S4()+"-"+S4()+"-"+S4()+"-"+S4()+"-"+S4()+S4()+S4()); +} + +// Pure JS SHA-256 (works in browser or Node) +// Returns hex string +function sha256(message) { + // UTF-8 encode string to bytes + const msgBytes = new TextEncoder().encode(message); + + // Constants (first 32 bits of the fractional parts of the cube roots of the first 64 primes) + const K = new Uint32Array([ + 0x428a2f98,0x71374491,0xb5c0fbcf,0xe9b5dba5,0x3956c25b,0x59f111f1,0x923f82a4,0xab1c5ed5, + 0xd807aa98,0x12835b01,0x243185be,0x550c7dc3,0x72be5d74,0x80deb1fe,0x9bdc06a7,0xc19bf174, + 0xe49b69c1,0xefbe4786,0x0fc19dc6,0x240ca1cc,0x2de92c6f,0x4a7484aa,0x5cb0a9dc,0x76f988da, + 0x983e5152,0xa831c66d,0xb00327c8,0xbf597fc7,0xc6e00bf3,0xd5a79147,0x06ca6351,0x14292967, + 0x27b70a85,0x2e1b2138,0x4d2c6dfc,0x53380d13,0x650a7354,0x766a0abb,0x81c2c92e,0x92722c85, + 0xa2bfe8a1,0xa81a664b,0xc24b8b70,0xc76c51a3,0xd192e819,0xd6990624,0xf40e3585,0x106aa070, + 0x19a4c116,0x1e376c08,0x2748774c,0x34b0bcb5,0x391c0cb3,0x4ed8aa4a,0x5b9cca4f,0x682e6ff3, + 0x748f82ee,0x78a5636f,0x84c87814,0x8cc70208,0x90befffa,0xa4506ceb,0xbef9a3f7,0xc67178f2 + ]); + + // Initial hash values (first 32 bits of the fractional parts of the square roots of the first 8 primes) + let H = new Uint32Array([ + 0x6a09e667,0xbb67ae85,0x3c6ef372,0xa54ff53a, + 0x510e527f,0x9b05688c,0x1f83d9ab,0x5be0cd19 + ]); + + // Pre-processing: padding the message + const l = msgBytes.length * 8; + // Append 0x80 then zero bytes, then 64-bit big-endian length + const k = (512 - ((l + 8 + 64) % 512)) % 512; // number of zero bits + const totalBits = l + 1 + k + 64; + const totalBytes = totalBits / 8; + const padded = new Uint8Array(totalBytes); + + // copy message + padded.set(msgBytes); + + // append 1 bit (0x80) + padded[msgBytes.length] = 0x80; + + // append length in bits as 64-bit big-endian integer + const lenPos = totalBytes - 8; + for (let i = 0; i < 8; i++) { + padded[lenPos + 7 - i] = (l >>> (i * 8)) & 0xff; + } + + // Process the message in successive 512-bit chunks + const W = new Uint32Array(64); + + function ROTR(n, x) { return (x >>> n) | (x << (32 - n)); } + function Σ0(x) { return ROTR(2,x) ^ ROTR(13,x) ^ ROTR(22,x); } + function Σ1(x) { return ROTR(6,x) ^ ROTR(11,x) ^ ROTR(25,x); } + function σ0(x) { return ROTR(7,x) ^ ROTR(18,x) ^ (x >>> 3); } + function σ1(x) { return ROTR(17,x) ^ ROTR(19,x) ^ (x >>> 10); } + + for (let chunkStart = 0; chunkStart < padded.length; chunkStart += 64) { + // prepare message schedule W[0..63] + for (let i = 0; i < 16; i++) { + const j = chunkStart + i * 4; + W[i] = (padded[j] << 24) | (padded[j + 1] << 16) | (padded[j + 2] << 8) | (padded[j + 3]); + } + for (let i = 16; i < 64; i++) { + W[i] = (σ1(W[i - 2]) + W[i - 7] + σ0(W[i - 15]) + W[i - 16]) >>> 0; + } + + // initialize working variables + let a = H[0], b = H[1], c = H[2], d = H[3]; + let e = H[4], f = H[5], g = H[6], h = H[7]; + + for (let t = 0; t < 64; t++) { + const T1 = (h + Σ1(e) + ((e & f) ^ (~e & g)) + K[t] + W[t]) >>> 0; + const T2 = (Σ0(a) + ((a & b) ^ (a & c) ^ (b & c))) >>> 0; + h = g; + g = f; + f = e; + e = (d + T1) >>> 0; + d = c; + c = b; + b = a; + a = (T1 + T2) >>> 0; + } + + // compute intermediate hash value + H[0] = (H[0] + a) >>> 0; + H[1] = (H[1] + b) >>> 0; + H[2] = (H[2] + c) >>> 0; + H[3] = (H[3] + d) >>> 0; + H[4] = (H[4] + e) >>> 0; + H[5] = (H[5] + f) >>> 0; + H[6] = (H[6] + g) >>> 0; + H[7] = (H[7] + h) >>> 0; + } + + // produce the final hash (big-endian) + let hex = ""; + for (let i = 0; i < 8; i++) { + hex += ("00000000" + H[i].toString(16)).slice(-8); + } + return hex; +} + +function v3Encoder(vec3) { // THREE.Vector3 to Float32Array(3) + const arr = new Float32Array(3); + arr[0] = vec3.x; + arr[1] = vec3.y; + arr[2] = vec3.z; + return arr; +} + +function v3Decoder(arr) { // Float32Array(3) to THREE.Vector3 + return new THREE.Vector3(arr[0], arr[1], arr[2]); +} + +function eulerEncoder(euler) { // THREE.Euler to Float32Array(3) + const arr = new Float32Array(3); + arr[0] = euler.x; + arr[1] = euler.y; + arr[2] = euler.z; + return arr; +} + +function eulerDecoder(arr) { // Float32Array(3) to THREE.Euler + return new THREE.Euler(arr[0], arr[1], arr[2]); +} + +const encoders = { + Vector3: { encode: v3Encoder, decode: v3Decoder }, + Euler: { encode: eulerEncoder, decode: eulerDecoder }, + // Add more as needed +}; + +export { guidGenerator, sha256, encoders }; \ No newline at end of file diff --git a/js/instances/BasePart.js b/js/instances/BasePart.js new file mode 100644 index 0000000..d9d9b1c --- /dev/null +++ b/js/instances/BasePart.js @@ -0,0 +1,152 @@ +import * as THREE from "three"; +import * as CANNON from "cannon-es"; +import { Instance } from "./Instance.js"; + +export class BasePart extends Instance { + constructor() { + super("BasePart"); + + // === Visual properties === + this.Color = 0x00ff00; + this.Size = new THREE.Vector3(1, 1, 1); + this.Position = new THREE.Vector3(0, 0, 0); + this.Orientation = new THREE.Euler(0, 0, 0); + this.Anchored = false; // Roblox-style toggle + + // === Three.js setup === + const geometry = new THREE.BoxGeometry(this.Size.x, this.Size.y, this.Size.z); + const material = new THREE.MeshStandardMaterial({ color: this.Color }); + this.mesh = new THREE.Mesh(geometry, material); + this.mesh.castShadow = true; + this.mesh.receiveShadow = true; + this.mesh.userData.basePart = this; + + // === Cannon.js setup === + const shape = new CANNON.Box( + new CANNON.Vec3(this.Size.x / 2, this.Size.y / 2, this.Size.z / 2) + ); + this.body = new CANNON.Body({ + mass: 1, + position: new CANNON.Vec3(this.Position.x, this.Position.y, this.Position.z), + shape: shape, + }); + this.body.userData = { basePart: this }; + + this._originalMass = this.body.mass; // store default mass + this._renderService = null; // assigned when added to scene + } + + // === Scene lifecycle === + OnAddedToScene(parent) { + const dm = this.GetDataModel(); + const renderService = dm.GetService("RenderService"); + this._renderService = renderService; + + if (renderService.scene) renderService.scene.add(this.mesh); + if (!renderService.physicsWorld) renderService._initPhysicsWorld(); + + renderService.physicsWorld.addBody(this.body); + this._applyAnchoredState(); + } + + // === Anchoring logic === + setAnchored(value) { + if (this.Anchored === value) return; + this.Anchored = value; + this._applyAnchoredState(); + } + + _applyAnchoredState() { + if (!this.body) return; + if (this.Anchored) { + this.body.type = CANNON.Body.STATIC; + this.body.mass = 0; + this.body.velocity.set(0, 0, 0); + this.body.angularVelocity.set(0, 0, 0); + } else { + this.body.type = CANNON.Body.DYNAMIC; + this.body.mass = this._originalMass || 1; + } + this.body.updateMassProperties(); + } + + // === Updates === + updateVisual() { + // Only update mesh from physics if not anchored + if (!this.Anchored) { + this.mesh.position.copy(this.body.position); + this.mesh.quaternion.copy(this.body.quaternion); + } + } + + updateBodyPosition() { + this.body.position.set(this.Position.x, this.Position.y, this.Position.z); + this.body.quaternion.setFromEuler( + this.Orientation.x, + this.Orientation.y, + this.Orientation.z + ); + this.body.velocity.set(0, 0, 0); + this.body.angularVelocity.set(0, 0, 0); + } + + updateSizes() { + // Update Three geometry + this.mesh.geometry.dispose(); + this.mesh.geometry = new THREE.BoxGeometry(this.Size.x, this.Size.y, this.Size.z); + + // Update Cannon shape + this.body.shapes = []; + const newShape = new CANNON.Box( + new CANNON.Vec3(this.Size.x / 2, this.Size.y / 2, this.Size.z / 2) + ); + this.body.addShape(newShape); + this.body.updateMassProperties(); + } + + OnReplicated() { + if (this.IsReplicated) { + this.body.type = CANNON.Body.KINEMATIC; + this.body.velocity.set(0, 0, 0); + this.body.angularVelocity.set(0, 0, 0); + } + + this.body.position.copy(this.Position); + this.body.quaternion.setFromEuler( + this.Orientation.x, + this.Orientation.y, + this.Orientation.z + ); + + this.updateVisual(); + } + + OnPhysicsTick() { + + if (this.body.position.y < -1000) { + // Fell out of world, destroy like in Roblox + this.Destroy(); + console.debug("BasePart fell out of world and was destroyed."); + return; + } + + if (this.IsReplicated || this.Anchored) return; + + this.Position.set( + this.body.position.x, + this.body.position.y, + this.body.position.z + ); + this.Orientation.setFromQuaternion(this.body.quaternion); + } + + // === Cleanup === + Destroy() { + if (this._renderService) { + if (this.mesh.parent) this.mesh.parent.remove(this.mesh); + if (this._renderService.physicsWorld) + this._renderService.physicsWorld.removeBody(this.body); + } + super.Destroy(); + } +} diff --git a/js/instances/DebugTextService.js b/js/instances/DebugTextService.js new file mode 100644 index 0000000..8578f13 --- /dev/null +++ b/js/instances/DebugTextService.js @@ -0,0 +1,105 @@ +import { Instance } from "./Instance.js"; +import { BO3ScriptSignal } from "../core/BO3ScriptSignal.js"; + +export class DebugTextService extends Instance { + constructor() { + super("DebugTextService"); + + this.isClient = true; // Mark client-only + this._renderService = null; + this._networkService = null; + this._lastBytes = 0; + this._bps = 0; + this._lastMeasure = performance.now(); + + // Tracks text lines + this._lines = []; + + // Hook into render loop later + } + + OnAddedToScene(parent) { + const dm = this.GetDataModel(); + + // Get references to RenderService and NetworkService + this._renderService = dm.GetService("RenderService"); + this._networkService = dm.GetService("NetworkService"); + + if (!this._renderService) { + console.warn("[DebugTextService] No RenderService found!"); + return; + } + + // Initialize debug text array + this._renderService.debugText = []; + + // Hook into RenderStepped + this._renderService.RenderStepped.Connect((dt) => this._onRenderStep(dt)); + } + + // Called each RenderStepped frame (~60fps) + _onRenderStep(dt) { + this._lines.length = 0; // clear + + // === Network stats === + if (this._networkService) { + const now = performance.now(); + const bytesNow = this._networkService.totalBytes || 0; + const deltaBytes = bytesNow - this._lastBytes; + const deltaTime = (now - this._lastMeasure) / 1000; + + if (deltaTime > 0.25) { // smooth every 250ms + this._bps = deltaBytes / deltaTime; + this._lastBytes = bytesNow; + this._lastMeasure = now; + } + + this._lines.push( + `Net: ${(this._bps / 1024).toFixed(2)} KB/s` + ); + } + + // === Instance tree summary === + const dm = this.GetDataModel(); + if (dm && dm.Children) { + const totalInstances = this._countInstances(dm); + this._lines.push(`Instances: ${totalInstances}`); + } + + // === Misc stats === + if (this._renderService.physicsWorld) { + const bodies = this._renderService.physicsWorld.bodies.length; + this._lines.push(`Physics Bodies: ${bodies}`); + } + + // === Output === + this._renderService.debugText.length = 0; + this._renderService.debugText.push(...this._lines); + + // Generate instance tree (for future use) + const instanceTree = this._generateTree(dm); + // Push this to text overlay if needed + this._renderService.debugText.push(...instanceTree); + } + + _countInstances(instance) { + let count = 1; + if (!instance.Children) return count; + for (const child of instance.Children) { + count += this._countInstances(child); + } + return count; + } + _generateTree(instance, depth = 0) { + let lines = []; + const indent = " ".repeat(depth); + lines.push(`${indent}- ${instance.ClassName} (${instance.Name}) [${instance.InstanceId}]`); + if (instance.Children) { + for (const child of instance.Children) { + lines.push(...this._generateTree(child, depth + 1)); + } + } + return lines; + } +} +// Displays debug text overlay on screen with network and instance stats \ No newline at end of file diff --git a/js/instances/ExampleService.js b/js/instances/ExampleService.js new file mode 100644 index 0000000..210c9e6 --- /dev/null +++ b/js/instances/ExampleService.js @@ -0,0 +1,6 @@ +import { Instance } from "./Instance.js"; +export class ExampleService extends Instance { + constructor() { + super("ExampleService"); + } +} // Basic example of a service in BO3 \ No newline at end of file diff --git a/js/instances/Instance.js b/js/instances/Instance.js new file mode 100644 index 0000000..41286d9 --- /dev/null +++ b/js/instances/Instance.js @@ -0,0 +1,123 @@ +import { BO3ScriptSignal } from "../core/BO3ScriptSignal.js"; +import { guidGenerator } from "../core/util.js"; +let instanceCounter = 0; + +export class Instance { + constructor(className = "Instance") { + this.ClassName = className; + this.Name = `${className}${instanceCounter++}`; + this.Parent = null; + this.Children = []; + this.InstanceId = guidGenerator(); // Unique identifier, used for replication + + // Signals + this.AncestryChanged = new BO3ScriptSignal(); + this.Changed = new BO3ScriptSignal(); + } + + // Set a property dynamically + SetProperty(prop, value) { + this[prop] = value; + this.Changed.Fire(prop, value); + } + + GetDataModel() { // Recursively calls itself on parent until DataModel is found, since DataModel overrides this method to return itself + if (this.Parent) return this.Parent.GetDataModel(); + } + + FindInstanceRecursively(instanceId) { + if (this.InstanceId === instanceId) return this; + if (this.Children) { + for (const child of this.Children) { + if (typeof child.FindInstanceRecursively === 'function') { + const found = child.FindInstanceRecursively(instanceId); + if (found) return found; + } + } + } + return null; + } + + OnReplicated() { + // Hook for when instance is replicated to client. Override in subclasses. + } + + // Parent management + SetParent(newParent) { + if (this.Parent === newParent) return; + + // Remove from old parent + if (this.Parent) { + const oldChildren = this.Parent.Children; + this.Parent.Children = oldChildren.filter(c => c !== this); + } + + const oldParent = this.Parent; + this.Parent = newParent; + + // Add to new parent + if (newParent) { + newParent.Children.push(this); + } + + try { + this.AncestryChanged.Fire(this, oldParent); + } catch { + console.warn("AncestryChanged signal error! May be caused by ReplicatorService during replication."); + } + if (oldParent === null && newParent !== null && this.OnAddedToScene) { + // If added to scene (i.e. RenderService), call hook + setTimeout(() => { + this.OnAddedToScene(newParent); + }, 0); // Defer to avoid issues during replication + } + } + + // Utility: find first child with name + FindFirstChild(name) { + return this.Children.find(c => c.Name === name) || null; + } + + // Utility: create deep clone + Clone() { + const clone = new Instance(this.ClassName); + for (const key of Object.keys(this)) { + if (key !== "Parent" && key !== "Children") { + clone[key] = structuredClone(this[key]); + } + } + for (const child of this.Children) { + const childClone = child.Clone(); + childClone.SetParent(clone); + } + return clone; + } + + Destroy() { + // Remove from parent + const dm = this.GetDataModel(); // Get DataModel for replication service, BEFORE removing parent. otherwise can't find it. + this.SetParent(null); + // Recursively destroy children + for (const child of [...this.Children]) { + if (typeof child.Destroy === 'function') { + child.Destroy(); + } else { + child.SetParent(null); + } + } + // Clear signals + this.AncestryChanged.DisconnectAll(); + this.Changed.DisconnectAll(); + // Request replicator to remove this instance if needed + if (dm) { + const replicator = dm.GetService("ReplicatorService"); + if (replicator) { + replicator.requestReplicateDestroy(this); + } else { + console.warn("Destroy: No ReplicatorService found in DataModel."); + } + } else { + console.warn("Destroy: No DataModel found in ancestry. Are you destroying an unparented instance?"); + } + } // Garbage collected after this +} diff --git a/js/instances/LerpedBasePart.js b/js/instances/LerpedBasePart.js new file mode 100644 index 0000000..7ac5a6c --- /dev/null +++ b/js/instances/LerpedBasePart.js @@ -0,0 +1,63 @@ +import * as THREE from "three"; +import { BasePart } from "./BasePart.js"; + +export class LerpedBasePart extends BasePart { // Extends BasePart to add smooth interpolation for networked parts + constructor() { + super(); + this.ClassName = "LerpedBasePart"; // 👈 Roblox-style ClassName + this._lastReplicatedState = null; + this._nextReplicatedState = null; + } + + // Called when replication data arrives from the server + OnReplicated() { + // Keep old state to interpolate from + this._lastReplicatedState = this._nextReplicatedState; + + // Store new replication state + this._nextReplicatedState = { + pos: this.Position.clone(), + quat: new THREE.Quaternion().setFromEuler(this.Orientation), + time: performance.now(), + }; + + // Apply anchored state from server + this._applyAnchoredState?.(); + this.updateSizes(); // In case Size changed + } + + // Smooth visual updates + updateVisual(dt) { + // Don’t lerp anchored parts — they don’t move + if (this.Anchored) { + this.mesh.position.copy(this.Position); + this.mesh.quaternion.setFromEuler(this.Orientation); + return; + } + + if (this._nextReplicatedState && this._lastReplicatedState) { + // Interpolate between last and next states + const now = performance.now(); + const t = Math.min( + (now - this._lastReplicatedState.time) / 100, // ~10Hz replication → 100ms + 1 + ); + + this.mesh.position.lerpVectors( + this._lastReplicatedState.pos, + this._nextReplicatedState.pos, + t + ); + this.mesh.quaternion.slerpQuaternions( + this._lastReplicatedState.quat, + this._nextReplicatedState.quat, + t + ); + } else { + // Fallback to default behavior + this.mesh.position.copy(this.body.position); + this.mesh.quaternion.copy(this.body.quaternion); + console.warn("LerpedBasePart: No replication data yet, using physics state."); + } + } +} diff --git a/js/instances/NetworkService.js b/js/instances/NetworkService.js new file mode 100644 index 0000000..ed6d78a --- /dev/null +++ b/js/instances/NetworkService.js @@ -0,0 +1,200 @@ +let WebSocketImpl; +let WebSocketServer; +if (typeof window !== "undefined") { + WebSocketImpl = WebSocket; +} else { + // Dynamically require 'ws' only on server side + const wsModule = await import("ws"); + WebSocketImpl = wsModule.default; + WebSocketServer = wsModule.WebSocketServer; +} +import { Instance } from "./Instance.js"; +import { BO3ScriptSignal } from "../core/BO3ScriptSignal.js"; + +/** + * NetworkService + * - Handles both client & server networking + * - Binary framed packets: [uint32 length][ptype][psub][payload] + * - Fires PacketReceived signal for incoming packets + */ +export class NetworkService extends Instance { + constructor() { + super("NetworkService"); + + /** @type {boolean} */ + this.isServer = false; + + /** @type {WebSocketServer|null} */ + this.wss = null; + + /** @type {WebSocket|null} */ + this.client = null; + + /** @type {Map} */ + this.clients = new Map(); + + /** @type {Map} */ + this.handlers = new Map(); + + /** @type {BO3ScriptSignal} */ + this.PacketReceived = new BO3ScriptSignal(); + + // Total bytes received (for stats) + this.totalBytes = 0; + // Interval to clear totalBytes every second + setInterval(() => { + this.totalBytes = 0; + }, 1000); + } + + // ========================== + // === Utility Functions === + // ========================== + static pktKey(ptype, psub) { + return (ptype << 8) | psub; + } + + registerHandler(ptype, psub, fn) { + const key = NetworkService.pktKey(ptype, psub); + this.handlers.set(key, fn); + } + + unregisterHandler(ptype, psub) { + const key = NetworkService.pktKey(ptype, psub); + this.handlers.delete(key); + } + + invokeHandler(ptype, psub, payload, client = null) { + const key = NetworkService.pktKey(ptype, psub); + const handler = this.handlers.get(key); + if (handler) handler(payload, client); + this.PacketReceived.Fire({ ptype, psub, payload, client }); + } + + // ====================== + // === Packet Encode === + // ====================== + encodePacket(ptype, psub, payload = new Uint8Array()) { + if (!(payload instanceof Uint8Array)) { + if (typeof payload === "string") { + payload = new TextEncoder().encode(payload); + } else if (payload instanceof ArrayBuffer) { + payload = new Uint8Array(payload); + } else { + throw new Error("Payload must be Uint8Array, ArrayBuffer, or string"); + } + } + const body = new Uint8Array(2 + payload.length); + body[0] = ptype; + body[1] = psub; + body.set(payload, 2); + + const header = new ArrayBuffer(4); + new DataView(header).setUint32(0, body.length); + return new Uint8Array([...new Uint8Array(header), ...body]); + } + + decodePacket(buffer) { + const view = new DataView(buffer); + const length = view.getUint32(0); + const body = new Uint8Array(buffer, 4, length); + const ptype = body[0]; + const psub = body[1]; + const payload = body.slice(2); + return { ptype, psub, payload }; + } + + // ============================ + // === Client Functionality === + // ============================ + async connect(url) { + this.isServer = false; + return new Promise((resolve, reject) => { + const ws = new WebSocket(url); + this.client = ws; + + ws.binaryType = "arraybuffer"; + + ws.onopen = () => { + console.log(`[NetworkService] Connected to ${url}`); + resolve(); + }; + + ws.onmessage = (event) => { + const buf = event.data instanceof ArrayBuffer ? event.data : event.data.buffer; + this.totalBytes += Math.max(buf.byteLength,0); // safeguard + const { ptype, psub, payload } = this.decodePacket(buf); + this.invokeHandler(ptype, psub, payload); + }; + + ws.onerror = (err) => reject(err); + ws.onclose = () => { + console.warn("[NetworkService] Disconnected from server.") + alert("Disconnected from server! If this was unintentional, please refresh the page. You might have been kicked by the server.\n\n(If you are the server host, check if the servers running.)"); + }; + }); + } + + send(ptype, psub, payload) { + if (!this.client || this.client.readyState !== WebSocket.OPEN) return; + const packet = this.encodePacket(ptype, psub, payload); + this.client.send(packet); + } + + // ============================ + // === Server Functionality === + // ============================ + listen(port = 3000, host = "0.0.0.0") { + this.isServer = true; + this.wss = new WebSocketServer({ port, host }); + console.log(`[NetworkService] Listening on ws://${host}:${port}`); + + this.wss.on("connection", (ws) => { + this.clients.set(ws, {}); + console.log("[NetworkService] New client connected. Total clients:", this.clients.size); + ws.binaryType = "arraybuffer"; + + ws.on("message", (msg) => { + const buf = msg instanceof ArrayBuffer ? msg : msg.buffer; + const { ptype, psub, payload } = this.decodePacket(buf); + this.invokeHandler(ptype, psub, payload, ws); + }); + + ws.on("close", () => { + this.clients.delete(ws) + console.log("[NetworkService] Client disconnected. Total clients:", this.clients.size); + }); + }); + } + + broadcast(ptype, psub, payload) { + if (!this.isServer || !this.wss) return; + const packet = this.encodePacket(ptype, psub, payload); + for (const ws of this.clients.keys()) { + if (ws.readyState === WebSocket.OPEN) ws.send(packet); + } + } + + sendToClient(ws, ptype, psub, payload) { + if (!this.isServer) return; + const packet = this.encodePacket(ptype, psub, payload); + if (ws.readyState === WebSocket.OPEN) ws.send(packet); + } + + Broadcast(ptype, psub, payload) { + console.warn("[NetworkService] Broadcast is deprecated, use broadcast() instead."); + this.broadcast(ptype, psub, payload); + } + + // ============================ + // === Shutdown === + // ============================ + close() { + if (this.isServer && this.wss) { + for (const ws of this.clients.keys()) ws.close(); + this.wss.close(); + } else if (this.client) { + this.client.close(); + } + } +} diff --git a/js/instances/RenderService.js b/js/instances/RenderService.js new file mode 100644 index 0000000..a20a108 --- /dev/null +++ b/js/instances/RenderService.js @@ -0,0 +1,185 @@ +import * as THREE from "three"; +import { OrbitControls } from "three/addons/controls/OrbitControls.js"; +import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js"; +import { Instance } from "./Instance.js"; +import { BO3ScriptSignal } from "../core/BO3ScriptSignal.js"; +import * as CANNON from "cannon-es"; +export class RenderService extends Instance { + constructor() { + super("RenderService"); + + + // Add canvas to DOM + if (typeof document !== "undefined") { + // Three.js setup + this.renderer = new THREE.WebGLRenderer({ antialias: true }); + this.scene = new THREE.Scene(); + this.camera = new THREE.PerspectiveCamera( + 75, + window.innerWidth / window.innerHeight, + 0.1, + 1000 + ); + this.renderer.setSize(window.innerWidth, window.innerHeight); + document.body.appendChild(this.renderer.domElement); + // Controls and loader + this.controls = new OrbitControls(this.camera, this.renderer.domElement); + this.loader = new GLTFLoader(); + } + + + + // Handle window resize + if (typeof window !== "undefined") { + window.addEventListener("resize", () => this.updateWindowSize()); + // Basic lighting + const light = new THREE.DirectionalLight(0xffffff, 1); + light.position.set(5, 5, 5); + this.scene.add(light); + + // Default test cube (you can remove this later) + /*const geometry = new THREE.BoxGeometry(1, 1, 1); + const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 }); + const cube = new THREE.Mesh(geometry, material); + this.scene.add(cube);*/ + // I commented out the cube because it was annoying me + // just like the blender default cube + + // Camera position + this.camera.position.z = 5; + } + + // Rendering state + this._running = false; + this._lastFrame = 0; + this.isServer = false; // If this is true it will ignore rendering and only run physics. Useful for dedicated servers. + // Event signals + this.RenderStepped = new BO3ScriptSignal(); + this.Heartbeat = new BO3ScriptSignal(); + this.Stepped = new BO3ScriptSignal(); + // Physics world + this.physicsWorld = null; + this._initPhysicsWorld(); + + // Debug text + this.debugText = ["Debug text!"]; // Line array + // Debug overlay + if (typeof document !== "undefined") { + this.overlay = document.createElement("canvas"); + this.overlay.style.position = "absolute"; + this.overlay.style.top = "0"; + this.overlay.style.left = "0"; + this.overlay.style.pointerEvents = "none"; + this.overlay.width = window.innerWidth; + this.overlay.height = window.innerHeight; + document.body.appendChild(this.overlay); + this.debugCtx = this.overlay.getContext("2d"); + } + + } + + // === Instance Overrides === + Destroy() { + if (typeof window !== "undefined") { + window.removeEventListener("resize", this.updateWindowSize); + } + if (this.renderer.domElement?.parentNode) { + this.renderer.domElement.parentNode.removeChild(this.renderer.domElement); + } + } + + // === Rendering === + updateWindowSize() { + this.camera.aspect = window.innerWidth / window.innerHeight; + this.camera.updateProjectionMatrix(); + this.renderer.setSize(window.innerWidth, window.innerHeight); + if (this.overlay) { + this.overlay.width = window.innerWidth; + this.overlay.height = window.innerHeight; + } + } + + + // === Main loop === + start() { + if (this._running) return; + this._running = true; + const loop = (time) => { + if (!this._running) return; + const dt = (time - this._lastFrame) / 1000; + this._lastFrame = time; + this.renderFrame(dt); + if (typeof window !== "undefined") requestAnimationFrame(loop); + }; + if (typeof window !== "undefined") { + requestAnimationFrame(loop); + } else { + // Server-side loop at ~60Hz + setInterval(() => { + const now = Date.now(); + const dt = (now - this._lastFrame) / 1000; + this._lastFrame = now; + this.renderFrame(dt); + }, 1000 / 60); + } + } + + stop() { + this._running = false; + } + _initPhysicsWorld() { + this.physicsWorld = new CANNON.World(); + this.physicsWorld.gravity.set(0, -9.81, 0); + } + + renderFrame(dt) { + // Emit Stepped signals + this.Stepped.Fire(dt); + if (this.physicsWorld) { + this.physicsWorld.step(1 / 60, dt, 3); + } + + if (!this.isServer) { + // update all BaseParts’ visuals + this.scene.traverse(obj => { + const base = obj.userData?.basePart; + if (base) { + base.updateVisual(); + //base.clientUpdate(dt); + //console.debug("Updated visual for BasePart:", base.Name); + } else if (obj.isMesh && !obj.userData?.checked) { + obj.userData.checked = true; // mark it so we don’t warn every frame + console.debug("[RenderService] Non-BasePart mesh (ignored):", obj.name || obj); + } + }); + + this.RenderStepped.Fire(dt); + // Render debug text + if (this.debugCtx && this.debugText.length > 0) { + const ctx = this.debugCtx; + ctx.clearRect(0, 0, this.overlay.width, this.overlay.height); + ctx.save(); + ctx.font = "14px Arial"; + ctx.fillStyle = "white"; + ctx.textAlign = "left"; + ctx.textBaseline = "top"; + this.debugText.forEach((line, i) => { + ctx.fillText(line, 10, 10 + i * 16); + }); + ctx.restore(); + } + + this.controls.update(); + this.renderer.render(this.scene, this.camera); + } else { + // Update position variables for all BaseParts from physicsWorld, scene doesnt exist + this.physicsWorld.bodies.forEach(body => { + const base = body.userData?.basePart; + if (base) { + base.OnPhysicsTick(); + } + }); + } + this.Heartbeat.Fire(dt); + } // Just like roblox, Heartbeat is always fired, RenderStepped only on clients, Stepped always +} diff --git a/js/instances/ReplicatorService.js b/js/instances/ReplicatorService.js new file mode 100644 index 0000000..d770b4f --- /dev/null +++ b/js/instances/ReplicatorService.js @@ -0,0 +1,232 @@ +import { Instance } from "./Instance.js"; +import { sha256, encoders } from "../core/util.js"; // Since SubtleCrypto isnt available in http only, we use a JS implementation +export class ReplicatorService extends Instance { + constructor(datamodel, network) { + super("ReplicatorService"); + // Get tickloop from RenderService + this._renderService = datamodel.GetService("RenderService"); + if (!this._renderService) { + throw new Error("ReplicatorService requires RenderService in DataModel"); + } + this._networkService = network; + if (!this._networkService) { + throw new Error("ReplicatorService requires NetworkService in DataModel"); + } + this.bytesSent = 0; // For client stats + this.bytesReceived = 0; // For client stats + this.frameCounter = 0; // To prevent spamming the replication, run it every 10 frames + if (!this._networkService.isServer) { + this._renderService.Heartbeat.Connect((dt) => this._onHeartbeatCli(dt)); + this._networkService.registerHandler(0x00, 0x02, (payload) => { + this.bytesReceived += payload.byteLength + 2; // +2 for ptype and psub + const data = JSON.parse(new TextDecoder().decode(payload)); + // Find instance by InstanceId using FindInstanceRecursively on datamodel + const inst = this.Parent.FindInstanceRecursively(data.InstanceId); + if (!inst) { + // Instance not found, create a new one + import(`./${data.ClassName}.js`).then((module) => { + const NewClass = module[data.ClassName]; + if (!NewClass) { + console.warn(`ReplicatorService: Class ${data.ClassName} not found for replication.`); + return; + } + const newInst = new NewClass(); + newInst.InstanceId = data.InstanceId; // Set the same InstanceId + newInst.Name = data.Name; + // Set properties + for (const [key, value] of Object.entries(data.Properties)) { + if (value && typeof value === 'object' && value._type && encoders[value._type]) { + newInst[key] = encoders[value._type].decode(value.value); + } else { + newInst[key] = value; + } + } + newInst.IsReplicated = true; // Mark as replicated to disable physics etc if needed + console.debug('ReplicatorService: Received new instance!'); + // Set parent + if (data.ParentId) { + // Find parent by InstanceId using FindInstanceRecursively + const parentInst = this.GetDataModel().FindInstanceRecursively(data.ParentId); + if (parentInst) { + newInst.SetParent(parentInst); + } else { + console.warn(`ReplicatorService: Parent with InstanceId ${data.ParentId} not found. Double check server code.`); + } + newInst.OnReplicated(); // Call hook for when instance is replicated + } else { + console.warn(`ReplicatorService: No ParentId provided for instance ${data.InstanceId}! Double check server code.`); + } + console.log(`ReplicatorService: Created new instance ${newInst.Name} (${newInst.InstanceId}) from server.`); + }).catch(err => { + console.error(`ReplicatorService: Failed to load class ${data.ClassName} for replication.`, err); + }); + } else { + // Instance found, update properties + for (const [key, value] of Object.entries(data.Properties)) { + if (value && typeof value === 'object' && value._type && encoders[value._type]) { + inst[key] = encoders[value._type].decode(value.value); + } else { + inst[key] = value; + } + } + // Handle destruction if ParentId is not provided (instance destroyed) + if (!data.ParentId) { + // Remove instance from parent/children and data model + if (typeof inst.Destroy === 'function') { + inst.Destroy(); + } + console.log(`ReplicatorService: Instance ${inst.Name} (${inst.InstanceId}) destroyed (ParentId is nil).`); + return; + } + // Update parent if changed + if (data.ParentId) { + if (!inst.Parent || inst.Parent.InstanceId !== data.ParentId) { + // Find new parent by InstanceId using FindInstanceRecursively + const newParentInst = this.GetDataModel().FindInstanceRecursively(data.ParentId); + if (newParentInst) { + inst.SetParent(newParentInst); + } else { + console.warn(`ReplicatorService: Parent with InstanceId ${data.ParentId} not found for existing instance. Double check server code.`); + } + } + } else { + console.warn(`ReplicatorService: No ParentId provided for existing instance ${data.InstanceId}! Double check server code.`); + } + inst.OnReplicated(); // Call hook for when instance is replicated + //console.log(`ReplicatorService: Updated instance ${inst.Name} (${inst.InstanceId}) from server.`); + } + }); + } else { + this._renderService.Heartbeat.Connect((dt) => this._onHeartbeat(dt)); + } + this.SetParent(datamodel); + this.lastClearedStatCheck = 0; // If above 1 second, clear stats + console.log("ReplicatorService initialized."); + } + // Client-side heartbeat, receive updates from server + _onHeartbeatCli(delta) { + this.lastClearedStatCheck += delta; + if (this.lastClearedStatCheck >= 1) { + console.log(`Client stats: ${this.bytesSent} bytes sent, ${this.bytesReceived} bytes received in the last second.`); + this.bytesSent = 0; + this.bytesReceived = 0; + this.lastClearedStatCheck = 0; + } + } + _onHeartbeat(delta) { + // Clear stats every second + this.lastClearedStatCheck += delta; + if (this.lastClearedStatCheck >= 1) { console.log(`Server stats: ${this.bytesSent} bytes sent, ${this.bytesReceived} bytes received in the last second.`); + this.bytesSent = 0; + this.bytesReceived = 0; + this.lastClearedStatCheck = 0; + + } + this.frameCounter += delta; + if (this.frameCounter < (1/30)) { // Run replication at ~30fps max + return; + } else { + this.frameCounter = 0; + }; + // For each instance in valid services, replicate its state to clients. Keep old state hash to avoid spamming unchanged data. + const toReplicate = []; + // Get workspace and its children + const workspace = this.Parent.GetService("Workspace"); + if (workspace) { + toReplicate.push(...workspace.Children); + } + // Send over the pipe + for (const inst of toReplicate) { + // Check property hashes to see if anything changed + let hashState = ""; + for (const key of Object.keys(inst)) { + if (key === "Parent" || key === "Children") continue; + + let val; + try { + val = JSON.stringify(inst[key]); + } catch { + val = "[unserializable]"; + } + + hashState += `${key}:${val};`; + } + + const hash = sha256(hashState); + if (inst._lastHash !== hash) { + // Something changed, replicate + inst._lastHash = hash; + const payload = { + InstanceId: inst.InstanceId, + ClassName: inst.ClassName, + Name: inst.Name, + Properties: {}, + ParentId: inst.Parent ? inst.Parent.InstanceId : null, + }; + for (const key of Object.keys(inst)) { + // Skip internals and heavy engine data + if ( + key.startsWith("_") || // private/internal fields + key === "Parent" || + key === "Children" || + key === "mesh" || + key === "body" || + key === "renderer" || + key === "scene" || + key === "controls" || + key === "physicsWorld" || + key === "loader" + ) { + continue; + } + + // Get type + const val = inst[key]; + const type = val && val.constructor ? val.constructor.name : typeof val; + if (type === "function" || type === "undefined" || type === "symbol" || type === "BO3ScriptSignal") { + continue; // skip non-serializable types + } + // If we have an encoder, encode and store type info + if (encoders[type]) { + payload.Properties[key] = { _type: type, value: encoders[type].encode(val) }; + } else { + payload.Properties[key] = val; + } + + } + //console.debug(JSON.stringify(payload)); + + // Send to all clients + let outpay = JSON.stringify(payload); + this.bytesSent += outpay.length + 2; // +2 for ptype and psub + this._networkService.broadcast(0x00, 0x02, outpay); // Refer to ptype and psub definitions to see what these mean + // Update the _lastHash property to avoid re-sending unchanged data + inst._lastHash = hash; + //console.log(`Replicated instance ${inst.Name} (${inst.InstanceId}) to clients.`); + } + } + } + + requestReplicateDestroy(instance,ignoreIS) { // Server side function to request destruction of an instance on clients + if (!this._networkService.isServer) { + if (!ignoreIS) console.warn("ReplicatorService: requestReplicateDestroy can only be called on server."); + // ignoreIS is for internal server calls to avoid spamming the warning + return; + } + if (!instance || !instance.InstanceId) { + console.warn("ReplicatorService: requestReplicateDestroy requires a valid instance with InstanceId."); + return; + } + const payload = { + InstanceId: instance.InstanceId, + ClassName: instance.ClassName, + Name: instance.Name, + Properties: {}, // No properties needed for destruction + ParentId: null, // Null ParentId indicates destruction + }; + let outpay = JSON.stringify(payload); + this.bytesSent += outpay.length + 2; // +2 for ptype and psub + this._networkService.broadcast(0x00, 0x02, outpay); // Refer to ptype and psub definitions to see what these mean + console.log(`ReplicatorService: Requested destruction of instance ${instance.Name} (${instance.InstanceId}) on clients.`); + } +} \ No newline at end of file diff --git a/js/instances/Workspace.js b/js/instances/Workspace.js new file mode 100644 index 0000000..c3d81f0 --- /dev/null +++ b/js/instances/Workspace.js @@ -0,0 +1,8 @@ +import { Instance } from "./Instance.js"; +export class Workspace extends Instance { + constructor() { + super("Workspace"); + this.InstanceId = "WorkspaceRoot"; // Fixed ID for Workspace (Bad idea to replicate services over network?) + // Most likely: yes, but we can filter them out in ReplicatorService + } +} // Workspace is the root container for all 3D objects in the scene \ No newline at end of file diff --git a/js/servermain.js b/js/servermain.js new file mode 100644 index 0000000..295f14f --- /dev/null +++ b/js/servermain.js @@ -0,0 +1,72 @@ +import { NetworkService } from "./instances/NetworkService.js"; +import { RenderService } from "./instances/RenderService.js"; +import { ReplicatorService } from "./instances/ReplicatorService.js"; +import { DataModel } from "./core/DataModel.js"; +import { Workspace } from "./instances/Workspace.js"; +import * as THREE from "three"; + +const dm = new DataModel(); +dm.SetParent(null); + +// === Workspace === +const ws = new Workspace(); +ws.SetParent(dm); + +// === RenderService (headless physics) === +const render = new RenderService(dm); +render.isServer = true; +render.start(); +render.SetParent(dm); + +// === Networking === +const net = new NetworkService(); +net.listen(8080); +net.SetParent(dm); + +// Simple echo handler +net.registerHandler(0x01, 0x00, (payload, client) => { + console.log("Received from client:", new TextDecoder().decode(payload)); + net.sendToClient(client, 0x01, 0x01, payload); +}); + +// === Replication Service === +const replication = new ReplicatorService(dm, net); + +// === Test parts === +const { LerpedBasePart } = await import("./instances/LerpedBasePart.js"); + +// Falling cube +// Anchored ground +const ground = new LerpedBasePart(); +ground.Size = new THREE.Vector3(10, 1, 10); +ground.Position.set(0, -0.5, 0); +ground.setAnchored(true); +ground.SetParent(ws); +ground.updateSizes(); +// Spin ground +let angle = 0; +setInterval(() => { + angle += 0.01; + ground.Orientation.set(0, angle, 0); + ground.Position.set(5 * Math.sin(angle * 2), -0.5, 5 * Math.cos(angle * 2)); +}, 16); +/*setInterval(() => { + const part = new LerpedBasePart(); + part.Position.set(0, 35, 0); + part.SetParent(ws); + part.setAnchored(true); + part.updateVisual(1 / 60); // Ensure physics is initialized + setTimeout(() => { + part.updateBodyPosition(); // Reset physics to new position + part.setAnchored(false); + }, 2000); + part.updateSizes(); +}, 1000);*/ +// Falling cube singularity +const part = new LerpedBasePart(); +part.Size = new THREE.Vector3(1, 1, 1); +part.Position.set(10, 5, 0); +part.setAnchored(false); +part.SetParent(ws); +part.updateSizes(); + diff --git a/jsconfig.json b/jsconfig.json new file mode 100644 index 0000000..a5cb691 --- /dev/null +++ b/jsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + // other options... + "baseUrl": ".", + "paths": { + "three/webgpu": ["node_modules/three/build/three.webgpu.js"], + "three/tsl": ["node_modules/three/build/three.tsl.js"], + }, + } +} \ No newline at end of file diff --git a/netcodedocs/ptype-psub-defs.md b/netcodedocs/ptype-psub-defs.md new file mode 100644 index 0000000..742156e --- /dev/null +++ b/netcodedocs/ptype-psub-defs.md @@ -0,0 +1,14 @@ +# PType and PSub Definitions + +A BO3 GameServer packet has a 2 byte header. +Byte 1 is PType (packet type), which is used to check whether its serverbound or clientbound, however nonstandard BO3 servers can have the PType above 0x01 +Byte 2 is PSub (packet subtype), which is the actual packet name. Each ptype can have 256 different subtypes + +## PType +0x00 = Clientbound (should never be sent by a client) +0x01 = Serverbound (should never be recieved by a client) + +## PSub + +0x02 = Replication channel +0x01 = Echo (Server will return what you send) \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..5fc7dcc --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1098 @@ +{ + "name": "jsrobloxclone", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "jsrobloxclone", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "cannon-es": "^0.20.0", + "three": "^0.180.0", + "ws": "^8.18.3" + }, + "devDependencies": { + "vite": "^7.1.9" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.10.tgz", + "integrity": "sha512-0NFWnA+7l41irNuaSVlLfgNT12caWJVLzp5eAVhZ0z1qpxbockccEt3s+149rE64VUI3Ml2zt8Nv5JVc4QXTsw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.10.tgz", + "integrity": "sha512-dQAxF1dW1C3zpeCDc5KqIYuZ1tgAdRXNoZP7vkBIRtKZPYe2xVr/d3SkirklCHudW1B45tGiUlz2pUWDfbDD4w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.10.tgz", + "integrity": "sha512-LSQa7eDahypv/VO6WKohZGPSJDq5OVOo3UoFR1E4t4Gj1W7zEQMUhI+lo81H+DtB+kP+tDgBp+M4oNCwp6kffg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.10.tgz", + "integrity": "sha512-MiC9CWdPrfhibcXwr39p9ha1x0lZJ9KaVfvzA0Wxwz9ETX4v5CHfF09bx935nHlhi+MxhA63dKRRQLiVgSUtEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.10.tgz", + "integrity": "sha512-JC74bdXcQEpW9KkV326WpZZjLguSZ3DfS8wrrvPMHgQOIEIG/sPXEN/V8IssoJhbefLRcRqw6RQH2NnpdprtMA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.10.tgz", + "integrity": "sha512-tguWg1olF6DGqzws97pKZ8G2L7Ig1vjDmGTwcTuYHbuU6TTjJe5FXbgs5C1BBzHbJ2bo1m3WkQDbWO2PvamRcg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.10.tgz", + "integrity": "sha512-3ZioSQSg1HT2N05YxeJWYR+Libe3bREVSdWhEEgExWaDtyFbbXWb49QgPvFH8u03vUPX10JhJPcz7s9t9+boWg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.10.tgz", + "integrity": "sha512-LLgJfHJk014Aa4anGDbh8bmI5Lk+QidDmGzuC2D+vP7mv/GeSN+H39zOf7pN5N8p059FcOfs2bVlrRr4SK9WxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.10.tgz", + "integrity": "sha512-oR31GtBTFYCqEBALI9r6WxoU/ZofZl962pouZRTEYECvNF/dtXKku8YXcJkhgK/beU+zedXfIzHijSRapJY3vg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.10.tgz", + "integrity": "sha512-5luJWN6YKBsawd5f9i4+c+geYiVEw20FVW5x0v1kEMWNq8UctFjDiMATBxLvmmHA4bf7F6hTRaJgtghFr9iziQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.10.tgz", + "integrity": "sha512-NrSCx2Kim3EnnWgS4Txn0QGt0Xipoumb6z6sUtl5bOEZIVKhzfyp/Lyw4C1DIYvzeW/5mWYPBFJU3a/8Yr75DQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.10.tgz", + "integrity": "sha512-xoSphrd4AZda8+rUDDfD9J6FUMjrkTz8itpTITM4/xgerAZZcFW7Dv+sun7333IfKxGG8gAq+3NbfEMJfiY+Eg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.10.tgz", + "integrity": "sha512-ab6eiuCwoMmYDyTnyptoKkVS3k8fy/1Uvq7Dj5czXI6DF2GqD2ToInBI0SHOp5/X1BdZ26RKc5+qjQNGRBelRA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.10.tgz", + "integrity": "sha512-NLinzzOgZQsGpsTkEbdJTCanwA5/wozN9dSgEl12haXJBzMTpssebuXR42bthOF3z7zXFWH1AmvWunUCkBE4EA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.10.tgz", + "integrity": "sha512-FE557XdZDrtX8NMIeA8LBJX3dC2M8VGXwfrQWU7LB5SLOajfJIxmSdyL/gU1m64Zs9CBKvm4UAuBp5aJ8OgnrA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.10.tgz", + "integrity": "sha512-3BBSbgzuB9ajLoVZk0mGu+EHlBwkusRmeNYdqmznmMc9zGASFjSsxgkNsqmXugpPk00gJ0JNKh/97nxmjctdew==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.10.tgz", + "integrity": "sha512-QSX81KhFoZGwenVyPoberggdW1nrQZSvfVDAIUXr3WqLRZGZqWk/P4T8p2SP+de2Sr5HPcvjhcJzEiulKgnxtA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.10.tgz", + "integrity": "sha512-AKQM3gfYfSW8XRk8DdMCzaLUFB15dTrZfnX8WXQoOUpUBQ+NaAFCP1kPS/ykbbGYz7rxn0WS48/81l9hFl3u4A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.10.tgz", + "integrity": "sha512-7RTytDPGU6fek/hWuN9qQpeGPBZFfB4zZgcz2VK2Z5VpdUxEI8JKYsg3JfO0n/Z1E/6l05n0unDCNc4HnhQGig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.10.tgz", + "integrity": "sha512-5Se0VM9Wtq797YFn+dLimf2Zx6McttsH2olUBsDml+lm0GOCRVebRWUvDtkY4BWYv/3NgzS8b/UM3jQNh5hYyw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.10.tgz", + "integrity": "sha512-XkA4frq1TLj4bEMB+2HnI0+4RnjbuGZfet2gs/LNs5Hc7D89ZQBHQ0gL2ND6Lzu1+QVkjp3x1gIcPKzRNP8bXw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.10.tgz", + "integrity": "sha512-AVTSBhTX8Y/Fz6OmIVBip9tJzZEUcY8WLh7I59+upa5/GPhh2/aM6bvOMQySspnCCHvFi79kMtdJS1w0DXAeag==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.10.tgz", + "integrity": "sha512-fswk3XT0Uf2pGJmOpDB7yknqhVkJQkAQOcW/ccVOtfx05LkbWOaRAtn5SaqXypeKQra1QaEa841PgrSL9ubSPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.10.tgz", + "integrity": "sha512-ah+9b59KDTSfpaCg6VdJoOQvKjI33nTaQr4UluQwW7aEwZQsbMCfTmfEO4VyewOxx4RaDT/xCy9ra2GPWmO7Kw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.10.tgz", + "integrity": "sha512-QHPDbKkrGO8/cz9LKVnJU22HOi4pxZnZhhA2HYHez5Pz4JeffhDjf85E57Oyco163GnzNCVkZK0b/n4Y0UHcSw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.10.tgz", + "integrity": "sha512-9KpxSVFCu0iK1owoez6aC/s/EdUQLDN3adTxGCqxMVhrPDj6bt5dbrHDXUuq+Bs2vATFBBrQS5vdQ/Ed2P+nbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.4.tgz", + "integrity": "sha512-BTm2qKNnWIQ5auf4deoetINJm2JzvihvGb9R6K/ETwKLql/Bb3Eg2H1FBp1gUb4YGbydMA3jcmQTR73q7J+GAA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.4.tgz", + "integrity": "sha512-P9LDQiC5vpgGFgz7GSM6dKPCiqR3XYN1WwJKA4/BUVDjHpYsf3iBEmVz62uyq20NGYbiGPR5cNHI7T1HqxNs2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.4.tgz", + "integrity": "sha512-QRWSW+bVccAvZF6cbNZBJwAehmvG9NwfWHwMy4GbWi/BQIA/laTIktebT2ipVjNncqE6GLPxOok5hsECgAxGZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.4.tgz", + "integrity": "sha512-hZgP05pResAkRJxL1b+7yxCnXPGsXU0fG9Yfd6dUaoGk+FhdPKCJ5L1Sumyxn8kvw8Qi5PvQ8ulenUbRjzeCTw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.4.tgz", + "integrity": "sha512-xmc30VshuBNUd58Xk4TKAEcRZHaXlV+tCxIXELiE9sQuK3kG8ZFgSPi57UBJt8/ogfhAF5Oz4ZSUBN77weM+mQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.4.tgz", + "integrity": "sha512-WdSLpZFjOEqNZGmHflxyifolwAiZmDQzuOzIq9L27ButpCVpD7KzTRtEG1I0wMPFyiyUdOO+4t8GvrnBLQSwpw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.4.tgz", + "integrity": "sha512-xRiOu9Of1FZ4SxVbB0iEDXc4ddIcjCv2aj03dmW8UrZIW7aIQ9jVJdLBIhxBI+MaTnGAKyvMwPwQnoOEvP7FgQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.4.tgz", + "integrity": "sha512-FbhM2p9TJAmEIEhIgzR4soUcsW49e9veAQCziwbR+XWB2zqJ12b4i/+hel9yLiD8pLncDH4fKIPIbt5238341Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.4.tgz", + "integrity": "sha512-4n4gVwhPHR9q/g8lKCyz0yuaD0MvDf7dV4f9tHt0C73Mp8h38UCtSCSE6R9iBlTbXlmA8CjpsZoujhszefqueg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.4.tgz", + "integrity": "sha512-u0n17nGA0nvi/11gcZKsjkLj1QIpAuPFQbR48Subo7SmZJnGxDpspyw2kbpuoQnyK+9pwf3pAoEXerJs/8Mi9g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.4.tgz", + "integrity": "sha512-0G2c2lpYtbTuXo8KEJkDkClE/+/2AFPdPAbmaHoE870foRFs4pBrDehilMcrSScrN/fB/1HTaWO4bqw+ewBzMQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.4.tgz", + "integrity": "sha512-teSACug1GyZHmPDv14VNbvZFX779UqWTsd7KtTM9JIZRDI5NUwYSIS30kzI8m06gOPB//jtpqlhmraQ68b5X2g==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.4.tgz", + "integrity": "sha512-/MOEW3aHjjs1p4Pw1Xk4+3egRevx8Ji9N6HUIA1Ifh8Q+cg9dremvFCUbOX2Zebz80BwJIgCBUemjqhU5XI5Eg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.4.tgz", + "integrity": "sha512-1HHmsRyh845QDpEWzOFtMCph5Ts+9+yllCrREuBR/vg2RogAQGGBRC8lDPrPOMnrdOJ+mt1WLMOC2Kao/UwcvA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.4.tgz", + "integrity": "sha512-seoeZp4L/6D1MUyjWkOMRU6/iLmCU2EjbMTyAG4oIOs1/I82Y5lTeaxW0KBfkUdHAWN7j25bpkt0rjnOgAcQcA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.4.tgz", + "integrity": "sha512-Wi6AXf0k0L7E2gteNsNHUs7UMwCIhsCTs6+tqQ5GPwVRWMaflqGec4Sd8n6+FNFDw9vGcReqk2KzBDhCa1DLYg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.4.tgz", + "integrity": "sha512-dtBZYjDmCQ9hW+WgEkaffvRRCKm767wWhxsFW3Lw86VXz/uJRuD438/XvbZT//B96Vs8oTA8Q4A0AfHbrxP9zw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.4.tgz", + "integrity": "sha512-1ox+GqgRWqaB1RnyZXL8PD6E5f7YyRUJYnCqKpNzxzP0TkaUh112NDrR9Tt+C8rJ4x5G9Mk8PQR3o7Ku2RKqKA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.4.tgz", + "integrity": "sha512-8GKr640PdFNXwzIE0IrkMWUNUomILLkfeHjXBi/nUvFlpZP+FA8BKGKpacjW6OUUHaNI6sUURxR2U2g78FOHWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.4.tgz", + "integrity": "sha512-AIy/jdJ7WtJ/F6EcfOb2GjR9UweO0n43jNObQMb6oGxkYTfLcnN7vYYpG+CN3lLxrQkzWnMOoNSHTW54pgbVxw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.4.tgz", + "integrity": "sha512-UF9KfsH9yEam0UjTwAgdK0anlQ7c8/pWPU2yVjyWcF1I1thABt6WXE47cI71pGiZ8wGvxohBoLnxM04L/wj8mQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.4.tgz", + "integrity": "sha512-bf9PtUa0u8IXDVxzRToFQKsNCRz9qLYfR/MpECxl4mRoWYjAeFjgxj1XdZr2M/GNVpT05p+LgQOHopYDlUu6/w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/cannon-es": { + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/cannon-es/-/cannon-es-0.20.0.tgz", + "integrity": "sha512-eZhWTZIkFOnMAJOgfXJa9+b3kVlvG+FX4mdkpePev/w/rP5V8NRquGyEozcjPfEoXUlb+p7d9SUcmDSn14prOA==", + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.10.tgz", + "integrity": "sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.10", + "@esbuild/android-arm": "0.25.10", + "@esbuild/android-arm64": "0.25.10", + "@esbuild/android-x64": "0.25.10", + "@esbuild/darwin-arm64": "0.25.10", + "@esbuild/darwin-x64": "0.25.10", + "@esbuild/freebsd-arm64": "0.25.10", + "@esbuild/freebsd-x64": "0.25.10", + "@esbuild/linux-arm": "0.25.10", + "@esbuild/linux-arm64": "0.25.10", + "@esbuild/linux-ia32": "0.25.10", + "@esbuild/linux-loong64": "0.25.10", + "@esbuild/linux-mips64el": "0.25.10", + "@esbuild/linux-ppc64": "0.25.10", + "@esbuild/linux-riscv64": "0.25.10", + "@esbuild/linux-s390x": "0.25.10", + "@esbuild/linux-x64": "0.25.10", + "@esbuild/netbsd-arm64": "0.25.10", + "@esbuild/netbsd-x64": "0.25.10", + "@esbuild/openbsd-arm64": "0.25.10", + "@esbuild/openbsd-x64": "0.25.10", + "@esbuild/openharmony-arm64": "0.25.10", + "@esbuild/sunos-x64": "0.25.10", + "@esbuild/win32-arm64": "0.25.10", + "@esbuild/win32-ia32": "0.25.10", + "@esbuild/win32-x64": "0.25.10" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rollup": { + "version": "4.52.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.4.tgz", + "integrity": "sha512-CLEVl+MnPAiKh5pl4dEWSyMTpuflgNQiLGhMv8ezD5W/qP8AKvmYpCOKRRNOh7oRKnauBZ4SyeYkMS+1VSyKwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.52.4", + "@rollup/rollup-android-arm64": "4.52.4", + "@rollup/rollup-darwin-arm64": "4.52.4", + "@rollup/rollup-darwin-x64": "4.52.4", + "@rollup/rollup-freebsd-arm64": "4.52.4", + "@rollup/rollup-freebsd-x64": "4.52.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.52.4", + "@rollup/rollup-linux-arm-musleabihf": "4.52.4", + "@rollup/rollup-linux-arm64-gnu": "4.52.4", + "@rollup/rollup-linux-arm64-musl": "4.52.4", + "@rollup/rollup-linux-loong64-gnu": "4.52.4", + "@rollup/rollup-linux-ppc64-gnu": "4.52.4", + "@rollup/rollup-linux-riscv64-gnu": "4.52.4", + "@rollup/rollup-linux-riscv64-musl": "4.52.4", + "@rollup/rollup-linux-s390x-gnu": "4.52.4", + "@rollup/rollup-linux-x64-gnu": "4.52.4", + "@rollup/rollup-linux-x64-musl": "4.52.4", + "@rollup/rollup-openharmony-arm64": "4.52.4", + "@rollup/rollup-win32-arm64-msvc": "4.52.4", + "@rollup/rollup-win32-ia32-msvc": "4.52.4", + "@rollup/rollup-win32-x64-gnu": "4.52.4", + "@rollup/rollup-win32-x64-msvc": "4.52.4", + "fsevents": "~2.3.2" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/three": { + "version": "0.180.0", + "resolved": "https://registry.npmjs.org/three/-/three-0.180.0.tgz", + "integrity": "sha512-o+qycAMZrh+TsE01GqWUxUIKR1AL0S8pq7zDkYOQw8GqfX8b8VoCKYUoHbhiX5j+7hr8XsuHDVU6+gkQJQKg9w==", + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/vite": { + "version": "7.1.9", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.9.tgz", + "integrity": "sha512-4nVGliEpxmhCL8DslSAUdxlB6+SMrhB0a1v5ijlh1xB1nEPuy1mxaHxysVucLHuWryAxLWg6a5ei+U4TLn/rFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..765510e --- /dev/null +++ b/package.json @@ -0,0 +1,20 @@ +{ + "name": "jsrobloxclone", + "version": "1.0.0", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "ISC", + "description": "", + "dependencies": { + "cannon-es": "^0.20.0", + "three": "^0.180.0", + "ws": "^8.18.3" + }, + "devDependencies": { + "vite": "^7.1.9" + }, + "type":"module" +}