base2020/src/Net/LockstepClient.ts

88 lines
2.9 KiB
TypeScript

import { Callbag } from "callbag";
import animationFrames from "callbag-animation-frames";
import map from "callbag-map";
import { INPUT_FREQUENCY, LockstepProcessor, LockstepState } from "../Ecs/Lockstep";
export const enum MessageTypes {
RESET = 0,
INPUT = 1,
}
export type Packet<TypeId, Payload> = { t: TypeId } & Payload;
export type ClientMessage<Input, State> =
| Packet<MessageTypes.INPUT, { i: Partial<Input> }>;
export type ServerMessage<Input, State> =
| Packet<MessageTypes.RESET, { s: Partial<State> }>
| Packet<MessageTypes.INPUT, { i: Input }>;
type Server<Input, State> = Callbag<ClientMessage<Input, State>, ServerMessage<Input, State>>;
export abstract class LockstepClient<Input, State> {
private state: LockstepState<Input, State>;
public constructor(
public readonly engine: LockstepProcessor<Input, State>,
) {
const initialState = this.initState({});
this.state = new LockstepState(initialState, engine);
}
public abstract initState(init: Partial<State>): State;
public abstract gatherInput(): Input;
/**
* Connect to a [perhaps emulated] server and return a disconnect callback
*/
public connect(server: Server<Input, State>): () => void {
let serverTalkback: Server<Input, State> | 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<Input, State> | ServerMessage<Input, State>) => {
if (mode == 0) {
serverTalkback = data as Server<Input, State>;
// kickoff input sender
setTimeout(sampleInput, INPUT_FREQUENCY);
} else if (mode == 1) {
// server message
const message = data as ServerMessage<Input, State>;
switch (message.t) {
case MessageTypes.RESET:
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 = map((ms: number) => this.state.getStateToRender())(animationFrames);
}