feat: HMR++

This commit is contained in:
cha0s 2024-07-02 20:43:55 -05:00
parent b21ef309aa
commit 26b85f6520
8 changed files with 203 additions and 135 deletions

29
app/create-ecs.js Normal file
View File

@ -0,0 +1,29 @@
import Components from '@/ecs-components/index.js';
import Systems from '@/ecs-systems/index.js';
export default function createEcs(Ecs) {
const ecs = new Ecs({Components, Systems});
const defaultSystems = [
'ResetForces',
'ApplyControlMovement',
'IntegratePhysics',
'ClampPositions',
'PlantGrowth',
'FollowCamera',
'VisibleAabbs',
'Collliders',
'ControlDirection',
'SpriteDirection',
'RunAnimations',
'RunTickingPromises',
'Water',
'Interactions',
];
defaultSystems.forEach((defaultSystem) => {
const System = ecs.system(defaultSystem);
if (System) {
System.active = true;
}
});
return ecs;
}

30
app/create-homestead.js Normal file
View File

@ -0,0 +1,30 @@
import createEcs from './create-ecs.js';
export default async function createHomestead(Ecs) {
const ecs = createEcs(Ecs);
const area = {x: 100, y: 60};
await ecs.create({
AreaSize: {x: area.x * 16, y: area.y * 16},
Engine: {},
TileLayers: {
layers: [
{
area,
data: Array(area.x * area.y).fill(0).map(() => 1 + Math.floor(Math.random() * 4)),
source: '/assets/tileset.json',
tileSize: {x: 16, y: 16},
}
],
},
Water: {water: {}},
});
await ecs.create({
Position: {x: 100, y: 100},
Sprite: {
anchor: {x: 0.5, y: 0.8},
source: '/assets/shit-shack/shit-shack.json',
},
VisibleAabb: {},
});
return ecs;
}

50
app/create-player.js Normal file
View File

@ -0,0 +1,50 @@
export default async function createPlayer(id) {
const player = {
Camera: {},
Controlled: {},
Direction: {direction: 2},
Ecs: {path: ['homesteads', `${id}`].join('/')},
Emitter: {},
Forces: {},
Interacts: {},
Inventory: {
slots: {
1: {
qty: 100,
source: '/assets/potion/potion.json',
},
2: {
qty: 1,
source: '/assets/watering-can/watering-can.json',
},
3: {
qty: 1,
source: '/assets/tomato-seeds/tomato-seeds.json',
},
4: {
qty: 1,
source: '/assets/hoe/hoe.json',
},
},
},
Health: {health: 100},
Position: {x: 368, y: 368},
Speed: {speed: 100},
Sound: {},
Sprite: {
anchor: {x: 0.5, y: 0.8},
animation: 'moving:down',
frame: 0,
frames: 8,
source: '/assets/dude/dude.json',
speed: 0.115,
},
Ticking: {},
VisibleAabb: {},
Wielder: {
activeSlot: 0,
},
};
return (new TextEncoder()).encode(JSON.stringify(player));
}

View File

