refactor: structure
This commit is contained in:
parent
b1a9975b89
commit
0a16f10889
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -118,3 +118,4 @@ dist
|
|||
# local
|
||||
/dist
|
||||
/packages/*/yarn.lock
|
||||
/universe
|
||||
|
|
|
@ -111,11 +111,11 @@
|
|||
- '@flecks/repl'
|
||||
'@flecks/socket': {}
|
||||
'@flecks/socket/server':
|
||||
authenticate:
|
||||
- '@flecks/user/session/server'
|
||||
- '@flecks/user/server'
|
||||
connect:
|
||||
- '@flecks/socket/server'
|
||||
'request.socket':
|
||||
- '@flecks/user/session'
|
||||
- '@flecks/user'
|
||||
- '@flecks/governor'
|
||||
- '@humus/universe'
|
||||
'@flecks/user': {}
|
||||
'@flecks/user/local': {}
|
||||
'@flecks/user/session': {}
|
||||
|
@ -125,6 +125,4 @@
|
|||
'@humus/farm': {}
|
||||
'@humus/inventory': {}
|
||||
'@humus/universe':
|
||||
root: '../persea/projects/c41ddaac-89c2-46a4-b3e5-1d634a1a7c36'
|
||||
'@humus/universe/server':
|
||||
running: 'c41ddaac-89c2-46a4-b3e5-1d634a1a7c36'
|
||||
resource: '../persea/projects/c41ddaac-89c2-46a4-b3e5-1d634a1a7c36'
|
||||
|
|
|
@ -1,13 +1,9 @@
|
|||
import {Hooks} from '@flecks/core';
|
||||
|
||||
import Receiver from './receiver';
|
||||
import Universe from '../components';
|
||||
import Title from '../components/title';
|
||||
|
||||
export default {
|
||||
[Hooks]: {
|
||||
'@humus/app.components': () => Universe,
|
||||
'@humus/app.title': () => Title,
|
||||
'@flecks/web/client.up': (flecks) => {
|
||||
window.flecks = flecks;
|
||||
const Synchronizer = Receiver(flecks);
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
import Join from '../../packets/join';
|
||||
|
||||
export default () => class ClientJoin extends Join() {};
|
|
@ -1,24 +1,23 @@
|
|||
import {Hooks, Flecks} from '@flecks/core';
|
||||
|
||||
import Universe from './components';
|
||||
import Title from './components/title';
|
||||
import {universes} from './state';
|
||||
|
||||
export * from './state';
|
||||
|
||||
const clientPackets = Flecks.provide(require.context('./client/packets', false, /\.js$/));
|
||||
const serverPackets = Flecks.provide(require.context('./server/packets', false, /\.js$/));
|
||||
|
||||
export default {
|
||||
[Hooks]: {
|
||||
'@flecks/core.config': () => ({
|
||||
root: 'resource',
|
||||
}),
|
||||
'@flecks/socket.packets': (
|
||||
'http' === process.env.FLECKS_CORE_BUILD_TARGET ? clientPackets : serverPackets
|
||||
),
|
||||
'@avocado/resource.resources': Flecks.provide(require.context('./resources', false, /\.js$/)),
|
||||
'@avocado/traits.traits': Flecks.provide(require.context('./traits', false, /\.js$/)),
|
||||
'@flecks/core.config': () => ({
|
||||
resource: 'resource',
|
||||
universe: 'universe',
|
||||
}),
|
||||
'@flecks/socket.packets': Flecks.provide(require.context('./packets', false, /\.js$/)),
|
||||
'@flecks/redux.slices': () => ({
|
||||
universes,
|
||||
}),
|
||||
'@humus/app.components': () => Universe,
|
||||
'@humus/app.title': () => Title,
|
||||
},
|
||||
};
|
||||
|
|
|
@ -1,21 +0,0 @@
|
|||
export default class Player {
|
||||
|
||||
constructor({
|
||||
entity,
|
||||
socket,
|
||||
user,
|
||||
} = {}) {
|
||||
this.entity = entity;
|
||||
this.socket = socket;
|
||||
this.user = user;
|
||||
}
|
||||
|
||||
cleanInformingPackets() {
|
||||
this.entity.cleanInformingPackets();
|
||||
}
|
||||
|
||||
inform() {
|
||||
this.entity.inform(this.socket);
|
||||
}
|
||||
|
||||
}
|
|
@ -1,161 +0,0 @@
|
|||
import {dirname, join} from 'path';
|
||||
import {promisify} from 'util';
|
||||
|
||||
import {JsonResource} from '@avocado/resource';
|
||||
import {createLoop, destroyLoop} from '@avocado/timing';
|
||||
import glob from 'glob';
|
||||
|
||||
import Player from '../player';
|
||||
|
||||
const pglob = promisify(glob);
|
||||
|
||||
export default (flecks) => class Universe extends JsonResource {
|
||||
|
||||
#informLoopHandle = null;
|
||||
|
||||
#mainLoopHandle = null;
|
||||
|
||||
#players = [];
|
||||
|
||||
#rooms = {};
|
||||
|
||||
#roomsFlat = [];
|
||||
|
||||
#tps;
|
||||
|
||||
addPlayer({entity, socket, user}) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
entity.universe = this;
|
||||
const player = new Player({
|
||||
entity,
|
||||
socket,
|
||||
user,
|
||||
});
|
||||
this.#players.push(player);
|
||||
return player;
|
||||
}
|
||||
|
||||
addRoom(uri, room) {
|
||||
this.#rooms[uri] = room;
|
||||
this.#roomsFlat.push(room);
|
||||
}
|
||||
|
||||
async load(json = {}) {
|
||||
super.load(json);
|
||||
const {tps = 60, uri} = json;
|
||||
const {Room} = flecks.get('$avocado/resource.resources');
|
||||
if (uri) {
|
||||
const universePath = dirname(uri);
|
||||
await Promise.all(
|
||||
(
|
||||
await pglob(
|
||||
'**/*.room.json',
|
||||
{
|
||||
cwd: join(this.constructor.root, universePath),
|
||||
},
|
||||
)
|
||||
)
|
||||
.map(async (roomUri) => {
|
||||
try {
|
||||
this.addRoom(roomUri, await Room.load({extends: join(universePath, roomUri)}));
|
||||
}
|
||||
catch (error) {
|
||||
error.message = `Couldn't load room ${roomUri}: ${error.message}`;
|
||||
throw error;
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
this.#tps = tps;
|
||||
}
|
||||
|
||||
playerForSocket(socketId) {
|
||||
return this.#players.find(({socket}) => socket.id === socketId);
|
||||
}
|
||||
|
||||
playerForUser(userId) {
|
||||
return this.#players.find(({user}) => user.id === userId);
|
||||
}
|
||||
|
||||
async inform() {
|
||||
const promises = [];
|
||||
for (let i = 0; i < this.#players.length; ++i) {
|
||||
promises.push(this.#players[i].inform());
|
||||
}
|
||||
// TODO: rogue client?
|
||||
await Promise.all(promises);
|
||||
for (let i = 0; i < this.#roomsFlat.length; i++) {
|
||||
this.#roomsFlat[i].cleanPackets();
|
||||
}
|
||||
for (let i = 0; i < this.#players.length; ++i) {
|
||||
this.#players[i].cleanInformingPackets();
|
||||
}
|
||||
}
|
||||
|
||||
static async load(json = {}) {
|
||||
if (!json.extends) {
|
||||
throw new Error('Universe::load needs a URI');
|
||||
}
|
||||
return super.load(json);
|
||||
}
|
||||
|
||||
removePlayer(player) {
|
||||
const {entity} = player;
|
||||
const room = this.room(entity.currentRoom);
|
||||
entity.stopInforming(room);
|
||||
room.removeEntity(entity);
|
||||
const index = this.#players.indexOf(player);
|
||||
if (-1 !== index) {
|
||||
this.#players.splice(player, 1);
|
||||
}
|
||||
}
|
||||
|
||||
room(uri) {
|
||||
return this.#rooms[uri];
|
||||
}
|
||||
|
||||
start() {
|
||||
if (!this.#informLoopHandle) {
|
||||
this.#informLoopHandle = setInterval(
|
||||
async () => {
|
||||
try {
|
||||
await this.inform();
|
||||
}
|
||||
catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('Informing error:', error);
|
||||
}
|
||||
},
|
||||
1000 / 60,
|
||||
);
|
||||
}
|
||||
if (!this.#mainLoopHandle) {
|
||||
// eslint-disable-next-line no-eval
|
||||
const {performance} = eval('require')('perf_hooks');
|
||||
this.#mainLoopHandle = createLoop((elapsed) => {
|
||||
this.tick(elapsed);
|
||||
}, {
|
||||
sampler: performance.now,
|
||||
frequency: 1 / this.#tps,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
stop() {
|
||||
if (this.#informLoopHandle) {
|
||||
clearInterval(this.#informLoopHandle);
|
||||
this.#informLoopHandle = null;
|
||||
}
|
||||
if (this.#mainLoopHandle) {
|
||||
destroyLoop(this.#mainLoopHandle);
|
||||
this.#mainLoopHandle = null;
|
||||
}
|
||||
}
|
||||
|
||||
tick(elapsed) {
|
||||
for (let i = 0; i < this.#roomsFlat.length; i++) {
|
||||
this.#roomsFlat[i].tick(elapsed);
|
||||
}
|
||||
}
|
||||
|
||||
};
|
|
@ -1,37 +1,31 @@
|
|||
import {dirname, join} from 'path';
|
||||
|
||||
// import {ValidationError} from '@flecks/socket';
|
||||
|
||||
import Join from '../../packets/join';
|
||||
|
||||
export default (flecks) => class ServerJoin extends Join() {
|
||||
|
||||
// static async validate(packet, {req: {user}}) {
|
||||
// // if (0 === user.id) {
|
||||
// // throw new ValidationError({code: 401, reason: 'unauthenticated'});
|
||||
// // }
|
||||
// }
|
||||
|
||||
static async respond(packet, socket) {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const {req: {user}} = socket;
|
||||
const universe = flecks.get('$humus/universe.universe');
|
||||
let player = universe.playerForUser(user.id);
|
||||
if (player) {
|
||||
return player.entity.instanceUuid;
|
||||
}
|
||||
const {Entity} = flecks.get('$avocado/resource.resources');
|
||||
const entity = await Entity.load(
|
||||
{
|
||||
extends: join(
|
||||
dirname(universe.uri),
|
||||
'players',
|
||||
'cha0s',
|
||||
'index.entity.json',
|
||||
),
|
||||
const defaultJson = (name) => ({
|
||||
traits: {
|
||||
Alive: {
|
||||
state: {
|
||||
life: 200,
|
||||
maxLife: 200,
|
||||
},
|
||||
);
|
||||
await entity.addTrait('Controllable', {
|
||||
},
|
||||
Animated: {
|
||||
params: {
|
||||
animations: {
|
||||
idle: {
|
||||
offset: [0, -12],
|
||||
extends: '/player/idle.animation.json',
|
||||
},
|
||||
moving: {
|
||||
offset: [0, -12],
|
||||
extends: '/player/moving.animation.json',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Collider: {
|
||||
params: {
|
||||
activeCollision: true,
|
||||
},
|
||||
},
|
||||
Controllable: {
|
||||
params: {
|
||||
actions: {
|
||||
HotbarSlot0: [
|
||||
|
@ -103,22 +97,59 @@ export default (flecks) => class ServerJoin extends Join() {
|
|||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
await entity.addTrait('Informed');
|
||||
player = universe.addPlayer({
|
||||
entity,
|
||||
socket,
|
||||
user,
|
||||
});
|
||||
const removePlayer = () => {
|
||||
const player = universe.playerForSocket(socket.id);
|
||||
if (player) {
|
||||
universe.removePlayer(player);
|
||||
}
|
||||
};
|
||||
entity.on('destroying', removePlayer);
|
||||
socket.on('disconnect', removePlayer);
|
||||
return entity.instanceUuid;
|
||||
}
|
||||
},
|
||||
Directional: {
|
||||
params: {
|
||||
directionCount: 4,
|
||||
},
|
||||
},
|
||||
Informed: {},
|
||||
Magnetic: {
|
||||
params: {
|
||||
isAttractor: true,
|
||||
},
|
||||
state: {
|
||||
attraction: 20,
|
||||
},
|
||||
},
|
||||
Mobile: {
|
||||
state: {
|
||||
speed: 85,
|
||||
},
|
||||
},
|
||||
Named: {
|
||||
state: {
|
||||
name,
|
||||
},
|
||||
},
|
||||
Physical: {},
|
||||
Positioned: {},
|
||||
Receptacle: {},
|
||||
Shaped: {
|
||||
params: {
|
||||
shape: {
|
||||
type: 'circle',
|
||||
radius: 4,
|
||||
},
|
||||
},
|
||||
},
|
||||
Universed: {},
|
||||
Vulnerable: {
|
||||
params: {
|
||||
harmedAs: [
|
||||
'good',
|
||||
],
|
||||
},
|
||||
},
|
||||
Wielder: {},
|
||||
},
|
||||
});
|
||||
|
||||
const createPlayerEntity = async (flecks, name) => {
|
||||
const {Entity} = flecks.get('$avocado/resource.resources');
|
||||
const entity = await Entity.load(defaultJson(name));
|
||||
flecks.invoke('@humus/universe.player.entity', entity);
|
||||
return entity;
|
||||
};
|
||||
|
||||
export default createPlayerEntity;
|
79
packages/universe/src/server/gen/player-room.js
Normal file
79
packages/universe/src/server/gen/player-room.js
Normal file
|
@ -0,0 +1,79 @@
|
|||
const defaultJson = () => ({
|
||||
entities: [
|
||||
{
|
||||
extends: '/npc/legit-dude.entity.json',
|
||||
traits: {
|
||||
Positioned: {
|
||||
state: {
|
||||
x: 49,
|
||||
y: 37,
|
||||
},
|
||||
},
|
||||
Named: {
|
||||
state: {
|
||||
name: 'Sups',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
extends: '/npc/legit-dude.entity.json',
|
||||
traits: {
|
||||
Positioned: {
|
||||
state: {
|
||||
x: 134,
|
||||
y: 142,
|
||||
},
|
||||
},
|
||||
Directional: {
|
||||
params: {
|
||||
directionCount: 4,
|
||||
trackMovement: true,
|
||||
},
|
||||
state: {
|
||||
direction: 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
evaporation: 64,
|
||||
size: [
|
||||
384,
|
||||
384,
|
||||
],
|
||||
tiles: [
|
||||
{
|
||||
data: 'eNpjY2AbhaNwFI7CAYEAc8INgQ==',
|
||||
area: [
|
||||
24,
|
||||
24,
|
||||
],
|
||||
tileImageUri: '/tileset.png',
|
||||
tileSize: [
|
||||
16,
|
||||
16,
|
||||
],
|
||||
},
|
||||
{
|
||||
area: [
|
||||
24,
|
||||
24,
|
||||
],
|
||||
tileImageUri: '/tileset.png',
|
||||
tileSize: [
|
||||
16,
|
||||
16,
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const createPlayerRoom = async (flecks) => {
|
||||
const {Room} = flecks.get('$avocado/resource.resources');
|
||||
const room = await Room.load(defaultJson());
|
||||
flecks.invoke('@humus/universe.player.room', room);
|
||||
return room;
|
||||
};
|
||||
|
||||
export default createPlayerRoom;
|
|
@ -1,39 +1,40 @@
|
|||
import {stat} from 'fs/promises';
|
||||
import {resolve} from 'path';
|
||||
|
||||
import {Resource} from '@avocado/resource';
|
||||
import {D, Hooks} from '@flecks/core';
|
||||
import {D, Flecks, Hooks} from '@flecks/core';
|
||||
import express from 'express';
|
||||
|
||||
import alpha from './gen/alpha';
|
||||
import UniverseInput from './packets/decorators/universe-input';
|
||||
import Universe from './universe';
|
||||
|
||||
const {
|
||||
FLECKS_CORE_ROOT = process.cwd(),
|
||||
} = process.env;
|
||||
|
||||
const debug = D('@humus/universe/server');
|
||||
|
||||
export default {
|
||||
[Hooks]: {
|
||||
'@flecks/core.hmr': (path, flecks) => {
|
||||
if ('@humus/universe/server' === path) {
|
||||
alpha(flecks.get('$humus/universe.universe').room('players/cha0s/rooms/water/0.room.json'), flecks);
|
||||
'@flecks/core.starting': async (flecks) => {
|
||||
const {resource} = flecks.get('@humus/universe');
|
||||
const stats = await stat(resource);
|
||||
if (!stats.isDirectory()) {
|
||||
throw new Error(`resource root ${resource} is not a directory`);
|
||||
}
|
||||
Resource.root = resource;
|
||||
flecks.set('$humus/universe.resource-server', express.static(resource));
|
||||
debug('resource root: %s', resource);
|
||||
},
|
||||
'@flecks/server.up': async (flecks) => {
|
||||
const {root} = flecks.get('@humus/universe');
|
||||
const {running} = flecks.get('@humus/universe/server');
|
||||
if (!running) {
|
||||
debug('no universe to run, aborting');
|
||||
return;
|
||||
}
|
||||
Resource.root = root;
|
||||
flecks.set('$humus/universe.resource-server', express.static(Resource.root));
|
||||
debug('resource root: %s', root);
|
||||
const {universe: path} = flecks.get('@humus/universe');
|
||||
try {
|
||||
const {Universe} = flecks.get('$avocado/resource.resources');
|
||||
const universe = await Universe.load({extends: `/universe/${running}/index.universe.json`});
|
||||
alpha(universe.room('players/cha0s/rooms/water/0.room.json'), flecks);
|
||||
const universe = await Universe.loadFrom(flecks, resolve(FLECKS_CORE_ROOT, path));
|
||||
flecks.set('$humus/universe.universe', universe);
|
||||
universe.start();
|
||||
debug(`universe '${running}' up and running!`);
|
||||
debug('universe up and running!');
|
||||
}
|
||||
catch (error) {
|
||||
throw new Error(`Couldn't run universe '${running}': ${error.stack}`);
|
||||
throw new Error(`couldn't run universe: ${error.stack}`);
|
||||
}
|
||||
},
|
||||
'@flecks/web/server.request.socket': (flecks) => (req, res, next) => {
|
||||
|
@ -44,9 +45,17 @@ export default {
|
|||
}
|
||||
next();
|
||||
},
|
||||
'@flecks/socket.packets.decorate': (Packets, flecks) => ({
|
||||
...Packets,
|
||||
Input: UniverseInput(flecks, Packets.Input),
|
||||
}),
|
||||
'@flecks/socket.packets.decorate': (
|
||||
Flecks.decorate(require.context('./packets/decorators', false, /\.js$/))
|
||||
),
|
||||
'@flecks/socket/server.request.socket': (flecks) => async ({handshake}, next) => {
|
||||
const {universe} = flecks.get('$humus/universe');
|
||||
const {user} = handshake;
|
||||
if (user) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
handshake.entity = await universe.loadOrCreateEntity(user);
|
||||
}
|
||||
next();
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
15
packages/universe/src/server/packets/decorators/input.js
Normal file
15
packages/universe/src/server/packets/decorators/input.js
Normal file
|
@ -0,0 +1,15 @@
|
|||
import {ValidationError} from '@flecks/socket';
|
||||
|
||||
export default (Input) => class UniverseInputPacket extends Input {
|
||||
|
||||
static respond(packet, {req: {entity}}) {
|
||||
entity.acceptActionStream(packet.data);
|
||||
}
|
||||
|
||||
static validate(packet, {req: {entity}}) {
|
||||
if (!entity) {
|
||||
throw new ValidationError({code: 400, reason: 'anonymous'});
|
||||
}
|
||||
}
|
||||
|
||||
};
|
24
packages/universe/src/server/packets/decorators/join.js
Normal file
24
packages/universe/src/server/packets/decorators/join.js
Normal file
|
@ -0,0 +1,24 @@
|
|||
import {ValidationError} from '@flecks/socket';
|
||||
|
||||
export default (Join, flecks) => class UniverseJoinPacket extends Join {
|
||||
|
||||
static async respond(packet, socket) {
|
||||
const {req: {entity}} = socket;
|
||||
const universe = flecks.get('$humus/universe.universe');
|
||||
if (universe.hasEntity(entity)) {
|
||||
throw new ValidationError({code: 409, reason: 'already logged in'});
|
||||
}
|
||||
await universe.addPlayer({
|
||||
entity,
|
||||
socket,
|
||||
});
|
||||
return entity.instanceUuid;
|
||||
}
|
||||
|
||||
static validate(packet, {req: {entity}}) {
|
||||
if (!entity) {
|
||||
throw new ValidationError({code: 400, reason: 'anonymous'});
|
||||
}
|
||||
}
|
||||
|
||||
};
|
|
@ -1,17 +0,0 @@
|
|||
// import {ValidationError} from '@flecks/socket';
|
||||
|
||||
export default (flecks, InputPacket) => class UniverseInputPacket extends InputPacket {
|
||||
|
||||
static respond(packet, socket) {
|
||||
const universe = flecks.get('$humus/universe.universe');
|
||||
const {entity} = universe.playerForSocket(socket.id);
|
||||
entity.acceptActionStream(packet.data);
|
||||
}
|
||||
|
||||
// static validate(packet, {req: {user}}) {
|
||||
// if (0 === user.id) {
|
||||
// throw new ValidationError({code: 400, reason: 'anonymous'});
|
||||
// }
|
||||
// }
|
||||
|
||||
};
|
228
packages/universe/src/server/universe/index.js
Normal file
228
packages/universe/src/server/universe/index.js
Normal file
|
@ -0,0 +1,228 @@
|
|||
import {
|
||||
mkdir,
|
||||
readFile,
|
||||
stat,
|
||||
writeFile,
|
||||
} from 'fs/promises';
|
||||
import {join} from 'path';
|
||||
import {promisify} from 'util';
|
||||
|
||||
import {createLoop, destroyLoop} from '@avocado/timing';
|
||||
import {D} from '@flecks/core';
|
||||
import glob from 'glob';
|
||||
|
||||
import createPlayerEntity from '../gen/player-entity';
|
||||
import createPlayerRoom from '../gen/player-room';
|
||||
import Player from './player';
|
||||
|
||||
const debug = D('@humus/universe/server/universe');
|
||||
|
||||
const pglob = promisify(glob);
|
||||
|
||||
export default class Universe {
|
||||
|
||||
#flecks;
|
||||
|
||||
#informLoopHandle = null;
|
||||
|
||||
#mainLoopHandle = null;
|
||||
|
||||
#players = [];
|
||||
|
||||
#rooms = {};
|
||||
|
||||
#root;
|
||||
|
||||
#roomsFlat = [];
|
||||
|
||||
#tps = 60;
|
||||
|
||||
constructor(flecks, root) {
|
||||
this.#flecks = flecks;
|
||||
this.#root = root;
|
||||
}
|
||||
|
||||
addPlayer({entity, socket}) {
|
||||
debug('adding player %j', entity);
|
||||
const player = new Player({
|
||||
entity,
|
||||
socket,
|
||||
});
|
||||
const onCurrentRoomChanged = (oldRoom, newRoom) => {
|
||||
if (oldRoom) {
|
||||
const room = this.room(oldRoom);
|
||||
entity.stopInforming(room);
|
||||
room.removeEntity(entity);
|
||||
}
|
||||
if (newRoom) {
|
||||
const room = this.room(newRoom);
|
||||
debug('new room', newRoom);
|
||||
room.addEntity(entity);
|
||||
entity.startInforming(room);
|
||||
}
|
||||
};
|
||||
entity.on('currentRoomChanged', onCurrentRoomChanged);
|
||||
player.once('removed', () => {
|
||||
entity.off('currentRoomChanged', onCurrentRoomChanged);
|
||||
this.removePlayer(player);
|
||||
});
|
||||
onCurrentRoomChanged(undefined, entity.currentRoom);
|
||||
this.#players.push(player);
|
||||
return player;
|
||||
}
|
||||
|
||||
addRoom(uri, room) {
|
||||
debug('adding room %s', uri);
|
||||
this.#rooms[uri] = room;
|
||||
this.#roomsFlat.push(room);
|
||||
}
|
||||
|
||||
async createEntity(user) {
|
||||
const playerPath = join(this.#root, 'players', `${user.id}`);
|
||||
const {Entity} = this.#flecks.get('$avocado/resource.resources');
|
||||
await mkdir(playerPath, {recursive: true});
|
||||
const entity = await createPlayerEntity(this.#flecks, user.email);
|
||||
entity.currentRoom = `/${join('players', `${user.id}`, 'index.room.json')}`;
|
||||
await writeFile(
|
||||
join(playerPath, 'index.entity.json'),
|
||||
JSON.stringify(Entity.withoutDefaults(entity.toJSON()), null, 2),
|
||||
);
|
||||
const room = await createPlayerRoom(this.#flecks);
|
||||
this.addRoom(entity.currentRoom, room);
|
||||
await writeFile(
|
||||
join(playerPath, 'index.room.json'),
|
||||
JSON.stringify(room, null, 2),
|
||||
);
|
||||
return entity;
|
||||
}
|
||||
|
||||
hasEntity(check) {
|
||||
return this.#players.find(({entity}) => entity === check);
|
||||
}
|
||||
|
||||
async inform() {
|
||||
const promises = [];
|
||||
for (let i = 0; i < this.#players.length; ++i) {
|
||||
promises.push(this.#players[i].inform());
|
||||
}
|
||||
// TODO: rogue client?
|
||||
await Promise.all(promises);
|
||||
for (let i = 0; i < this.#roomsFlat.length; i++) {
|
||||
this.#roomsFlat[i].cleanPackets();
|
||||
}
|
||||
for (let i = 0; i < this.#players.length; ++i) {
|
||||
this.#players[i].cleanInformingPackets();
|
||||
}
|
||||
}
|
||||
|
||||
static async loadFrom(flecks, path) {
|
||||
try {
|
||||
const stats = await stat(path);
|
||||
if (!stats.isDirectory()) {
|
||||
throw new Error(`universe ${path} is not a directory`);
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
if ('ENOENT' !== error.code) {
|
||||
throw error;
|
||||
}
|
||||
debug("universe doesn't exist, creating...");
|
||||
await mkdir(path);
|
||||
}
|
||||
const universe = new this(flecks, path);
|
||||
const {Room} = flecks.get('$avocado/resource.resources');
|
||||
await Promise.all(
|
||||
(await pglob('**/*.room.json', {cwd: path}))
|
||||
.map(async (roomUri) => {
|
||||
const qualified = `/${roomUri}`;
|
||||
try {
|
||||
const roomJson = JSON.parse((await readFile(join(path, roomUri))).toString());
|
||||
universe.addRoom(qualified, await Room.load(roomJson));
|
||||
}
|
||||
catch (error) {
|
||||
error.message = `couldn't load room ${qualified}: ${error.message}`;
|
||||
throw error;
|
||||
}
|
||||
}),
|
||||
);
|
||||
return universe;
|
||||
}
|
||||
|
||||
async loadOrCreateEntity(user) {
|
||||
debug('loadOrCreateEntity from user id %d', user.id);
|
||||
const playerPath = join(this.#root, 'players', `${user.id}`);
|
||||
const {Entity} = this.#flecks.get('$avocado/resource.resources');
|
||||
try {
|
||||
await stat(join(playerPath, 'index.entity.json'));
|
||||
const json = JSON.parse((await readFile(join(playerPath, 'index.entity.json'))).toString());
|
||||
debug('loaded %j', json);
|
||||
return Entity.load(json);
|
||||
}
|
||||
catch (error) {
|
||||
if ('ENOENT' !== error.code) {
|
||||
throw error;
|
||||
}
|
||||
return this.createEntity(user);
|
||||
}
|
||||
}
|
||||
|
||||
removePlayer(player) {
|
||||
const {entity} = player;
|
||||
const room = this.room(entity.currentRoom);
|
||||
entity.stopInforming(room);
|
||||
room.removeEntity(entity);
|
||||
const index = this.#players.indexOf(player);
|
||||
if (-1 !== index) {
|
||||
this.#players.splice(player, 1);
|
||||
}
|
||||
}
|
||||
|
||||
room(uri) {
|
||||
return this.#rooms[uri];
|
||||
}
|
||||
|
||||
start() {
|
||||
if (!this.#informLoopHandle) {
|
||||
this.#informLoopHandle = setInterval(
|
||||
async () => {
|
||||
try {
|
||||
await this.inform();
|
||||
}
|
||||
catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('Informing error:', error);
|
||||
}
|
||||
},
|
||||
1000 / 60,
|
||||
);
|
||||
}
|
||||
if (!this.#mainLoopHandle) {
|
||||
// eslint-disable-next-line no-eval
|
||||
const {performance} = eval('require')('perf_hooks');
|
||||
this.#mainLoopHandle = createLoop((elapsed) => {
|
||||
this.tick(elapsed);
|
||||
}, {
|
||||
sampler: performance.now,
|
||||
frequency: 1 / this.#tps,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
stop() {
|
||||
if (this.#informLoopHandle) {
|
||||
clearInterval(this.#informLoopHandle);
|
||||
this.#informLoopHandle = null;
|
||||
}
|
||||
if (this.#mainLoopHandle) {
|
||||
destroyLoop(this.#mainLoopHandle);
|
||||
this.#mainLoopHandle = null;
|
||||
}
|
||||
}
|
||||
|
||||
tick(elapsed) {
|
||||
for (let i = 0; i < this.#roomsFlat.length; i++) {
|
||||
this.#roomsFlat[i].tick(elapsed);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
31
packages/universe/src/server/universe/player.js
Normal file
31
packages/universe/src/server/universe/player.js
Normal file
|
@ -0,0 +1,31 @@
|
|||
import {Class, compose, EventEmitter} from '@flecks/core';
|
||||
|
||||
const decorate = compose(
|
||||
EventEmitter,
|
||||
);
|
||||
|
||||
export default class Player extends decorate(Class) {
|
||||
|
||||
constructor({
|
||||
entity,
|
||||
socket,
|
||||
} = {}) {
|
||||
super();
|
||||
this.entity = entity;
|
||||
this.socket = socket;
|
||||
const emitRemove = () => {
|
||||
this.emit('removed');
|
||||
};
|
||||
entity.once('destroying', emitRemove);
|
||||
socket.once('disconnect', emitRemove);
|
||||
}
|
||||
|
||||
cleanInformingPackets() {
|
||||
this.entity.cleanInformingPackets();
|
||||
}
|
||||
|
||||
inform() {
|
||||
this.entity.inform(this.socket);
|
||||
}
|
||||
|
||||
}
|
|
@ -10,43 +10,10 @@ const decorate = compose(
|
|||
|
||||
export default () => class Universed extends decorate(Trait) {
|
||||
|
||||
#universe;
|
||||
|
||||
static defaultState() {
|
||||
return {
|
||||
currentRoom: 'rooms/town.room.json',
|
||||
currentRoom: '',
|
||||
};
|
||||
}
|
||||
|
||||
onCurrentRoomChanged(oldRoom) {
|
||||
if (!this.#universe) {
|
||||
return;
|
||||
}
|
||||
if (oldRoom) {
|
||||
const room = this.#universe.room(oldRoom);
|
||||
this.entity.stopInforming(room);
|
||||
room.removeEntity(this.entity);
|
||||
}
|
||||
if (this.entity.currentRoom) {
|
||||
const room = this.#universe.room(this.entity.currentRoom);
|
||||
room.addEntity(this.entity);
|
||||
this.entity.startInforming(room);
|
||||
}
|
||||
}
|
||||
|
||||
listeners() {
|
||||
return {
|
||||
|
||||
currentRoomChanged: (oldRoom) => {
|
||||
this.onCurrentRoomChanged(oldRoom);
|
||||
},
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
set universe(universe) {
|
||||
this.#universe = universe;
|
||||
this.onCurrentRoomChanged();
|
||||
}
|
||||
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue
Block a user