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 },
+2 -1
View File
@@ -1,4 +1,4 @@
import type { ModData, ModListing } from "$/common/mod_loader.ts";
import type { AtlasListing, ModData, ModListing } from "$/common/mod_loader.ts";
// messages between server/main.ts (the host) and the game server worker
@@ -7,6 +7,7 @@ export type HostToGame =
type: "init";
save: string | undefined;
default_seed: string;
atlas: AtlasListing;
// in load order, with the scripts' code since the worker can't read files
mods: { listing: ModListing; data: ModData; server_code?: string; worldgen_code?: string }[];
}
+3 -1
View File
@@ -1,7 +1,7 @@
// starts a game server with its mods: registers their data, then imports worldgen and server scripts.
// the worker loads built mods, tests can load mods straight from their source folders
import { register_mod_data } from "$/common/mod_loader.ts";
import type { ModData, ModListing } from "$/common/mod_loader.ts";
import type { AtlasListing, ModData, ModListing } from "$/common/mod_loader.ts";
import { load_worldgen } from "$/common/worldgen_loader.ts";
import { add_base_item_hooks } from "./blocks.ts";
import { GameHost, GameServer } from "./game_server.ts";
@@ -19,6 +19,7 @@ export async function start_game(
host: GameHost,
save: string | undefined,
default_seed: string,
atlas: AtlasListing,
mods: ServerModSource[],
): Promise<GameServer> {
const recipes = register_mod_data(mods.map((mod) => ({ id: mod.listing.id, data: mod.data })));
@@ -32,6 +33,7 @@ export async function start_game(
const runtime = new ModRuntime();
const game = new GameServer(host, save, default_seed, {
listings: mods.map((mod) => mod.listing),
atlas,
recipes,
worldgen,
runtime,
+1
View File
@@ -54,6 +54,7 @@ async function init(message: Extract<HostToGame, { type: "init" }>) {
},
message.save,
message.default_seed,
message.atlas,
message.mods.map((mod) => ({
listing: mod.listing,
data: mod.data,