server-side persistence
This commit is contained in:
parent
1a701cd391
commit
aa70bebc32
Binary file not shown.
@ -2,6 +2,31 @@
|
|||||||
-- Up
|
-- 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
|
-- Down
|
||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
DROP TABLE User;
|
||||||
|
DROP TABLE Pony;
|
||||||
|
|
||||||
|
PRAGMA foreign_keys = OFF;
|
||||||
|
@ -56,7 +56,7 @@ export class Game {
|
|||||||
'/assets/terrain/decoration/flowers01.png',
|
'/assets/terrain/decoration/flowers01.png',
|
||||||
);
|
);
|
||||||
|
|
||||||
await PonyModelLoader.getInstance().loadPonyModel();
|
await PonyModelLoader.getInstance().initialize();
|
||||||
await PonyEyes.getInstance().initialize();
|
await PonyEyes.getInstance().initialize();
|
||||||
await this.world.initialize();
|
await this.world.initialize();
|
||||||
|
|
||||||
@ -74,13 +74,6 @@ export class Game {
|
|||||||
this.party = (localStorage.getItem('party')?.split('|') || []).filter(
|
this.party = (localStorage.getItem('party')?.split('|') || []).filter(
|
||||||
(item) => item,
|
(item) => item,
|
||||||
);
|
);
|
||||||
|
|
||||||
const character = localStorage.getItem('character')
|
|
||||||
? JSON.parse(localStorage.getItem('character'))
|
|
||||||
: {};
|
|
||||||
if (character) {
|
|
||||||
this.character = character;
|
|
||||||
}
|
|
||||||
// end of
|
// end of
|
||||||
|
|
||||||
this.chat.registerSendFunction((message) => {
|
this.chat.registerSendFunction((message) => {
|
||||||
@ -167,6 +160,7 @@ export class Game {
|
|||||||
|
|
||||||
public dispose() {
|
public dispose() {
|
||||||
this.players.forEach((player) => {
|
this.players.forEach((player) => {
|
||||||
|
player.dispose();
|
||||||
this.renderer.scene.remove(player.container);
|
this.renderer.scene.remove(player.container);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -192,10 +186,17 @@ export class Game {
|
|||||||
console.log('connected');
|
console.log('connected');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.socket.on('error.duplicate', () => {
|
||||||
|
this._loading.showError(
|
||||||
|
'Error: You are already connected on another device!',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
this.socket.on('me', (user) => {
|
this.socket.on('me', (user) => {
|
||||||
console.log(user);
|
console.log(user);
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
|
this._loading.showError('Error: You need to log in!');
|
||||||
window.location.href = '/login';
|
window.location.href = '/login';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -211,8 +212,11 @@ export class Game {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const player = Player.fromUser(user, this.renderer.scene);
|
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);
|
player.setHeightSource(this.world);
|
||||||
|
|
||||||
this.players.push(player);
|
this.players.push(player);
|
||||||
this.player = player;
|
this.player = player;
|
||||||
this.thirdPersonCamera = new ThirdPersonCamera(
|
this.thirdPersonCamera = new ThirdPersonCamera(
|
||||||
@ -357,7 +361,6 @@ export class Game {
|
|||||||
this.character = { ...this.character, color };
|
this.character = { ...this.character, color };
|
||||||
this.player.setColor(color);
|
this.player.setColor(color);
|
||||||
this.socket.emit('character', this.character);
|
this.socket.emit('character', this.character);
|
||||||
localStorage.setItem('character', JSON.stringify(this.character));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (message.startsWith('!eyecolor')) {
|
if (message.startsWith('!eyecolor')) {
|
||||||
|
@ -151,7 +151,7 @@ export class Player extends PonyEntity {
|
|||||||
this._direction.x = 0;
|
this._direction.x = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.keydownMap[' '] && !this._prevKeydownMap[' '] && this.onFloor) {
|
if (this.keydownMap[' '] && !this._prevKeydownMap[' ']) {
|
||||||
this.jump();
|
this.jump();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -61,4 +61,10 @@ export class LoadingManagerWrapper {
|
|||||||
this._status.innerText = 'Connected!';
|
this._status.innerText = 'Connected!';
|
||||||
this.element.classList.add('loading--complete');
|
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 ponyModel!: THREE.Group;
|
||||||
public animations!: THREE.AnimationClip[];
|
public animations!: THREE.AnimationClip[];
|
||||||
|
|
||||||
loadPonyModel(): Promise<THREE.Group> {
|
initialize(): Promise<THREE.Group> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
// Load a glTF resource
|
// Load a glTF resource
|
||||||
loader.load(
|
loader.load(
|
||||||
|
@ -233,6 +233,12 @@ body {
|
|||||||
&.loading--complete {
|
&.loading--complete {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.loading--error {
|
||||||
|
.loading__bar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__bar {
|
&__bar {
|
||||||
|
@ -2,6 +2,7 @@ import { Server, Socket } from 'socket.io';
|
|||||||
import { RequestHandler } from 'express';
|
import { RequestHandler } from 'express';
|
||||||
import { IcyNetUser } from '../common/types/user';
|
import { IcyNetUser } from '../common/types/user';
|
||||||
import { CharacterPacket, PositionUpdatePacket } from '../common/types/packet';
|
import { CharacterPacket, PositionUpdatePacket } from '../common/types/packet';
|
||||||
|
import { Persistence } from './object/persistence';
|
||||||
|
|
||||||
const PLACEHOLDER_USER = (socket: Socket): IcyNetUser => {
|
const PLACEHOLDER_USER = (socket: Socket): IcyNetUser => {
|
||||||
const randomName = `player-${socket.id.substring(0, 8)}`;
|
const randomName = `player-${socket.id.substring(0, 8)}`;
|
||||||
@ -17,6 +18,7 @@ const PLACEHOLDER_USER = (socket: Socket): IcyNetUser => {
|
|||||||
export class Game {
|
export class Game {
|
||||||
private _connections: Socket[] = [];
|
private _connections: Socket[] = [];
|
||||||
private _changedPlayers: number[] = [];
|
private _changedPlayers: number[] = [];
|
||||||
|
private _db = new Persistence();
|
||||||
|
|
||||||
constructor(private io: Server, private session: RequestHandler) {}
|
constructor(private io: Server, private session: RequestHandler) {}
|
||||||
|
|
||||||
@ -29,6 +31,8 @@ export class Game {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async initialize() {
|
async initialize() {
|
||||||
|
await this._db.initialize();
|
||||||
|
|
||||||
this.io.use((socket, next) =>
|
this.io.use((socket, next) =>
|
||||||
this.session(socket.request as any, {} as any, next as any),
|
this.session(socket.request as any, {} as any, next as any),
|
||||||
);
|
);
|
||||||
@ -43,7 +47,7 @@ export class Game {
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
user &&
|
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');
|
socket.emit('error.duplicate');
|
||||||
return;
|
return;
|
||||||
@ -51,8 +55,6 @@ export class Game {
|
|||||||
|
|
||||||
this._connections.push(socket);
|
this._connections.push(socket);
|
||||||
|
|
||||||
socket.emit('me', publicUserInfo);
|
|
||||||
|
|
||||||
if (user) {
|
if (user) {
|
||||||
socket.broadcast.emit('playerjoin', publicUserInfo);
|
socket.broadcast.emit('playerjoin', publicUserInfo);
|
||||||
|
|
||||||
@ -66,25 +68,11 @@ export class Game {
|
|||||||
character: {},
|
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.on('move', (packet) => {
|
||||||
socket.data.playerinfo = {
|
socket.data.playerinfo = {
|
||||||
...socket.data.playerinfo,
|
...socket.data.playerinfo,
|
||||||
...packet,
|
...packet,
|
||||||
};
|
};
|
||||||
|
|
||||||
// if (!this._changedPlayers.includes(socket.data.user.id)) {
|
|
||||||
// this._changedPlayers.push(socket.data.user.id);
|
|
||||||
// }
|
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('character', (info: CharacterPacket) => {
|
socket.on('character', (info: CharacterPacket) => {
|
||||||
@ -103,6 +91,46 @@ export class Game {
|
|||||||
const message = raw.trim().substring(0, 280);
|
const message = raw.trim().substring(0, 280);
|
||||||
this.io.emit('chat', { sender: publicUserInfo, 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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
socket.emit('me', null);
|
||||||
}
|
}
|
||||||
|
|
||||||
socket.on('disconnect', () => {
|
socket.on('disconnect', () => {
|
||||||
@ -110,6 +138,7 @@ export class Game {
|
|||||||
|
|
||||||
if (user) {
|
if (user) {
|
||||||
this.io.emit('playerleave', publicUserInfo);
|
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