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
+65 -31
View File
@@ -394,6 +394,7 @@ interface ServerContext {
world: ServerWorld;
players: PlayerList;
containers: ContainerApi;
items: ItemApi;
recipes: RecipeApi;
ui: ServerUi; // see GUIs
net: ServerNet; // see Mod channels
@@ -477,6 +478,7 @@ interface ServerWorld {
get_state(x: number, y: number, z: number, name: string): number | undefined;
set_state(x: number, y: number, z: number, name: string, value: number): boolean;
get_block_data<T>(x: number, y: number, z: number): T | undefined;
drop_item(x: number, y: number, z: number, item: ItemStack): void; // pops out of the block, like drops
is_loaded(x: number, z: number): boolean;
readonly seed: string;
}
@@ -612,8 +614,8 @@ code sees it.
### 2. Container screens (server only)
For inventories and machines. The server owns the container's contents and syncs them to every player viewing it. Clicks
go to the server, which applies them. The chest and furnace would be rebuilt this way instead of their hand-written
`GuiChest` / `GuiFurnace` classes.
go to the server, which applies them. The base game's chest and furnace are built this way, see
`mods/bworld/scripts/server.ts`.
```ts
ctx.components.register_block("copper_tools:smelter", {
@@ -623,36 +625,50 @@ ctx.components.register_block("copper_tools:smelter", {
on_interact(block, _params, player) {
const container = ctx.containers.get(block.data.container)!;
const screen = ctx.ui.open_container(player, {
title: "Smelter",
container,
layout: [
{ slot: 0, x: 3, y: 0, filter: "smeltable" },
{ slot: 1, x: 3, y: 2, filter: (item) => ctx.recipes.is_fuel(item.id) },
{ slot: 2, x: 5, y: 1, output_only: true },
],
container,
player_inventory: true,
bars: [{ id: "progress", x: 4, y: 1, texture: "bworld:arrow" }],
bars: [{
x: 4,
y: 1,
value: "progress",
max: "progress_max",
direction: "right",
empty_texture: "bworld:arrow_empty",
full_texture: "bworld:arrow_full",
}],
});
screen.set_property("progress", block.data.progress);
screen.set_property("progress_max", 200);
return true;
},
on_break(block) {
ctx.containers.delete(block.data.container);
},
});
```
- `x` / `y` are in slot units on a grid. The client scales and centers the screen, and draws the player's inventory
below it when `player_inventory` is true.
- The screen is drawn below the player's inventory and hotbar. `x` / `y` are in slot units in the screen's own area and
can be fractional. `rows` sets its height, by default it fits the slots and bars.
- `filter` is a function `(item) => boolean` or one of the built-in filters `"smeltable"` (has a furnace recipe) and
`"fuel"`. It and `output_only` run **on the server**, so players can't put the wrong items in by editing their client.
- `bars` are progress bars filled from 0 to 1 by `screen.set_property(id, value)`. Properties can also be shown as text
with `labels: [{ x, y, property }]`.
- The screen handle has `set_property`, `close()` and `on_close(fn)`. Changes to the container (from scripts, hoppers,
`output_only` slots can only be taken from, all at once, like the furnace result.
- `bars` are progress bars filled with `value / max`, both names of properties set with `screen.set_property(id, n)`.
`direction` is `"up"` (like the furnace's fire) or `"right"` (like its arrow). Properties are synced when they change,
so a component can update them every tick.
- The screen handle has `player`, `open`, `set_property`, `close()` and `on_close(fn)`. It closes when the player closes
it, leaves, or opens another screen, and when its container is deleted. Changes to the container (from scripts,
other players) show up for everyone viewing it.
- Containers are saved with the world until they're deleted, so a block that has one deletes it in `on_break`.
```ts
interface ContainerApi {
create(size: number): Container; // saved with the world
create(size: number): Container; // 1 to 256 slots, saved with the world
get(id: string): Container | undefined;
delete(id: string): void;
delete(id: string): void; // closes screens showing it
}
interface Container {
@@ -663,8 +679,15 @@ interface Container {
add(item: ItemStack): ItemStack | undefined; // returns what didn't fit
on_change(fn: (slot: number) => void): () => void;
}
interface ItemApi {
exists(id: string): boolean;
max_stack(id: string): number;
}
```
Items handed to scripts are live: setting `count` changes the stack, and a stack counted down to 0 is gone.
### 3. Custom screens (client code)
For anything forms and containers can't do: maps, skill trees, minigames, custom layouts. The client script registers a
@@ -1036,15 +1059,21 @@ export function setup(ctx: ServerContext) {
on_interact(block, _params, player) {
const data = block.data as SmelterData;
ctx.ui.open_container(player, {
title: "Smelter",
container: ctx.containers.get(data.container)!,
layout: [
{ slot: 0, x: 3, y: 0, filter: "smeltable" },
{ slot: 1, x: 3, y: 2, filter: "fuel" },
{ slot: 2, x: 5, y: 1, output_only: true },
],
player_inventory: true,
bars: [{ id: "progress", x: 4, y: 1, texture: "bworld:arrow" }],
bars: [{
x: 4,
y: 1,
value: "progress",
max: "progress_max",
direction: "right",
empty_texture: "bworld:arrow_empty",
full_texture: "bworld:arrow_full",
}],
}).set_property("progress", data.progress);
return true;
},
@@ -1126,10 +1155,10 @@ anything the base game does.
Not moved:
- **Crops** (`common/blocks/crops.ts`) are unfinished and mostly commented out. They stay where they are and get rebuilt
as a component once server scripts exist, as a good first test of tile data plus ticking.
- **The watering can's lore** (`get_lore`) isn't shown anywhere yet, because the game has no item tooltips. It moves
once tooltips exist.
- **Carrots, potatoes, tomatoes and pumpkins** (`common/blocks/crops.ts`) are unfinished and mostly commented out.
Wheat uses the `bworld:crop` component instead, and they can too once they have blocks.
- **The watering can's lore** moved into its component's `get_lore`, but it isn't shown anywhere yet, because the game
has no item tooltips.
### What stays in the engine
@@ -1150,7 +1179,8 @@ Worlds saved before the move must load afterwards with nothing changed:
3. **Tile data keeps its shape.** Chests store their items in `containers.main`, and furnaces keep `containers.main`
plus `progress`, `progress_max`, `fuel` and `fuel_max` in `data`. The components read exactly those, so tiles saved
by the current code load into them.
4. **The save format and protocol don't change.** Clients receive `mods/bworld` like any other mod.
4. **The protocol doesn't change, and old saves load.** Clients receive `mods/bworld` like any other mod. The save
format may change when it has to, as long as older saves are upgraded when loading (phase 2 did, to version 3).
### Phases
@@ -1180,14 +1210,17 @@ loading mods (the base game and the template), their scripts and worldgen, saves
- Delete `common/blocks/` and `common/items/`.
- Check: the registry built from JSON equals `registry.json`, and all tests pass.
**Phase 2: behavior** (after step 6, and container screens from step 7).
**Phase 2: behavior** (after step 6, and container screens from step 7). _Done._
- `mods/bworld/scripts/server.ts` registers `bworld:hoeable`, `bworld:storage`, `bworld:furnace` and
`bworld:watering_can`. The chest and furnace open their screens with `ctx.ui.open_container`, whose layouts are the
`ScreenLayout`s the server sends now.
- The block JSON lists the components. Delete `server/game/blocks.ts`.
- Check: the server logic tests (hoeing, chest, furnace smelting, breaking a chest gives its contents back) and
`world_v2.json` still pass.
- `mods/bworld/scripts/server.ts` registers `bworld:hoeable`, `bworld:storage`, `bworld:furnace`, `bworld:crop` (bone
meal on wheat) and `bworld:watering_can`. The chest and furnace open their screens with `ctx.ui.open_container`, with
the same layouts as before.
- The block and item JSON lists the components. `server/game/blocks.ts` is deleted.
- Saves went to version 3: tiles only keep `mod_data`, and containers are saved by id. Version 2 chests and furnaces are
upgraded when loaded: their `data` fields stay and their `containers.main` becomes a container whose id is in
`container`, which is the shape the components use.
- Check: `tests/base_game_test.ts` (hoeing, chest, furnace smelting, breaking a chest gives its contents back, bone meal,
the watering can) and `tests/fixtures/world_v2.json`, saved by the engine code before the move, pass.
**Phase 3: world generation** (after step 9).
@@ -1239,10 +1272,11 @@ working:
joining. Add the confirm screen for cross-origin servers and CORS headers on the server. _Done._
6. **Server scripts.** Components, events, commands, system, storage and `ctx.recipes`. Move `FURNACE_RECIPES` and
`FUEL_VALUES` into the recipe registry. _Done_ (`server/game/mod_runtime.ts`, with the loop in
`server/game/game_loop.ts`). `ctx.containers`, `ctx.ui` and `ctx.net` throw until steps 7 and 8, and so do the
client's `ctx.ui`, `ctx.hud`, `ctx.input` and `ctx.net`.
`server/game/game_loop.ts`). `ctx.net` throws until step 8, and so do the client's `ctx.ui`, `ctx.hud`,
`ctx.input` and `ctx.net`.
7. **GUIs.** Forms, then container screens (rebuild chest and furnace with them), then custom screens, the `Graphics`
API (built on the existing renderer and debug UI widgets) and the HUD.
API (built on the existing renderer and debug UI widgets) and the HUD. _Container screens done_ (`ctx.containers`,
`ctx.ui.open_container`), forms and custom screens throw until they're built.
8. **Mod channels** and keybinds.
9. **Worldgen mods.** Worldgen URLs and mod ores go into the chunk worker `init` message. Workers must finish importing
before generating anything. _Done for features and ores._ `register_terrain` throws until phase 3 moves the base