import { Callbag } from "callbag"; import animationFrames from "callbag-animation-frames"; import map from "callbag-map"; import pipe from "callbag-pipe"; import { INPUT_FREQUENCY, LockstepProcessor, LockstepState } from "../Ecs/Lockstep"; export const enum MessageTypes { META = 0, SET_STATE = 1, INPUT = 2, GET_STATE = 3, PING = 4 } export type Packet = { t: TypeId } & Payload; export type ClientMessage = | Packet }> | Packet; export type ServerMessage = | Packet }> | Packet; export type Server = Callbag, ServerMessage>; export abstract class LockstepClient { private state: LockstepState; public constructor( public readonly engine: LockstepProcessor, ) { const initialState = this.initState({}); this.state = new LockstepState(initialState, engine); } public abstract initState(init: Partial): State; public abstract gatherInput(): Input; /** * Connect to a [perhaps emulated] server and return a disconnect callback */ public connect(server: Server): () => void { let serverTalkback: Server | null = null; const sampleInput = () => { if (serverTalkback) { const input = this.gatherInput(); this.state.addLocalInput(input); serverTalkback(1, { t: MessageTypes.INPUT, i: input }); setTimeout(sampleInput, INPUT_FREQUENCY); } }; // connect to server server(0, (mode: number, data: Server | ServerMessage) => { if (mode == 0) { serverTalkback = data as Server; // kickoff input sender setTimeout(sampleInput, INPUT_FREQUENCY); } else if (mode == 1) { // server message const message = data as ServerMessage; switch (message.t) { case MessageTypes.SET_STATE: const resetState = this.initState(message.s); this.state = new LockstepState(resetState, this.engine); break; case MessageTypes.INPUT: this.state.addCanonInput(message.i); break; } } else if (mode == 2) { // disconnected console.log("Disconnected from server", data); serverTalkback = null; } }); // disposal return () => { serverTalkback?.(2); serverTalkback = null; }; } public renderFrames = pipe( animationFrames, map(_ms => this.state.getStateToRender()) ); }