Add launchable
This commit is contained in:
13
js/instances/BaseService.js
Normal file
13
js/instances/BaseService.js
Normal file
@@ -0,0 +1,13 @@
|
||||
import { Instance } from "./Instance.js";
|
||||
export class BaseService extends Instance {
|
||||
constructor(name) {
|
||||
super(name);
|
||||
}
|
||||
SetParent(parent) {
|
||||
if (this.Parent === null) {
|
||||
super.SetParent(parent);
|
||||
return;
|
||||
} // Only accept it once
|
||||
throw new Error("Cannot reparent a service instance.");
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
import { Instance } from "./Instance.js";
|
||||
import { BaseService } from "./BaseService.js";
|
||||
import { BO3ScriptSignal } from "../core/BO3ScriptSignal.js";
|
||||
|
||||
export class DebugTextService extends Instance {
|
||||
export class DebugTextService extends BaseService {
|
||||
constructor() {
|
||||
super("DebugTextService");
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Instance } from "./Instance.js";
|
||||
export class ExampleService extends Instance {
|
||||
import { BaseService } from "./BaseService.js";
|
||||
export class ExampleService extends BaseService {
|
||||
constructor() {
|
||||
super("ExampleService");
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ let instanceCounter = 0;
|
||||
export class Instance {
|
||||
constructor(className = "Instance") {
|
||||
this.ClassName = className;
|
||||
this.Name = `${className}${instanceCounter++}`;
|
||||
this.Name = `${className}`;
|
||||
this.Parent = null;
|
||||
this.Children = [];
|
||||
this.InstanceId = guidGenerator(); // Unique identifier, used for replication
|
||||
@@ -120,4 +120,21 @@ export class Instance {
|
||||
console.warn("Destroy: No DataModel found in ancestry. Are you destroying an unparented instance?");
|
||||
}
|
||||
} // Garbage collected after this
|
||||
|
||||
LuaBridge(exclude, visited = new Set()) { // Convert to a plain object for LuaBridge. Should be overridden in subclasses to add properties.
|
||||
if (visited.has(this.InstanceId)) {
|
||||
// Already visited, avoid circular reference
|
||||
return null;
|
||||
}
|
||||
visited.add(this.InstanceId);
|
||||
const obj = {
|
||||
ClassName: this.ClassName,
|
||||
Name: this.Name,
|
||||
InstanceId: this.InstanceId,
|
||||
Properties: {},
|
||||
Children: this.Children.filter(c => c !== exclude).map(c => c.LuaBridge(this, visited)).filter(Boolean),
|
||||
Parent: this.Parent && this.Parent !== exclude ? this.Parent.LuaBridge(this, visited) : null // Avoid circular reference by excluding self
|
||||
};
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ if (typeof window !== "undefined") {
|
||||
WebSocketServer = wsModule.WebSocketServer;
|
||||
}
|
||||
import { Instance } from "./Instance.js";
|
||||
import { BaseService } from "./BaseService.js";
|
||||
import { BO3ScriptSignal } from "../core/BO3ScriptSignal.js";
|
||||
|
||||
/**
|
||||
@@ -17,7 +18,7 @@ import { BO3ScriptSignal } from "../core/BO3ScriptSignal.js";
|
||||
* - Binary framed packets: [uint32 length][ptype][psub][payload]
|
||||
* - Fires PacketReceived signal for incoming packets
|
||||
*/
|
||||
export class NetworkService extends Instance {
|
||||
export class NetworkService extends BaseService {
|
||||
constructor() {
|
||||
super("NetworkService");
|
||||
|
||||
@@ -132,6 +133,17 @@ export class NetworkService extends Instance {
|
||||
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.)");
|
||||
};
|
||||
// Register 0xFF kick handler
|
||||
this.registerHandler(0x00, 0xFF, (payload) => {
|
||||
let reason = "Kicked by server.";
|
||||
try {
|
||||
reason = new TextDecoder().decode(payload);
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
console.warn("[NetworkService] Kicked by server:", reason);
|
||||
alert("You have been kicked by the server:\n\n" + reason);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -151,6 +163,7 @@ export class NetworkService extends Instance {
|
||||
|
||||
this.wss.on("connection", (ws) => {
|
||||
this.clients.set(ws, {});
|
||||
ws.isAuthed = false; // Determine whether to send any data except auth packets
|
||||
console.log("[NetworkService] New client connected. Total clients:", this.clients.size);
|
||||
ws.binaryType = "arraybuffer";
|
||||
|
||||
@@ -167,10 +180,12 @@ export class NetworkService extends Instance {
|
||||
});
|
||||
}
|
||||
|
||||
broadcast(ptype, psub, payload) {
|
||||
broadcast(ptype, psub, payload,ignoreUnauthed,ignoreCheck) {
|
||||
if (!this.isServer || !this.wss) return;
|
||||
const packet = this.encodePacket(ptype, psub, payload);
|
||||
for (const ws of this.clients.keys()) {
|
||||
if(ignoreUnauthed && !ws.isAuthed) continue;
|
||||
if (ignoreCheck && ignoreCheck(ws)) continue;
|
||||
if (ws.readyState === WebSocket.OPEN) ws.send(packet);
|
||||
}
|
||||
}
|
||||
@@ -181,6 +196,17 @@ export class NetworkService extends Instance {
|
||||
if (ws.readyState === WebSocket.OPEN) ws.send(packet);
|
||||
}
|
||||
|
||||
KickClientLater(ws,code=4000,reason="") {
|
||||
// Send a kick packet
|
||||
this.sendToClient(ws, 0x00, 0xFF, new TextEncoder().encode(reason));
|
||||
setTimeout(() => {
|
||||
if (ws.readyState === WebSocket.OPEN) {
|
||||
ws.close(code,reason);
|
||||
}
|
||||
}, 500); // Slight delay to ensure any pending packets are sent
|
||||
}
|
||||
|
||||
|
||||
Broadcast(ptype, psub, payload) {
|
||||
console.warn("[NetworkService] Broadcast is deprecated, use broadcast() instead.");
|
||||
this.broadcast(ptype, psub, payload);
|
||||
|
||||
11
js/instances/Player.js
Normal file
11
js/instances/Player.js
Normal file
@@ -0,0 +1,11 @@
|
||||
|
||||
import { Instance } from "./Instance.js";
|
||||
export class Player extends Instance {
|
||||
constructor( id, name, ws ) {
|
||||
super("Player");
|
||||
this.InstanceId = id; // To fix jank with replication system
|
||||
this.UserId = id; // For compatibility
|
||||
this.Name = name;
|
||||
this.ws = ws; // WebSocket or client reference
|
||||
}
|
||||
}
|
||||
105
js/instances/Players.js
Normal file
105
js/instances/Players.js
Normal file
@@ -0,0 +1,105 @@
|
||||
import { NetworkService } from "./NetworkService.js";
|
||||
import { Player } from "./Player.js";
|
||||
import { BaseService } from "./BaseService.js";
|
||||
import { Auth } from "../core/Auth.js";
|
||||
/**
|
||||
* Players manager
|
||||
* - Tracks connected players after successful auth (0x01 0x03)
|
||||
*/
|
||||
export class Players extends BaseService {
|
||||
constructor(networkService,isClient) {
|
||||
super("Players");
|
||||
this.InstanceId = "Players"; // For replication system
|
||||
if (isClient) {
|
||||
this.networkService = networkService;
|
||||
this.networkService.registerHandler(0x00, 0x04, (payload) => {
|
||||
// Auth response from server, parse it
|
||||
const decoder = new TextDecoder();
|
||||
const msg = decoder.decode(payload);
|
||||
console.debug("[Players] Received auth response from server:", msg);
|
||||
if (msg.startsWith("AUTH_SUCCESS|")) {
|
||||
const playerName = msg.split("|")[1];
|
||||
const id = msg.split("|")[2];
|
||||
console.debug(`[Players] You have logged in as ${playerName}`);
|
||||
// Create a local Player instance
|
||||
const player = new Player( id, playerName, null );
|
||||
this.players = new Map();
|
||||
this.players.set(null, player); // single local player
|
||||
this.LocalPlayer = player;
|
||||
player.SetParent(this);
|
||||
} else {
|
||||
console.warn("[Players] Auth failed or unknown response:", msg);
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.networkService = networkService;
|
||||
this.players = new Map(); // id -> Player
|
||||
this.authenticator = new Auth();
|
||||
|
||||
// Listen for auth packets
|
||||
this.networkService.registerHandler(0x01, 0x03, async (payload, ws) => {
|
||||
if (ws.isAuthed) return; // Already authed, ignore additional auth attempts
|
||||
console.debug("[Players] Received auth packet:", "CENSORED");
|
||||
// Assume payload is a string (player name)
|
||||
let token;
|
||||
try {
|
||||
// Decode bytes
|
||||
const decoder = new TextDecoder();
|
||||
token = decoder.decode(payload);
|
||||
} catch (e) {
|
||||
console.warn("[Players] Invalid auth payload:", payload);
|
||||
networkService.KickClientLater(ws, 4000, "Invalid auth payload, please try again.");
|
||||
|
||||
return;
|
||||
}
|
||||
// Verify token (dummy implementation)
|
||||
const id = await this.authenticator.verifySessionToken(token);
|
||||
if (id === null) {
|
||||
// Kick off the client
|
||||
networkService.KickClientLater(ws, 4001, "Authentication failed; are you logged in?");
|
||||
// Reason must be understandable as its shown to the user
|
||||
return;
|
||||
}
|
||||
console.debug(`[Players] Client authenticated as ${id}`);
|
||||
ws.isAuthed = true; // Mark ws as authed
|
||||
// Create Player instance
|
||||
const name = await this.authenticator.getUserNameById(id);
|
||||
const player = new Player(id, name, ws );
|
||||
this.players.set(ws, player);
|
||||
player.SetParent(this);
|
||||
// Listen for disconnection
|
||||
ws.on("close", () => {
|
||||
this.players.delete(ws);
|
||||
player.Destroy();
|
||||
console.debug(`[Players] Player ${name} (${id}) disconnected.`);
|
||||
});
|
||||
console.debug(`[Players] Player ${name} (${id}) connected.`);
|
||||
// Send auth success packet
|
||||
const encoder = new TextEncoder();
|
||||
const successPayload = encoder.encode("AUTH_SUCCESS|" + name + "|" + id);
|
||||
this.networkService.sendToClient(ws, 0x00, 0x04, successPayload);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all connected players
|
||||
*/
|
||||
getAll() {
|
||||
return Array.from(this.players.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove player by ws
|
||||
*/
|
||||
removeByWs(ws) {
|
||||
this.players.delete(ws);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get player by ws
|
||||
*/
|
||||
getByWs(ws) {
|
||||
return this.players.get(ws);
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,8 @@ 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 {
|
||||
import { BaseService } from "./BaseService.js";
|
||||
export class RenderService extends BaseService {
|
||||
constructor() {
|
||||
super("RenderService");
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Instance } from "./Instance.js";
|
||||
import { BaseService } from "./BaseService.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 {
|
||||
export class ReplicatorService extends BaseService {
|
||||
constructor(datamodel, network) {
|
||||
super("ReplicatorService");
|
||||
// Get tickloop from RenderService
|
||||
@@ -98,6 +99,9 @@ export class ReplicatorService extends Instance {
|
||||
});
|
||||
} else {
|
||||
this._renderService.Heartbeat.Connect((dt) => this._onHeartbeat(dt));
|
||||
this._networkService.registerHandler(0x01, 0x02, (payload,ws) => {
|
||||
}); // Server side replicator reciever, will be used later for client to server replication
|
||||
// aka what roblox does for certain instances like under the player character
|
||||
}
|
||||
this.SetParent(datamodel);
|
||||
this.lastClearedStatCheck = 0; // If above 1 second, clear stats
|
||||
@@ -132,9 +136,14 @@ export class ReplicatorService extends Instance {
|
||||
const toReplicate = [];
|
||||
// Get workspace and its children
|
||||
const workspace = this.Parent.GetService("Workspace");
|
||||
const players = this.Parent.GetService("Players");
|
||||
if (workspace) {
|
||||
toReplicate.push(...workspace.Children);
|
||||
}
|
||||
if (players) {
|
||||
toReplicate.push(...players.Children); // Replicate player instances too
|
||||
// (so players can see each other)
|
||||
}
|
||||
// Send over the pipe
|
||||
for (const inst of toReplicate) {
|
||||
// Check property hashes to see if anything changed
|
||||
@@ -175,7 +184,8 @@ export class ReplicatorService extends Instance {
|
||||
key === "scene" ||
|
||||
key === "controls" ||
|
||||
key === "physicsWorld" ||
|
||||
key === "loader"
|
||||
key === "loader" ||
|
||||
key === "LuaState" // Script Lua state
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
@@ -199,7 +209,14 @@ export class ReplicatorService extends Instance {
|
||||
// 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
|
||||
this._networkService.broadcast(0x00, 0x02, outpay,true,(ws) => {
|
||||
const player = players ? players.getByWs(ws) : null;
|
||||
// Dont send to the player if its their own character
|
||||
if (player && inst.InstanceId === player.InstanceId) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}); // 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.`);
|
||||
|
||||
63
js/instances/Script.js
Normal file
63
js/instances/Script.js
Normal file
@@ -0,0 +1,63 @@
|
||||
import { lua, lauxlib, lualib, to_luastring } from "fengari";
|
||||
import { ScriptSchedulerService } from "./ScriptSchedulerService.js";
|
||||
import { Instance } from "./Instance.js"; // your engine's instance class
|
||||
|
||||
// Import interop from fengari-interop
|
||||
import { push, luaopen_js } from "fengari-interop";
|
||||
|
||||
export class Script extends Instance {
|
||||
constructor(sourceCode) {
|
||||
super("Script");
|
||||
this.Source = sourceCode;
|
||||
this.LuaState = null;
|
||||
}
|
||||
|
||||
Run() {
|
||||
const L = lauxlib.luaL_newstate();
|
||||
luaopen_js(L);
|
||||
lualib.luaL_openlibs(L);
|
||||
|
||||
// Bridge all engine APIs
|
||||
this._registerGlobals(L);
|
||||
|
||||
// Compile the source
|
||||
const status = lauxlib.luaL_loadstring(L, to_luastring(this.Source));
|
||||
if (status !== lua.LUA_OK) {
|
||||
const err = lua.lua_tojsstring(L, -1);
|
||||
throw new Error("Lua load error: " + err);
|
||||
}
|
||||
|
||||
// Create a coroutine thread
|
||||
const thread = lua.lua_newthread(L);
|
||||
|
||||
// Schedule with scheduler
|
||||
this._getScheduler().scheduleFengariCoroutine(thread, L);
|
||||
this.LuaState = L;
|
||||
}
|
||||
|
||||
_registerGlobals(L) {
|
||||
// game global
|
||||
push(L, globalThis.game);
|
||||
lua.lua_setglobal(L, to_luastring("game"));
|
||||
|
||||
// task + wait bridge
|
||||
lua.lua_pushjsfunction(L, () => {
|
||||
const secs = lua.lua_isnumber(L, 1) ? lua.lua_tonumber(L, 1) : 0;
|
||||
const yieldVal = JSON.stringify({ wait: secs });
|
||||
lua.lua_pushstring(L, to_luastring(yieldVal));
|
||||
return lua.lua_yield(L, 1);
|
||||
});
|
||||
lua.lua_setglobal(L, to_luastring("task_wait"));
|
||||
|
||||
// Lua API helpers
|
||||
lua.lua_pushjsfunction(L, () => {
|
||||
const inst = new Instance("Part"); // example
|
||||
return inst.LuaBridge(); // converts to Lua table/proxy
|
||||
});
|
||||
lua.lua_setglobal(L, to_luastring("Instance_new"));
|
||||
}
|
||||
|
||||
_getScheduler() {
|
||||
return game.GetService("ScriptSchedulerService");
|
||||
}
|
||||
}
|
||||
111
js/instances/ScriptSchedulerService.js
Normal file
111
js/instances/ScriptSchedulerService.js
Normal file
@@ -0,0 +1,111 @@
|
||||
import { lua, lauxlib, lualib, to_luastring } from "fengari";
|
||||
const lua_resume = lua.lua_resume;
|
||||
const LUA_OK = lua.LUA_OK;
|
||||
const LUA_YIELD = lua.LUA_YIELD;
|
||||
import { BaseService } from "./BaseService.js";
|
||||
export class ScriptSchedulerService extends BaseService {
|
||||
constructor() {
|
||||
super("ScriptSchedulerService");
|
||||
this._scheduled = [];
|
||||
this._waiting = [];
|
||||
this._running = false;
|
||||
}
|
||||
|
||||
// Add a script (generator function) to the scheduler
|
||||
schedule(scriptGen) {
|
||||
if (typeof scriptGen === "function") {
|
||||
const gen = scriptGen();
|
||||
this._scheduled.push({ gen, wakeTime: 0 });
|
||||
} else if (scriptGen && typeof scriptGen.next === "function") {
|
||||
this._scheduled.push({ gen: scriptGen, wakeTime: 0 });
|
||||
} else {
|
||||
throw new Error("Script must be a generator or generator function");
|
||||
}
|
||||
}
|
||||
|
||||
// Add a Fengari coroutine (thread) to the scheduler
|
||||
scheduleFengariCoroutine(luaThread, luaState) {
|
||||
// luaThread: the coroutine (thread) object from Fengari
|
||||
// luaState: the Lua state (L) from Fengari
|
||||
console.debug("Scheduling Fengari coroutine");
|
||||
this._scheduled.push({ fengari: true, thread: luaThread, state: luaState, wakeTime: 0 });
|
||||
}
|
||||
|
||||
// Main update loop, call this every frame with dt (delta time in seconds)
|
||||
step(dt) {
|
||||
const now = Date.now();
|
||||
// Wake up any waiting scripts whose time has come
|
||||
for (let i = this._waiting.length - 1; i >= 0; i--) {
|
||||
const item = this._waiting[i];
|
||||
if (now >= item.wakeTime) {
|
||||
this._scheduled.push(item);
|
||||
this._waiting.splice(i, 1);
|
||||
console.log("Woke up routine",i,"from waiting.");
|
||||
}
|
||||
}
|
||||
// Run scheduled scripts
|
||||
for (let i = this._scheduled.length - 1; i >= 0; i--) {
|
||||
const item = this._scheduled[i];
|
||||
if (item.fengari) {
|
||||
// Fengari coroutine
|
||||
const L = item.state;
|
||||
const thread = item.thread;
|
||||
// Resume the coroutine
|
||||
const status = lua_resume(thread, L, 0);
|
||||
if (status === LUA_OK) {
|
||||
// Coroutine finished
|
||||
this._scheduled.splice(i, 1);
|
||||
console.log("Woke up routine",i,"from fengari, finished.");
|
||||
} else if (status === LUA_YIELD) {
|
||||
// Check for yield value (wait)
|
||||
// You must implement a way for Lua to yield with a wait time, e.g. coroutine.yield({wait=seconds})
|
||||
// For now, we assume the top of the stack is a table with a 'wait' field
|
||||
const luaTable = lua.lua_tojsstring(L, -1);
|
||||
let waitSeconds = 0;
|
||||
try {
|
||||
// Try to parse the yielded value as JSON (if you yield a JSON string)
|
||||
const val = JSON.parse(luaTable);
|
||||
if (val && val.wait) waitSeconds = val.wait;
|
||||
} catch (e) { }
|
||||
if (waitSeconds > 0) {
|
||||
this._waiting.push({ ...item, wakeTime: now + waitSeconds * 1000 });
|
||||
this._scheduled.splice(i, 1);
|
||||
}
|
||||
// Otherwise, just continue next frame
|
||||
} else {
|
||||
// Error or unknown status
|
||||
this._scheduled.splice(i, 1);
|
||||
}
|
||||
} else {
|
||||
// JavaScript generator
|
||||
const { value, done } = item.gen.next();
|
||||
if (done) {
|
||||
this._scheduled.splice(i, 1);
|
||||
console.log("Woke up routine",i,"from JS generator, finished.");
|
||||
} else if (value && value.wait) {
|
||||
// Script yielded with {wait: seconds}
|
||||
const ms = value.wait * 1000;
|
||||
this._waiting.push({ gen: item.gen, wakeTime: now + ms });
|
||||
this._scheduled.splice(i, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Utility for scripts to yield for a certain time (in seconds)
|
||||
static wait(seconds) {
|
||||
return { wait: seconds };
|
||||
}
|
||||
}
|
||||
|
||||
// Example usage:
|
||||
// function* myScript() {
|
||||
// console.log("Script start");
|
||||
// yield ScriptSchedulerService.wait(1);
|
||||
// console.log("1 second later");
|
||||
// yield ScriptSchedulerService.wait(2);
|
||||
// console.log("2 more seconds later");
|
||||
// }
|
||||
// const scheduler = new ScriptSchedulerService();
|
||||
// scheduler.schedule(myScript);
|
||||
// setInterval(() => scheduler.step(1/60), 1000/60);
|
||||
117
js/instances/ServiceService.js
Normal file
117
js/instances/ServiceService.js
Normal file
@@ -0,0 +1,117 @@
|
||||
// ServiceService.js
|
||||
import { BaseService } from "./BaseService.js";
|
||||
import { NetworkService } from "./NetworkService.js";
|
||||
import { RenderService } from "./RenderService.js";
|
||||
import { ReplicatorService } from "./ReplicatorService.js";
|
||||
import { ScriptSchedulerService } from "./ScriptSchedulerService.js";
|
||||
import { Players } from "./Players.js";
|
||||
import { Workspace } from "./Workspace.js";
|
||||
import { DataModel } from "../core/DataModel.js";
|
||||
import * as THREE from "three";
|
||||
|
||||
export class ServiceService extends BaseService {
|
||||
constructor() {
|
||||
super("ServiceService");
|
||||
this._services = new Map();
|
||||
}
|
||||
|
||||
register(name, ctor, ...args) {
|
||||
if (this._services.has(name)) return this._services.get(name);
|
||||
const instance = new ctor(...args);
|
||||
this._services.set(name, instance);
|
||||
return instance;
|
||||
}
|
||||
|
||||
GetService(name) {
|
||||
return this._services.get(name);
|
||||
}
|
||||
GetDataModel() {
|
||||
return globalThis.game;
|
||||
}
|
||||
|
||||
initAll(port) {
|
||||
// === Core DataModel ===
|
||||
const dm = this.register("DataModel", DataModel);
|
||||
dm.SetParent(null);
|
||||
this.SetParent(dm); // Parent myself to the DataModel
|
||||
globalThis.game = dm;
|
||||
|
||||
// === Workspace ===
|
||||
const ws = this.register("Workspace", Workspace);
|
||||
ws.SetParent(dm);
|
||||
|
||||
// === RenderService ===
|
||||
const render = this.register("RenderService", RenderService, dm);
|
||||
render.isServer = true;
|
||||
render.start();
|
||||
render.SetParent(dm);
|
||||
|
||||
// === Networking ===
|
||||
const net = this.register("NetworkService", NetworkService);
|
||||
net.listen(port);
|
||||
net.SetParent(dm);
|
||||
|
||||
// === Players ===
|
||||
const players = this.register("Players", Players, net);
|
||||
players.SetParent(dm);
|
||||
|
||||
// === ReplicatorService ===
|
||||
const replication = this.register("ReplicatorService", ReplicatorService, dm, net);
|
||||
|
||||
// === ScriptSchedulerService ===
|
||||
const scriptScheduler = this.register(
|
||||
"ScriptSchedulerService",
|
||||
ScriptSchedulerService,
|
||||
dm
|
||||
);
|
||||
scriptScheduler.SetParent(dm);
|
||||
// Connect to RenderService for update loop
|
||||
render.Stepped.Connect((dt) => {
|
||||
scriptScheduler.step(dt);
|
||||
});
|
||||
|
||||
// Cross-link
|
||||
replication.Players = players;
|
||||
players.Replicator = replication;
|
||||
|
||||
// === Example echo handler ===
|
||||
net.registerHandler(0x01, 0x00, (payload, client) => {
|
||||
console.log("Echo:", new TextDecoder().decode(payload));
|
||||
net.sendToClient(client, 0x01, 0x01, payload);
|
||||
});
|
||||
|
||||
// === Example test parts ===
|
||||
//this._spawnDemoScene(ws);
|
||||
|
||||
console.log("[ServiceService] Initialized all services.");
|
||||
}
|
||||
|
||||
async _spawnDemoScene(ws) {
|
||||
const { LerpedBasePart } = await import("./LerpedBasePart.js");
|
||||
|
||||
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();
|
||||
|
||||
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);
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { BaseService } from "./BaseService.js";
|
||||
import { Instance } from "./Instance.js";
|
||||
export class Workspace extends Instance {
|
||||
export class Workspace extends BaseService {
|
||||
constructor() {
|
||||
super("Workspace");
|
||||
this.InstanceId = "WorkspaceRoot"; // Fixed ID for Workspace (Bad idea to replicate services over network?)
|
||||
|
||||
Reference in New Issue
Block a user