489 lines
14 KiB
JavaScript
489 lines
14 KiB
JavaScript
import {
|
|
CHUNK_SIZE,
|
|
RESOLUTION,
|
|
TPS,
|
|
} from '@/constants.js';
|
|
import Ecs from '@/ecs/ecs.js';
|
|
import {decode, encode} from '@/packets/index.js';
|
|
import Script from '@/util/script.js';
|
|
|
|
import createEcs from './create-ecs.js';
|
|
import createForest from './create-forest.js';
|
|
import createHomestead from './create-homestead.js';
|
|
import createHouse from './create-house.js';
|
|
import createPlayer from './create-player.js';
|
|
|
|
import {LRUCache} from 'lru-cache';
|
|
|
|
const cache = new LRUCache({
|
|
max: 128,
|
|
});
|
|
|
|
export default class Engine {
|
|
|
|
connectedPlayers = new Map();
|
|
ecses = {};
|
|
frame = 0;
|
|
handle;
|
|
incomingActions = new Map();
|
|
last = Date.now();
|
|
server;
|
|
|
|
constructor(Server) {
|
|
this.ecses = {};
|
|
class SilphiusServer extends Server {
|
|
accept(connection, packed) {
|
|
super.accept(connection, decode(packed));
|
|
}
|
|
transmit(connection, packet) {
|
|
super.transmit(connection, encode(packet));
|
|
}
|
|
}
|
|
const engine = this;
|
|
const server = this.server = new SilphiusServer();
|
|
this.Ecs = class EngineEcs extends Ecs {
|
|
get frame() {
|
|
return engine.frame;
|
|
}
|
|
async readAsset(uri) {
|
|
if (!cache.has(uri)) {
|
|
let promise, resolve, reject;
|
|
promise = new Promise((res, rej) => {
|
|
resolve = res;
|
|
reject = rej;
|
|
});
|
|
cache.set(uri, promise);
|
|
server.readAsset(uri)
|
|
.then(resolve)
|
|
.catch(reject);
|
|
}
|
|
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) {
|
|
for (const [connection, connectedPlayer] of engine.connectedPlayers) {
|
|
if (entity !== connectedPlayer.entity) {
|
|
continue;
|
|
}
|
|
// remove entity link to connection to start queueing actions and pause updates
|
|
delete connectedPlayer.entity;
|
|
// forget previous state
|
|
connectedPlayer.memory = {
|
|
chunks: new Map(),
|
|
nearby: new Set(),
|
|
};
|
|
// inform client of the upcoming change
|
|
server.send(
|
|
connection,
|
|
{
|
|
type: 'EcsChange',
|
|
payload: {},
|
|
},
|
|
);
|
|
// dump entity state with updates for the transition
|
|
const dumped = {
|
|
...entity.toJSON(),
|
|
Controlled: entity.Controlled,
|
|
Ecs: {path},
|
|
...updates,
|
|
};
|
|
// remove from old ECS
|
|
this.destroy(entity.id);
|
|
// load if necessary
|
|
if (!engine.ecses[path]) {
|
|
await engine.loadEcs(path);
|
|
}
|
|
// recreate the entity in the new ECS and again associate it with the connection
|
|
connectedPlayer.entity = engine.ecses[path].get(await engine.ecses[path].create(dumped));
|
|
}
|
|
}
|
|
}
|
|
this.server.addPacketListener('Action', (connection, payload) => {
|
|
if (!this.incomingActions.has(connection)) {
|
|
this.incomingActions.set(connection, []);
|
|
}
|
|
this.incomingActions.get(connection).push(payload);
|
|
});
|
|
this.server.addPacketListener('AdminAction', (connection, payload) => {
|
|
// check...
|
|
if (!this.incomingActions.has(connection)) {
|
|
this.incomingActions.set(connection, []);
|
|
}
|
|
this.incomingActions.get(connection).push(payload);
|
|
});
|
|
}
|
|
|
|
acceptActions() {
|
|
for (const [connection, payloads] of this.incomingActions) {
|
|
if (!this.connectedPlayers.get(connection)) {
|
|
continue;
|
|
}
|
|
const {entity} = this.connectedPlayers.get(connection);
|
|
if (!entity) {
|
|
continue;
|
|
}
|
|
const {Controlled, Ecs, Interacts, Inventory, Wielder} = entity;
|
|
for (const payload of payloads) {
|
|
switch (payload.type) {
|
|
case 'paint': {
|
|
const ecs = this.ecses[Ecs.path];
|
|
const {TileLayers} = ecs.get(1);
|
|
const {brush, layer: paintLayer, stamp} = payload.value;
|
|
const layer = TileLayers.layer(paintLayer);
|
|
switch (brush) {
|
|
case 0: {
|
|
layer.stamp(stamp.at, stamp.data)
|
|
break;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case 'changeSlot': {
|
|
if (!Controlled.locked) {
|
|
Wielder.activeSlot = payload.value - 1;
|
|
}
|
|
break;
|
|
}
|
|
case 'moveUp':
|
|
case 'moveRight':
|
|
case 'moveDown':
|
|
case 'moveLeft': {
|
|
Controlled[payload.type] = payload.value;
|
|
break;
|
|
}
|
|
case 'swapSlots': {
|
|
if (!Controlled.locked) {
|
|
Inventory.swapSlots(...payload.value);
|
|
}
|
|
break;
|
|
}
|
|
case 'use': {
|
|
if (!Controlled.locked) {
|
|
Wielder.useActiveItem(payload.value);
|
|
}
|
|
break;
|
|
}
|
|
case 'interact': {
|
|
if (!Controlled.locked) {
|
|
if (payload.value) {
|
|
if (Interacts.willInteractWith) {
|
|
const ecs = this.ecses[Ecs.path];
|
|
const subject = ecs.get(Interacts.willInteractWith);
|
|
subject.Interactive.interact(entity);
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
this.incomingActions.set(connection, []);
|
|
}
|
|
}
|
|
|
|
async connectPlayer(connection, id) {
|
|
const entityJson = await this.loadPlayer(id);
|
|
if (!this.ecses[entityJson.Ecs.path]) {
|
|
await this.loadEcs(entityJson.Ecs.path);
|
|
}
|
|
const ecs = this.ecses[entityJson.Ecs.path];
|
|
const entity = await ecs.create(entityJson);
|
|
this.connectedPlayers.set(
|
|
connection,
|
|
{
|
|
entity: ecs.get(entity),
|
|
id,
|
|
memory: {
|
|
chunks: new Map(),
|
|
nearby: new Set(),
|
|
},
|
|
},
|
|
);
|
|
}
|
|
|
|
async disconnectPlayer(connection) {
|
|
const connectedPlayer = this.connectedPlayers.get(connection);
|
|
if (!connectedPlayer) {
|
|
return;
|
|
}
|
|
const {entity, id} = connectedPlayer;
|
|
const ecs = this.ecses[entity.Ecs.path];
|
|
await this.savePlayer(id, entity);
|
|
ecs.destroy(entity.id);
|
|
this.connectedPlayers.delete(connection);
|
|
this.incomingActions.delete(connection);
|
|
}
|
|
|
|
async load() {
|
|
}
|
|
|
|
async loadEcs(path) {
|
|
this.ecses[path] = await this.Ecs.deserialize(
|
|
createEcs(this.Ecs),
|
|
await this.server.readData(path),
|
|
);
|
|
}
|
|
|
|
async loadPlayer(id) {
|
|
let buffer;
|
|
try {
|
|
buffer = await this.server.readData(['players', `${id}`].join('/'))
|
|
}
|
|
catch (error) {
|
|
if ('ENOENT' !== error.code) {
|
|
throw error;
|
|
}
|
|
const homestead = createEcs(this.Ecs);
|
|
for (const entity of await createHomestead(id)) {
|
|
await homestead.create(entity);
|
|
}
|
|
await this.saveEcs(
|
|
['homesteads', `${id}`].join('/'),
|
|
homestead,
|
|
);
|
|
const house = await createHouse(this.Ecs, id);
|
|
await this.saveEcs(
|
|
['houses', `${id}`].join('/'),
|
|
house,
|
|
);
|
|
const forest = createEcs(this.Ecs);
|
|
for (const entity of await createForest()) {
|
|
await forest.create(entity);
|
|
}
|
|
await this.saveEcs(
|
|
['forests', `${id}`].join('/'),
|
|
forest,
|
|
);
|
|
buffer = await createPlayer(id);
|
|
await this.server.writeData(
|
|
['players', `${id}`].join('/'),
|
|
buffer,
|
|
);
|
|
}
|
|
return JSON.parse((new TextDecoder()).decode(buffer));
|
|
}
|
|
|
|
async saveEcs(path, ecs) {
|
|
const view = this.Ecs.serialize(ecs);
|
|
await this.server.writeData(path, view);
|
|
}
|
|
|
|
async saveEcses() {
|
|
const promises = []
|
|
for (const i in this.ecses) {
|
|
promises.push(this.saveEcs(i, this.ecses[i]));
|
|
}
|
|
await Promise.all(promises);
|
|
}
|
|
|
|
async savePlayer(id, entity) {
|
|
const encoder = new TextEncoder();
|
|
const buffer = encoder.encode(JSON.stringify(entity.toJSON()));
|
|
await this.server.writeData(['players', `${id}`].join('/'), buffer);
|
|
}
|
|
|
|
setClean() {
|
|
for (const i in this.ecses) {
|
|
this.ecses[i].setClean();
|
|
}
|
|
}
|
|
|
|
start() {
|
|
const loop = async () => {
|
|
this.acceptActions();
|
|
const elapsed = (Date.now() - this.last) / 1000;
|
|
this.last = Date.now();
|
|
this.tick(elapsed);
|
|
this.update(elapsed);
|
|
this.setClean();
|
|
this.frame += 1;
|
|
this.handle = setTimeout(loop, 1000 / TPS);
|
|
};
|
|
loop();
|
|
}
|
|
|
|
stop() {
|
|
clearTimeout(this.handle);
|
|
this.handle = undefined;
|
|
}
|
|
|
|
tick(elapsed) {
|
|
for (const i in this.ecses) {
|
|
this.ecses[i].tick(elapsed);
|
|
}
|
|
}
|
|
|
|
update(elapsed) {
|
|
for (const [connection, {entity}] of this.connectedPlayers) {
|
|
if (!entity) {
|
|
continue;
|
|
}
|
|
this.server.send(
|
|
connection,
|
|
{
|
|
type: 'Tick',
|
|
payload: {
|
|
ecs: this.updateFor(connection),
|
|
elapsed,
|
|
frame: this.frame,
|
|
},
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
updateFor(connection) {
|
|
const update = {};
|
|
const {entity, memory} = this.connectedPlayers.get(connection);
|
|
const mainEntityId = entity.id;
|
|
const ecs = this.ecses[entity.Ecs.path];
|
|
// Entities within half a screen offscreen.
|
|
const x0 = entity.Position.x - RESOLUTION.x;
|
|
const y0 = entity.Position.y - RESOLUTION.y;
|
|
const nearby = ecs.system('VisibleAabbs').within({
|
|
x0,
|
|
x1: x0 + (RESOLUTION.x * 2),
|
|
y0,
|
|
y1: y0 + (RESOLUTION.y * 2),
|
|
});
|
|
// Master entity.
|
|
const master = ecs.get(1);
|
|
nearby.add(master);
|
|
const lastNearby = new Set(memory.nearby.values());
|
|
const firstUpdate = 0 === lastNearby.size;
|
|
for (const entity of nearby) {
|
|
const {id} = entity;
|
|
lastNearby.delete(id);
|
|
if (!memory.nearby.has(id)) {
|
|
update[id] = entity.toJSON();
|
|
if (mainEntityId === id) {
|
|
update[id].MainEntity = {};
|
|
}
|
|
}
|
|
else if (ecs.diff[id]) {
|
|
update[id] = ecs.diff[id];
|
|
}
|
|
memory.nearby.add(id);
|
|
}
|
|
for (const id of lastNearby) {
|
|
memory.nearby.delete(id);
|
|
update[id] = false;
|
|
}
|
|
// Tile layer chunking
|
|
const {TileLayers} = master;
|
|
const {layers} = TileLayers;
|
|
let layerChange;
|
|
for (const i in layers) {
|
|
const layer = TileLayers.layer(i);
|
|
const cx = CHUNK_SIZE * layer.tileSize.x;
|
|
const cy = CHUNK_SIZE * layer.tileSize.y;
|
|
const rx = 1 + Math.ceil((RESOLUTION.x * 2) / cx);
|
|
const ry = 1 + Math.ceil((RESOLUTION.y * 2) / cy);
|
|
const lx = Math.floor((entity.Position.x - RESOLUTION.x) / cx);
|
|
const ly = Math.floor((entity.Position.y - RESOLUTION.y) / cy);
|
|
const ax = Math.ceil(layer.area.x / CHUNK_SIZE);
|
|
for (let wy = 0; wy < ry; ++wy) {
|
|
for (let wx = 0; wx < rx; ++wx) {
|
|
const iy = wy + ly;
|
|
const ix = wx + lx;
|
|
if (
|
|
ix >= 0
|
|
&& iy >= 0
|
|
&& ix < Math.ceil(layer.area.x / CHUNK_SIZE)
|
|
&& iy < Math.ceil(layer.area.y / CHUNK_SIZE)
|
|
) {
|
|
const chunk = iy * ax + ix;
|
|
if (!memory.chunks.has(i)) {
|
|
memory.chunks.set(i, new Set());
|
|
}
|
|
if (!memory.chunks.get(i).has(chunk)) {
|
|
memory.chunks.get(i).add(chunk);
|
|
if (!layerChange) {
|
|
layerChange = {};
|
|
}
|
|
if (!layerChange[i]) {
|
|
layerChange[i] = {};
|
|
}
|
|
for (let y = 0; y < CHUNK_SIZE; ++y) {
|
|
for (let x = 0; x < CHUNK_SIZE; ++x) {
|
|
const ty = (iy * CHUNK_SIZE) + y;
|
|
const tx = (ix * CHUNK_SIZE) + x;
|
|
if (
|
|
tx < 0
|
|
|| ty < 0
|
|
|| tx >= layers[i].area.x
|
|
|| ty >= layers[i].area.y
|
|
) {
|
|
continue;
|
|
}
|
|
const computed = ((iy * CHUNK_SIZE) + y) * layers[i].area.x + ((ix * CHUNK_SIZE) + x);
|
|
layerChange[i][computed] = layers[i].data[computed];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (firstUpdate && update['1']) {
|
|
const {TileLayers} = update['1'];
|
|
if (TileLayers) {
|
|
const layersUpdate = [];
|
|
const {layers} = TileLayers;
|
|
for (const l in layers) {
|
|
layersUpdate[l] = {
|
|
...layers[l],
|
|
data: [],
|
|
};
|
|
}
|
|
update['1'].TileLayers = {layers: layersUpdate};
|
|
}
|
|
}
|
|
if (layerChange) {
|
|
if (!update['1']) {
|
|
update['1'] = {};
|
|
}
|
|
if (!update['1'].TileLayers) {
|
|
update['1'].TileLayers = {};
|
|
}
|
|
update['1'].TileLayers.layerChange = layerChange;
|
|
}
|
|
return update;
|
|
}
|
|
|
|
}
|