New modding system
This commit is contained in:
@@ -110,7 +110,9 @@ mod refers to exists (and that it depends on the mods those come from), and type
|
||||
`common/mod_api/`, which scripts import as `bworld/common`, `bworld/server`, `bworld/client`, `bworld/worldgen` and,
|
||||
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.
|
||||
`deno task build` packs every mod in `mods/` into a `.bmod` in `server_mods/` and fails if any has errors;
|
||||
`deno task server` then loads every `.bmod` there. `deno task pack-mod mods/copper_tools` packs one mod into a file to
|
||||
share, see [Mod files](#mod-files).
|
||||
|
||||
## Mod layout
|
||||
|
||||
@@ -118,6 +120,7 @@ with engine access, `bworld/engine`. Folders in `mods/` starting with `_` or `.`
|
||||
mods/
|
||||
copper_tools/
|
||||
manifest.json
|
||||
deno.json # optional: the mod is its own deno project, see Mod files
|
||||
blocks/*.json
|
||||
models/*.json
|
||||
items/*.json
|
||||
@@ -201,8 +204,7 @@ program**, so the server and each client can number blocks differently. Saves an
|
||||
|
||||
- Every `.png` in `textures/` becomes a texture with id `<mod_id>:<file name without .png>`. Subfolders are joined with
|
||||
`_`, so `textures/ores/tin.png` becomes `<mod_id>:ores_tin`.
|
||||
- Textures are 16×16. The server's build puts every installed mod's textures into one atlas with the base game's, and
|
||||
clients download that atlas instead of using their own.
|
||||
- Textures are 16×16. They go in the mod's `.bmod`, and clients put every mod's textures into one atlas when they join.
|
||||
- A missing texture shows the magenta and black checker (`engine:missing`) and logs a warning. It isn't a load error.
|
||||
- The engine's own textures use the `engine` namespace: `engine:missing` and the breaking cracks `engine:break_0` to
|
||||
`engine:break_8`. They come from `assets/`, not from a mod.
|
||||
@@ -1228,24 +1230,46 @@ function hook<T, K extends keyof T>(target: T, method: K, handlers: {
|
||||
|
||||
## Delivery to clients
|
||||
|
||||
### Build
|
||||
### Mod files
|
||||
|
||||
`deno task build` builds each mod in `mods/` into two places:
|
||||
A mod is shipped as a `.bmod` file: the mod, built, in one zip. Servers load every `.bmod` in their `server_mods/`
|
||||
folder, so installing a mod is dropping its file there and restarting.
|
||||
|
||||
```
|
||||
build/mods/<id>/<hash>/ # public, served to players
|
||||
manifest.json
|
||||
data.json # all blocks, items, models, recipes, overrides and ores merged
|
||||
common.js
|
||||
client.js
|
||||
worldgen.js
|
||||
server_mods/<id>/<hash>/ # private, outside the static root, never served
|
||||
server.js
|
||||
```sh
|
||||
deno task pack-mod mods/copper_tools # writes copper_tools.bmod
|
||||
deno task build # packs every mod in mods/ into server_mods/<id>.bmod
|
||||
```
|
||||
|
||||
`<hash>` is a content hash of the mod's public files, so URLs never change content. The server sends them with
|
||||
`Cache-Control: immutable`, and players only download a mod again when it changes. Textures from every mod go into the
|
||||
server's atlas, which gets a hash-named URL the same way.
|
||||
```
|
||||
copper_tools.bmod
|
||||
manifest.json # the mod's manifest, with scripts and credits pointing into the zip
|
||||
data.json # every block, model, item, recipe and ore, checked when it was packed
|
||||
scripts/server.js # each script bundled into one module
|
||||
scripts/client.js
|
||||
scripts/worldgen.js
|
||||
textures/<name>.png # the texture copper_tools:<name>
|
||||
credits.md
|
||||
```
|
||||
|
||||
- A mod is a Deno project. When its folder has a `deno.json`, its scripts are bundled with it, so its own imports and
|
||||
npm or JSR packages work and end up in the bundle. Mods without one use the `deno.json` of the folder the build runs
|
||||
in, like the ones in this repository.
|
||||
- Packing checks the mod first, like `check-mods`, and refuses to pack one with errors.
|
||||
- Packing the same mod twice gives the same bytes, so the file's hash only changes when the mod does.
|
||||
- `deno task build` leaves other `.bmod` files in `server_mods/` alone, so mods from elsewhere can sit next to the ones
|
||||
it builds.
|
||||
|
||||
When the server starts it opens every `.bmod`, checks it again (it may come from anyone): its manifest and data, its
|
||||
textures, the ids it uses from other mods, and that its dependencies are there. A problem in any mod stops the server
|
||||
from starting, saying which mod and what's wrong, so the game never runs with some mods missing. Then it sorts them so
|
||||
dependencies load first.
|
||||
|
||||
Players get each mod as a `.bmod` too, **without its server script**: the server makes a copy without
|
||||
`scripts/server.js` and serves it at `/mods/<sha256>.bmod`, named by its hash so it can be cached forever. The code is in
|
||||
`common/bmod.ts` (reading and writing), `tools/pack_mod.ts` and `server/load_bmods.ts`.
|
||||
|
||||
The engine's own files (the client, its UI sprites and font, and the `engine:` textures) aren't in any mod. They're in
|
||||
`build/`, which the server serves as it is.
|
||||
|
||||
### Joining
|
||||
|
||||
@@ -1253,15 +1277,14 @@ server's atlas, which gets a hash-named URL the same way.
|
||||
client server
|
||||
│ hello { name, protocol } │
|
||||
├────────────────────────────────────────►│ a different protocol gets rejected { reason }
|
||||
│ welcome { protocol, seed, atlas, │ mods[i] = { id, name, version, hash,
|
||||
│ mods[] } │ data, client?, worldgen?, sha256 }
|
||||
│◄────────────────────────────────────────┤ atlas = { png, json, sha256 } (paths on the server)
|
||||
│ welcome { protocol, seed, mods[] } │ mods[i] = { id, name, version, sha256, file, size }
|
||||
│◄────────────────────────────────────────┤ file is the .bmod's path on the server
|
||||
│ │
|
||||
│ [cross-origin: confirm screen] │
|
||||
│ download the atlas and every mod file, │
|
||||
│ check each one's sha256, load content │
|
||||
│ in the listed order, run client │
|
||||
│ setup(), start chunk workers │
|
||||
│ download every .bmod, check each one's │
|
||||
│ sha256, build the texture atlas, load │
|
||||
│ content in the listed order, run │
|
||||
│ client setup(), start chunk workers │
|
||||
│ │
|
||||
│ ready { registry_hash } │ a different hash gets rejected { reason }
|
||||
├────────────────────────────────────────►│
|
||||
@@ -1271,17 +1294,20 @@ client server
|
||||
```
|
||||
|
||||
- The client registers each mod's data **in the order the server lists them**.
|
||||
- Every file is checked against its SHA-256 before it's used, and scripts are imported from the checked bytes (as
|
||||
- Every `.bmod` is checked against its SHA-256 before it's opened, and scripts are imported from the checked bytes (as
|
||||
`blob:` URLs), so what runs is exactly what was checked. The client code is in `client/handshake.ts` and
|
||||
`client/mods.ts`.
|
||||
- The client builds the texture atlas itself, from the engine's textures and the ones in the `.bmod` files
|
||||
(`client/atlas.ts`). The server never needs to decode an image.
|
||||
- If any download, hash check or `setup` fails, the client disconnects and shows which mod failed. It never joins with
|
||||
some mods missing.
|
||||
- The `protocol` in `hello` is the game's protocol version (`PROTOCOL_VERSION` in `common/protocol.ts`). A mismatch is
|
||||
rejected before anything is downloaded.
|
||||
- Until `ready`, the connection isn't a player: other players don't see it, and anything it sends besides `ready` is
|
||||
ignored. A client that doesn't send `ready` within 60 seconds is rejected.
|
||||
- Mod files and the atlas are served with `Access-Control-Allow-Origin: *` and cached for a year, since their paths
|
||||
change whenever their content does.
|
||||
- `.bmod` files are served with `Access-Control-Allow-Origin: *` and cached for a year, since their paths change
|
||||
whenever their content does. The engine's assets are served with `Access-Control-Allow-Origin: *` too, for pages
|
||||
from other origins.
|
||||
- Leaving a server reloads the page, so one server's mod code never stays loaded while playing on another.
|
||||
|
||||
## Security
|
||||
@@ -1294,7 +1320,7 @@ where the page came from:
|
||||
- **Page from one origin, game server on another** (a different server typed in the title screen, or `?server=`): mod
|
||||
code from that server runs with _the page's_ origin, including its local storage. The client shows the server's mod
|
||||
list and asks before loading anything, and remembers the answer per server and mod hash. The server needs CORS
|
||||
headers on `build/mods/`.
|
||||
headers on its `.bmod` downloads and assets, which it sends.
|
||||
- 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
|
||||
@@ -1307,6 +1333,9 @@ lists which mods use engine access.
|
||||
`--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.
|
||||
|
||||
**Server scripts stay on the server.** Players get each mod's `.bmod` without its server script, so a mod can keep
|
||||
anti-cheat logic or anything else private there.
|
||||
|
||||
**Players.** Clients are untrusted. The server checks reach and the item being held for every break, place and interact,
|
||||
applies container rules on the server, validates form responses, and rate-limits mod channels. Server script code and
|
||||
tile data are never sent to clients.
|
||||
@@ -1474,8 +1503,8 @@ Worlds saved before any of this must load afterwards with nothing changed:
|
||||
|
||||
## Implementation plan
|
||||
|
||||
**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,
|
||||
**Done so far:** the mod loader, `.bmod` mod files (packing, loading them from `server_mods/`, sending players a copy
|
||||
without the server script), 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`.
|
||||
|
||||
Reference in New Issue
Block a user