From aa70bebc32a6864d2feb93bacf306e7529d5a731 Mon Sep 17 00:00:00 2001 From: Evert Prants Date: Sat, 16 Apr 2022 13:21:07 +0300 Subject: [PATCH] server-side persistence --- assets/clean-pony2fix.glb | Bin 436748 -> 435192 bytes migrations/001-initial.sql | 25 ++++ src/client/game.ts | 23 +-- src/client/object/player.ts | 2 +- src/client/object/resource/loading-manager.ts | 6 + src/client/object/resource/pony-loader.ts | 2 +- src/client/scss/index.scss | 6 + src/server/game.ts | 63 +++++--- src/server/object/persistence.ts | 139 ++++++++++++++++++ src/server/types/database.ts | 16 ++ 10 files changed, 253 insertions(+), 29 deletions(-) create mode 100644 src/server/object/persistence.ts create mode 100644 src/server/types/database.ts diff --git a/assets/clean-pony2fix.glb b/assets/clean-pony2fix.glb index 2cf27a8a66288ed4142b790f15db6bc1033d8eea..ce35692ba20e96927c6bdd9aa0970820e247438a 100644 GIT binary patch delta 5799 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 delta 7424 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 diff --git a/migrations/001-initial.sql b/migrations/001-initial.sql index ddcbb9f..8a07582 100644 --- a/migrations/001-initial.sql +++ b/migrations/001-initial.sql @@ -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; diff --git a/src/client/game.ts b/src/client/game.ts index 3ea43f1..a3742a7 100644 --- a/src/client/game.ts +++ b/src/client/game.ts @@ -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')) { diff --git a/src/client/object/player.ts b/src/client/object/player.ts index 751fd1b..5c97afc 100644 --- a/src/client/object/player.ts +++ b/src/client/object/player.ts @@ -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(); } diff --git a/src/client/object/resource/loading-manager.ts b/src/client/object/resource/loading-manager.ts index d9ae66e..52e4d26 100644 --- a/src/client/object/resource/loading-manager.ts +++ b/src/client/object/resource/loading-manager.ts @@ -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; + } } diff --git a/src/client/object/resource/pony-loader.ts b/src/client/object/resource/pony-loader.ts index 9a7277c..56b09cb 100644 --- a/src/client/object/resource/pony-loader.ts +++ b/src/client/object/resource/pony-loader.ts @@ -18,7 +18,7 @@ export class PonyModelLoader { public ponyModel!: THREE.Group; public animations!: THREE.AnimationClip[]; - loadPonyModel(): Promise { + initialize(): Promise { return new Promise((resolve, reject) => { // Load a glTF resource loader.load( diff --git a/src/client/scss/index.scss b/src/client/scss/index.scss index e182f76..8b2a5d4 100644 --- a/src/client/scss/index.scss +++ b/src/client/scss/index.scss @@ -233,6 +233,12 @@ body { &.loading--complete { opacity: 0; } + + &.loading--error { + .loading__bar { + display: none; + } + } } &__bar { diff --git a/src/server/game.ts b/src/server/game.ts index 1bda4f0..6cda4ed 100644 --- a/src/server/game.ts +++ b/src/server/game.ts @@ -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); } }); }); diff --git a/src/server/object/persistence.ts b/src/server/object/persistence.ts new file mode 100644 index 0000000..62abe1e --- /dev/null +++ b/src/server/object/persistence.ts @@ -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 { + this.db = await open({ + filename: this._store, + driver: sqlite3.cached.Database, + }); + + await this.db.migrate(); + } + + async upsertUser(user: IcyNetUser): Promise { + const existing = await this.db.get( + `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 { + const pony = await this.db.get( + '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 { + const result = await this.db.get( + '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( + (list, current) => (keys.includes(current) ? [...list, current] : list), + [], + ); + } +} diff --git a/src/server/types/database.ts b/src/server/types/database.ts new file mode 100644 index 0000000..c4960f9 --- /dev/null +++ b/src/server/types/database.ts @@ -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; +}