Server actually ticks
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
// runs mods' server scripts: builds each one's ServerContext and keeps what they register.
|
||||
// see "Server scripts" in MODS.md. parts not built yet throw when used, saying so
|
||||
import { EverythingRegistry, ItemRegistry } from "$/common/everything_registry.ts";
|
||||
import { ItemStack as EngineItemStack } from "$/common/inventory.ts";
|
||||
import { BlockRegistry, EverythingRegistry, ItemRegistry } from "$/common/everything_registry.ts";
|
||||
import { Container as EngineContainer, ItemStack as EngineItemStack } from "$/common/inventory.ts";
|
||||
import { get_state_value, set_state_value } from "$/common/utils.ts";
|
||||
import type {
|
||||
BlockComponent,
|
||||
BlockRef,
|
||||
@@ -47,12 +48,19 @@ export class Signal<T> {
|
||||
}
|
||||
}
|
||||
|
||||
// the mod whose code is running right now, for apis shared between mods that need to know who called
|
||||
let running_mod = "";
|
||||
|
||||
export function run_guarded<T>(mod: string, what: string, fn: () => T): T | undefined {
|
||||
const previous = running_mod;
|
||||
running_mod = mod;
|
||||
try {
|
||||
return fn();
|
||||
} catch (e) {
|
||||
console.error(`[${mod}] ${what} threw:`, e);
|
||||
return undefined;
|
||||
} finally {
|
||||
running_mod = previous;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,6 +102,8 @@ export class ModRuntime {
|
||||
storage: Record<string, Record<string, unknown>> = {};
|
||||
|
||||
#timers = new Map<number, Timer>();
|
||||
// containers mods listen to with on_change, compared slot by slot every tick
|
||||
#watched: { mod: string; container: EngineContainer; last: string[]; fn: (slot: number) => void }[] = [];
|
||||
#next_timer = 1;
|
||||
#setting_up = true;
|
||||
#players = new WeakMap<ServerPlayer, Player>();
|
||||
@@ -107,7 +117,12 @@ export class ModRuntime {
|
||||
if (typeof module.setup !== "function") {
|
||||
throw new ModLoadError(mod, "the server script doesn't export a setup function");
|
||||
}
|
||||
await module.setup(this.#context(mod, version));
|
||||
running_mod = mod;
|
||||
try {
|
||||
await module.setup(this.#context(mod, version));
|
||||
} finally {
|
||||
running_mod = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,6 +180,36 @@ export class ModRuntime {
|
||||
}));
|
||||
}
|
||||
|
||||
// components on an item, with their params from its json
|
||||
item_components_of(item_id: string): { mod: string; id: string; component: ItemComponent; params: unknown }[] {
|
||||
const item = EverythingRegistry.get<ItemRegistry>("items", item_id);
|
||||
return Object.entries(item?.components ?? {}).map(([id, params]) => ({
|
||||
...this.item_components.get(id)!,
|
||||
id,
|
||||
params,
|
||||
}));
|
||||
}
|
||||
|
||||
watch_container(mod: string, container: EngineContainer, fn: (slot: number) => void) {
|
||||
const entry = { mod, container, fn, last: snapshot(container) };
|
||||
this.#watched.push(entry);
|
||||
return () => {
|
||||
this.#watched = this.#watched.filter((w) => w !== entry);
|
||||
};
|
||||
}
|
||||
|
||||
// items change in many places (clicks, drops, crafting, scripts), so compare instead of hooking every one
|
||||
check_watched_containers() {
|
||||
for (const watch of [...this.#watched]) {
|
||||
const now = snapshot(watch.container);
|
||||
const changed = now.flatMap((slot, i) => slot !== watch.last[i] ? [i] : []);
|
||||
watch.last = now;
|
||||
for (const slot of changed) {
|
||||
run_guarded(watch.mod, "a container on_change", () => watch.fn(slot));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tick() {
|
||||
const now = this.game.current_tick;
|
||||
for (const [handle, timer] of [...this.#timers]) {
|
||||
@@ -246,7 +291,7 @@ export class ModRuntime {
|
||||
if (!/^[a-z0-9_]+$/.test(name)) {
|
||||
throw new ModLoadError(mod, `command name "${name}" must be a-z, 0-9 and _`);
|
||||
}
|
||||
if (name === "give") throw new ModLoadError(mod, `/give belongs to the base game`);
|
||||
if (["give", "tps"].includes(name)) throw new ModLoadError(mod, `/${name} belongs to the engine`);
|
||||
const existing = this.commands.get(name);
|
||||
if (existing) {
|
||||
throw new ModLoadError(mod, `/${name} is also registered by ${existing.mod}`);
|
||||
@@ -275,8 +320,24 @@ export class ModRuntime {
|
||||
game().set_block(x, y, z, id);
|
||||
return true;
|
||||
},
|
||||
get_state: not_yet(mod, "world.get_state", "block states aren't synced or saved yet"),
|
||||
set_state: not_yet(mod, "world.set_state", "block states aren't synced or saved yet"),
|
||||
get_state: (x, y, z, name) => {
|
||||
const id = game().world.get_block_id(x, y, z);
|
||||
if (id === AIR_ID) return undefined;
|
||||
const info = state_owner(id, name);
|
||||
return get_state_value(game().world.get_block_value(x, y, z), info, name);
|
||||
},
|
||||
set_state: (x, y, z, name, value) => {
|
||||
const id = game().world.get_block_id(x, y, z);
|
||||
if (id === AIR_ID) return false;
|
||||
const info = state_owner(id, name);
|
||||
const bits = info.states!.find((s) => s.name === name)!.bits;
|
||||
if (!Number.isInteger(value) || value < 0 || value >= 2 ** bits) {
|
||||
throw new Error(`${id} state "${name}" has ${bits} bits, ${value} doesn't fit`);
|
||||
}
|
||||
const new_value = set_state_value(game().world.get_block_value(x, y, z), info, name, value)!;
|
||||
game().set_block_state(x, y, z, new_value >>> 16);
|
||||
return true;
|
||||
},
|
||||
get_block_data: (x, y, z) => game().world.get_tile(x, y, z)?.mod_data as never,
|
||||
is_loaded: () => true,
|
||||
get seed() {
|
||||
@@ -330,6 +391,19 @@ export class ModRuntime {
|
||||
}
|
||||
|
||||
// each event's signal, as seen by one mod so errors say which mod threw
|
||||
// a block that has this state, or an error saying it doesn't
|
||||
function state_owner(id: string, name: string): BlockRegistry {
|
||||
const info = EverythingRegistry.get<BlockRegistry>("blocks", id)!;
|
||||
if (!info.states?.some((s) => s.name === name)) {
|
||||
throw new Error(`${id} has no state "${name}"`);
|
||||
}
|
||||
return info;
|
||||
}
|
||||
|
||||
function snapshot(container: EngineContainer): string[] {
|
||||
return container.to_data().map((item) => JSON.stringify(item));
|
||||
}
|
||||
|
||||
function map_signals<T extends { [name: string]: { for_mod(mod: string): unknown } }>(
|
||||
signals: T,
|
||||
mod: string,
|
||||
@@ -394,7 +468,7 @@ function player_api(game: GameServer, player: ServerPlayer): Player {
|
||||
const left = player.inventory.add_item(stack);
|
||||
return left > 0 ? { id: item.id, count: left, data: stack.data } : undefined;
|
||||
},
|
||||
on_change: not_yet("player", "inventory.on_change", "containers are step 7 in MODS.md"),
|
||||
on_change: (fn) => game.mods.watch_container(running_mod || "unknown", player.inventory, fn),
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user