small server refacto

This commit is contained in:
Evert Prants 2022-04-17 16:00:13 +03:00
parent c900e81fb8
commit 78374144a8
Signed by: evert
GPG Key ID: 1688DA83D222D0B5
21 changed files with 1589 additions and 143 deletions

1292
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -16,6 +16,7 @@
"connect-redis": "^6.1.3",
"express": "^4.17.3",
"express-session": "^1.17.2",
"jimp": "^0.16.1",
"passport": "^0.5.2",
"passport-icynet": "^0.0.2",
"redis": "^3.1.2",

View File

@ -1,9 +1,8 @@
import { Socket } from 'socket.io-client';
import { Color, DoubleSide, MeshBasicMaterial, Vector3 } from 'three';
import { clamp, isMobileOrTablet } from '../common/helper';
import { isMobileOrTablet } from '../common/helper';
import { CharacterPacket, CompositePacket } from '../common/types/packet';
import { IcyNetUser } from '../common/types/user';
import { ThirdPersonCamera } from './object/camera';
import { Chat } from './object/chat';
import { Joystick } from './object/joystick';
import { PonyModelLoader } from './object/resource/pony-loader';
@ -24,7 +23,6 @@ export class Game {
public players: (Player | PlayerEntity)[] = [];
public player!: Player;
public me!: IcyNetUser;
public thirdPersonCamera!: ThirdPersonCamera;
public joystick!: Joystick;
public chat!: Chat;
private _loading = LoadingManagerWrapper.getInstance();
@ -153,7 +151,6 @@ export class Game {
public toggleCamLock(): boolean {
this._locked = !this._locked;
this.thirdPersonCamera?.setLock(this._locked);
this.player?.setCameraLock(this._locked);
return this._locked;
}
@ -164,7 +161,6 @@ export class Game {
this.renderer.scene.remove(player.container);
});
this.thirdPersonCamera?.dispose();
this.joystick?.dispose();
this.players.length = 0;
@ -173,7 +169,6 @@ export class Game {
public update(dt: number) {
this.players.forEach((player) => player.update(dt));
this.player?.createPacket(this.socket);
this.thirdPersonCamera?.update(dt);
this.joystick?.update(dt);
this.player && this.world?.update(this.player.container.position);
@ -214,22 +209,10 @@ export class Game {
user.position && player.container.position.fromArray(user.position);
user.rotation && player.container.rotation.fromArray(user.rotation);
player.setHeightSource(this.world);
player.setCamera(this.renderer);
this.players.push(player);
this.player = player;
this.thirdPersonCamera = new ThirdPersonCamera(
this.renderer.camera,
this.player.container,
this.renderer.canvas,
);
this.thirdPersonCamera.initialize();
this.thirdPersonCamera.registerAltMoveFunction((x, y) => {
this.player.angularVelocity.set(
0,
clamp(x * 0.5, -Math.PI, Math.PI),
0,
);
});
this.joystick = new Joystick(player);
this.joystick.initialize();

View File

@ -7,6 +7,7 @@ import {
import { CanvasUtils } from './canvas-utils';
import { ChatBubble } from './chat-bubble';
import { Vector3, Scene, Euler } from 'three';
import { clamp } from '../../common/helper';
const chatBuilder = new CanvasUtils({
rounded: true,
@ -139,7 +140,7 @@ export class PlayerEntity extends PonyEntity {
this.setRotation(new Euler().fromArray(packet.rotation));
}
if (packet.animState) {
if (packet.animState !== undefined) {
this.setWalkAnimationState(packet.animState);
}
}
@ -173,7 +174,7 @@ export class PlayerEntity extends PonyEntity {
this._pf.copy(this._p1);
this._qf.copy(this._q1);
const t = Math.max(Math.min(this._targetFrame.time / 0.1, 1.0), 0.0);
const t = clamp(this._targetFrame.time / 0.1, 0.0, 1.0);
this._pf.lerp(this._p2, t);
this._qf.lerp(this._q2, t);

View File

@ -2,8 +2,13 @@ import { IcyNetUser } from '../../common/types/user';
import { Socket } from 'socket.io-client';
import { PonyEntity } from './model/pony';
import { Scene, Vector2, Vector3 } from 'three';
import { ThirdPersonCamera } from './camera';
import { clamp } from '../../common/helper';
import { Renderer } from '../renderer';
export class Player extends PonyEntity {
public thirdPersonCamera!: ThirdPersonCamera;
public keydownMap: { [x: string]: boolean } = {};
private _prevKeydownMap: { [x: string]: boolean } = {};
@ -66,6 +71,23 @@ export class Player extends PonyEntity {
});
}
dispose(): void {
super.dispose();
this.thirdPersonCamera?.dispose();
}
public setCamera(renderer: Renderer) {
this.thirdPersonCamera = new ThirdPersonCamera(
renderer.camera,
this.container,
renderer.renderer.domElement,
);
this.thirdPersonCamera.initialize();
this.thirdPersonCamera.registerAltMoveFunction((x, y) => {
this.angularVelocity.set(0, clamp(x * 0.5, -Math.PI, Math.PI), 0);
});
}
public createPacket(socket: Socket): void {
if (Object.keys(this.changes).length) {
socket.emit('move', this.changes);
@ -157,11 +179,13 @@ export class Player extends PonyEntity {
this.moveCharacter(dt);
super.update(dt);
this.thirdPersonCamera?.update(dt);
this._prevKeydownMap = { ...this.keydownMap };
}
public setCameraLock(isLocked: boolean) {
this.thirdPersonCamera?.setLock(isLocked);
this._cameraLock = isLocked;
}
}

View File

@ -1,5 +1,5 @@
import { ShaderMaterial, UniformsLib, UniformsUtils } from 'three';
import { WorldChunk } from '../../../common/world/WorldChunk';
import { WorldChunk } from '../../../common/world/world-chunk';
import { ClientWorldTexture } from './client-world-texture';
// Adapted from the Lambert Material shader

View File

@ -1,5 +1,5 @@
import { ImageLoader } from 'three';
import { WorldLoader } from '../../../common/world/WorldLoader';
import { WorldLoader } from '../../../common/world/world-loader';
import { CanvasUtils } from '../canvas-utils';
import { LoadingManagerWrapper } from '../resource/loading-manager';

View File

@ -1,7 +1,7 @@
import {
WorldManifest,
WorldManifestRegion,
} from '../../../common/world/WorldManifest';
} from '../../../common/world/world-manifest';
export class ClientWorldManifest implements WorldManifest {
constructor(

View File

@ -1,6 +1,6 @@
import { Object3D, Vector2, Vector3 } from 'three';
import { WorldChunk } from '../../../common/world/WorldChunk';
import { WorldManager } from '../../../common/world/WorldManager';
import { WorldChunk } from '../../../common/world/world-chunk';
import { WorldManager } from '../../../common/world/world-manager';
import { ClientWorldChunkShader } from './client-world-chunk-shader';
import { ClientWorldTexture } from './client-world-texture';
import { QuadtreeMesher } from './quadtree/quadtree-mesher';

View File

@ -9,7 +9,7 @@ export function debounce(func: Function, timeout = 300) {
return (...args: any[]) => {
clearTimeout(timer);
timer = setTimeout(() => {
func.apply(this, args);
func.apply(null, args);
}, timeout);
};
}

View File

@ -3,7 +3,7 @@ import { IcyNetUser } from './user';
export interface PositionUpdatePacket {
id?: number;
position?: number[];
rotation?: (number | string)[];
rotation?: number[];
animState?: number;
time?: number;
}
@ -12,7 +12,7 @@ export interface FullStatePacket {
velocity?: number[];
angular?: number[];
position?: number[];
rotation?: (number | string)[];
rotation?: number[];
animState?: number;
character?: CharacterPacket;
}

View File

@ -1,7 +1,7 @@
import { Vector3, Vector2 } from 'three';
import { to1D } from '../convert';
import { barycentricPoint } from '../helper';
import { WorldManifestRegion } from './WorldManifest';
import { WorldManifestRegion } from './world-manifest';
export class WorldChunk {
constructor(

View File

@ -1,7 +1,7 @@
import { to1D } from '../convert';
import { WorldChunk } from './WorldChunk';
import { WorldLoader } from './WorldLoader';
import { WorldManifest } from './WorldManifest';
import { WorldChunk } from './world-chunk';
import { WorldLoader } from './world-loader';
import { WorldManifest } from './world-manifest';
export class WorldManager {
protected _chunks!: WorldChunk[];
@ -34,7 +34,7 @@ export class WorldManager {
chunkX,
chunkY,
this.worldChunkSize,
this.manifest.regionMap.find(({ x, y }) => x === chunkX && y === chunkY),
this.manifest.regionMap.find(({ x, y }) => x === chunkX && y === chunkY)!,
);
}
@ -46,6 +46,8 @@ export class WorldManager {
}
}
async initialize() {}
getPointHeight(x: number, y: number): number {
const chunkX = Math.floor(x / this.worldChunkSize);
const chunkY = Math.floor(y / this.worldChunkSize);

3
src/server/constants.ts Normal file
View File

@ -0,0 +1,3 @@
import { join } from 'path';
export const ASSETS = join(__dirname, '..', '..', 'assets');

View File

@ -1,9 +1,12 @@
import { Server, Socket } from 'socket.io';
import { RequestHandler } from 'express';
import { IcyNetUser } from '../common/types/user';
import { CharacterPacket, PositionUpdatePacket } from '../common/types/packet';
import { Persistence } from './object/persistence';
import { Logger } from './object/logger';
import { ServerWorld } from './world/server-world';
import { ServerWorldLoader } from './world/server-world-loader';
import { ServerWorldManifest } from './world/server-world-manifest';
import { Player } from './object/player';
const PLACEHOLDER_USER = (socket: Socket): IcyNetUser => {
const randomName = `player-${socket.id.substring(0, 8)}`;
@ -17,23 +20,19 @@ const PLACEHOLDER_USER = (socket: Socket): IcyNetUser => {
};
export class Game {
private _connections: Socket[] = [];
private _changedPlayers: number[] = [];
private _players: Player[] = [];
private _db = new Persistence();
private _log = new Logger();
private _world!: ServerWorld;
constructor(private io: Server, private session: RequestHandler) {}
private mapPlayer(user: IcyNetUser): any {
return {
id: user.id,
username: user.username,
display_name: user.display_name,
};
}
async initialize() {
const manifest = await ServerWorldManifest.loadManifest();
this._world = new ServerWorld(new ServerWorldLoader(), manifest);
await this._db.initialize();
await this._world.initialize();
this._log.initialize();
this.io.use((socket, next) =>
@ -46,92 +45,38 @@ export class Game {
process.env.SKIP_LOGIN === 'true'
? PLACEHOLDER_USER(socket)
: (session?.passport?.user as IcyNetUser);
const publicUserInfo = user ? this.mapPlayer(user) : null;
if (
user &&
this._connections.find((entry) => entry.data.user?.id === user.id)
) {
if (user && this._players.find((entry) => entry.user?.id === user.id)) {
socket.emit('error.duplicate');
return;
}
this._connections.push(socket);
if (user) {
socket.broadcast.emit('playerjoin', publicUserInfo);
const player = new Player(socket, this._db, user);
this._players.push(player);
this._log.writeEvent('PlayerJoin', `<${user.display_name}:${user.id}>`);
socket.data.user = user;
socket.data.playerinfo = {
velocity: [0, 0, 0],
angular: [0, 0, 0, 'XYZ'],
position: [0, 0, 0],
rotation: [0, 0, 0, 'XYZ'],
animState: 0,
character: {},
};
socket.on('move', (packet) => {
socket.data.playerinfo = {
...socket.data.playerinfo,
...packet,
};
});
socket.on('character', (info: CharacterPacket) => {
socket.data.playerinfo.character = {
...socket.data.playerinfo.character,
...info,
};
this.io.emit('playercharacter', {
id: socket.data.user.id,
...socket.data.playerinfo.character,
});
socket.data.player = player;
player.initialize().then(() => {
// Send player list to new player
socket.emit(
'players',
this._players
.filter((player) => player.user)
.map((player) => ({
...player.getPublicUserProfile(),
...player.toSave(),
})),
);
});
socket.on('chat-send', (raw) => {
const message = raw.trim().substring(0, 280);
this._log.writeChat(user, message);
this.io.emit('chat', { sender: publicUserInfo, message });
});
this._db.upsertUser(user).then((user) => {
this._db.getUserPony(user).then((pony) => {
if (pony) {
socket.data.playerinfo.character =
(pony.character as CharacterPacket) || {};
socket.data.playerinfo.position = pony.position;
socket.data.playerinfo.rotation = pony.rotation;
} else {
this._db.upsertPony(user, socket.data.playerinfo);
}
socket.emit('me', {
...publicUserInfo,
character: socket.data.playerinfo.character,
position: pony?.position,
rotation: pony?.rotation,
});
socket.emit(
'players',
this._connections
.filter((player) => player.data.user)
.map((conn) => ({
...this.mapPlayer(conn.data.user),
...conn.data.playerinfo,
})),
);
if (pony) {
this.io.emit('playercharacter', {
id: socket.data.user.id,
...socket.data.playerinfo.character,
});
}
this.io.emit('chat', {
sender: player.getPublicUserProfile(),
message,
});
});
} else {
@ -139,35 +84,34 @@ export class Game {
}
socket.on('disconnect', () => {
this._connections.splice(this._connections.indexOf(socket), 1);
if (user) {
this.io.emit('playerleave', publicUserInfo);
const player = this._players.find(
(player) => player.user.id === user.id,
);
if (!player) {
return;
}
this.io.emit('playerleave', player.getPublicUserProfile());
this._log.writeEvent(
'PlayerLeave',
`<${user.display_name}:${user.id}>`,
);
this._db.upsertPony(user, socket.data.playerinfo);
player.dispose();
this._players.splice(this._players.indexOf(player), 1);
}
});
});
setInterval(() => {
const playerInfo: PositionUpdatePacket[] = [];
this._connections
.filter(
(conn) => conn.data.user, // && this._changedPlayers.includes(conn.data.user.id),
)
.forEach((conn) =>
playerInfo.push({
position: conn.data.playerinfo.position,
rotation: conn.data.playerinfo.rotation,
animState: conn.data.playerinfo.animState,
id: conn.data.user.id,
}),
);
this.io.emit('playerupdate', playerInfo);
this._changedPlayers.length = 0;
this._players.forEach((player) => player.update(1000 / 60));
this.io.emit(
'playerupdate',
this._players.map((player) => player.toUpdatePacket()),
);
}, 1000 / 60);
}
}

129
src/server/object/player.ts Normal file
View File

@ -0,0 +1,129 @@
import { Socket } from 'socket.io';
import { Vector3 } from 'three';
import {
CharacterPacket,
FullStatePacket,
PositionUpdatePacket,
} from '../../common/types/packet';
import { IcyNetUser } from '../../common/types/user';
import { DatabasePony } from '../types/database';
import { Persistence } from './persistence';
export class Player {
public position = new Vector3();
public rotation = new Vector3();
public velocity = new Vector3();
public angularVelocity = new Vector3();
public animState = 0;
public character: CharacterPacket = {};
constructor(
private socket: Socket,
private db: Persistence,
public user: IcyNetUser,
) {}
async initialize() {
this.socket.on('move', (packet: FullStatePacket) => {
this.fromPacket(packet);
});
this.socket.on('character', (info: CharacterPacket) => {
this.setCharacter(info);
this.socket.broadcast.emit('playercharacter', {
id: this.user.id,
...this.getCharacter(),
});
});
const user = await this.db.upsertUser(this.user);
const pony = await this.db.getUserPony(user);
if (pony) {
this.fromSave(pony);
} else {
await this.db.upsertPony(user, this.toSave());
}
this.socket.emit('me', {
...this.getPublicUserProfile(),
...this.toSave(),
});
this.socket.broadcast.emit('playerjoin', this.getPublicUserProfile());
if (pony && pony.character) {
this.socket.broadcast.emit('playercharacter', {
id: this.user.id,
character: this.getCharacter(),
});
}
}
async dispose() {
await this.db.upsertPony(this.user, this.toSave());
}
update(dt: number) {}
fromPacket(packet: FullStatePacket | PositionUpdatePacket) {
if ((packet as FullStatePacket).angular) {
this.angularVelocity.fromArray((packet as FullStatePacket).angular!);
}
if ((packet as FullStatePacket).velocity) {
this.velocity.fromArray((packet as FullStatePacket).velocity!);
}
if (packet.position) {
this.position.fromArray(packet.position);
}
if (packet.rotation) {
this.rotation.fromArray(packet.rotation as number[]);
}
if (packet.animState !== undefined) {
this.animState = packet.animState;
}
}
fromSave(save: DatabasePony) {
save.position && this.position.fromArray(save.position as number[]);
save.rotation && this.rotation.fromArray(save.rotation as number[]);
save.character && this.setCharacter(save.character as CharacterPacket);
}
toSave(): DatabasePony {
return {
...this.toUpdatePacket(),
character: this.getCharacter(),
};
}
toUpdatePacket(): PositionUpdatePacket {
return {
id: this.user.id,
position: this.position.toArray(),
rotation: this.rotation.toArray(),
animState: this.animState,
};
}
getPublicUserProfile(): Partial<IcyNetUser> {
return {
id: this.user.id,
username: this.user.username,
display_name: this.user.display_name,
};
}
setCharacter(packet: CharacterPacket) {
this.character = { ...this.character, ...packet };
}
getCharacter(): CharacterPacket {
return this.character;
}
}

View File

@ -0,0 +1,32 @@
import { WorldLoader } from '../../common/world/world-loader';
import { join } from 'path';
import jimp from 'jimp';
import { ASSETS } from '../constants';
import { to1D } from '../../common/convert';
export class ServerWorldLoader implements WorldLoader {
constructor(private _path = join(ASSETS, 'terrain', 'region')) {}
async loadHeightMap(
chunkX: number,
chunkY: number,
scale: number,
): Promise<number[]> {
console.log(`<WORLD> Loading region ${chunkX}, ${chunkY}..`);
const regionHeight = join(this._path, `height-${chunkX}-${chunkY}.png`);
const image = await jimp.read(regionHeight);
const width = image.getWidth();
const height = image.getHeight();
const heights = new Array(width * height);
for (let x = 0; x < width; x++) {
for (let y = 0; y < height; y++) {
const id = to1D(x, y, width);
const rgb = (image.getPixelColor(x, y) - 255) / 256;
heights[id] = (((rgb >> 16) & 255) * scale) / 255;
}
}
return heights;
}
}

View File

@ -0,0 +1,39 @@
import {
WorldManifest,
WorldManifestRegion,
} from '../../common/world/world-manifest';
import { join } from 'path';
import { readFile } from 'fs/promises';
import { ASSETS } from '../constants';
const manifestAsset = join(ASSETS, 'terrain', 'manifest.json');
export class ServerWorldManifest implements WorldManifest {
constructor(
public worldWidth: number,
public worldHeight: number,
public worldChunkSize: number,
public worldHeightScale: number,
public textureBombingNoise: string,
public regionMap: WorldManifestRegion[],
) {}
public static async loadManifest(): Promise<ServerWorldManifest> {
console.log(`<WORLD> Loading manifest..`);
const file = await readFile(manifestAsset, { encoding: 'utf-8' });
const json = JSON.parse(file);
const manifest = new ServerWorldManifest(
json.worldWidth,
json.worldHeight,
json.worldChunkSize,
json.worldHeightScale,
json.textureBombingNoise,
json.regionMap,
);
console.log(
`<WORLD> Loaded manifest: ${manifest.worldWidth} x ${manifest.worldHeight} ^ ${manifest.worldHeightScale}, ${manifest.regionMap.length} regions`,
);
return manifest;
}
}

View File

@ -0,0 +1,8 @@
import { WorldManager } from '../../common/world/world-manager';
export class ServerWorld extends WorldManager {
async initialize(): Promise<void> {
await this.loadWorld();
console.log('<WORLD> World has been loaded.');
}
}