Something went wrong. Try again.
Starship Simulator Controls thoriumsim.com
Something went wrong. Try again.
7.4 kB · 239 lines
TypeScript
at develop
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240import path from "node:path";
import RAPIER from "@thorium-sim/rapier3d-node";import { initECS } from "@thorium/.server/classes/initECS";import { thoriumContext } from "@thorium/utils/.server/context";import { DataStore, type DataStoreOptions } from "@thorium/utils/.server/db-fs";import { executeBlocks } from "@thorium/utils/.server/executeBlocks";import { loadGltf } from "@thorium/utils/.server/loadGltf";import { processTriggers } from "@thorium/utils/.server/processTriggers";import { triggerAction } from "@thorium/utils/.server/triggerAction";import { ECS, Entity } from "@thorium/utils/ecs";import randomWords from "@thorium/utils/random-words";import { ConvexHull } from "@thorium/utils/starmap/ConvexHull.js";
import type ShipPlugin from "./Plugins/Ship";import type { ServerDataModel } from "./ServerDataModel";
export class FlightDataModel extends DataStore { static INTERVAL = 1000 / 60; name!: string; date!: number; paused!: boolean; hasFlightDirector!: boolean; state!: "in-progress" | "success" | "failure"; /** Why the flight was successful or failed */ stateReason!: string; /** How long destroyed player ships must wait before respawning. Null means no respawns. */ defaultRespawnTimeMs!: null | number; ecs!: ECS; pluginIds!: string[]; serverDataModel: ServerDataModel; interval!: ReturnType<typeof setInterval>; startInput!: { ships: any[]; missionId: any; startingPoint: any }; flightClientIndex: Map<string, number>; mode!: "nova" | "legacy";
static async create( params: Partial<FlightDataModel> & { serverDataModel: ServerDataModel; initialLoad?: boolean; entities: Entity[]; }, storeOptions: DataStoreOptions, ) { const instance = new this(params, storeOptions); const data = await instance.getData<FlightDataModel & { entities: Entity[] }>();
instance.name ??= data.name || params.name || "New Flight"; instance.paused ??= data.paused ?? true; instance.mode ??= data.mode ?? params.mode ?? "nova"; instance.hasFlightDirector ??= data.hasFlightDirector ?? params.hasFlightDirector ?? true; instance.date ??= Number(data.date ? new Date(data.date) : new Date()); instance.pluginIds ??= data.pluginIds || []; instance.state = data.state || "in-progress"; instance.stateReason = data.stateReason || ""; instance.startInput = data.startInput || {}; await instance.initEcs(params.serverDataModel, data.entities || []);
return instance; } constructor( params: Partial<FlightDataModel> & { serverDataModel: ServerDataModel; initialLoad?: boolean; entities: Entity[]; }, storeOptions: DataStoreOptions, ) { const flightName = params.name || randomWords(3).join("-");
super( { name: flightName, paused: false, date: Number(params.date ? new Date(params.date) : new Date()), }, storeOptions, ); this.flightClientIndex = new Map(); this.serverDataModel = params.serverDataModel; } run = () => { // Run all the systems if (!this.paused) { this.ecs.update(); } if (process.env.NODE_ENV === "test") return; this.writeThrottle(); this.interval = setTimeout(this.run, FlightDataModel.INTERVAL); }; destroy() { clearInterval(this.interval);
this.ecs.dispose(); this.remove(true); } private async initEcs(server: ServerDataModel, entities: Entity[]) { this.ecs = new ECS(server); initECS(this.ecs, entities, this.mode); this.setupEcsCallbacks(); if (!this.flightEntity) { const flight = new Entity(); flight.addComponent("isFlight", { respawnTimeMs: this.defaultRespawnTimeMs, }); this.ecs.addEntity(flight); } if (!this.interval) { this.run(); } } setupEcsCallbacks() { this.ecs.processTriggers = (event) => processTriggers(this.ecs, event); this.ecs.executeBlocks = (blocks, metadata) => executeBlocks(this.ecs, blocks, metadata); this.ecs.triggerAction = triggerAction; } async initPhysics() { // Fetch and calculate all of the colliders for the ships in the plugins // Loop over every ship in every loaded plugin const ships: ShipPlugin[] = []; for (const plugin of this.serverDataModel.plugins) { if (!this.pluginIds.includes(plugin.id)) continue; for (const ship of plugin.aspects.ships) { ships.push(ship); } }
await Promise.all( ships.map(async (ship) => { if (!ship.assets.model) return; const assetUrl = path.join(await ship.getAssetUrl(), ship.assets.model); if (!assetUrl) return; const colliderDesc = await generateColliderDesc(assetUrl, ship.mass /*ship.length*/); if (!colliderDesc) return; this.ecs.colliderCache.set(ship.assets.model, colliderDesc); }), ); }
stop() { this.paused = true; clearTimeout(this.interval); this.ecs.dispose(); this.flightClientIndex.clear(); }
// Helper Getters /** * All player ships in the universe. */ get playerShips() { return [...(this.ecs.componentCache.get("isPlayerShip") || [])]; } /** * All ships in the universe. */ get ships() { return [...(this.ecs.componentCache.get("isShip") || [])]; } async getSnapshots() { return thoriumContext.getStore()!.getFlightSnapshots(this.name); } get flightEntity() { return this.ecs.componentCache.get("isFlight")?.values().next().value; } /** * Ships that are available for spawning in the universe, based on the flight's plugins. */ get availableShips() { const allShips = this.pluginIds.reduce((prev: ShipPlugin[], next) => { const plugin = this.serverDataModel.plugins.find((plugin) => plugin.id === next); if (!plugin) return prev; return prev.concat(plugin.aspects.ships); }, []); return allShips; } get clients() { const clients: Record<string, Entity | undefined> = {}; for (const client of this.ecs.componentCache.get("flightClient") || []) { if (client.components.flightClient) { clients[client.components.flightClient.clientId] = client; } } return clients; } toJSON() { const entities = []; for (const [, entity] of this.ecs.entities) { entities.push(entity.toJSON()); } // Get all of the entities in the world and serialize them into objects const data = { name: this.name, paused: this.paused, date: this.date, hasFlightDirector: this.hasFlightDirector, pluginIds: this.pluginIds, entities, rng: { seed: this.ecs.rng.seed, skip: this.ecs.rng.iterationCount }, mode: this.mode, state: this.state, stateReason: this.stateReason, startInput: this.startInput, defaultRespawnTimeMs: this.defaultRespawnTimeMs, lastSaved: Date.now(), }; return data; }}
async function generateColliderDesc( filePath: string, mass: number, // size: number,) { try { const hull = new ConvexHull(); const gltf = await loadGltf(filePath); if (!gltf) { throw new Error("Failed to load gltf"); } // This properly scales the collider to the size of the ship // gltf.scene.children[0].scale.multiplyScalar(size / 1000);
hull.setFromObject(gltf.scene.children[0]); const vertices = []; for (const vertex of hull.vertices) { vertices.push(vertex.point.x, vertex.point.y, vertex.point.z); } const verticesFloat32 = new Float32Array(vertices); const colliderDesc = RAPIER.ColliderDesc.convexHull(verticesFloat32)?.setMass(mass);
return colliderDesc; } catch (err) { console.error("Failed to generate convex hulls for", filePath); console.error(err); return null; }}