diff --git a/app/client-ecs.js b/app/client-ecs.js new file mode 100644 index 0000000..49a45d4 --- /dev/null +++ b/app/client-ecs.js @@ -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); + } + } +} diff --git a/app/context/ecs.js b/app/context/ecs.js index 44ce680..f02af05 100644 --- a/app/context/ecs.js +++ b/app/context/ecs.js @@ -11,6 +11,6 @@ export function useEcs() { } export function useEcsTick(fn, dependencies) { - const ecs = useEcs(); + const [ecs] = useEcs(); usePacket(':Ecs', fn, [ecs, ...dependencies]); } \ No newline at end of file diff --git a/app/create-homestead.js b/app/create-homestead.js index e76350d..4cde265 100644 --- a/app/create-homestead.js +++ b/app/create-homestead.js @@ -30,6 +30,7 @@ export default async function createHomestead(Ecs) { ], collisionStartScript: '/assets/shit-shack/collision-start.js', }, + Ecs: {}, Position: {x: 100, y: 100}, Sprite: { anchor: {x: 0.5, y: 0.8}, diff --git a/app/create-house.js b/app/create-house.js index aa7ffac..7819f5d 100644 --- a/app/create-house.js +++ b/app/create-house.js @@ -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; } diff --git a/app/engine.js b/app/engine.js index 34ae5cb..b9cfdf8 100644 --- a/app/engine.js +++ b/app/engine.js @@ -13,13 +13,11 @@ import createPlayer from './create-player.js'; export default class Engine { - connections = []; connectedPlayers = new Map(); - connectingPlayers = []; ecses = {}; frame = 0; handle; - incomingActions = []; + incomingActions = new Map(); last = Date.now(); server; @@ -33,6 +31,7 @@ export default class Engine { super.transmit(connection, encode(packet)); } } + const engine = this; const server = this.server = new SilphiusServer(); this.Ecs = class EngineEcs extends Ecs { async readAsset(uri) { @@ -51,63 +50,102 @@ export default class Engine { 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.incomingActions.push([connection, payload]); + if (!this.incomingActions.has(connection)) { + this.incomingActions.set(connection, []); + } + this.incomingActions.get(connection).push(payload); }); } acceptActions() { - for (const [ - connection, - payload, - ] of this.incomingActions) { + 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; - switch (payload.type) { - case 'changeSlot': { - if (!Controlled.locked) { - Wielder.activeSlot = payload.value - 1; + for (const payload of payloads) { + switch (payload.type) { + case 'changeSlot': { + if (!Controlled.locked) { + Wielder.activeSlot = payload.value - 1; + } + break; } - break; - } - case 'moveUp': - case 'moveRight': - case 'moveDown': - case 'moveLeft': { - Controlled[payload.type] = payload.value; - break; - } - case 'swapSlots': { - if (!Controlled.locked) { - Inventory.swapSlots(...payload.value); + case 'moveUp': + case 'moveRight': + case 'moveDown': + case 'moveLeft': { + Controlled[payload.type] = payload.value; + break; } - break; - } - case 'use': { - if (!Controlled.locked) { - Wielder.useActiveItem(payload.value); + case 'swapSlots': { + if (!Controlled.locked) { + Inventory.swapSlots(...payload.value); + } + break; } - 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); + 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; } - break; } } + this.incomingActions.set(connection, []); } - this.incomingActions = []; } async connectPlayer(connection, id) { @@ -117,7 +155,6 @@ export default class Engine { } const ecs = this.ecses[entityJson.Ecs.path]; const entity = await ecs.create(entityJson); - this.connections.push(connection); this.connectedPlayers.set( connection, { @@ -138,7 +175,7 @@ export default class Engine { await this.savePlayer(id, entity); ecs.destroy(entity.id); this.connectedPlayers.delete(connection); - this.connections.splice(this.connections.indexOf(connection), 1); + this.incomingActions.delete(connection); } async load() { @@ -160,13 +197,17 @@ export default class Engine { if ('ENOENT' !== error.code) { throw error; } + const homestead = await createHomestead(this.Ecs); + homestead.get(2).Ecs.path = ['houses', `${id}`].join('/'); await this.saveEcs( ['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( ['houses', `${id}`].join('/'), - await createHouse(this.Ecs), + house, ); buffer = await createPlayer(id); await this.server.writeData( @@ -228,7 +269,10 @@ export default class Engine { } update(elapsed) { - for (const connection of this.connections) { + for (const [connection, {entity}] of this.connectedPlayers) { + if (!entity) { + continue; + } this.server.send( connection, { diff --git a/app/net/server/worker.js b/app/net/server/worker.js index e7b2166..3f85153 100644 --- a/app/net/server/worker.js +++ b/app/net/server/worker.js @@ -97,7 +97,9 @@ if (import.meta.hot) { await beforeResolver; delete engine.ecses['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(); }); import.meta.hot.on('vite:afterUpdate', async () => { diff --git a/app/packets/ecs-change.js b/app/packets/ecs-change.js new file mode 100644 index 0000000..dd4236e --- /dev/null +++ b/app/packets/ecs-change.js @@ -0,0 +1,3 @@ +import Packet from '@/net/packet.js'; + +export default class EcsChange extends Packet {} diff --git a/app/react-components/ecs.jsx b/app/react-components/ecs.jsx index 3914c30..5408531 100644 --- a/app/react-components/ecs.jsx +++ b/app/react-components/ecs.jsx @@ -2,6 +2,7 @@ import {Container} from '@pixi/react'; import {useState} from 'react'; import {RESOLUTION} from '@/constants.js'; +import {usePacket} from '@/context/client.js'; import {useEcs, useEcsTick} from '@/context/ecs.js'; import {useMainEntity} from '@/context/main-entity.js'; @@ -15,6 +16,9 @@ export default function Ecs({scale}) { const [ecs] = useEcs(); const [entities, setEntities] = useState({}); const [mainEntity] = useMainEntity(); + usePacket('EcsChange', async () => { + setEntities({}); + }, [setEntities]); useEcsTick((payload) => { if (!ecs) { return; diff --git a/app/react-components/ui.jsx b/app/react-components/ui.jsx index 28c43ba..2c57712 100644 --- a/app/react-components/ui.jsx +++ b/app/react-components/ui.jsx @@ -1,6 +1,7 @@ import {useEffect, useState} from 'react'; import addKeyListener from '@/add-key-listener.js'; +import ClientEcs from '@/client-ecs'; import {RESOLUTION} from '@/constants.js'; import {useClient, usePacket} from '@/context/client.js'; import {useDebug} from '@/context/debug.js'; @@ -24,12 +25,26 @@ export default function Ui({disconnected}) { const client = useClient(); const [mainEntity, setMainEntity] = useMainEntity(); const [debug, setDebug] = useDebug(); - const [ecs] = useEcs(); + const [ecs, setEcs] = useEcs(); const [showDisconnected, setShowDisconnected] = useState(false); const [bufferSlot, setBufferSlot] = useState(); const [hotbarSlots, setHotbarSlots] = useState(emptySlots()); const [activeSlot, setActiveSlot] = useState(0); 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(() => { let handle; if (disconnected) { @@ -165,6 +180,10 @@ export default function Ui({disconnected}) { } }); }, [client, debug, setDebug, setScale]); + usePacket('EcsChange', async () => { + setMainEntity(undefined); + setEcs(new ClientEcs({Components, Systems})); + }, [Components, Systems, setEcs, setMainEntity]); usePacket('Tick', async (payload, client) => { if (0 === Object.keys(payload.ecs).length) { return; diff --git a/app/routes/_main-menu.play.$.$/route.jsx b/app/routes/_main-menu.play.$.$/route.jsx index f50fc16..3beaea1 100644 --- a/app/routes/_main-menu.play.$.$/route.jsx +++ b/app/routes/_main-menu.play.$.$/route.jsx @@ -7,45 +7,8 @@ import ClientContext from '@/context/client.js'; import DebugContext from '@/context/debug.js'; import EcsContext from '@/context/ecs.js'; import MainEntityContext from '@/context/main-entity.js'; -import Ecs from '@/ecs/ecs.js'; import Ui from '@/react-components/ui.jsx'; 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}) { await juggleSession(request); @@ -59,25 +22,10 @@ export default function PlaySpecific() { const mainEntityTuple = useState(); const setMainEntity = mainEntityTuple[1]; const debugTuple = useState(false); - const [Components, setComponents] = useState(); - const [Systems, setSystems] = useState(); const ecsTuple = useState(); - const setEcs = ecsTuple[1]; const [disconnected, setDisconnected] = useState(false); const params = useParams(); 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(() => { if (!Client) { return; diff --git a/public/assets/house/collision-start.js b/public/assets/house/collision-start.js new file mode 100644 index 0000000..876585b --- /dev/null +++ b/public/assets/house/collision-start.js @@ -0,0 +1,10 @@ +ecs.switchEcs( + other, + entity.Ecs.path, + { + Position: { + x: 74, + y: 108, + }, + }, +); diff --git a/public/assets/shit-shack/collision-start.js b/public/assets/shit-shack/collision-start.js index 05ead87..7e7b678 100644 --- a/public/assets/shit-shack/collision-start.js +++ b/public/assets/shit-shack/collision-start.js @@ -1 +1,10 @@ -console.log("I'ma warp yo azz") +ecs.switchEcs( + other, + entity.Ecs.path, + { + Position: { + x: 72, + y: 304, + }, + }, +);