191 lines
5.6 KiB
TypeScript
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,
|
|
);
|
|
}
|
|
}
|