diff --git a/plan.txt b/plan.txt index 0c202c7..85476dc 100644 --- a/plan.txt +++ b/plan.txt @@ -1,11 +1,11 @@ Open: -- Refactor input generics to distinct local/full types - Multiplayer loopback server - Insecured websocket server implementation - Cloneable RNG that goes in state (use MurmurHash3 finalizer in counter mode?) - remove all random() calls Done: +- Refactor input generics to distinct local/full types - Refactor input messages for more than one player - Rework State implementation for easier cloning/deserialization - Test Lockstep/rollback diff --git a/src/Ecs/Lockstep.ts b/src/Ecs/Lockstep.ts index 711b76b..e1e78dd 100644 --- a/src/Ecs/Lockstep.ts +++ b/src/Ecs/Lockstep.ts @@ -1,28 +1,28 @@ export const INPUT_FREQUENCY = 33; // roughly 30fps -export interface LockstepProcessor { - compareInput(a: Input[], b: Input[]): boolean; - predictInput(prev: Input[] | null, localPlayer: number, localInput: Input): Input[]; +export interface LockstepProcessor { + compareInput(a: GlobalInput, b: GlobalInput): boolean; + predictInput(prev: GlobalInput | null, localPlayer: number, localInput: LocalInput): GlobalInput; cloneState(source: State): State; - advanceState(state: State, input: Input[]): void; + advanceState(state: State, input: GlobalInput): void; } -export class LockstepState { +export class LockstepState { private inputIndex = -1; - private inputLog: Input[][] = []; + private inputLog: GlobalInput[] = []; private canonIndex = -1; private canonState: State; private renderIndex = -1; private renderState: State; - constructor(private initialState: State, private engine: LockstepProcessor) { + constructor(private initialState: State, private engine: LockstepProcessor) { this.canonState = engine.cloneState(initialState); this.renderState = engine.cloneState(initialState); } - public addCanonInput(input: Input[]): void { + public addCanonInput(input: GlobalInput): void { this.canonIndex++; // advance canonical game state @@ -39,7 +39,7 @@ export class LockstepState { } /** Warning: this only supports one player input per frame. There is no support for two players using the same lockstep simulation instance. */ - public addLocalInput(player: number, input: Input): void { + public addLocalInput(player: number, input: LocalInput): void { this.inputIndex++; // ensure that we don't overwrite the canon input with local input somehow @@ -66,13 +66,13 @@ export class LockstepState { } } -export class Playback { +export class Playback { private frame = 0; public constructor( private state: State, - private inputLog: Input[][], - private engine: LockstepProcessor, + private inputLog: GlobalInput[], + private engine: LockstepProcessor, ) {} public getNextState(): State { diff --git a/src/Game/GameComponents.ts b/src/Game/GameComponents.ts index a289019..e11a608 100644 --- a/src/Game/GameComponents.ts +++ b/src/Game/GameComponents.ts @@ -176,7 +176,7 @@ export class PlayerControl extends Component { } } -export class Engine implements LockstepProcessor { +export class Engine implements LockstepProcessor { cloneState(old: Data) { return new Data(old); } diff --git a/src/Game/Main.ts b/src/Game/Main.ts index 28fb918..2f75099 100644 --- a/src/Game/Main.ts +++ b/src/Game/Main.ts @@ -11,7 +11,7 @@ import { Loopback } from "../Net/LoopbackServer"; import { Data, Engine, PlayerControl } from "./GameComponents"; import { Buttons } from "./Input"; -export class Main extends LockstepClient { +export class Main extends LockstepClient { buttons = new Buttons(); diff --git a/src/Net/LockstepClient.ts b/src/Net/LockstepClient.ts index 43ea9df..02fddcb 100644 --- a/src/Net/LockstepClient.ts +++ b/src/Net/LockstepClient.ts @@ -35,29 +35,29 @@ export const enum MessageTypes { export type Packet = { t: TypeId } & Payload; -export type ClientMessage = +export type ClientMessage = | Packet }> - | Packet + | Packet | Packet | Packet ; -export type ServerMessage = +export type ServerMessage = | Packet }> - | Packet + | Packet | Packet | Packet ; -export type Server = Callbag, ServerMessage>; +export type Server = Callbag, ServerMessage>; -export abstract class LockstepClient { +export abstract class LockstepClient { private playerNumber = -1; - private state: LockstepState; + private state: LockstepState; public constructor( - public readonly engine: LockstepProcessor, + public readonly engine: LockstepProcessor, ) { const initialState = this.initState({}); this.state = new LockstepState(initialState, engine); @@ -65,13 +65,13 @@ export abstract class LockstepClient { public abstract initState(init: Partial): State; - public abstract gatherInput(): Input; + public abstract gatherInput(): LocalInput; /** * Connect to a [perhaps emulated] server and return a disconnect callback */ - public connect(server: Server): () => void { - let serverTalkback: Server | null = null; + public connect(server: Server): () => void { + let serverTalkback: Server | null = null; const sampleInput = () => { if (serverTalkback) { @@ -85,15 +85,15 @@ export abstract class LockstepClient { }; // connect to server - server(0, (mode: number, data: Server | ServerMessage) => { + server(0, (mode: number, data: Server | ServerMessage) => { if (mode == 0) { - serverTalkback = data as Server; + serverTalkback = data as Server; // kickoff input sender setTimeout(sampleInput, INPUT_FREQUENCY); } else if (mode == 1) { // server message - const message = data as ServerMessage; + const message = data as ServerMessage; switch (message.t) { case MessageTypes.SET_STATE: const resetState = this.initState(message.s); diff --git a/src/Net/LoopbackServer.ts b/src/Net/LoopbackServer.ts index 323b858..fde9bde 100644 --- a/src/Net/LoopbackServer.ts +++ b/src/Net/LoopbackServer.ts @@ -2,17 +2,18 @@ import { Callbag } from "callbag"; import { ClientMessage, MessageTypes, ServerMessage } from "./LockstepClient"; -type Client = Callbag, ClientMessage>; +type Client = Callbag, ClientMessage>; -export function Loopback(start: number, data?: Client | ClientMessage) { +/** Stub loopback server that handles a single client, for schemes where GlobalInput = LocalInput[] */ +export function Loopback(start: number, data?: Client | ClientMessage) { if(start != 0) return; - const sink = data as Client; + const sink = data as Client; - sink(0, (type: number, data?: Client | ClientMessage) => { + sink(0, (type: number, data?: Client | ClientMessage) => { if(type == 1) { // message from client; just reflect for now - const message = data as ClientMessage; + const message = data as ClientMessage; switch(message.t) { case MessageTypes.INPUT: sink(1, {