Actually implement mod code into server
This commit is contained in:
+142
-11
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user