silphius/app/engine.js

386 lines
10 KiB
JavaScript
Raw Normal View History

2024-06-10 22:42:30 -05:00
import {
2024-07-02 14:41:54 -05:00
RESOLUTION,
2024-06-10 22:42:30 -05:00
TPS,
} from '@/constants.js';
2024-06-15 18:23:10 -05:00
import Ecs from '@/ecs/ecs.js';
2024-06-10 23:55:06 -05:00
import {decode, encode} from '@/packets/index.js';
2024-06-28 12:12:38 -05:00
import Script from '@/util/script.js';
2024-06-10 22:42:30 -05:00
2024-07-02 20:43:55 -05:00
import createEcs from './create-ecs.js';
2024-07-10 14:59:07 -05:00
import createForest from './create-forest.js';
2024-07-02 20:43:55 -05:00
import createHomestead from './create-homestead.js';
2024-07-02 22:42:56 -05:00
import createHouse from './create-house.js';
2024-07-02 20:43:55 -05:00
import createPlayer from './create-player.js';
2024-06-15 20:59:11 -05:00
2024-07-10 14:14:10 -05:00
import {LRUCache} from 'lru-cache';
const cache = new LRUCache({
max: 128,
});
2024-06-10 22:42:30 -05:00
export default class Engine {
2024-06-13 12:24:32 -05:00
connectedPlayers = new Map();
ecses = {};
frame = 0;
2024-06-21 18:16:41 -05:00
handle;
2024-07-03 11:17:36 -05:00
incomingActions = new Map();
2024-06-13 12:24:32 -05:00
last = Date.now();
server;
2024-06-10 22:42:30 -05:00
constructor(Server) {
2024-06-14 15:18:55 -05:00
this.ecses = {};
2024-06-10 22:42:30 -05:00
class SilphiusServer extends Server {
accept(connection, packed) {
super.accept(connection, decode(packed));
}
transmit(connection, packet) {
super.transmit(connection, encode(packet));
}
}
2024-07-03 11:17:36 -05:00
const engine = this;
2024-06-27 10:53:52 -05:00
const server = this.server = new SilphiusServer();
this.Ecs = class EngineEcs extends Ecs {
2024-07-04 09:06:41 -05:00
get frame() {
return engine.frame;
}
2024-06-28 12:12:38 -05:00
async readAsset(uri) {
2024-07-10 14:14:10 -05:00
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);
2024-06-27 10:53:52 -05:00
}
2024-06-28 12:12:38 -05:00
async readJson(uri) {
2024-07-10 14:14:10 -05:00
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);
2024-06-28 12:12:38 -05:00
}
2024-07-01 17:23:04 -05:00
async readScript(uri, context) {
2024-07-02 22:42:56 -05:00
if (!uri) {
return undefined;
}
2024-06-28 12:12:38 -05:00
const code = await this.readAsset(uri);
if (code.byteLength > 0) {
2024-07-01 17:23:04 -05:00
return Script.fromCode((new TextDecoder()).decode(code), context);
2024-06-28 12:12:38 -05:00
}
}
2024-07-03 11:17:36 -05:00
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.clear();
// 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));
}
}
2024-06-27 10:53:52 -05:00
}
2024-06-13 01:26:01 -05:00
this.server.addPacketListener('Action', (connection, payload) => {
2024-07-03 11:17:36 -05:00
if (!this.incomingActions.has(connection)) {
this.incomingActions.set(connection, []);
}
this.incomingActions.get(connection).push(payload);
2024-06-10 22:42:30 -05:00
});
2024-07-07 23:30:48 -05:00
this.server.addPacketListener('AdminAction', (connection, payload) => {
// check...
if (!this.incomingActions.has(connection)) {
this.incomingActions.set(connection, []);
}
this.incomingActions.get(connection).push(payload);
});
2024-06-10 22:42:30 -05:00
}
2024-07-02 17:46:31 -05:00
acceptActions() {
2024-07-03 11:17:36 -05:00
for (const [connection, payloads] of this.incomingActions) {
if (!this.connectedPlayers.get(connection)) {
continue;
}
const {entity} = this.connectedPlayers.get(connection);
2024-07-03 11:17:36 -05:00
if (!entity) {
continue;
}
2024-07-01 18:12:53 -05:00
const {Controlled, Ecs, Interacts, Inventory, Wielder} = entity;
2024-07-03 11:17:36 -05:00
for (const payload of payloads) {
switch (payload.type) {
2024-07-07 23:30:48 -05:00
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);
2024-07-09 15:07:22 -05:00
switch (brush) {
case 0: {
layer.stamp(stamp.at, stamp.data)
break;
}
}
2024-07-07 23:30:48 -05:00
break;
}
2024-07-03 11:17:36 -05:00
case 'changeSlot': {
if (!Controlled.locked) {
Wielder.activeSlot = payload.value - 1;
}
break;
2024-06-25 12:05:14 -05:00
}
2024-07-03 11:17:36 -05:00
case 'moveUp':
case 'moveRight':
case 'moveDown':
case 'moveLeft': {
Controlled[payload.type] = payload.value;
break;
2024-06-25 12:05:14 -05:00
}
2024-07-03 11:17:36 -05:00
case 'swapSlots': {
if (!Controlled.locked) {
Inventory.swapSlots(...payload.value);
}
break;
2024-06-25 12:05:14 -05:00
}
2024-07-03 11:17:36 -05:00
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);
}
2024-07-01 18:12:53 -05:00
}
}
2024-07-03 11:17:36 -05:00
break;
2024-07-01 18:12:53 -05:00
}
}
2024-06-25 12:05:14 -05:00
}
2024-07-03 11:17:36 -05:00
this.incomingActions.set(connection, []);
2024-06-25 12:05:14 -05:00
}
}
2024-06-14 15:18:55 -05:00
async connectPlayer(connection, id) {
2024-07-02 17:46:31 -05:00
const entityJson = await this.loadPlayer(id);
if (!this.ecses[entityJson.Ecs.path]) {
await this.loadEcs(entityJson.Ecs.path);
2024-06-14 15:18:55 -05:00
}
2024-07-02 17:46:31 -05:00
const ecs = this.ecses[entityJson.Ecs.path];
const entity = await ecs.create(entityJson);
this.connectedPlayers.set(
connection,
{
entity: ecs.get(entity),
id,
memory: new Set(),
},
);
2024-06-10 22:42:30 -05:00
}
2024-06-14 15:18:55 -05:00
async disconnectPlayer(connection) {
2024-07-02 20:43:55 -05:00
const connectedPlayer = this.connectedPlayers.get(connection);
if (!connectedPlayer) {
return;
}
const {entity, id} = connectedPlayer;
2024-06-14 15:18:55 -05:00
const ecs = this.ecses[entity.Ecs.path];
await this.savePlayer(id, entity);
2024-06-10 22:42:30 -05:00
ecs.destroy(entity.id);
this.connectedPlayers.delete(connection);
2024-07-03 11:17:36 -05:00
this.incomingActions.delete(connection);
2024-06-10 22:42:30 -05:00
}
async load() {
}
2024-06-14 15:18:55 -05:00
async loadEcs(path) {
2024-06-27 10:53:52 -05:00
this.ecses[path] = await this.Ecs.deserialize(
2024-07-02 20:43:55 -05:00
createEcs(this.Ecs),
2024-06-15 18:23:10 -05:00
await this.server.readData(path),
);
2024-06-14 15:18:55 -05:00
}
async loadPlayer(id) {
let buffer;
try {
buffer = await this.server.readData(['players', `${id}`].join('/'))
}
catch (error) {
if ('ENOENT' !== error.code) {
throw error;
}
2024-07-09 15:05:23 -05:00
const homestead = await createHomestead(this.Ecs, id);
2024-07-02 20:43:55 -05:00
await this.saveEcs(
['homesteads', `${id}`].join('/'),
2024-07-03 11:17:36 -05:00
homestead,
2024-07-02 20:43:55 -05:00
);
2024-07-09 15:05:23 -05:00
const house = await createHouse(this.Ecs, id);
2024-07-02 22:42:56 -05:00
await this.saveEcs(
['houses', `${id}`].join('/'),
2024-07-03 11:17:36 -05:00
house,
2024-07-02 22:42:56 -05:00
);
2024-07-10 14:59:07 -05:00
const forest = await createForest(this.Ecs, id);
await this.saveEcs(
['forests', `${id}`].join('/'),
forest,
);
2024-07-02 20:43:55 -05:00
buffer = await createPlayer(id);
await this.server.writeData(
['players', `${id}`].join('/'),
buffer,
);
2024-06-14 15:18:55 -05:00
}
return JSON.parse((new TextDecoder()).decode(buffer));
}
2024-06-21 18:16:41 -05:00
async saveEcs(path, ecs) {
2024-06-27 10:53:52 -05:00
const view = this.Ecs.serialize(ecs);
2024-06-21 18:16:41 -05:00
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);
}
2024-06-14 15:18:55 -05:00
async savePlayer(id, entity) {
const encoder = new TextEncoder();
const buffer = encoder.encode(JSON.stringify(entity.toJSON()));
await this.server.writeData(['players', `${id}`].join('/'), buffer);
2024-06-10 22:42:30 -05:00
}
2024-06-25 12:05:14 -05:00
setClean() {
for (const i in this.ecses) {
this.ecses[i].setClean();
}
}
2024-06-10 22:42:30 -05:00
start() {
2024-07-02 12:00:12 -05:00
const loop = async () => {
2024-07-02 17:46:31 -05:00
this.acceptActions();
2024-06-10 22:42:30 -05:00
const elapsed = (Date.now() - this.last) / 1000;
this.last = Date.now();
this.tick(elapsed);
this.update(elapsed);
2024-06-25 12:05:14 -05:00
this.setClean();
2024-06-10 22:42:30 -05:00
this.frame += 1;
2024-07-02 12:00:12 -05:00
this.handle = setTimeout(loop, 1000 / TPS);
};
loop();
2024-06-10 22:42:30 -05:00
}
2024-06-21 18:16:41 -05:00
stop() {
2024-07-02 12:00:12 -05:00
clearTimeout(this.handle);
2024-06-21 18:16:41 -05:00
this.handle = undefined;
}
2024-06-10 22:42:30 -05:00
tick(elapsed) {
2024-06-12 19:35:51 -05:00
for (const i in this.ecses) {
2024-06-10 22:42:30 -05:00
this.ecses[i].tick(elapsed);
}
}
update(elapsed) {
2024-07-03 11:17:36 -05:00
for (const [connection, {entity}] of this.connectedPlayers) {
if (!entity) {
continue;
}
2024-06-10 22:42:30 -05:00
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);
2024-06-11 01:41:19 -05:00
const mainEntityId = entity.id;
2024-06-14 15:18:55 -05:00
const ecs = this.ecses[entity.Ecs.path];
2024-07-02 14:41:54 -05:00
// 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),
});
2024-06-12 13:19:16 -05:00
// Master entity.
nearby.add(ecs.get(1));
2024-06-10 22:42:30 -05:00
const lastMemory = new Set(memory.values());
for (const entity of nearby) {
const {id} = entity;
lastMemory.delete(id);
if (!memory.has(id)) {
update[id] = entity.toJSON();
2024-06-11 01:41:19 -05:00
if (mainEntityId === id) {
update[id].MainEntity = {};
}
2024-06-10 22:42:30 -05:00
}
else if (ecs.diff[id]) {
update[id] = ecs.diff[id];
}
memory.add(id);
}
for (const id of lastMemory) {
memory.delete(id);
update[id] = false;
}
return update;
}
}