diff --git a/.vscode/settings.json b/.vscode/settings.json index ad92582..95c6938 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,4 @@ { - "editor.formatOnSave": true + "editor.formatOnSave": true, + "editor.tabSize": 2 } diff --git a/packages/editor/src/editor/core/editor.ts b/packages/editor/src/editor/core/editor.ts index 97bfc61..1b50ff3 100644 --- a/packages/editor/src/editor/core/editor.ts +++ b/packages/editor/src/editor/core/editor.ts @@ -13,6 +13,7 @@ import { EditorEvents } from '../types/events'; import { WorkspaceComponent } from './workspace'; import { HistoryComponent } from './history'; import { ShortcutsComponent } from './shortcuts'; +import { Object3D } from 'three'; export class Editor extends Engine { public lastTick = performance.now(); @@ -20,8 +21,8 @@ export class Editor extends Engine { public running = false; override mount(element: HTMLElement) { - this.element = element; - this.render = new WebGLRenderer(element); + this.render = new WebGLRenderer(); + (this.render as WebGLRenderer).mount(element) this.use(HistoryComponent); this.use(ViewportComponent); @@ -78,7 +79,7 @@ export class Editor extends Engine { /** * Get selected objects. */ - public getSelection() { + public getSelection(): Object3D[] { return this.getComponent(WorkspaceComponent).selection; } } diff --git a/packages/engine/package.json b/packages/engine/package.json index 1fb44df..881b697 100644 --- a/packages/engine/package.json +++ b/packages/engine/package.json @@ -4,6 +4,7 @@ "description": "Freeblox Engine", "main": "dist/index.js", "types": "dist/index.d.ts", + "type": "module", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "build": "tsc", @@ -15,14 +16,10 @@ ], "exports": { ".": { - "import": "./dist/index.js", - "require": "./dist/index.js", - "types": "./dist/index.d.ts" + "import": "./dist/index.js" }, - "./node": { - "import": "./dist/node.js", - "require": "./dist/node.js", - "types": "./dist/node.d.ts" + "./dist/node": { + "import": "./dist/node.js" } }, "keywords": [ @@ -42,7 +39,17 @@ "buffer": "^6.0.3", "reflect-metadata": "^0.2.2", "smart-buffer": "^4.2.0", - "three": "^0.166.0", + "three": "^0.166.1", "uuid": "^10.0.0" + }, + "typesVersions": { + "*": { + "*": [ + "dist/index.d.ts" + ], + "dist/node": [ + "dist/node.d.ts" + ] + } } } diff --git a/packages/engine/pnpm-lock.yaml b/packages/engine/pnpm-lock.yaml new file mode 100644 index 0000000..a23633b --- /dev/null +++ b/packages/engine/pnpm-lock.yaml @@ -0,0 +1,62 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + three: + specifier: ^0.166.1 + version: 0.166.1 + devDependencies: + '@types/three': + specifier: ^0.166.0 + version: 0.166.0 + +packages: + + '@tweenjs/tween.js@23.1.2': + resolution: {integrity: sha512-kMCNaZCJugWI86xiEHaY338CU5JpD0B97p1j1IKNn/Zto8PgACjQx0UxbHjmOcLl/dDOBnItwD07KmCs75pxtQ==} + + '@types/stats.js@0.17.3': + resolution: {integrity: sha512-pXNfAD3KHOdif9EQXZ9deK82HVNaXP5ZIF5RP2QG6OQFNTaY2YIetfrE9t528vEreGQvEPRDDc8muaoYeK0SxQ==} + + '@types/three@0.166.0': + resolution: {integrity: sha512-FHMnpcdhdbdOOIYbfkTkUVpYMW53odxbTRwd0/xJpYnTzEsjnVnondGAvHZb4z06UW0vo6WPVuvH0/9qrxKx7g==} + + '@types/webxr@0.5.19': + resolution: {integrity: sha512-4hxA+NwohSgImdTSlPXEqDqqFktNgmTXQ05ff1uWam05tNGroCMp4G+4XVl6qWm1p7GQ/9oD41kAYsSssF6Mzw==} + + fflate@0.8.2: + resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} + + meshoptimizer@0.18.1: + resolution: {integrity: sha512-ZhoIoL7TNV4s5B6+rx5mC//fw8/POGyNxS/DZyCJeiZ12ScLfVwRE/GfsxwiTkMYYD5DmK2/JXnEVXqL4rF+Sw==} + + three@0.166.1: + resolution: {integrity: sha512-LtuafkKHHzm61AQA1be2MAYIw1IjmhOUxhBa0prrLpEMWbV7ijvxCRHjSgHPGp2493wLBzwKV46tA9nivLEgKg==} + +snapshots: + + '@tweenjs/tween.js@23.1.2': {} + + '@types/stats.js@0.17.3': {} + + '@types/three@0.166.0': + dependencies: + '@tweenjs/tween.js': 23.1.2 + '@types/stats.js': 0.17.3 + '@types/webxr': 0.5.19 + fflate: 0.8.2 + meshoptimizer: 0.18.1 + + '@types/webxr@0.5.19': {} + + fflate@0.8.2: {} + + meshoptimizer@0.18.1: {} + + three@0.166.1: {} diff --git a/packages/engine/src/assets/index.ts b/packages/engine/src/assets/index.ts index f8179ed..9d97b9d 100644 --- a/packages/engine/src/assets/index.ts +++ b/packages/engine/src/assets/index.ts @@ -1 +1 @@ -export * from './manager'; +export * from './manager.js'; diff --git a/packages/engine/src/assets/manager.ts b/packages/engine/src/assets/manager.ts index d202f3e..1609eb9 100644 --- a/packages/engine/src/assets/manager.ts +++ b/packages/engine/src/assets/manager.ts @@ -1,6 +1,6 @@ import { CubeTexture, CubeTextureLoader, Texture, TextureLoader } from 'three'; -import { Asset, AssetType, AssetsEvents } from '../types/asset'; -import { EventEmitter } from '../utils/events'; +import { Asset, AssetType, AssetsEvents } from '../types/asset.js'; +import { EventEmitter } from '../utils/events.js'; import { GLTF, GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'; export class AssetManagerFactory extends EventEmitter { @@ -12,7 +12,7 @@ export class AssetManagerFactory extends EventEmitter { constructor() { super(); this.on('load', (input) => - Array.isArray(input) ? this.loadAll(input) : this.load(input) + Array.isArray(input) ? this.loadAll(input) : this.load(input), ); } @@ -50,12 +50,16 @@ export class AssetManagerFactory extends EventEmitter { * @param asset Remote or local asset data */ async load(asset: Asset) { + if ((globalThis.FREEBLOX_SIDE = 'server')) { + return asset; + } + this.emit('loadStart', asset); if (asset.type === 'Mesh') { const data = await this.loadMeshData( asset.remote ? asset.path : asset.data, - asset.name + asset.name, ).catch((error) => { this.emit('loadError', error); throw error; @@ -73,11 +77,11 @@ export class AssetManagerFactory extends EventEmitter { asset.type === 'Texture' ? this.loadTextureData( asset.remote ? asset.path : asset.data, - asset.name + asset.name, ) : this.loadCubeTexture( asset.remote ? asset.path : asset.data, - asset.name + asset.name, ) ) .then((texture) => { @@ -97,7 +101,7 @@ export class AssetManagerFactory extends EventEmitter { */ async loadAll(assets: Asset[]) { const loaded = await Promise.allSettled( - assets.map((item) => this.load(item)) + assets.map((item) => this.load(item)), ); loaded @@ -107,7 +111,7 @@ export class AssetManagerFactory extends EventEmitter { this.assets.push( ...loaded .filter((entry) => entry.status === 'fulfilled') - .map((fulfilled) => (fulfilled as PromiseFulfilledResult).value) + .map((fulfilled) => (fulfilled as PromiseFulfilledResult).value), ); } @@ -144,7 +148,7 @@ export class AssetManagerFactory extends EventEmitter { resolve(texture); }, undefined, - reject + reject, ); }); } @@ -163,7 +167,7 @@ export class AssetManagerFactory extends EventEmitter { resolve(texture); }, undefined, - reject + reject, ); }); } diff --git a/packages/engine/src/canvas/index.ts b/packages/engine/src/canvas/index.ts index 04bca77..9be8099 100644 --- a/packages/engine/src/canvas/index.ts +++ b/packages/engine/src/canvas/index.ts @@ -1 +1 @@ -export * from './utils'; +export * from './utils.js'; diff --git a/packages/engine/src/components/environment.ts b/packages/engine/src/components/environment.ts index 43f1b73..08fd4af 100644 --- a/packages/engine/src/components/environment.ts +++ b/packages/engine/src/components/environment.ts @@ -1,10 +1,10 @@ import { AmbientLight, Color, DirectionalLight } from 'three'; -import { EngineEvents, EnvironmentEvent } from '../types/events'; -import { EngineComponent } from '../types/engine-component'; -import { Renderer } from '../types/renderer'; -import { EventEmitter } from '../utils/events'; -import { Environment } from '../gameobjects/environment.object'; -import { environmentDefaults } from '../defaults/environment'; +import { EngineEvents, EnvironmentEvent } from '../types/events.js'; +import { EngineComponent } from '../types/engine-component.js'; +import { Renderer } from '../types/renderer.js'; +import { EventEmitter } from '../utils/events.js'; +import { Environment } from '../gameobjects/environment.object.js'; +import { environmentDefaults } from '../defaults/environment.js'; /** * This component manages game environment and world lighting. @@ -19,7 +19,7 @@ export class EnvironmentComponent extends EngineComponent { constructor( protected renderer: Renderer, - protected events: EventEmitter + protected events: EventEmitter, ) { super(renderer, events); } @@ -30,11 +30,11 @@ export class EnvironmentComponent extends EngineComponent { this.ambient = new AmbientLight( environmentDefaults.ambientColor, - environmentDefaults.ambientStrength + environmentDefaults.ambientStrength, ); this.directional = new DirectionalLight( environmentDefaults.sunColor, - environmentDefaults.sunStrength + environmentDefaults.sunStrength, ); this.directional.position.copy(environmentDefaults.sunPosition); diff --git a/packages/engine/src/components/gameserver.ts b/packages/engine/src/components/gameserver.ts new file mode 100644 index 0000000..a492ebd --- /dev/null +++ b/packages/engine/src/components/gameserver.ts @@ -0,0 +1,303 @@ +import { Object3D } from 'three'; +import { Player } from '../gameobjects/player.object.js'; +import { Players } from '../gameobjects/players.object.js'; +import { ErrorType, Packet, PacketType } from '../net/index.js'; +import { EngineComponent } from '../types/engine-component.js'; +import { ServerEvents } from '../types/events.js'; +import { Renderer } from '../types/renderer.js'; +import { + AuthBackend, + AuthData, + AuthResult, + PlayerSocket, +} from '../types/server.js'; +import { EventEmitter } from '../utils/events.js'; +import { LevelComponent } from './level.js'; +import { PhysicsWorldComponent } from './physicsworld.js'; +import { GameObject } from '../types/game-object.js'; + +/** + * This component manages an authoritative game server host. + */ +export class GameServerComponent extends EngineComponent { + static engineVersionSupportLevels = [0]; + + public name = GameServerComponent.name; + public players = new Players(); + + private cleanUpEvents?: () => void; + + protected level?: LevelComponent; + protected physicsWorld?: PhysicsWorldComponent; + protected authBackend?: AuthBackend; + + constructor( + protected renderer: Renderer, + protected events: EventEmitter, + ) { + super(renderer, events); + } + + /** + * Set the level and physics components we want to synchronize. + * Required to create synchronization snapshots. + * @param level Level manager + * @param physics Physics manager + */ + setSynchronize(level: LevelComponent, physics: PhysicsWorldComponent) { + this.level = level; + this.physicsWorld = physics; + } + + /** + * Use an authentication backend. + * @param auth Authentication backend + */ + useAuthentication(auth: AuthBackend) { + this.authBackend = auth; + } + + protected sendWorldToPlayer(player: Player) { + const packets: Buffer[] = []; + this.level?.world.traverse((object) => { + if (!(object instanceof GameObject)) return; + if (object.virtual) return; + packets.push( + new Packet(PacketType.STREAM_OBJECT) + .write(object.uuid, String) + .write(object.parent?.uuid || '', String) + .write(object.objectType, String) + .write(JSON.stringify(object.serialize()), String) + .toBuffer(), + ); + }); + packets.forEach((packet) => player.send(packet)); + } + + protected getPlayerList() { + return this.players.players.map( + (player) => `${player.playerId}:${player.name}`, + ); + } + + protected sendPlayerInit(player: Player) { + if (!this.level) return; + + // Stream start + player.send( + new Packet(PacketType.STREAM_START) + .write(this.level.world.name, String) + .toBuffer(), + ); + + // TODO: assets + + // Stream world + this.sendWorldToPlayer(player); + + // Send player list + const packet = new Packet(PacketType.PLAYER_LIST); + const players = this.getPlayerList(); + packet.write(players.length, 'uint32'); + players.forEach((player) => packet.write(player, String)); + player.send(packet.toBuffer()); + + // Broadcast join + this.broadcast( + new Packet(PacketType.PLAYER_JOIN) + .write(player.id, String) + .write(player.name, String), + ); + + // Tell player they're ready to play + player.send( + new Packet(PacketType.STREAM_FINISH) + .write(this.level.world.name, String) + .toBuffer(), + ); + } + + /** + * Add a new player into the scene and broadcast the join. + * @param socket Player socket + * @param auth Player auth request + * @param result Player auth response + */ + protected createPlayer( + socket: PlayerSocket, + auth: AuthData, + result: AuthResult, + ) { + const existingPlayer = this.players.getByPlayerId(result.playerId); + if (existingPlayer) { + socket.close( + 1007, + new Packet(PacketType.ERROR) + .write(ErrorType.ALREADY_CONNECTED, 'uint8') + .toBuffer(), + ); + return; + } + + console.log( + socket.id, + 'has successfully authenticated as', + result.playerId, + result.name, + ); + socket.id = result.playerId; + + const playerObject = new Player( + result.playerId, + result.name, + result.displayName, + socket, + ); + this.players.add(playerObject); + this.events.emit('sceneJoin', playerObject); + + console.log(` --> ${playerObject.name} has joined the game.`); + this.sendPlayerInit(playerObject); + } + + protected sendToPlayer(player: Player, packet: Packet) { + player.send(packet.toBuffer()); + } + + protected sendToId(playerId: string, packet: Packet) { + this.players.getByPlayerId(playerId)?.send(packet.toBuffer()); + } + + protected broadcast(packet: Packet) { + const packetBuffer = packet.toBuffer(); + this.players.players.forEach(({ send }) => send(packetBuffer)); + } + + protected broadcastExcept(packet: Packet, exclude: string[]) { + const packetBuffer = packet.toBuffer(); + this.players.players + .filter(({ playerId }) => !exclude.includes(playerId as string)) + .forEach(({ send }) => send(packetBuffer)); + } + + initialize(): void { + this.renderer.scene.add(this.players); + + // Player -> Server packet handling + const handlePlayerPacket = (client: PlayerSocket, buffer: Buffer) => { + const packet = Packet.from(buffer); + switch (packet.packet) { + // *** Player authentication packet + case PacketType.AUTH: { + const playerId = packet.read(String) as string; + const playerToken = packet.read(String) as string; + const clientVersion = packet.read('uint8'); + + // Unsupported client version + if ( + GameServerComponent.engineVersionSupportLevels.includes( + clientVersion as number, + ) + ) { + client.close( + 1007, + new Packet(PacketType.ERROR) + .write(ErrorType.INVALID_CLIENT_VERSION, 'uint8') + .toBuffer(), + ); + return; + } + + const playerData = { + socketId: client.id, + socketIp: client.ip, + playerId, + playerToken, + }; + + // Use authentication backend + if (this.authBackend) { + this.authBackend.authenticate(playerData).then((result) => { + if (!result) { + client.close( + 1007, + new Packet(PacketType.ERROR) + .write(ErrorType.AUTH_FAIL, 'uint8') + .toBuffer(), + ); + return; + } + + this.createPlayer(client, playerData, result); + }); + return; + } + + // No auth specified, just go ahead and create the player + this.createPlayer(client, playerData, { + playerId, + name: `unauth${Date.now()}`, + }); + break; + } + default: { + client.close( + 1007, + new Packet(PacketType.ERROR) + .write(ErrorType.INVALID_PACKET, 'uint8') + .toBuffer(), + ); + } + } + }; + + // Player disconnection handler. + // Removes character and player objects. + const handleDisconnect = (socket: PlayerSocket) => { + const isAuthedPlayer = this.players.getByPlayerId(socket.id); + if (!isAuthedPlayer) return; + if (isAuthedPlayer.character) { + this.events.emit('queueFree', isAuthedPlayer.character); + } + this.events.emit('queueFree', isAuthedPlayer); + console.log(` <-- ${isAuthedPlayer.name} has left the game.`); + }; + + const broadcastSceneJoin = (newObject: Object3D) => { + if (!(newObject instanceof GameObject)) return; + this.broadcast( + new Packet(PacketType.STREAM_OBJECT) + .write(newObject.uuid, String) + .write(newObject.parent?.uuid || '', String) + .write(newObject.objectType, String) + .write(JSON.stringify(newObject.serialize()), String), + ); + }; + + const broadcastSceneLeave = (newObject: Object3D) => { + if (!(newObject instanceof GameObject)) return; + this.broadcast( + new Packet(PacketType.STREAM_DESTROY).write(newObject.uuid, String), + ); + }; + + this.events.addListener('playerPacket', handlePlayerPacket); + this.events.addListener('disconnected', handleDisconnect); + this.events.addListener('sceneJoin', broadcastSceneJoin); + this.events.addListener('sceneLeave', broadcastSceneLeave); + + this.cleanUpEvents = () => { + this.events.removeEventListener('playerPacket', handlePlayerPacket); + this.events.removeEventListener('disconnected', handleDisconnect); + this.events.removeEventListener('sceneJoin', broadcastSceneJoin); + this.events.removeEventListener('sceneLeave', broadcastSceneLeave); + }; + } + + update(delta: number): void {} + + dispose(): void { + this.renderer.scene.remove(this.players); + this.cleanUpEvents?.(); + } +} diff --git a/packages/engine/src/components/index.ts b/packages/engine/src/components/index.ts index 2c176a0..ed363ad 100644 --- a/packages/engine/src/components/index.ts +++ b/packages/engine/src/components/index.ts @@ -1,5 +1,6 @@ -export * from './environment'; -export * from './viewport'; -export * from './mouse'; -export * from './level'; -export * from './physicsworld'; +export * from './environment.js'; +export * from './viewport.js'; +export * from './mouse.js'; +export * from './level.js'; +export * from './physicsworld.js'; +export * from './gameserver.js'; diff --git a/packages/engine/src/components/level.ts b/packages/engine/src/components/level.ts index 85d8be0..46833bb 100644 --- a/packages/engine/src/components/level.ts +++ b/packages/engine/src/components/level.ts @@ -1,21 +1,22 @@ -import { Renderer } from '../types/renderer'; -import { EngineComponent } from '../types/engine-component'; +import { Renderer } from '../types/renderer.js'; +import { EngineComponent } from '../types/engine-component.js'; import { ChangeEvent, EngineEvents, InstanceEvent, RemoveEvent, ReparentEvent, -} from '../types/events'; -import { EventEmitter } from '../utils/events'; -import { Environment } from '../gameobjects/environment.object'; -import { assetManager } from '../assets/manager'; -import { WorldFile } from '../types/world-file'; -import { World } from '../gameobjects/world.object'; -import { PhysicalObject, instancableGameObjects } from '../gameobjects'; +} from '../types/events.js'; +import { EventEmitter } from '../utils/events.js'; +import { Environment } from '../gameobjects/environment.object.js'; +import { assetManager } from '../assets/manager.js'; +import { WorldFile } from '../types/world-file.js'; +import { World } from '../gameobjects/world.object.js'; +import { PhysicalObject } from '../gameobjects/physical.object.js'; import { Object3D } from 'three'; -import { GameObject, SerializedObject } from '../types/game-object'; -import { environmentDefaults } from '../defaults/environment'; +import { GameObject, SerializedObject } from '../types/game-object.js'; +import { environmentDefaults } from '../defaults/environment.js'; +import { instancableGameObjects } from '../gameobjects/index.js'; /** * Game level management component. This component provides the World. @@ -33,7 +34,7 @@ export class LevelComponent extends EngineComponent { constructor( protected renderer: Renderer, - protected events: EventEmitter + protected events: EventEmitter, ) { super(renderer, events); } @@ -41,7 +42,7 @@ export class LevelComponent extends EngineComponent { initialize(): void { this.renderer.scene.add(this.world); this.environment = this.renderer.scene.getObjectByName( - 'Environment' + 'Environment', ) as Environment; this.cleanUpEvents = this.bindEvents(); } diff --git a/packages/engine/src/components/mouse.ts b/packages/engine/src/components/mouse.ts index de2bc0f..3940d74 100644 --- a/packages/engine/src/components/mouse.ts +++ b/packages/engine/src/components/mouse.ts @@ -1,9 +1,9 @@ import { Object3D, Raycaster, Vector2 } from 'three'; -import { EngineComponent } from '../types/engine-component'; -import { Renderer } from '../types/renderer'; -import { EngineEvents } from '../types/events'; -import { EventEmitter } from '../utils/events'; -import { World } from '../gameobjects/world.object'; +import { EngineComponent } from '../types/engine-component.js'; +import { Renderer } from '../types/renderer.js'; +import { EngineEvents } from '../types/events.js'; +import { EventEmitter } from '../utils/events.js'; +import { World } from '../gameobjects/world.object.js'; type MouseMap = [boolean, boolean, boolean]; @@ -30,7 +30,7 @@ export class MouseComponent extends EngineComponent { constructor( protected renderer: Renderer, - protected events: EventEmitter + protected events: EventEmitter, ) { super(renderer, events); } @@ -57,7 +57,7 @@ export class MouseComponent extends EngineComponent { const mouseDown = (ev: MouseEvent) => { const [object, ...rest] = this.ray.intersectObjects( this.world.children, - true + true, ); this.mouseButtons[ev.button] = true; this.events.emit('mouseDown', { @@ -76,7 +76,7 @@ export class MouseComponent extends EngineComponent { const mouseUp = (ev: MouseEvent) => { const [object, ...rest] = this.ray.intersectObjects( this.world.children, - true + true, ); this.mouseButtons[ev.button] = false; this.events.emit('mouseUp', { @@ -99,7 +99,7 @@ export class MouseComponent extends EngineComponent { this.mousePosition.set(ev.clientX, ev.clientY).sub(this.canvasOffset); this.mousepositionNDC.set( (this.mousePosition.x / this.renderer.resolution.x) * 2 - 1, - -(this.mousePosition.y / this.renderer.resolution.y) * 2 + 1 + -(this.mousePosition.y / this.renderer.resolution.y) * 2 + 1, ); this.events.emit('mouseMove', { diff --git a/packages/engine/src/components/physicsworld.ts b/packages/engine/src/components/physicsworld.ts index 850e8d0..92136e4 100644 --- a/packages/engine/src/components/physicsworld.ts +++ b/packages/engine/src/components/physicsworld.ts @@ -1,23 +1,23 @@ import { Object3D, SkinnedMesh, Vector3 } from 'three'; -import { EngineComponent } from '../types/engine-component'; -import { World } from '../gameobjects/world.object'; -import { getRapier, PhysicsTicking } from '../physics'; -import { Humanoid } from '../gameobjects/humanoid.object'; -import { PhysicsObject } from '../gameobjects/physics.object'; -import { GameObject } from '../types/game-object'; +import { EngineComponent } from '../types/engine-component.js'; +import { World } from '../gameobjects/world.object.js'; +import { rapierSource, PhysicsTicking } from '../physics/index.js'; +import { Humanoid } from '../gameobjects/humanoid.object.js'; +import { PhysicsObject } from '../gameobjects/physics.object.js'; +import { GameObject } from '../types/game-object.js'; import type Rapier from '@dimforge/rapier3d'; -import { PlayerEvent, ServerTransformEvent } from '../types/events'; +import { PlayerEvent, ServerTransformEvent } from '../types/events.js'; export class PhysicsWorldComponent extends EngineComponent { public name = PhysicsWorldComponent.name; - private world!: World; - private physicsWorld!: Rapier.World; - private physicsEngine!: typeof Rapier; - private characterPhysics!: Rapier.KinematicCharacterController; - private cleanUpEvents?: () => void; - private sceneInitialized = false; - private trackedObjects: PhysicsTicking[] = []; - private physicsTick?: ReturnType; + protected world!: World; + protected physicsWorld!: Rapier.World; + protected physicsEngine!: typeof Rapier; + protected characterPhysics!: Rapier.KinematicCharacterController; + protected cleanUpEvents?: () => void; + protected sceneInitialized = false; + protected trackedObjects: PhysicsTicking[] = []; + protected physicsTick?: ReturnType; initialize(): void { this.world = this.renderer.scene.getObjectByName('World') as World; @@ -41,7 +41,7 @@ export class PhysicsWorldComponent extends EngineComponent { } private createRapier() { - getRapier().then((physicsEngine) => { + rapierSource.getRapier().then((physicsEngine) => { const gravity = new Vector3(0, this.world.gravity, 0); const world = new physicsEngine.World(gravity); this.physicsWorld = world; @@ -51,7 +51,7 @@ export class PhysicsWorldComponent extends EngineComponent { }); } - private bindEvents() { + protected bindEvents() { const worldLoadEvent = () => { this.createRapier(); }; @@ -66,7 +66,7 @@ export class PhysicsWorldComponent extends EngineComponent { const serverTransformEvent = (event: ServerTransformEvent) => { const object = this.trackedObjects.find( - (obj) => event.object === obj.uuid + (obj) => event.object === obj.uuid, ) as PhysicsObject; if (!object || !object.rigidBody) return; if (object instanceof Humanoid) { @@ -82,7 +82,7 @@ export class PhysicsWorldComponent extends EngineComponent { const characterMoveEvent = (event: PlayerEvent) => { const object = this.trackedObjects.find( - (obj) => event.playerId === obj.uuid + (obj) => event.playerId === obj.uuid, ) as Humanoid; if (!object) return; object.setVelocity(event.velocity); @@ -105,7 +105,7 @@ export class PhysicsWorldComponent extends EngineComponent { }; } - private applyPhysics(root: Object3D) { + protected applyPhysics(root: Object3D) { if (!this.physicsEngine) return; root.traverse((object) => { // Prevent double-init @@ -117,7 +117,7 @@ export class PhysicsWorldComponent extends EngineComponent { object.initializePhysics( this.physicsEngine, this.physicsWorld, - this.characterPhysics + this.characterPhysics, ); this.trackedObjects.push(object); return; @@ -138,7 +138,7 @@ export class PhysicsWorldComponent extends EngineComponent { }); } - private removePhysics(root: Object3D) { + protected removePhysics(root: Object3D) { const trackedObjects = [...this.trackedObjects]; const physicsLeaveUUIDs: string[] = []; @@ -153,11 +153,11 @@ export class PhysicsWorldComponent extends EngineComponent { }); this.trackedObjects = this.trackedObjects.filter( - (object) => !physicsLeaveUUIDs.includes(object.uuid) + (object) => !physicsLeaveUUIDs.includes(object.uuid), ); } - private async initializePhysicsScene() { + protected async initializePhysicsScene() { if (this.sceneInitialized) return; this.characterPhysics = this.physicsWorld.createCharacterController(0.01); diff --git a/packages/engine/src/components/viewport.ts b/packages/engine/src/components/viewport.ts index 41df1de..8d4ad49 100644 --- a/packages/engine/src/components/viewport.ts +++ b/packages/engine/src/components/viewport.ts @@ -1,8 +1,8 @@ import { Vector2 } from 'three'; -import { EngineEvents } from '../types/events'; -import { EngineComponent } from '../types/engine-component'; -import { EventEmitter } from '../utils/events'; -import { Renderer } from '../types/renderer'; +import { EngineEvents } from '../types/events.js'; +import { EngineComponent } from '../types/engine-component.js'; +import { EventEmitter } from '../utils/events.js'; +import { Renderer } from '../types/renderer.js'; /** * Manage viewport sizing @@ -13,9 +13,10 @@ export class ViewportComponent extends EngineComponent { constructor( protected render: Renderer, - protected events: EventEmitter + protected events: EventEmitter, ) { super(render, events); + if (!this.render.viewport) throw new Error('Unsupported component'); } get scene() { @@ -38,15 +39,15 @@ export class ViewportComponent extends EngineComponent { } setSize(width: number, height: number) { - this.render.viewport.style.width = `${width}px`; - this.render.viewport.style.height = `${height}px`; + this.render.viewport!.style.width = `${width}px`; + this.render.viewport!.style.height = `${height}px`; this.render.setSize(width, height); } setSizeFromWindow() { this.events.emit( 'resize', - new Vector2(window.innerWidth, window.innerHeight) + new Vector2(window.innerWidth, window.innerHeight), ); } @@ -54,9 +55,9 @@ export class ViewportComponent extends EngineComponent { this.events.emit( 'resize', new Vector2( - this.render.viewport.parentElement!.clientWidth, - this.render.viewport.parentElement!.clientHeight - ) + this.render.viewport!.parentElement!.clientWidth, + this.render.viewport!.parentElement!.clientHeight, + ), ); } diff --git a/packages/engine/src/controls/index.ts b/packages/engine/src/controls/index.ts index a779f85..fd896ca 100644 --- a/packages/engine/src/controls/index.ts +++ b/packages/engine/src/controls/index.ts @@ -1,3 +1,3 @@ -export * from './camera-controls'; -export * from './transform-controls'; -export * from './third-person-camera'; +export * from './camera-controls.js'; +export * from './transform-controls.js'; +export * from './third-person-camera.js'; diff --git a/packages/engine/src/controls/third-person-camera.ts b/packages/engine/src/controls/third-person-camera.ts index 0edd8be..8eb0343 100644 --- a/packages/engine/src/controls/third-person-camera.ts +++ b/packages/engine/src/controls/third-person-camera.ts @@ -1,10 +1,5 @@ -import { - Vector3, - Vector2, - PerspectiveCamera, - Object3D, -} from 'three'; -import { clamp, degToRad } from 'three/src/math/MathUtils.js'; +import { Vector3, Vector2, PerspectiveCamera, Object3D } from 'three'; +import { clamp, degToRad } from '../utils/math.js'; export class ThirdPersonCamera { private currentPosition = new Vector3(); @@ -31,7 +26,7 @@ export class ThirdPersonCamera { private camera: PerspectiveCamera, private target: Object3D, private eventTarget: HTMLElement, - private cameraOrignOffset = new Vector3(0, 4.75, 0) + private cameraOrignOffset = new Vector3(0, 4.75, 0), ) {} private dragEvent = (x: number, y: number) => { @@ -99,7 +94,7 @@ export class ThirdPersonCamera { if (ev.touches.length === 2 && this.pinching) { const pinchLength = Math.hypot( ev.touches[0].pageX - ev.touches[1].pageX, - ev.touches[0].pageY - ev.touches[1].pageY + ev.touches[0].pageY - ev.touches[1].pageY, ); if (this.previousPinchLength) { @@ -172,7 +167,7 @@ export class ThirdPersonCamera { this.offsetFromPlayer.set( hdist * Math.sin(degToRad(this.angleAroundPlayer)), -vdist, - hdist * Math.cos(degToRad(this.angleAroundPlayer)) + hdist * Math.cos(degToRad(this.angleAroundPlayer)), ); } diff --git a/packages/engine/src/core/engine.ts b/packages/engine/src/core/engine.ts index 48f5547..3d37c0e 100644 --- a/packages/engine/src/core/engine.ts +++ b/packages/engine/src/core/engine.ts @@ -1,17 +1,16 @@ -import { WebGLRenderer } from '../render/webgl-renderer'; -import { EngineComponent } from '../types/engine-component'; -import { EngineEvents } from '../types/events'; -import { GameRunner } from '../types/game-runner'; -import { Instancable } from '../types/instancable'; -import { Renderer } from '../types/renderer'; -import { EventEmitter } from '../utils/events'; +import { WebGLRenderer } from '../render/webgl-renderer.js'; +import { EngineComponent } from '../types/engine-component.js'; +import { EngineEvents } from '../types/events.js'; +import { GameRunner } from '../types/game-runner.js'; +import { Instancable } from '../types/instancable.js'; +import { Renderer } from '../types/renderer.js'; +import { EventEmitter } from '../utils/events.js'; export class Engine extends GameRunner { public lastTick = performance.now(); public running = false; public events!: EventEmitter; public render!: Renderer; - public element!: HTMLElement; public components: EngineComponent[] = []; static engineSingleton: Engine; @@ -25,12 +24,13 @@ export class Engine extends GameRunner { * Get global engine instance */ public static getInstance() { - return (Engine.engineSingleton || (Engine.engineSingleton = new Engine())) as E; + return (Engine.engineSingleton || + (Engine.engineSingleton = new Engine())) as E; } override mount(element: HTMLElement): void { - this.element = element; - this.render = new WebGLRenderer(element); + this.render = new WebGLRenderer(); + (this.render as WebGLRenderer).mount(element); } override loop(now: number): void { diff --git a/packages/engine/src/core/index.ts b/packages/engine/src/core/index.ts index 7a6a328..e35ce04 100644 --- a/packages/engine/src/core/index.ts +++ b/packages/engine/src/core/index.ts @@ -1 +1 @@ -export * from './engine'; +export * from './engine.js'; diff --git a/packages/engine/src/decorators/index.ts b/packages/engine/src/decorators/index.ts index 363875b..e07641b 100644 --- a/packages/engine/src/decorators/index.ts +++ b/packages/engine/src/decorators/index.ts @@ -1 +1 @@ -export * from './property'; +export * from './property.js'; diff --git a/packages/engine/src/decorators/property.ts b/packages/engine/src/decorators/property.ts index e8e2598..a386033 100644 --- a/packages/engine/src/decorators/property.ts +++ b/packages/engine/src/decorators/property.ts @@ -1,17 +1,17 @@ import 'reflect-metadata'; -import { Property, PropertyDefinition } from '../types/property'; +import { Property, PropertyDefinition } from '../types/property.js'; /** * Register an exposed property. This is displayed in the editor and serialized to files. * @param editorPropertyDefinition Additional options */ export function ExposeProperty( - editorPropertyDefinition?: Omit + editorPropertyDefinition?: Omit, ): PropertyDecorator { return (target, propertyKey): void => { let properties: Array = Reflect.getOwnMetadata( 'properties', - target + target, ); if (!properties) { @@ -19,7 +19,7 @@ export function ExposeProperty( } properties.push( - new Property({ ...editorPropertyDefinition, name: String(propertyKey) }) + new Property({ ...editorPropertyDefinition, name: String(propertyKey) }), ); }; } @@ -31,7 +31,7 @@ export function RetractProperty(): PropertyDecorator { return (target, propertyKey): void => { let excluded: Array = Reflect.getOwnMetadata( 'excludedProperties', - target + target, ); if (!excluded) { diff --git a/packages/engine/src/gameobjects/brick.object.ts b/packages/engine/src/gameobjects/brick.object.ts index e41c935..ed102bc 100644 --- a/packages/engine/src/gameobjects/brick.object.ts +++ b/packages/engine/src/gameobjects/brick.object.ts @@ -1,5 +1,5 @@ -import { PhysicsObject } from './physics.object'; -import { gameObjectGeometries } from './geometries'; +import { PhysicsObject } from './physics.object.js'; +import { gameObjectGeometries } from './geometries.js'; import { BufferGeometry, NormalBufferAttributes, Vector3 } from 'three'; import type Rapier from '@dimforge/rapier3d'; @@ -12,13 +12,13 @@ export class Brick extends PhysicsObject { protected override createCollider( factory: typeof Rapier, world: Rapier.World, - body?: Rapier.RigidBody + body?: Rapier.RigidBody, ) { const scale = this.getWorldScale(new Vector3()).divideScalar(2); const collider = factory.ColliderDesc.cuboid( Math.abs(scale.x), Math.abs(scale.y), - Math.abs(scale.z) + Math.abs(scale.z), ); return world.createCollider(collider, body); } diff --git a/packages/engine/src/gameobjects/capsule.object.ts b/packages/engine/src/gameobjects/capsule.object.ts index 8555260..52fc311 100644 --- a/packages/engine/src/gameobjects/capsule.object.ts +++ b/packages/engine/src/gameobjects/capsule.object.ts @@ -1,6 +1,6 @@ import { Vector3 } from 'three'; -import { Brick } from './brick.object'; -import { gameObjectGeometries } from './geometries'; +import { Brick } from './brick.object.js'; +import { gameObjectGeometries } from './geometries.js'; import type Rapier from '@dimforge/rapier3d'; export class Capsule extends Brick { @@ -15,7 +15,7 @@ export class Capsule extends Brick { protected override createCollider( factory: typeof Rapier, world: Rapier.World, - body?: Rapier.RigidBody + body?: Rapier.RigidBody, ) { const scale = this.getWorldScale(new Vector3()); const height = Math.abs(scale.y) / 2; diff --git a/packages/engine/src/gameobjects/cylinder.object.ts b/packages/engine/src/gameobjects/cylinder.object.ts index d6c31b1..67089a5 100644 --- a/packages/engine/src/gameobjects/cylinder.object.ts +++ b/packages/engine/src/gameobjects/cylinder.object.ts @@ -1,6 +1,6 @@ import { Vector3 } from 'three'; -import { Brick } from './brick.object'; -import { gameObjectGeometries } from './geometries'; +import { Brick } from './brick.object.js'; +import { gameObjectGeometries } from './geometries.js'; import type Rapier from '@dimforge/rapier3d'; export class Cylinder extends Brick { @@ -15,7 +15,7 @@ export class Cylinder extends Brick { protected override createCollider( factory: typeof Rapier, world: Rapier.World, - body?: Rapier.RigidBody + body?: Rapier.RigidBody, ) { const scale = this.getWorldScale(new Vector3()); const height = Math.abs(scale.y) / 2; diff --git a/packages/engine/src/gameobjects/environment.object.ts b/packages/engine/src/gameobjects/environment.object.ts index 5c8f3e1..2c2d8ac 100644 --- a/packages/engine/src/gameobjects/environment.object.ts +++ b/packages/engine/src/gameobjects/environment.object.ts @@ -1,7 +1,7 @@ import { Color, Vector3 } from 'three'; -import { GameObject, SerializedObject } from '../types/game-object'; -import { environmentDefaults } from '../defaults/environment'; -import { ExposeProperty, RetractProperty } from '../decorators/property'; +import { GameObject, SerializedObject } from '../types/game-object.js'; +import { environmentDefaults } from '../defaults/environment.js'; +import { ExposeProperty, RetractProperty } from '../decorators/property.js'; export class Environment extends GameObject { public objectType = 'Environment'; diff --git a/packages/engine/src/gameobjects/group.object.ts b/packages/engine/src/gameobjects/group.object.ts index ccc9ee4..1d3cf34 100644 --- a/packages/engine/src/gameobjects/group.object.ts +++ b/packages/engine/src/gameobjects/group.object.ts @@ -1,4 +1,4 @@ -import { GameObject3D } from '../types/game-object'; +import { GameObject3D } from '../types/game-object.js'; export class Group extends GameObject3D { public objectType = 'Group'; diff --git a/packages/engine/src/gameobjects/humanoid.object.ts b/packages/engine/src/gameobjects/humanoid.object.ts index efac3fb..4fa2ae2 100644 --- a/packages/engine/src/gameobjects/humanoid.object.ts +++ b/packages/engine/src/gameobjects/humanoid.object.ts @@ -9,15 +9,15 @@ import { SkinnedMesh, Vector3, } from 'three'; -import { GameObject } from '../types/game-object'; -import { ExposeProperty } from '../decorators/property'; -import { MeshPart } from './mesh.object'; +import { GameObject } from '../types/game-object.js'; +import { ExposeProperty } from '../decorators/property.js'; +import { MeshPart } from './mesh.object.js'; import { clamp } from 'three/src/math/MathUtils.js'; -import { NameTag } from './nametag.object'; -import { CanvasUtils } from '../canvas/utils'; -import { PhysicsTicking } from '../physics'; +import { NameTag } from './nametag.object.js'; +import { CanvasUtils } from '../canvas/utils.js'; +import { PhysicsTicking } from '../physics/index.js'; import type Rapier from '@dimforge/rapier3d'; -import { ServerTransformEvent } from '../types/events'; +import { ServerTransformEvent } from '../types/events.js'; export class Humanoid extends GameObject implements PhysicsTicking { public isTickingObject = true; @@ -135,14 +135,14 @@ export class Humanoid extends GameObject implements PhysicsTicking { getBoneForBodyPart(part: string) { return this.skeleton.getBoneByName( - this.bodyBoneNames[this.bodyPartNames.indexOf(part)] + this.bodyBoneNames[this.bodyPartNames.indexOf(part)], ); } initializePhysics( physicsEngine: typeof Rapier, physicsWorld: Rapier.World, - characterController: Rapier.KinematicCharacterController + characterController: Rapier.KinematicCharacterController, ): void { if (!this.parent) throw new Error('Cannot initialize Humanoid to empty parent'); @@ -180,7 +180,7 @@ export class Humanoid extends GameObject implements PhysicsTicking { const colliderDesc = physicsEngine.ColliderDesc.cuboid( 1, this.characterHalfHeight, - 0.5 + 0.5, ); const rigidBodyDesc = physicsEngine.RigidBodyDesc.kinematicPositionBased() .setTranslation(...this.parent!.position.toArray()) @@ -243,7 +243,7 @@ export class Humanoid extends GameObject implements PhysicsTicking { // Apply velocity this.applyVelocity( - this._velocity.clone().add(this._appliedGravity).multiplyScalar(dt) + this._velocity.clone().add(this._appliedGravity).multiplyScalar(dt), ); // Apply look direction to the physics engine @@ -253,7 +253,7 @@ export class Humanoid extends GameObject implements PhysicsTicking { .lookAt( this.parent!.position, this.parent!.position.clone().sub(this._lookAt), - new Vector3(0, 1, 0) + new Vector3(0, 1, 0), ) .decompose(sink, rotQuat, sink); this.rigidBody?.setRotation(rotQuat, false); @@ -321,7 +321,7 @@ export class Humanoid extends GameObject implements PhysicsTicking { const vec3 = this.parent.position.clone(); this.characterControllerRef.computeColliderMovement( this.collider!, - velocity + velocity, ); const computed = this.characterControllerRef.computedMovement(); const grounded = this.characterControllerRef.computedGrounded(); diff --git a/packages/engine/src/gameobjects/index.ts b/packages/engine/src/gameobjects/index.ts index 20d1888..0eae638 100644 --- a/packages/engine/src/gameobjects/index.ts +++ b/packages/engine/src/gameobjects/index.ts @@ -1,16 +1,16 @@ -import { Cylinder } from './cylinder.object'; -import { Brick } from './brick.object'; -import { Sphere } from './sphere.object'; -import { Wedge } from './wedge.object'; -import { WedgeCorner } from './wedge-corner.object'; -import { WedgeInnerCorner } from './wedge-inner-corner.object'; -import { GameObject } from '../types/game-object'; -import { Instancable } from '../types/instancable'; -import { Group } from './group.object'; -import { Torus } from './torus.object'; -import { Capsule } from './capsule.object'; -import { MeshPart } from './mesh.object'; -import { Humanoid } from './humanoid.object'; +import { Cylinder } from './cylinder.object.js'; +import { Brick } from './brick.object.js'; +import { Sphere } from './sphere.object.js'; +import { Wedge } from './wedge.object.js'; +import { WedgeCorner } from './wedge-corner.object.js'; +import { WedgeInnerCorner } from './wedge-inner-corner.object.js'; +import { GameObject } from '../types/game-object.js'; +import { Instancable } from '../types/instancable.js'; +import { Group } from './group.object.js'; +import { Torus } from './torus.object.js'; +import { Capsule } from './capsule.object.js'; +import { MeshPart } from './mesh.object.js'; +import { Humanoid } from './humanoid.object.js'; export const instancableGameObjects: Record> = { ['Group']: Group, @@ -24,11 +24,11 @@ export const instancableGameObjects: Record> = { ['WedgeInnerCorner']: WedgeInnerCorner, }; -export * from './environment.object'; -export * from './world.object'; -export * from './nametag.object'; -export * from './physical.object'; -export * from './physics.object'; +export * from './environment.object.js'; +export * from './world.object.js'; +export * from './nametag.object.js'; +export * from './physical.object.js'; +export * from './physics.object.js'; export { Group, Cylinder, diff --git a/packages/engine/src/gameobjects/mesh.object.ts b/packages/engine/src/gameobjects/mesh.object.ts index 5674663..191e1fb 100644 --- a/packages/engine/src/gameobjects/mesh.object.ts +++ b/packages/engine/src/gameobjects/mesh.object.ts @@ -1,5 +1,5 @@ import { MeshPhongMaterial, SkinnedMesh, Vector3 } from 'three'; -import { Brick } from '.'; +import { Brick } from '../gameobjects/brick.object.js'; import type Rapier from '@dimforge/rapier3d'; export class MeshPart extends Brick { @@ -20,13 +20,13 @@ export class MeshPart extends Brick { protected override createCollider( factory: typeof Rapier, world: Rapier.World, - body?: Rapier.RigidBody + body?: Rapier.RigidBody, ) { const scale = this.getWorldScale(new Vector3()).divideScalar(2); const collider = factory.ColliderDesc.cuboid( Math.abs(scale.x), Math.abs(scale.y), - Math.abs(scale.z) + Math.abs(scale.z), ); return world.createCollider(collider, body); } diff --git a/packages/engine/src/gameobjects/nametag.object.ts b/packages/engine/src/gameobjects/nametag.object.ts index 80c0cfb..20687e9 100644 --- a/packages/engine/src/gameobjects/nametag.object.ts +++ b/packages/engine/src/gameobjects/nametag.object.ts @@ -1,7 +1,7 @@ import { Sprite, CanvasTexture, SpriteMaterial } from 'three'; -import { CanvasUtils } from '../canvas'; -import { Disposable } from '../types/disposable'; -import { GameObject } from '../types/game-object'; +import { CanvasUtils } from '../canvas/index.js'; +import { Disposable } from '../types/disposable.js'; +import { GameObject } from '../types/game-object.js'; export class NameTag extends GameObject implements Disposable { public objectType = 'NameTag'; @@ -17,7 +17,7 @@ export class NameTag extends GameObject implements Disposable { const { texture, width, height } = builder.createTextCanvas( name, false, - 48 + 48, ); const obj = new NameTag(); @@ -28,6 +28,7 @@ export class NameTag extends GameObject implements Disposable { obj.material = new SpriteMaterial({ map: texture, transparent: true, + depthTest: false, }); const label = new Sprite(obj.material); diff --git a/packages/engine/src/gameobjects/physical.object.ts b/packages/engine/src/gameobjects/physical.object.ts index 5423a09..cb579c6 100644 --- a/packages/engine/src/gameobjects/physical.object.ts +++ b/packages/engine/src/gameobjects/physical.object.ts @@ -1,4 +1,4 @@ -import { GameObject3D, IGameObject3D } from '../types/game-object'; +import { GameObject3D, IGameObject3D } from '../types/game-object.js'; import { BufferGeometry, Color, @@ -6,9 +6,9 @@ import { Mesh, MeshPhongMaterial, } from 'three'; -import { assetManager } from '../assets/manager'; -import { AssetInfo } from '../types/asset'; -import { ExposeProperty } from '../decorators/property'; +import { assetManager } from '../assets/manager.js'; +import { AssetInfo } from '../types/asset.js'; +import { ExposeProperty } from '../decorators/property.js'; export interface IPhysicalObject extends IGameObject3D { color: ColorRepresentation; diff --git a/packages/engine/src/gameobjects/physics.object.ts b/packages/engine/src/gameobjects/physics.object.ts index b020a7d..831e8c9 100644 --- a/packages/engine/src/gameobjects/physics.object.ts +++ b/packages/engine/src/gameobjects/physics.object.ts @@ -1,9 +1,9 @@ -import { PhysicsTicking } from '../physics/ticking'; +import { PhysicsTicking } from '../physics/ticking.js'; import type Rapier from '@dimforge/rapier3d'; -import { PhysicalObject } from './physical.object'; +import { PhysicalObject } from './physical.object.js'; import { Quaternion, Vector3 } from 'three'; -import { ExposeProperty } from '../decorators/property'; -import { GameObject } from '../types/game-object'; +import { ExposeProperty } from '../decorators/property.js'; +import { GameObject } from '../types/game-object.js'; export class PhysicsObject extends PhysicalObject implements PhysicsTicking { public objectType = 'PhysicsObject'; @@ -31,7 +31,7 @@ export class PhysicsObject extends PhysicalObject implements PhysicsTicking { initializePhysics( physicsEngine: typeof Rapier, - physicsWorld: Rapier.World + physicsWorld: Rapier.World, ): void { if (this.virtual) return; this.physicsWorldRef = physicsWorld; @@ -101,7 +101,7 @@ export class PhysicsObject extends PhysicalObject implements PhysicsTicking { protected createCollider( factory: typeof Rapier, world: Rapier.World, - body?: Rapier.RigidBody + body?: Rapier.RigidBody, ): Rapier.Collider | undefined { return undefined; } diff --git a/packages/engine/src/gameobjects/player.object.ts b/packages/engine/src/gameobjects/player.object.ts new file mode 100644 index 0000000..15650a9 --- /dev/null +++ b/packages/engine/src/gameobjects/player.object.ts @@ -0,0 +1,39 @@ +import { ExposeProperty } from '../decorators/property.js'; +import { GameObject } from '../types/game-object.js'; +import { PlayerSocket } from '../types/server.js'; + +export class Player extends GameObject { + public objectType = 'Player'; + public name = 'Player'; + public archivable = false; + public virtual = true; + public visible = false; + + protected socket?: PlayerSocket; + + @ExposeProperty({ type: GameObject, exposed: true, readonly: true }) + public character?: GameObject; + + @ExposeProperty({ type: String, exposed: true, readonly: true }) + public playerId?: string; + + @ExposeProperty({ type: String, exposed: true, readonly: true }) + public username?: string; + + constructor( + id: string, + name: string, + displayName?: string, + socket?: PlayerSocket, + ) { + super(); + this.playerId = id; + this.name = displayName || name; + this.username = name; + this.socket = socket; + } + + send(data: Buffer) { + this.socket?.sendPacket(data); + } +} diff --git a/packages/engine/src/gameobjects/players.object.ts b/packages/engine/src/gameobjects/players.object.ts new file mode 100644 index 0000000..2c80ddd --- /dev/null +++ b/packages/engine/src/gameobjects/players.object.ts @@ -0,0 +1,22 @@ +import { GameObject } from '../types/game-object.js'; +import { Player } from './player.object.js'; + +export class Players extends GameObject { + public objectType = 'Players'; + public name = 'Players'; + public archivable = false; + public virtual = true; + public visible = false; + + public getByPlayerId(playerId: string) { + return this.getObjectByProperty('playerId', playerId) as Player; + } + + public getByCharacter(character: GameObject) { + return this.getObjectByProperty('character', character); + } + + public get players() { + return this.children.filter((object) => object instanceof Player) as Player[]; + } +} diff --git a/packages/engine/src/gameobjects/sphere.object.ts b/packages/engine/src/gameobjects/sphere.object.ts index 3bd2f1d..5200773 100644 --- a/packages/engine/src/gameobjects/sphere.object.ts +++ b/packages/engine/src/gameobjects/sphere.object.ts @@ -1,6 +1,6 @@ import { Vector3 } from 'three'; -import { Brick } from './brick.object'; -import { gameObjectGeometries } from './geometries'; +import { Brick } from './brick.object.js'; +import { gameObjectGeometries } from './geometries.js'; import type Rapier from '@dimforge/rapier3d'; export class Sphere extends Brick { @@ -15,7 +15,7 @@ export class Sphere extends Brick { protected override createCollider( factory: typeof Rapier, world: Rapier.World, - body?: Rapier.RigidBody + body?: Rapier.RigidBody, ) { const scale = this.getWorldScale(new Vector3()); const radius = diff --git a/packages/engine/src/gameobjects/torus.object.ts b/packages/engine/src/gameobjects/torus.object.ts index 7850902..25e7823 100644 --- a/packages/engine/src/gameobjects/torus.object.ts +++ b/packages/engine/src/gameobjects/torus.object.ts @@ -1,6 +1,6 @@ import { Quaternion, Vector3 } from 'three'; -import { Brick } from './brick.object'; -import { gameObjectGeometries } from './geometries'; +import { Brick } from './brick.object.js'; +import { gameObjectGeometries } from './geometries.js'; import type Rapier from '@dimforge/rapier3d'; export class Torus extends Brick { @@ -15,16 +15,16 @@ export class Torus extends Brick { protected override createCollider( factory: typeof Rapier, world: Rapier.World, - body?: Rapier.RigidBody + body?: Rapier.RigidBody, ) { const scale = this.getWorldScale(new Vector3()); const height = Math.abs(scale.y) * 0.4; const radius = (Math.abs(scale.x) + Math.abs(scale.z)) / 2; const collider = factory.ColliderDesc.cylinder( height, - radius * 1.4 + radius * 1.4, ).setRotation( - new Quaternion().setFromAxisAngle(new Vector3(1, 0, 0), Math.PI / 2) + new Quaternion().setFromAxisAngle(new Vector3(1, 0, 0), Math.PI / 2), ); return world.createCollider(collider, body); } diff --git a/packages/engine/src/gameobjects/wedge-corner.object.ts b/packages/engine/src/gameobjects/wedge-corner.object.ts index 8b05ac5..be3e479 100644 --- a/packages/engine/src/gameobjects/wedge-corner.object.ts +++ b/packages/engine/src/gameobjects/wedge-corner.object.ts @@ -1,6 +1,6 @@ import { Matrix4, Vector3 } from 'three'; -import { Brick } from './brick.object'; -import { gameObjectGeometries } from './geometries'; +import { Brick } from './brick.object.js'; +import { gameObjectGeometries } from './geometries.js'; import type Rapier from '@dimforge/rapier3d'; export class WedgeCorner extends Brick { @@ -15,7 +15,7 @@ export class WedgeCorner extends Brick { protected override createCollider( factory: typeof Rapier, world: Rapier.World, - body?: Rapier.RigidBody + body?: Rapier.RigidBody, ) { const scale = this.getWorldScale(new Vector3()); const mat = new Matrix4(); diff --git a/packages/engine/src/gameobjects/wedge-inner-corner.object.ts b/packages/engine/src/gameobjects/wedge-inner-corner.object.ts index 529d0b4..7b72696 100644 --- a/packages/engine/src/gameobjects/wedge-inner-corner.object.ts +++ b/packages/engine/src/gameobjects/wedge-inner-corner.object.ts @@ -1,6 +1,6 @@ import { Matrix4, Vector3 } from 'three'; -import { Brick } from './brick.object'; -import { gameObjectGeometries } from './geometries'; +import { Brick } from './brick.object.js'; +import { gameObjectGeometries } from './geometries.js'; import type Rapier from '@dimforge/rapier3d'; export class WedgeInnerCorner extends Brick { @@ -15,7 +15,7 @@ export class WedgeInnerCorner extends Brick { protected override createCollider( factory: typeof Rapier, world: Rapier.World, - body?: Rapier.RigidBody + body?: Rapier.RigidBody, ) { const scale = this.getWorldScale(new Vector3()); const mat = new Matrix4(); diff --git a/packages/engine/src/gameobjects/wedge.object.ts b/packages/engine/src/gameobjects/wedge.object.ts index e00c151..ac394ee 100644 --- a/packages/engine/src/gameobjects/wedge.object.ts +++ b/packages/engine/src/gameobjects/wedge.object.ts @@ -1,6 +1,6 @@ import { Matrix4, Vector3 } from 'three'; -import { Brick } from './brick.object'; -import { gameObjectGeometries } from './geometries'; +import { Brick } from './brick.object.js'; +import { gameObjectGeometries } from './geometries.js'; import type Rapier from '@dimforge/rapier3d'; export class Wedge extends Brick { @@ -15,7 +15,7 @@ export class Wedge extends Brick { protected override createCollider( factory: typeof Rapier, world: Rapier.World, - body?: Rapier.RigidBody + body?: Rapier.RigidBody, ) { const scale = this.getWorldScale(new Vector3()); const mat = new Matrix4(); diff --git a/packages/engine/src/gameobjects/world.object.ts b/packages/engine/src/gameobjects/world.object.ts index eafb993..240cf03 100644 --- a/packages/engine/src/gameobjects/world.object.ts +++ b/packages/engine/src/gameobjects/world.object.ts @@ -1,5 +1,5 @@ -import { ExposeProperty } from '../decorators/property'; -import { GameObject } from '../types/game-object'; +import { ExposeProperty } from '../decorators/property.js'; +import { GameObject } from '../types/game-object.js'; export class World extends GameObject { public objectType = 'World'; diff --git a/packages/engine/src/index.ts b/packages/engine/src/index.ts index a2f03d4..fd06061 100644 --- a/packages/engine/src/index.ts +++ b/packages/engine/src/index.ts @@ -16,15 +16,16 @@ globalThis.FREEBLOX_SIDE = 'client'; globalThis.FREEBLOX_THREAD = 'main'; globalThis.Buffer = BufferPolyfill; -export * from './core'; -export * from './render'; -export * from './net'; -export * from './net/socket'; -export * from './utils'; -export * from './types'; -export * from './components'; -export * from './gameobjects'; -export * from './assets'; -export * from './defaults/environment'; -export * from './decorators'; -export * from './controls'; +export * from './core/index.js'; +export * from './render/index.js'; +export * from './physics/index.js'; +export * from './net/index.js'; +export * from './net/socket.js'; +export * from './utils/index.js'; +export * from './types/index.js'; +export * from './components/index.js'; +export * from './gameobjects/index.js'; +export * from './assets/index.js'; +export * from './defaults/environment.js'; +export * from './decorators/index.js'; +export * from './controls/index.js'; diff --git a/packages/engine/src/net/error-type.enum.ts b/packages/engine/src/net/error-type.enum.ts index 7d41f64..b2e5827 100644 --- a/packages/engine/src/net/error-type.enum.ts +++ b/packages/engine/src/net/error-type.enum.ts @@ -1,3 +1,6 @@ export enum ErrorType { - AUTH_FAIL = 0, + INVALID_PACKET = 0, + INVALID_CLIENT_VERSION, + AUTH_FAIL, + ALREADY_CONNECTED, } diff --git a/packages/engine/src/net/index.ts b/packages/engine/src/net/index.ts index 4dac6f9..6848265 100644 --- a/packages/engine/src/net/index.ts +++ b/packages/engine/src/net/index.ts @@ -1,4 +1,4 @@ -export * from './packet-type.enum'; -export * from './chat-type.enum'; -export * from './error-type.enum'; -export * from './packet'; +export * from './packet-type.enum.js'; +export * from './chat-type.enum.js'; +export * from './error-type.enum.js'; +export * from './packet.js'; diff --git a/packages/engine/src/net/packet-type.enum.ts b/packages/engine/src/net/packet-type.enum.ts index 38df5a2..b94f2ab 100644 --- a/packages/engine/src/net/packet-type.enum.ts +++ b/packages/engine/src/net/packet-type.enum.ts @@ -1,66 +1,98 @@ export enum PacketType { /** + * Client -> Server + * * [][Player ID][Session Token][Client Version] */ AUTH = 0, KEEPALIVE, /** + * Server -> Client + * * [][World name] */ STREAM_START, /** + * Server -> Client + * * [][Asset Path][Asset Name][Asset Type][Buffer] */ STREAM_ASSET, /** + * Server -> Client + * * [][Object UUID][Parent UUID][Object Type][Object data JSON] */ STREAM_OBJECT, /** + * Server -> Client + * * [][Object UUID] */ STREAM_DESTROY, /** + * Server -> Client + * * [][Object UUID][Event][JSON] */ STREAM_EVENT, /** + * Server -> Client + * * [][World name] */ STREAM_FINISH, /** + * Server -> Client + * * [][UUID][Sequence ID][x y z Position][x y z w Rotation][x y z linvel][x y z angvel] */ STREAM_TRANSFORM, /** + * Server -> Client + * * [][Player ID][Chat Type][Chat Message] */ STREAM_CHAT, /** + * Server -> Client + * * [][Player count][Player ID:Player Name] */ PLAYER_LIST, /** + * Server -> Client + * * [][Player ID][Player Name] */ PLAYER_JOIN, /** + * Server -> Client + * * [][Player ID][Player Name][Reason] */ PLAYER_QUIT, /** + * Server -> Client + * * [][Player ID][Player Name][x y z Position][x y z w quaternion][Object UUID][Data...] */ PLAYER_CHARACTER, /** + * Client -> Server + * * [][Sequence ID][Player ID][Velocity][LookAt][Jump][AnimState] */ PLAYER_MOVEMENT, /** + * Client -> Server + * * [][Player ID][Chat Type][Chat Message] */ PLAYER_CHAT, /** + * Client -> Server + * * [][Sequence ID][Player ID][Object UUID][Event][JSON] */ PLAYER_EVENT, diff --git a/packages/engine/src/net/packet.ts b/packages/engine/src/net/packet.ts index cc1ab8e..e258435 100644 --- a/packages/engine/src/net/packet.ts +++ b/packages/engine/src/net/packet.ts @@ -1,6 +1,6 @@ -import { PacketType } from './packet-type.enum'; +import { PacketType } from './packet-type.enum.js'; import { SmartBuffer } from 'smart-buffer'; -import { Instancable } from '../types/instancable'; +import { Instancable } from '../types/instancable.js'; import { Quaternion, Vector3 } from 'three'; export class Packet { @@ -66,14 +66,14 @@ export class Packet { return new Vector3( this.buffer.readFloatLE(), this.buffer.readFloatLE(), - this.buffer.readFloatLE() + this.buffer.readFloatLE(), ) as T; case 'quat': return new Quaternion( this.buffer.readFloatLE(), this.buffer.readFloatLE(), this.buffer.readFloatLE(), - this.buffer.readFloatLE() + this.buffer.readFloatLE(), ) as T; } } diff --git a/packages/engine/src/net/socket.ts b/packages/engine/src/net/socket.ts index db8a28d..9c3bee6 100644 --- a/packages/engine/src/net/socket.ts +++ b/packages/engine/src/net/socket.ts @@ -1,9 +1,10 @@ import { Quaternion, Vector3 } from 'three'; -import { Packet, PacketType } from '.'; -import { World } from '../gameobjects/world.object'; -import { Disposable } from '../types/disposable'; -import { EngineEvents, PlayerEvent } from '../types/events'; -import { EventEmitter } from '../utils/events'; +import { PacketType } from './packet-type.enum.js'; +import { World } from '../gameobjects/world.object.js'; +import { Disposable } from '../types/disposable.js'; +import { EngineEvents, PlayerEvent } from '../types/events.js'; +import { EventEmitter } from '../utils/events.js'; +import { Packet } from './packet.js'; export class GameSocket implements Disposable { private ws?: WebSocket; @@ -98,7 +99,7 @@ export class GameSocket implements Disposable { case PacketType.PLAYER_LIST: { const playerCount = incoming.read('uint32')!; const players = Array.from({ length: playerCount }, () => null).map( - () => incoming.read(String)?.split(':') + () => incoming.read(String)?.split(':'), ); console.log('player list', players); break; @@ -172,7 +173,7 @@ export class GameSocket implements Disposable { .write(event.velocity, 'vec3') .write(event.lookAt, 'vec3') .write(event.jump, 'bool') - .toBuffer() + .toBuffer(), ); }; diff --git a/packages/engine/src/node.ts b/packages/engine/src/node.ts index f80630c..b21e274 100644 --- a/packages/engine/src/node.ts +++ b/packages/engine/src/node.ts @@ -12,14 +12,15 @@ declare global { globalThis.FREEBLOX_SIDE = 'server'; globalThis.FREEBLOX_THREAD = 'main'; -export * from './core'; -export * from './render'; -export * from './net'; -export * from './utils'; -export * from './types'; -export * from './components'; -export * from './gameobjects'; -export * from './assets'; -export * from './defaults/environment'; -export * from './decorators'; -export * from './controls'; +export * from './core/index.js'; +export * from './render/index.js'; +export * from './physics/index.js'; +export * from './net/index.js'; +export * from './utils/index.js'; +export * from './types/index.js'; +export * from './components/index.js'; +export * from './gameobjects/index.js'; +export * from './assets/index.js'; +export * from './defaults/environment.js'; +export * from './decorators/index.js'; +export * from './controls/index.js'; diff --git a/packages/engine/src/physics/index.ts b/packages/engine/src/physics/index.ts index a8734c5..f5c3060 100644 --- a/packages/engine/src/physics/index.ts +++ b/packages/engine/src/physics/index.ts @@ -1,2 +1,2 @@ -export * from './rapier'; -export * from './ticking'; +export * from './rapier.js'; +export * from './ticking.js'; diff --git a/packages/engine/src/physics/rapier.ts b/packages/engine/src/physics/rapier.ts index 2376cf6..0760396 100644 --- a/packages/engine/src/physics/rapier.ts +++ b/packages/engine/src/physics/rapier.ts @@ -1,5 +1,7 @@ export type Rapier = typeof import('@dimforge/rapier3d'); -export function getRapier() { - return import('@dimforge/rapier3d'); -} +export const rapierSource = { + getRapier() { + return import('@dimforge/rapier3d'); + }, +}; diff --git a/packages/engine/src/physics/ticking.ts b/packages/engine/src/physics/ticking.ts index 18aa265..00f4b11 100644 --- a/packages/engine/src/physics/ticking.ts +++ b/packages/engine/src/physics/ticking.ts @@ -1,5 +1,5 @@ import type Rapier from '@dimforge/rapier3d'; -import { Disposable } from '../types/disposable'; +import { Disposable } from '../types/disposable.js'; export interface PhysicsTicking extends Disposable { /** @@ -18,7 +18,7 @@ export interface PhysicsTicking extends Disposable { initializePhysics( physicsEngine: typeof Rapier, physicsWorld: Rapier.World, - controller?: Rapier.KinematicCharacterController + controller?: Rapier.KinematicCharacterController, ): void; /** diff --git a/packages/engine/src/render/index.ts b/packages/engine/src/render/index.ts index e58595d..5adb6d0 100644 --- a/packages/engine/src/render/index.ts +++ b/packages/engine/src/render/index.ts @@ -1,2 +1,2 @@ -export * from './webgl-renderer'; -export * from './stub-renderer'; +export * from './webgl-renderer.js'; +export * from './stub-renderer.js'; diff --git a/packages/engine/src/render/stub-renderer.ts b/packages/engine/src/render/stub-renderer.ts index c4d322f..57a4ebc 100644 --- a/packages/engine/src/render/stub-renderer.ts +++ b/packages/engine/src/render/stub-renderer.ts @@ -8,10 +8,12 @@ import { Object3D, Object3DEventMap, } from 'three'; -import { Renderer } from '../types/renderer'; +import { Renderer } from '../types/renderer.js'; export class NoopThreeRenderer implements ThreeRenderer { - domElement = document?.createElement('canvas'); + domElement = (globalThis.FREEBLOX_SIDE === 'client' + ? document?.createElement('canvas') + : undefined) as HTMLCanvasElement; render(scene: Object3D, camera: Camera): void { // stub @@ -28,14 +30,13 @@ export class StubRenderer implements Renderer { 75, this.resolution.x / this.resolution.y, 0.1, - 10000 + 10000, ); public scene = new Scene(); constructor( - public viewport: HTMLElement, public resolution = new Vector2(1080, 720), - private params = {} + private params = {}, ) {} setSize(width: number, height: number) { diff --git a/packages/engine/src/render/webgl-renderer.ts b/packages/engine/src/render/webgl-renderer.ts index 255e906..2f7740e 100644 --- a/packages/engine/src/render/webgl-renderer.ts +++ b/packages/engine/src/render/webgl-renderer.ts @@ -7,7 +7,13 @@ import { ColorRepresentation, Color, } from 'three'; -import { Renderer } from '../types/renderer'; +import { Renderer } from '../types/renderer.js'; +import { + EffectComposer, + OutputPass, + RenderPass, + SSAOPass, +} from 'three/examples/jsm/Addons.js'; export class WebGLRenderer implements Renderer { public renderer = new ThreeWebGLRenderer(this.params); @@ -15,47 +21,91 @@ export class WebGLRenderer implements Renderer { 75, this.resolution.x / this.resolution.y, 0.1, - 10000 + 2000, ); public scene = new Scene(); + public composer?: EffectComposer; + public viewport?: HTMLElement; + private renderPass?: RenderPass; + private outputPass?: OutputPass; + private ssaoPass?: SSAOPass; constructor( - public viewport: HTMLElement, public resolution = new Vector2(1080, 720), - private params?: WebGLRendererParameters + private params?: WebGLRendererParameters, ) { this.renderer.setSize(resolution.x, resolution.y); - viewport.appendChild(this.renderer.domElement); + this.viewport?.appendChild(this.renderer.domElement); this.renderer.autoClear = false; + // this.setupComposer(); + } + + mount(target: HTMLElement) { + if (this.viewport) { + this.viewport.removeChild(this.renderer.domElement); + } + + this.viewport = target; + this.viewport.appendChild(this.renderer.domElement); } reset(params?: WebGLRendererParameters) { const color = new Color(); const alpha = this.renderer.getClearAlpha(); this.renderer.getClearColor(color); - this.viewport.removeChild(this.renderer.domElement); + this.viewport?.removeChild(this.renderer.domElement); this.renderer.dispose(); + this.composer?.dispose(); this.renderer = new ThreeWebGLRenderer({ ...this.params, ...params }); this.renderer.setSize(this.resolution.x, this.resolution.y); this.renderer.autoClear = false; this.renderer.setClearColor(color, alpha); - this.viewport.appendChild(this.renderer.domElement); + this.viewport?.appendChild(this.renderer.domElement); + // this.setupComposer(); + } + + setupComposer() { + this.composer = new EffectComposer(this.renderer); + + this.renderPass = new RenderPass(this.scene, this.camera, undefined); + this.composer.addPass(this.renderPass); + + this.ssaoPass = new SSAOPass( + this.scene, + this.camera, + this.resolution.x, + this.resolution.y, + ); + this.ssaoPass.kernelRadius = 1; + this.ssaoPass.minDistance = 0.001; + this.ssaoPass.maxDistance = 0.3; + this.composer.addPass(this.ssaoPass); + + this.outputPass = new OutputPass(); + this.composer.addPass(this.outputPass); } setSize(width: number, height: number) { this.resolution.set(width, height); this.camera.aspect = this.resolution.x / this.resolution.y; this.renderer.setSize(this.resolution.x, this.resolution.y); + this.composer?.setSize(this.resolution.x, this.resolution.y); this.camera.updateProjectionMatrix(); } render() { + if (this.composer) { + this.composer.render(); + return; + } + this.renderer.clear(); this.renderer.render(this.scene, this.camera); } dispose() { + this.composer?.dispose(); this.renderer.dispose(); } diff --git a/packages/engine/src/types/engine-component.ts b/packages/engine/src/types/engine-component.ts index 2a453f6..cd5cf5f 100644 --- a/packages/engine/src/types/engine-component.ts +++ b/packages/engine/src/types/engine-component.ts @@ -1,14 +1,14 @@ -import { Disposable } from './disposable'; -import { EventEmitter } from '../utils/events'; -import { EngineEvents } from './events'; -import { Renderer } from './renderer'; +import { Disposable } from './disposable.js'; +import { EventEmitter } from '../utils/events.js'; +import { EngineEvents } from './events.js'; +import { Renderer } from './renderer.js'; export abstract class EngineComponent implements Disposable { public abstract name: string; constructor( protected renderer: Renderer, - protected events: EventEmitter + protected events: EventEmitter, ) {} /** diff --git a/packages/engine/src/types/events.ts b/packages/engine/src/types/events.ts index 889fd2b..e7a5773 100644 --- a/packages/engine/src/types/events.ts +++ b/packages/engine/src/types/events.ts @@ -6,7 +6,8 @@ import { Vector3, Quaternion, } from 'three'; -import { SerializedObject } from './game-object'; +import { SerializedObject } from './game-object.js'; +import { PlayerSocket } from './server.js'; export interface MousePositionEvent { position: Vector2; @@ -118,3 +119,10 @@ export type EngineEvents = { serverTransform: (obj: ServerTransformEvent) => void; serverDisconnect: () => void; }; + +export type ServerEvents = { + listening: () => void; + newConnection: (socket: PlayerSocket) => void; + disconnected: (socket: PlayerSocket) => void; + playerPacket: (socket: PlayerSocket, data: Buffer) => void; +} & EngineEvents; diff --git a/packages/engine/src/types/game-object.ts b/packages/engine/src/types/game-object.ts index 639975a..d6958ca 100644 --- a/packages/engine/src/types/game-object.ts +++ b/packages/engine/src/types/game-object.ts @@ -1,7 +1,7 @@ import { Color, Euler, Object3D, Vector3 } from 'three'; -import { Property } from './property'; -import { ExposeProperty } from '../decorators/property'; -import { readMetadataOf } from '../utils/read-metadata'; +import { Property } from './property.js'; +import { ExposeProperty } from '../decorators/property.js'; +import { readMetadataOf } from '../utils/read-metadata.js'; export interface IGameObject { objectType: string; @@ -88,7 +88,7 @@ export class GameObject extends Object3D implements IGameObject { objectType: this.objectType, children: this.children .filter( - (entry) => entry instanceof GameObject && entry.archivable !== false + (entry) => entry instanceof GameObject && entry.archivable !== false, ) .map((entry) => (entry as GameObject).serialize()), visible: this.visible, @@ -108,7 +108,7 @@ export class GameObject extends Object3D implements IGameObject { ...obj, [key]: value, }; - }, {}) + }, {}), ); return object; diff --git a/packages/engine/src/types/index.ts b/packages/engine/src/types/index.ts index c4fd90d..696a651 100644 --- a/packages/engine/src/types/index.ts +++ b/packages/engine/src/types/index.ts @@ -1,10 +1,11 @@ -export * from './game-runner'; -export * from './events'; -export * from './engine-component'; -export * from './game-object'; -export * from './instancable'; -export * from './world-file'; -export * from './asset'; -export * from './disposable'; -export * from './renderer'; -export * from './character'; +export * from './game-runner.js'; +export * from './server.js'; +export * from './events.js'; +export * from './engine-component.js'; +export * from './game-object.js'; +export * from './instancable.js'; +export * from './world-file.js'; +export * from './asset.js'; +export * from './disposable.js'; +export * from './renderer.js'; +export * from './character.js'; diff --git a/packages/engine/src/types/renderer.ts b/packages/engine/src/types/renderer.ts index b95d54d..83634ed 100644 --- a/packages/engine/src/types/renderer.ts +++ b/packages/engine/src/types/renderer.ts @@ -25,7 +25,7 @@ export interface Renderer { /** * Viewport element. */ - viewport: HTMLElement; + viewport?: HTMLElement; /** * Resolution. diff --git a/packages/engine/src/types/server.ts b/packages/engine/src/types/server.ts new file mode 100644 index 0000000..0b5e82e --- /dev/null +++ b/packages/engine/src/types/server.ts @@ -0,0 +1,29 @@ +import { Packet } from '../net/packet.js'; + +export interface PlayerSocket { + id: string; + ip: string; + sendPacket(data: Buffer): void; + close(code: number, data: Buffer): void; +} + +export interface SocketServer { + listen(port: number, host?: string): void; +} + +export interface AuthData { + socketId: string; + socketIp: string; + playerId: string; + playerToken: string; +} + +export interface AuthResult { + playerId: string; + name: string; + displayName?: string; +} + +export interface AuthBackend { + authenticate(data: AuthData): Promise; +} diff --git a/packages/engine/src/types/world-file.ts b/packages/engine/src/types/world-file.ts index 9281539..11d54b7 100644 --- a/packages/engine/src/types/world-file.ts +++ b/packages/engine/src/types/world-file.ts @@ -1,6 +1,6 @@ -import { SerializedEnvironment } from '../gameobjects/environment.object'; -import { Asset } from './asset'; -import { SerializedObject } from './game-object'; +import { SerializedEnvironment } from '../gameobjects/environment.object.js'; +import { Asset } from './asset.js'; +import { SerializedObject } from './game-object.js'; export interface WorldFile { name: string; diff --git a/packages/engine/src/utils/character.ts b/packages/engine/src/utils/character.ts index fe7cda9..abdacb4 100644 --- a/packages/engine/src/utils/character.ts +++ b/packages/engine/src/utils/character.ts @@ -6,16 +6,16 @@ import { SkinnedMesh, Vector3, } from 'three'; -import { assetManager } from '../assets'; -import { Group } from '../gameobjects/group.object'; -import { MeshPart } from '../gameobjects/mesh.object'; -import { Humanoid } from '../gameobjects/humanoid.object'; +import { assetManager } from '../assets/manager.js'; +import { Group } from '../gameobjects/group.object.js'; +import { MeshPart } from '../gameobjects/mesh.object.js'; +import { Humanoid } from '../gameobjects/humanoid.object.js'; import * as SkeletonUtils from 'three/addons/utils/SkeletonUtils.js'; import { CharacterBodyPart, CharacterSpecification, CharacterTextureType, -} from '../types/character'; +} from '../types/character.js'; const CHARACTER_MAX_VERSION = 1; const CHARACTER_MIN_VERSION = 1; @@ -88,7 +88,7 @@ export const parseCharacterSpecificaton = (armature: Object3D) => { const fblxc = temporaryObject.FBLXC; Object.assign( fblxc, - splitKeys(fblxc, ['Parts', 'ColorParts', 'Bones', 'Accessories']) + splitKeys(fblxc, ['Parts', 'ColorParts', 'Bones', 'Accessories']), ); // Comma-separated lists to string arrays @@ -102,7 +102,7 @@ export const parseCharacterSpecificaton = (armature: Object3D) => { for (const [key, value] of Object.entries(fblxc.Accessory[accessory])) { if (key === 'Origin') { fblxc.Accessory[accessory][key] = new Vector3().fromArray( - value as number[] + value as number[], ); } @@ -148,12 +148,12 @@ export const loadBaseCharacter = async () => { // TODO: temporary loading method const loadMesh = await assetManager.loadMeshData( - 'https://lunasqu.ee/freeblox/character-base.glb' + 'https://lunasqu.ee/freeblox/character-base.glb', ); const loadFace = await assetManager.createAsset( 'https://lunasqu.ee/freeblox/face-1.png', 'CharacterFace1', - 'Texture' + 'Texture', ); const armature = loadMesh.scene.getObjectByName('Armature'); if (!armature) throw new Error('Invalid character asset'); @@ -170,7 +170,7 @@ export const loadBaseCharacter = async () => { export const instanceCharacterObject = async ( name: string, - pos = new Vector3(0, 1, 0) + pos = new Vector3(0, 1, 0), ) => { const base = await loadBaseCharacter(); const cloned = SkeletonUtils.clone(base.root!); @@ -203,10 +203,10 @@ export const instanceCharacterObject = async ( controller.position.set(0, 4.75, 0); controller.archivable = false; controller.bodyPartNames = base.specification!.bodyParts.map((entry) => - convertKey(entry.meshName) + convertKey(entry.meshName), ); controller.bodyBoneNames = base.specification!.bodyParts.map((entry) => - convertKey(entry.boneName) + convertKey(entry.boneName), ); baseObject.add(controller); baseObject.position.copy(pos); diff --git a/packages/engine/src/utils/index.ts b/packages/engine/src/utils/index.ts index 12170b2..c411540 100644 --- a/packages/engine/src/utils/index.ts +++ b/packages/engine/src/utils/index.ts @@ -1,5 +1,5 @@ -export * from './debounce'; -export * from './events'; -export * from './read-metadata'; -export * from './character'; -export * from './random'; +export * from './debounce.js'; +export * from './events.js'; +export * from './read-metadata.js'; +export * from './character.js'; +export * from './random.js'; diff --git a/packages/engine/src/utils/math.ts b/packages/engine/src/utils/math.ts new file mode 100644 index 0000000..8393a97 --- /dev/null +++ b/packages/engine/src/utils/math.ts @@ -0,0 +1,14 @@ +export const DEG2RAD = Math.PI / 180; +export const RAD2DEG = 180 / Math.PI; + +export function clamp(value: number, min: number, max: number) { + return Math.max(min, Math.min(max, value)); +} + +export function degToRad(degrees: number) { + return degrees * DEG2RAD; +} + +export function radToDeg(radians: number) { + return radians * RAD2DEG; +} diff --git a/packages/engine/tsconfig.json b/packages/engine/tsconfig.json index dfad0ae..d1484f1 100644 --- a/packages/engine/tsconfig.json +++ b/packages/engine/tsconfig.json @@ -2,8 +2,10 @@ "extends": "../../tsconfig.json", "compilerOptions": { "outDir": "./dist", + "moduleResolution": "NodeNext", + "module": "NodeNext" }, "include": [ "src/**/*" - ] + ], } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ec0d29c..b6f7c56 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -179,8 +179,8 @@ importers: specifier: ^4.2.0 version: 4.2.0 three: - specifier: ^0.166.0 - version: 0.166.0 + specifier: ^0.166.1 + version: 0.166.1 uuid: specifier: ^10.0.0 version: 10.0.0 @@ -217,6 +217,43 @@ importers: specifier: ^1.4.2 version: 1.4.2(typescript@5.1.3) + server: + dependencies: + '@dimforge/rapier3d': + specifier: ^0.13.1 + version: 0.13.1 + '@dimforge/rapier3d-compat': + specifier: ^0.13.1 + version: 0.13.1 + '@freeblox/engine': + specifier: workspace:^ + version: link:../packages/engine + three: + specifier: ^0.166.1 + version: 0.166.1 + uuid: + specifier: ^10.0.0 + version: 10.0.0 + ws: + specifier: ^8.18.0 + version: 8.18.0 + devDependencies: + '@types/node': + specifier: ^18.0.0 + version: 18.0.0 + '@types/three': + specifier: ^0.166.0 + version: 0.166.0 + '@types/ws': + specifier: ^8.5.10 + version: 8.5.10 + prettier: + specifier: ^3.3.2 + version: 3.3.2 + typescript: + specifier: ^5.5.3 + version: 5.5.3 + packages: '@alloc/quick-lru@5.2.0': @@ -394,6 +431,9 @@ packages: '@cloudflare/kv-asset-handler@0.3.0': resolution: {integrity: sha512-9CB/MKf/wdvbfkUdfrj+OkEwZ5b7rws0eogJ4293h+7b6KX5toPwym+VQKmILafNB9YiehqY0DlNrDcDhdWHSQ==} + '@dimforge/rapier3d-compat@0.13.1': + resolution: {integrity: sha512-SsQ/MTH4Vvs8f62g31iVV1VOmzwNwsJl91rVzafi5B2mCrI2OsQ3VyjJOb4/tvS5VNc6OLjIDgfClcOAkW+O2A==} + '@dimforge/rapier3d@0.13.1': resolution: {integrity: sha512-ITCUCqks70njfFt7S+AEynMw4mpNJLmVWkWDzePhe74YLRWbe4Pu8VOyks2hqHBeTCj2Ts7SE5xoyvg9VsyNMg==} @@ -1197,6 +1237,9 @@ packages: '@types/webxr@0.5.2': resolution: {integrity: sha512-szL74BnIcok9m7QwYtVmQ+EdIKwbjPANudfuvDrAF8Cljg9MKUlIoc1w5tjj9PMpeSH3U1Xnx//czQybJ0EfSw==} + '@types/ws@8.5.10': + resolution: {integrity: sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==} + '@unhead/dom@1.1.27': resolution: {integrity: sha512-sUrzpKIVvFp8TFx1mgp5t0k5ts1+KmgjMgRRuvRTZMBMVeGQRLSuL3uo34iwuFmKxeI6BXT5lVBk5H02c1XdGg==} @@ -3513,6 +3556,11 @@ packages: resolution: {integrity: sha512-0vzE+lAiG7hZl1/9I8yzKLx3aR9Xbof3fBHKunvMfOCYAtMhrsnccJY2iTURb9EZd5+pLuiNV9/c/GZJOHsgIw==} engines: {node: ^10 || ^12 || >=14} + prettier@3.3.2: + resolution: {integrity: sha512-rAVeHYMcv8ATV5d508CFdn+8/pHPpXeIid1DdrPwXnaAdH7cqjVbpJaT5eq4yRAFU/lsbwYwSF/n5iNrdJHPQA==} + engines: {node: '>=14'} + hasBin: true + pretty-bytes@6.1.0: resolution: {integrity: sha512-Rk753HI8f4uivXi4ZCIYdhmG1V+WKzvRMg/X+M42a6t7D07RcmopXJMDNk6N++7Bl75URRGsb40ruvg7Hcp2wQ==} engines: {node: ^14.13.1 || >=16.0.0} @@ -3975,6 +4023,9 @@ packages: three@0.166.0: resolution: {integrity: sha512-3Gw7oyZ/vCmz3RmNx1xuyNu7Ou/igDtoh953QsJh/QkAoi6B7jpkKwk05N8Y7/9bZeIE44zdC+i2KZNF+KWQ8A==} + three@0.166.1: + resolution: {integrity: sha512-LtuafkKHHzm61AQA1be2MAYIw1IjmhOUxhBa0prrLpEMWbV7ijvxCRHjSgHPGp2493wLBzwKV46tA9nivLEgKg==} + through@2.3.8: resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} @@ -4056,6 +4107,11 @@ packages: engines: {node: '>=14.17'} hasBin: true + typescript@5.5.3: + resolution: {integrity: sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==} + engines: {node: '>=14.17'} + hasBin: true + ufo@1.1.2: resolution: {integrity: sha512-TrY6DsjTQQgyS3E3dBaOXf0TpPD8u9FVrVYmKVegJuFw51n/YB9XPt+U6ydzFG5ZIN7+DIjPbNmXoBj9esYhgQ==} @@ -4466,6 +4522,18 @@ packages: utf-8-validate: optional: true + ws@8.18.0: + resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==} + 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 + xtend@4.0.2: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} @@ -4739,6 +4807,8 @@ snapshots: dependencies: mime: 3.0.0 + '@dimforge/rapier3d-compat@0.13.1': {} + '@dimforge/rapier3d@0.13.1': {} '@esbuild-kit/cjs-loader@2.4.2': @@ -5607,6 +5677,10 @@ snapshots: '@types/webxr@0.5.2': {} + '@types/ws@8.5.10': + dependencies: + '@types/node': 18.0.0 + '@unhead/dom@1.1.27': dependencies: '@unhead/schema': 1.1.27 @@ -6442,12 +6516,12 @@ snapshots: css-tree@2.2.1: dependencies: mdn-data: 2.0.28 - source-map-js: 1.0.2 + source-map-js: 1.2.0 css-tree@2.3.1: dependencies: mdn-data: 2.0.30 - source-map-js: 1.0.2 + source-map-js: 1.2.0 css-what@6.1.0: {} @@ -8305,6 +8379,8 @@ snapshots: picocolors: 1.0.1 source-map-js: 1.2.0 + prettier@3.3.2: {} + pretty-bytes@6.1.0: {} proc-log@3.0.0: {} @@ -8737,7 +8813,7 @@ snapshots: css-select: 5.1.0 css-tree: 2.3.1 csso: 5.0.5 - picocolors: 1.0.0 + picocolors: 1.0.1 tailwindcss@3.3.2: dependencies: @@ -8815,6 +8891,8 @@ snapshots: three@0.166.0: {} + three@0.166.1: {} + through@2.3.8: {} tiny-invariant@1.3.1: {} @@ -8854,7 +8932,7 @@ snapshots: '@esbuild-kit/core-utils': 3.1.0 '@esbuild-kit/esm-loader': 2.5.5 optionalDependencies: - fsevents: 2.3.2 + fsevents: 2.3.3 tuf-js@1.1.7: dependencies: @@ -8876,6 +8954,8 @@ snapshots: typescript@5.5.2: {} + typescript@5.5.3: {} + ufo@1.1.2: {} ultrahtml@1.2.0: {} @@ -9369,6 +9449,8 @@ snapshots: ws@8.13.0: {} + ws@8.18.0: {} + xtend@4.0.2: {} xxhashjs@0.2.2: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index ae21bf1..4009129 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,3 +1,4 @@ packages: - 'app' + - 'server' - 'packages/**' diff --git a/server/package.json b/server/package.json new file mode 100644 index 0000000..9d51512 --- /dev/null +++ b/server/package.json @@ -0,0 +1,31 @@ +{ + "name": "@freeblox/server", + "version": "1.0.0", + "description": "", + "main": "dist/index.js", + "type": "module", + "private": true, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "build": "tsc", + "start": "node dist" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "@dimforge/rapier3d": "^0.13.1", + "@dimforge/rapier3d-compat": "^0.13.1", + "@freeblox/engine": "workspace:^", + "three": "^0.166.1", + "uuid": "^10.0.0", + "ws": "^8.18.0" + }, + "devDependencies": { + "@types/node": "^18.0.0", + "@types/three": "^0.166.0", + "@types/ws": "^8.5.10", + "prettier": "^3.3.2", + "typescript": "^5.5.3" + } +} diff --git a/server/pnpm-lock.yaml b/server/pnpm-lock.yaml new file mode 100644 index 0000000..f1b0fa3 --- /dev/null +++ b/server/pnpm-lock.yaml @@ -0,0 +1,113 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@freeblox/engine': + specifier: workspace:^ + version: link:../packages/engine + three: + specifier: ^0.166.1 + version: 0.166.1 + uuid: + specifier: ^10.0.0 + version: 10.0.0 + ws: + specifier: ^8.18.0 + version: 8.18.0 + devDependencies: + '@types/three': + specifier: ^0.166.0 + version: 0.166.0 + '@types/ws': + specifier: ^8.5.10 + version: 8.5.10 + +packages: + + '@tweenjs/tween.js@23.1.2': + resolution: {integrity: sha512-kMCNaZCJugWI86xiEHaY338CU5JpD0B97p1j1IKNn/Zto8PgACjQx0UxbHjmOcLl/dDOBnItwD07KmCs75pxtQ==} + + '@types/node@18.19.39': + resolution: {integrity: sha512-nPwTRDKUctxw3di5b4TfT3I0sWDiWoPQCZjXhvdkINntwr8lcoVCKsTgnXeRubKIlfnV+eN/HYk6Jb40tbcEAQ==} + + '@types/stats.js@0.17.3': + resolution: {integrity: sha512-pXNfAD3KHOdif9EQXZ9deK82HVNaXP5ZIF5RP2QG6OQFNTaY2YIetfrE9t528vEreGQvEPRDDc8muaoYeK0SxQ==} + + '@types/three@0.166.0': + resolution: {integrity: sha512-FHMnpcdhdbdOOIYbfkTkUVpYMW53odxbTRwd0/xJpYnTzEsjnVnondGAvHZb4z06UW0vo6WPVuvH0/9qrxKx7g==} + + '@types/webxr@0.5.19': + resolution: {integrity: sha512-4hxA+NwohSgImdTSlPXEqDqqFktNgmTXQ05ff1uWam05tNGroCMp4G+4XVl6qWm1p7GQ/9oD41kAYsSssF6Mzw==} + + '@types/ws@8.5.10': + resolution: {integrity: sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==} + + fflate@0.8.2: + resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} + + meshoptimizer@0.18.1: + resolution: {integrity: sha512-ZhoIoL7TNV4s5B6+rx5mC//fw8/POGyNxS/DZyCJeiZ12ScLfVwRE/GfsxwiTkMYYD5DmK2/JXnEVXqL4rF+Sw==} + + three@0.166.1: + resolution: {integrity: sha512-LtuafkKHHzm61AQA1be2MAYIw1IjmhOUxhBa0prrLpEMWbV7ijvxCRHjSgHPGp2493wLBzwKV46tA9nivLEgKg==} + + undici-types@5.26.5: + resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + + uuid@10.0.0: + resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==} + hasBin: true + + ws@8.18.0: + resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==} + 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 + +snapshots: + + '@tweenjs/tween.js@23.1.2': {} + + '@types/node@18.19.39': + dependencies: + undici-types: 5.26.5 + + '@types/stats.js@0.17.3': {} + + '@types/three@0.166.0': + dependencies: + '@tweenjs/tween.js': 23.1.2 + '@types/stats.js': 0.17.3 + '@types/webxr': 0.5.19 + fflate: 0.8.2 + meshoptimizer: 0.18.1 + + '@types/webxr@0.5.19': {} + + '@types/ws@8.5.10': + dependencies: + '@types/node': 18.19.39 + + fflate@0.8.2: {} + + meshoptimizer@0.18.1: {} + + three@0.166.1: {} + + undici-types@5.26.5: {} + + uuid@10.0.0: {} + + ws@8.18.0: {} diff --git a/server/src/engine/engine.ts b/server/src/engine/engine.ts new file mode 100644 index 0000000..e8f8a35 --- /dev/null +++ b/server/src/engine/engine.ts @@ -0,0 +1,64 @@ +import { + Engine, + EnvironmentComponent, + EventEmitter, + GameServerComponent, + LevelComponent, + PhysicsWorldComponent, + ServerEvents, + StubRenderer, +} from '@freeblox/engine/dist/node'; +import { Vector2 } from 'three'; + +const PHYSICS_STEPPING = 1000 / 60; +const PHYSICS_STEPPING_S = 1 / 60; +const UPDATE_STEPPING = 1000 / 10; +const UPDATE_STEPPING_S = 0.1; + +export class ServerEngine extends Engine { + public render = new StubRenderer(new Vector2()); + + private physicsTicker?: ReturnType; + private syncTicker?: ReturnType; + + constructor(public events: EventEmitter) { + super(); + this.use(EnvironmentComponent); + this.use(LevelComponent); + this.use(PhysicsWorldComponent); + this.use(GameServerComponent); + } + + override start(): void { + this.running = true; + + const level = this.getComponent(LevelComponent); + const physicsWorld = this.getComponent(PhysicsWorldComponent); + + const gameServer = this.getComponent(GameServerComponent); + gameServer.setSynchronize(level, physicsWorld); + + this.physicsTicker = setInterval(() => { + if (!this.running) { + clearInterval(this.physicsTicker); + return; + } + + physicsWorld.update(PHYSICS_STEPPING_S); + }, PHYSICS_STEPPING); + + this.syncTicker = setInterval(() => { + if (!this.running) { + clearInterval(this.syncTicker); + return; + } + + gameServer.update(UPDATE_STEPPING_S); + }, UPDATE_STEPPING); + } + + async loadLevel(asset: string) { + const file = await fetch(asset).then((l) => l.json()); + this.getComponent(LevelComponent).deserializeLevelSave(file); + } +} diff --git a/server/src/engine/index.ts b/server/src/engine/index.ts new file mode 100644 index 0000000..cddba05 --- /dev/null +++ b/server/src/engine/index.ts @@ -0,0 +1,2 @@ +export * from './net/index.js'; +export * from './engine.js'; diff --git a/server/src/engine/net/index.ts b/server/src/engine/net/index.ts new file mode 100644 index 0000000..12e9b8a --- /dev/null +++ b/server/src/engine/net/index.ts @@ -0,0 +1 @@ +export * from './websocket-server.js'; diff --git a/server/src/engine/net/websocket-server.ts b/server/src/engine/net/websocket-server.ts new file mode 100644 index 0000000..5302ba8 --- /dev/null +++ b/server/src/engine/net/websocket-server.ts @@ -0,0 +1,57 @@ +import { + EventEmitter, + ServerEvents, + SocketServer, + randomUUID, +} from '@freeblox/engine/dist/node'; +import { IncomingMessage } from 'http'; +import { WebSocketServer as WSS } from 'ws'; +import { PlayerSocket } from '../../types/index.js'; + +export class WebSocketServer implements SocketServer { + public wss!: WSS; + + constructor(public events: EventEmitter) {} + + listen(port: number, host?: string) { + this.wss = new WSS({ + port, + host, + }); + this.wss.addListener('connection', this.handleConnection.bind(this)); + this.wss.addListener('listening', () => this.events.emit('listening')); + } + + handleConnection(client: PlayerSocket, request: IncomingMessage) { + client.id = randomUUID(); + client.ip = request.socket.remoteAddress as string; + client.sendPacket = (packet) => + client.send(packet, { binary: true }, (err) => { + // TODO: handle error + }); + + console.log( + ` => New client ${client.id} connected from ${client.ip}:${request.socket.remotePort}, waiting for ident...`, + ); + this.events.emit('newConnection', client); + + client.on('close', () => { + if (!client.id) return; + + console.log( + ` <= Client ${client.id} from ${client.ip}:${request.socket.remotePort}, disconnected`, + ); + this.events.emit('disconnected', client); + }); + + client.on('message', (data) => + Array.isArray(data) + ? data.forEach((packet) => this.handlePacket(client, packet)) + : this.handlePacket(client, data as Buffer), + ); + } + + handlePacket(client: PlayerSocket, data: Buffer) { + this.events.emit('playerPacket', client, data); + } +} diff --git a/server/src/index.ts b/server/src/index.ts new file mode 100644 index 0000000..b62080a --- /dev/null +++ b/server/src/index.ts @@ -0,0 +1,35 @@ +import { + EventEmitter, + ServerEvents, + rapierSource, +} from '@freeblox/engine/dist/node'; +import { ServerEngine } from './engine/engine.js'; +import { WebSocketServer } from './engine/net/websocket-server.js'; + +const events = new EventEmitter(); +const engine = new ServerEngine(events); +const server = new WebSocketServer(events); + +// For node.js we need compat implementation +rapierSource.getRapier = () => + import('@dimforge/rapier3d-compat').then((engine) => + engine.init().then(() => engine), + ) as unknown as ReturnType; + +const init = async () => { + const world = 'https://lunasqu.ee/freeblox/test-level.json'; + const port = Number(process.env.PORT || 8128); + + await engine.loadLevel(world); + + events.on('listening', () => console.log(`Listening on port ${port}`)); + events.on('loadComplete', () => + console.log('Game world has been initialized'), + ); + events.on('physicsComplete', () => + console.log('Physics world has been initialized'), + ); + server.listen(port, '0.0.0.0'); +}; + +init().catch(console.error); diff --git a/server/src/players/index.ts b/server/src/players/index.ts new file mode 100644 index 0000000..e69de29 diff --git a/server/src/types/index.ts b/server/src/types/index.ts new file mode 100644 index 0000000..cd5f1a0 --- /dev/null +++ b/server/src/types/index.ts @@ -0,0 +1,7 @@ +import { WebSocket } from 'ws'; + +export interface PlayerSocket extends WebSocket { + id: string; + ip: string; + sendPacket(data: Buffer): void; +} diff --git a/server/tsconfig.json b/server/tsconfig.json new file mode 100644 index 0000000..180a52c --- /dev/null +++ b/server/tsconfig.json @@ -0,0 +1,108 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig to read more about this file */ + + /* Projects */ + // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ + // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ + // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ + // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ + // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ + // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ + + /* Language and Environment */ + "target": "ES2020" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, + // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + // "jsx": "preserve", /* Specify what JSX code is generated. */ + "experimentalDecorators": true /* Enable experimental support for legacy experimental decorators. */, + "emitDecoratorMetadata": true /* Emit design-type metadata for decorated declarations in source files. */, + // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ + // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ + // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ + // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ + // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ + // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ + // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ + + /* Modules */ + "module": "NodeNext" /* Specify what module code is generated. */, + "rootDir": "./src" /* Specify the root folder within your source files. */, + "moduleResolution": "NodeNext" /* Specify how TypeScript looks up a file from a given module specifier. */, + // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ + // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ + // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ + // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ + // "types": [], /* Specify type package names to be included without being referenced in a source file. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ + // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ + // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ + // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ + // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ + // "resolveJsonModule": true, /* Enable importing .json files. */ + // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ + // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ + + /* JavaScript Support */ + // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ + // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ + // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ + + /* Emit */ + // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ + // "declarationMap": true, /* Create sourcemaps for d.ts files. */ + // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ + // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ + // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ + "outDir": "./dist" /* Specify an output folder for all emitted files. */, + // "removeComments": true, /* Disable emitting comments. */ + // "noEmit": true, /* Disable emitting files from a compilation. */ + // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ + // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ + // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ + // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ + // "newLine": "crlf", /* Set the newline character for emitting files. */ + // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ + // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ + // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ + // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ + // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ + + /* Interop Constraints */ + // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ + // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ + // "isolatedDeclarations": true, /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */ + // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ + "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, + // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ + "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, + + /* Type Checking */ + "strict": true /* Enable all strict type-checking options. */, + // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ + // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ + // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ + // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ + // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ + // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ + // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ + // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ + // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ + // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ + // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ + // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ + // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ + // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ + // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ + // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ + // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ + + /* Completeness */ + // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ + "skipLibCheck": true /* Skip type checking all .d.ts files. */ + } +}