Compare commits
7 Commits
6957365723
...
a872d9ae5f
Author | SHA1 | Date | |
---|---|---|---|
|
a872d9ae5f | ||
|
1a7d55f6d3 | ||
|
8be6c23711 | ||
|
a27f79dc33 | ||
|
ede9614e2d | ||
|
8bf26ef7cd | ||
|
1d35b9b6d5 |
|
@ -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() {
|
|
@ -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;
|
|
@ -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});
|
|
@ -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 = {};
|
||||||
|
|
|
@ -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) {
|
|
||||||
System.elapsed += elapsed;
|
|
||||||
if (System.elapsed < System.frequency) {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
while (!System.frequency || System.elapsed >= System.frequency) {
|
|
||||||
System.tick(System.frequency ? System.elapsed : elapsed);
|
|
||||||
for (let j = 0; j < System.destroying.length; j++) {
|
|
||||||
destroying.add(System.destroying[j]);
|
|
||||||
}
|
|
||||||
System.tickDestruction();
|
|
||||||
if (!System.frequency) {
|
if (!System.frequency) {
|
||||||
break;
|
System.tick(elapsed);
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
System.elapsed += elapsed;
|
||||||
|
while (System.elapsed >= System.frequency) {
|
||||||
|
System.tick(System.frequency);
|
||||||
System.elapsed -= System.frequency;
|
System.elapsed -= System.frequency;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
if (this.destroying.size > 0) {
|
||||||
if (destroying.size > 0) {
|
this.destroyMany(this.destroying);
|
||||||
this.destroyMany(destroying.values());
|
this.destroying.clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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() {}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
27
app/net/client.js
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
32
app/net/server.js
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -62,6 +62,6 @@ export default async function createPlayer(id) {
|
||||||
activeSlot: 0,
|
activeSlot: 0,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
return (new TextEncoder()).encode(JSON.stringify(player));
|
return player;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
|
@ -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')
|
|
@ -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
30
app/util/event-emitter.js
Normal 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];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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) {
|
||||||
|
|
Loading…
Reference in New Issue
Block a user