@ -3,14 +3,12 @@ import {
TPS,
} from '@/constants.js';
import Ecs from '@/ecs/ecs.js';
import Components from '@/ecs-components/index.js';
import Systems from '@/ecs-systems/index.js';
import {decode, encode} from '@/packets/index.js';
import Script from '@/util/script.js';
function join(...parts) {
return parts.join('/');
}
import createEcs from './create-ecs.js';
import createHomestead from './create-homestead.js';
import createPlayer from './create-player.js';
export default class Engine {
@ -126,118 +124,12 @@ export default class Engine {
);
}
createEcs() {
return new this.Ecs({Components, Systems});
}
async createHomestead(id) {
const ecs = this.createEcs();
const area = {x: 100, y: 60};
await ecs.create({
AreaSize: {x: area.x * 16, y: area.y * 16},
Engine: {},
TileLayers: {
layers: [
{
area,
data: Array(area.x * area.y).fill(0).map(() => 1 + Math.floor(Math.random() * 4)),
source: '/assets/tileset.json',
tileSize: {x: 16, y: 16},
}
],
},
Water: {water: {}},
});
await ecs.create({
Position: {x: 100, y: 100},
Sprite: {
anchor: {x: 0.5, y: 0.8},
source: '/assets/shit-shack/shit-shack.json',
},
VisibleAabb: {},
});
const defaultSystems = [
'ResetForces',
'ApplyControlMovement',
'IntegratePhysics',
'ClampPositions',
'PlantGrowth',
'FollowCamera',
'VisibleAabbs',
'Collliders',
'ControlDirection',
'SpriteDirection',
'RunAnimations',
'RunTickingPromises',
'Water',
'Interactions',
];
defaultSystems.forEach((defaultSystem) => {
const System = ecs.system(defaultSystem);
if (System) {
System.active = true;
}
});
await this.saveEcs(join('homesteads', `${id}`), ecs);
}
async createPlayer(id) {
const player = {
Camera: {},
Controlled: {},
Direction: {direction: 2},
Ecs: {path: join('homesteads', `${id}`)},
Emitter: {},
Forces: {},
Interacts: {},
Inventory: {
slots: {
// 1: {
// qty: 10,
// source: '/assets/potion/potion.json',
// },
2: {
qty: 1,
source: '/assets/watering-can/watering-can.json',
},
3: {
qty: 1,
source: '/assets/tomato-seeds/tomato-seeds.json',
},
4: {
qty: 1,
source: '/assets/hoe/hoe.json',
},
},
},
Health: {health: 100},
Position: {x: 368, y: 368},
Speed: {speed: 100},
Sound: {},
Sprite: {
anchor: {x: 0.5, y: 0.8},
animation: 'moving:down',
frame: 0,
frames: 8,
source: '/assets/dude/dude.json',
speed: 0.115,
},
Ticking: {},
VisibleAabb: {},
Wielder: {
activeSlot: 0,
},
};
const buffer = (new TextEncoder()).encode(JSON.stringify(player));
await this.server.writeData(
join('players', `${id}`),
buffer,
);
return buffer;
}
async disconnectPlayer(connection) {
const {entity, id} = this.connectedPlayers.get(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);
@ -250,7 +142,7 @@ export default class Engine {
async loadEcs(path) {
this.ecses[path] = await this.Ecs.deserialize(
this.createEcs(),
createEcs(this.Ecs),
await this.server.readData(path),
);
}
@ -264,8 +156,15 @@ export default class Engine {
if ('ENOENT' !== error.code) {
throw error;
}
await this.createHomestead(id);
buffer = await this.createPlayer(id);
await this.saveEcs(
['homesteads', `${id}`].join('/'),
await createHomestead(this.Ecs),
);
buffer = await createPlayer(id);
await this.server.writeData(
['players', `${id}`].join('/'),
buffer,
);
}
return JSON.parse((new TextDecoder()).decode(buffer));
}

View File

@ -2,6 +2,9 @@ import {del, get, set} from 'idb-keyval';
import {encode} from '@/packets/index.js';
import '../../create-homestead.js';
import '../../create-player.js';
import Engine from '../../engine.js';
import Server from './server.js';
@ -57,15 +60,50 @@ onmessage = async (event) => {
})();
if (import.meta.hot) {
import.meta.hot.accept('../../engine.js', async ({default: Engine}) => {
const createResolver = () => {
let r;
const promise = new Promise((resolve) => {
r = resolve;
});
promise.resolve = r;
return promise;
};
const beforeResolver = createResolver();
const resolvers = [];
import.meta.hot.on('vite:beforeUpdate', async () => {
engine.stop();
await engine.disconnectPlayer(0);
if (Engine.prototype.createHomestead.toString() !== engine.createHomestead.toString()) {
delete engine.ecses['homesteads/0'];
await engine.server.removeData('homesteads/0');
const newEngine = new Engine(WorkerServer);
await newEngine.createHomestead(0);
}
beforeResolver.resolve();
});
import.meta.hot.accept('../../engine.js');
import.meta.hot.accept('../../create-player.js', async ({default: createPlayer}) => {
const resolver = createResolver();
resolvers.push(resolver);
await beforeResolver;
const oldBuffer = await engine.server.readData('players/0');
const oldPlayer = JSON.parse((new TextDecoder()).decode(oldBuffer));
const buffer = await createPlayer(0);
const player = JSON.parse((new TextDecoder()).decode(buffer));
// Less jarring
player.Ecs = oldPlayer.Ecs;
player.Direction = oldPlayer.Direction;
player.Position = oldPlayer.Position;
await engine.server.writeData('players/0', (new TextEncoder()).encode(JSON.stringify(player)));
resolver.resolve();
});
import.meta.hot.accept('../../create-homestead.js', async ({default: createHomestead}) => {
const resolver = createResolver();
resolvers.push(resolver);
await beforeResolver;
delete engine.ecses['homesteads/0'];
await engine.server.removeData('homesteads/0');
await engine.saveEcs('homesteads/0', await createHomestead(engine.Ecs));
resolver.resolve();
});
import.meta.hot.on('vite:afterUpdate', async () => {
await Promise.all(resolvers);
postMessage(encode({type: 'ConnectionStatus', payload: 'aborted'}));
close();
});
import.meta.hot.accept();
}

View File

@ -16,6 +16,9 @@ export default function Ecs({scale}) {
const [entities, setEntities] = useState({});
const [mainEntity] = useMainEntity();
useEcsTick((payload) => {
if (!ecs) {
return;
}
const updatedEntities = {...entities};
for (const id in payload) {
const update = payload[id];
@ -34,10 +37,13 @@ export default function Ecs({scale}) {
}
setEntities(updatedEntities);
}, [ecs, entities, mainEntity]);
if (!mainEntity) {
if (!ecs || !mainEntity) {
return false;
}
const entity = ecs.get(mainEntity);
if (!entity) {
return false;
}
const {Direction, Position, Wielder} = entity;
const projected = Wielder.activeItem()?.project(Position.tile, Direction.direction)
const {Camera} = entity;

View File

@ -173,7 +173,7 @@ export default function Ui({disconnected}) {
for (const listener of client.listeners[':Ecs'] ?? []) {
listener(payload.ecs);
}
}, [hotbarSlots, mainEntity, setMainEntity]);
}, [ecs]);
useEcsTick((payload) => {
let localMainEntity = mainEntity;
for (const id in payload) {
@ -201,7 +201,7 @@ export default function Ui({disconnected}) {
}
}
}
}, [hotbarSlots, mainEntity, setMainEntity]);
}, [ecs, mainEntity]);
useEffect(() => {
function onContextMenu(event) {
event.preventDefault();

View File

@ -8,15 +8,13 @@ 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 Components from '@/ecs-components/index.js';
import Systems from '@/ecs-systems/index.js';
import Ui from '@/react-components/ui.jsx';
import {juggleSession} from '@/session.server';
import Script from '@/util/script.js';
import {LRUCache} from 'lru-cache';
export const cache = new LRUCache({
const cache = new LRUCache({
max: 128,
});
@ -59,11 +57,28 @@ export default function PlaySpecific() {
const assetsTuple = useState({});
const [client, setClient] = useState();
const mainEntityTuple = useState();
const setMainEntity = mainEntityTuple[1];
const debugTuple = useState(false);
const ecsTuple = useState(new ClientEcs({Components, Systems}));
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}));
setDisconnected(true);
}, [Components, setEcs, Systems]);
useEffect(() => {
if (!Client) {
return;
@ -117,9 +132,10 @@ export default function PlaySpecific() {
};
}, [client]);
useEffect(() => {
if (!disconnected) {
if (!client || !disconnected) {
return;
}
setMainEntity(undefined);
async function reconnect() {
await client.connect(url);
}
@ -128,7 +144,7 @@ export default function PlaySpecific() {
return () => {
clearInterval(handle);
};
}, [client, disconnected, url]);
}, [client, disconnected, setMainEntity, url]);
useEffect(() => {
let source = true;
async function play() {