@mavonengine/cli 0.0.1 → 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +289 -10
- package/dist/chunk-NIBSLCCZ.js +91 -0
- package/dist/chunk-XPKCYSMN.js +44 -0
- package/dist/chunk-ZEMFSSJP.js +87 -0
- package/dist/docs-O2V2WSUG.js +13 -0
- package/dist/index.js +2440 -359
- package/dist/server-NNL4IMYV.js +925 -0
- package/package.json +7 -5
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ Requires Node 24 or newer.
|
|
|
6
6
|
|
|
7
7
|
```sh
|
|
8
8
|
npm install -D @mavonengine/cli
|
|
9
|
-
npx mavonengine --help
|
|
9
|
+
npx @mavonengine/cli --help
|
|
10
10
|
```
|
|
11
11
|
|
|
12
12
|
## `mavonengine create`
|
|
@@ -30,15 +30,17 @@ Asks for a name if none is given. While the template downloads and `npm install`
|
|
|
30
30
|
|
|
31
31
|
## `mavonengine dev`
|
|
32
32
|
|
|
33
|
-
Runs the
|
|
33
|
+
Runs the dev script of the MavonEngine project you are in and shows the [dashboard](#mavonengine-dashboard) for it. For the multiplayer template that starts the game server and the client together.
|
|
34
|
+
|
|
35
|
+
It runs the project's `dev:server` script, or `dev` when there is none. So `dev` can be `mavonengine dev` itself, as in the multiplayer template, and `npm run dev` brings up the dashboard. A `dev` script that would start `mavonengine dev` again, without a `dev:server` next to it, is refused instead of looping.
|
|
34
36
|
|
|
35
37
|
While the game server starts, a startup screen shows the latest output of the dev script; the dashboard takes over as soon as the server answers. Once the server is up, the game it serves opens in your default browser (not over SSH, and only if the server answers `/` with a page); `--no-open` turns that off, and `o` opens it again any time. Quitting the dashboard (`q` or Ctrl-C) stops the dev script and everything it started. If the dev script exits on its own, the dashboard closes and its output is printed.
|
|
36
38
|
|
|
37
39
|
```sh
|
|
38
|
-
npx mavonengine dev
|
|
40
|
+
npx @mavonengine/cli dev
|
|
39
41
|
|
|
40
42
|
# From somewhere else
|
|
41
|
-
npx mavonengine dev --cwd path/to/my-game
|
|
43
|
+
npx @mavonengine/cli dev --cwd path/to/my-game
|
|
42
44
|
```
|
|
43
45
|
|
|
44
46
|
The project root is the nearest `package.json`, from the current directory upwards, that depends on `@mavonengine/core` and has a `dev` script, so it also works from inside `client/` or `server/`. The package manager comes from the lockfile (`npm`, `pnpm`, `yarn` or `bun`, default `npm`). Without an interactive terminal, or with `--no-dashboard`, the dev script's output goes straight to the terminal instead. Ctrl-C stops everything, and the command exits with the dev script's exit code.
|
|
@@ -52,21 +54,96 @@ The project root is the nearest `package.json`, from the current directory upwar
|
|
|
52
54
|
| `--transparent` | | Let a transparent terminal background show through |
|
|
53
55
|
| `--no-open` | | Don't open the game in the browser once the server is up |
|
|
54
56
|
|
|
57
|
+
|
|
58
|
+
### Installing plugins while it runs
|
|
59
|
+
|
|
60
|
+
`mavonengine dev` can install packages into the project and restart the dev servers afterwards, so a new plugin is loaded without stopping anything yourself:
|
|
61
|
+
|
|
62
|
+
- from the dashboard's Plugins tab, with `i` on a plugin that is available to install;
|
|
63
|
+
- from the editor's Plugins view, with the Install button of an available plugin, or by typing any package (a name, `name@version`, a `file:` path or a URL) into "Another package".
|
|
64
|
+
|
|
65
|
+
Both ask first and point at the package's source: installing a package runs its code on your machine, and nothing reviews what you install. The install is the same as [`mavonengine install --yes`](#mavonengine-install), followed by a restart of the dev script.
|
|
66
|
+
|
|
67
|
+
The editor reaches `mavonengine dev` through the dev server it started (`POST /__mavonengine/plugin-install`), which only takes requests from localhost and from the game's own page, so another site open in the browser can't install anything. Version ranges such as `^1.0.0` and anything with spaces are refused there; use the terminal for those.
|
|
68
|
+
|
|
69
|
+
## `mavonengine plugins`
|
|
70
|
+
|
|
71
|
+
Lists the plugins that are available to install.
|
|
72
|
+
|
|
73
|
+
```sh
|
|
74
|
+
npx @mavonengine/cli plugins
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
The list is [`plugins.json`](https://github.com/MavonEngine/Core/blob/main/plugins.json) at the root of the MavonEngine repository, read from the `main` branch on GitHub every time the command runs:
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
https://raw.githubusercontent.com/MavonEngine/Core/main/plugins.json
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
So a plugin shows up as soon as it is added to that file on `main` (GitHub may serve the file a few minutes old). Inside a project the command also says which of the plugins are already in its `mavonengine.json`. `--json` prints the list as JSON, and `--url <url>` or `MAVONENGINE_PLUGINS_URL` reads it from somewhere else, such as a fork or a branch.
|
|
84
|
+
|
|
85
|
+
To list a plugin, add an entry to `plugins.json`: `package` (the npm name) is required; `name`, `description`, `target` (`client`, `server` or `both`), `engine` (the supported `@mavonengine/core` range), `homepage` and `readme` (the URL of its README as Markdown, such as the raw file on GitHub, which the editor shows) are optional.
|
|
86
|
+
|
|
87
|
+
## `mavonengine install`
|
|
88
|
+
|
|
89
|
+
Installs packages into the project you are in, with its package manager. For each one that is a MavonEngine [plugin](#writing-a-plugin), it shows where the plugin runs (client, server or both) and which prefabs it brings, and asks whether to add it to the `plugins` in `mavonengine.json`. Registered plugins load on the client and/or the game server and are built into the release.
|
|
90
|
+
|
|
91
|
+
```sh
|
|
92
|
+
npx @mavonengine/cli install my-custom-plugin
|
|
93
|
+
|
|
94
|
+
# Anything your package manager takes
|
|
95
|
+
npx @mavonengine/cli install @acme/forest@^2 file:../my-plugin --yes
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
`mavonengine.json` sits in the project root, next to `package.json`, and is created if it doesn't exist:
|
|
99
|
+
|
|
100
|
+
```json
|
|
101
|
+
{
|
|
102
|
+
"plugins": {
|
|
103
|
+
"my-custom-plugin": { "enabled": true, "prefabs": ["Tree"], "options": { "density": 2 } }
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
- `enabled`: set to `false` to turn a plugin off without uninstalling it.
|
|
109
|
+
- `prefabs`: the plugin prefabs the project uses. The editor adds a prefab here when you place it, and you can edit the list by hand. Only the assets of these prefabs (plus the plugin's shared assets) go into the release.
|
|
110
|
+
- `options`: passed to the plugin as `ctx.options`.
|
|
111
|
+
|
|
112
|
+
The client reloads when the plugins in `mavonengine.json` change. Restart the game server to load a new server plugin. Without an interactive terminal it only installs, unless `--yes` is given.
|
|
113
|
+
|
|
114
|
+
| Option | Default | |
|
|
115
|
+
| --- | --- | --- |
|
|
116
|
+
| `--cwd <dir>` | current directory | Where to start looking for the project |
|
|
117
|
+
| `-y`, `--yes` | | Add plugins to `mavonengine.json` without asking |
|
|
118
|
+
| `--no-register` | | Only install |
|
|
119
|
+
|
|
120
|
+
## `mavonengine build`
|
|
121
|
+
|
|
122
|
+
Runs the project's `build` script, then lists what each plugin put into the release. Plugin assets are copied into the client's output under `plugins/<package>/`. Only the shared ones and those of the prefabs in `mavonengine.json` are copied. The client skips loading the others, so a release never requests a file it doesn't contain.
|
|
123
|
+
|
|
124
|
+
```sh
|
|
125
|
+
npx @mavonengine/cli build
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
| Option | Default | |
|
|
129
|
+
| --- | --- | --- |
|
|
130
|
+
| `--cwd <dir>` | current directory | Where to start looking for the project |
|
|
131
|
+
|
|
55
132
|
## `mavonengine dashboard`
|
|
56
133
|
|
|
57
|
-
A live terminal dashboard for a running game server, built with [hqtui](https://github.com/profullstack/hqtui).
|
|
134
|
+
A live terminal dashboard for a running game server, built with [hqtui](https://github.com/profullstack/hqtui). For the same dashboard in a browser, behind a login, see the [`@mavonengine/http-monitoring`](https://github.com/MavonEngine/Core/tree/main/packages/plugins/http-monitoring) plugin.
|
|
58
135
|
|
|
59
136
|
It shows CPU, network bandwidth, tick timings against the tick budget, world/physics/memory stats, the connected players, the server's recent log output and the HTTP requests it received. It reads them from the server's `GET /api/game/metrics`, `GET /api/game/logs` and `GET /api/game/requests` endpoints, which need a `@mavonengine/core` release that includes them.
|
|
60
137
|
|
|
61
138
|
```sh
|
|
62
139
|
# Local server on the default HTTP port (8050)
|
|
63
|
-
npx mavonengine dashboard
|
|
140
|
+
npx @mavonengine/cli dashboard
|
|
64
141
|
|
|
65
142
|
# Remote server with a metrics token
|
|
66
|
-
MAVONENGINE_METRICS_TOKEN=secret npx mavonengine dashboard --host game.example.com --port 8050
|
|
143
|
+
MAVONENGINE_METRICS_TOKEN=secret npx @mavonengine/cli dashboard --host game.example.com --port 8050
|
|
67
144
|
|
|
68
145
|
# No server needed: simulated data
|
|
69
|
-
npx mavonengine dashboard --sim
|
|
146
|
+
npx @mavonengine/cli dashboard --sim
|
|
70
147
|
```
|
|
71
148
|
|
|
72
149
|
| Option | Default | |
|
|
@@ -74,16 +151,26 @@ npx mavonengine dashboard --sim
|
|
|
74
151
|
| `--host <host>` | `localhost` | Server host |
|
|
75
152
|
| `--port <port>` | `8050` | Server HTTP port (`ServerOptions.httpPort`) |
|
|
76
153
|
| `--url <url>` | | Full metrics URL, overrides `--host` and `--port` |
|
|
77
|
-
| `--token <token>` | `$MAVONENGINE_METRICS_TOKEN` | Metrics token (`ServerOptions.metrics.token`) |
|
|
154
|
+
| `--token <token>` | `$MAVONENGINE_METRICS_TOKEN`, then `$MAVONENGINE_ADMIN_TOKEN` | Metrics token (`ServerOptions.metrics.token`), or the server's [admin token](#admin-token) |
|
|
78
155
|
| `--interval <ms>` | `1000` | Poll interval |
|
|
79
156
|
| `--theme <name>` | `dark` | `dark`, `dracula`, `nord`, `tokyoNight`, `gruvbox`, `matrix`, `monochrome`, `highContrast`, `light` |
|
|
80
157
|
| `--transparent` | | Let a transparent terminal background show through. Text can be hard to read over a light wallpaper |
|
|
81
158
|
| `--sim` | | Simulated data instead of a server |
|
|
82
159
|
|
|
83
|
-
Keys: `q` quits, `r` refreshes now, `o` opens the game the server serves in the browser, `1`–`
|
|
160
|
+
Keys: `q` quits, `r` refreshes now, `o` opens the game the server serves in the browser, `1`–`7` switch between the Overview, Players, Logs, HTTP, Network, Routes and Plugins tabs. The mouse wheel scrolls the players table, the logs, the requests, the packets and the routes; scroll the logs back to the bottom, or the requests and packets back to the top, to follow new ones again.
|
|
84
161
|
|
|
85
162
|
The HTTP tab lists every request the game server's HTTP server received, newest first: the client (static files or Vite in dev), your own `onHttp` routes, the geckos.io signaling requests and WebSocket upgrades such as Vite's HMR socket, each with status, duration, size and client address. Query strings are left out since they can hold tokens, and the dashboard's own polling is not recorded. The game traffic itself is WebRTC, not HTTP, so it shows on the Players tab instead; with a separate `socketPort` the signaling requests go to that port and don't show up either.
|
|
86
163
|
|
|
164
|
+
The Network tab shows the game socket while developing. State sync lists each player with the entities in its last state update, its last acknowledged input and the size and tick of that update (red when it fell behind the others), and counts the synced entities no player is in range of. By message breaks the traffic of the last 5 seconds down by message name and direction: packets per second, average size and bytes per second, the largest share first. Below, every packet and network event, newest first, with its client, size, tick, input sequence id and a few facts about its payload; `p` shows the pings, `e` only the events (`connect`, `disconnect`, `dropped`, `sync_enter`, `sync_leave`). Click a packet, or move through the list with the arrow keys (or `j` / `k`), to see its content next to the list: the payload as it went over the wire, scrolled with the mouse wheel. `esc`, or a second click on it, closes it. The server drops the payloads of old packets to bound its memory, so only recent ones can be opened. The packets are only fetched while the tab shows, and only a server that is not running in production records them, see the [network tools](#mavonengine-mcp).
|
|
165
|
+
|
|
166
|
+
The Routes tab lists every HTTP endpoint the server has: the engine's own, the ones your server registered in `onHttp` and those of plugins, each with its method, path, whether it only exists in development or is served in production too, where it comes from and what it does. The list is read from the running server, so a new route shows up without being declared anywhere; say what it does with `describeRoute()` from `@mavonengine/core/Networking/Server/Http/RouteCatalog`, and pass it `devOnly: true` for a route you only register in development, since a route that doesn't say is listed as `prod + dev`. It needs a server on a `@mavonengine/core` release with `GET /api/game/routes`.
|
|
167
|
+
|
|
168
|
+
The Plugins tab lists the plugins in the project's `mavonengine.json` with their version, whether they are enabled, where they run and how many endpoints and prefabs they have. `↑`/`↓` (or `j`/`k`, or a click) pick one to see its description, supported engine versions, license, prefabs and the HTTP endpoints it registered. A server plugin that is enabled but not running in this server shows as `not loaded`. It needs a server on a `@mavonengine/core` release with `GET /api/game/plugins`.
|
|
169
|
+
|
|
170
|
+
Below it, "Available to install" lists the plugins from [`mavonengine plugins`](#mavonengine-plugins) that the project doesn't have yet. The dashboard reads that list from GitHub itself, the first time the tab is shown and again on `r`; a dashboard that never opens the tab never asks for it.
|
|
171
|
+
|
|
172
|
+
Under [`mavonengine dev`](#mavonengine-dev) a plugin from that list can be installed right there: select it with `↓` and press `i`. The dashboard first asks whether you checked its source, because installing a package runs its code on your machine; `y` installs it with the project's package manager, adds it to `mavonengine.json` and restarts the dev servers, `n` cancels. A dashboard started on its own with `mavonengine dashboard` only shows the command, since it can't restart servers it didn't start.
|
|
173
|
+
|
|
87
174
|
### Server access
|
|
88
175
|
|
|
89
176
|
The metrics endpoint is configured on the game server:
|
|
@@ -103,5 +190,197 @@ const server = new MyServer(logger, physicsWorld, {
|
|
|
103
190
|
|
|
104
191
|
- With no token, the endpoint only answers requests from localhost.
|
|
105
192
|
- With `NODE_ENV=production` and no token, the endpoint is turned off.
|
|
193
|
+
- The server's [admin token](#admin-token) is taken as well, and turns the endpoint on in production by itself.
|
|
106
194
|
- Set `metrics.enabled: false` to turn it off everywhere.
|
|
107
195
|
- The logs endpoint follows the same rules. It serves whatever passes your logger's level.
|
|
196
|
+
|
|
197
|
+
### Admin token
|
|
198
|
+
|
|
199
|
+
A game server has one admin token, for everything that changes it or tells what not everyone should know. The engine and every plugin use the same one, so no plugin needs a secret of its own:
|
|
200
|
+
|
|
201
|
+
```sh
|
|
202
|
+
MAVONENGINE_ADMIN_TOKEN=… npm start
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
or `adminToken` in the server's options. Requests send it as `Authorization: Bearer <token>`. Without a token, admin routes only answer requests from localhost.
|
|
206
|
+
|
|
207
|
+
- It also reads the metrics, so the dashboard and `mavonengine mcp` take it for `--token`. The metrics token stays read-only: it administers nothing.
|
|
208
|
+
- [`@mavonengine/chat`](../plugins/chat) guards its admin API with it, and [`@mavonengine/http-monitoring`](../plugins/http-monitoring) takes it as the password of its login.
|
|
209
|
+
- The Routes tab shows which routes ask for it.
|
|
210
|
+
|
|
211
|
+
## `mavonengine mcp`
|
|
212
|
+
|
|
213
|
+
A [Model Context Protocol](https://modelcontextprotocol.io) server over stdio, so an AI coding assistant can look at your running game: the game server, and the game tabs open in a browser. It works with any MCP client and any model: Claude Code, Cursor, VS Code, Codex, Gemini CLI, Zed, or a local model behind an MCP-capable client.
|
|
214
|
+
|
|
215
|
+
Projects made with `mavonengine create` get a `.mcp.json` that registers it. Elsewhere, add it to your client as a stdio server; most clients take this JSON:
|
|
216
|
+
|
|
217
|
+
```json
|
|
218
|
+
{
|
|
219
|
+
"mcpServers": {
|
|
220
|
+
"mavonengine": { "command": "npx", "args": ["-y", "@mavonengine/cli", "mcp"] }
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
Game server tools:
|
|
226
|
+
|
|
227
|
+
| Tool | |
|
|
228
|
+
| --- | --- |
|
|
229
|
+
| `server_status` | Uptime, tick time against its budget, CPU, bandwidth, memory, player / entity / physics counts |
|
|
230
|
+
| `server_metrics` | The full metrics snapshot, optionally with its per-second history |
|
|
231
|
+
| `list_players` | Connected players with position and connection details |
|
|
232
|
+
| `get_scene` | The active scene: file, hot reload, unsaved changes, settings and objects (dev only) |
|
|
233
|
+
| `list_entities` | Entities in the world, filtered by type and distance to a point (dev only) |
|
|
234
|
+
| `get_entity` | One entity with the state it sends to clients (dev only) |
|
|
235
|
+
| `recent_logs` | Server log lines, filtered by level and text |
|
|
236
|
+
| `recent_requests` | HTTP requests the server answered, filtered by path and status |
|
|
237
|
+
| `network_packets` / `network_packet` | Packets the server sent and received and its network events, per client: size, tick, input sequence id; and one packet with its payload (dev only) |
|
|
238
|
+
| `network_sync` | Who is sent what: each player's tracked entities and last state update, and by how many players each entity is tracked (dev only) |
|
|
239
|
+
|
|
240
|
+
Client tools, for the game tabs open while Vite serves the client in dev:
|
|
241
|
+
|
|
242
|
+
| Tool | |
|
|
243
|
+
| --- | --- |
|
|
244
|
+
| `list_clients` | Open tabs: page, editor or player, the player's id on the server, visibility, FPS |
|
|
245
|
+
| `client_status` | Draw calls and triangles of one frame, GPU geometries / textures / programs, JS heap, canvas, camera |
|
|
246
|
+
| `client_entities` / `client_entity` | The tab's world, in the same shape as the server's, with its local player marked |
|
|
247
|
+
| `client_scene` | The scene loaded in the tab, and which objects the client built |
|
|
248
|
+
| `client_network` | Connection, ping, inputs sent but not yet acknowledged by the server, and the last state update: its server tick, ack and age |
|
|
249
|
+
| `client_packets` / `client_packet` | Packets the tab sent and received and its network events, and one packet with its payload |
|
|
250
|
+
| `network_timeline` | The server's records for one tab and the tab's own, merged by time |
|
|
251
|
+
| `client_logs` | Console output, uncaught errors and unhandled rejections, filtered by level and text |
|
|
252
|
+
| `client_screenshot` | What the tab renders, as an image (the WebGL canvas, without HTML UI) |
|
|
253
|
+
| `client_trace` | Records a few seconds of the tab and analyses it: frame times, render CPU vs GPU, composer passes, ticks, WebGL work per frame, the bottleneck, and the worst hitches with their causes. Saves a flame chart trace file |
|
|
254
|
+
| `compare_entity` | One entity on the server next to a tab's copy: position / rotation / scale deltas and differing fields |
|
|
255
|
+
|
|
256
|
+
Plugin tools:
|
|
257
|
+
|
|
258
|
+
| Tool | |
|
|
259
|
+
| --- | --- |
|
|
260
|
+
| `list_plugins` | The plugins in the project's `mavonengine.json`: version, whether each is enabled and runs on the server, its prefabs and the HTTP endpoints it registered |
|
|
261
|
+
| `available_plugins` | The official plugins from [`mavonengine plugins`](#mavonengine-plugins): what each does, where it runs, the command that installs it, and whether the project already has it |
|
|
262
|
+
| `plugin_readme` | The README of one of them as Markdown: its options, how a game hooks into it and its limits |
|
|
263
|
+
|
|
264
|
+
With these an assistant can suggest a plugin instead of building what one already does, and set it up from its readme. It can't install one: every tool is read-only, so it hands you the command. `available_plugins` and `plugin_readme` read the list and the readmes from GitHub when they are used; `--plugins-url` or `MAVONENGINE_PLUGINS_URL` points them at another list and `--no-plugin-list` leaves them out.
|
|
265
|
+
|
|
266
|
+
Documentation tools, passed through to the MCP server of [mavonengine.com](https://mavonengine.com) (`https://mavonengine.com/mcp`), so answers come from the current docs:
|
|
267
|
+
|
|
268
|
+
| Tool | |
|
|
269
|
+
| --- | --- |
|
|
270
|
+
| `docs_list_pages` | Every docs page with its path, title, description and category |
|
|
271
|
+
| `docs_get_page` | One docs page as Markdown, by path (e.g. `/networking/state-sync`) |
|
|
272
|
+
|
|
273
|
+
They connect to the site the first time one is used, so the other tools also work offline. `--docs-url` points them at another docs server, `--no-docs` leaves them out.
|
|
274
|
+
|
|
275
|
+
Client tools take a `clientId` from `list_clients`. Without one they ask the only open tab, or the only one not running the editor.
|
|
276
|
+
|
|
277
|
+
Every tool is read-only. It takes `--host`, `--port`, `--url` and `--token` like the dashboard, where `--url` is the server's base URL (`http://localhost:8050`). When Vite serves the client on its own port instead of through the game server, pass `--client-url`, e.g. `http://localhost:5173`.
|
|
278
|
+
|
|
279
|
+
The server scene and entity tools read `GET /api/game/dev/*` on the game server. Those routes are only registered when `NODE_ENV` is not `production`: a metrics token does not turn them on there. Otherwise they follow the [server access](#server-access) rules of the metrics endpoint.
|
|
280
|
+
|
|
281
|
+
The client tools go through the dev bridge of the Vite plugin in `@mavonengine/core/vite.config` (Vite 8). In dev it adds a small module to the page that answers over Vite's hot-reload WebSocket, and serves `GET /__mavonengine/clients` to localhost, or with `MAVONENGINE_METRICS_TOKEN` as bearer token when that is set for the dev server. Builds never include it. Turn it off with `mavonEnginePlugins({ devBridge: false })`.
|
|
282
|
+
|
|
283
|
+
`client_trace` patches the tab only while it records and puts everything back afterwards. It times every frame and composer pass, counts the WebGL calls on the context, keeps slow single calls (shader compiles, texture uploads, `readPixels` / `getError` stalls), long animation frames with the scripts that ran, and your own `performance.measure()` spans. GPU time per frame needs `EXT_disjoint_timer_query_webgl2` (Chrome and Edge on most GPUs; the three-perf panel pauses while tracing since it uses the same queries). JS call stacks come from the [JS Self-Profiling API](https://wicg.github.io/js-self-profiling/) in Chromium browsers: the Vite plugin serves dev pages with `Document-Policy: js-profiling` for it, so reload tabs opened before you updated. The trace file is [Chrome Trace Event](https://docs.google.com/document/d/1CvAClvFfyA5R-PhYUmn5OOQtYMH4h6I0nSsKchNAySU) JSON with tracks for frames, passes, GPU time, ticks, JS samples and slow WebGL calls: open it in [Perfetto](https://ui.perfetto.dev), `chrome://tracing` or [speedscope](https://www.speedscope.app). Keep the tab in the foreground while it records, browsers throttle hidden tabs.
|
|
284
|
+
|
|
285
|
+
The network tools read what both sides record while developing: every message through the game socket, the engine's and your own, with its size, and events such as `connect`, `disconnect`, `dropped` (the transport dropped a message), `sync_enter` / `sync_leave` (entities coming into or leaving a player's sync distance) and, on a client, `state_gap` (state updates that never arrived) and `state_out_of_order`. Each side keeps its last 2000 records, and 8 MB of their payloads. The server records when `NODE_ENV` is not `production` and serves them on `GET /api/game/dev/net/packets`, `/packets/:id` and `/sync`; a tab records while the dev bridge or the editor runs. Nothing is recorded, and the socket is left untouched, in a production build. `network_timeline` orders the two sides by their own clocks, which line up on one machine.
|
|
286
|
+
|
|
287
|
+
To let `compare_entity` match your own player with the server's, set `game.localPlayer` to the entity the tab controls, or give it `isLocalPlayer = true`: the server keys players by socket id, which the client often doesn't.
|
|
288
|
+
|
|
289
|
+
## Writing a plugin
|
|
290
|
+
|
|
291
|
+
A plugin is an npm package with a `mavonengine` field in its `package.json`:
|
|
292
|
+
|
|
293
|
+
```json
|
|
294
|
+
{
|
|
295
|
+
"name": "my-custom-plugin",
|
|
296
|
+
"type": "module",
|
|
297
|
+
"mavonengine": {
|
|
298
|
+
"target": "both",
|
|
299
|
+
"client": "./dist/client.js",
|
|
300
|
+
"server": "./dist/server.js",
|
|
301
|
+
"engine": "^0.0.22",
|
|
302
|
+
"assets": {
|
|
303
|
+
"dir": "./assets",
|
|
304
|
+
"shared": ["textures/bark.png"],
|
|
305
|
+
"prefabs": {
|
|
306
|
+
"Tree": ["models/tree.glb"],
|
|
307
|
+
"Rock": ["models/rock.glb"]
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
- `target`: `client`, `server` or `both`. The matching entries are required.
|
|
315
|
+
- `assets`: paths relative to `dir` (default `assets`). They are served at `/plugins/<package>/<path>`: all of them in dev, and only the shared ones and those of used prefabs in a release.
|
|
316
|
+
|
|
317
|
+
Each entry default-exports the plugin. Every hook gets the plugin's context last, which carries `package`, `options`, `game` and `logger`.
|
|
318
|
+
|
|
319
|
+
```ts
|
|
320
|
+
// client.ts
|
|
321
|
+
import { defineClientPlugin } from '@mavonengine/core/Plugins/types'
|
|
322
|
+
|
|
323
|
+
export default defineClientPlugin({
|
|
324
|
+
assets: ctx => [{ name: 'tree', type: 'gltfModel', path: ctx.assetUrl('models/tree.glb') }],
|
|
325
|
+
setup(ctx) {
|
|
326
|
+
ctx.registerPrefab({
|
|
327
|
+
name: 'Tree',
|
|
328
|
+
spawn: position => ({ object: new Tree(position) }),
|
|
329
|
+
})
|
|
330
|
+
},
|
|
331
|
+
update(delta, ctx) {},
|
|
332
|
+
// Dev only, when the editor boots
|
|
333
|
+
editor(ctx) {
|
|
334
|
+
ctx.editor.on('objectSelected', () => {})
|
|
335
|
+
},
|
|
336
|
+
})
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
```ts
|
|
340
|
+
// server.ts
|
|
341
|
+
import { defineServerPlugin } from '@mavonengine/core/Plugins/types'
|
|
342
|
+
|
|
343
|
+
export default defineServerPlugin({
|
|
344
|
+
setup(ctx) {},
|
|
345
|
+
// Runs after the server's own onHttp routes, before the game client
|
|
346
|
+
http(app, ctx) {
|
|
347
|
+
app.get('/api/my-plugin/status', (_req, res) => res.json({ ok: true }))
|
|
348
|
+
},
|
|
349
|
+
onConnection(channel, client, ctx) {},
|
|
350
|
+
onDisconnect(channel, ctx) {},
|
|
351
|
+
tick(delta, ctx) {},
|
|
352
|
+
destroy(ctx) {},
|
|
353
|
+
})
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
A hook that throws is logged and turned off for that plugin, and the game keeps running.
|
|
357
|
+
|
|
358
|
+
Guard a plugin's admin routes with `ctx.requireAdmin`, which asks for the server's [admin token](#admin-token), instead of giving the plugin a token of its own:
|
|
359
|
+
|
|
360
|
+
```ts
|
|
361
|
+
export default defineServerPlugin({
|
|
362
|
+
http(app, ctx) {
|
|
363
|
+
app.post('/api/my-plugin/reset', ctx.requireAdmin, (_req, res) => res.json({ reset: true }))
|
|
364
|
+
},
|
|
365
|
+
})
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
A plugin that takes the token some other way, such as the password of a login, checks it with `ctx.adminToken.matches(candidate)`, without ever seeing the token.
|
|
369
|
+
|
|
370
|
+
Projects made with `mavonengine create` are already set up for plugins. To set up an existing project:
|
|
371
|
+
|
|
372
|
+
```ts
|
|
373
|
+
// client: Game.usePlugins() before new Game()
|
|
374
|
+
import Game from '@mavonengine/core/Game'
|
|
375
|
+
import plugins from 'virtual:mavonengine/plugins'
|
|
376
|
+
|
|
377
|
+
Game.usePlugins(plugins)
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
```ts
|
|
381
|
+
// server: the directory with mavonengine.json
|
|
382
|
+
const server = new MyServer(logger, physicsWorld, { plugins: { root: projectRoot } })
|
|
383
|
+
await server.start()
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
The client's Vite config needs to extend `@mavonengine/core/vite.config`, which provides `virtual:mavonengine/plugins`, serves plugin assets in dev and copies them into the release.
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
DEFAULT_DOCS_URL,
|
|
4
|
+
version
|
|
5
|
+
} from "./chunk-XPKCYSMN.js";
|
|
6
|
+
|
|
7
|
+
// src/mcp/docs.ts
|
|
8
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
9
|
+
import { z as z2 } from "zod";
|
|
10
|
+
|
|
11
|
+
// src/mcp/result.ts
|
|
12
|
+
import { z } from "zod";
|
|
13
|
+
var READ_ONLY = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
|
|
14
|
+
var LOG_LEVELS = ["error", "warn", "info", "http", "verbose", "debug", "silly"];
|
|
15
|
+
var vec3 = z.object({ x: z.number(), y: z.number(), z: z.number() });
|
|
16
|
+
function json(data) {
|
|
17
|
+
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
18
|
+
}
|
|
19
|
+
async function run(body) {
|
|
20
|
+
try {
|
|
21
|
+
return json(await body());
|
|
22
|
+
} catch (err) {
|
|
23
|
+
return { isError: true, content: [{ type: "text", text: err.message }] };
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// src/mcp/docs.ts
|
|
28
|
+
var REQUEST_TIMEOUT_MS = 15e3;
|
|
29
|
+
var DocsClient = class {
|
|
30
|
+
constructor(url = DEFAULT_DOCS_URL, connect = connectHttp) {
|
|
31
|
+
this.url = url;
|
|
32
|
+
this.connect = connect;
|
|
33
|
+
}
|
|
34
|
+
url;
|
|
35
|
+
connect;
|
|
36
|
+
client;
|
|
37
|
+
async call(tool, args = {}) {
|
|
38
|
+
this.client ??= this.connect(this.url);
|
|
39
|
+
try {
|
|
40
|
+
const client = await this.client;
|
|
41
|
+
return await client.callTool({ name: tool, arguments: args }, void 0, { timeout: REQUEST_TIMEOUT_MS });
|
|
42
|
+
} catch (err) {
|
|
43
|
+
const failed = this.client;
|
|
44
|
+
this.client = void 0;
|
|
45
|
+
void failed.then((client) => client.close()).catch(() => {
|
|
46
|
+
});
|
|
47
|
+
throw new Error(`Could not read the MavonEngine docs at ${this.url}: ${err.message}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
async close() {
|
|
51
|
+
const client = this.client;
|
|
52
|
+
this.client = void 0;
|
|
53
|
+
await client?.then((c) => c.close()).catch(() => {
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
async function connectHttp(url) {
|
|
58
|
+
const { StreamableHTTPClientTransport } = await import("@modelcontextprotocol/sdk/client/streamableHttp.js");
|
|
59
|
+
const client = new Client({ name: "mavonengine-cli", version });
|
|
60
|
+
await client.connect(new StreamableHTTPClientTransport(new URL(url)));
|
|
61
|
+
return client;
|
|
62
|
+
}
|
|
63
|
+
function registerDocsTools(server, docs) {
|
|
64
|
+
const forward = async (tool, args) => {
|
|
65
|
+
try {
|
|
66
|
+
return await docs.call(tool, args);
|
|
67
|
+
} catch (err) {
|
|
68
|
+
return { isError: true, content: [{ type: "text", text: err.message }] };
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
server.registerTool("docs_list_pages", {
|
|
72
|
+
title: "List MavonEngine docs pages",
|
|
73
|
+
description: "Every page of the MavonEngine documentation (mavonengine.com) with its path, title, description and category. Use it to find the page about an engine concept or API, then read it with docs_get_page.",
|
|
74
|
+
annotations: { ...READ_ONLY, openWorldHint: true }
|
|
75
|
+
}, () => forward("list-pages"));
|
|
76
|
+
server.registerTool("docs_get_page", {
|
|
77
|
+
title: "Read a MavonEngine docs page",
|
|
78
|
+
description: "One page of the MavonEngine documentation as Markdown, by its path from docs_list_pages (e.g. /networking/state-sync).",
|
|
79
|
+
inputSchema: { path: z2.string().min(1).describe("Page path, e.g. /getting-started/installation") },
|
|
80
|
+
annotations: { ...READ_ONLY, openWorldHint: true }
|
|
81
|
+
}, ({ path }) => forward("get-page", { path }));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export {
|
|
85
|
+
READ_ONLY,
|
|
86
|
+
LOG_LEVELS,
|
|
87
|
+
vec3,
|
|
88
|
+
run,
|
|
89
|
+
DocsClient,
|
|
90
|
+
registerDocsTools
|
|
91
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// package.json
|
|
4
|
+
var version = "0.0.3";
|
|
5
|
+
|
|
6
|
+
// src/mcp/config.ts
|
|
7
|
+
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
8
|
+
import { join } from "path";
|
|
9
|
+
var DEFAULT_DOCS_URL = "https://mavonengine.com/mcp";
|
|
10
|
+
var MCP_SERVER_NAME = "mavonengine";
|
|
11
|
+
var MCP_CONFIG_FILE = ".mcp.json";
|
|
12
|
+
function mcpServerEntry() {
|
|
13
|
+
return {
|
|
14
|
+
command: "npx",
|
|
15
|
+
args: ["-y", "@mavonengine/cli", "mcp"]
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
function writeMcpConfig(dir) {
|
|
19
|
+
const file = join(dir, MCP_CONFIG_FILE);
|
|
20
|
+
let config = {};
|
|
21
|
+
if (existsSync(file)) {
|
|
22
|
+
try {
|
|
23
|
+
config = JSON.parse(readFileSync(file, "utf8"));
|
|
24
|
+
} catch {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
if (!config || typeof config !== "object" || Array.isArray(config))
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
const servers = config.mcpServers && typeof config.mcpServers === "object" ? config.mcpServers : {};
|
|
31
|
+
if (MCP_SERVER_NAME in servers)
|
|
32
|
+
return false;
|
|
33
|
+
config.mcpServers = { ...servers, [MCP_SERVER_NAME]: mcpServerEntry() };
|
|
34
|
+
writeFileSync(file, `${JSON.stringify(config, null, 2)}
|
|
35
|
+
`);
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export {
|
|
40
|
+
version,
|
|
41
|
+
DEFAULT_DOCS_URL,
|
|
42
|
+
MCP_CONFIG_FILE,
|
|
43
|
+
writeMcpConfig
|
|
44
|
+
};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// ../core/dist/Plugins/registry.js
|
|
4
|
+
var PLUGIN_REGISTRY_URL = "https://raw.githubusercontent.com/MavonEngine/Core/main/plugins.json";
|
|
5
|
+
var PLUGIN_REGISTRY_ENV = "MAVONENGINE_PLUGINS_URL";
|
|
6
|
+
var SUPPORTED_REGISTRY_VERSION = 1;
|
|
7
|
+
var PluginRegistryError = class extends Error {
|
|
8
|
+
};
|
|
9
|
+
function text(value) {
|
|
10
|
+
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
11
|
+
}
|
|
12
|
+
function normalizeRegistry(raw, url) {
|
|
13
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw) || !Array.isArray(raw.plugins))
|
|
14
|
+
throw new PluginRegistryError(`${url} is not a plugin list: expected { "plugins": [...] }`);
|
|
15
|
+
const { version, plugins } = raw;
|
|
16
|
+
return {
|
|
17
|
+
url,
|
|
18
|
+
version: typeof version === "number" ? version : 1,
|
|
19
|
+
plugins: plugins.flatMap((entry) => {
|
|
20
|
+
const plugin = entry && typeof entry === "object" ? entry : {};
|
|
21
|
+
const pkg = text(plugin.package);
|
|
22
|
+
if (!pkg)
|
|
23
|
+
return [];
|
|
24
|
+
const target = plugin.target === "client" || plugin.target === "server" || plugin.target === "both" ? plugin.target : void 0;
|
|
25
|
+
return [{
|
|
26
|
+
package: pkg,
|
|
27
|
+
name: text(plugin.name) ?? pkg,
|
|
28
|
+
description: text(plugin.description),
|
|
29
|
+
target,
|
|
30
|
+
engine: text(plugin.engine),
|
|
31
|
+
homepage: text(plugin.homepage),
|
|
32
|
+
readme: text(plugin.readme)
|
|
33
|
+
}];
|
|
34
|
+
})
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
function notInProject(plugins, inProject) {
|
|
38
|
+
const have = new Set(inProject);
|
|
39
|
+
return plugins.filter((plugin) => !have.has(plugin.package));
|
|
40
|
+
}
|
|
41
|
+
var HTTP_URL_RE = /^https?:\/\//i;
|
|
42
|
+
var MAX_REMOTE_README_CHARS = 2e5;
|
|
43
|
+
async function fetchPluginReadme(plugin, { timeoutMs = 1e4, fetch = globalThis.fetch } = {}) {
|
|
44
|
+
const url = plugin.readme;
|
|
45
|
+
if (!url || !HTTP_URL_RE.test(url))
|
|
46
|
+
throw new PluginRegistryError(`The list has no readme for ${plugin.package}`);
|
|
47
|
+
let res;
|
|
48
|
+
try {
|
|
49
|
+
res = await fetch(url, { cache: "no-store", signal: AbortSignal.timeout(timeoutMs) });
|
|
50
|
+
} catch (err) {
|
|
51
|
+
throw new PluginRegistryError(`Can't reach ${url}: ${err.message}`);
|
|
52
|
+
}
|
|
53
|
+
if (!res.ok)
|
|
54
|
+
throw new PluginRegistryError(res.status === 404 ? `${url} was not found` : `${url} answered ${res.status} ${res.statusText}`);
|
|
55
|
+
const markdown = await res.text();
|
|
56
|
+
return markdown.length > MAX_REMOTE_README_CHARS ? `${markdown.slice(0, MAX_REMOTE_README_CHARS)}
|
|
57
|
+
|
|
58
|
+
\u2026 (cut off)` : markdown;
|
|
59
|
+
}
|
|
60
|
+
async function fetchPluginRegistry({ url = PLUGIN_REGISTRY_URL, timeoutMs = 1e4, fetch = globalThis.fetch } = {}) {
|
|
61
|
+
let res;
|
|
62
|
+
try {
|
|
63
|
+
res = await fetch(url, { cache: "no-store", signal: AbortSignal.timeout(timeoutMs) });
|
|
64
|
+
} catch (err) {
|
|
65
|
+
throw new PluginRegistryError(`Can't reach ${url}: ${err.message}`);
|
|
66
|
+
}
|
|
67
|
+
if (res.status === 404)
|
|
68
|
+
throw new PluginRegistryError(`${url} was not found`);
|
|
69
|
+
if (!res.ok)
|
|
70
|
+
throw new PluginRegistryError(`${url} answered ${res.status} ${res.statusText}`);
|
|
71
|
+
let raw;
|
|
72
|
+
try {
|
|
73
|
+
raw = JSON.parse(await res.text());
|
|
74
|
+
} catch (err) {
|
|
75
|
+
throw new PluginRegistryError(`${url} is not valid JSON: ${err.message}`);
|
|
76
|
+
}
|
|
77
|
+
return normalizeRegistry(raw, url);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export {
|
|
81
|
+
PLUGIN_REGISTRY_URL,
|
|
82
|
+
PLUGIN_REGISTRY_ENV,
|
|
83
|
+
SUPPORTED_REGISTRY_VERSION,
|
|
84
|
+
notInProject,
|
|
85
|
+
fetchPluginReadme,
|
|
86
|
+
fetchPluginRegistry
|
|
87
|
+
};
|