Compare commits

...

7 Commits

Author SHA1 Message Date
cha0s
a872d9ae5f refactor: separate concerns 2024-07-21 04:00:55 -05:00
cha0s
1a7d55f6d3 refactor: lifetimes 2024-07-21 03:54:03 -05:00
cha0s
8be6c23711 chore: tidy 2024-07-21 03:01:12 -05:00
cha0s
a27f79dc33 refactor: structure 2024-07-21 02:57:29 -05:00
cha0s
ede9614e2d refactor: events 2024-07-21 02:30:13 -05:00
cha0s
8bf26ef7cd refactor: asset and data I/O 2024-07-21 01:37:40 -05:00
cha0s
1d35b9b6d5 refactor: packets 2024-07-20 21:26:00 -05:00
33 changed files with 213 additions and 381 deletions

View File

@ -1,4 +1,4 @@
import Client from './client.js'; import Client from '@/net/client.js';
export default class LocalClient extends Client { export default class LocalClient extends Client {
async connect() { async connect() {

View File

@ -1,4 +1,4 @@
import {encode} from '@/packets/index.js'; import {encode} from '@/net/packets/index.js';
let connected = false; let connected = false;
let socket; let socket;

View File

@ -1,8 +1,7 @@
import {encode} from '@/packets/index.js'; import Client from '@/net/client.js';
import {encode} from '@/net/packets/index.js';
import {CLIENT_PREDICTION} from '@/util/constants.js'; import {CLIENT_PREDICTION} from '@/util/constants.js';
import Client from './client.js';
export default class RemoteClient extends Client { export default class RemoteClient extends Client {
constructor() { constructor() {
super(); super();
@ -16,7 +15,7 @@ export default class RemoteClient extends Client {
async connect(host) { async connect(host) {
if (CLIENT_PREDICTION) { if (CLIENT_PREDICTION) {
this.worker = new Worker( this.worker = new Worker(
new URL('../client/prediction.js', import.meta.url), new URL('./prediction.js', import.meta.url),
{type: 'module'}, {type: 'module'},
); );
this.worker.postMessage({host}); this.worker.postMessage({host});

View File

@ -153,7 +153,7 @@ export default class Component {
return Component.constructor.filterDefaults(this); return Component.constructor.filterDefaults(this);
} }
toJSON() { toJSON() {
return this.toNet(); return Component.constructor.filterDefaults(this);
} }
}; };
const properties = {}; const properties = {};

View File

@ -1,10 +1,19 @@
import {Encoder, Decoder} from '@msgpack/msgpack';
import {LRUCache} from 'lru-cache';
import Script from '@/util/script.js';
import EntityFactory from './entity-factory.js'; import EntityFactory from './entity-factory.js';
import {Encoder, Decoder} from '@msgpack/msgpack'; const cache = new LRUCache({
max: 128,
});
const decoder = new Decoder(); const decoder = new Decoder();
const encoder = new Encoder(); const encoder = new Encoder();
const textDecoder = new TextDecoder();
export default class Ecs { export default class Ecs {
$$caret = 1; $$caret = 1;
@ -13,6 +22,8 @@ export default class Ecs {
deferredChanges = {} deferredChanges = {}
destroying = new Set();
diff = {}; diff = {};
Systems = {}; Systems = {};
@ -197,6 +208,10 @@ export default class Ecs {
} }
destroy(entityId) { destroy(entityId) {
this.destroying.add(entityId);
}
destroyImmediately(entityId) {
this.destroyMany([entityId]); this.destroyMany([entityId]);
} }
@ -308,6 +323,48 @@ export default class Ecs {
} }
} }
async readJson(uri) {
const key = ['$$json', uri].join(':');
if (!cache.has(key)) {
let promise, resolve, reject;
promise = new Promise((res, rej) => {
resolve = res;
reject = rej;
});
cache.set(key, promise);
this.readAsset(uri)
.then((chars) => {
resolve(
chars.byteLength > 0
? JSON.parse(textDecoder.decode(chars))
: {},
);
})
.catch(reject);
}
return cache.get(key);
}
async readScript(uriOrCode, context = {}) {
if (!uriOrCode) {
return undefined;
}
let code = '';
if (!uriOrCode.startsWith('/')) {
code = uriOrCode;
}
else {
const buffer = await this.readAsset(uriOrCode);
if (buffer.byteLength > 0) {
code = textDecoder.decode(buffer);
}
}
if (!code) {
return undefined;
}
return Script.fromCode(code, context);
}
rebuild(entityId, componentNames) { rebuild(entityId, componentNames) {
let Class; let Class;
if (componentNames) { if (componentNames) {
@ -376,31 +433,24 @@ export default class Ecs {
} }
tick(elapsed) { tick(elapsed) {
const destroying = new Set();
for (const systemName in this.Systems) { for (const systemName in this.Systems) {
const System = this.Systems[systemName]; const System = this.Systems[systemName];
if (System.active) { if (!System.active) {
if (System.frequency) { continue;
System.elapsed += elapsed; }
if (System.elapsed < System.frequency) { if (!System.frequency) {
continue; System.tick(elapsed);
} continue;
} }
while (!System.frequency || System.elapsed >= System.frequency) { System.elapsed += elapsed;
System.tick(System.frequency ? System.elapsed : elapsed); while (System.elapsed >= System.frequency) {
for (let j = 0; j < System.destroying.length; j++) { System.tick(System.frequency);
destroying.add(System.destroying[j]); System.elapsed -= System.frequency;
}
System.tickDestruction();
if (!System.frequency) {
break;
}
System.elapsed -= System.frequency;
}
} }
} }
if (destroying.size > 0) { if (this.destroying.size > 0) {
this.destroyMany(destroying.values()); this.destroyMany(this.destroying);
this.destroying.clear();
} }
} }

View File

@ -114,7 +114,7 @@ test('destroys entities', async () => {
expect(ecs.get(entity)) expect(ecs.get(entity))
.to.be.undefined; .to.be.undefined;
expect(() => { expect(() => {
ecs.destroy(entity); ecs.destroyImmediately(entity);
}) })
.to.throw(); .to.throw();
}); });
@ -170,50 +170,14 @@ test('ticks systems', async () => {
.to.deep.equal(JSON.stringify({y: 128 + 30})); .to.deep.equal(JSON.stringify({y: 128 + 30}));
}); });
test('creates many entities when ticking systems', () => {
const ecs = new Ecs({
Systems: {
Spawn: class extends System {
tick() {
this.createManyEntities(Array.from({length: 5}).map(() => []));
}
},
},
});
ecs.system('Spawn').active = true;
ecs.create();
expect(ecs.get(5))
.to.be.undefined;
ecs.tick(1);
expect(ecs.get(5))
.to.not.be.undefined;
});
test('creates entities when ticking systems', () => {
const ecs = new Ecs({
Systems: {
Spawn: class extends System {
tick() {
this.createEntity();
}
},
},
});
ecs.system('Spawn').active = true;
ecs.create();
expect(ecs.get(2))
.to.be.undefined;
ecs.tick(1);
expect(ecs.get(2))
.to.not.be.undefined;
});
test('schedules entities to be deleted when ticking systems', () => { test('schedules entities to be deleted when ticking systems', () => {
const ecs = new Ecs({ const ecs = new Ecs({
Systems: { Systems: {
Despawn: class extends System { Despawn: class extends System {
tick() { tick() {
this.destroyEntity(1); this.ecs.destroy(1);
expect(ecs.get(1))
.to.not.be.undefined;
} }
}, },
}, },
@ -225,50 +189,6 @@ test('schedules entities to be deleted when ticking systems', () => {
.to.be.undefined; .to.be.undefined;
}); });
test('adds components to and remove components from entities when ticking systems', async () => {
let promise;
const ecs = new Ecs({
Components: {Foo: wrapProperties('Foo', {bar: {type: 'uint8'}})},
Systems: {
AddComponent: class extends System {
static queries() {
return {
default: ['Foo'],
};
}
tick() {
promise = this.insertComponents(1, {Foo: {}});
}
},
RemoveComponent: class extends System {
static queries() {
return {
default: ['Foo'],
};
}
tick() {
this.removeComponents(1, ['Foo']);
}
},
},
});
ecs.system('AddComponent').active = true;
ecs.create();
ecs.tick(1);
await promise;
expect(Array.from(ecs.system('AddComponent').select('default')).length)
.to.equal(1);
expect(ecs.get(1).Foo)
.to.not.be.undefined;
ecs.system('AddComponent').active = false;
ecs.system('RemoveComponent').active = true;
ecs.tick(1);
expect(Array.from(ecs.system('RemoveComponent').select('default')).length)
.to.equal(0);
expect(ecs.get(1).Foo)
.to.be.undefined;
});
test('generates diffs for entity creation', async () => { test('generates diffs for entity creation', async () => {
const ecs = new Ecs(); const ecs = new Ecs();
let entity; let entity;
@ -347,7 +267,7 @@ test('generates diffs for deletions', async () => {
let entity; let entity;
entity = await ecs.create(); entity = await ecs.create();
ecs.setClean(); ecs.setClean();
ecs.destroy(entity); ecs.destroyImmediately(entity);
expect(ecs.diff) expect(ecs.diff)
.to.deep.equal({[entity]: false}); .to.deep.equal({[entity]: false});
}); });

View File

@ -6,8 +6,6 @@ export default class System {
active = false; active = false;
destroying = [];
ecs; ecs;
elapsed = 0; elapsed = 0;
@ -25,38 +23,12 @@ export default class System {
this.reindex(ecs.entities); this.reindex(ecs.entities);
} }
createEntity(components) {
return this.ecs.create(components);
}
createManyEntities(componentsList) {
return this.ecs.createMany(componentsList);
}
deindex(entityIds) { deindex(entityIds) {
for (const i in this.queries) { for (const i in this.queries) {
this.queries[i].deindex(entityIds); this.queries[i].deindex(entityIds);
} }
} }
destroyEntity(entityId) {
this.destroyManyEntities([entityId]);
}
destroyManyEntities(entityIds) {
for (let i = 0; i < entityIds.length; i++) {
this.destroying.push(entityIds[i]);
}
}
async insertComponents(entityId, components) {
return this.ecs.insert(entityId, components);
}
async insertManyComponents(components) {
return this.ecs.insertMany(components);
}
static get priority() { static get priority() {
return { return {
phase: 'normal', phase: 'normal',
@ -73,14 +45,6 @@ export default class System {
} }
} }
removeComponents(entityId, components) {
this.ecs.remove(entityId, components);
}
removeManyComponents(entityIds) {
this.ecs.removeMany(entityIds);
}
select(query) { select(query) {
return this.queries[query].select(); return this.queries[query].select();
} }
@ -117,13 +81,6 @@ export default class System {
); );
} }
tickDestruction() {
if (this.destroying.length > 0) {
this.deindex(this.destroying);
}
this.destroying = [];
}
tick() {} tick() {}
} }

View File

@ -17,10 +17,10 @@ export async function websocket(server, viteDevServer) {
if (viteDevServer) { if (viteDevServer) {
const {createViteRuntime} = await import('vite'); const {createViteRuntime} = await import('vite');
const runtime = await createViteRuntime(viteDevServer); const runtime = await createViteRuntime(viteDevServer);
(await runtime.executeEntrypoint('/app/websocket.js')).default(server); (await runtime.executeEntrypoint('/app/server/websocket.js')).default(server);
} }
else { else {
(await import('./websocket.js')).default(server); (await import('./server/websocket.js')).default(server);
} }
} }

27
app/net/client.js Normal file
View File

@ -0,0 +1,27 @@
import {CLIENT_LATENCY} from '@/util/constants.js';
import EventEmitter from '@/util/event-emitter.js';
export default class Client {
constructor() {
this.emitter = new EventEmitter();
}
accept(packet) {
this.emitter.invoke(packet.type, packet.payload);
}
addPacketListener(type, listener) {
this.emitter.addListener(type, listener);
}
removePacketListener(type, listener) {
this.emitter.removeListener(type, listener);
}
send(packet) {
if (CLIENT_LATENCY > 0) {
setTimeout(() => {
this.transmit(packet);
}, CLIENT_LATENCY);
}
else {
this.transmit(packet);
}
}
}

View File

@ -1,42 +0,0 @@
import {CLIENT_LATENCY} from '@/util/constants.js';
export default class Client {
constructor() {
this.listeners = {};
}
accept(packet) {
const listeners = this.listeners[packet.type];
if (!listeners) {
return;
}
for (const i in listeners) {
listeners[i](packet.payload);
}
}
addPacketListener(type, listener) {
if (!this.listeners[type]) {
this.listeners[type] = [];
}
this.listeners[type].push(listener);
}
removePacketListener(type, listener) {
const listeners = this.listeners[type];
if (!listeners) {
return;
}
const index = listeners.indexOf(listener);
if (-1 !== index) {
listeners.splice(index, 1);
}
}
send(packet) {
if (CLIENT_LATENCY > 0) {
setTimeout(() => {
this.transmit(packet);
}, CLIENT_LATENCY);
}
else {
this.transmit(packet);
}
}
}

32
app/net/server.js Normal file
View File

@ -0,0 +1,32 @@
import {SERVER_LATENCY} from '@/util/constants.js';
import EventEmitter from '@/util/event-emitter.js';
const textDecoder = new TextDecoder();
export default class Server {
constructor() {
this.emitter = new EventEmitter();
}
accept(connection, packet) {
this.emitter.invoke(packet.type, connection, packet.payload);
}
addPacketListener(type, listener) {
this.emitter.addListener(type, listener);
}
async readJson(path) {
return JSON.parse(textDecoder.decode(await this.readData(path)));
}
removePacketListener(type, listener) {
this.emitter.removeListener(type, listener);
}
send(connection, packet) {
if (SERVER_LATENCY > 0) {
setTimeout(() => {
this.transmit(connection, packet);
}, SERVER_LATENCY);
}
else {
this.transmit(connection, packet);
}
}
}

View File

@ -1,42 +0,0 @@
import {SERVER_LATENCY} from '@/util/constants.js';
export default class Server {
constructor() {
this.listeners = {};
}
accept(connection, packet) {
const listeners = this.listeners[packet.type];
if (!listeners) {
return;
}
for (const i in listeners) {
listeners[i](connection, packet.payload);
}
}
addPacketListener(type, listener) {
if (!this.listeners[type]) {
this.listeners[type] = [];
}
this.listeners[type].push(listener);
}
removePacketListener(type, listener) {
const listeners = this.listeners[type];
if (!listeners) {
return;
}
const index = listeners.indexOf(listener);
if (-1 !== index) {
listeners.splice(index, 1);
}
}
send(connection, packet) {
if (SERVER_LATENCY > 0) {
setTimeout(() => {
this.transmit(connection, packet);
}, SERVER_LATENCY);
}
else {
this.transmit(connection, packet);
}
}
}

View File

@ -1,8 +1,7 @@
import Ecs from '@/ecs/ecs.js';
import Script from '@/util/script.js';
import {LRUCache} from 'lru-cache'; import {LRUCache} from 'lru-cache';
import Ecs from '@/ecs/ecs.js';
const cache = new LRUCache({ const cache = new LRUCache({
max: 128, max: 128,
}); });
@ -24,39 +23,4 @@ export default class ClientEcs extends Ecs {
} }
return cache.get(uri); return cache.get(uri);
} }
async readJson(uri) {
const key = ['$$json', uri].join(':');
if (!cache.has(key)) {
let promise, resolve, reject;
promise = new Promise((res, rej) => {
resolve = res;
reject = rej;
});
cache.set(key, promise);
this.readAsset(uri)
.then((chars) => {
resolve(chars.byteLength > 0 ? JSON.parse((new TextDecoder()).decode(chars)) : {});
})
.catch(reject);
}
return cache.get(key);
}
async readScript(uriOrCode, context = {}) {
if (!uriOrCode) {
return undefined;
}
let code = '';
if (!uriOrCode.startsWith('/')) {
code = uriOrCode;
}
else {
const buffer = await this.readAsset(uriOrCode);
if (buffer.byteLength > 0) {
code = (new TextDecoder()).decode(buffer);
}
}
if (code) {
return Script.fromCode(code, context);
}
}
} }

View File

@ -5,6 +5,7 @@ import {useDebug} from '@/react/context/debug.js';
import {useEcs, useEcsTick} from '@/react/context/ecs.js'; import {useEcs, useEcsTick} from '@/react/context/ecs.js';
import {useMainEntity} from '@/react/context/main-entity.js'; import {useMainEntity} from '@/react/context/main-entity.js';
import {RESOLUTION} from '@/util/constants.js'; import {RESOLUTION} from '@/util/constants.js';
import EventEmitter from '@/util/event-emitter.js';
import addKeyListener from './add-key-listener.js'; import addKeyListener from './add-key-listener.js';
import ClientEcs from './client-ecs.js'; import ClientEcs from './client-ecs.js';
@ -21,31 +22,7 @@ function emptySlots() {
return Array(10).fill(undefined); return Array(10).fill(undefined);
} }
const devEventsChannel = { const devEventsChannel = new EventEmitter();
$$listeners: {},
addListener(type, listener) {
if (!this.$$listeners[type]) {
this.$$listeners[type] = new Set();
}
this.$$listeners[type].add(listener);
},
invoke(type, payload) {
const listeners = this.$$listeners[type];
if (!listeners) {
return;
}
for (const listener of listeners) {
listener(payload);
}
},
removeListener(type, listener) {
const listeners = this.$$listeners[type];
if (!listeners) {
return;
}
listeners.delete(listener);
},
};
function Ui({disconnected}) { function Ui({disconnected}) {
// Key input. // Key input.
@ -261,9 +238,7 @@ function Ui({disconnected}) {
return; return;
} }
await ecs.apply(payload.ecs); await ecs.apply(payload.ecs);
for (const listener of client.listeners[':Ecs'] ?? []) { client.emitter.invoke(':Ecs', payload.ecs);
listener(payload.ecs);
}
}, [ecs]); }, [ecs]);
useEcsTick((payload) => { useEcsTick((payload) => {
let localMainEntity = mainEntity; let localMainEntity = mainEntity;

View File

@ -9,8 +9,8 @@ import DebugContext from '@/react/context/debug.js';
import EcsContext from '@/react/context/ecs.js'; import EcsContext from '@/react/context/ecs.js';
import MainEntityContext from '@/react/context/main-entity.js'; import MainEntityContext from '@/react/context/main-entity.js';
import RadiansContext from '@/react/context/radians.js'; import RadiansContext from '@/react/context/radians.js';
import {juggleSession} from '@/server/session.server.js';
import {TAU} from '@/util/math.js'; import {TAU} from '@/util/math.js';
import {juggleSession} from '@/util/session.server';
export async function loader({request}) { export async function loader({request}) {
await juggleSession(request); await juggleSession(request);

View File

@ -1,7 +1,7 @@
import {useEffect, useState} from 'react'; import {useEffect, useState} from 'react';
import {Outlet, useParams} from 'react-router-dom'; import {Outlet, useParams} from 'react-router-dom';
import {decode, encode} from '@/packets/index.js'; import {decode, encode} from '@/net/packets/index.js';
import styles from './play.module.css'; import styles from './play.module.css';
@ -14,10 +14,10 @@ export default function Play() {
let Client; let Client;
switch (type) { switch (type) {
case 'local': case 'local':
({default: Client} = await import('@/net/client/local.js')); ({default: Client} = await import('@/client/local.js'));
break; break;
case 'remote': case 'remote':
({default: Client} = await import('@/net/client/remote.js')); ({default: Client} = await import('@/client/remote.js'));
break; break;
} }
class SilphiusClient extends Client { class SilphiusClient extends Client {

View File

@ -62,6 +62,6 @@ export default async function createPlayer(id) {
activeSlot: 0, activeSlot: 0,
}, },
}; };
return (new TextEncoder()).encode(JSON.stringify(player)); return player;
} }

View File

@ -1,11 +1,12 @@
import {LRUCache} from 'lru-cache';
import Ecs from '@/ecs/ecs.js'; import Ecs from '@/ecs/ecs.js';
import {decode, encode} from '@/packets/index.js'; import {decode, encode} from '@/net/packets/index.js';
import { import {
CHUNK_SIZE, CHUNK_SIZE,
RESOLUTION, RESOLUTION,
TPS, TPS,
} from '@/util/constants.js'; } from '@/util/constants.js';
import Script from '@/util/script.js';
import createEcs from './create/ecs.js'; import createEcs from './create/ecs.js';
import createForest from './create/forest.js'; import createForest from './create/forest.js';
@ -13,12 +14,12 @@ import createHomestead from './create/homestead.js';
import createHouse from './create/house.js'; import createHouse from './create/house.js';
import createPlayer from './create/player.js'; import createPlayer from './create/player.js';
import {LRUCache} from 'lru-cache';
const cache = new LRUCache({ const cache = new LRUCache({
max: 128, max: 128,
}); });
const textEncoder = new TextEncoder();
export default class Engine { export default class Engine {
connectedPlayers = new Map(); connectedPlayers = new Map();
@ -59,41 +60,6 @@ export default class Engine {
} }
return cache.get(uri); return cache.get(uri);
} }
async readJson(uri) {
const key = ['$$json', uri].join(':');
if (!cache.has(key)) {
let promise, resolve, reject;
promise = new Promise((res, rej) => {
resolve = res;
reject = rej;
});
cache.set(key, promise);
this.readAsset(uri)
.then((chars) => {
resolve(chars.byteLength > 0 ? JSON.parse((new TextDecoder()).decode(chars)) : {});
})
.catch(reject);
}
return cache.get(key);
}
async readScript(uriOrCode, context) {
if (!uriOrCode) {
return undefined;
}
let code = '';
if (!uriOrCode.startsWith('/')) {
code = uriOrCode;
}
else {
const buffer = await this.readAsset(uriOrCode);
if (buffer.byteLength > 0) {
code = (new TextDecoder()).decode(buffer);
}
}
if (code) {
return Script.fromCode(code, context);
}
}
async switchEcs(entity, path, updates) { async switchEcs(entity, path, updates) {
for (const [connection, connectedPlayer] of engine.connectedPlayers) { for (const [connection, connectedPlayer] of engine.connectedPlayers) {
if (entity !== connectedPlayer.entity) { if (entity !== connectedPlayer.entity) {
@ -280,9 +246,9 @@ export default class Engine {
} }
async loadPlayer(id) { async loadPlayer(id) {
let buffer; let player;
try { try {
buffer = await this.server.readData(['players', `${id}`].join('/')) player = await this.server.readJson(['players', `${id}`].join('/'))
} }
catch (error) { catch (error) {
if ('ENOENT' !== error.code) { if ('ENOENT' !== error.code) {
@ -309,13 +275,10 @@ export default class Engine {
['forests', `${id}`].join('/'), ['forests', `${id}`].join('/'),
forest, forest,
); );
buffer = await createPlayer(id); player = await createPlayer(id);
await this.server.writeData( await this.savePlayer(id, player);
['players', `${id}`].join('/'),
buffer,
);
} }
return JSON.parse((new TextDecoder()).decode(buffer)); return player;
} }
async saveEcs(path, ecs) { async saveEcs(path, ecs) {
@ -332,8 +295,7 @@ export default class Engine {
} }
async savePlayer(id, entity) { async savePlayer(id, entity) {
const encoder = new TextEncoder(); const buffer = textEncoder.encode(JSON.stringify(entity));
const buffer = encoder.encode(JSON.stringify(entity.toJSON()));
await this.server.writeData(['players', `${id}`].join('/'), buffer); await this.server.writeData(['players', `${id}`].join('/'), buffer);
} }
@ -360,6 +322,7 @@ export default class Engine {
stop() { stop() {
clearTimeout(this.handle); clearTimeout(this.handle);
this.handle = undefined; this.handle = undefined;
this.tick(0);
} }
tick(elapsed) { tick(elapsed) {

View File

@ -3,8 +3,8 @@ import {dirname, join} from 'node:path';
import {WebSocketServer} from 'ws'; import {WebSocketServer} from 'ws';
import Server from '@/net/server/server.js'; import Server from '@/net/server.js';
import {getSession} from '@/util/session.server.js'; import {getSession} from '@/server/session.server.js';
import Engine from './engine.js'; import Engine from './engine.js';
@ -48,7 +48,7 @@ class SocketServer extends Server {
await mkdir(path, {recursive: true}); await mkdir(path, {recursive: true});
} }
static qualify(path) { static qualify(path) {
return join(import.meta.dirname, '..', 'data', 'remote', 'UNIVERSE', path); return join(import.meta.dirname, '..', '..', 'data', 'remote', 'UNIVERSE', path);
} }
async readAsset(path) { async readAsset(path) {
const url = new URL(path, 'https://localhost:3000') const url = new URL(path, 'https://localhost:3000')

View File

@ -1,14 +1,14 @@
import {del, get, set} from 'idb-keyval'; import {del, get, set} from 'idb-keyval';
import {encode} from '@/packets/index.js'; import {encode} from '@/net/packets/index.js';
import Server from '@/net/server.js';
import createEcs from '../../create/ecs.js'; import createEcs from './create/ecs.js';
import '../../create/forest.js'; import './create/forest.js';
import '../../create/homestead.js'; import './create/homestead.js';
import '../../create/player.js'; import './create/player.js';
import Engine from '../../engine.js'; import Engine from './engine.js';
import Server from './server.js';
class WorkerServer extends Server { class WorkerServer extends Server {
constructor() { constructor() {
@ -45,8 +45,8 @@ const engine = new Engine(WorkerServer);
onmessage = async (event) => { onmessage = async (event) => {
if (0 === event.data) { if (0 === event.data) {
engine.stop();
await engine.disconnectPlayer(0); await engine.disconnectPlayer(0);
engine.stop();
await engine.saveEcses(); await engine.saveEcses();
postMessage(0); postMessage(0);
return; return;
@ -78,13 +78,12 @@ if (import.meta.hot) {
await engine.saveEcses(); await engine.saveEcses();
beforeResolver.resolve(); beforeResolver.resolve();
}); });
import.meta.hot.accept('../../engine.js'); import.meta.hot.accept('./engine.js');
import.meta.hot.accept('../../create-player.js', async ({default: createPlayer}) => { import.meta.hot.accept('./create/player.js', async ({default: createPlayer}) => {
const resolver = createResolver(); const resolver = createResolver();
resolvers.push(resolver); resolvers.push(resolver);
await beforeResolver; await beforeResolver;
const oldBuffer = await engine.server.readData('players/0'); const oldPlayer = await engine.server.readJson('players/0');
const oldPlayer = JSON.parse((new TextDecoder()).decode(oldBuffer));
const buffer = await createPlayer(0); const buffer = await createPlayer(0);
const player = JSON.parse((new TextDecoder()).decode(buffer)); const player = JSON.parse((new TextDecoder()).decode(buffer));
// Less jarring // Less jarring
@ -94,7 +93,7 @@ if (import.meta.hot) {
await engine.server.writeData('players/0', (new TextEncoder()).encode(JSON.stringify(player))); await engine.server.writeData('players/0', (new TextEncoder()).encode(JSON.stringify(player)));
resolver.resolve(); resolver.resolve();
}); });
import.meta.hot.accept('../../create-forest.js', async ({default: createForest}) => { import.meta.hot.accept('./create/forest.js', async ({default: createForest}) => {
const resolver = createResolver(); const resolver = createResolver();
resolvers.push(resolver); resolvers.push(resolver);
await beforeResolver; await beforeResolver;
@ -107,7 +106,7 @@ if (import.meta.hot) {
await engine.saveEcs('forests/0', forest); await engine.saveEcs('forests/0', forest);
resolver.resolve(); resolver.resolve();
}); });
import.meta.hot.accept('../../create-homestead.js', async ({default: createHomestead}) => { import.meta.hot.accept('./create/homestead.js', async ({default: createHomestead}) => {
const resolver = createResolver(); const resolver = createResolver();
resolvers.push(resolver); resolvers.push(resolver);
await beforeResolver; await beforeResolver;

30
app/util/event-emitter.js Normal file
View File

@ -0,0 +1,30 @@
import fastCall from './fast-call.js';
export default class EventEmitter {
$$listeners = {};
addListener(type, listener) {
if (!this.$$listeners[type]) {
this.$$listeners[type] = new Set();
}
this.$$listeners[type].add(listener);
}
invoke(type, ...args) {
const listeners = this.$$listeners[type];
if (!listeners) {
return;
}
for (const listener of listeners) {
fastCall(listener, undefined, args);
}
}
removeListener(type, listener) {
const listeners = this.$$listeners[type];
if (!listeners) {
return;
}
listeners.delete(listener);
if (0 === listeners.size) {
delete this.$$listeners[type];
}
}
}

View File

@ -1,4 +1,4 @@
export default function(fn, holder, args) { export default function fastCall(fn, holder, args) {
if (holder) { if (holder) {
const {name} = fn; const {name} = fn;
if ('object' === typeof holder && name in holder && holder[name] === fn) { if ('object' === typeof holder && name in holder && holder[name] === fn) {