Actually implement mod code into server

This commit is contained in:
2026-09-25 23:13:46 -03:00
parent 099811670f
commit 9237152aa1
22 changed files with 1359 additions and 457 deletions
+142 -11
View File
@@ -8,10 +8,12 @@ import type {
BlockRef,
Command,
Container,
ContainerScreenOptions,
EventSignal,
ItemComponent,
ItemStack,
Player,
ScreenHandle,
ServerAfterEvents,
ServerBeforeEvents,
ServerContext,
@@ -19,7 +21,8 @@ import type {
import { ModLoadError } from "$/common/mod_loader.ts";
import { AIR_ID } from "$/common/protocol.ts";
import type { GameServer } from "./game_server.ts";
import type { ServerPlayer } from "./player.ts";
import type { OpenScreen, ServerPlayer } from "./player.ts";
import type { ScreenLayout } from "$/common/protocol.ts";
// a list of handlers that can't break each other: one throwing is logged under its mod and the rest still run
export class Signal<T> {
@@ -107,6 +110,8 @@ export class ModRuntime {
#next_timer = 1;
#setting_up = true;
#players = new WeakMap<ServerPlayer, Player>();
// the other way, for apis that take a player
#server_players = new WeakMap<Player, ServerPlayer>();
game!: GameServer;
// imports each server script and calls its setup, in load order
@@ -152,6 +157,19 @@ export class ModRuntime {
);
}
}
const lore = components.filter(([component_id]) =>
this.item_components.get(component_id)!.component.get_lore
);
if (lore.length > 0) {
item.get_lore = (stack) =>
lore.map(([component_id, params]) => {
const { mod, component } = this.item_components.get(component_id)!;
return run_guarded(mod, `${component_id} get_lore`, () =>
component.get_lore!(item_api(stack), params));
}).filter((line) =>
typeof line === "string"
).join("\n");
}
// items are created all over the engine, hook their creation once here
const previous = item.on_create;
item.on_create = (stack) => {
@@ -231,6 +249,7 @@ export class ModRuntime {
if (!api) {
api = player_api(this.game, player);
this.#players.set(player, api);
this.#server_players.set(api, player);
}
return api;
}
@@ -339,6 +358,7 @@ export class ModRuntime {
return true;
},
get_block_data: (x, y, z) => game().world.get_tile(x, y, z)?.mod_data as never,
drop_item: (x, y, z, item) => void game().pop_item(x, y, z, new_stack(item)),
is_loaded: () => true,
get seed() {
return game().world.seed;
@@ -355,6 +375,14 @@ export class ModRuntime {
return p && this.player(p);
},
},
items: {
exists: (id) => EverythingRegistry.get("items", id) !== undefined,
max_stack: (id) => {
const item = EverythingRegistry.get<ItemRegistry>("items", id);
if (!item) throw new Error(`unknown item ${id}`);
return item.max_stack ?? 64;
},
},
recipes: {
furnace_result: (input) => {
const recipe = game().recipes.furnace.get(input);
@@ -364,8 +392,27 @@ export class ModRuntime {
is_fuel: (item) => game().recipes.fuel.has(item),
is_smeltable: (item) => game().recipes.furnace.has(item),
},
containers: not_yet_object(mod, "containers", "step 7 in MODS.md"),
ui: not_yet_object(mod, "ui", "step 7 in MODS.md"),
containers: {
create: (size) => {
if (!Number.isInteger(size) || size < 1 || size > MAX_CONTAINER_SIZE) {
throw new Error(`a container has 1 to ${MAX_CONTAINER_SIZE} slots, not ${size}`);
}
const { id, container } = game().create_container(size);
return container_api(game(), id, container);
},
get: (id) => {
const container = game().containers.get(id);
return container && container_api(game(), id, container);
},
delete: (id) => game().delete_container(id),
},
ui: {
open_container: (player, options) => this.#open_container(mod, player, options),
message_form: not_yet(mod, "ui.message_form", "step 7 in MODS.md"),
action_form: not_yet(mod, "ui.action_form", "step 7 in MODS.md"),
modal_form: not_yet(mod, "ui.modal_form", "step 7 in MODS.md"),
open_screen: not_yet(mod, "ui.open_screen", "step 7 in MODS.md"),
},
net: not_yet_object(mod, "net", "step 8 in MODS.md"),
storage: {
get: (key) => structuredClone(this.storage[mod]?.[key]) as never,
@@ -383,6 +430,78 @@ export class ModRuntime {
return ctx;
}
#open_container(mod: string, player: Player, options: ContainerScreenOptions): ScreenHandle {
const game = this.game;
const server_player = this.#server_players.get(player);
if (!server_player || !game.players().includes(server_player)) {
throw new Error(`${player.name} isn't online`);
}
const container = game.containers.get(options.container.id);
if (!container) {
throw new Error("open_container needs a container from ctx.containers");
}
const filters = new Map<number, (item: EngineItemStack) => boolean>();
for (const { slot, filter } of options.layout) {
if (!Number.isInteger(slot) || slot < 0 || slot >= container.size) {
throw new Error(`slot ${slot} isn't in the container, it has ${container.size}`);
}
if (filter === "smeltable") {
filters.set(slot, (item) => game.recipes.furnace.has(item.type_id));
} else if (filter === "fuel") {
filters.set(slot, (item) => game.recipes.fuel.has(item.type_id));
} else if (typeof filter === "function") {
filters.set(slot, (item) => run_guarded(mod, "a slot filter", () => filter(item_api(item))) === true);
}
}
const bars = options.bars ?? [];
const bottom = Math.max(0, ...options.layout.map((s) => s.y + 1), ...bars.map((b) => b.y + 1));
const layout: ScreenLayout = {
rows: options.rows ?? Math.ceil(bottom),
slots: options.layout.map(({ slot, x, y, output_only }) =>
output_only ? { index: slot, x, y, output: true } : { index: slot, x, y }
),
bars: bars.map(({ x, y, value, max, direction, empty_texture, full_texture }) => ({
x,
y,
value,
max,
direction,
empty_texture,
full_texture,
})),
};
let open = true;
const screen: OpenScreen = { container, layout, properties: {}, filters, on_close: [() => open = false] };
game.open_screen(server_player, screen);
return {
player,
get open() {
return open;
},
set_property(id, value) {
if (typeof value !== "number" || !Number.isFinite(value)) {
throw new Error(`property ${id} must be a number, not ${value}`);
}
screen.properties[id] = value;
},
update: not_yet(mod, "update", "custom screens are step 7 in MODS.md"),
close() {
if (server_player.screen === screen) game.close_screen(server_player);
},
on_close(fn) {
if (!open) {
run_guarded(mod, "a screen on_close", fn);
return;
}
screen.on_close.push(() => run_guarded(mod, "a screen on_close", fn));
},
};
}
#add_timer(mod: string, fn: () => void, ticks: number, every?: number) {
const handle = this.#next_timer++;
this.#timers.set(handle, { mod, fn, at: this.game.current_tick + Math.max(0, ticks), every });
@@ -426,6 +545,8 @@ function not_yet_object<T>(mod: string, name: string, where: string): T {
}) as T;
}
const MAX_CONTAINER_SIZE = 256;
// the engine's item stacks as mods see them: { id, count, data }
export function item_api(stack: EngineItemStack): ItemStack {
return {
@@ -454,22 +575,32 @@ function new_stack(item: ItemStack): EngineItemStack {
return stack;
}
function player_api(game: GameServer, player: ServerPlayer): Player {
const inventory: Container = {
id: `player:${player.id}`,
size: player.inventory.size,
// an engine container as mods see it
function container_api(game: GameServer, id: string, container: EngineContainer): Container {
return {
id,
size: container.size,
get: (slot) => {
const stack = player.inventory.get_item(slot);
const stack = container.get_item(slot);
return stack && item_api(stack);
},
set: (slot, item) => player.inventory.set_item(slot, item ? new_stack(item) : undefined),
set: (slot, item) => {
if (!Number.isInteger(slot) || slot < 0 || slot >= container.size) {
throw new Error(`slot ${slot} isn't in the container, it has ${container.size}`);
}
container.set_item(slot, item ? new_stack(item) : undefined);
},
add: (item) => {
const stack = new_stack(item);
const left = player.inventory.add_item(stack);
const left = container.add_item(stack);
return left > 0 ? { id: item.id, count: left, data: stack.data } : undefined;
},
on_change: (fn) => game.mods.watch_container(running_mod || "unknown", player.inventory, fn),
on_change: (fn) => game.mods.watch_container(running_mod || "unknown", container, fn),
};
}
function player_api(game: GameServer, player: ServerPlayer): Player {
const inventory = container_api(game, `player:${player.id}`, player.inventory);
return {
get id() {