Skip to content

Commit

Permalink
feat: latency calculation and handshake protocol
Browse files Browse the repository at this point in the history
  • Loading branch information
Veradictus committed Jan 14, 2024
1 parent 0b2e455 commit 4bdbd6d
Show file tree
Hide file tree
Showing 17 changed files with 182 additions and 61 deletions.
4 changes: 3 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"editor.formatOnPaste": true,
"editor.codeActionsOnSave": { "source.fixAll": true },
"editor.codeActionsOnSave": {
"source.fixAll": "explicit"
},
"npm.packageManager": "yarn",
"prettier.trailingComma": "none",
"typescript.tsdk": "./node_modules/typescript/lib",
Expand Down
2 changes: 1 addition & 1 deletion packages/client/src/entity/character/player/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ export default class Handler extends CharacterHandler {
playerY: this.character.gridY,
nextGridX: this.character.nextGridX,
nextGridY: this.character.nextGridY,
timestamp: Date.now()
timestamp: ~~(performance.now() - this.game.timeOffset)
});

// Update the last step coordinates.
Expand Down
1 change: 1 addition & 0 deletions packages/client/src/game.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ export default class Game {
public timeDiff = Date.now(); // Used for FPS calculation.
public timeLast = Date.now();
public targetFPS = 1000 / 50;
public timeOffset = 0; // Offset relative to the server time.

public started = false;
public ready = false;
Expand Down
36 changes: 32 additions & 4 deletions packages/client/src/network/connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ import type { TradePacketValues } from '@kaetram/common/network/impl/trade';
import type { EquipmentPacketValues } from '@kaetram/common/network/impl/equipment';
import type Resource from '../entity/objects/resource/resource';
import type { ResourcePacketData } from '@kaetram/common/network/impl/resource';
import type { NetworkPacketData } from '@kaetram/common/network/impl/network';

export default class Connection {
/**
Expand Down Expand Up @@ -125,6 +126,7 @@ export default class Connection {
this.lastEntityListRequest = Date.now();
});

this.messages.onConnected(this.handleConnected.bind(this));
this.messages.onHandshake(this.handleHandshake.bind(this));
this.messages.onWelcome(this.handleWelcome.bind(this));
this.messages.onMap(this.handleMap.bind(this));
Expand Down Expand Up @@ -176,6 +178,18 @@ export default class Connection {
this.messages.onResource(this.handleResource.bind(this));
}

/**
* Received signal from the server that we have fully connected, we now begin
* sending the handshake packet to the server.
*/

private handleConnected(): void {
// Send the handshake with the game version.
this.socket.send(Packets.Handshake, {
gVer: this.app.config.version
});
}

/**
* Handles the handshake packet from the server. The handshake signals
* to the client that the connection is now established and the client
Expand All @@ -191,6 +205,9 @@ export default class Connection {

this.app.updateLoader('Connecting to server');

// Calculate the offset of timing relative to the server.
this.game.timeOffset = ~~performance.now() - data.serverTime!;

// Set the server id and instance
this.game.player.instance = data.instance!;
this.game.player.serverId = data.serverId!;
Expand Down Expand Up @@ -656,14 +673,25 @@ export default class Connection {
}
}

/**
/*
* Handler for the network packet. These are debugging methods such
* as latency tests that may be implemented in the future.
*/

private handleNetwork(): void {
// Send a resposne to the ping back.
this.socket.send(Packets.Network, [Opcodes.Network.Pong]);
private handleNetwork(opcode: Opcodes.Network, info?: NetworkPacketData): void {
switch (opcode) {
case Opcodes.Network.Ping: {
return this.socket.send(Packets.Network, [Opcodes.Network.Pong]);
}

case Opcodes.Network.Sync: {
if (!info?.timestamp) return;

// Calculate the offset of timing relative to the server.
this.game.timeOffset = ~~performance.now() - info.timestamp!;
return;
}
}
}

/**
Expand Down
22 changes: 17 additions & 5 deletions packages/client/src/network/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,12 @@ import type {
UpdatePacketCallback,
WelcomePacketCallback
} from '@kaetram/common/types/messages/outgoing';
import type { ConnectedPacketCallback } from '@kaetram/common/network/impl/connected';

export default class Messages {
private messages: (() => ((...data: never[]) => void) | undefined)[] = [];

private connectedCallback?: ConnectedPacketCallback;
private handshakeCallback?: HandshakePacketCallback;
private welcomeCallback?: WelcomePacketCallback;
private mapCallback?: MapPacketCallback;
Expand Down Expand Up @@ -117,6 +119,7 @@ export default class Messages {
* accordingly.
*/
public constructor(private app: App) {
this.messages[Packets.Connected] = () => this.connectedCallback;
this.messages[Packets.Handshake] = () => this.handshakeCallback;
this.messages[Packets.Welcome] = () => this.welcomeCallback;
this.messages[Packets.Spawn] = () => this.spawnCallback;
Expand Down Expand Up @@ -192,15 +195,15 @@ export default class Messages {
}

/**
* UTF8 messages handler. These are simple messages that are pure
* strings. These errors are displayed on the login page.
* @param message UTF8 message received from the server.
* Handles the close event when the connection is closed. The reason passed determines
* what error we display to the user.
* @param reason UTF8 reason received from the server.
*/

public handleUTF8(message: string): void {
public handleCloseReason(reason: string): void {
this.app.toggleLogin(false);

switch (message) {
switch (reason) {
case 'worldfull': {
this.app.sendError('The servers are currently full!');
break;
Expand Down Expand Up @@ -253,6 +256,11 @@ export default class Messages {
break;
}

case 'swappedworlds': {
this.app.sendError('You have recently swapped worlds, please wait 15 seconds.');
break;
}

case 'loggedin': {
this.app.sendError('The player is already logged in!');
break;
Expand Down Expand Up @@ -314,6 +322,10 @@ export default class Messages {
* Packet callbacks.
*/

public onConnected(callback: ConnectedPacketCallback): void {
this.connectedCallback = callback;
}

public onHandshake(callback: HandshakePacketCallback): void {
this.handshakeCallback = callback;
}
Expand Down
35 changes: 21 additions & 14 deletions packages/client/src/network/socket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@ import Messages from './messages';

import log from '../lib/log';

import { Packets } from '@kaetram/common/network';

import type Game from '../game';
import type { Packets } from '@kaetram/common/network';
import type { SerializedServer } from '@kaetram/common/types/network';
import type { TradePacketOutgoing } from '@kaetram/common/network/impl/trade';

Expand Down Expand Up @@ -69,7 +68,12 @@ export default class Socket {
this.connection.addEventListener('error', () => this.handleConnectionError(host, port));

// Handler for when a disconnection occurs.
this.connection.addEventListener('close', () => this.game.handleDisconnection());
this.connection.addEventListener('close', (event: CloseEvent) => {
// Event code 1010 is our custom code when the server rejects us for a specific reason.
if (event.code === 1010 && event.reason) this.messages.handleCloseReason(event.reason);

this.game.handleDisconnection();
});

/**
* The audio controller can only be properly initialized when the player interacts
Expand All @@ -87,12 +91,20 @@ export default class Socket {
private receive(message: string): void {
if (!this.listening) return;

if (message.startsWith('[')) {
let data = JSON.parse(message);
/**
* Invalid message format, we skip. Previously we would handle UTF8
* messages separately here, but we now rely on the close event to
* signal to use the appropriate reason for closing.
*/

if (data.length > 1) this.messages.handleBulkData(data);
else this.messages.handleData(data.shift());
} else this.messages.handleUTF8(message);
if (!message.startsWith('[')) return;

// Parse the JSON string into an array.
let data = JSON.parse(message);

// Handle bulk data or single data.
if (data.length > 1) this.messages.handleBulkData(data);
else this.messages.handleData(data.shift());
}

/**
Expand All @@ -101,7 +113,7 @@ export default class Socket {
* @param data Packet data in an array format.
*/

public send<const P extends Packets>(packet: P, data?: OutgoingPackets[P & number]): void {
public send(packet: Packets, data?: unknown): void {
// Ensure the connection is open before sending.
if (this.connection?.readyState !== WebSocket.OPEN) return;

Expand All @@ -118,11 +130,6 @@ export default class Socket {
log.info('Connection established...');

this.game.app.updateLoader('Preparing handshake');

// Send the handshake with the game version.
this.send(Packets.Handshake, {
gVer: this.config.version
});
}

/**
Expand Down
11 changes: 11 additions & 0 deletions packages/common/network/impl/connected.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import Packet from '../packet';

import { Packets } from '@kaetram/common/network';

export type ConnectedPacketCallback = () => void;

export default class ConnectedPacket extends Packet {
public constructor() {
super(Packets.Connected);
}
}
1 change: 1 addition & 0 deletions packages/common/network/impl/handshake.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export interface ClientHandshakePacketData {
type: 'client';
instance?: string; // Player's instance.
serverId?: number;
serverTime?: number;
}

export interface HubHandshakePacketData {
Expand Down
1 change: 1 addition & 0 deletions packages/common/network/impl/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { default as ConnectedPacket } from './connected';
export { default as HandshakePacket } from './handshake';
export { default as WelcomePacket } from './welcome';
export { default as MapPacket } from './map';
Expand Down
10 changes: 7 additions & 3 deletions packages/common/network/impl/network.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,14 @@ import { Packets } from '@kaetram/common/network';

import type { Opcodes } from '@kaetram/common/network';

export type NetworkPacketCallback = (opcode?: Opcodes.Network) => void;
export interface NetworkPacketData {
timestamp?: number;
}

export type NetworkPacketCallback = (opcode: Opcodes.Network, data?: NetworkPacketData) => void;

export default class NetworkPacket extends Packet {
public constructor(opcode: Opcodes.Network) {
super(Packets.Network, opcode);
public constructor(opcode: Opcodes.Network, data?: NetworkPacketData) {
super(Packets.Network, opcode, data);
}
}
3 changes: 2 additions & 1 deletion packages/common/network/opcodes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@ export enum Projectile {

export enum Network {
Ping,
Pong
Pong,
Sync
}

export enum Container {
Expand Down
1 change: 1 addition & 0 deletions packages/common/network/packets.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
enum Packets {
Connected,
Handshake,
Login,
Welcome,
Expand Down
2 changes: 0 additions & 2 deletions packages/server/src/game/entity/character/effect/status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,6 @@ export default class {
*/

public load(effects: SerializedEffects): void {
console.log(effects);

for (let type in effects) {
let effect = effects[type];

Expand Down
33 changes: 30 additions & 3 deletions packages/server/src/game/entity/character/player/incoming.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import log from '@kaetram/common/util/log';
import Utils from '@kaetram/common/util/utils';
import Filter from '@kaetram/common/util/filter';
import Creator from '@kaetram/common/database/mongodb/creator';
import { SpawnPacket } from '@kaetram/common/network/impl';
import { SpawnPacket, HandshakePacket as Handshake } from '@kaetram/common/network/impl';
import { Opcodes, Packets } from '@kaetram/common/network';

import type Player from './player';
Expand All @@ -29,8 +29,8 @@ import type {
StorePacket,
WarpPacket,
FriendsPacket,
TradePacket,
HandshakePacket,
TradePacket,
EnchantPacket,
GuildPacket,
CraftingPacket,
Expand All @@ -46,6 +46,8 @@ export default class Incoming {
private database: MongoDB;
private commands: Commands;

private completedHandshake = false;

public constructor(private player: Player) {
this.connection = player.connection;
this.world = player.world;
Expand Down Expand Up @@ -158,7 +160,25 @@ export default class Incoming {
*/

private handleHandshake(data: HandshakePacket): void {
if (data.gVer !== config.gver) this.connection.reject('updated');
// Reject if the client is not on the right version.
if (data.gVer !== config.gver) return this.connection.reject('updated');

// Used to prevent the client from sending any packets before the handshake is complete.
this.completedHandshake = true;

/**
* Immediately send the handshake packet and bypass the queue so that the client
* can obtain the most accurace server time.
*/

this.player.connection.send([
new Handshake({
type: 'client',
instance: this.player.instance,
serverId: config.serverId,
serverTime: ~~performance.now()
}).serialize()
]);
}

/**
Expand All @@ -169,6 +189,9 @@ export default class Incoming {
*/

private handleLogin(data: LoginPacket): void {
// Ensure the handshake has been completed before proceeding.
if (!this.completedHandshake) return this.connection.reject('lost');

let { opcode, username, password, email } = data;

if (username) {
Expand Down Expand Up @@ -424,10 +447,14 @@ export default class Incoming {

switch (opcode) {
case Opcodes.Network.Pong: {
if (!this.player.requestedPing) return;

let time = Date.now();

this.player.notify(`Latency of ${time - this.player.pingTime}ms`, 'red');

this.player.requestedPing = false;

break;
}
}
Expand Down
Loading

0 comments on commit 4bdbd6d

Please sign in to comment.