feat: switch ecs

This commit is contained in:
cha0s 2024-07-03 11:17:36 -05:00
parent 5fb34d66ac
commit 3d862d39d5
12 changed files with 196 additions and 101 deletions

37
app/client-ecs.js Normal file
View File

@ -0,0 +1,37 @@
import Ecs from '@/ecs/ecs.js';
import Script from '@/util/script.js';
import {LRUCache} from 'lru-cache';
const cache = new LRUCache({
max: 128,
});
export default class ClientEcs extends Ecs {
async readAsset(uri) {
if (!cache.has(uri)) {
let promise, resolve, reject;
promise = new Promise((res, rej) => {
resolve = res;
reject = rej;
});
cache.set(uri, promise);
fetch(new URL(uri, window.location.origin))
.then(async (response) => {
resolve(response.ok ? response.arrayBuffer() : new ArrayBuffer(0));
})
.catch(reject);
}
return cache.get(uri);
}
async readJson(uri) {
const chars = await this.readAsset(uri);
return chars.byteLength > 0 ? JSON.parse((new TextDecoder()).decode(chars)) : {};
}
async readScript(uri, context = {}) {
const code = await this.readAsset(uri);
if (code.byteLength > 0) {
return Script.fromCode((new TextDecoder()).decode(code), context);
}
}
}

View File

@ -11,6 +11,6 @@ export function useEcs() {
} }
export function useEcsTick(fn, dependencies) { export function useEcsTick(fn, dependencies) {
const ecs = useEcs(); const [ecs] = useEcs();
usePacket(':Ecs', fn, [ecs, ...dependencies]); usePacket(':Ecs', fn, [ecs, ...dependencies]);
} }

View File

@ -30,6 +30,7 @@ export default async function createHomestead(Ecs) {
], ],
collisionStartScript: '/assets/shit-shack/collision-start.js', collisionStartScript: '/assets/shit-shack/collision-start.js',
}, },
Ecs: {},
Position: {x: 100, y: 100}, Position: {x: 100, y: 100},
Sprite: { Sprite: {
anchor: {x: 0.5, y: 0.8}, anchor: {x: 0.5, y: 0.8},

View File

@ -17,5 +17,23 @@ export default async function createHouse(Ecs) {
], ],
}, },
}); });
await ecs.create({
Collider: {
bodies: [
[
{x: -8, y: -8},
{x: 7, y: -8},
{x: 7, y: 7},
{x: -8, y: 7},
],
],
collisionStartScript: '/assets/house/collision-start.js',
},
Ecs: {},
Position: {
x: 72,
y: 320,
},
});
return ecs; return ecs;
} }

View File

@ -13,13 +13,11 @@ import createPlayer from './create-player.js';
export default class Engine { export default class Engine {
connections = [];
connectedPlayers = new Map(); connectedPlayers = new Map();
connectingPlayers = [];
ecses = {}; ecses = {};
frame = 0; frame = 0;
handle; handle;
incomingActions = []; incomingActions = new Map();
last = Date.now(); last = Date.now();
server; server;
@ -33,6 +31,7 @@ export default class Engine {
super.transmit(connection, encode(packet)); super.transmit(connection, encode(packet));
} }
} }
const engine = this;
const server = this.server = new SilphiusServer(); const server = this.server = new SilphiusServer();
this.Ecs = class EngineEcs extends Ecs { this.Ecs = class EngineEcs extends Ecs {
async readAsset(uri) { async readAsset(uri) {
@ -51,63 +50,102 @@ export default class Engine {
return Script.fromCode((new TextDecoder()).decode(code), context); return Script.fromCode((new TextDecoder()).decode(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.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));
}
}
} }
this.server.addPacketListener('Action', (connection, payload) => { this.server.addPacketListener('Action', (connection, payload) => {
this.incomingActions.push([connection, payload]); if (!this.incomingActions.has(connection)) {
this.incomingActions.set(connection, []);
}
this.incomingActions.get(connection).push(payload);
}); });
} }
acceptActions() { acceptActions() {
for (const [ for (const [connection, payloads] of this.incomingActions) {
connection,
payload,
] of this.incomingActions) {
if (!this.connectedPlayers.get(connection)) { if (!this.connectedPlayers.get(connection)) {
continue; continue;
} }
const {entity} = this.connectedPlayers.get(connection); const {entity} = this.connectedPlayers.get(connection);
if (!entity) {
continue;
}
const {Controlled, Ecs, Interacts, Inventory, Wielder} = entity; const {Controlled, Ecs, Interacts, Inventory, Wielder} = entity;
switch (payload.type) { for (const payload of payloads) {
case 'changeSlot': { switch (payload.type) {
if (!Controlled.locked) { case 'changeSlot': {
Wielder.activeSlot = payload.value - 1; if (!Controlled.locked) {
Wielder.activeSlot = payload.value - 1;
}
break;
} }
break; case 'moveUp':
} case 'moveRight':
case 'moveUp': case 'moveDown':
case 'moveRight': case 'moveLeft': {
case 'moveDown': Controlled[payload.type] = payload.value;
case 'moveLeft': { break;
Controlled[payload.type] = payload.value;
break;
}
case 'swapSlots': {
if (!Controlled.locked) {
Inventory.swapSlots(...payload.value);
} }
break; case 'swapSlots': {
} if (!Controlled.locked) {
case 'use': { Inventory.swapSlots(...payload.value);
if (!Controlled.locked) { }
Wielder.useActiveItem(payload.value); break;
} }
break; case 'use': {
} if (!Controlled.locked) {
case 'interact': { Wielder.useActiveItem(payload.value);
if (!Controlled.locked) { }
if (payload.value) { break;
if (Interacts.willInteractWith) { }
const ecs = this.ecses[Ecs.path]; case 'interact': {
const subject = ecs.get(Interacts.willInteractWith); if (!Controlled.locked) {
subject.Interactive.interact(entity); if (payload.value) {
if (Interacts.willInteractWith) {
const ecs = this.ecses[Ecs.path];
const subject = ecs.get(Interacts.willInteractWith);
subject.Interactive.interact(entity);
}
} }
} }
break;
} }
break;
} }
} }
this.incomingActions.set(connection, []);
} }
this.incomingActions = [];
} }
async connectPlayer(connection, id) { async connectPlayer(connection, id) {
@ -117,7 +155,6 @@ export default class Engine {
} }
const ecs = this.ecses[entityJson.Ecs.path]; const ecs = this.ecses[entityJson.Ecs.path];
const entity = await ecs.create(entityJson); const entity = await ecs.create(entityJson);
this.connections.push(connection);
this.connectedPlayers.set( this.connectedPlayers.set(
connection, connection,
{ {
@ -138,7 +175,7 @@ export default class Engine {
await this.savePlayer(id, entity); await this.savePlayer(id, entity);
ecs.destroy(entity.id); ecs.destroy(entity.id);
this.connectedPlayers.delete(connection); this.connectedPlayers.delete(connection);
this.connections.splice(this.connections.indexOf(connection), 1); this.incomingActions.delete(connection);
} }
async load() { async load() {
@ -160,13 +197,17 @@ export default class Engine {
if ('ENOENT' !== error.code) { if ('ENOENT' !== error.code) {
throw error; throw error;
} }
const homestead = await createHomestead(this.Ecs);
homestead.get(2).Ecs.path = ['houses', `${id}`].join('/');
await this.saveEcs( await this.saveEcs(
['homesteads', `${id}`].join('/'), ['homesteads', `${id}`].join('/'),
await createHomestead(this.Ecs), homestead,
); );
const house = await createHouse(this.Ecs);
house.get(2).Ecs.path = ['homesteads', `${id}`].join('/');
await this.saveEcs( await this.saveEcs(
['houses', `${id}`].join('/'), ['houses', `${id}`].join('/'),
await createHouse(this.Ecs), house,
); );
buffer = await createPlayer(id); buffer = await createPlayer(id);
await this.server.writeData( await this.server.writeData(
@ -228,7 +269,10 @@ export default class Engine {
} }
update(elapsed) { update(elapsed) {
for (const connection of this.connections) { for (const [connection, {entity}] of this.connectedPlayers) {
if (!entity) {
continue;
}
this.server.send( this.server.send(
connection, connection,
{ {

View File

@ -97,7 +97,9 @@ if (import.meta.hot) {
await beforeResolver; await beforeResolver;
delete engine.ecses['homesteads/0']; delete engine.ecses['homesteads/0'];
await engine.server.removeData('homesteads/0'); await engine.server.removeData('homesteads/0');
await engine.saveEcs('homesteads/0', await createHomestead(engine.Ecs)); const homestead = await createHomestead(engine.Ecs);
homestead.get(2).Ecs.path = 'houses/0';
await engine.saveEcs('homesteads/0', homestead);
resolver.resolve(); resolver.resolve();
}); });
import.meta.hot.on('vite:afterUpdate', async () => { import.meta.hot.on('vite:afterUpdate', async () => {

View File

@ -0,0 +1,3 @@
import Packet from '@/net/packet.js';
export default class EcsChange extends Packet {}

View File

@ -2,6 +2,7 @@ import {Container} from '@pixi/react';
import {useState} from 'react'; import {useState} from 'react';
import {RESOLUTION} from '@/constants.js'; import {RESOLUTION} from '@/constants.js';
import {usePacket} from '@/context/client.js';
import {useEcs, useEcsTick} from '@/context/ecs.js'; import {useEcs, useEcsTick} from '@/context/ecs.js';
import {useMainEntity} from '@/context/main-entity.js'; import {useMainEntity} from '@/context/main-entity.js';
@ -15,6 +16,9 @@ export default function Ecs({scale}) {
const [ecs] = useEcs(); const [ecs] = useEcs();
const [entities, setEntities] = useState({}); const [entities, setEntities] = useState({});
const [mainEntity] = useMainEntity(); const [mainEntity] = useMainEntity();
usePacket('EcsChange', async () => {
setEntities({});
}, [setEntities]);
useEcsTick((payload) => { useEcsTick((payload) => {
if (!ecs) { if (!ecs) {
return; return;

View File

@ -1,6 +1,7 @@
import {useEffect, useState} from 'react'; import {useEffect, useState} from 'react';
import addKeyListener from '@/add-key-listener.js'; import addKeyListener from '@/add-key-listener.js';
import ClientEcs from '@/client-ecs';
import {RESOLUTION} from '@/constants.js'; import {RESOLUTION} from '@/constants.js';
import {useClient, usePacket} from '@/context/client.js'; import {useClient, usePacket} from '@/context/client.js';
import {useDebug} from '@/context/debug.js'; import {useDebug} from '@/context/debug.js';
@ -24,12 +25,26 @@ export default function Ui({disconnected}) {
const client = useClient(); const client = useClient();
const [mainEntity, setMainEntity] = useMainEntity(); const [mainEntity, setMainEntity] = useMainEntity();
const [debug, setDebug] = useDebug(); const [debug, setDebug] = useDebug();
const [ecs] = useEcs(); const [ecs, setEcs] = useEcs();
const [showDisconnected, setShowDisconnected] = useState(false); const [showDisconnected, setShowDisconnected] = useState(false);
const [bufferSlot, setBufferSlot] = useState(); const [bufferSlot, setBufferSlot] = useState();
const [hotbarSlots, setHotbarSlots] = useState(emptySlots()); const [hotbarSlots, setHotbarSlots] = useState(emptySlots());
const [activeSlot, setActiveSlot] = useState(0); const [activeSlot, setActiveSlot] = useState(0);
const [scale, setScale] = useState(2); const [scale, setScale] = useState(2);
const [Components, setComponents] = useState();
const [Systems, setSystems] = useState();
useEffect(() => {
async function setEcsStuff() {
const {default: Components} = await import('@/ecs-components/index.js');
const {default: Systems} = await import('@/ecs-systems/index.js');
setComponents(Components);
setSystems(Systems);
}
setEcsStuff();
}, []);
useEffect(() => {
setEcs(new ClientEcs({Components, Systems}));
}, [Components, setEcs, Systems]);
useEffect(() => { useEffect(() => {
let handle; let handle;
if (disconnected) { if (disconnected) {
@ -165,6 +180,10 @@ export default function Ui({disconnected}) {
} }
}); });
}, [client, debug, setDebug, setScale]); }, [client, debug, setDebug, setScale]);
usePacket('EcsChange', async () => {
setMainEntity(undefined);
setEcs(new ClientEcs({Components, Systems}));
}, [Components, Systems, setEcs, setMainEntity]);
usePacket('Tick', async (payload, client) => { usePacket('Tick', async (payload, client) => {
if (0 === Object.keys(payload.ecs).length) { if (0 === Object.keys(payload.ecs).length) {
return; return;

View File

@ -7,45 +7,8 @@ import ClientContext from '@/context/client.js';
import DebugContext from '@/context/debug.js'; import DebugContext from '@/context/debug.js';
import EcsContext from '@/context/ecs.js'; import EcsContext from '@/context/ecs.js';
import MainEntityContext from '@/context/main-entity.js'; import MainEntityContext from '@/context/main-entity.js';
import Ecs from '@/ecs/ecs.js';
import Ui from '@/react-components/ui.jsx'; import Ui from '@/react-components/ui.jsx';
import {juggleSession} from '@/session.server'; import {juggleSession} from '@/session.server';
import Script from '@/util/script.js';
import {LRUCache} from 'lru-cache';
const cache = new LRUCache({
max: 128,
});
class ClientEcs extends Ecs {
async readAsset(uri) {
if (!cache.has(uri)) {
let promise, resolve, reject;
promise = new Promise((res, rej) => {
resolve = res;
reject = rej;
});
cache.set(uri, promise);
fetch(new URL(uri, window.location.origin))
.then(async (response) => {
resolve(response.ok ? response.arrayBuffer() : new ArrayBuffer(0));
})
.catch(reject);
}
return cache.get(uri);
}
async readJson(uri) {
const chars = await this.readAsset(uri);
return chars.byteLength > 0 ? JSON.parse((new TextDecoder()).decode(chars)) : {};
}
async readScript(uri, context = {}) {
const code = await this.readAsset(uri);
if (code.byteLength > 0) {
return Script.fromCode((new TextDecoder()).decode(code), context);
}
}
}
export async function loader({request}) { export async function loader({request}) {
await juggleSession(request); await juggleSession(request);
@ -59,25 +22,10 @@ export default function PlaySpecific() {
const mainEntityTuple = useState(); const mainEntityTuple = useState();
const setMainEntity = mainEntityTuple[1]; const setMainEntity = mainEntityTuple[1];
const debugTuple = useState(false); const debugTuple = useState(false);
const [Components, setComponents] = useState();
const [Systems, setSystems] = useState();
const ecsTuple = useState(); const ecsTuple = useState();
const setEcs = ecsTuple[1];
const [disconnected, setDisconnected] = useState(false); const [disconnected, setDisconnected] = useState(false);
const params = useParams(); const params = useParams();
const [type, url] = params['*'].split('/'); const [type, url] = params['*'].split('/');
useEffect(() => {
async function setEcsStuff() {
const {default: Components} = await import('@/ecs-components/index.js');
const {default: Systems} = await import('@/ecs-systems/index.js');
setComponents(Components);
setSystems(Systems);
}
setEcsStuff();
});
useEffect(() => {
setEcs(new ClientEcs({Components, Systems}));
}, [Components, setEcs, Systems]);
useEffect(() => { useEffect(() => {
if (!Client) { if (!Client) {
return; return;

View File

@ -0,0 +1,10 @@
ecs.switchEcs(
other,
entity.Ecs.path,
{
Position: {
x: 74,
y: 108,
},
},
);

View File

@ -1 +1,10 @@
console.log("I'ma warp yo azz") ecs.switchEcs(
other,
entity.Ecs.path,
{
Position: {
x: 72,
y: 304,
},
},
);