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