Improve networking

This commit is contained in:
2026-09-25 00:08:01 -03:00
parent 6458bc0440
commit 2a2ccae9ce
18 changed files with 600 additions and 162 deletions
+63 -7
View File
@@ -18,9 +18,10 @@ import {
CRAFTING_RESULT_SLOT,
MAX_CHAT_LENGTH,
MAX_NAME_LENGTH,
PROTOCOL_VERSION,
ServerMessage,
} from "$/common/protocol.ts";
import type { ModListing, RecipeBook } from "$/common/mod_loader.ts";
import type { AtlasListing, ModListing, RecipeBook } from "$/common/mod_loader.ts";
import type { WorldgenSetup } from "$/common/generation.ts";
import { BLOCK_BEHAVIORS } from "./blocks.ts";
import { ModRuntime, run_guarded } from "./mod_runtime.ts";
@@ -34,6 +35,8 @@ const EYE_HEIGHT = 1.69;
// tiles further than this many chunks from every player don't tick
const SIMULATION_DISTANCE = 6;
const MAX_GIVE = 64 * 36;
// how long a client gets between welcome and ready to download and load everything
const READY_TIMEOUT_TICKS = 60 * TICKS_PER_SECOND;
// everything the game server needs from whatever runs it
export interface GameHost {
@@ -63,6 +66,7 @@ export interface SavedWorld {
// the loaded mods, see load_mods.ts
export interface GameMods {
listings: ModListing[];
atlas: AtlasListing;
recipes: RecipeBook;
worldgen?: WorldgenSetup;
runtime: ModRuntime;
@@ -72,6 +76,8 @@ export class GameServer {
world: ServerWorld;
#host: GameHost;
#players = new Map<number, ServerPlayer>();
// connections that got welcome and are downloading mods, by conn
#pending = new Map<number, { name: unknown; since: number }>();
#saved_players: Record<string, SavedPlayer> = {};
#tick = 0;
#mods: GameMods;
@@ -108,6 +114,7 @@ export class GameServer {
on_connect(_conn: number) {}
on_disconnect(conn: number) {
this.#pending.delete(conn);
const player = this.#players.get(conn);
if (!player) {
return;
@@ -135,9 +142,7 @@ export class GameServer {
const player = this.#players.get(conn);
if (!player) {
if (message.type === "hello") {
this.#join(conn, message.name);
}
this.#handshake(conn, message);
return;
}
@@ -182,6 +187,13 @@ export class GameServer {
this.mods.tick();
for (const [conn, pending] of this.#pending) {
if (this.#tick - pending.since > READY_TIMEOUT_TICKS) {
this.#pending.delete(conn);
this.#reject(conn, "Took too long to load the server's mods");
}
}
for (const player of this.#players.values()) {
this.#sync(player);
}
@@ -308,6 +320,52 @@ export class GameServer {
// messages
// hello -> welcome, then ready -> join. see "Delivery to clients" in MODS.md
#handshake(conn: number, message: ClientMessage) {
if (message.type === "hello") {
if (this.#pending.has(conn)) {
return;
}
if (message.protocol !== PROTOCOL_VERSION) {
this.#reject(
conn,
`This server needs a game version with protocol ${PROTOCOL_VERSION}, yours has ${
message.protocol ?? "none"
}. Reload the page to update.`,
);
return;
}
this.#pending.set(conn, { name: message.name, since: this.#tick });
this.#host.send(
conn,
JSON.stringify(
{
type: "welcome",
protocol: PROTOCOL_VERSION,
seed: this.world.seed,
atlas: this.#mods.atlas,
mods: this.#mods.listings,
} satisfies ServerMessage,
),
);
return;
}
if (message.type === "ready") {
const pending = this.#pending.get(conn);
if (!pending) {
return;
}
this.#pending.delete(conn);
this.#join(conn, pending.name);
}
}
#reject(conn: number, reason: string) {
this.#host.send(conn, JSON.stringify({ type: "rejected", reason } satisfies ServerMessage));
this.#host.close(conn);
}
#join(conn: number, raw_name: unknown) {
const player = new ServerPlayer(conn, this.#unique_name(raw_name));
const saved = this.#saved_players[player.name];
@@ -316,11 +374,9 @@ export class GameServer {
}
this.#send(player, {
type: "welcome",
type: "join",
id: player.id,
name: player.name,
seed: this.world.seed,
mods: this.#mods.listings,
players: [...this.#players.values()].map((p) => p.info()),
changes: this.world.all_changes(),
spawn: { x: player.x, y: player.y, z: player.z, yaw: player.yaw, pitch: player.pitch },