Skip to content

Commit 7163fa8

Browse files
committed
feat: client reconnection
1 parent 862408c commit 7163fa8

File tree

11 files changed

+416
-240
lines changed

11 files changed

+416
-240
lines changed

client/config.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -533,3 +533,7 @@ const FLAGS = {
533533
isCachable: 1 << 5,
534534
showsAboveParent: 1 << 6
535535
};
536+
537+
// Keep in sync with index.ts
538+
const RECONNECTION_KEY_LENGTH = 16;
539+
const RECONNECTION_KEY_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";

client/loader.js

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -618,7 +618,21 @@ Module.todo.push([() => {
618618
// executes spawn command
619619
spawn: name => window.input.execute(`game_spawn ${name}`),
620620
// executes reconnect command
621-
reconnect: () => window.input.execute(`lb_reconnect`)
621+
reconnect: () => window.input.execute(`lb_reconnect`),
622+
// gets or creates reconnection key
623+
getReconnectionKey: () => {
624+
let key = window.localStorage.getItem('reconnectionKey');
625+
if (!key) {
626+
key = "";
627+
for (let i = 0; i < RECONNECTION_KEY_LENGTH; i++) {
628+
key += RECONNECTION_KEY_ALPHABET[Math.floor(
629+
Math.random() * RECONNECTION_KEY_ALPHABET.length
630+
)];
631+
}
632+
window.localStorage.setItem('reconnectionKey', key);
633+
}
634+
return key;
635+
}
622636
};
623637

624638
// custom commands
@@ -1255,7 +1269,9 @@ class ASMConsts {
12551269

12561270
static createWebSocket(urlPtr) {
12571271
const url = Module.UTF8ToString(urlPtr);
1258-
const ws = new WebSocket(`ws${location.protocol.slice(4)}//${location.host}/${url.slice(5, url.length - 4)}`);
1272+
const reconnectionKey = window.Game.getReconnectionKey();
1273+
const protocol = reconnectionKey ? `reconkey#${reconnectionKey}` : undefined;
1274+
const ws = new WebSocket(`ws${location.protocol.slice(4)}//${location.host}/${url.slice(5, url.length - 4)}`, protocol);
12591275
ws.binaryType = "arraybuffer";
12601276
ws.events = [];
12611277
ws.onopen = function() {

src/Client.ts

Lines changed: 50 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@ import { WebSocket } from "uWebSockets.js";
2323
import Reader from "./Coder/Reader";
2424
import Writer from "./Coder/Writer";
2525
import GameServer from "./Game";
26-
import ClientCamera from "./Native/Camera";
27-
import { ArenaState } from "./Native/Arena";
26+
import { CameraEntity } from "./Native/Camera";
27+
2828
import ObjectEntity from "./Entity/Object";
2929
import TankDefinitions, { getTankById, TankCount } from "./Const/TankDefinitions";
3030
import DevTankDefinitions, { DevTank } from "./Const/DevTankDefinitions";
@@ -80,6 +80,7 @@ export interface ClientWrapper {
8080
client: Client | null;
8181
ipAddress: string;
8282
gamemode: string;
83+
reconnectionKey: string | null;
8384
}
8485

8586
export default class Client {
@@ -102,7 +103,10 @@ export default class Client {
102103
/** Inner websocket connection. */
103104
public ws: WebSocket<ClientWrapper> | null;
104105
/** Client's camera entity. */
105-
public camera: ClientCamera | null = null;
106+
public camera: CameraEntity | null = null;
107+
108+
/** Client's reconnection key for reclaiming cameras. */
109+
public reconnectionKey: string | null = null;
106110

107111
/** Whether or not the player has used in game dev cheats before (such as level up or godmode). */
108112
private devCheatsUsed: boolean = false;
@@ -119,14 +123,48 @@ export default class Client {
119123
this.game.clients.add(this);
120124
this.ws = ws;
121125
this.lastPingTick = this.connectTick = game.tick;
126+
this.reconnectionKey = ws.getUserData().reconnectionKey;
122127
}
123128

124129
/** Accepts the client and creates a camera for it. */
125130
public acceptClient() {
126131
this.write().u8(ClientBound.ServerInfo).stringNT(this.game.gamemode).stringNT(config.host).send();
127132
this.write().u8(ClientBound.PlayerCount).vu(GameServer.globalPlayerCount).send();
128133
this.write().u8(ClientBound.Accept).vi(this.accessLevel).send();
129-
this.camera = new ClientCamera(this.game, this);
134+
135+
// Check for reconnection
136+
const didRestoreCamera = this.tryReconnection();
137+
if (!didRestoreCamera) {
138+
// If reconnection failed, create a new camera
139+
this.camera = new CameraEntity(this.game);
140+
this.camera.setClient(this);
141+
}
142+
}
143+
144+
private tryReconnection(): boolean {
145+
if (!config.enableReconnection) return false;
146+
const reconnectionKey = this.reconnectionKey;
147+
if (!reconnectionKey) return false;
148+
149+
for (const id of this.game.entities.cameras) {
150+
const camera = this.game.entities.inner[id] as CameraEntity;
151+
if (!camera) continue;
152+
153+
if (camera.reconnectionKey === reconnectionKey) {
154+
if (camera.getClient()) {
155+
// Already claimed, so our reconnection key itself is a duplicate
156+
this.reconnectionKey = null;
157+
return false;
158+
}
159+
160+
camera.setClient(this);
161+
this.camera = camera;
162+
163+
return true;
164+
}
165+
}
166+
167+
return false;
130168
}
131169

132170
/** Sends data to client. */
@@ -154,8 +192,14 @@ export default class Client {
154192

155193
this.inputs.deleted = true;
156194
this.inputs.movement.magnitude = 0;
157-
158-
if (Entity.exists(this.camera)) this.camera.delete();
195+
if (Entity.exists(this.camera)) {
196+
if (config.enableReconnection && this.reconnectionKey) {
197+
// Mark camera for reconnection instead of deleting
198+
this.camera.markForReconnection();
199+
} else {
200+
this.camera.delete();
201+
}
202+
}
159203
}
160204

161205
/** Handles to incoming messages. */

src/Entity/Misc/Dominator.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818

1919
import { Color, ColorsHexCode, NameFlags, StyleFlags, Tank, ClientBound } from "../../Const/Enums";
2020
import ArenaEntity from "../../Native/Arena";
21-
import ClientCamera, { CameraEntity } from "../../Native/Camera";
21+
import { CameraEntity } from "../../Native/Camera";
2222
import { AI, AIState, Inputs } from "../AI";
2323
import LivingEntity from "../Live";
2424
import Bullet from "../Tank/Projectile/Bullet";

src/Entity/Tank/TankBody.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -280,26 +280,28 @@ export default class TankBody extends LivingEntity implements BarrelBase {
280280
if (client && client.accessLevel < AccessLevel.FullAccess) this.setInvulnerability(false);
281281
}
282282
}
283-
if (!this.deletionAnimation && !this.inputs.deleted) this.physicsData.size = this.baseSize * this.cameraEntity.sizeFactor;
284-
else this.regenPerTick = 0;
283+
if (!this.deletionAnimation) {
284+
this.physicsData.size = this.baseSize * this.cameraEntity.sizeFactor;
285+
}
286+
287+
if (this.deletionAnimation) this.regenPerTick = 0;
285288

286289
super.tick(tick);
287290

288291
// If we're currently in a deletion animation
289292
if (this.deletionAnimation) return;
290293

291-
if (this.inputs.deleted) {
292-
if (this.cameraEntity.cameraData.values.level <= 5) return this.destroy();
294+
if (!Entity.exists(this.cameraEntity)) {
293295
this.lastDamageTick = tick;
294296
this.healthData.health -= 2 + this.healthData.values.maxHealth / 500;
297+
this.regenPerTick = 0;
295298

296299
if (this.isInvulnerable) this.setInvulnerability(false);
297300
if (this.styleData.values.flags & StyleFlags.isFlashing) {
298301
this.styleData.flags ^= StyleFlags.isFlashing;
299302
this.damageReduction = 1.0;
300303
}
301304
return;
302-
// return this.destroy();
303305
}
304306

305307
if (this.definition.flags.zoomAbility && (this.inputs.flags & InputFlags.rightclick)) {

0 commit comments

Comments
 (0)