Improve networking
This commit is contained in:
@@ -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 },
|
||||
|
||||
@@ -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 }[];
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user