Actually implement mod code into server
This commit is contained in:
+117
-67
@@ -26,7 +26,6 @@ import {
|
||||
} from "$/common/protocol.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";
|
||||
import type { GameLoop } from "./game_loop.ts";
|
||||
import { consume_recipe_items, update_crafting_result } from "./crafting.ts";
|
||||
@@ -61,19 +60,12 @@ export interface GameHost {
|
||||
}
|
||||
|
||||
export interface SavedWorld {
|
||||
version: 2;
|
||||
version: 3;
|
||||
seed: string;
|
||||
changes: BlockChange[];
|
||||
tiles: {
|
||||
id: string;
|
||||
x: number;
|
||||
y: number;
|
||||
z: number;
|
||||
data: Record<string, unknown>;
|
||||
containers: Record<string, (ItemData | null)[]>;
|
||||
// a mod block's data, what BlockRef.data holds
|
||||
mod_data?: unknown;
|
||||
}[];
|
||||
tiles: SavedTile[];
|
||||
// every ctx.containers container, by id
|
||||
containers?: Record<string, (ItemData | null)[]>;
|
||||
players: Record<string, SavedPlayer>;
|
||||
// items on the ground
|
||||
entities?: SavedItemEntity[];
|
||||
@@ -81,6 +73,18 @@ export interface SavedWorld {
|
||||
mod_storage?: Record<string, Record<string, unknown>>;
|
||||
}
|
||||
|
||||
interface SavedTile {
|
||||
id: string;
|
||||
x: number;
|
||||
y: number;
|
||||
z: number;
|
||||
// what BlockRef.data holds
|
||||
mod_data?: unknown;
|
||||
// version 2, when chests and furnaces were engine code: their fields and their items
|
||||
data?: Record<string, unknown>;
|
||||
containers?: Record<string, (ItemData | null)[]>;
|
||||
}
|
||||
|
||||
// the loaded mods, see load_mods.ts
|
||||
export interface GameMods {
|
||||
listings: ModListing[];
|
||||
@@ -102,6 +106,8 @@ export class GameServer {
|
||||
#entities = new Map<string, ItemEntity>();
|
||||
#next_entity_id = 1;
|
||||
#mods: GameMods;
|
||||
// ctx.containers, saved with the world
|
||||
containers = new Map<string, Container>();
|
||||
mods: ModRuntime;
|
||||
recipes: RecipeBook;
|
||||
// what runs tick(), for /tps. tests call tick() themselves
|
||||
@@ -118,20 +124,20 @@ export class GameServer {
|
||||
this.world = new ServerWorld(saved?.seed ?? default_seed, mods.worldgen);
|
||||
this.mods.storage = saved?.mod_storage ?? {};
|
||||
this.world.load_changes(saved?.changes ?? []);
|
||||
for (const [id, items] of Object.entries(saved?.containers ?? {})) {
|
||||
const container = new Container(items.length);
|
||||
container.load(items);
|
||||
this.containers.set(id, container);
|
||||
}
|
||||
for (const tile of saved?.tiles ?? []) {
|
||||
const containers: Record<string, Container> = {};
|
||||
for (const [name, items] of Object.entries(tile.containers)) {
|
||||
containers[name] = new Container(items.length);
|
||||
containers[name].load(items);
|
||||
}
|
||||
this.world.tiles.set(`${tile.x},${tile.y},${tile.z}`, { ...tile, containers });
|
||||
this.world.add_tile({ id: tile.id, x: tile.x, y: tile.y, z: tile.z, mod_data: this.#upgrade_tile(tile) });
|
||||
}
|
||||
for (const entity of saved?.entities ?? []) {
|
||||
const item = ItemEntity.load(this.#new_entity_id(), entity);
|
||||
this.#entities.set(item.id, item);
|
||||
}
|
||||
this.#saved_players = saved?.players ?? {};
|
||||
this.world.dirty = saved?.version !== 2;
|
||||
this.world.dirty = saved?.version !== 3;
|
||||
|
||||
this.world.on_block_change = (x, y, z, id, state) =>
|
||||
this.#broadcast(state ? { type: "set_block", x, y, z, id, state } : { type: "set_block", x, y, z, id });
|
||||
@@ -185,28 +191,21 @@ export class GameServer {
|
||||
const second = this.#tick % TICKS_PER_SECOND === 0;
|
||||
|
||||
for (const tile of [...this.world.tiles.values()]) {
|
||||
const behavior = BLOCK_BEHAVIORS[tile.id];
|
||||
if (tile.mod_data === undefined) {
|
||||
continue;
|
||||
}
|
||||
const components = this.mods.components_of(tile.id)
|
||||
.filter(({ component }) => component.on_tick || component.on_second);
|
||||
if (!behavior?.on_tick && !behavior?.on_second && components.length === 0) {
|
||||
if (components.length === 0 || !this.#near_any_player(tile.x, tile.z)) {
|
||||
continue;
|
||||
}
|
||||
if (!this.#near_any_player(tile.x, tile.z)) {
|
||||
continue;
|
||||
}
|
||||
behavior?.on_tick?.(this, tile);
|
||||
if (second) {
|
||||
behavior?.on_second?.(this, tile);
|
||||
}
|
||||
if (components.length > 0 && tile.mod_data !== undefined) {
|
||||
const block = this.mods.block_ref(tile.x, tile.y, tile.z, tile.id);
|
||||
for (const { mod, id, component, params } of components) {
|
||||
if (component.on_tick) {
|
||||
run_guarded(mod, `${id} on_tick`, () => component.on_tick!(block, params, TICK_DELTA));
|
||||
}
|
||||
if (second && component.on_second) {
|
||||
run_guarded(mod, `${id} on_second`, () => component.on_second!(block, params, 1));
|
||||
}
|
||||
const block = this.mods.block_ref(tile.x, tile.y, tile.z, tile.id);
|
||||
for (const { mod, id, component, params } of components) {
|
||||
if (component.on_tick) {
|
||||
run_guarded(mod, `${id} on_tick`, () => component.on_tick!(block, params, TICK_DELTA));
|
||||
}
|
||||
if (second && component.on_second) {
|
||||
run_guarded(mod, `${id} on_second`, () => component.on_second!(block, params, 1));
|
||||
}
|
||||
}
|
||||
// tile data can change on any tick, saving is cheap enough to not track it exactly
|
||||
@@ -235,7 +234,7 @@ export class GameServer {
|
||||
this.#saved_players[player.name] = player.save();
|
||||
}
|
||||
const saved: SavedWorld = {
|
||||
version: 2,
|
||||
version: 3,
|
||||
seed: this.world.seed,
|
||||
changes: this.world.all_changes(),
|
||||
tiles: [...this.world.tiles.values()].map((tile) => ({
|
||||
@@ -243,12 +242,11 @@ export class GameServer {
|
||||
x: tile.x,
|
||||
y: tile.y,
|
||||
z: tile.z,
|
||||
data: tile.data,
|
||||
containers: Object.fromEntries(
|
||||
Object.entries(tile.containers).map(([name, container]) => [name, container.to_data()]),
|
||||
),
|
||||
mod_data: tile.mod_data,
|
||||
})),
|
||||
containers: Object.fromEntries(
|
||||
[...this.containers].map(([id, container]) => [id, container.to_data()]),
|
||||
),
|
||||
players: this.#saved_players,
|
||||
entities: [...this.#entities.values()].map((entity) => entity.save()),
|
||||
mod_storage: this.mods.storage,
|
||||
@@ -365,24 +363,9 @@ export class GameServer {
|
||||
}
|
||||
}
|
||||
|
||||
const old_tile = this.world.get_tile(x, y, z);
|
||||
if (old_tile) {
|
||||
BLOCK_BEHAVIORS[old_tile.id]?.on_break?.(this, old_tile, player);
|
||||
this.world.remove_tile(x, y, z);
|
||||
for (const other of this.#players.values()) {
|
||||
if (other.screen?.tile === old_tile) {
|
||||
this.#close_screen(other);
|
||||
this.#send(other, { type: "close_screen" });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.world.remove_tile(x, y, z);
|
||||
this.world.set_block(x, y, z, id, state);
|
||||
|
||||
if (BLOCK_BEHAVIORS[id]?.create_tile) {
|
||||
this.get_or_create_tile(x, y, z);
|
||||
}
|
||||
|
||||
if (id !== AIR_ID) {
|
||||
const block = this.mods.block_ref(x, y, z, id);
|
||||
for (const { mod, id: component_id, component, params } of this.mods.components_of(id)) {
|
||||
@@ -396,23 +379,72 @@ export class GameServer {
|
||||
get_or_create_tile(x: number, y: number, z: number): Tile {
|
||||
let tile = this.world.get_tile(x, y, z);
|
||||
if (!tile) {
|
||||
// blocks placed before tiles were saved have none yet
|
||||
const id = this.world.get_block_id(x, y, z);
|
||||
tile = { id, x, y, z, data: {}, containers: {} };
|
||||
BLOCK_BEHAVIORS[id]?.create_tile?.(tile);
|
||||
tile = { id: this.world.get_block_id(x, y, z), x, y, z };
|
||||
this.world.add_tile(tile);
|
||||
}
|
||||
return tile;
|
||||
}
|
||||
|
||||
// containers
|
||||
|
||||
create_container(size: number): { id: string; container: Container } {
|
||||
const id = crypto.randomUUID();
|
||||
const container = new Container(size);
|
||||
this.containers.set(id, container);
|
||||
this.world.dirty = true;
|
||||
return { id, container };
|
||||
}
|
||||
|
||||
// anyone looking at it gets their screen closed
|
||||
delete_container(id: string) {
|
||||
const container = this.containers.get(id);
|
||||
if (!container) {
|
||||
return;
|
||||
}
|
||||
this.containers.delete(id);
|
||||
this.world.dirty = true;
|
||||
for (const player of this.#players.values()) {
|
||||
if (player.screen?.container === container) {
|
||||
this.close_screen(player);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// screens
|
||||
|
||||
open_screen(player: ServerPlayer, screen: OpenScreen) {
|
||||
this.#close_screen(player);
|
||||
player.screen = screen;
|
||||
this.#send(player, { type: "open_screen", layout: screen.layout, properties: screen.properties() });
|
||||
player.sent.set("properties", JSON.stringify(screen.properties()));
|
||||
this.#send(player, { type: "open_screen", layout: screen.layout, properties: { ...screen.properties } });
|
||||
player.sent.set("properties", JSON.stringify(screen.properties));
|
||||
player.sent.delete("screen");
|
||||
}
|
||||
|
||||
// closes it for the player too, not just on the server
|
||||
close_screen(player: ServerPlayer) {
|
||||
if (!player.screen) {
|
||||
return;
|
||||
}
|
||||
this.#close_screen(player);
|
||||
this.#send(player, { type: "close_screen" });
|
||||
}
|
||||
|
||||
// version 2 saves had chests and furnaces in the engine, with their fields in data and their items in
|
||||
// containers.main. the components keep those fields and put the items in a ctx.containers container
|
||||
#upgrade_tile(tile: SavedTile): unknown {
|
||||
if (tile.mod_data !== undefined || (!tile.data && !tile.containers)) {
|
||||
return tile.mod_data;
|
||||
}
|
||||
const data: Record<string, unknown> = { ...tile.data };
|
||||
const items = tile.containers?.main;
|
||||
if (items) {
|
||||
const { id, container } = this.create_container(items.length);
|
||||
container.load(items);
|
||||
data.container = id;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
// messages
|
||||
|
||||
// hello -> welcome, then ready -> join. see "Delivery to clients" in MODS.md
|
||||
@@ -623,7 +655,7 @@ export class GameServer {
|
||||
return;
|
||||
}
|
||||
|
||||
let handled = BLOCK_BEHAVIORS[info.id]?.on_interact?.(this, { x, y, z, id: info.id }, player) ?? false;
|
||||
let handled = false;
|
||||
for (const { mod, id: component_id, component, params } of this.mods.components_of(info.id)) {
|
||||
if (component.on_interact) {
|
||||
handled = run_guarded(mod, `${component_id} on_interact`, () =>
|
||||
@@ -715,6 +747,11 @@ export class GameServer {
|
||||
if (item && take_output(item, player.cursor)) {
|
||||
container.set_item(index, undefined);
|
||||
}
|
||||
} else if (
|
||||
key === "screen" && player.cursor.item && !(player.screen?.filters.get(index)?.(player.cursor.item) ?? true)
|
||||
) {
|
||||
// the slot doesn't take what the cursor holds
|
||||
return;
|
||||
} else {
|
||||
click_slot(container, index, player.cursor, button);
|
||||
}
|
||||
@@ -722,6 +759,9 @@ export class GameServer {
|
||||
if (key === "crafting") {
|
||||
update_crafting_result(container, this.recipes.shaped);
|
||||
}
|
||||
if (key === "screen") {
|
||||
this.world.dirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
#chat(player: ServerPlayer, raw: unknown) {
|
||||
@@ -756,8 +796,16 @@ export class GameServer {
|
||||
this.#send(player, { type: "chat", text: "Usage: /give <item> [count]" });
|
||||
return;
|
||||
}
|
||||
// a name without a namespace works when only one mod has an item called that
|
||||
if (!item_id.includes(":")) {
|
||||
item_id = `bworld:${item_id}`;
|
||||
const matches = EverythingRegistry.entries("items").map(([id]) => id).filter((id) =>
|
||||
id.endsWith(`:${item_id}`)
|
||||
);
|
||||
if (matches.length > 1) {
|
||||
this.#send(player, { type: "chat", text: `Which one? ${matches.join(", ")}` });
|
||||
return;
|
||||
}
|
||||
item_id = matches[0] ?? item_id;
|
||||
}
|
||||
const amount = count === undefined ? 1 : Number(count);
|
||||
if (!EverythingRegistry.get("items", item_id)) {
|
||||
@@ -793,7 +841,9 @@ export class GameServer {
|
||||
}
|
||||
|
||||
#close_screen(player: ServerPlayer) {
|
||||
const screen = player.screen;
|
||||
player.screen = undefined;
|
||||
for (const fn of screen?.on_close ?? []) fn();
|
||||
player.sent.delete("screen");
|
||||
player.sent.delete("properties");
|
||||
|
||||
@@ -949,7 +999,7 @@ export class GameServer {
|
||||
if (player.screen) {
|
||||
const items = player.screen.container.to_data();
|
||||
sync("screen", items, () => ({ type: "container", container: "screen", items }));
|
||||
const properties = player.screen.properties();
|
||||
const properties = { ...player.screen.properties };
|
||||
sync("properties", properties, () => ({ type: "screen_properties", properties }));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user