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
1 changed files with 15073 additions and 4754 deletions
167
test/backpressure.test.ts
Normal file
167
test/backpressure.test.ts
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { createRequire } from 'node:module';
|
||||
|
||||
import { PhysicalContainer } from '../backend/src/physics/physical-container';
|
||||
import { PlayerContainer } from '../backend/src/players/player-container';
|
||||
import { createWorld } from '../backend/src/create-world';
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
const shared = require('../shared/lib/main.js');
|
||||
const {
|
||||
Random,
|
||||
settings,
|
||||
TransportEvents,
|
||||
deserialize,
|
||||
CreateObjectsCommand,
|
||||
DeleteObjectsCommand,
|
||||
PropertyUpdatesForObjects,
|
||||
PropertyUpdatesForObject,
|
||||
UpdatePropertyCommand,
|
||||
InputAcknowledgement,
|
||||
UpdateMinimap,
|
||||
ServerAnnouncement,
|
||||
} = shared;
|
||||
|
||||
class FakeSocket {
|
||||
public readonly sent: Array<{ event: string; payload: unknown }> = [];
|
||||
public readonly conn = {
|
||||
transport: { socket: { bufferedAmount: 0 } },
|
||||
writeBuffer: [] as Array<{ data: unknown }>,
|
||||
};
|
||||
|
||||
public set bufferedAmount(bytes: number) {
|
||||
this.conn.transport.socket.bufferedAmount = bytes;
|
||||
}
|
||||
|
||||
public on() {}
|
||||
public off() {}
|
||||
public emit(event: string, payload?: unknown) {
|
||||
this.sent.push({ event, payload });
|
||||
}
|
||||
|
||||
public get lastBatch(): Array<{ constructor: { name: string } }> {
|
||||
const last = [...this.sent]
|
||||
.reverse()
|
||||
.find((m) => m.event === TransportEvents.ServerToPlayer);
|
||||
return last ? deserialize(last.payload as string) : [];
|
||||
}
|
||||
}
|
||||
|
||||
const makePlayer = () => {
|
||||
Random.seed = 3;
|
||||
const objects = new PhysicalContainer({ addPoints() {}, announce() {} });
|
||||
createWorld(objects);
|
||||
const players = new PlayerContainer(objects, 4, 0);
|
||||
const socket = new FakeSocket();
|
||||
const player = players.createPlayer({ name: 'player' }, socket as never);
|
||||
player.sendQueuedCommandsToClient();
|
||||
socket.sent.length = 0;
|
||||
return { player, socket };
|
||||
};
|
||||
|
||||
const queueOneSnapshot = (player: ReturnType<typeof makePlayer>['player']) => {
|
||||
player.queueCommandSend(new CreateObjectsCommand([]));
|
||||
player.queueCommandSend(new DeleteObjectsCommand([1234]));
|
||||
player.queueCommandSend(new ServerAnnouncement('team blue wins'));
|
||||
player.queueCommandSend(new UpdateMinimap([]));
|
||||
player.queueCommandSend(
|
||||
new PropertyUpdatesForObjects(
|
||||
[
|
||||
new PropertyUpdatesForObject(1234, [
|
||||
new UpdatePropertyCommand('ownership', 1, 0),
|
||||
]),
|
||||
],
|
||||
1,
|
||||
),
|
||||
);
|
||||
player.queueCommandSend(
|
||||
new InputAcknowledgement(123, player.character!.movementSnapshot, 0, 0),
|
||||
);
|
||||
};
|
||||
|
||||
const typesIn = (batch: Array<{ constructor: { name: string } }>) =>
|
||||
batch.map((c) => c.constructor.name);
|
||||
|
||||
describe('slow-client backpressure', () => {
|
||||
beforeEach(() => {
|
||||
Random.seed = 3;
|
||||
});
|
||||
|
||||
it('sends everything while the socket is draining', () => {
|
||||
const { player, socket } = makePlayer();
|
||||
queueOneSnapshot(player);
|
||||
player.sendQueuedCommandsToClient();
|
||||
|
||||
expect(typesIn(socket.lastBatch)).toEqual([
|
||||
CreateObjectsCommand.name,
|
||||
DeleteObjectsCommand.name,
|
||||
ServerAnnouncement.name,
|
||||
UpdateMinimap.name,
|
||||
PropertyUpdatesForObjects.name,
|
||||
InputAcknowledgement.name,
|
||||
]);
|
||||
});
|
||||
|
||||
it('reads the buffered byte count engine.io actually exposes', () => {
|
||||
const { player, socket } = makePlayer();
|
||||
socket.bufferedAmount = settings.maxBufferedBytesPerClient + 1;
|
||||
queueOneSnapshot(player);
|
||||
player.sendQueuedCommandsToClient();
|
||||
|
||||
const marker = socket.lastBatch.find((c) => c instanceof PropertyUpdatesForObjects);
|
||||
expect(marker).toMatchObject({ updates: [], timestamp: 1 });
|
||||
});
|
||||
|
||||
it('sheds only the state the next tick regenerates', () => {
|
||||
const { player, socket } = makePlayer();
|
||||
socket.bufferedAmount = settings.maxBufferedBytesPerClient + 1;
|
||||
queueOneSnapshot(player);
|
||||
player.sendQueuedCommandsToClient();
|
||||
|
||||
const types = typesIn(socket.lastBatch);
|
||||
expect(types).toContain(PropertyUpdatesForObjects.name);
|
||||
expect(types).not.toContain(InputAcknowledgement.name);
|
||||
expect(types).not.toContain(UpdateMinimap.name);
|
||||
expect(types).toContain(CreateObjectsCommand.name);
|
||||
expect(types).toContain(DeleteObjectsCommand.name);
|
||||
expect(types).toContain(ServerAnnouncement.name);
|
||||
});
|
||||
|
||||
it('stays quiet when a backed-up client has nothing but sheddable state', () => {
|
||||
const { player, socket } = makePlayer();
|
||||
socket.bufferedAmount = settings.maxBufferedBytesPerClient + 1;
|
||||
player.queueCommandSend(new PropertyUpdatesForObjects([], 1));
|
||||
const before = socket.sent.length;
|
||||
player.sendQueuedCommandsToClient();
|
||||
|
||||
expect(socket.sent.length).toBe(before);
|
||||
});
|
||||
|
||||
it('also measures packets waiting inside Engine.IO for the transport to drain', () => {
|
||||
const { player, socket } = makePlayer();
|
||||
socket.conn.writeBuffer.push({
|
||||
data: Buffer.alloc(settings.maxBufferedBytesPerClient + 1),
|
||||
});
|
||||
queueOneSnapshot(player);
|
||||
player.sendQueuedCommandsToClient();
|
||||
|
||||
const marker = socket.lastBatch.find((c) => c instanceof PropertyUpdatesForObjects);
|
||||
expect(marker).toMatchObject({ updates: [], timestamp: 1 });
|
||||
expect(typesIn(socket.lastBatch)).not.toContain(InputAcknowledgement.name);
|
||||
});
|
||||
|
||||
it('sends a fresh pose and acknowledgement after the transport drains', () => {
|
||||
const { player, socket } = makePlayer();
|
||||
socket.bufferedAmount = settings.maxBufferedBytesPerClient + 1;
|
||||
queueOneSnapshot(player);
|
||||
player.sendQueuedCommandsToClient();
|
||||
|
||||
socket.bufferedAmount = 0;
|
||||
queueOneSnapshot(player);
|
||||
player.sendQueuedCommandsToClient();
|
||||
|
||||
const snapshot = socket.lastBatch.find((c) => c instanceof PropertyUpdatesForObjects);
|
||||
expect(snapshot).toMatchObject({ updates: [expect.anything()], timestamp: 1 });
|
||||
expect(typesIn(socket.lastBatch)).toContain(InputAcknowledgement.name);
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue