Improve netcode and simplify (#4)
- Simplify the logic wherever possible to remove overengineering and simplify the netcode - Actually improve the netcode - The new lines are mainly the package locks and tests Reviewed-on: https://home.schmelczer.dev/git/andras/decla-red/pulls/4
This commit is contained in:
parent
5d854440ec
commit
93a7fe01ab
60 changed files with 15073 additions and 4754 deletions
2196
shared/package-lock.json
generated
Normal file
2196
shared/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -1,3 +0,0 @@
|
|||
export type CommandExecutors = {
|
||||
[type: string]: (command: any) => unknown;
|
||||
};
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
import { CommandReceiver } from './command-receiver';
|
||||
import { Command } from './command';
|
||||
|
||||
export abstract class CommandGenerator {
|
||||
private subscribers: Array<CommandReceiver> = [];
|
||||
|
||||
public subscribe(subscriber: CommandReceiver): void {
|
||||
this.subscribers.push(subscriber);
|
||||
}
|
||||
|
||||
public clearSubscribers(): void {
|
||||
this.subscribers = [];
|
||||
}
|
||||
|
||||
protected sendCommandToSubscribers(command: Command): void {
|
||||
this.subscribers.forEach((s) => s.handleCommand(command));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,13 +1,16 @@
|
|||
import { CommandExecutors } from './command-executors';
|
||||
import { Command } from './command';
|
||||
|
||||
export type CommandExecutors = {
|
||||
[type: string]: (command: any) => unknown;
|
||||
};
|
||||
|
||||
export abstract class CommandReceiver {
|
||||
protected commandExecutors: CommandExecutors = {};
|
||||
|
||||
protected defaultCommandExecutor(_: Command) {}
|
||||
|
||||
public handleCommand(command: Command) {
|
||||
const commandType = command.type;
|
||||
const commandType = command.constructor.name;
|
||||
|
||||
if (Object.prototype.hasOwnProperty.call(this.commandExecutors, commandType)) {
|
||||
this.commandExecutors[commandType]!(command);
|
||||
|
|
|
|||
|
|
@ -1,9 +1 @@
|
|||
export abstract class Command {
|
||||
public static get type(): string {
|
||||
return (this as any).name;
|
||||
}
|
||||
|
||||
public get type(): string {
|
||||
return (this as any).constructor.name;
|
||||
}
|
||||
}
|
||||
export abstract class Command {}
|
||||
|
|
|
|||
13
shared/src/commands/types/actions/client-heartbeat.ts
Normal file
13
shared/src/commands/types/actions/client-heartbeat.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import { serializable } from '../../../serialization/serializable';
|
||||
import { Command } from '../../command';
|
||||
|
||||
@serializable
|
||||
export class ClientHeartbeatCommand extends Command {
|
||||
public constructor(public readonly clientTimeMs: number) {
|
||||
super();
|
||||
}
|
||||
|
||||
public toArray(): Array<any> {
|
||||
return [this.clientTimeMs];
|
||||
}
|
||||
}
|
||||
|
|
@ -1,15 +1,9 @@
|
|||
import { serializable } from '../../../serialization/serializable';
|
||||
import { Command } from '../../command';
|
||||
|
||||
// Sent client -> server when the player triggers a leap (Space / leap button).
|
||||
// The launch direction is derived server-side from the character's surface
|
||||
// normal and current movement input. clientTimeMs is the client's wall-clock at
|
||||
// the press, echoed back via InputAcknowledgement so the predictor knows which
|
||||
// leaps the server has already folded into the streamed launch momentum and
|
||||
// must not replay again.
|
||||
@serializable
|
||||
export class LeapActionCommand extends Command {
|
||||
public constructor(public readonly clientTimeMs: number = 0) {
|
||||
public constructor(public readonly clientTimeMs: number) {
|
||||
super();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,14 +4,9 @@ import { Command } from '../../command';
|
|||
|
||||
@serializable
|
||||
export class MoveActionCommand extends Command {
|
||||
// clientTimeMs is the client's wall-clock (integer ms, survives the
|
||||
// serializer's toFixed(3)) when the input was generated. The server echoes
|
||||
// the latest one back via InputAcknowledgement so the client knows how much
|
||||
// of its input timeline is already baked into a snapshot and can replay the
|
||||
// rest for prediction. Defaults to 0 for inputs the server itself synthesises.
|
||||
public constructor(
|
||||
public readonly direction: vec2,
|
||||
public readonly clientTimeMs: number = 0,
|
||||
public readonly clientTimeMs: number,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,12 +6,13 @@ import { Command } from '../../command';
|
|||
export class PrimaryActionCommand extends Command {
|
||||
public constructor(
|
||||
public readonly position: vec2,
|
||||
public readonly charge: number = 0,
|
||||
public readonly charge: number,
|
||||
public readonly clientTimeMs: number,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
public toArray(): Array<any> {
|
||||
return [this.position, this.charge];
|
||||
return [this.position, this.charge, this.clientTimeMs];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Id } from '../../communication/id';
|
||||
import { Id } from '../../communication/communication';
|
||||
import { serializable } from '../../serialization/serializable';
|
||||
import { Command } from '../command';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,18 +1,9 @@
|
|||
import { CharacterTeam } from '../../objects/types/character-base';
|
||||
import { serializable } from '../../serialization/serializable';
|
||||
import { Command } from '../command';
|
||||
|
||||
@serializable
|
||||
export class GameEndCommand extends Command {
|
||||
constructor(
|
||||
public readonly winningTeam: CharacterTeam,
|
||||
public readonly endCardLengthInSeconds: number,
|
||||
public readonly shouldReconnect: boolean,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
public toArray(): Array<any> {
|
||||
return [this.winningTeam, this.endCardLengthInSeconds, this.shouldReconnect];
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,10 +3,6 @@ import { Command } from '../command';
|
|||
|
||||
@serializable
|
||||
export class GameStartCommand extends Command {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
public toArray(): Array<any> {
|
||||
return [];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,28 +1,46 @@
|
|||
import { vec2 } from 'gl-matrix';
|
||||
import { Id } from '../../communication/communication';
|
||||
import { serializable } from '../../serialization/serializable';
|
||||
import { Command } from '../command';
|
||||
|
||||
// Sent server -> owning client only, alongside that client's own character
|
||||
// snapshot. Carries the clientTimeMs of the most recent movement input the
|
||||
// server had received when the snapshot was taken (so the client's predictor
|
||||
// can reset to the snapshot and replay just the inputs the server hasn't seen
|
||||
// yet) and the authoritative launch momentum (so the predictor reproduces a
|
||||
// leap/slingshot/recoil flight rather than only snapping to it). See
|
||||
// LocalCharacterPredictor.
|
||||
// `direction` is not recoverable from the pose: the body lags the posture it is sprung towards.
|
||||
@serializable
|
||||
export class CharacterMovementSnapshot {
|
||||
public constructor(
|
||||
public readonly direction: number,
|
||||
public readonly bodyVelocity: vec2,
|
||||
public readonly leftFootNormal: vec2,
|
||||
public readonly rightFootNormal: vec2,
|
||||
public readonly groundPlanetId: Id | null,
|
||||
public readonly secondsSinceOnSurface: number,
|
||||
public readonly leapCooldownRemaining: number = 0,
|
||||
) {}
|
||||
|
||||
public toArray(): Array<any> {
|
||||
return [
|
||||
this.direction,
|
||||
this.bodyVelocity,
|
||||
this.leftFootNormal,
|
||||
this.rightFootNormal,
|
||||
this.groundPlanetId,
|
||||
this.secondsSinceOnSurface,
|
||||
this.leapCooldownRemaining,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@serializable
|
||||
export class InputAcknowledgement extends Command {
|
||||
public constructor(
|
||||
public readonly clientTimeMs: number,
|
||||
public readonly bodyVelocity: vec2,
|
||||
// clientTimeMs of the most recent leap the server has received: any leap at
|
||||
// or before it is already reflected in bodyVelocity, so the predictor must
|
||||
// not replay it.
|
||||
public readonly movement: CharacterMovementSnapshot,
|
||||
public readonly lastLeapClientTimeMs: number,
|
||||
public readonly ackAgeMs: number = 0,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
public toArray(): Array<any> {
|
||||
return [this.clientTimeMs, this.bodyVelocity, this.lastLeapClientTimeMs];
|
||||
return [this.clientTimeMs, this.movement, this.lastLeapClientTimeMs, this.ackAgeMs];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,8 +6,6 @@ import { Command } from '../command';
|
|||
export class PropertyUpdatesForObjects extends Command {
|
||||
constructor(
|
||||
public readonly updates: Array<PropertyUpdatesForObject>,
|
||||
// Seconds on the server's monotonic clock at the moment this state was
|
||||
// captured. Only differences between timestamps are meaningful.
|
||||
public readonly timestamp: number,
|
||||
) {
|
||||
super();
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { Id, RemoteCall } from '../../main';
|
||||
import { Id } from '../../communication/communication';
|
||||
import { RemoteCall } from '../../objects/game-object';
|
||||
import { serializable } from '../../serialization/serializable';
|
||||
import { Command } from '../command';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { vec2 } from 'gl-matrix';
|
||||
import { Id } from '../../communication/id';
|
||||
import { Id } from '../../communication/communication';
|
||||
import { CharacterTeam } from '../../objects/types/character-base';
|
||||
import { serializable } from '../../serialization/serializable';
|
||||
import { Command } from '../command';
|
||||
|
|
|
|||
18
shared/src/communication/communication.ts
Normal file
18
shared/src/communication/communication.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
export type Id = number;
|
||||
|
||||
let currentId = 0;
|
||||
export const id = (): Id => currentId++;
|
||||
|
||||
export interface PlayerInformation {
|
||||
name: string;
|
||||
reconnectToken?: string;
|
||||
}
|
||||
|
||||
export interface ServerInformation {
|
||||
playerLimit: number;
|
||||
playerCount: number;
|
||||
serverName: string;
|
||||
gameStatePercent: number;
|
||||
}
|
||||
|
||||
export const serverInformationEndpoint = '/state';
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
export type Id = number | null;
|
||||
|
||||
let currentId = 0;
|
||||
|
||||
export const id = (): number => {
|
||||
return currentId++;
|
||||
};
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
export interface PlayerInformation {
|
||||
name: string;
|
||||
}
|
||||
|
|
@ -6,4 +6,13 @@ export enum TransportEvents {
|
|||
ServerInfoUpdate = 'ServerInfoUpdate',
|
||||
Ping = 'Ping',
|
||||
Pong = 'Pong',
|
||||
PlayerJoined = 'PlayerJoined',
|
||||
JoinRejected = 'JoinRejected',
|
||||
}
|
||||
|
||||
export enum JoinRejectionReason {
|
||||
ServerFull = 'ServerFull',
|
||||
RoundEnding = 'RoundEnding',
|
||||
AlreadyJoined = 'AlreadyJoined',
|
||||
InvalidRequest = 'InvalidRequest',
|
||||
}
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue