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:
- 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

View file

@ -1,28 +1,28 @@
export const INPUT_FREQUENCY = 33; // roughly 30fps
export interface LockstepProcessor<Input, State> {
compareInput(a: Input[], b: Input[]): boolean;
predictInput(prev: Input[] | null, localPlayer: number, localInput: Input): Input[];
export interface LockstepProcessor<LocalInput, GlobalInput, State> {
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<Input, State> {
export class LockstepState<LocalInput, GlobalInput, State> {
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<Input, State>) {
constructor(private initialState: State, private engine: LockstepProcessor<LocalInput, GlobalInput, State>) {
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<Input, State> {
}
/** 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<Input, State> {
}
}
export class Playback<Input, State> {
export class Playback<LocalInput, GlobalInput, State> {
private frame = 0;
public constructor(
private state: State,
private inputLog: Input[][],
private engine: LockstepProcessor<Input, State>,
private inputLog: GlobalInput[],
private engine: LockstepProcessor<LocalInput, GlobalInput, 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) {
return new Data(old);
}

View file

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

View file

@ -35,29 +35,29 @@ export const enum MessageTypes {
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.INPUT, { i: Input }>
| Packet<MessageTypes.INPUT, { i: LocalInput }>
| Packet<MessageTypes.GET_STATE, { c: number, s: State }>
| 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.INPUT, { i: Input[] }>
| Packet<MessageTypes.INPUT, { i: GlobalInput }>
| Packet<MessageTypes.GET_STATE, { c: 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 state: LockstepState<Input, State>;
private state: LockstepState<LocalInput, GlobalInput, State>;
public constructor(
public readonly engine: LockstepProcessor<Input, State>,
public readonly engine: LockstepProcessor<LocalInput, GlobalInput, State>,
) {
const initialState = this.initState({});
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 gatherInput(): Input;
public abstract gatherInput(): LocalInput;
/**
* 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;
public connect(server: Server<LocalInput, GlobalInput, State>): () => void {
let serverTalkback: Server<LocalInput, GlobalInput, State> | null = null;
const sampleInput = () => {
if (serverTalkback) {
@ -85,15 +85,15 @@ export abstract class LockstepClient<Input, State> {
};
// 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) {
serverTalkback = data as Server<Input, State>;
serverTalkback = data as Server<LocalInput, GlobalInput, State>;
// kickoff input sender
setTimeout(sampleInput, INPUT_FREQUENCY);
} else if (mode == 1) {
// server message
const message = data as ServerMessage<Input, State>;
const message = data as ServerMessage<GlobalInput, State>;
switch (message.t) {
case MessageTypes.SET_STATE:
const resetState = this.initState(message.s);

View file

@ -2,17 +2,18 @@ import { Callbag } from "callbag";
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;
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) {
// message from client; just reflect for now
const message = data as ClientMessage<Input, State>;
const message = data as ClientMessage<LocalInput, State>;
switch(message.t) {
case MessageTypes.INPUT:
sink(1, {