Partly mods plan
This commit is contained in:
@@ -1,51 +1,65 @@
|
|||||||
# bworld mods specification
|
# bworld mods specification
|
||||||
|
|
||||||
Status: **draft, format_version 1**. Nothing here is implemented yet, see [Implementation plan](#implementation-plan).
|
Status: **draft, format_version 1**, partly built. The [Implementation plan](#implementation-plan) lists what works
|
||||||
|
today and what's planned, and sections about planned features say so.
|
||||||
|
|
||||||
Mods add blocks, items, textures, recipes, world generation, game logic and GUIs to bworld. A mod is installed **on the
|
Mods add blocks, items, textures, recipes, world generation, game logic and GUIs to bworld, and can change how the game
|
||||||
server**. Players who join download its client code, data and textures from the server automatically, so they don't
|
itself works. A mod is installed **on the server**. Players who join download its client code, data and textures from
|
||||||
install anything themselves. bworld is always played on a server; there is no single player mode.
|
the server automatically, so they don't install anything themselves. bworld is always played on a server; there is no
|
||||||
|
single player mode.
|
||||||
|
|
||||||
This borrows from Minecraft Bedrock add-ons: data lives in JSON, blocks get behavior from named custom components, and
|
bworld's modding takes from three places:
|
||||||
scripts use before/after events. Bedrock servers also push resource packs to joining players, and their server scripts
|
|
||||||
can show forms that the client draws (`@minecraft/server-ui`). bworld goes one step further and pushes client _scripts_
|
- **Minetest: the game is mods.** The engine is small, and everything that makes bworld _bworld_ (its blocks, items,
|
||||||
too, since the client is already a web page.
|
crafting, inventory screen, HUD, commands and terrain) lives in `mods/bworld`, written against the same API as any
|
||||||
|
other mod. Mods can change each other's content, including the base game's.
|
||||||
|
- **Minecraft Bedrock add-ons: easy to start.** Most content is JSON, blocks and items get behavior from named
|
||||||
|
components, scripts use before/after events and server-driven forms, and players get everything from the server.
|
||||||
|
- **Minecraft Java with Fabric: no ceiling.** A mod that needs to can reach into the engine itself and hook any of its
|
||||||
|
methods, on the server and on the client, like Fabric's mixins.
|
||||||
|
|
||||||
## Contents
|
## Contents
|
||||||
|
|
||||||
- [Overview](#overview)
|
- [Overview](#overview)
|
||||||
|
- [Loading](#loading)
|
||||||
- [Creating a mod](#creating-a-mod)
|
- [Creating a mod](#creating-a-mod)
|
||||||
- [Mod layout](#mod-layout)
|
- [Mod layout](#mod-layout)
|
||||||
- [manifest.json](#manifestjson)
|
- [manifest.json](#manifestjson)
|
||||||
- [Identifiers](#identifiers)
|
- [Identifiers](#identifiers)
|
||||||
- [Textures](#textures)
|
- Data: [Textures](#textures), [Blocks](#blocks), [Block models](#block-models), [Items](#items),
|
||||||
- [Blocks](#blocks)
|
[Recipes](#recipes), [Overrides](#overrides)
|
||||||
- [Block models](#block-models)
|
- Game API: [Common scripts](#common-scripts), [Server scripts](#server-scripts), [Client scripts](#client-scripts),
|
||||||
- [Items](#items)
|
[GUIs](#guis), [Mod channels](#mod-channels), [World generation](#world-generation)
|
||||||
- [Recipes](#recipes)
|
- [Engine access](#engine-access)
|
||||||
- [Server scripts](#server-scripts)
|
|
||||||
- [Client scripts](#client-scripts)
|
|
||||||
- [GUIs](#guis)
|
|
||||||
- [Mod channels](#mod-channels)
|
|
||||||
- [World generation](#world-generation)
|
|
||||||
- [Delivery to clients](#delivery-to-clients)
|
- [Delivery to clients](#delivery-to-clients)
|
||||||
- [Security](#security)
|
- [Security](#security)
|
||||||
- [Example mod](#example-mod)
|
- [Example mod](#example-mod)
|
||||||
- [The base game as a mod](#the-base-game-as-a-mod)
|
- [The engine and the base game](#the-engine-and-the-base-game)
|
||||||
- [Implementation plan](#implementation-plan)
|
- [Implementation plan](#implementation-plan)
|
||||||
- [Open questions](#open-questions)
|
- [Open questions](#open-questions)
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
A mod can have up to three scripts, each running in a different place:
|
A mod is built from up to three layers. Most mods only need the first one or two.
|
||||||
|
|
||||||
|
| Layer | Like | What a mod writes | Stability |
|
||||||
|
| --------------------------------- | --------------------------------- | --------------------------------------------------------------------------------- | -------------------------------------------------- |
|
||||||
|
| **Data** | Bedrock behavior packs | JSON in `blocks/`, `items/`, `models/`, `recipes/`, `overrides/`, `worldgen/`, and textures | Versioned by `format_version` |
|
||||||
|
| **Game API** | Minetest's Lua API, Bedrock's Script API | TypeScript against `bworld/common`, `bworld/server`, `bworld/client` and `bworld/worldgen` | Stable, only breaks with the game's major version |
|
||||||
|
| **[Engine access](#engine-access)** | Fabric's mixins | `import { engine, hook } from "bworld/engine"`, opted into in the manifest | None, tied to one game version |
|
||||||
|
|
||||||
|
Data is shorthand for the Game API: loading `blocks/copper_block.json` does exactly what `ctx.blocks.register(...)` with
|
||||||
|
the same object does in a common script. So anything a JSON file can do, code can do too, for content that's easier to
|
||||||
|
generate (every color of wool) or that depends on other mods.
|
||||||
|
|
||||||
|
A mod can have up to four scripts, each running in a different place:
|
||||||
|
|
||||||
| Script | Runs on | Sent to clients | Owns |
|
| Script | Runs on | Sent to clients | Owns |
|
||||||
| ---------- | ------------------------------- | --------------- | ------------------------------------------------------------------------------ |
|
| ---------- | ---------------------------------------------- | --------------- | ------------------------------------------------------------------------------------ |
|
||||||
|
| `common` | the server and every client, before the others | yes | content: registering blocks, items, models and recipes, and changing other mods' content |
|
||||||
| `server` | the server, in a worker | **never** | game logic: block components, ticking, tile data, containers, commands, events |
|
| `server` | the server, in a worker | **never** | game logic: block components, ticking, tile data, containers, commands, events |
|
||||||
| `client` | every client, main thread | yes | presentation: custom screens, HUD, keybinds |
|
| `client` | every client, main thread | yes | presentation: custom screens, HUD, keybinds |
|
||||||
| `worldgen` | server and client chunk workers | yes | terrain features, must be deterministic |
|
| `worldgen` | server and client chunk workers | yes | terrain and features, must be deterministic |
|
||||||
|
|
||||||
The mod's JSON data and textures go to both sides.
|
|
||||||
|
|
||||||
**The server is the authority.** Clients send what the player is trying to do ("break the block at x, y, z", "click slot
|
**The server is the authority.** Clients send what the player is trying to do ("break the block at x, y, z", "click slot
|
||||||
3"). The server checks it, runs mod logic and sends back what happened. For responsiveness, clients show the expected
|
3"). The server checks it, runs mod logic and sends back what happened. For responsiveness, clients show the expected
|
||||||
@@ -55,17 +69,33 @@ result of breaking and placing immediately, and the server sends a correction if
|
|||||||
server process browser
|
server process browser
|
||||||
┌───────────────────────────────────────┐ ┌──────────────────────────────────┐
|
┌───────────────────────────────────────┐ ┌──────────────────────────────────┐
|
||||||
│ host: http, websocket, files │ │ game client (rendering, input) │
|
│ host: http, websocket, files │ │ game client (rendering, input) │
|
||||||
│ ┌───────────────────────────────────┐ │ ws │ + mod client scripts │
|
│ ┌───────────────────────────────────┐ │ ws │ + mod common and client │
|
||||||
│ │ game server worker │◄├──────────┤► (screens, HUD, keybinds) │
|
│ │ game server worker │◄├──────────┤► scripts │
|
||||||
│ │ world, inventories, tile data │ │ protocol │ │
|
│ │ world, inventories, tile data │ │ protocol │ │
|
||||||
│ │ + mod server scripts │ │ │ chunk workers │
|
│ │ + mod common and server scripts │ │ │ chunk workers │
|
||||||
│ └───────────────────────────────────┘ │ │ + mod worldgen scripts │
|
│ └───────────────────────────────────┘ │ │ + mod worldgen scripts │
|
||||||
│ chunk workers + mod worldgen scripts │ └──────────────────────────────────┘
|
│ chunk workers + mod worldgen scripts │ └──────────────────────────────────┘
|
||||||
└───────────────────────────────────────┘
|
└───────────────────────────────────────┘
|
||||||
```
|
```
|
||||||
|
|
||||||
Moving from the current setup to this is a big change, see the [Implementation plan](#implementation-plan). Right now
|
## Loading
|
||||||
the server only relays block changes and doesn't know about blocks, inventories or terrain.
|
|
||||||
|
The server and every client load mods the same way, in the same order: dependencies first, otherwise alphabetical. The
|
||||||
|
server sends its order to clients, and they follow it.
|
||||||
|
|
||||||
|
1. **Content**, one mod at a time: the mod's JSON data is registered, then its `common` script's `setup` runs. So a
|
||||||
|
common script sees its own data and everything from the mods before it, and can change it.
|
||||||
|
2. **Freeze.** The registries close: blocks get their numeric ids, block items are created and recipes are indexed.
|
||||||
|
Registering or changing content after this throws.
|
||||||
|
3. **Scripts.** `server` scripts' `setup` runs on the server, `client` scripts' on each client, and `worldgen` scripts in
|
||||||
|
every chunk worker. These can read every registry but not change them.
|
||||||
|
4. The server starts ticking, or the client sends `ready` and joins.
|
||||||
|
|
||||||
|
**Content has to come out the same on every side.** Common scripts run on the server and on every client, and all of
|
||||||
|
them must end up with exactly the same blocks, items and recipes: no `Math.random`, dates, or anything that differs
|
||||||
|
between the server and a client. After the freeze each side hashes its registries (every id and its definition), and
|
||||||
|
the client sends the hash with `ready`. When it doesn't match the server's, the player is turned away with a message
|
||||||
|
naming the first thing that differs, instead of playing with mismatched blocks. _Planned, phase 1._
|
||||||
|
|
||||||
## Creating a mod
|
## Creating a mod
|
||||||
|
|
||||||
@@ -74,14 +104,13 @@ deno task new-mod copper_tools "Copper Tools" # copies templates/mod to mods/c
|
|||||||
deno task check-mods # validates every mod in mods/
|
deno task check-mods # validates every mod in mods/
|
||||||
```
|
```
|
||||||
|
|
||||||
The template has one of everything: a block with a custom component, an item, a shaped recipe, textures, and all three
|
The template has one of everything: a block with a custom component, an item, a shaped recipe, textures, and a script
|
||||||
scripts. `check-mods` checks manifests and data files against this spec, checks that every id, item and texture a mod
|
for each side. `check-mods` checks manifests and data files against this spec, checks that every id, item and texture a
|
||||||
refers to exists (and that it depends on the mods those come from), and typechecks scripts against the API types in
|
mod refers to exists (and that it depends on the mods those come from), and typechecks scripts against the API types in
|
||||||
`common/mod_api/`, which scripts import as `bworld/server`, `bworld/client` and `bworld/worldgen`. Folders in `mods/`
|
`common/mod_api/`, which scripts import as `bworld/common`, `bworld/server`, `bworld/client`, `bworld/worldgen` and,
|
||||||
starting with `_` or `.` are ignored.
|
with engine access, `bworld/engine`. Folders in `mods/` starting with `_` or `.` are ignored.
|
||||||
|
|
||||||
`deno task build` builds every mod in `mods/` and fails if any has errors; `deno task server` then loads them. What
|
`deno task build` builds every mod in `mods/` and fails if any has errors; `deno task server` then loads them.
|
||||||
works so far is listed in the [Implementation plan](#implementation-plan).
|
|
||||||
|
|
||||||
## Mod layout
|
## Mod layout
|
||||||
|
|
||||||
@@ -93,13 +122,15 @@ mods/
|
|||||||
models/*.json
|
models/*.json
|
||||||
items/*.json
|
items/*.json
|
||||||
recipes/*.json
|
recipes/*.json
|
||||||
|
overrides/*.json # changes to other mods' content
|
||||||
worldgen/ores.json
|
worldgen/ores.json
|
||||||
textures/*.png
|
textures/*.png
|
||||||
scripts/
|
scripts/
|
||||||
|
common.ts # content, runs on both sides
|
||||||
server.ts # game logic, stays on the server
|
server.ts # game logic, stays on the server
|
||||||
client.ts # sent to players
|
client.ts # sent to players
|
||||||
worldgen.ts # sent to players, also runs on the server
|
worldgen.ts # sent to players, also runs on the server
|
||||||
shared/ # anything both sides import, bundled into each
|
shared/ # anything several scripts import, bundled into each
|
||||||
```
|
```
|
||||||
|
|
||||||
Only `manifest.json` is required. Data folders can have subfolders, with one definition per file.
|
Only `manifest.json` is required. Data folders can have subfolders, with one definition per file.
|
||||||
@@ -116,9 +147,11 @@ Only `manifest.json` is required. Data folders can have subfolders, with one def
|
|||||||
"authors": ["paula"],
|
"authors": ["paula"],
|
||||||
"game_version": ">=0.1.0",
|
"game_version": ">=0.1.0",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
|
{ "id": "bworld", "version": "*" },
|
||||||
{ "id": "more_ores", "version": "^2.0.0" }
|
{ "id": "more_ores", "version": "^2.0.0" }
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"common": "scripts/common.ts",
|
||||||
"server": "scripts/server.ts",
|
"server": "scripts/server.ts",
|
||||||
"client": "scripts/client.ts",
|
"client": "scripts/client.ts",
|
||||||
"worldgen": "scripts/worldgen.ts"
|
"worldgen": "scripts/worldgen.ts"
|
||||||
@@ -134,16 +167,18 @@ Only `manifest.json` is required. Data folders can have subfolders, with one def
|
|||||||
| `description` | no | One or two sentences. |
|
| `description` | no | One or two sentences. |
|
||||||
| `version` | yes | Semver of the mod itself. |
|
| `version` | yes | Semver of the mod itself. |
|
||||||
| `authors` | no | List of names. |
|
| `authors` | no | List of names. |
|
||||||
| `game_version` | no | Semver range of bworld versions the mod works with. |
|
| `game_version` | no | Semver range of bworld versions the mod works with. Mods with `engine_access` need one. |
|
||||||
| `dependencies` | no | Other mods by `id`, with a semver range. Missing or mismatched dependencies fail loading. |
|
| `dependencies` | no | Other mods by `id`, with a semver range. Missing or mismatched dependencies fail loading. A mod can only change content of mods it depends on. |
|
||||||
|
| `scripts.common` | no | Content entry, run on the server and sent to every player. _Planned, phase 1._ |
|
||||||
| `scripts.server` | no | Server entry. Never sent to clients. |
|
| `scripts.server` | no | Server entry. Never sent to clients. |
|
||||||
| `scripts.client` | no | Client entry, sent to every player. |
|
| `scripts.client` | no | Client entry, sent to every player. |
|
||||||
| `scripts.worldgen` | no | Worldgen entry, sent to every player and also run on the server. |
|
| `scripts.worldgen` | no | Worldgen entry, sent to every player and also run on the server. |
|
||||||
|
| `engine_access` | no | `true` lets the mod's scripts import `bworld/engine`, see [Engine access](#engine-access). _Planned, phase 3._ |
|
||||||
| `credits` | no | A markdown file in the mod, shown on the Credits screen. For asset licenses and thanks. |
|
| `credits` | no | A markdown file in the mod, shown on the Credits screen. For asset licenses and thanks. |
|
||||||
|
|
||||||
Scripts can be `.js` or `.ts`. The build bundles each entry separately into one ES module. Code imported by both the
|
Scripts can be `.js` or `.ts`. The build bundles each entry separately into one ES module. Code imported by several
|
||||||
server and client entries is copied into both bundles, so **don't import secrets into shared code**. Anything the client
|
entries is copied into each bundle, so **don't import secrets into shared code**. Anything the common, client or worldgen
|
||||||
bundle imports is visible to players.
|
bundles import is visible to players.
|
||||||
|
|
||||||
## Identifiers
|
## Identifiers
|
||||||
|
|
||||||
@@ -151,13 +186,13 @@ Everything a mod registers has an id of the form `namespace:name`:
|
|||||||
|
|
||||||
- `namespace` is the mod's `id`. It must match `^[a-z0-9_]+$` and be at most 32 characters.
|
- `namespace` is the mod's `id`. It must match `^[a-z0-9_]+$` and be at most 32 characters.
|
||||||
- Two namespaces are reserved. `bworld` belongs to the base game mod shipped in `mods/bworld` (see
|
- Two namespaces are reserved. `bworld` belongs to the base game mod shipped in `mods/bworld` (see
|
||||||
[The base game as a mod](#the-base-game-as-a-mod)). `engine` belongs to the engine itself, for things every game
|
[The engine and the base game](#the-engine-and-the-base-game)). `engine` belongs to the engine itself, for things
|
||||||
needs, like the block-breaking cracks. The one exception is `bworld:air`: it's the engine's empty block, but it keeps
|
every game needs, like the block-breaking cracks and the built-in block models. The one exception is `bworld:air`:
|
||||||
that id because every save already contains it.
|
it's the engine's empty block, but it keeps that id because every save already contains it.
|
||||||
- `name` must match `^[a-z0-9_]+$`.
|
- `name` must match `^[a-z0-9_]+$`.
|
||||||
- A mod only registers ids in its own namespace: blocks, items, components, screens, channels and HUD elements. It can
|
- A mod only registers ids in its own namespace: blocks, items, models, components, recipe types, screens, channels and
|
||||||
_refer to_ any id.
|
HUD elements. It can _refer to_ any id, and [change](#overrides) other mods' content.
|
||||||
- Registering an id that already exists is a load error.
|
- Registering an id that already exists is a load error. Changing an existing one is what overrides are for.
|
||||||
|
|
||||||
Blocks get a numeric id when they register, and chunk data stores those numbers. Numeric ids are **local to one running
|
Blocks get a numeric id when they register, and chunk data stores those numbers. Numeric ids are **local to one running
|
||||||
program**, so the server and each client can number blocks differently. Saves and the network always use string ids.
|
program**, so the server and each client can number blocks differently. Saves and the network always use string ids.
|
||||||
@@ -213,7 +248,7 @@ program**, so the server and each client can number blocks differently. Saves an
|
|||||||
| `interactive` | `false` | `interactive` | Right clicking it does something (opens a screen), so clients don't guess it places a block. |
|
| `interactive` | `false` | `interactive` | Right clicking it does something (opens a screen), so clients don't guess it places a block. |
|
||||||
| `replaceable` | `false` | new | Placing a block into it replaces it, like water. |
|
| `replaceable` | `false` | new | Placing a block into it replaces it, like water. |
|
||||||
| `states` | none | `states` | Up to 16 bits of named state, like `hoed_dirt` has now. |
|
| `states` | none | `states` | Up to 16 bits of named state, like `hoed_dirt` has now. |
|
||||||
| `components` | none | the `on_*` hooks | Custom components with parameters, handled by server scripts. |
|
| `components` | none | `components` | Custom components with parameters, handled by server scripts. |
|
||||||
|
|
||||||
The client needs this data too (for meshing, mining time and collision), so it's sent to every player. It can't contain
|
The client needs this data too (for meshing, mining time and collision), so it's sent to every player. It can't contain
|
||||||
functions.
|
functions.
|
||||||
@@ -348,7 +383,7 @@ clients as part of the container it's in, so client screens can show it.
|
|||||||
|
|
||||||
| `type` | Fields | Notes |
|
| `type` | Fields | Notes |
|
||||||
| --------- | ---------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- |
|
| --------- | ---------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- |
|
||||||
| `shaped` | `pattern` rows (space is empty), `key` letter to item id, `result` `{ id, count }` | Fits anywhere in the 3×3 grid, like the recipes in `server/game/crafting.ts` |
|
| `shaped` | `pattern` rows (space is empty), `key` letter to item id, `result` `{ id, count }` | Used by the crafting grid, fits anywhere in it |
|
||||||
| `furnace` | `input` item id, `output` `{ id, count }`, `cook_time` in ticks | Used by the furnace |
|
| `furnace` | `input` item id, `output` `{ id, count }`, `cook_time` in ticks | Used by the furnace |
|
||||||
| `fuel` | `item` id, `burn_time` in ticks | Used by the furnace |
|
| `fuel` | `item` id, `burn_time` in ticks | Used by the furnace |
|
||||||
| `smithing` | `tool` item id, `material` `{ id, count }`, optional `addition` item id, `result` item id | Used by the smithing table: upgrades the tool, keeping its data. Uses up the tool, `count` of the material and the addition |
|
| `smithing` | `tool` item id, `material` `{ id, count }`, optional `addition` item id, `result` item id | Used by the smithing table: upgrades the tool, keeping its data. Uses up the tool, `count` of the material and the addition |
|
||||||
@@ -382,8 +417,128 @@ A shaped recipe, for the crafting grid in the player's inventory screen:
|
|||||||
```
|
```
|
||||||
|
|
||||||
Two furnace recipes with the same `input`, two smithing recipes with the same tool, material and addition, or two
|
Two furnace recipes with the same `input`, two smithing recipes with the same tool, material and addition, or two
|
||||||
shaped recipes with the same pattern and key, are a load error. Server
|
shaped recipes with the same pattern and key, are a load error. Mods can add recipe types of their own for their
|
||||||
scripts can also register recipe types of their own through `ctx.recipes`.
|
machines, see [Common scripts](#common-scripts).
|
||||||
|
|
||||||
|
## Overrides
|
||||||
|
|
||||||
|
A mod can change content other mods registered, like Minetest's `override_item`. The JSON form covers the common cases,
|
||||||
|
and a [common script](#common-scripts) can do the same with `override` and `extend`. _Planned, phase 2._
|
||||||
|
|
||||||
|
`overrides/grass_weeds.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"format_version": 1,
|
||||||
|
"override": {
|
||||||
|
"block": "bworld:grass",
|
||||||
|
"set": { "mining": { "toughness": 1, "tool": "shovel" } },
|
||||||
|
"add_components": { "farming_plus:weeds": { "chance": 0.01 } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Field | Meaning |
|
||||||
|
| ------------------- | ---------------------------------------------------------------------------------------------------- |
|
||||||
|
| `block` or `item` | The id to change. |
|
||||||
|
| `set` | Fields to replace, with the same names as in the block or item JSON. Each listed field is replaced whole, so `mining` above replaces all of the old `mining`. |
|
||||||
|
| `add_components` | Components to add. One the block or item already has gets these params instead. |
|
||||||
|
| `remove_components` | Component ids to take away. |
|
||||||
|
|
||||||
|
A recipe override removes recipes, so a mod can replace them with its own:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"format_version": 1,
|
||||||
|
"override": { "remove_recipes": { "type": "shaped", "result": "bworld:chest" } }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`remove_recipes` matches recipes whose fields equal the ones given (`result` matches the result's id).
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
|
||||||
|
- A mod can only change content of mods it lists in `dependencies`, so it always loads after them. `check-mods` makes
|
||||||
|
this an error.
|
||||||
|
- Changes apply in load order, so when two mods change the same field the later one wins. `check-mods` warns about it.
|
||||||
|
- Ids never change, and blocks and items can't be removed, because saves refer to them. To make something unobtainable,
|
||||||
|
remove its recipes and drops.
|
||||||
|
- Overrides are content like everything else: they're applied on the server and every client, and are part of the
|
||||||
|
registry hash.
|
||||||
|
|
||||||
|
## Common scripts
|
||||||
|
|
||||||
|
`scripts.common` exports `setup`, called on the server and on every client while content loads, right after the mod's
|
||||||
|
own JSON is registered (see [Loading](#loading)). _Planned, phase 1._
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import type { CommonContext } from "bworld/common";
|
||||||
|
|
||||||
|
const COLORS = ["white", "red", "green", "blue", "black"];
|
||||||
|
|
||||||
|
export function setup(ctx: CommonContext) {
|
||||||
|
for (const color of COLORS) {
|
||||||
|
ctx.blocks.register({
|
||||||
|
id: `wool:${color}_wool`,
|
||||||
|
textures: `wool:${color}_wool`,
|
||||||
|
mining: { toughness: 1 },
|
||||||
|
drops: `wool:${color}_wool`,
|
||||||
|
});
|
||||||
|
ctx.recipes.register({
|
||||||
|
type: "shaped",
|
||||||
|
pattern: ["D", "W"],
|
||||||
|
key: { D: `wool:${color}_dye`, W: "wool:white_wool" },
|
||||||
|
result: { id: `wool:${color}_wool`, count: 1 },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// wool burns in the base game's furnace
|
||||||
|
for (const color of COLORS) {
|
||||||
|
ctx.recipes.register({ type: "fuel", item: `wool:${color}_wool`, burn_time: 100 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// every leaf block any mod registered drops sticks sometimes
|
||||||
|
for (const id of ctx.blocks.ids().filter((id) => id.endsWith("_leaves"))) {
|
||||||
|
ctx.blocks.extend(id, { components: { "wool:stick_drop": { chance: 0.05 } } });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```ts
|
||||||
|
interface CommonContext {
|
||||||
|
mod: { id: string; version: string };
|
||||||
|
blocks: ContentRegistry<BlockJson>;
|
||||||
|
items: ContentRegistry<ItemJson>;
|
||||||
|
models: ContentRegistry<ModelJson>;
|
||||||
|
recipes: RecipeRegistry;
|
||||||
|
log(...args: unknown[]): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
// T is the same shape as the JSON file's content, checked the same way
|
||||||
|
interface ContentRegistry<T> {
|
||||||
|
register(definition: T): void;
|
||||||
|
get(id: string): Readonly<T> | undefined; // what's registered so far, by any mod
|
||||||
|
ids(): string[];
|
||||||
|
override(id: string, fields: Partial<T>): void; // like "set" in an override file
|
||||||
|
extend(id: string, change: { components?: Record<string, unknown>; remove_components?: string[] }): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RecipeRegistry {
|
||||||
|
register(recipe: RecipeJson): void;
|
||||||
|
remove(match: (recipe: RecipeJson) => boolean): number; // how many it removed
|
||||||
|
list(type: string): readonly RecipeJson[];
|
||||||
|
// a new recipe type: its recipes are checked with validate, and scripts read them with list
|
||||||
|
register_type(id: string, type: { validate(recipe: unknown): string[] }): void;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- Definitions are checked exactly like JSON files, and a problem is a load error naming the mod.
|
||||||
|
- Common scripts only run during loading. They can't keep state for later, and can't reach the world, players or
|
||||||
|
anything else: that's what server and client scripts are for. They get the same `ctx` shape on both sides on
|
||||||
|
purpose, so they can't accidentally register different things.
|
||||||
|
- Recipe types are ids like everything else. `shaped`, `furnace`, `fuel` and `smithing` are short for `bworld:shaped`
|
||||||
|
and so on, since the base game's crafting grid, furnace and smithing table use them. A mod's machine registers its own
|
||||||
|
type (`copper_tools:alloy`) and its server script reads the recipes with `ctx.recipes.list("copper_tools:alloy")`.
|
||||||
|
|
||||||
## Server scripts
|
## Server scripts
|
||||||
|
|
||||||
@@ -400,6 +555,9 @@ export function setup(ctx: ServerContext) {
|
|||||||
Server scripts run in the game server worker with **no Deno permissions** (see [Security](#security)). They get the mod
|
Server scripts run in the game server worker with **no Deno permissions** (see [Security](#security)). They get the mod
|
||||||
API and standard JavaScript, but no file, network or subprocess access; persistent state goes through `ctx.storage`.
|
API and standard JavaScript, but no file, network or subprocess access; persistent state goes through `ctx.storage`.
|
||||||
|
|
||||||
|
`ctx` is the Game API for the server. Content is already registered and frozen by now (see [Loading](#loading)), so
|
||||||
|
server scripts read registries but don't change them.
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
interface ServerContext {
|
interface ServerContext {
|
||||||
mod: { id: string; version: string };
|
mod: { id: string; version: string };
|
||||||
@@ -557,7 +715,7 @@ ctx.commands.register("heal", {
|
|||||||
```
|
```
|
||||||
|
|
||||||
Commands run on the server when a player types `/name` in chat. Two mods using the same name, or a mod using one of the
|
Commands run on the server when a player types `/name` in chat. Two mods using the same name, or a mod using one of the
|
||||||
engine's (`give`, `time` and `tps`), is a load error. `/copper_tools:heal` always works as the unambiguous form.
|
engine's (`give`, `time` and `tps`, until phase 5 moves `give` and `time` into `mods/bworld`), is a load error. `/copper_tools:heal` always works as the unambiguous form.
|
||||||
|
|
||||||
## Client scripts
|
## Client scripts
|
||||||
|
|
||||||
@@ -584,6 +742,8 @@ interface ClientContext {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Only `ctx.player`, `ctx.world` and `ctx.log` work so far, the rest is phase 4.
|
||||||
|
|
||||||
Client scripts are for presentation. They can't change the world or inventories; they ask the server over a
|
Client scripts are for presentation. They can't change the world or inventories; they ask the server over a
|
||||||
[mod channel](#mod-channels). Anything they show should be treated as a view of the server's state. Everything in a
|
[mod channel](#mod-channels). Anything they show should be treated as a view of the server's state. Everything in a
|
||||||
client script is visible to players and can be changed by them, so it must not be trusted for anything that matters.
|
client script is visible to players and can be changed by them, so it must not be trusted for anything that matters.
|
||||||
@@ -597,6 +757,8 @@ There are three ways to show a GUI. They're listed from least code to most. Pick
|
|||||||
|
|
||||||
### 1. Forms (server only)
|
### 1. Forms (server only)
|
||||||
|
|
||||||
|
_Planned, phase 4._
|
||||||
|
|
||||||
Simple dialogs defined entirely by server code, like Bedrock's `@minecraft/server-ui`. The client draws them with the
|
Simple dialogs defined entirely by server code, like Bedrock's `@minecraft/server-ui`. The client draws them with the
|
||||||
game's UI style. No client script is needed.
|
game's UI style. No client script is needed.
|
||||||
|
|
||||||
@@ -716,6 +878,8 @@ Items handed to scripts are live: setting `count` changes the stack, and a stack
|
|||||||
|
|
||||||
### 3. Custom screens (client code)
|
### 3. Custom screens (client code)
|
||||||
|
|
||||||
|
_Planned, phase 4._
|
||||||
|
|
||||||
For anything forms and containers can't do: maps, skill trees, minigames, custom layouts. The client script registers a
|
For anything forms and containers can't do: maps, skill trees, minigames, custom layouts. The client script registers a
|
||||||
screen class, and the server opens it with some props:
|
screen class, and the server opens it with some props:
|
||||||
|
|
||||||
@@ -798,6 +962,8 @@ doesn't involve the server.
|
|||||||
|
|
||||||
### HUD
|
### HUD
|
||||||
|
|
||||||
|
_Planned, phase 4._
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
ctx.hud.register("copper_tools:heat", {
|
ctx.hud.register("copper_tools:heat", {
|
||||||
on_render(g) {
|
on_render(g) {
|
||||||
@@ -810,7 +976,7 @@ HUD elements draw every frame after the world, under open screens. They get thei
|
|||||||
|
|
||||||
## Mod channels
|
## Mod channels
|
||||||
|
|
||||||
Client and server parts of a mod talk over named channels. Messages are JSON.
|
_Planned, phase 4._ Client and server parts of a mod talk over named channels. Messages are JSON.
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
// server.ts
|
// server.ts
|
||||||
@@ -848,7 +1014,7 @@ Each chunk is generated in three passes:
|
|||||||
3. **Features.** Every mod's registered features, in load order.
|
3. **Features.** Every mod's registered features, in load order.
|
||||||
|
|
||||||
The world is 256 blocks tall and the sea is at y 64 (`CHUNK_HEIGHT` and `SEA_LEVEL` in `common/constants.ts`). The
|
The world is 256 blocks tall and the sea is at y 64 (`CHUNK_HEIGHT` and `SEA_LEVEL` in `common/constants.ts`). The
|
||||||
base overworld lives in `common/worldgen/` until phase 3 moves it into `mods/bworld`. It works like Minecraft 1.18+ and
|
base overworld lives in `common/worldgen/` until phase 6 moves it into `mods/bworld`. It works like Minecraft 1.18+ and
|
||||||
the Terralith datapack: continentalness, erosion and weirdness noises feed nested splines that give every column a
|
the Terralith datapack: continentalness, erosion and weirdness noises feed nested splines that give every column a
|
||||||
height, jaggedness and roughness, a 3D density around that height is sampled on a coarse grid and interpolated, and
|
height, jaggedness and roughness, a 3D density around that height is sampled on a coarse grid and interpolated, and
|
||||||
caves are cut out of it. On top of that come Terralith-style shapes: terraced plateaus, shattered hills, river valleys
|
caves are cut out of it. On top of that come Terralith-style shapes: terraced plateaus, shattered hills, river valleys
|
||||||
@@ -859,7 +1025,8 @@ how many grow and which kinds. `deno run -A tools/worldgen_preview.ts [seed]` re
|
|||||||
|
|
||||||
### Terrain generators
|
### Terrain generators
|
||||||
|
|
||||||
A world uses exactly one terrain generator, registered by a worldgen script. The base game's is `bworld:overworld`.
|
A world uses exactly one terrain generator, registered by a worldgen script. The base game's will be `bworld:overworld`
|
||||||
|
(phase 6); until then `register_terrain` throws and the engine's own overworld is used.
|
||||||
Which one a world uses is saved with the world. A new world uses the only one installed, or the server's `TERRAIN`
|
Which one a world uses is saved with the world. A new world uses the only one installed, or the server's `TERRAIN`
|
||||||
setting when there are several. A server with no terrain generator refuses to start.
|
setting when there are several. A server with no terrain generator refuses to start.
|
||||||
|
|
||||||
@@ -954,6 +1121,111 @@ interface FeatureChunk {
|
|||||||
- Each feature's `rng` is seeded with its own id, so adding or removing another mod's feature doesn't change what this
|
- Each feature's `rng` is seeded with its own id, so adding or removing another mod's feature doesn't change what this
|
||||||
one generates.
|
one generates.
|
||||||
|
|
||||||
|
## Engine access
|
||||||
|
|
||||||
|
For what the Game API doesn't cover yet: a new kind of rendering, different player physics, a change to how lighting
|
||||||
|
spreads, an entity type. Like Fabric's mixins, a mod can reach the engine's own classes and hook their methods, on the
|
||||||
|
server and on the client. It's powerful and unstable at once, so a mod has to ask for it. _Planned, phase 3._
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "engine_access": true, "game_version": "~0.4.0" }
|
||||||
|
```
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// client.ts: an outline around every dropped item
|
||||||
|
import type { ClientContext } from "bworld/client";
|
||||||
|
import { engine, hook } from "bworld/engine";
|
||||||
|
|
||||||
|
export function setup(ctx: ClientContext) {
|
||||||
|
hook(engine.client.LevelRenderer.prototype, "render_entities", {
|
||||||
|
after(renderer, [level, camera_entity, partial_tick]) {
|
||||||
|
for (const entity of level.entities.values()) {
|
||||||
|
if (entity instanceof engine.client.ItemEntity) draw_outline(entity, partial_tick);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// server.ts: broken blocks drop twice
|
||||||
|
import { engine, hook } from "bworld/engine";
|
||||||
|
|
||||||
|
export function setup() {
|
||||||
|
hook(engine.server.GameServer.prototype, "pop_item", {
|
||||||
|
after(game, [x, y, z, stack], entity) {
|
||||||
|
game.spawn_item(entity.x, entity.y, entity.z, stack.clone());
|
||||||
|
return entity;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// client.ts: everything falls at half speed. physics is an object so its functions can be hooked, see below
|
||||||
|
import { engine, hook } from "bworld/engine";
|
||||||
|
|
||||||
|
export function setup() {
|
||||||
|
hook(engine.common.physics, "move_body", {
|
||||||
|
before(_physics, args) {
|
||||||
|
args[1] /= 2; // gravity
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### What `bworld/engine` gives
|
||||||
|
|
||||||
|
- `engine` holds the engine's modules as they run on that side: `engine.common` (registries, constants, inventory,
|
||||||
|
physics, generation, the protocol types) everywhere, `engine.server` (`GameServer`, `ServerWorld`, `ModRuntime`, the
|
||||||
|
running `game`) in server scripts, and `engine.client` (`Client`, `ClientLevel`, the renderer, GUI classes, the running
|
||||||
|
`client`) in client scripts. Worldgen scripts get `engine.common` only.
|
||||||
|
- They're the live classes and objects the game uses, not copies: the engine puts them on the global object at startup,
|
||||||
|
and `bworld/engine` in a mod's bundle is a small shim that reads them. So a hook on a prototype changes every instance
|
||||||
|
the engine already made.
|
||||||
|
- The types are the engine's own source, so `check-mods` typechecks a mod against exactly the engine it will run on, and
|
||||||
|
an engine change that breaks a mod shows up there first.
|
||||||
|
|
||||||
|
### Hooks
|
||||||
|
|
||||||
|
```ts
|
||||||
|
function hook<T, K extends keyof T>(target: T, method: K, handlers: {
|
||||||
|
// runs first. change args in place, or return { result } to skip the method and everything after
|
||||||
|
before?(self: T, args: Parameters<T[K]>): { result: ReturnType<T[K]> } | void;
|
||||||
|
// wraps the method, calling original (or not)
|
||||||
|
replace?(self: T, args: Parameters<T[K]>, original: T[K]): ReturnType<T[K]>;
|
||||||
|
// runs last, and returns the result the caller gets
|
||||||
|
after?(self: T, args: Parameters<T[K]>, result: ReturnType<T[K]>): ReturnType<T[K]>;
|
||||||
|
// lower runs earlier, the default is 1000. ties go in load order
|
||||||
|
priority?: number;
|
||||||
|
}): () => void; // unhook
|
||||||
|
```
|
||||||
|
|
||||||
|
- Hooks on a prototype apply to every instance, hooks on one object only to it.
|
||||||
|
- Several mods can hook the same method. `before` hooks run in priority order, `replace` hooks nest (the first one's
|
||||||
|
`original` is the next one's wrapper, the last one's is the engine's method), and `after` hooks run in priority order,
|
||||||
|
each getting the previous one's result.
|
||||||
|
- A hook that throws is logged under its mod and skipped, like event handlers. The method still runs.
|
||||||
|
- Functions exported from a module can't be hooked, because a module's exports can't be reassigned. The engine puts
|
||||||
|
functions mods may want to change on plain objects (like `engine.common.physics`) and calls them through those objects
|
||||||
|
itself, so a hook changes what the engine calls. Classes' methods are hookable on their prototype.
|
||||||
|
- JavaScript's `#private` members can't be hooked or read. The engine keeps things that mods should be able to change
|
||||||
|
as normal members, and `#private` for things with rules that must hold (like a container's slots). Making something
|
||||||
|
hookable is an ordinary engine change, and asking for one is how the Game API grows.
|
||||||
|
- Hooks reach one JavaScript realm: the game server worker for server scripts, the page for client scripts, and one
|
||||||
|
chunk worker for worldgen scripts. Each chunk worker runs its own copy of the engine, so a hook in a client script
|
||||||
|
doesn't change meshing; a worldgen script with engine access can.
|
||||||
|
|
||||||
|
### Rules
|
||||||
|
|
||||||
|
- **It isn't a stable API.** Any game update can rename or change what a hook targets. A mod with `engine_access` must
|
||||||
|
set `game_version` to a range within one minor version (like `~0.4.0`). Loading it on another version is a load error
|
||||||
|
naming the mod, instead of a crash somewhere later.
|
||||||
|
- **Prefer the Game API.** When it can do something, it keeps working across updates. Engine access is for what it
|
||||||
|
can't do yet, and a mod that hooks the same thing as many others is a sign it should become part of the Game API.
|
||||||
|
- Without `engine_access`, importing `bworld/engine` is a `check-mods` error and a load error.
|
||||||
|
- See [Security](#security) for what it means for players and servers.
|
||||||
|
|
||||||
## Delivery to clients
|
## Delivery to clients
|
||||||
|
|
||||||
### Build
|
### Build
|
||||||
@@ -963,7 +1235,8 @@ interface FeatureChunk {
|
|||||||
```
|
```
|
||||||
build/mods/<id>/<hash>/ # public, served to players
|
build/mods/<id>/<hash>/ # public, served to players
|
||||||
manifest.json
|
manifest.json
|
||||||
data.json # all blocks, items, recipes and ores merged
|
data.json # all blocks, items, models, recipes, overrides and ores merged
|
||||||
|
common.js
|
||||||
client.js
|
client.js
|
||||||
worldgen.js
|
worldgen.js
|
||||||
server_mods/<id>/<hash>/ # private, outside the static root, never served
|
server_mods/<id>/<hash>/ # private, outside the static root, never served
|
||||||
@@ -986,11 +1259,11 @@ client server
|
|||||||
│ │
|
│ │
|
||||||
│ [cross-origin: confirm screen] │
|
│ [cross-origin: confirm screen] │
|
||||||
│ download the atlas and every mod file, │
|
│ download the atlas and every mod file, │
|
||||||
│ check each one's sha256, register data │
|
│ check each one's sha256, load content │
|
||||||
│ in the listed order, run client │
|
│ in the listed order, run client │
|
||||||
│ setup(), start chunk workers │
|
│ setup(), start chunk workers │
|
||||||
│ │
|
│ │
|
||||||
│ ready │
|
│ ready { registry_hash } │ a different hash gets rejected { reason }
|
||||||
├────────────────────────────────────────►│
|
├────────────────────────────────────────►│
|
||||||
│ join { id, name, players, entities, │ the player is created here, player_join fires,
|
│ join { id, name, players, entities, │ the player is created here, player_join fires,
|
||||||
│ changes, spawn, selected_slot } │ and their inventory follows as container messages
|
│ changes, spawn, selected_slot } │ and their inventory follows as container messages
|
||||||
@@ -1024,6 +1297,12 @@ where the page came from:
|
|||||||
headers on `build/mods/`.
|
headers on `build/mods/`.
|
||||||
- The hash check confirms the files are the ones the server listed. It doesn't protect against a malicious server.
|
- The hash check confirms the files are the ones the server listed. It doesn't protect against a malicious server.
|
||||||
|
|
||||||
|
**Engine access.** A server mod with engine access controls the game server worker completely, but the worker still
|
||||||
|
has no Deno permissions, so it can't reach files or the network any more than other server scripts. A client mod with
|
||||||
|
engine access changes nothing about what it _can_ reach, since client scripts already run with the page's full access,
|
||||||
|
but it can change how the game behaves for players in ways the Game API can't. The confirm screen for other servers
|
||||||
|
lists which mods use engine access.
|
||||||
|
|
||||||
**Server scripts.** They run in a worker with **no Deno permissions** (Deno worker permissions; currently needs
|
**Server scripts.** They run in a worker with **no Deno permissions** (Deno worker permissions; currently needs
|
||||||
`--unstable-worker-options`). They can't read files, open connections or run programs. Everything they need goes through
|
`--unstable-worker-options`). They can't read files, open connections or run programs. Everything they need goes through
|
||||||
the mod API. Installing a server mod still means trusting it with the game world and everything players send.
|
the mod API. Installing a server mod still means trusting it with the game world and everything players send.
|
||||||
@@ -1044,6 +1323,7 @@ A smelter block with a container screen, a stats screen written in client code,
|
|||||||
"id": "copper_tools",
|
"id": "copper_tools",
|
||||||
"name": "Copper Tools",
|
"name": "Copper Tools",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
|
"dependencies": [{ "id": "bworld", "version": "*" }],
|
||||||
"scripts": { "server": "scripts/server.ts", "client": "scripts/client.ts" }
|
"scripts": { "server": "scripts/server.ts", "client": "scripts/client.ts" }
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -1152,182 +1432,150 @@ class SmelterStats implements ModScreen {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## The base game as a mod
|
## The engine and the base game
|
||||||
|
|
||||||
Everything that makes bworld _bworld_ (its blocks, items, recipes, textures, block behavior and terrain) moves into a
|
Like a Minetest game, bworld is the engine plus one mod, `mods/bworld`, written against the same API as any other mod.
|
||||||
mod at `mods/bworld`, written against the same API as any other mod. The engine keeps only what every game built on it
|
This is how the API gets tested for real: if the base game can't be written as a mod, the API is missing something, and
|
||||||
needs.
|
other mods would hit the same wall. It also means there's one way content works, so mods can do anything the base game
|
||||||
|
does, and change any of it.
|
||||||
|
|
||||||
This is how the mod API gets tested for real: if the base game can't be written as a mod, the API is missing something,
|
**The engine** keeps what every game built on it needs: rendering, meshing, lighting and the sky; physics, player
|
||||||
and other mods would hit the same wall. It also means there's one way content works instead of two, so mods can do
|
movement and input; networking, saving, the game server and its loop; registries, the mod loader and the Game API;
|
||||||
anything the base game does.
|
containers, slot click rules, screens and the GUI toolkit; item entities; chat; world time; `/tps`; `bworld:air`; and
|
||||||
|
the engine assets in `assets/` (UI sprites, font, player sprite, and the `engine:` textures and block models).
|
||||||
|
|
||||||
### What moves
|
**`mods/bworld`** has everything else:
|
||||||
|
|
||||||
| Content | Now | In `mods/bworld` |
|
| What | Where it is now | Status |
|
||||||
| ------------------------------------ | ------------------------------ | --------------------------------------------------------------- |
|
| ---------------------------------------------------- | ----------------------------------- | ----------------------- |
|
||||||
| 18 blocks | `common/blocks/*.ts` | `blocks/*.json` |
|
| Blocks, items, models, recipes, textures, credits | `mods/bworld` JSON and `textures/` | Done |
|
||||||
| 11 items | `common/items/*.ts` | `items/*.json` |
|
| Hoeing, chest, furnace, smithing table, crops, watering can | `mods/bworld/scripts/server.ts` | Done |
|
||||||
| 62 textures | `assets/sprites/textures/` | `textures/` |
|
| Recipe types `shaped`, `furnace`, `fuel`, `smithing` | engine (`common/mod_loader.ts`, `server/game/crafting.ts`) | Phase 5 |
|
||||||
| 5 crafting recipes | `server/game/crafting.ts` | `recipes/*.json`, type `shaped` |
|
| The player's inventory screen and crafting grid | engine (`client/gui/gui_player_inventory.ts`, `server/game/crafting.ts`) | Phase 5 |
|
||||||
| 6 furnace recipes, 2 fuels | `server/game/blocks.ts` | `recipes/*.json`, types `furnace` and `fuel` |
|
| The hotbar and crosshair | engine (`client/gui/hud.ts`) | Phase 5 |
|
||||||
| Hoeing grass and dirt | `server/game/blocks.ts` | component `bworld:hoeable`, params `{ tool, into }` |
|
| `/give` and `/time` | engine (`server/game/game_server.ts`) | Phase 5 |
|
||||||
| Chest | `server/game/blocks.ts` | component `bworld:storage`, params `{ rows }` |
|
| Mining: tool speed, `requires_tool`, drops | engine (`client/game_mode.ts`, `server/game/game_server.ts`) | Phase 5 |
|
||||||
| Furnace (smelting, fuel, its screen) | `server/game/blocks.ts` | component `bworld:furnace` |
|
| Day and night lengths, sky colors | engine (`common/time.ts`, `client/rendering/game_renderer.ts`) | Phase 5 |
|
||||||
| Watering can's starting water | `common/items/watering_can.ts` | item component `bworld:watering_can`, params `{ max_water }` |
|
| Where new players spawn | engine (`server/game/world.ts`) | Phase 5 |
|
||||||
| Terrain and biomes | `common/worldgen/` | terrain generator `bworld:overworld` in `scripts/worldgen.ts` |
|
| Terrain, biomes, trees and ores | engine (`common/worldgen/`, `common/generation.ts`) | Phase 6 |
|
||||||
| Ore table (`BASE_ORES`) | `common/generation.ts` | `worldgen/ores.json`, unchanged |
|
|
||||||
| Texture credits (the Kenney packs) | `assets/ASSETS.md` | `CREDITS.md`, listed in the manifest's `credits` |
|
|
||||||
|
|
||||||
Not moved:
|
|
||||||
|
|
||||||
- **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
|
|
||||||
|
|
||||||
Rendering, physics and player controls; chunks, meshing and the generation passes (noise helpers, chunk assembly, ores,
|
|
||||||
features); networking, saving and the game server; registries and the mod loader; inventories, the crafting grid, slot
|
|
||||||
click rules and the generic container screen; chat and `/give`; `bworld:air`; and the engine assets in `assets/`: the UI
|
|
||||||
sprites, font, player sprite and the `engine:` textures (the breaking cracks and the missing texture).
|
|
||||||
|
|
||||||
### Rules that keep existing worlds working
|
### Rules that keep existing worlds working
|
||||||
|
|
||||||
Worlds saved before the move must load afterwards with nothing changed:
|
Worlds saved before any of this must load afterwards with nothing changed:
|
||||||
|
|
||||||
1. **Ids don't change.** Saves store blocks and items by string id, including player inventories and chest contents. The
|
1. **Ids don't change.** Saves store blocks and items by string id, including player inventories and chest contents.
|
||||||
JSON uses exactly the ids the TypeScript files register now.
|
|
||||||
2. **Terrain is identical, block for block.** A save only stores what players changed _on top of_ generated terrain. If
|
2. **Terrain is identical, block for block.** A save only stores what players changed _on top of_ generated terrain. If
|
||||||
the generator's output changes at all, every existing world silently changes with it: trees move and blocks players
|
the generator's output changes at all, every existing world silently changes with it: trees move and blocks players
|
||||||
broke reappear. The golden terrain test (phase 0) enforces this.
|
broke reappear. The golden terrain test (phase 6) enforces this.
|
||||||
3. **Tile data keeps its shape.** Chests store their items in `containers.main`, and furnaces keep `containers.main`
|
3. **Old saves are upgraded when loading.** The save format can change when it has to, like version 3 did for
|
||||||
plus `progress`, `progress_max`, `fuel` and `fuel_max` in `data`. The components read exactly those, so tiles saved
|
containers, as long as older saves load into the new shape. `tests/fixtures/` keeps a save from each version.
|
||||||
by the current code load into them.
|
4. **The protocol can change, with `PROTOCOL_VERSION`.** Clients on another version are asked to reload before anything
|
||||||
4. **The protocol doesn't change, and old saves load.** Clients receive `mods/bworld` like any other mod. The save
|
else happens.
|
||||||
format may change when it has to, as long as older saves are upgraded when loading (phase 2 did, to version 3).
|
|
||||||
|
|
||||||
### Phases
|
## Implementation plan
|
||||||
|
|
||||||
Each phase ends with every test passing and the game playable.
|
**Done so far:** the mod loader, build and delivery to clients (hashed downloads, the confirm screen for other servers,
|
||||||
|
credits); the data layer (blocks, block models with variants, items, recipes, ores); server scripts with components,
|
||||||
|
events, commands, timers, storage, recipes, containers and container screens; worldgen features and ores; the base
|
||||||
|
game's block behavior as a mod; world time with day and night. The code is in `common/mod_loader.ts`,
|
||||||
|
`common/mod_data.ts`, `server/game/mod_runtime.ts`, `client/mods.ts` and `common/worldgen_loader.ts`.
|
||||||
|
|
||||||
**Phase 0: safety net.** Do this first, before any other mod work. Started: `deno task test` runs `tests/`, which covers
|
Each phase ends with every test passing and the game playable. Phases 1 to 3 are the foundation; 4 to 6 can happen in
|
||||||
loading mods (the base game and the template), their scripts and worldgen, saves, and the mod tools.
|
any order after them.
|
||||||
|
|
||||||
- Move the test scripts used while building steps 1–3 into the repo as `deno test` files under `tests/`: server logic
|
**Phase 1: loading and common scripts.**
|
||||||
(breaking, placing, crafting, chest, furnace, saves), client and server terrain agreement, click prediction, and the
|
|
||||||
end-to-end WebSocket test.
|
|
||||||
- Record golden fixtures from the current code: `tests/fixtures/registry.json`, every block and item with all its fields
|
|
||||||
(functions left out), and `tests/fixtures/terrain.json`, a SHA-256 of the final blocks of 64 chunks for 3 seeds,
|
|
||||||
including negative coordinates and chunks with trees on their borders.
|
|
||||||
- Save a world from the current code with chests, a running furnace and some player inventories as
|
|
||||||
`tests/fixtures/world_v2.json`, with a test that loads it and checks everything is where it was.
|
|
||||||
|
|
||||||
**Phase 1: data** (after steps 4 and 5). _Done._
|
- `scripts.common`, bundled like the client script, run on the server and on clients between each mod's data and the
|
||||||
|
next mod.
|
||||||
|
- `CommonContext` with `ctx.blocks`, `ctx.items`, `ctx.models` and `ctx.recipes` (`register`, `get`, `ids`). The JSON
|
||||||
|
loader goes through the same functions, so there's one path for content.
|
||||||
|
- The freeze: registries close after content loads, and registering later throws. Numeric ids, block items and recipe
|
||||||
|
indexes are made at the freeze, not while registering.
|
||||||
|
- The registry hash: both sides hash their registries after the freeze, the client sends its hash with `ready`, and a
|
||||||
|
mismatch turns the player away naming the first difference.
|
||||||
|
- Recipe types become ids with `register_type` and `list`, with `shaped`, `furnace`, `fuel` and `smithing` as short
|
||||||
|
names for the base game's. Server scripts get `ctx.recipes.list(type)`.
|
||||||
|
- Check: a test mod registers blocks, items and recipes from code, and the server and a client end up with equal
|
||||||
|
registries and hashes. A common script that registers something different on each side is caught by the hash.
|
||||||
|
|
||||||
- Create `mods/bworld` with its manifest and credits, and move the 62 textures there. Rename the breaking cracks to
|
**Phase 2: overrides.**
|
||||||
`engine:break_0`–`8` and the fallback to `engine:missing`.
|
|
||||||
- Generate the block, item and recipe JSON with a script that reads the current registries, instead of writing it by
|
|
||||||
hand. Hand-copying 18 blocks' fields is how typos get in. They were generated this way, checked equal to the
|
|
||||||
TypeScript definitions, and those were then deleted.
|
|
||||||
- The loader registers the recipes and `server/game/crafting.ts` matches against them. Behaviors stay in
|
|
||||||
`server/game/blocks.ts`, still keyed by block id, for now.
|
|
||||||
- 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). _Done._
|
- `override` and `extend` in common scripts, and `overrides/*.json` for the same thing as data. `recipes.remove`.
|
||||||
|
- `check-mods`: changing content of a mod that isn't a dependency is an error, two mods changing the same field is a
|
||||||
|
warning.
|
||||||
|
- Check: a test mod adds a component to `bworld:grass`, changes `bworld:glass`'s toughness and replaces the chest
|
||||||
|
recipe, and all of it works on both sides and in the registry hash.
|
||||||
|
|
||||||
- `mods/bworld/scripts/server.ts` registers `bworld:hoeable`, `bworld:storage`, `bworld:furnace`, `bworld:crop` (bone
|
**Phase 3: engine access.**
|
||||||
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).
|
- The engine puts its modules on the global object at startup on each side (page, game server worker, chunk workers),
|
||||||
|
and the build turns `bworld/engine` imports into a shim that reads them. Types come from the engine's source.
|
||||||
|
- `hook` with `before`, `replace`, `after`, priorities and unhooking.
|
||||||
|
- `engine_access` in the manifest: `check-mods` and the loader refuse `bworld/engine` imports without it, and refuse
|
||||||
|
loading on a game version outside the mod's range. The confirm screen and the join screen say which mods use it.
|
||||||
|
- Go through the engine for things mods will want to change and make them hookable: move module functions like
|
||||||
|
`move_body` onto objects the engine calls through, and turn `#private` members that don't protect a rule into normal
|
||||||
|
ones.
|
||||||
|
- Check: test mods hook a server method, a client method and a worldgen method, several hooks on one method run in
|
||||||
|
priority order, and a throwing hook is logged without breaking the game.
|
||||||
|
|
||||||
- Port `common/worldgen/` to `mods/bworld/scripts/worldgen.ts` as the `bworld:overworld` terrain generator. It's a
|
**Phase 4: the rest of the Game API.**
|
||||||
straight port: it only uses named noises, which are exactly `noise_2d` / `noise_3d` in
|
|
||||||
[Terrain generators](#terrain-generators).
|
|
||||||
- Move `BASE_ORES` to `mods/bworld/worldgen/ores.json`. They already run through the same ore pass as mods' ores, in
|
|
||||||
the same order, so the result is identical.
|
|
||||||
- Remove the content from `common/generation.ts`, leaving the passes and noise helpers.
|
|
||||||
- Check: `terrain.json` matches exactly, client and server terrain still agree, and `world_v2.json` loads unchanged.
|
|
||||||
|
|
||||||
**Phase 4: engine cleanup.**
|
- Mod channels (`ctx.net` on both sides, `{ type: "mod", channel, data }`, the size and rate limits).
|
||||||
|
- Keybinds (`ctx.input.bind`), forms (`message_form`, `action_form`, `modal_form`), custom screens with `Graphics`, and
|
||||||
|
HUD elements.
|
||||||
|
- More events: items picked up and dropped, containers clicked, players moving between chunks, entities.
|
||||||
|
- Check: the example mod below works end to end, and every server event has a test.
|
||||||
|
|
||||||
- Replace the hardcoded water checks in `server/game/game_server.ts` with the `replaceable` block field, like
|
**Phase 5: gameplay out of the engine.**
|
||||||
`client/game_mode.ts` already does.
|
|
||||||
- `/give` stops assuming `bworld:`. It looks names up across all namespaces and asks for the full id when two mods have
|
- `/give` and `/time` become commands in `mods/bworld`. `/tps` stays, it's about the engine.
|
||||||
the same name.
|
- The crafting grid's matching and the `shaped` recipe type move into `mods/bworld`, and the player's inventory screen
|
||||||
- Rename engine asset keys like `bworld:ui` and `bworld:m6x11` in `client/main.ts` to `engine:`, so `bworld:` only means
|
becomes a screen `mods/bworld` registers, opened by the engine's inventory key.
|
||||||
content.
|
- The hotbar and crosshair become `mods/bworld` HUD elements.
|
||||||
- The Credits screen (title screen and pause menu) shows the engine's `assets/ASSETS.md` plus every loaded mod's credits.
|
- Mining rules (how long breaking takes, tool speed, `requires_tool`, what drops) become something `mods/bworld` sets,
|
||||||
_Done._ Credits are published next to the mod's data and checked by hash like everything else.
|
with the engine asking it on both sides so clients still predict breaking.
|
||||||
|
- The day and night lengths and sky colors become settings `mods/bworld` makes in its common script. The engine keeps
|
||||||
|
counting ticks.
|
||||||
|
- Where new players spawn becomes `mods/bworld`'s choice.
|
||||||
|
- Check: all tests pass unchanged, and `world_v2.json` and a version 3 save still load.
|
||||||
|
|
||||||
|
**Phase 6: world generation.**
|
||||||
|
|
||||||
|
- First the golden fixture: `tests/fixtures/terrain.json`, a SHA-256 of the final blocks of 64 chunks for 3 seeds,
|
||||||
|
including negative coordinates and chunks with trees on their borders, recorded from the current code.
|
||||||
|
- Port `common/worldgen/` to `mods/bworld/scripts/worldgen.ts` as the `bworld:overworld` terrain generator. It only uses
|
||||||
|
named noises, which are exactly `noise_2d` / `noise_3d` in [Terrain generators](#terrain-generators).
|
||||||
|
- Move the ore table to `mods/bworld/worldgen/ores.json`, and take the content out of `common/generation.ts`, leaving
|
||||||
|
the passes and noise helpers.
|
||||||
- The server refuses to start without a terrain generator, naming the mods that could provide one.
|
- The server refuses to start without a terrain generator, naming the mods that could provide one.
|
||||||
|
- Check: `terrain.json` matches exactly, client and server terrain still agree, and old saves load unchanged.
|
||||||
|
|
||||||
### Done when
|
**Done when:**
|
||||||
|
|
||||||
- Searching `client/`, `common/` and `server/` for `"bworld:` finds only `bworld:air`.
|
- Searching `client/`, `common/` and `server/` for `"bworld:` finds only `bworld:air`.
|
||||||
- Removing `mods/bworld` gives an engine that starts and says it needs a terrain generator, instead of crashing
|
- Removing `mods/bworld` gives an engine that starts and says it needs a terrain generator, instead of crashing
|
||||||
somewhere.
|
somewhere.
|
||||||
- All tests pass, the golden fixtures are unchanged, and a world saved before the move plays exactly the same after it.
|
- The base game uses only the Game API, no engine access. If it needs a hook for something, that thing belongs in the
|
||||||
|
Game API.
|
||||||
## Implementation plan
|
- All tests pass, the golden fixtures are unchanged, and a world saved before any of this plays exactly the same.
|
||||||
|
|
||||||
Steps 1–6 are done and 9 mostly, enough that a mod made from the template loads and runs. Each step keeps the game
|
|
||||||
working:
|
|
||||||
|
|
||||||
1. **Game server core.** Move `client/generation.ts` to `common/` (it already only needs constants and the rng package)
|
|
||||||
and have the server generate terrain. Move world state, chunks, tile data and player inventories into a game server
|
|
||||||
module that runs in a Deno worker, with `server/main.ts` handling HTTP, WebSockets and files.
|
|
||||||
2. **Server authority.** Change the protocol from "here's the block I changed" to intents (`break_block`, `place_block`,
|
|
||||||
`interact`, `select_slot`, `container_click`). The client keeps showing breaks and places immediately and accepts
|
|
||||||
corrections. Move inventories, drops and `/give` to the server. Replace `GuiChest` / `GuiFurnace` with server-synced
|
|
||||||
containers.
|
|
||||||
3. **Server only.** Remove the offline fallback in `client/main.ts`, which currently starts a local game when it can't
|
|
||||||
reach a server, and show a connection error instead.
|
|
||||||
4. **Mod loader and build.** Discover mods, check manifests, sort by dependencies, turn JSON into registry entries,
|
|
||||||
build the combined atlas (textures are currently all named `bworld:<file>`), bundle scripts per side, and write
|
|
||||||
hashed output to `build/mods/` and `server_mods/`. _Done._
|
|
||||||
5. **Delivery.** Split `welcome` into `welcome` / `ready` / `join`. Clients download, verify and run mods before
|
|
||||||
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.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. _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
|
|
||||||
terrain out of the engine.
|
|
||||||
|
|
||||||
Steps 1–3 are engine work every multiplayer feature needs, with or without mods. Mods could start with data only (step 4
|
|
||||||
plus data in step 5) before scripts exist.
|
|
||||||
|
|
||||||
Moving the base game into `mods/bworld` happens alongside steps 4–9. See
|
|
||||||
[The base game as a mod](#the-base-game-as-a-mod) for which phase follows which step.
|
|
||||||
|
|
||||||
## Open questions
|
## Open questions
|
||||||
|
|
||||||
- **Chunk delivery.** Clients generate terrain from the seed, which needs deterministic worldgen and trusts clients to
|
- **Chunk delivery.** Clients generate terrain from the seed, which needs deterministic worldgen and trusts clients to
|
||||||
run the same generator. Sending chunks from the server instead (like Minecraft) removes both problems but costs
|
run the same generator. Sending chunks from the server instead (like Minecraft) removes both problems but costs
|
||||||
bandwidth, roughly 5–15 KB per chunk compressed. It could be worth it once the server generates terrain anyway.
|
bandwidth, roughly 5–15 KB per chunk compressed. It could be worth it once the server generates terrain anyway.
|
||||||
- **Client-only mods.** Should players be able to install their own client mods (minimaps, UI tweaks) that work on any
|
- **Client-only mods.** Should players be able to install their own client mods (minimaps, UI tweaks, shaders) that work
|
||||||
server? They'd need a separate `"side": "client"` kind of mod that can't add content, and servers might want to forbid
|
on any server, like Fabric's client mods? They'd need a separate `"side": "client"` kind of mod that can't add content
|
||||||
them.
|
or change the registry hash, and servers might want to forbid them.
|
||||||
- **Client-side prediction for mods.** Custom block components only run on the server, so interacting with a mod block
|
- **Client-side prediction for mods.** Custom block components only run on the server, so interacting with a mod block
|
||||||
always waits one round trip. Optional client-side component handlers for prediction could fix that later.
|
always waits one round trip. Components could get optional handlers in common scripts that clients run to predict.
|
||||||
- **Overriding base game content.** Replacing `bworld:` blocks is forbidden for now. An `"extends"` field that adds
|
- **Engine access versions.** Tying engine access mods to one minor version is simple but strict. Fabric gets further
|
||||||
components to an existing block (for example to `bworld:grass`) is a possible middle ground.
|
with mapped names and mixin error reporting; how much of that is worth building depends on how often the engine
|
||||||
|
changes under mods.
|
||||||
|
- **Game API versions.** The Game API only breaking with the game's major version is a promise that needs deprecation
|
||||||
|
warnings and a changelog to keep.
|
||||||
- **Screen scaling.** The GUI currently works in raw canvas pixels (`SLOT_SIZE` is 54). `Graphics` should probably use a
|
- **Screen scaling.** The GUI currently works in raw canvas pixels (`SLOT_SIZE` is 54). `Graphics` should probably use a
|
||||||
UI scale the player can change, with screens laid out in scaled units.
|
UI scale the player can change, with screens laid out in scaled units.
|
||||||
- **Entities.** The only entities are players (`client/entity/`), so mob mods are out of scope until there are more
|
- **Entities.** The only entities are players and dropped items, so mob mods need entity types in the Game API first.
|
||||||
entity types.
|
|
||||||
- **Hot reload.** Restarting the server and reconnecting is the version 1 answer.
|
- **Hot reload.** Restarting the server and reconnecting is the version 1 answer.
|
||||||
|
|||||||
Reference in New Issue
Block a user