split generics for local/full input types for now

LoopbackServer still makes the assumption that GlobalInput = LocalInput[]
This commit is contained in:
Tangent Wantwight 2020-05-01 22:51:21 -04:00
parent 743905cad4
commit 258c179887
6 changed files with 35 additions and 34 deletions

View file

@ -1,11 +1,11 @@
Open: Open:
- Refactor input generics to distinct local/full types
- Multiplayer loopback server - Multiplayer loopback server
- Insecured websocket server implementation - Insecured websocket server implementation
- Cloneable RNG that goes in state (use MurmurHash3 finalizer in counter mode?) - Cloneable RNG that goes in state (use MurmurHash3 finalizer in counter mode?)
- remove all random() calls - remove all random() calls
Done: Done:
- Refactor input generics to distinct local/full types
- Refactor input messages for more than one player - Refactor input messages for more than one player
- Rework State implementation for easier cloning/deserialization - Rework State implementation for easier cloning/deserialization
- Test Lockstep/rollback - Test Lockstep/rollback

View file

@ -1,28 +1,28 @@
export const INPUT_FREQUENCY = 33; // roughly 30fps export const INPUT_FREQUENCY = 33; // roughly 30fps
export interface LockstepProcessor<Input, State> { export interface LockstepProcessor<LocalInput, GlobalInput, State> {
compareInput(a: Input[], b: Input[]): boolean; compareInput(a: GlobalInput, b: GlobalInput): boolean;
predictInput(prev: Input[] | null, localPlayer: number, localInput: Input): Input[]; predictInput(prev: GlobalInput | null, localPlayer: number, localInput: LocalInput): GlobalInput;
cloneState(source: State): State; cloneState(source: State): State;
advanceState(state: State, input: Input[]): void; advanceState(state: State, input: GlobalInput): void;
} }
export class LockstepState<Input, State> { export class LockstepState<LocalInput, GlobalInput, State> {
private inputIndex = -1; private inputIndex = -1;
private inputLog: Input[][] = []; private inputLog: GlobalInput[] = [];
private canonIndex = -1; private canonIndex = -1;
private canonState: State; private canonState: State;
private renderIndex = -1; private renderIndex = -1;
private renderState: State; private renderState: State;
constructor(private initialState: State, private engine: LockstepProcessor<Input, State>) { constructor(private initialState: State, private engine: LockstepProcessor<LocalInput, GlobalInput, State>) {
this.canonState = engine.cloneState(initialState); this.canonState = engine.cloneState(initialState);
this.renderState = engine.cloneState(initialState); this.renderState = engine.cloneState(initialState);
} }
public addCanonInput(input: Input[]): void { public addCanonInput(input: GlobalInput): void {
this.canonIndex++; this.canonIndex++;
// advance canonical game state // advance canonical game state
@ -39,7 +39,7 @@ export class LockstepState<Input, State> {
} }
/** Warning: this only supports one player input per frame. There is no support for two players using the same lockstep simulation instance. */ /** 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++; this.inputIndex++;
// ensure that we don't overwrite the canon input with local input somehow // ensure that we don't overwrite the canon input with local input somehow
@ -66,13 +66,13 @@ export class LockstepState<Input, State> {
} }
} }
export class Playback<Input, State> { export class Playback<LocalInput, GlobalInput, State> {
private frame = 0; private frame = 0;
public constructor( public constructor(
private state: State, private state: State,
private inputLog: Input[][], private inputLog: GlobalInput[],
private engine: LockstepProcessor<Input, State>, private engine: LockstepProcessor<LocalInput, GlobalInput, State>,
) {} ) {}
public getNextState(): State { public getNextState(): State {

View file

@ -176,7 +176,7 @@ export class PlayerControl extends Component<PlayerControl> {
} }
} }
export class Engine implements LockstepProcessor<KeyName[], Data> { export class Engine implements LockstepProcessor<KeyName[], KeyName[][], Data> {
cloneState(old: Data) { cloneState(old: Data) {
return new Data(old); return new Data(old);
} }

View file

@ -11,7 +11,7 @@ import { Loopback } from "../Net/LoopbackServer";
import { Data, Engine, PlayerControl } from "./GameComponents"; import { Data, Engine, PlayerControl } from "./GameComponents";
import { Buttons } from "./Input"; import { Buttons } from "./Input";
export class Main extends LockstepClient<KeyName[], Data> { export class Main extends LockstepClient<KeyName[], KeyName[][], Data> {
buttons = new Buttons(); buttons = new Buttons();

View file

@ -35,29 +35,29 @@ export const enum MessageTypes {
export type Packet<TypeId, Payload> = { t: TypeId } & Payload; export type Packet<TypeId, Payload> = { t: TypeId } & Payload;
export type ClientMessage<Input, State> = export type ClientMessage<LocalInput, State> =
| Packet<MessageTypes.SET_STATE, { s: Partial<State> }> | Packet<MessageTypes.SET_STATE, { s: Partial<State> }>
| Packet<MessageTypes.INPUT, { i: Input }> | Packet<MessageTypes.INPUT, { i: LocalInput }>
| Packet<MessageTypes.GET_STATE, { c: number, s: State }> | Packet<MessageTypes.GET_STATE, { c: number, s: State }>
| Packet<MessageTypes.PING, { z: number }> | Packet<MessageTypes.PING, { z: number }>
; ;
export type ServerMessage<Input, State> = export type ServerMessage<GlobalInput, State> =
| Packet<MessageTypes.SET_STATE, { u: number, s: Partial<State> }> | Packet<MessageTypes.SET_STATE, { u: number, s: Partial<State> }>
| Packet<MessageTypes.INPUT, { i: Input[] }> | Packet<MessageTypes.INPUT, { i: GlobalInput }>
| Packet<MessageTypes.GET_STATE, { c: number }> | Packet<MessageTypes.GET_STATE, { c: number }>
| Packet<MessageTypes.PING, { z: number }> | Packet<MessageTypes.PING, { z: number }>
; ;
export type Server<Input, State> = Callbag<ClientMessage<Input, State>, ServerMessage<Input, State>>; export type Server<LocalInput, GlobalInput, State> = Callbag<ClientMessage<LocalInput, State>, ServerMessage<GlobalInput, State>>;
export abstract class LockstepClient<Input, State> { export abstract class LockstepClient<LocalInput, GlobalInput, State> {
private playerNumber = -1; private playerNumber = -1;
private state: LockstepState<Input, State>; private state: LockstepState<LocalInput, GlobalInput, State>;
public constructor( public constructor(
public readonly engine: LockstepProcessor<Input, State>, public readonly engine: LockstepProcessor<LocalInput, GlobalInput, State>,
) { ) {
const initialState = this.initState({}); const initialState = this.initState({});
this.state = new LockstepState(initialState, engine); this.state = new LockstepState(initialState, engine);
@ -65,13 +65,13 @@ export abstract class LockstepClient<Input, State> {
public abstract initState(init: Partial<State>): State; public abstract initState(init: Partial<State>): State;
public abstract gatherInput(): Input; public abstract gatherInput(): LocalInput;
/** /**
* Connect to a [perhaps emulated] server and return a disconnect callback * Connect to a [perhaps emulated] server and return a disconnect callback
*/ */
public connect(server: Server<Input, State>): () => void { public connect(server: Server<LocalInput, GlobalInput, State>): () => void {
let serverTalkback: Server<Input, State> | null = null; let serverTalkback: Server<LocalInput, GlobalInput, State> | null = null;
const sampleInput = () => { const sampleInput = () => {
if (serverTalkback) { if (serverTalkback) {
@ -85,15 +85,15 @@ export abstract class LockstepClient<Input, State> {
}; };
// connect to server // connect to server
server(0, (mode: number, data: Server<Input, State> | ServerMessage<Input, State>) => { server(0, (mode: number, data: Server<LocalInput, GlobalInput, State> | ServerMessage<GlobalInput, State>) => {
if (mode == 0) { if (mode == 0) {
serverTalkback = data as Server<Input, State>; serverTalkback = data as Server<LocalInput, GlobalInput, State>;
// kickoff input sender // kickoff input sender
setTimeout(sampleInput, INPUT_FREQUENCY); setTimeout(sampleInput, INPUT_FREQUENCY);
} else if (mode == 1) { } else if (mode == 1) {
// server message // server message
const message = data as ServerMessage<Input, State>; const message = data as ServerMessage<GlobalInput, State>;
switch (message.t) { switch (message.t) {
case MessageTypes.SET_STATE: case MessageTypes.SET_STATE:
const resetState = this.initState(message.s); const resetState = this.initState(message.s);

View file

@ -2,17 +2,18 @@ import { Callbag } from "callbag";
import { ClientMessage, MessageTypes, ServerMessage } from "./LockstepClient"; import { ClientMessage, MessageTypes, ServerMessage } from "./LockstepClient";
type Client<Input, State> = Callbag<ServerMessage<Input, State>, ClientMessage<Input, State>>; type Client<LocalInput, State> = Callbag<ServerMessage<LocalInput[], State>, ClientMessage<LocalInput, State>>;
export function Loopback<Input, State>(start: number, data?: Client<Input, State> | ClientMessage<Input, State>) { /** Stub loopback server that handles a single client, for schemes where GlobalInput = LocalInput[] */
export function Loopback<LocalInput, State>(start: number, data?: Client<LocalInput, State> | ClientMessage<LocalInput, State>) {
if(start != 0) return; if(start != 0) return;
const sink = data as Client<Input, State>; const sink = data as Client<LocalInput, State>;
sink(0, (type: number, data?: Client<Input, State> | ClientMessage<Input, State>) => { sink(0, (type: number, data?: Client<LocalInput, State> | ClientMessage<LocalInput, State>) => {
if(type == 1) { if(type == 1) {
// message from client; just reflect for now // message from client; just reflect for now
const message = data as ClientMessage<Input, State>; const message = data as ClientMessage<LocalInput, State>;
switch(message.t) { switch(message.t) {
case MessageTypes.INPUT: case MessageTypes.INPUT:
sink(1, { sink(1, {