server-side persistence
This commit is contained in:
parent
1a701cd391
commit
aa70bebc32
Binary file not shown.
@ -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;
|
||||
|
@ -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')) {
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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(
|
||||
|
@ -233,6 +233,12 @@ body {
|
||||
&.loading--complete {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
&.loading--error {
|
||||
.loading__bar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__bar {
|
||||
|
@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
139
src/server/object/persistence.ts
Normal file
139
src/server/object/persistence.ts
Normal 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),
|
||||
[],
|
||||
);
|
||||
}
|
||||
}
|
16
src/server/types/database.ts
Normal file
16
src/server/types/database.ts
Normal 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;
|
||||
}
|
Loading…
Reference in New Issue
Block a user