server/src/world/world.service.ts

191 lines
5.6 KiB
TypeScript

import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { PlayerStoreService } from 'src/player/player-store.service';
import { HttpService } from '@nestjs/axios';
import { WorldFile } from 'src/types/world-file';
import { Environment, World, instancableGameObjects } from 'src/game';
import { SerializedObject } from 'src/types/serialized';
import { Object3D } from 'three';
import { lastValueFrom } from 'rxjs';
import { Packet } from 'src/net/packet';
import { PacketType } from 'src/types/packet-type.enum';
import { Player } from 'src/game/player';
import { GameObject } from 'src/game/game-object';
import { PhysicsService } from './physics.service';
@Injectable()
export class WorldService implements OnModuleInit {
private logger = new Logger(WorldService.name);
private world = new World();
private environment = new Environment();
private broadcastWorldStateTicker!: ReturnType<typeof setInterval>;
constructor(
private readonly players: PlayerStoreService,
private readonly physics: PhysicsService,
private readonly config: ConfigService,
private readonly http: HttpService,
) {}
onModuleInit() {
this.logger.log('Loading world file from environment');
this.loadWorld()
.then(() => {
this.logger.log('World file loaded');
this.physics.start(this.world);
this.startUpdateTick();
})
.catch((err) => {
this.logger.error('Failed to load world:', err.stack);
process.exit(1);
});
}
async loadWorld() {
const path = this.config.get('world').worldAsset;
const file = await lastValueFrom(this.http.get(path));
await this.deserializeLevelSave(file.data);
}
public async sendWorldToPlayer(player: Player) {
const packets: Buffer[] = [];
this.world.traverse((object) => {
if (!(object instanceof GameObject)) 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(true)), String)
.toBuffer(),
);
});
packets.forEach((packet) => player.send(packet));
}
public async initializePlayer(player: Player) {
// Streaming start
player.send(
new Packet(PacketType.STREAM_START)
.write(this.world.name, String)
.toBuffer(),
);
// TODO: assets
// Send world objects
await this.sendWorldToPlayer(player);
// Player is initialized
player.initialized = true;
// Send player list
player.send(this.players.getPlayerListPacket());
// Broadcast join
this.players.broadcast(
new Packet(PacketType.PLAYER_JOIN)
.write(player.id, String)
.write(player.name, String)
.toBuffer(),
);
// Tell player they're ready to play
player.send(
new Packet(PacketType.STREAM_FINISH)
.write(this.world.name, String)
.toBuffer(),
);
// Spawn player character
const character = player.createPlayerCharacter();
this.world.add(character);
this.physics.applyPhysics(character);
// TODO: position
this.players.broadcast(
new Packet(PacketType.PLAYER_CHARACTER)
.write(player.id, String)
.write(player.name, String)
.write(character.position, 'vec3')
.write(character.quaternion, 'quat')
.toBuffer(),
);
this.players
.getPlayerCharacterPackets(player)
.forEach((packet) => player.send(packet));
}
public createObject(object: string, setParent?: Object3D, skipEvent = false) {
const parent = setParent || this.world;
const ObjectType = instancableGameObjects[object];
if (!ObjectType) return;
const newObject = new ObjectType();
parent.add(newObject);
if (!skipEvent) {
this.players.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)
.toBuffer(),
);
}
return newObject;
}
public deserializeObject(root: SerializedObject, setParent?: Object3D) {
const parent = setParent || this.world;
if (root.objectType === 'World') {
root.children.forEach((entry) => this.recursiveCreate(entry, parent));
return parent;
}
return this.recursiveCreate(root, parent);
}
public serializeLevelSave(name: string): WorldFile {
const world = this.world.serialize();
const environment = this.environment.serialize();
return {
name,
world,
environment,
assets: [],
};
}
public async deserializeLevelSave(save: WorldFile) {
this.environment.deserialize(save.environment);
// Load world
this.deserializeObject(save.world);
this.world.deserialize(save.world);
}
private recursiveCreate(entry: SerializedObject, setParent?: Object3D) {
const parent = setParent || this.world;
const newObject = this.createObject(entry.objectType, parent, true);
newObject?.deserialize(entry);
entry.children.forEach((child) => this.recursiveCreate(child, newObject));
return newObject;
}
private broadcastWorldState() {
const data = this.physics.getObjectPackets();
data.forEach((data) =>
this.players.broadcastExcept(data, this.players.getUninitializedIds()),
);
}
private startUpdateTick() {
this.broadcastWorldStateTicker = setInterval(
() => this.broadcastWorldState(),
5000,
);
}
}