refactor: structure

This commit is contained in:
cha0s 2022-03-21 03:16:51 -05:00
parent b1a9975b89
commit 0a16f10889
16 changed files with 508 additions and 332 deletions

1
.gitignore vendored
View File

@ -118,3 +118,4 @@ dist
# local
/dist
/packages/*/yarn.lock
/universe

View File

@ -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'

View File

@ -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);

View File

@ -1,3 +0,0 @@
import Join from '../../packets/join';
export default () => class ClientJoin extends Join() {};

View File

@ -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,
},
};

View File

@ -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);
}
}

View File

@ -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);
}
}
};

View File

@ -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;

View 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;

View File

@ -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();
},
},
};

View 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'});
}
}
};

View 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'});
}
}
};

View File

@ -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'});
// }
// }
};

View 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);
}
}
}

View 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);
}
}

View File

@ -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();
}
};