2020-02-15 19:52:00 -05:00
|
|
|
|
|
|
|
import { Callbag } from "callbag";
|
2020-02-15 21:10:19 -05:00
|
|
|
import animationFrames from "callbag-animation-frames";
|
|
|
|
import map from "callbag-map";
|
2020-04-03 18:35:23 -04:00
|
|
|
import pipe from "callbag-pipe";
|
2020-02-15 19:52:00 -05:00
|
|
|
|
2020-05-10 18:47:03 -04:00
|
|
|
import { INPUT_FREQUENCY, LockstepProcessor, LockstepState } from "../ecs/Lockstep";
|
2020-02-15 19:52:00 -05:00
|
|
|
|
|
|
|
export const enum MessageTypes {
|
2020-04-04 22:17:50 -04:00
|
|
|
/**
|
|
|
|
* Reserved, likely for transient messages that wouldn't make sense appearing in a replay
|
|
|
|
*/
|
2020-05-12 23:00:06 -04:00
|
|
|
META = "m",
|
2020-04-04 22:17:50 -04:00
|
|
|
/**
|
|
|
|
* From server: initialize client state / load new level, plus informing client of their "controller number"
|
|
|
|
* From client: (op only) force a new game state, likely loading a level
|
|
|
|
*/
|
2020-05-12 23:00:06 -04:00
|
|
|
SET_STATE = "s",
|
2020-04-04 22:17:50 -04:00
|
|
|
/**
|
|
|
|
* From server: array of canonical inputs for a frame
|
|
|
|
* From client: input for their controller for the next frame (TBD: strategy for handling late inputs)
|
|
|
|
*/
|
2020-05-12 23:00:06 -04:00
|
|
|
INPUT = "i",
|
2020-04-04 22:17:50 -04:00
|
|
|
/**
|
|
|
|
* From server: issue request for the current game state, with a cookie to identify the response
|
|
|
|
* From client: serialized game state, with the cookie of the request
|
|
|
|
*/
|
2020-05-12 23:00:06 -04:00
|
|
|
GET_STATE = "g",
|
2020-02-15 19:52:00 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
export type Packet<TypeId, Payload> = { t: TypeId } & Payload;
|
|
|
|
|
2020-05-17 23:39:28 -04:00
|
|
|
export type Meta = {
|
|
|
|
helo?: string;
|
|
|
|
};
|
|
|
|
|
2020-05-01 22:51:21 -04:00
|
|
|
export type ClientMessage<LocalInput, State> =
|
2020-02-20 23:55:16 -05:00
|
|
|
| Packet<MessageTypes.SET_STATE, { s: Partial<State> }>
|
2020-05-01 22:51:21 -04:00
|
|
|
| Packet<MessageTypes.INPUT, { i: LocalInput }>
|
2020-04-04 22:17:50 -04:00
|
|
|
| Packet<MessageTypes.GET_STATE, { c: number, s: State }>
|
|
|
|
;
|
2020-02-15 19:52:00 -05:00
|
|
|
|
2020-05-01 22:51:21 -04:00
|
|
|
export type ServerMessage<GlobalInput, State> =
|
2020-05-17 23:39:28 -04:00
|
|
|
| Packet<MessageTypes.META, Meta>
|
2020-05-01 21:45:31 -04:00
|
|
|
| Packet<MessageTypes.SET_STATE, { u: number, s: Partial<State> }>
|
2020-05-01 22:51:21 -04:00
|
|
|
| Packet<MessageTypes.INPUT, { i: GlobalInput }>
|
2020-04-04 22:17:50 -04:00
|
|
|
| Packet<MessageTypes.GET_STATE, { c: number }>
|
|
|
|
;
|
2020-02-15 19:52:00 -05:00
|
|
|
|
2020-05-01 22:51:21 -04:00
|
|
|
export type Server<LocalInput, GlobalInput, State> = Callbag<ClientMessage<LocalInput, State>, ServerMessage<GlobalInput, State>>;
|
2020-02-15 19:52:00 -05:00
|
|
|
|
2020-05-01 22:51:21 -04:00
|
|
|
export abstract class LockstepClient<LocalInput, GlobalInput, State> {
|
2020-02-15 19:52:00 -05:00
|
|
|
|
2020-05-01 21:45:31 -04:00
|
|
|
private playerNumber = -1;
|
2020-05-01 22:51:21 -04:00
|
|
|
private state: LockstepState<LocalInput, GlobalInput, State>;
|
2020-05-09 19:33:19 -04:00
|
|
|
private serverTalkback: Server<LocalInput, GlobalInput, State> | null = null;
|
2020-02-15 19:52:00 -05:00
|
|
|
|
|
|
|
public constructor(
|
2020-05-01 22:51:21 -04:00
|
|
|
public readonly engine: LockstepProcessor<LocalInput, GlobalInput, State>,
|
2020-02-15 19:52:00 -05:00
|
|
|
) {
|
|
|
|
const initialState = this.initState({});
|
|
|
|
this.state = new LockstepState(initialState, engine);
|
|
|
|
}
|
|
|
|
|
|
|
|
public abstract initState(init: Partial<State>): State;
|
|
|
|
|
2020-05-01 22:51:21 -04:00
|
|
|
public abstract gatherInput(): LocalInput;
|
2020-02-15 19:52:00 -05:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Connect to a [perhaps emulated] server and return a disconnect callback
|
2020-05-09 19:33:19 -04:00
|
|
|
*
|
|
|
|
* Only call this once or things will break.
|
2020-02-15 19:52:00 -05:00
|
|
|
*/
|
2020-05-01 22:51:21 -04:00
|
|
|
public connect(server: Server<LocalInput, GlobalInput, State>): () => void {
|
2020-02-15 19:52:00 -05:00
|
|
|
// connect to server
|
2020-05-01 22:51:21 -04:00
|
|
|
server(0, (mode: number, data: Server<LocalInput, GlobalInput, State> | ServerMessage<GlobalInput, State>) => {
|
2020-02-15 19:52:00 -05:00
|
|
|
if (mode == 0) {
|
2020-05-09 19:33:19 -04:00
|
|
|
this.serverTalkback = data as Server<LocalInput, GlobalInput, State>;
|
2020-02-15 19:52:00 -05:00
|
|
|
|
|
|
|
// kickoff input sender
|
2020-05-09 19:33:19 -04:00
|
|
|
setTimeout(this.sampleInput, INPUT_FREQUENCY);
|
2020-02-15 19:52:00 -05:00
|
|
|
} else if (mode == 1) {
|
|
|
|
// server message
|
2020-05-01 22:51:21 -04:00
|
|
|
const message = data as ServerMessage<GlobalInput, State>;
|
2020-05-09 19:33:19 -04:00
|
|
|
this.processMessage(message);
|
2020-02-15 19:52:00 -05:00
|
|
|
} else if (mode == 2) {
|
|
|
|
// disconnected
|
|
|
|
console.log("Disconnected from server", data);
|
2020-05-09 19:33:19 -04:00
|
|
|
this.serverTalkback = null;
|
2020-02-15 19:52:00 -05:00
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
// disposal
|
|
|
|
return () => {
|
2020-05-09 19:33:19 -04:00
|
|
|
this.serverTalkback?.(2);
|
|
|
|
this.serverTalkback = null;
|
2020-02-15 19:52:00 -05:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2020-05-09 19:33:19 -04:00
|
|
|
private sampleInput = () => {
|
|
|
|
if (this.serverTalkback) {
|
|
|
|
const input = this.gatherInput();
|
|
|
|
if(this.playerNumber >= 0) {
|
|
|
|
this.state.addLocalInput(this.playerNumber, input);
|
|
|
|
this.serverTalkback(1, { t: MessageTypes.INPUT, i: input });
|
|
|
|
}
|
|
|
|
setTimeout(this.sampleInput, INPUT_FREQUENCY);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
private processMessage(message: ServerMessage<GlobalInput, State>) {
|
|
|
|
switch (message.t) {
|
2020-05-17 23:39:28 -04:00
|
|
|
case MessageTypes.META:
|
|
|
|
if(message.helo) {
|
|
|
|
console.log(`Connected to ${message.helo}`);
|
|
|
|
|
|
|
|
// Connection established, reset state for now
|
|
|
|
this.serverTalkback?.(1, {t: MessageTypes.SET_STATE, s: {}});
|
|
|
|
}
|
|
|
|
break;
|
2020-05-09 19:33:19 -04:00
|
|
|
case MessageTypes.SET_STATE:
|
|
|
|
const resetState = this.initState(message.s);
|
|
|
|
this.state = new LockstepState(resetState, this.engine);
|
|
|
|
this.playerNumber = message.u;
|
|
|
|
break;
|
|
|
|
case MessageTypes.INPUT:
|
|
|
|
this.state.addCanonInput(message.i);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-04-03 18:35:23 -04:00
|
|
|
public renderFrames = pipe(
|
|
|
|
animationFrames,
|
|
|
|
map(_ms => this.state.getStateToRender())
|
|
|
|
);
|
2020-02-15 21:10:19 -05:00
|
|
|
}
|