Improve networking
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
import { assert, assertEquals } from "@std/assert";
|
||||
import { PROTOCOL_VERSION } from "$/common/protocol.ts";
|
||||
import { TICKS_PER_SECOND } from "$/common/constants.ts";
|
||||
import { test_game } from "./helpers.ts";
|
||||
|
||||
Deno.test("a different protocol version is rejected before anything else", async () => {
|
||||
const { game, take, send, closed } = await test_game("mods");
|
||||
game.on_connect(1);
|
||||
send(1, { type: "hello", name: "old", protocol: PROTOCOL_VERSION - 1 });
|
||||
const [message] = take(1);
|
||||
assertEquals(message.type, "rejected");
|
||||
assert(message.reason.includes(`protocol ${PROTOCOL_VERSION}`), message.reason);
|
||||
assert(closed.has(1));
|
||||
|
||||
game.on_connect(2);
|
||||
send(2, { type: "hello", name: "ancient" });
|
||||
assertEquals(take(2)[0].type, "rejected");
|
||||
});
|
||||
|
||||
Deno.test("welcome lists what to download, and nothing happens until ready", async () => {
|
||||
const { game, take, send, join } = await test_game("mods");
|
||||
join(1, "alice");
|
||||
take(1);
|
||||
|
||||
let joined = 0;
|
||||
game.mods.after.player_join.for_mod("test").subscribe(() => joined++);
|
||||
|
||||
game.on_connect(2);
|
||||
send(2, { type: "hello", name: "bob", protocol: PROTOCOL_VERSION });
|
||||
const [welcome] = take(2);
|
||||
assertEquals(welcome.type, "welcome");
|
||||
assertEquals(welcome.mods.map((m: { id: string }) => m.id), ["bworld"]);
|
||||
assert(welcome.atlas.png && welcome.atlas.sha256);
|
||||
assert(!("players" in welcome) && !("changes" in welcome), "welcome shouldn't have world state");
|
||||
|
||||
// not in the world yet: others don't hear about bob, and bob can't act
|
||||
send(2, { type: "chat", text: "am I here?" });
|
||||
send(2, { type: "break_block", x: 0, y: 60, z: 0 });
|
||||
assertEquals(take(1), []);
|
||||
assertEquals(take(2), []);
|
||||
assertEquals(joined, 0);
|
||||
|
||||
send(2, { type: "ready" });
|
||||
const messages = take(2);
|
||||
assertEquals(messages[0].type, "join");
|
||||
assertEquals(messages[0].name, "bob");
|
||||
assertEquals(messages[0].players.map((p: { name: string }) => p.name), ["alice"]);
|
||||
assert(messages.some((m) => m.type === "container" && m.container === "inventory"));
|
||||
assertEquals(joined, 1);
|
||||
assert(take(1).some((m) => m.type === "player_join" && m.player.name === "bob"));
|
||||
|
||||
// a second ready or hello does nothing
|
||||
send(2, { type: "ready" });
|
||||
send(2, { type: "hello", name: "bob", protocol: PROTOCOL_VERSION });
|
||||
assertEquals(take(2).filter((m) => m.type === "join" || m.type === "welcome"), []);
|
||||
assertEquals(joined, 1);
|
||||
});
|
||||
|
||||
Deno.test("clients that never get ready are dropped", async () => {
|
||||
const { game, take, send, closed } = await test_game("mods");
|
||||
game.on_connect(1);
|
||||
send(1, { type: "hello", name: "slow", protocol: PROTOCOL_VERSION });
|
||||
take(1);
|
||||
for (let i = 0; i < 59 * TICKS_PER_SECOND; i++) game.tick();
|
||||
assert(!closed.has(1), "dropped too early");
|
||||
for (let i = 0; i < 2 * TICKS_PER_SECOND; i++) game.tick();
|
||||
assert(closed.has(1));
|
||||
assertEquals(take(1)[0].type, "rejected");
|
||||
// too late now
|
||||
send(1, { type: "ready" });
|
||||
assertEquals(take(1), []);
|
||||
});
|
||||
+16
-3
@@ -4,6 +4,7 @@ import { load_and_check, load_order } from "$/tools/check_mods.ts";
|
||||
import type { ServerModSource } from "$/server/game/load_mods.ts";
|
||||
import { GameServer } from "$/server/game/game_server.ts";
|
||||
import { start_game } from "$/server/game/load_mods.ts";
|
||||
import { PROTOCOL_VERSION } from "$/common/protocol.ts";
|
||||
|
||||
// mods straight from their source folders, skipping the build: scripts are imported as typescript
|
||||
export function mod_sources(mods_dir: string): ServerModSource[] {
|
||||
@@ -20,6 +21,7 @@ export function mod_sources(mods_dir: string): ServerModSource[] {
|
||||
version: String(mod.manifest?.version),
|
||||
hash: "source",
|
||||
data: `mods/${mod.id}/source/data.json`,
|
||||
sha256: { data: "" },
|
||||
},
|
||||
data: {
|
||||
blocks: mod.blocks.map((b) => b.json),
|
||||
@@ -37,14 +39,18 @@ export function mod_sources(mods_dir: string): ServerModSource[] {
|
||||
export async function test_game(mods_dir: string, save?: string, seed = "test-seed") {
|
||||
EverythingRegistry.clear();
|
||||
const outbox = new Map<number, unknown[]>();
|
||||
const closed = new Set<number>();
|
||||
const host = {
|
||||
send: (conn: number, data: string) => {
|
||||
if (!outbox.has(conn)) outbox.set(conn, []);
|
||||
outbox.get(conn)!.push(JSON.parse(data));
|
||||
},
|
||||
close() {},
|
||||
close(conn: number) {
|
||||
closed.add(conn);
|
||||
},
|
||||
};
|
||||
const game: GameServer = await start_game(host, save, seed, mod_sources(mods_dir));
|
||||
const atlas = { png: "atlas.png", json: "atlas.json", sha256: { png: "", json: "" } };
|
||||
const game: GameServer = await start_game(host, save, seed, atlas, mod_sources(mods_dir));
|
||||
// deno-lint-ignore no-explicit-any
|
||||
const take = (conn: number): any[] => {
|
||||
const messages = outbox.get(conn) ?? [];
|
||||
@@ -52,7 +58,14 @@ export async function test_game(mods_dir: string, save?: string, seed = "test-se
|
||||
return messages;
|
||||
};
|
||||
const send = (conn: number, message: unknown) => game.on_message(conn, JSON.stringify(message));
|
||||
return { game, take, send };
|
||||
// the whole handshake: hello, welcome, ready, join. returns join
|
||||
const join = (conn: number, name: string) => {
|
||||
game.on_connect(conn);
|
||||
send(conn, { type: "hello", name, protocol: PROTOCOL_VERSION });
|
||||
send(conn, { type: "ready" });
|
||||
return take(conn).find((m) => m.type === "join");
|
||||
};
|
||||
return { game, take, send, join, closed };
|
||||
}
|
||||
|
||||
export function copy_dir(from: string, to: string) {
|
||||
|
||||
@@ -5,6 +5,7 @@ import { block_from_json, block_to_json, item_from_json, item_to_json } from "$/
|
||||
import { generate_raw_chunk } from "$/common/generation.ts";
|
||||
import { load_worldgen } from "$/common/worldgen_loader.ts";
|
||||
import { create_mod } from "$/tools/new_mod.ts";
|
||||
import { PROTOCOL_VERSION } from "$/common/protocol.ts";
|
||||
import { copy_dir, test_game } from "./helpers.ts";
|
||||
|
||||
// mods/bworld plus a mod made from the template, in a temp folder
|
||||
@@ -59,9 +60,10 @@ Deno.test("a mod made from the template loads and runs", async () => {
|
||||
);
|
||||
|
||||
game.on_connect(1);
|
||||
send(1, { type: "hello", name: "alice" });
|
||||
send(1, { type: "hello", name: "alice", protocol: PROTOCOL_VERSION });
|
||||
const welcome = take(1)[0];
|
||||
assertEquals(welcome.mods.map((m: Msg) => m.id), ["bworld", "copper_tools"]);
|
||||
send(1, { type: "ready" });
|
||||
|
||||
// the command, by name and by namespaced name
|
||||
send(1, { type: "chat", text: "/hello" });
|
||||
@@ -180,9 +182,8 @@ export function setup(ctx: ServerContext) {
|
||||
}
|
||||
`,
|
||||
);
|
||||
let { game, send, take } = await test_game(dir);
|
||||
game.on_connect(1);
|
||||
send(1, { type: "hello", name: "bob" });
|
||||
let { game, send, take, join } = await test_game(dir);
|
||||
join(1, "bob");
|
||||
for (let i = 0; i < 5; i++) game.tick();
|
||||
game.set_block(0, 100, 0, "copper_tools:example_block");
|
||||
send(1, { type: "move", x: 0.5, y: 100, z: 1.5, yaw: 0, pitch: 0 });
|
||||
@@ -190,9 +191,8 @@ export function setup(ctx: ServerContext) {
|
||||
assert(take(1).some((m) => m.text === "clicks 1, placed at 5"));
|
||||
|
||||
const saved = game.save();
|
||||
({ game, send, take } = await test_game(dir, saved));
|
||||
game.on_connect(1);
|
||||
send(1, { type: "hello", name: "bob" });
|
||||
({ game, send, take, join } = await test_game(dir, saved));
|
||||
join(1, "bob");
|
||||
send(1, { type: "use_block", x: 0, y: 100, z: 0, face: "top" });
|
||||
assert(take(1).some((m) => m.text === "clicks 2, placed at 5"));
|
||||
Deno.removeSync(dir, { recursive: true });
|
||||
|
||||
Reference in New Issue
Block a user