server-side persistence

This commit is contained in:
Evert Prants 2022-04-16 13:21:07 +03:00
parent 1a701cd391
commit aa70bebc32
Signed by: evert
GPG Key ID: 1688DA83D222D0B5
10 changed files with 253 additions and 29 deletions

Binary file not shown.

View File

@ -2,6 +2,31 @@
-- Up
--------------------------------------------------------------------------------
PRAGMA foreign_keys = ON;
CREATE TABLE User (
id INTEGER PRIMARY KEY,
username TEXT NOT NULL,
display_name TEXT NOT NULL,
created_at TEXT
);
CREATE TABLE Pony (
id INTEGER PRIMARY KEY,
display_name TEXT,
position TEXT,
rotation TEXT,
character TEXT,
created_at TEXT,
user_id INTEGER,
FOREIGN KEY(user_id) REFERENCES User(user_id)
);
--------------------------------------------------------------------------------
-- Down
--------------------------------------------------------------------------------
DROP TABLE User;
DROP TABLE Pony;
PRAGMA foreign_keys = OFF;

View File

@ -56,7 +56,7 @@ export class Game {
'/assets/terrain/decoration/flowers01.png',
);
await PonyModelLoader.getInstance().loadPonyModel();
await PonyModelLoader.getInstance().initialize();
await PonyEyes.getInstance().initialize();
await this.world.initialize();
@ -74,13 +74,6 @@ export class Game {
this.party = (localStorage.getItem('party')?.split('|') || []).filter(
(item) => item,
);
const character = localStorage.getItem('character')
? JSON.parse(localStorage.getItem('character'))
: {};
if (character) {
this.character = character;
}
// end of
this.chat.registerSendFunction((message) => {
@ -167,6 +160,7 @@ export class Game {
public dispose() {
this.players.forEach((player) => {
player.dispose();
this.renderer.scene.remove(player.container);
});
@ -192,10 +186,17 @@ export class Game {
console.log('connected');
});
this.socket.on('error.duplicate', () => {
this._loading.showError(
'Error: You are already connected on another device!',
);
});
this.socket.on('me', (user) => {
console.log(user);
if (!user) {
this._loading.showError('Error: You need to log in!');
window.location.href = '/login';
return;
}
@ -211,8 +212,11 @@ export class Game {
);
const player = Player.fromUser(user, this.renderer.scene);
player.setCharacter(this.character);
user.character && player.setCharacter(user.character);
user.position && player.container.position.fromArray(user.position);
user.rotation && player.container.rotation.fromArray(user.rotation);
player.setHeightSource(this.world);
this.players.push(player);
this.player = player;
this.thirdPersonCamera = new ThirdPersonCamera(
@ -357,7 +361,6 @@ export class Game {
this.character = { ...this.character, color };
this.player.setColor(color);
this.socket.emit('character', this.character);
localStorage.setItem('character', JSON.stringify(this.character));
}
if (message.startsWith('!eyecolor')) {

View File

@ -151,7 +151,7 @@ export class Player extends PonyEntity {
this._direction.x = 0;
}
if (this.keydownMap[' '] && !this._prevKeydownMap[' '] && this.onFloor) {
if (this.keydownMap[' '] && !this._prevKeydownMap[' ']) {
this.jump();
}

View File

@ -61,4 +61,10 @@ export class LoadingManagerWrapper {
this._status.innerText = 'Connected!';
this.element.classList.add('loading--complete');
}
public showError(message: string) {
this.element.classList.remove('loading--complete');
this.element.classList.add('loading--error');
this._status.innerText = message;
}
}

View File

@ -18,7 +18,7 @@ export class PonyModelLoader {
public ponyModel!: THREE.Group;
public animations!: THREE.AnimationClip[];
loadPonyModel(): Promise<THREE.Group> {
initialize(): Promise<THREE.Group> {
return new Promise((resolve, reject) => {
// Load a glTF resource
loader.load(

View File

@ -233,6 +233,12 @@ body {
&.loading--complete {
opacity: 0;
}
&.loading--error {
.loading__bar {
display: none;
}
}
}
&__bar {

View File

@ -2,6 +2,7 @@ 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';
const PLACEHOLDER_USER = (socket: Socket): IcyNetUser => {
const randomName = `player-${socket.id.substring(0, 8)}`;
@ -17,6 +18,7 @@ const PLACEHOLDER_USER = (socket: Socket): IcyNetUser => {
export class Game {
private _connections: Socket[] = [];
private _changedPlayers: number[] = [];
private _db = new Persistence();
constructor(private io: Server, private session: RequestHandler) {}
@ -29,6 +31,8 @@ export class Game {
}
async initialize() {
await this._db.initialize();
this.io.use((socket, next) =>
this.session(socket.request as any, {} as any, next as any),
);
@ -43,7 +47,7 @@ export class Game {
if (
user &&
this._connections.find((entry) => entry.data.user.id === user.id)
this._connections.find((entry) => entry.data.user?.id === user.id)
) {
socket.emit('error.duplicate');
return;
@ -51,8 +55,6 @@ export class Game {
this._connections.push(socket);
socket.emit('me', publicUserInfo);
if (user) {
socket.broadcast.emit('playerjoin', publicUserInfo);
@ -66,25 +68,11 @@ export class Game {
character: {},
};
socket.emit(
'players',
this._connections
.filter((player) => player.data.user)
.map((conn) => ({
...this.mapPlayer(conn.data.user),
...conn.data.playerinfo,
})),
);
socket.on('move', (packet) => {
socket.data.playerinfo = {
...socket.data.playerinfo,
...packet,
};
// if (!this._changedPlayers.includes(socket.data.user.id)) {
// this._changedPlayers.push(socket.data.user.id);
// }
});
socket.on('character', (info: CharacterPacket) => {
@ -103,6 +91,46 @@ export class Game {
const message = raw.trim().substring(0, 280);
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,
});
}
});
});
} else {
socket.emit('me', null);
}
socket.on('disconnect', () => {
@ -110,6 +138,7 @@ export class Game {
if (user) {
this.io.emit('playerleave', publicUserInfo);
this._db.upsertPony(user, socket.data.playerinfo);
}
});
});

View File

@ -0,0 +1,139 @@
import { join } from 'path';
import sqlite3 from 'sqlite3';
import { open, Database } from 'sqlite';
import { IcyNetUser } from '../../common/types/user';
import { DatabasePony, DatabaseUser } from '../types/database';
export class Persistence {
private db!: Database;
constructor(private _store = join(process.cwd(), 'ponies.db')) {}
async initialize(): Promise<void> {
this.db = await open({
filename: this._store,
driver: sqlite3.cached.Database,
});
await this.db.migrate();
}
async upsertUser(user: IcyNetUser): Promise<DatabaseUser> {
const existing = await this.db.get<DatabaseUser>(
`SELECT * FROM User WHERE id = ?`,
user.id,
);
if (existing) {
if (existing.display_name !== user.display_name) {
existing.display_name = user.display_name;
await this.db.run(`UPDATE User SET display_name = ? WHERE id = ?`, [
user.display_name,
user.id,
]);
}
return existing;
}
const time = new Date().toISOString();
await this.db.run(
`INSERT INTO User (id, username, display_name, created_at) VALUES (?,?,?,?)`,
[user.id, user.username, user.display_name, time],
);
return { ...user, created_at: time };
}
async upsertPony(
user: IcyNetUser,
data: DatabasePony,
): Promise<DatabasePony> {
const pony = await this.db.get<DatabasePony>(
'SELECT * FROM Pony WHERE user_id = ?',
[user.id],
);
const updateData = this.serializePony({ ...pony, ...data });
if (pony) {
await this.db.run(
`UPDATE Pony SET display_name = ?, position = ?, rotation = ?, character = ? WHERE user_id = ?`,
[
user.display_name,
updateData.position,
updateData.rotation,
updateData.character,
user.id,
],
);
return this.deserializePony(updateData);
}
updateData.created_at = new Date().toISOString();
updateData.user_id = user.id;
const keys = this.filterPonyKeys(updateData);
const insert = Array.from({ length: keys.length }, () => '?').join(',');
await this.db.run(
`INSERT INTO Pony (${keys.join(',')}) VALUES (${insert})`,
keys.map((key) => (updateData as any)[key]),
);
return this.deserializePony(updateData);
}
async getUserPony(user: IcyNetUser): Promise<DatabasePony | null> {
const result = await this.db.get<DatabasePony>(
'SELECT * FROM Pony WHERE user_id = ?',
user.id,
);
return result ? this.deserializePony(result) : null;
}
private serializePony(data: DatabasePony): DatabasePony {
return {
...data,
position: Array.isArray(data.position)
? JSON.stringify(data.position)
: data.position,
rotation: Array.isArray(data.rotation)
? JSON.stringify(data.rotation)
: data.rotation,
character:
typeof data.character !== 'string'
? JSON.stringify(data.character)
: data.character,
};
}
private deserializePony(data: DatabasePony): DatabasePony {
return {
...data,
position: Array.isArray(data.position)
? data.position
: JSON.parse(data.position as string),
rotation: Array.isArray(data.rotation)
? data.rotation
: JSON.parse(data.rotation as string),
character:
typeof data.character !== 'string'
? data.character
: JSON.parse(data.character as string),
};
}
private filterPonyKeys(data: DatabasePony): string[] {
const keys = [
'display_name',
'position',
'rotation',
'character',
'created_at',
'user_id',
];
return Object.keys(data).reduce<string[]>(
(list, current) => (keys.includes(current) ? [...list, current] : list),
[],
);
}
}

View File

@ -0,0 +1,16 @@
import { CharacterPacket } from '../../common/types/packet';
import { IcyNetUser } from '../../common/types/user';
export interface DatabaseUser extends IcyNetUser {
created_at: string;
}
export interface DatabasePony {
id?: number;
display_name?: string;
position?: string | number[];
rotation?: string | number[];
character?: string | CharacterPacket;
created_at?: string;
user_id?: number;
}