@mavonengine/cli 0.0.0-stage → 0.0.1
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 +106 -2
- package/dist/index.js +1830 -0
- package/package.json +45 -4
package/dist/index.js
ADDED
|
@@ -0,0 +1,1830 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import process6 from "process";
|
|
5
|
+
|
|
6
|
+
// package.json
|
|
7
|
+
var version = "0.0.1";
|
|
8
|
+
|
|
9
|
+
// src/commands/create.ts
|
|
10
|
+
import { existsSync as existsSync3 } from "fs";
|
|
11
|
+
import process5 from "process";
|
|
12
|
+
import { createInterface } from "readline/promises";
|
|
13
|
+
import { parseArgs as parseArgs3 } from "util";
|
|
14
|
+
|
|
15
|
+
// src/create/project.ts
|
|
16
|
+
import { spawn } from "child_process";
|
|
17
|
+
import { createWriteStream, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
|
|
18
|
+
import { tmpdir } from "os";
|
|
19
|
+
import { dirname, isAbsolute, join, relative, resolve } from "path";
|
|
20
|
+
import process from "process";
|
|
21
|
+
import { Readable, Transform } from "stream";
|
|
22
|
+
import { pipeline } from "stream/promises";
|
|
23
|
+
import AdmZip from "adm-zip";
|
|
24
|
+
|
|
25
|
+
// src/dashboard/format.ts
|
|
26
|
+
var BYTE_UNITS = ["B", "KiB", "MiB", "GiB", "TiB"];
|
|
27
|
+
function formatBytes(bytes, decimals = 1) {
|
|
28
|
+
if (!Number.isFinite(bytes) || bytes <= 0)
|
|
29
|
+
return "0 B";
|
|
30
|
+
const exponent = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), BYTE_UNITS.length - 1);
|
|
31
|
+
const value = bytes / 1024 ** exponent;
|
|
32
|
+
return `${exponent === 0 ? value : value.toFixed(decimals)} ${BYTE_UNITS[exponent]}`;
|
|
33
|
+
}
|
|
34
|
+
function formatRate(bytesPerSecond) {
|
|
35
|
+
return `${formatBytes(bytesPerSecond)}/s`;
|
|
36
|
+
}
|
|
37
|
+
function formatDuration(totalSeconds) {
|
|
38
|
+
const seconds = Math.max(0, Math.floor(totalSeconds));
|
|
39
|
+
const d = Math.floor(seconds / 86400);
|
|
40
|
+
const h = Math.floor(seconds % 86400 / 3600);
|
|
41
|
+
const m = Math.floor(seconds % 3600 / 60);
|
|
42
|
+
const s = seconds % 60;
|
|
43
|
+
if (d > 0)
|
|
44
|
+
return `${d}d ${h}h ${m}m`;
|
|
45
|
+
if (h > 0)
|
|
46
|
+
return `${h}h ${m}m ${s}s`;
|
|
47
|
+
if (m > 0)
|
|
48
|
+
return `${m}m ${s}s`;
|
|
49
|
+
return `${s}s`;
|
|
50
|
+
}
|
|
51
|
+
function formatMs(ms) {
|
|
52
|
+
return `${ms.toFixed(ms < 10 ? 2 : 1)} ms`;
|
|
53
|
+
}
|
|
54
|
+
function formatPercent(value) {
|
|
55
|
+
return `${value.toFixed(1)}%`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// src/create/project.ts
|
|
59
|
+
var TEMPLATE_URL = "https://github.com/MavonEngine/Core/archive/refs/tags/0.0.15-alpha.zip";
|
|
60
|
+
var TEMPLATE_SUBPATH = "packages/multiplayer-template";
|
|
61
|
+
var PROJECT_NAME_RE = /^[\w@./-][\w@./\- ]*$/;
|
|
62
|
+
function projectDirFor(projectName, parentDir) {
|
|
63
|
+
return resolve(parentDir, projectName);
|
|
64
|
+
}
|
|
65
|
+
async function downloadZip(url, { report, signal, fetch = globalThis.fetch }) {
|
|
66
|
+
const response = await fetch(url, { signal });
|
|
67
|
+
if (!response.ok || !response.body)
|
|
68
|
+
throw new Error(`Failed to download ${url}: ${response.status} ${response.statusText}`);
|
|
69
|
+
const total = Number(response.headers.get("content-length")) || 0;
|
|
70
|
+
let received = 0;
|
|
71
|
+
const progress = new Transform({
|
|
72
|
+
transform(chunk, _encoding, callback) {
|
|
73
|
+
received += chunk.length;
|
|
74
|
+
report.step(`Downloading the template\u2026 ${formatBytes(received)}${total ? ` of ${formatBytes(total)}` : ""}`);
|
|
75
|
+
callback(null, chunk);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
const dest = join(tmpdir(), `mavonengine-create-${process.pid}-${Date.now()}.zip`);
|
|
79
|
+
try {
|
|
80
|
+
await pipeline(Readable.fromWeb(response.body), progress, createWriteStream(dest), { signal });
|
|
81
|
+
} catch (err) {
|
|
82
|
+
rmSync(dest, { force: true });
|
|
83
|
+
throw err;
|
|
84
|
+
}
|
|
85
|
+
return dest;
|
|
86
|
+
}
|
|
87
|
+
function extractTemplate(zipPath, templateSubpath, destDir) {
|
|
88
|
+
const entries = new AdmZip(zipPath).getEntries();
|
|
89
|
+
if (entries.length === 0)
|
|
90
|
+
throw new Error("Downloaded zip archive is empty");
|
|
91
|
+
const rootFolder = entries[0].entryName.split("/")[0];
|
|
92
|
+
const templatePrefix = `${rootFolder}/${templateSubpath}/`;
|
|
93
|
+
const templateEntries = entries.filter((e) => e.entryName.startsWith(templatePrefix));
|
|
94
|
+
if (templateEntries.length === 0)
|
|
95
|
+
throw new Error(`Template path "${templateSubpath}" not found in zip archive (root: ${rootFolder})`);
|
|
96
|
+
const root = resolve(destDir);
|
|
97
|
+
let files = 0;
|
|
98
|
+
for (const entry of templateEntries) {
|
|
99
|
+
const relativePath = entry.entryName.slice(templatePrefix.length);
|
|
100
|
+
if (!relativePath)
|
|
101
|
+
continue;
|
|
102
|
+
const outputPath = resolve(root, relativePath);
|
|
103
|
+
const inside = relative(root, outputPath);
|
|
104
|
+
if (inside.startsWith("..") || isAbsolute(inside))
|
|
105
|
+
throw new Error(`Refusing to extract ${entry.entryName} outside the project`);
|
|
106
|
+
if (entry.isDirectory) {
|
|
107
|
+
mkdirSync(outputPath, { recursive: true });
|
|
108
|
+
} else {
|
|
109
|
+
mkdirSync(dirname(outputPath), { recursive: true });
|
|
110
|
+
writeFileSync(outputPath, entry.getData());
|
|
111
|
+
files++;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return files;
|
|
115
|
+
}
|
|
116
|
+
function runCommand(command, args, cwd, { report, signal }) {
|
|
117
|
+
return new Promise((resolvePromise, reject) => {
|
|
118
|
+
const child = spawn(command, args, {
|
|
119
|
+
cwd,
|
|
120
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
121
|
+
// npm is a .cmd shim on Windows
|
|
122
|
+
shell: process.platform === "win32",
|
|
123
|
+
signal
|
|
124
|
+
});
|
|
125
|
+
child.stdout.setEncoding("utf8").on("data", report.output);
|
|
126
|
+
child.stderr.setEncoding("utf8").on("data", report.output);
|
|
127
|
+
child.on("error", (err) => {
|
|
128
|
+
reject(err.code === "ENOENT" ? new Error(`'${command}' was not found on your PATH`) : err);
|
|
129
|
+
});
|
|
130
|
+
child.on("exit", (code, exitSignal) => {
|
|
131
|
+
if (code === 0)
|
|
132
|
+
resolvePromise();
|
|
133
|
+
else
|
|
134
|
+
reject(new Error(`'${command} ${args.join(" ")}' ${code === null ? `was stopped by ${exitSignal}` : `exited with code ${code}`}`));
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
async function createProject({ projectName, parentDir, report, signal, templateUrl = TEMPLATE_URL, fetch, install = true }) {
|
|
139
|
+
const projectDir = projectDirFor(projectName, parentDir);
|
|
140
|
+
if (existsSync(projectDir))
|
|
141
|
+
throw new Error(`Directory "${projectDir}" already exists`);
|
|
142
|
+
report.output(`Creating project "${projectName}" in ${projectDir}
|
|
143
|
+
`);
|
|
144
|
+
mkdirSync(projectDir, { recursive: true });
|
|
145
|
+
report.step("Downloading the template\u2026");
|
|
146
|
+
report.output(`Downloading the template from ${templateUrl}
|
|
147
|
+
`);
|
|
148
|
+
const zipPath = await downloadZip(templateUrl, { report, signal, fetch });
|
|
149
|
+
try {
|
|
150
|
+
report.step("Extracting the template\u2026");
|
|
151
|
+
const files = extractTemplate(zipPath, TEMPLATE_SUBPATH, projectDir);
|
|
152
|
+
report.output(`Extracted ${files} files
|
|
153
|
+
`);
|
|
154
|
+
} finally {
|
|
155
|
+
rmSync(zipPath, { force: true });
|
|
156
|
+
}
|
|
157
|
+
const pkgJsonPath = join(projectDir, "package.json");
|
|
158
|
+
if (existsSync(pkgJsonPath)) {
|
|
159
|
+
const pkg = JSON.parse(readFileSync(pkgJsonPath, "utf-8"));
|
|
160
|
+
pkg.name = projectName;
|
|
161
|
+
writeFileSync(pkgJsonPath, `${JSON.stringify(pkg, null, 2)}
|
|
162
|
+
`);
|
|
163
|
+
}
|
|
164
|
+
if (install) {
|
|
165
|
+
signal?.throwIfAborted();
|
|
166
|
+
report.step("Installing dependencies\u2026");
|
|
167
|
+
report.output("Running 'npm install'\n");
|
|
168
|
+
await runCommand("npm", ["install"], projectDir, { report, signal });
|
|
169
|
+
}
|
|
170
|
+
return projectDir;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// src/dashboard/snapshot.ts
|
|
174
|
+
var SUPPORTED_SCHEMA_VERSION = 1;
|
|
175
|
+
function isRecord(value) {
|
|
176
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
177
|
+
}
|
|
178
|
+
function num(value, fallback = 0) {
|
|
179
|
+
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
180
|
+
}
|
|
181
|
+
function str(value, fallback = "") {
|
|
182
|
+
return typeof value === "string" ? value : fallback;
|
|
183
|
+
}
|
|
184
|
+
function numOrNull(value) {
|
|
185
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
186
|
+
}
|
|
187
|
+
function strOrNull(value) {
|
|
188
|
+
return typeof value === "string" ? value : null;
|
|
189
|
+
}
|
|
190
|
+
function arr(value, map) {
|
|
191
|
+
return Array.isArray(value) ? value.filter(isRecord).map(map) : [];
|
|
192
|
+
}
|
|
193
|
+
function section(value) {
|
|
194
|
+
return isRecord(value) ? value : {};
|
|
195
|
+
}
|
|
196
|
+
function tickSample(t) {
|
|
197
|
+
return {
|
|
198
|
+
timestamp: num(t.timestamp),
|
|
199
|
+
ticks: num(t.ticks),
|
|
200
|
+
ticksPerSecond: num(t.ticksPerSecond),
|
|
201
|
+
avgMs: num(t.avgMs),
|
|
202
|
+
maxMs: num(t.maxMs),
|
|
203
|
+
commandsPerSecond: num(t.commandsPerSecond),
|
|
204
|
+
maxCommandsPerTick: num(t.maxCommandsPerTick)
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
function normalizeSnapshot(raw) {
|
|
208
|
+
const s = section(raw);
|
|
209
|
+
const world = section(s.world);
|
|
210
|
+
const cpu = section(s.cpu);
|
|
211
|
+
const bandwidth = section(s.bandwidth);
|
|
212
|
+
const tick = section(s.tick);
|
|
213
|
+
const memory = section(s.memory);
|
|
214
|
+
const physics = isRecord(s.physics) ? s.physics : null;
|
|
215
|
+
const latency = isRecord(s.latencySimulation) ? s.latencySimulation : null;
|
|
216
|
+
return {
|
|
217
|
+
schemaVersion: num(s.schemaVersion, SUPPORTED_SCHEMA_VERSION),
|
|
218
|
+
engineVersion: str(s.engineVersion, "unknown"),
|
|
219
|
+
serverTime: str(s.serverTime),
|
|
220
|
+
uptime: num(s.uptime),
|
|
221
|
+
sampleIntervalMs: num(s.sampleIntervalMs, 1e3) || 1e3,
|
|
222
|
+
tickRate: num(s.tickRate),
|
|
223
|
+
players: arr(s.players, (p) => {
|
|
224
|
+
const position = section(p.position);
|
|
225
|
+
const connection = isRecord(p.connection) ? p.connection : null;
|
|
226
|
+
const id = str(p.id, "?");
|
|
227
|
+
return {
|
|
228
|
+
id,
|
|
229
|
+
// Older servers don't send a name
|
|
230
|
+
displayName: str(p.displayName, id),
|
|
231
|
+
position: { x: num(position.x), y: num(position.y), z: num(position.z) },
|
|
232
|
+
lastProcessedSequenceId: num(p.lastProcessedSequenceId),
|
|
233
|
+
trackedEntities: num(p.trackedEntities),
|
|
234
|
+
connectedFor: num(p.connectedFor),
|
|
235
|
+
connection: connection ? {
|
|
236
|
+
address: strOrNull(connection.address),
|
|
237
|
+
rttMs: numOrNull(connection.rttMs),
|
|
238
|
+
bytesSent: num(connection.bytesSent),
|
|
239
|
+
bytesReceived: num(connection.bytesReceived)
|
|
240
|
+
} : null
|
|
241
|
+
};
|
|
242
|
+
}),
|
|
243
|
+
world: {
|
|
244
|
+
entities: num(world.entities)
|
|
245
|
+
},
|
|
246
|
+
physics: physics ? { bodies: num(physics.bodies), colliders: num(physics.colliders) } : null,
|
|
247
|
+
cpu: {
|
|
248
|
+
current: num(cpu.current),
|
|
249
|
+
avg1s: num(cpu.avg1s),
|
|
250
|
+
avg10s: num(cpu.avg10s),
|
|
251
|
+
avg60s: num(cpu.avg60s),
|
|
252
|
+
history: arr(cpu.history, (h) => ({ timestamp: num(h.timestamp), cpuPercentage: num(h.cpuPercentage) }))
|
|
253
|
+
},
|
|
254
|
+
bandwidth: {
|
|
255
|
+
sentPerSecond: num(bandwidth.sentPerSecond),
|
|
256
|
+
receivedPerSecond: num(bandwidth.receivedPerSecond),
|
|
257
|
+
history: arr(bandwidth.history, (b) => ({ timestamp: num(b.timestamp), sent: num(b.sent), received: num(b.received) }))
|
|
258
|
+
},
|
|
259
|
+
tick: {
|
|
260
|
+
budgetMs: num(tick.budgetMs),
|
|
261
|
+
latest: isRecord(tick.latest) ? tickSample(tick.latest) : null,
|
|
262
|
+
history: arr(tick.history, tickSample)
|
|
263
|
+
},
|
|
264
|
+
memory: {
|
|
265
|
+
rss: num(memory.rss),
|
|
266
|
+
heapUsed: num(memory.heapUsed),
|
|
267
|
+
heapTotal: num(memory.heapTotal),
|
|
268
|
+
external: num(memory.external)
|
|
269
|
+
},
|
|
270
|
+
latencySimulation: latency ? { fixedLatency: num(latency.fixedLatency), jitter: num(latency.jitter), packetLoss: num(latency.packetLoss) } : null
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
function normalizeLogs(raw) {
|
|
274
|
+
const r = section(raw);
|
|
275
|
+
const entries = arr(r.entries, (e) => ({
|
|
276
|
+
id: num(e.id),
|
|
277
|
+
timestamp: num(e.timestamp),
|
|
278
|
+
level: str(e.level, "info"),
|
|
279
|
+
message: str(e.message)
|
|
280
|
+
}));
|
|
281
|
+
return {
|
|
282
|
+
entries,
|
|
283
|
+
latestId: num(r.latestId, entries.at(-1)?.id ?? 0)
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
function normalizeRequests(raw) {
|
|
287
|
+
const r = section(raw);
|
|
288
|
+
const entries = arr(r.entries, (e) => ({
|
|
289
|
+
id: num(e.id),
|
|
290
|
+
timestamp: num(e.timestamp),
|
|
291
|
+
method: str(e.method, "?"),
|
|
292
|
+
path: str(e.path),
|
|
293
|
+
status: numOrNull(e.status),
|
|
294
|
+
durationMs: num(e.durationMs),
|
|
295
|
+
bytes: num(e.bytes),
|
|
296
|
+
address: strOrNull(e.address)
|
|
297
|
+
}));
|
|
298
|
+
return {
|
|
299
|
+
entries,
|
|
300
|
+
latestId: num(r.latestId, entries.at(-1)?.id ?? 0)
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// src/dashboard/MetricsClient.ts
|
|
305
|
+
var MAX_LOG_LINES = 1e3;
|
|
306
|
+
var MAX_REQUEST_LINES = 1e3;
|
|
307
|
+
var MetricsSourceError = class extends Error {
|
|
308
|
+
constructor(message, status) {
|
|
309
|
+
super(message);
|
|
310
|
+
this.status = status;
|
|
311
|
+
}
|
|
312
|
+
status;
|
|
313
|
+
};
|
|
314
|
+
function httpSource({ url, token, timeoutMs = 3e3, fetch = globalThis.fetch }) {
|
|
315
|
+
const logsUrl = new URL("logs", url);
|
|
316
|
+
const requestsUrl = new URL("requests", url);
|
|
317
|
+
async function request(target) {
|
|
318
|
+
let res;
|
|
319
|
+
try {
|
|
320
|
+
res = await fetch(target, {
|
|
321
|
+
headers: token ? { Authorization: `Bearer ${token}` } : {},
|
|
322
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
323
|
+
});
|
|
324
|
+
} catch (err) {
|
|
325
|
+
throw new MetricsSourceError(`Can't reach ${url}: ${err.message}`, "unreachable");
|
|
326
|
+
}
|
|
327
|
+
if (res.status === 401)
|
|
328
|
+
throw new MetricsSourceError("Metrics token missing or wrong (use --token)", "unauthorized");
|
|
329
|
+
if (res.status === 403)
|
|
330
|
+
throw new MetricsSourceError("Server only allows metrics from localhost without a token (use --token)", "forbidden");
|
|
331
|
+
if (res.status === 404)
|
|
332
|
+
throw new MetricsSourceError("Metrics endpoint not found: it is disabled, or the server runs a @mavonengine/core release without it", "disabled");
|
|
333
|
+
if (!res.ok)
|
|
334
|
+
throw new MetricsSourceError(`Server responded ${res.status} ${res.statusText}`, "unreachable");
|
|
335
|
+
return res.json();
|
|
336
|
+
}
|
|
337
|
+
return {
|
|
338
|
+
label: url,
|
|
339
|
+
fetch: () => request(url),
|
|
340
|
+
fetchLogs: (afterId) => {
|
|
341
|
+
logsUrl.searchParams.set("after", String(afterId));
|
|
342
|
+
return request(logsUrl.toString());
|
|
343
|
+
},
|
|
344
|
+
fetchRequests: (afterId) => {
|
|
345
|
+
requestsUrl.searchParams.set("after", String(afterId));
|
|
346
|
+
return request(requestsUrl.toString());
|
|
347
|
+
}
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
var Feed = class {
|
|
351
|
+
constructor(fetchPage, normalize, max) {
|
|
352
|
+
this.fetchPage = fetchPage;
|
|
353
|
+
this.normalize = normalize;
|
|
354
|
+
this.max = max;
|
|
355
|
+
this.available = fetchPage ? null : false;
|
|
356
|
+
}
|
|
357
|
+
fetchPage;
|
|
358
|
+
normalize;
|
|
359
|
+
max;
|
|
360
|
+
entries = [];
|
|
361
|
+
/** false once the server turned out not to serve it, null until known */
|
|
362
|
+
available;
|
|
363
|
+
lastId = 0;
|
|
364
|
+
async poll() {
|
|
365
|
+
if (!this.fetchPage || this.available === false)
|
|
366
|
+
return;
|
|
367
|
+
try {
|
|
368
|
+
let { entries, latestId } = this.normalize(await this.fetchPage(this.lastId));
|
|
369
|
+
if (latestId < this.lastId) {
|
|
370
|
+
this.entries = [];
|
|
371
|
+
this.lastId = 0;
|
|
372
|
+
({ entries, latestId } = this.normalize(await this.fetchPage(0)));
|
|
373
|
+
}
|
|
374
|
+
this.available = true;
|
|
375
|
+
this.entries.push(...entries.filter((entry) => entry.id > this.lastId));
|
|
376
|
+
this.lastId = Math.max(this.lastId, latestId);
|
|
377
|
+
if (this.entries.length > this.max)
|
|
378
|
+
this.entries.splice(0, this.entries.length - this.max);
|
|
379
|
+
} catch (err) {
|
|
380
|
+
if (err instanceof MetricsSourceError && err.status === "disabled")
|
|
381
|
+
this.available = false;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
};
|
|
385
|
+
var MetricsClient = class {
|
|
386
|
+
constructor(source, { intervalMs = 1e3, maxBackoffMs = 1e4, startupIntervalMs } = {}) {
|
|
387
|
+
this.source = source;
|
|
388
|
+
this.intervalMs = intervalMs;
|
|
389
|
+
this.maxBackoffMs = maxBackoffMs;
|
|
390
|
+
this.startupIntervalMs = startupIntervalMs;
|
|
391
|
+
this.logs = new Feed(source.fetchLogs?.bind(source), normalizeLogs, MAX_LOG_LINES);
|
|
392
|
+
this.requests = new Feed(source.fetchRequests?.bind(source), normalizeRequests, MAX_REQUEST_LINES);
|
|
393
|
+
this.state = {
|
|
394
|
+
status: "connecting",
|
|
395
|
+
source: source.label,
|
|
396
|
+
snapshot: null,
|
|
397
|
+
lastUpdate: null,
|
|
398
|
+
error: null,
|
|
399
|
+
failures: 0,
|
|
400
|
+
logs: [],
|
|
401
|
+
logsAvailable: this.logs.available,
|
|
402
|
+
requests: [],
|
|
403
|
+
requestsAvailable: this.requests.available
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
source;
|
|
407
|
+
state;
|
|
408
|
+
intervalMs;
|
|
409
|
+
maxBackoffMs;
|
|
410
|
+
startupIntervalMs;
|
|
411
|
+
timer = null;
|
|
412
|
+
running = false;
|
|
413
|
+
listeners = /* @__PURE__ */ new Set();
|
|
414
|
+
logs;
|
|
415
|
+
requests;
|
|
416
|
+
onChange(listener) {
|
|
417
|
+
this.listeners.add(listener);
|
|
418
|
+
return () => this.listeners.delete(listener);
|
|
419
|
+
}
|
|
420
|
+
start() {
|
|
421
|
+
if (this.running)
|
|
422
|
+
return;
|
|
423
|
+
this.running = true;
|
|
424
|
+
void this.loop();
|
|
425
|
+
}
|
|
426
|
+
stop() {
|
|
427
|
+
this.running = false;
|
|
428
|
+
if (this.timer)
|
|
429
|
+
clearTimeout(this.timer);
|
|
430
|
+
this.timer = null;
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* Poll once now, skipping the wait.
|
|
434
|
+
*/
|
|
435
|
+
async refresh() {
|
|
436
|
+
if (this.timer)
|
|
437
|
+
clearTimeout(this.timer);
|
|
438
|
+
this.timer = null;
|
|
439
|
+
await this.loop();
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* Fetch one snapshot, then any new log lines and requests, and update the state.
|
|
443
|
+
*/
|
|
444
|
+
async poll() {
|
|
445
|
+
try {
|
|
446
|
+
const raw = await this.source.fetch();
|
|
447
|
+
this.state.snapshot = normalizeSnapshot(raw);
|
|
448
|
+
this.state.status = "live";
|
|
449
|
+
this.state.lastUpdate = Date.now();
|
|
450
|
+
this.state.error = null;
|
|
451
|
+
this.state.failures = 0;
|
|
452
|
+
await Promise.all([this.logs.poll(), this.requests.poll()]);
|
|
453
|
+
this.state.logs = this.logs.entries;
|
|
454
|
+
this.state.logsAvailable = this.logs.available;
|
|
455
|
+
this.state.requests = this.requests.entries;
|
|
456
|
+
this.state.requestsAvailable = this.requests.available;
|
|
457
|
+
} catch (err) {
|
|
458
|
+
this.state.status = err instanceof MetricsSourceError ? err.status : "unreachable";
|
|
459
|
+
this.state.error = err.message;
|
|
460
|
+
this.state.failures++;
|
|
461
|
+
}
|
|
462
|
+
this.listeners.forEach((listener) => listener(this.state));
|
|
463
|
+
}
|
|
464
|
+
/**
|
|
465
|
+
* Delay before the next poll: the normal interval, doubling per failure up to maxBackoffMs.
|
|
466
|
+
* The startup interval until the server first answered, if one is set.
|
|
467
|
+
*/
|
|
468
|
+
nextDelay() {
|
|
469
|
+
if (this.startupIntervalMs !== void 0 && !this.state.snapshot)
|
|
470
|
+
return this.startupIntervalMs;
|
|
471
|
+
if (this.state.failures === 0)
|
|
472
|
+
return this.intervalMs;
|
|
473
|
+
return Math.min(this.intervalMs * 2 ** this.state.failures, Math.max(this.maxBackoffMs, this.intervalMs));
|
|
474
|
+
}
|
|
475
|
+
async loop() {
|
|
476
|
+
await this.poll();
|
|
477
|
+
if (this.running && !this.timer) {
|
|
478
|
+
this.timer = setTimeout(() => {
|
|
479
|
+
this.timer = null;
|
|
480
|
+
void this.loop();
|
|
481
|
+
}, this.nextDelay());
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
};
|
|
485
|
+
|
|
486
|
+
// src/commands/dashboard.ts
|
|
487
|
+
import process3 from "process";
|
|
488
|
+
import { parseArgs } from "util";
|
|
489
|
+
|
|
490
|
+
// src/browser.ts
|
|
491
|
+
import { spawn as spawn2 } from "child_process";
|
|
492
|
+
import process2 from "process";
|
|
493
|
+
var AMPERSAND_RE = /&/g;
|
|
494
|
+
function openCommand(url, platform = process2.platform) {
|
|
495
|
+
if (platform === "darwin")
|
|
496
|
+
return ["open", [url]];
|
|
497
|
+
if (platform === "win32")
|
|
498
|
+
return ["cmd", ["/c", "start", '""', url.replace(AMPERSAND_RE, "^&")]];
|
|
499
|
+
return ["xdg-open", [url]];
|
|
500
|
+
}
|
|
501
|
+
function openBrowser(url) {
|
|
502
|
+
const [command, args] = openCommand(url);
|
|
503
|
+
try {
|
|
504
|
+
const child = spawn2(command, args, { stdio: "ignore", detached: true, windowsHide: true });
|
|
505
|
+
child.on("error", () => {
|
|
506
|
+
});
|
|
507
|
+
child.unref();
|
|
508
|
+
} catch {
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
function canOpenBrowserHere(env = process2.env) {
|
|
512
|
+
return !env.SSH_CONNECTION && !env.SSH_TTY;
|
|
513
|
+
}
|
|
514
|
+
async function servesPage(url, { timeoutMs = 3e3, fetch = globalThis.fetch } = {}) {
|
|
515
|
+
try {
|
|
516
|
+
const res = await fetch(url, { signal: AbortSignal.timeout(timeoutMs) });
|
|
517
|
+
await res.body?.cancel();
|
|
518
|
+
return res.ok && (res.headers.get("content-type") ?? "").includes("text/html");
|
|
519
|
+
} catch {
|
|
520
|
+
return false;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// src/dashboard/simulator.ts
|
|
525
|
+
var HISTORY_LENGTH = 60;
|
|
526
|
+
function mulberry32(seed) {
|
|
527
|
+
return () => {
|
|
528
|
+
seed |= 0;
|
|
529
|
+
seed = seed + 1831565813 | 0;
|
|
530
|
+
let t = Math.imul(seed ^ seed >>> 15, 1 | seed);
|
|
531
|
+
t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
|
|
532
|
+
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
function push(history, item) {
|
|
536
|
+
history.push(item);
|
|
537
|
+
if (history.length > HISTORY_LENGTH)
|
|
538
|
+
history.shift();
|
|
539
|
+
}
|
|
540
|
+
function simulatedSource(seed = 1, sampleIntervalMs = 1e3) {
|
|
541
|
+
const random = mulberry32(seed);
|
|
542
|
+
const tickRate = 30;
|
|
543
|
+
const budgetMs = Math.round(1e3 / tickRate * 100) / 100;
|
|
544
|
+
let step = 0;
|
|
545
|
+
let now = Date.UTC(2026, 0, 1);
|
|
546
|
+
let sequence = 0;
|
|
547
|
+
const cpuHistory = [];
|
|
548
|
+
const bandwidthHistory = [];
|
|
549
|
+
const tickHistory = [];
|
|
550
|
+
const logs = [];
|
|
551
|
+
let previousPlayers = 0;
|
|
552
|
+
const joinedAt = [];
|
|
553
|
+
const log = (level, message) => {
|
|
554
|
+
logs.push({ id: logs.length + 1, timestamp: now, level, message });
|
|
555
|
+
};
|
|
556
|
+
const requests = [];
|
|
557
|
+
const request = (method, path, status, bytes, address) => {
|
|
558
|
+
requests.push({ id: requests.length + 1, timestamp: now, method, path, status, durationMs: Math.round((1 + random() * 20) * 100) / 100, bytes, address });
|
|
559
|
+
};
|
|
560
|
+
log("info", "Starting @mavon/engine - Socket Server");
|
|
561
|
+
log("info", "Starting @mavon/engine - HTTP Server");
|
|
562
|
+
return {
|
|
563
|
+
label: "simulation",
|
|
564
|
+
async fetch() {
|
|
565
|
+
step++;
|
|
566
|
+
now += sampleIntervalMs;
|
|
567
|
+
sequence += tickRate;
|
|
568
|
+
const playerCount = Math.max(1, Math.round(6 + 5 * Math.sin(step / 20) + random() * 2));
|
|
569
|
+
for (let i = previousPlayers; i < playerCount; i++)
|
|
570
|
+
joinedAt[i] = now;
|
|
571
|
+
const players = Array.from({ length: playerCount }, (_, i) => ({
|
|
572
|
+
id: `sim-${(i + 1).toString().padStart(2, "0")}`,
|
|
573
|
+
displayName: `Player ${i + 1}`,
|
|
574
|
+
position: {
|
|
575
|
+
x: Math.round(Math.cos(step / 10 + i) * (10 + i * 3) * 100) / 100,
|
|
576
|
+
y: 0,
|
|
577
|
+
z: Math.round(Math.sin(step / 10 + i) * (10 + i * 3) * 100) / 100
|
|
578
|
+
},
|
|
579
|
+
lastProcessedSequenceId: sequence - Math.floor(random() * 3),
|
|
580
|
+
trackedEntities: playerCount + 20 + Math.floor(random() * 5),
|
|
581
|
+
connectedFor: Math.round((now - joinedAt[i]) / 1e3),
|
|
582
|
+
connection: {
|
|
583
|
+
address: `203.0.113.${10 + i}`,
|
|
584
|
+
rttMs: Math.round(20 + i * 8 + random() * 10),
|
|
585
|
+
bytesSent: Math.round((now - joinedAt[i]) * (25 + i)),
|
|
586
|
+
bytesReceived: Math.round((now - joinedAt[i]) * (4 + i / 2))
|
|
587
|
+
}
|
|
588
|
+
}));
|
|
589
|
+
for (let i = previousPlayers; i < playerCount; i++) {
|
|
590
|
+
const address = players[i].connection.address;
|
|
591
|
+
request("GET", "/", 200, 1240, address);
|
|
592
|
+
const cached = step % 3 === 0;
|
|
593
|
+
request("GET", "/assets/index-4f2a9c1e.js", cached ? 304 : 200, cached ? 210 : 412e3, address);
|
|
594
|
+
request("GET", "/favicon.ico", 404, 160, address);
|
|
595
|
+
request("POST", "/.wrtc/v2/connections", 200, 2900, address);
|
|
596
|
+
request("POST", `/.wrtc/v2/connections/${players[i].id}/additional-candidates`, 200, 600, address);
|
|
597
|
+
log("info", `Connected: ${players[i].id}`);
|
|
598
|
+
log("info", `${i + 1} total players connected`);
|
|
599
|
+
}
|
|
600
|
+
for (let i = previousPlayers - 1; i >= playerCount; i--) {
|
|
601
|
+
log("info", `Disconnected sim-${(i + 1).toString().padStart(2, "0")}`);
|
|
602
|
+
log("info", `${i} total players connected`);
|
|
603
|
+
}
|
|
604
|
+
previousPlayers = playerCount;
|
|
605
|
+
if (step % 10 === 0)
|
|
606
|
+
request("GET", "/api/game/health", 200, 180, "198.51.100.7");
|
|
607
|
+
if (step % 23 === 0)
|
|
608
|
+
request("GET", "/wp-login.php", 404, 160, "192.0.2.66");
|
|
609
|
+
if (step % 5 === 0)
|
|
610
|
+
log("debug", `Client - Sent: ${playerCount * 4200} B, Received: ${playerCount * 600} B`);
|
|
611
|
+
const cpu = Math.max(0, 12 + playerCount * 2.5 + Math.sin(step / 7) * 6 + random() * 4);
|
|
612
|
+
push(cpuHistory, { timestamp: now, cpuPercentage: Math.round(cpu * 100) / 100 });
|
|
613
|
+
const sent = Math.round(playerCount * (900 + random() * 300) * tickRate * sampleIntervalMs / 1e3);
|
|
614
|
+
const received = Math.round(playerCount * (120 + random() * 60) * tickRate * sampleIntervalMs / 1e3);
|
|
615
|
+
push(bandwidthHistory, { timestamp: now, sent, received });
|
|
616
|
+
const avgMs = 2 + playerCount * 0.6 + random() * 1.5;
|
|
617
|
+
const maxMs = avgMs + random() * 3 + (step % 17 === 0 ? 25 : 0);
|
|
618
|
+
if (maxMs > budgetMs)
|
|
619
|
+
log("warn", `Slow tick: ${maxMs.toFixed(1)} ms (budget ${budgetMs} ms)`);
|
|
620
|
+
const commandsPerSecond = playerCount * tickRate;
|
|
621
|
+
push(tickHistory, {
|
|
622
|
+
timestamp: now,
|
|
623
|
+
ticks: tickRate,
|
|
624
|
+
ticksPerSecond: tickRate - (maxMs > budgetMs ? 1 : 0),
|
|
625
|
+
avgMs: Math.round(avgMs * 100) / 100,
|
|
626
|
+
maxMs: Math.round(maxMs * 100) / 100,
|
|
627
|
+
commandsPerSecond,
|
|
628
|
+
maxCommandsPerTick: playerCount + Math.floor(random() * 3)
|
|
629
|
+
});
|
|
630
|
+
const average = (ms) => {
|
|
631
|
+
const recent = cpuHistory.slice(-Math.max(1, Math.round(ms / sampleIntervalMs)));
|
|
632
|
+
return Math.round(recent.reduce((acc, h) => acc + h.cpuPercentage, 0) / recent.length * 100) / 100;
|
|
633
|
+
};
|
|
634
|
+
const heapUsed = 6e7 + playerCount * 2e6 + step % 30 * 1e6;
|
|
635
|
+
const snapshot = {
|
|
636
|
+
schemaVersion: 1,
|
|
637
|
+
engineVersion: "simulated",
|
|
638
|
+
serverTime: new Date(now).toISOString(),
|
|
639
|
+
uptime: Math.round(step * sampleIntervalMs / 1e3),
|
|
640
|
+
sampleIntervalMs,
|
|
641
|
+
tickRate,
|
|
642
|
+
players,
|
|
643
|
+
world: { entities: playerCount + 140 },
|
|
644
|
+
physics: { bodies: playerCount + 120, colliders: playerCount + 180 },
|
|
645
|
+
cpu: {
|
|
646
|
+
current: cpuHistory.at(-1).cpuPercentage,
|
|
647
|
+
avg1s: average(1e3),
|
|
648
|
+
avg10s: average(1e4),
|
|
649
|
+
avg60s: average(6e4),
|
|
650
|
+
history: [...cpuHistory]
|
|
651
|
+
},
|
|
652
|
+
bandwidth: {
|
|
653
|
+
sentPerSecond: Math.round(sent * 1e3 / sampleIntervalMs),
|
|
654
|
+
receivedPerSecond: Math.round(received * 1e3 / sampleIntervalMs),
|
|
655
|
+
history: [...bandwidthHistory]
|
|
656
|
+
},
|
|
657
|
+
tick: {
|
|
658
|
+
budgetMs,
|
|
659
|
+
latest: tickHistory.at(-1),
|
|
660
|
+
history: [...tickHistory]
|
|
661
|
+
},
|
|
662
|
+
memory: {
|
|
663
|
+
rss: heapUsed + 8e7,
|
|
664
|
+
heapUsed,
|
|
665
|
+
heapTotal: heapUsed + 3e7,
|
|
666
|
+
external: 5e6
|
|
667
|
+
},
|
|
668
|
+
latencySimulation: null
|
|
669
|
+
};
|
|
670
|
+
return snapshot;
|
|
671
|
+
},
|
|
672
|
+
async fetchLogs(afterId) {
|
|
673
|
+
return { entries: logs.filter((entry) => entry.id > afterId), latestId: logs.length };
|
|
674
|
+
},
|
|
675
|
+
async fetchRequests(afterId) {
|
|
676
|
+
return { entries: requests.filter((entry) => entry.id > afterId), latestId: requests.length };
|
|
677
|
+
}
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// src/dashboard/theme.ts
|
|
682
|
+
import { contrast, defineTheme, mix, resolveTheme } from "@profullstack/hqtui";
|
|
683
|
+
var MIN_MUTED_CONTRAST = 6;
|
|
684
|
+
function readableTheme(name) {
|
|
685
|
+
const base = resolveTheme(name);
|
|
686
|
+
let muted = base.muted;
|
|
687
|
+
for (let t = 0.05; contrast(muted, base.background) < MIN_MUTED_CONTRAST && t <= 1; t += 0.05)
|
|
688
|
+
muted = mix(base.muted, base.foreground, t);
|
|
689
|
+
return defineTheme({ name: base.name, muted }, base);
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// src/dashboard/view.ts
|
|
693
|
+
import { stripVTControlCharacters } from "util";
|
|
694
|
+
|
|
695
|
+
// ../core/dist/Utils/BootBanner.js
|
|
696
|
+
function bootBanner({ version: version2, threeRevision, rapierVersion, mode }) {
|
|
697
|
+
const row = (label, value) => `\u2502 ${`${label}:`.padEnd(10)}${value.padEnd(28)} \u2502`;
|
|
698
|
+
return [
|
|
699
|
+
"\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510",
|
|
700
|
+
"\u2502 \u2502",
|
|
701
|
+
"\u2502 MavonEngine \u2502",
|
|
702
|
+
"\u2502 \u2502",
|
|
703
|
+
row("Version", version2),
|
|
704
|
+
...threeRevision ? [row("Three.js", `r${threeRevision}`)] : [],
|
|
705
|
+
...rapierVersion ? [row("Rapier", rapierVersion)] : [],
|
|
706
|
+
row("Mode", mode),
|
|
707
|
+
"\u2502 \u2502",
|
|
708
|
+
"\u2502 https://github.com/MavonEngine/core \u2502",
|
|
709
|
+
"\u2502 https://mavonengine.com \u2502",
|
|
710
|
+
"\u2502 \u2502",
|
|
711
|
+
"\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518"
|
|
712
|
+
];
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
// src/dashboard/view.ts
|
|
716
|
+
import { DEFAULT_COLOR, hex } from "@profullstack/hqtui";
|
|
717
|
+
|
|
718
|
+
// src/dashboard/logo.ts
|
|
719
|
+
var LOGO_SIZES = [
|
|
720
|
+
[
|
|
721
|
+
"------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------",
|
|
722
|
+
"------------------------------------------------------------------------------------------------------------------174c6b1f627f216682194f6f------------------------------------------------------------------------------------------------------------------",
|
|
723
|
+
"------------------------------------------------------------------------------------------------------184a6c2164813d98a958cbd357cdd53e9cae236680194b69------------------------------------------------------------------------------------------------------",
|
|
724
|
+
"------------------------------------------------------------------------------------------------1b50723a8aa255b7c344a2b33a9dad399cae3f9caf51b3c13c92a51c5474------------------------------------------------------------------------------------------------",
|
|
725
|
+
"------------------------------------------------------------------------------------16436524648047a0b14da7b72e7590185a7a2c859d2e8ba1195d7b276c8848a0b34aa5b5296b851a4868------------------------------------------------------------------------------------",
|
|
726
|
+
"------------------------------------------------------------------------------1d56763a8ca250b1be3e8b9e194e6d174c6e29789444b6c646bbc92c7f991a5274194f713884984cacb83d93a5215e7b194668------------------------------------------------------------------------",
|
|
727
|
+
"------------------------------------------------------------------1c4f6f357f9655b2bf479eae245c77------205974358da53796ad389bb03a9bb03693a93692a823637f------20566f4090a353b1bf39879c1d5775174366------------------------------------------------------------",
|
|
728
|
+
"------------------------------------------------------------2a6a844da6b555acb82f6e87------------296c85358ba225667f14496726768e297b91154865215e7732879e28748b194965------2665804ca3b34eabba29728b------------------------------------------------------------",
|
|
729
|
+
"------------------------------------------------1d546d3e8fa152b1be378397194a67------245e7a398fa636879d------------1d54703395aa349aad1e5a73------------2e7e943690a8256782------174869307b904aabba3c93a51d546f------------------------------------------------",
|
|
730
|
+
"------------------------------------194b67347b9161c0cc53b0be1e5b78------1b4e6b2e7d94348ca2225c75------------2d76903492a926728c236c86348da32e7a94------------1f53703188a131859d1e5772------16577441a3b556c4ce3583951a4f6a------------------------------------",
|
|
731
|
+
"------------------------------25637d49a5b552b1bf3a8a9f4aa2b43d91a52d7d963490a7266a83------------1e516e338ba32e7c941c4b66------------------2a728c338da5215975------------23627d3491a82d839b368fa441a4b533889e4aa8b649abb8286b83------------------------------",
|
|
732
|
+
"------------------1e55723c93a54ab1c0307e97174b6c------1d506b3a889b56bfca3c90a4------1a4a672e7c963795ab1f526f------------------------------------1d4d693691a732839a1d516e------33829953bdc93d92a41f5571------17496b2a758f47adba4197a8215c76------------------",
|
|
733
|
+
"------------2b728b50bcc84dbbc92780991e64811f5c781d4f6c------------255d774499ab4db3c041a9b92d7e96------------------------------------------------------------2d799343aabb4fb3c148a1b226647c------------1b4c681f5e7b1e6782257d9545b2c155c3ce318198------------",
|
|
734
|
+
"------1e516d4dc6d463ecf54fc0cc3fa3b43b9caf3898aa3c9daf34869c1c4c69------------2c6c8348a5b23c91a11f526e------------------------------------------------1d4c6a3b8a9d4eaeb7327a8f------------------347f943c9cae3597a93a9caf3ea2b44bbac862e6f156d4df225d75------",
|
|
735
|
+
"------205a774abed14eb6d841a6bc3290a315516e------------307991419fb32c758d------------------3680964ca8b72d6b85------------------------------------29637c4ea9b53b8798------------------2a6c85439fb1368397------------164d6d318ba045aabb4bb1d44dc4da23657f------",
|
|
736
|
+
"------1f5b7a43abbe3e7fb4336097357d9c4dabbb358299154665------1e4e6a3385983992a6225c78------------1f53714197aa3d91a6225a771844651845662156743b8ba1449cad245a76------------1e526b388ea0398d9d1f5670------------2f7a914dafbb3b87a2305e913e7eb643aabf236881------",
|
|
737
|
+
"------2056753da3b52859874260a51c35681f50744292a84ba7b723607a------------225c763993a8388ca3205171------1b44672b6b892a6a881d53761d52772868882c708c1b4b6c------1948683483993d97a7256379------------1e58754aa6b64ba0b0255977------3f5ea12a59883d9eb023647d------",
|
|
738
|
+
"------1d54723b9faf1e4971384f8f364b88------------27627f45a1b237879d1d4e6e------------2e7591409db52f7996194067------19456a215b80225c801a456b------------2d718f419eb3337f97------------------337f9549a7b62e7089------------2f427b3d55941c436a3b9bad24637d------",
|
|
739
|
+
"------1c536c3b9eae1b476a2b40783d4d8f------------------1d456738839a4ea8ba397f9a1c476d------1e4e723385a0368ca7255c801f4e772d6d912d6e921f517923587e3989a43b8fa91f5576------16426530758e4da8b73c8ca01f496b------------------3848842e457d------3a9aaa216078------",
|
|
740
|
+
"------1d53703b9dad1b4668273b7437468b------2f6c8d396199------------27577b4597ae4ba5b92c6c8c1b40691b3d682c6f93358cad307ba04196b3429ab52e7aa0388eaf2f78991a3f67------28658449a2b34aa2b42a607f------------32558d327495------32407f2b4079------3a99a8206078------",
|
|
741
|
+
"------1d51713898ad1c4366263a7139438c------3b8ea95f9ae14e62b42d4380------------2b688843a1b64297b2295b8622457a27548b3376a650b7ce51bbd0347ca925568c20457724557f3e93ad48a8b931728e------------2c40784e61ac6394e24198b5------333f7f2b3f75------3895a7205f78------",
|
|
742
|
+
"------1c51703494ab------27397038478e------378fa7417db04b5eb03f54a33d51a2273873------204572367aa14da8c63c76ad335c9f3e83b658c6da59c8dc3e88b82f5e9d396fa649a8c43782a01f476d------23346a42549d4a5ca95164b54776b13e93ac------324281293f76------3693a71f5a78------",
|
|
743
|
+
"------1c4f6e3494ab------28397339498f------328ba1286788384f942a41802a3b794452a73d4c9a273a79243d7a3256974078b6448fc147a9cd4bc5d94bc7dc48a7cf4590c4427cb7305995223a7422346e35478e4455a3303e812d3c7e4256a02b5f873b91a9------324381293f77------3692a8215978------",
|
|
744
|
+
"------1c4e6e3593ad1d4166283a7437468f------2f879c21607c2541744073b224597e2132693e4f995160b74356a638549d448abf50c4df4ccde146c9dc44c8dc4ac9e054c5e14b91c536569d4154a4505fb53d4e991f2f651f47704077b12d4b86215577368da6------3242802b4078------3591a9205978------",
|
|
745
|
+
"------1d4d6e3691ad1d406627387334448b------2f859d205f7c243a6d456eb23a93b22a668e1e356a2f41874567b2468ec84a9dce4da6d451c7e245cbdd44cadc4ecae34eaad54b9bce488ec64968b53143861b2f6523527c3c8fae4677b329407a1e51713288a3------304080293e76------348ca91f5978------",
|
|
746
|
+
"------1d4a6f348dac1c3f65253670324388------2e839c205f7e2639703c509b265a843c9bb6327da01f3e702333732b3c853341964160b150a5d644b8d741b9d54badd74066b4314196293883222e711e366f2e70973f9eba2c60893a5196283c771d52713186a1------2f3c7e293a73------338aa51f5479------",
|
|
747
|
+
"------1b4a6e348bac------24366f32418a------2c819d1f5d7e24376e394a941b406a2d7c9b40aec6358bad2859892136742a387f3545963f6db73e8dc53c90c53d75b834469a29358220306e2551843586a943a9c2398fab204570384b92273d771e50732e82a1------2e3b7d293872------3189a51f5379------",
|
|
748
|
+
"------1c4a6e3287aa------243570313f8a------2a7e9c1f5b7d22346935458d------266d8d2a6d9022567f378bb13784ae26448027357e2e47933066a63169a72c499326337e243e7c3781aa3a90b3255a82245d812f76961d406735478d263a761d4e6f2d7e9f------2d3a7c283874------3085a81c5279------",
|
|
749
|
+
"------1c486e3284ab1c3d68263472303f89------287c991e597a213268324089------266d8d2a698d------1b376528638d3285ae2b5f952745832857902958922543832a58903586ad2c6e951c3b6a------24587c2e7595------3243892437711c4c6f2c7a9d------2c3a7e283875------2f82a71d507a------",
|
|
750
|
+
"------1b47703081ab1d3c6726347430408b------2777981d567920316731418a------266b8b286588------------------2148752d6f9c2b6093244c80244b7f2a5c912e729f244f7b------------------2256792d7395------3041872436711c486e28779b------2d3b80293879------2f7fa61b5077------",
|
|
751
|
+
"------1a446f2d7fac1e3d6e27367b303f8f------26749a1d547b21326a30408c------26658a275f82------------------------------------1d3c681e3d6b1c3465------------------------------2051732c6f92------2e3e862335721a466c257499------2d3d8329387b------2f7da81b4f78------",
|
|
752
|
+
"------1a436f2e7eae1f3e6f28377f2e3d8e183b652c86ae22678f1f336a2c3b87------246088275d82------------------------------------------------------------------------------------1f50712a6b8e------2b397f21346f1f5a832a87b01a3f662c39852838811d38692f7eaa1b4c77------",
|
|
753
|
+
"------1b426e2e7fae1f4171263c803161ab2c7fa92a779f2a74a02e62a02b3d8b------266088255c84------------------------------------273b72293e77------------------------------------1f4f762a6a90------28387d2d609d2876a027719b2d7fa83364ae2940871d3d722f7dae1b4a77------",
|
|
754
|
+
"------1a3f6a2e7daf22578b2f77b23a80c9285090------------2744842e4295------26618f275c85------------------------------------2736722a3878------------------------------------204f772b6b92------2b3b84284585------------274385417ecd337db9255990317cb21b4674------",
|
|
755
|
+
"------1b3c693690c6368bc22057881d37732d3b902c3a8f------1c2a662e398e------265d8b265780------------------------------------262f73293178------------------------------------1f4972296790------2c3684202a67------2c3587333d952438782251853487bb3d9ad21b4371------",
|
|
756
|
+
"------------2b659b3585c021568b------------202e72333d98313c972d378e------245784275483283072252e6f------------------------262d74292e79------------------------222c6729327521487a275f8b------2c3484333a9438409a28337a------------2152853583bc306fa4------------",
|
|
757
|
+
"------------------1d41722d71a73172ac1e4376------------2f368c2f3696------235486244f80------373c92353a90------------------272c7b292d7f------------------313788383e95222b681e4373275b8b------2c3586343893------------1a3c6f2d6ea73075ae214678------------------",
|
|
758
|
+
"------------------------------235087347bb52a699f------------2c378f1b346a285c95254f85------------24296937399729307b------272c7b292d7f------252b6f353891282d71------------1e42742b62991b376a2b36881e2c6a------265d96347bb6265a92------------------------------",
|
|
759
|
+
"------------------------------------------255a912e74ae2c589c3250a62c629d2f66a5283a87232770------------------33368e37379934349932329635359235358e------------------23286f2838842d64a22e66a13451a42b52992a6faa255c951a3667------------------------------------",
|
|
760
|
+
"------------------------------------------------1d3b6f2f69aa3475be27538c------------24277031318e252872------------------3834a03934a3222568------------22276a32338e282b76------------234c803672b6306cab1d3b6f------------------------------------------------",
|
|
761
|
+
"------------------------------------------------------------------------------------------------2e2d842a2b7b------------262673282877------------282972302f86------------------------------------------------------------------------------------------------",
|
|
762
|
+
"------------------------------------------------------------------------292d72413ea3323285------------------302f862b2b7d2d2b892e2b892a2a7a302f87262468------------2d2c7d3e3ca02b2f7b------------------------------------------------------------------------",
|
|
763
|
+
"------------------------------------------------------------------------------------34328a3b369b272770------------2d2a7e4037ac4136b0302c86------------2524673a3397363390------------------------------------------------------------------------------------",
|
|
764
|
+
"------------------------------------------------------------------------------------------2325653a3599353290------------2a26782c277c------------322e853e379d2a2870------------------------------------------------------------------------------------------",
|
|
765
|
+
"------------------------------------------------------------------------------------------------------332e883c35a02e2a812f2a84302a852c277939329634308a------------------------------------------------------------------------------------------------------",
|
|
766
|
+
"------------------------------------------------------------------------------------------------------------------362f914338b54438b7382f96262469------------------------------------------------------------------------------------------------------------",
|
|
767
|
+
"------------------------------------------------------------------------------------------------------------------------252369242369------------------------------------------------------------------------------------------------------------------------",
|
|
768
|
+
"------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------"
|
|
769
|
+
],
|
|
770
|
+
[
|
|
771
|
+
"------------------------------------------------------------------------------------------------------------------------------------------------------------------------",
|
|
772
|
+
"------------------------------------------------------------------------215c79368ea13790a4225f7b------------------------------------------------------------------------",
|
|
773
|
+
"------------------------------------------------------------1a47652f728f439cae3fa0b03d9eb0409aac3077911c4967------------------------------------------------------------",
|
|
774
|
+
"------------------------------------------------------235e7c3d8fa23d8da1226582308ca4318fa52267843a899f3d90a326637f------------------------------------------------------",
|
|
775
|
+
"------------------------------------------2255713c889d4294a527627b1e536e2d7d963594aa3594a82e7f981e5772255e773e8ea13c8ba0225a77------------------------------------------",
|
|
776
|
+
"------------------------------------2b6d834294a63172881e526e2a6c862c738a1b4d6926738c26758d1a4d67276f87286f891d53712d6e873d93a52b6e85------------------------------------",
|
|
777
|
+
"------------------------2560794a9eaf53b1bf205d79205b772a738b215771------2a738d28728a276f8729728b------1f536f2a738d21627d1c5e7a44a9ba43a1b027657c------------------------",
|
|
778
|
+
"------------1c4b673282973b90a42864812d6e85439dad3689a0194a66225b772d78911d4a66------------------2d778f25617a1a486532839b409fb02b738a24617d388da13584971e4f6a------------",
|
|
779
|
+
"------225c764ebdca42aaba2779922668831c4b671f4f6b38849942a0b02e7c93------------------------------------30799144a4b33a8a9e1f526d------2467812779923da4b651c1cd286880------",
|
|
780
|
+
"------338aa056cee33ca1b326738a21607c328097296982------23597339869a30728a------------------------306e843b8899276076------28677f34829720627b2570883ca0b255cbe13a97ac------",
|
|
781
|
+
"------30819a3e83b131679539879e2d7089------25627b2f7a91225976------2b69843076901d51711d4f702f758e2f6e86------1e526c2f7a8c27667d------2c6e863c8c9f326a933d7fb0328ca1------",
|
|
782
|
+
"------2d7b912c6088344989------2e6c8738889d2156731a47672c728d317a96204f731c486c1f577b1f567b1d4a6e1e4d703078922e758e1849651d536d3a8a9e33748c------3247852a5c8530859a------",
|
|
783
|
+
"------2c798e24567a344483------------1d446639819a3a829c225175215477307b9828698c2d6c902d6c9129688c347e9c2258791d4e6f37809639839a1f4667------------3345812251732e8396------",
|
|
784
|
+
"------2c778f2353752f3e7e20436c498ebc3f549c213b6c2a5f7f3a89a3317394244f7d2d6a99449ebd449fbd2d6e9b214e792e728f3d8ca32c6483213d6a3c50954b8dbf1f4266313f7c214e6e2e8194------",
|
|
785
|
+
"------28738e225073304081214a723c8aad465da93648922d3e81234174336e994184b33a75ae4eb1cf4eb3d13976af3f81b032719620426c2b3d7a3c4a924d60af4289b022496e30417d204c6d2d7c94------",
|
|
786
|
+
"------28728e235075303f831f486d2b7c9327457a3159902e4383424f9f3a4f9c3c6dac4bb4d549c8dd47c6dd4db4d83e70ae394f993e4d9a2d3e7e30568c2f4b872f799520476b30417e204d6e2c7a95------",
|
|
787
|
+
"------2a6f8e234e752d3d7f1e456c2a7891233e703c74a82c6f9329437d39579f417ab74ca5d449c4dd47c7de4ca8d4407ab73b579f273f7b2a658b3c75a52740772b718d1d44682e3f7d1f496d2b7795------",
|
|
788
|
+
"------286b8e214d732b3b7c1c426b2877932338702d4e872f809d3388a927508427347c3a59a5408cc53f90c5385da825307a2649803380a43789a630518a253d75296e8e1c41682d3b7b1e466a297394------",
|
|
789
|
+
"------27698e214b752b3a7e1c406b26729221366928457a236283265f862c6b9530709e2947892d56982c5798284488306e9d2f709825567e2966872a477d243972276a8c1a3f682c3b7b1e466c286f96------",
|
|
790
|
+
"------27658e2149772b39801d3f6c246e8e20326726427a235f801e456c------1f47712a6091265287264f862a6091234b74------------27638328447b22356f2464891b3e692d3b7e204470266d94------",
|
|
791
|
+
"------24618e224a7c2c38851d416f2571981f3369253e7a21597c------------------------------1d3765------------------------245d7f25407821336e23688f1b416d2c3981214474256b95------",
|
|
792
|
+
"------24608e234c7f2d4e98276c9a28729a2a5692233c7522567c------------------------------------------------------------245a7f213b71295491256e97276a9830529b22487f266796------",
|
|
793
|
+
"------2461902d76ab2b66a52a4b93------213774243e7e23557f------------------------------222f66------------------------245a80253c78223874------2c438f3167a92f76ae28689b------",
|
|
794
|
+
"------214c7c317bb51c4375------2732812c358b253779214f7b243e71------------------1f2865212866------------------20386d22527e2437752f368a2c35821e2a651d4173327cb2255383------",
|
|
795
|
+
"------------1a3765285f96275d92------252c7524367d214d7e203a6c272c6e2c327e------20276a21276a------2b317a282f721c3365234f7f243779272f7a------25588e29629a1d3b69------------",
|
|
796
|
+
"------------------------1f44762860982a56982e579e295591212e70------------2e31823233933131912c2f7f------------212c6e2855902e579e2a5396265e9620477b------------------------",
|
|
797
|
+
"------------------------------------254b85264a83------------27287524286d------282878282979------222667272a75------------24467c244b83------------------------------------",
|
|
798
|
+
"------------------------------------------------272b6e323284------------2728722d2b842d2b83292873------------2e2f7f282d73------------------------------------------------",
|
|
799
|
+
"------------------------------------------------------------312e832b2a79------2f2a842f2b87------2a2873332f85232565------------------------------------------------------",
|
|
800
|
+
"------------------------------------------------------------------292872312d86302b87302a84302b802b2973------------------------------------------------------------------",
|
|
801
|
+
"------------------------------------------------------------------------------322b88332c8a------------------------------------------------------------------------------",
|
|
802
|
+
"------------------------------------------------------------------------------------------------------------------------------------------------------------------------"
|
|
803
|
+
]
|
|
804
|
+
];
|
|
805
|
+
|
|
806
|
+
// src/dashboard/view.ts
|
|
807
|
+
var DASHBOARD_TABS = ["Overview", "Players", "Logs", "HTTP"];
|
|
808
|
+
var WIDE_LAYOUT = 100;
|
|
809
|
+
var NEWLINE_RE = /\r?\n/;
|
|
810
|
+
var STATUS_LABELS = {
|
|
811
|
+
connecting: "CONNECTING",
|
|
812
|
+
live: "LIVE",
|
|
813
|
+
unreachable: "UNREACHABLE",
|
|
814
|
+
unauthorized: "UNAUTHORIZED",
|
|
815
|
+
forbidden: "FORBIDDEN",
|
|
816
|
+
disabled: "METRICS DISABLED"
|
|
817
|
+
};
|
|
818
|
+
function tabForKey(key) {
|
|
819
|
+
const index = Number(key) - 1;
|
|
820
|
+
return Number.isInteger(index) && index >= 0 && index < DASHBOARD_TABS.length ? index : null;
|
|
821
|
+
}
|
|
822
|
+
function sortedPlayers(players) {
|
|
823
|
+
return [...players].sort((a, b) => a.id.localeCompare(b.id));
|
|
824
|
+
}
|
|
825
|
+
function handlePlayersKey(view, snapshot, key) {
|
|
826
|
+
if (DASHBOARD_TABS[view.tab] !== "Players" || !snapshot)
|
|
827
|
+
return false;
|
|
828
|
+
if (key === "escape") {
|
|
829
|
+
const had = view.selectedPlayerId !== null;
|
|
830
|
+
view.selectedPlayerId = null;
|
|
831
|
+
return had;
|
|
832
|
+
}
|
|
833
|
+
const delta = key === "down" || key === "j" ? 1 : key === "up" || key === "k" ? -1 : 0;
|
|
834
|
+
const rows = sortedPlayers(snapshot.players);
|
|
835
|
+
if (delta === 0 || rows.length === 0)
|
|
836
|
+
return false;
|
|
837
|
+
const current = rows.findIndex((p) => p.id === view.selectedPlayerId);
|
|
838
|
+
const next = current === -1 ? delta > 0 ? 0 : rows.length - 1 : Math.max(0, Math.min(rows.length - 1, current + delta));
|
|
839
|
+
view.selectedPlayerId = rows[next].id;
|
|
840
|
+
view.followSelectedPlayer = true;
|
|
841
|
+
return true;
|
|
842
|
+
}
|
|
843
|
+
function statusColor(status, theme) {
|
|
844
|
+
if (status === "live")
|
|
845
|
+
return theme.success;
|
|
846
|
+
if (status === "connecting")
|
|
847
|
+
return theme.warning;
|
|
848
|
+
return theme.danger;
|
|
849
|
+
}
|
|
850
|
+
function renderDashboard(ui, theme, state, view, { transparent = false, startup, openKey = false } = {}) {
|
|
851
|
+
const { snapshot } = state;
|
|
852
|
+
const starting = !snapshot && !!startup && (state.status === "connecting" || state.status === "unreachable");
|
|
853
|
+
ui.column({}, (root) => {
|
|
854
|
+
header(root, theme, state);
|
|
855
|
+
if (snapshot?.latencySimulation)
|
|
856
|
+
latencyBanner(root, theme, snapshot.latencySimulation);
|
|
857
|
+
if (state.error && snapshot)
|
|
858
|
+
root.text([{ text: ` ${state.error} \u2014 showing last known values`, fg: theme.danger }], { size: 1 });
|
|
859
|
+
if (starting) {
|
|
860
|
+
startupScreen(root, theme, startup);
|
|
861
|
+
} else if (!snapshot) {
|
|
862
|
+
waiting(root, theme, state);
|
|
863
|
+
} else {
|
|
864
|
+
root.tabs({
|
|
865
|
+
tabs: DASHBOARD_TABS.map((name, i) => `${i + 1} ${name}`),
|
|
866
|
+
active: view.tab,
|
|
867
|
+
size: 1,
|
|
868
|
+
onSelect: (index) => {
|
|
869
|
+
view.tab = index;
|
|
870
|
+
root.ctx.invalidate();
|
|
871
|
+
}
|
|
872
|
+
});
|
|
873
|
+
const tab = DASHBOARD_TABS[view.tab] ?? "Overview";
|
|
874
|
+
if (tab === "Players") {
|
|
875
|
+
playersTab(root, theme, snapshot, view, transparent);
|
|
876
|
+
} else if (tab === "Logs") {
|
|
877
|
+
logsPanel(root, theme, state, view);
|
|
878
|
+
} else if (tab === "HTTP") {
|
|
879
|
+
requestsPanel(root, theme, state, view, transparent);
|
|
880
|
+
} else {
|
|
881
|
+
root.responsive({
|
|
882
|
+
[WIDE_LAYOUT]: (c) => wideLayout(c, theme, state, snapshot, view, transparent),
|
|
883
|
+
0: (c) => narrowLayout(c, theme, state, snapshot, view, transparent)
|
|
884
|
+
});
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
root.statusBar({
|
|
888
|
+
size: 1,
|
|
889
|
+
background: transparent ? DEFAULT_COLOR : void 0,
|
|
890
|
+
items: [
|
|
891
|
+
{ key: "q", label: "Quit" },
|
|
892
|
+
{ key: "r", label: "Refresh" },
|
|
893
|
+
{ key: `1-${DASHBOARD_TABS.length}`, label: "Tabs" },
|
|
894
|
+
...openKey && snapshot ? [{ key: "o", label: "Open game" }] : [],
|
|
895
|
+
...snapshot && DASHBOARD_TABS[view.tab] === "Players" ? [{ key: "\u2191\u2193", label: "Select player" }, ...view.selectedPlayerId ? [{ key: "esc", label: "Close" }] : []] : []
|
|
896
|
+
],
|
|
897
|
+
right: [starting ? { label: "STARTING", color: statusColor("connecting", theme) } : { label: STATUS_LABELS[state.status], color: statusColor(state.status, theme), active: state.status === "live" }]
|
|
898
|
+
});
|
|
899
|
+
});
|
|
900
|
+
}
|
|
901
|
+
function header(ui, theme, state) {
|
|
902
|
+
const spans = [
|
|
903
|
+
{ text: " MavonEngine ", fg: theme.primary, bold: true },
|
|
904
|
+
{ text: state.source, fg: theme.muted }
|
|
905
|
+
];
|
|
906
|
+
if (state.snapshot) {
|
|
907
|
+
spans.push(
|
|
908
|
+
{ text: " uptime ", fg: theme.muted },
|
|
909
|
+
{ text: formatDuration(state.snapshot.uptime) },
|
|
910
|
+
{ text: " engine ", fg: theme.muted },
|
|
911
|
+
{ text: state.snapshot.engineVersion }
|
|
912
|
+
);
|
|
913
|
+
if (state.snapshot.schemaVersion > SUPPORTED_SCHEMA_VERSION)
|
|
914
|
+
spans.push({ text: " newer metrics schema, update @mavonengine/cli", fg: theme.warning });
|
|
915
|
+
}
|
|
916
|
+
ui.text(spans, { size: 1 });
|
|
917
|
+
}
|
|
918
|
+
function latencyBanner(ui, theme, latency) {
|
|
919
|
+
ui.text([{
|
|
920
|
+
text: ` LATENCY SIMULATION ON ${latency.fixedLatency}ms \xB1 ${latency.jitter / 2}ms, ${formatPercent(latency.packetLoss * 100)} packet loss `,
|
|
921
|
+
fg: theme.background,
|
|
922
|
+
bg: theme.warning,
|
|
923
|
+
bold: true
|
|
924
|
+
}], { size: 1 });
|
|
925
|
+
}
|
|
926
|
+
var STARTUP_OUTPUT_LINES = 8;
|
|
927
|
+
function logoLines(pixels) {
|
|
928
|
+
const pixel = (row, x) => {
|
|
929
|
+
const value = row?.slice(x * 6, x * 6 + 6);
|
|
930
|
+
return value && value !== "------" ? hex(Number.parseInt(value, 16)) : null;
|
|
931
|
+
};
|
|
932
|
+
const lines = [];
|
|
933
|
+
for (let y = 0; y < pixels.length; y += 2) {
|
|
934
|
+
const line = [];
|
|
935
|
+
for (let x = 0; x < pixels[y].length / 6; x++) {
|
|
936
|
+
const top = pixel(pixels[y], x);
|
|
937
|
+
const bottom = pixel(pixels[y + 1], x);
|
|
938
|
+
if (top !== null)
|
|
939
|
+
line.push({ text: "\u2580", fg: top, bg: bottom ?? void 0 });
|
|
940
|
+
else if (bottom !== null)
|
|
941
|
+
line.push({ text: "\u2584", fg: bottom });
|
|
942
|
+
else
|
|
943
|
+
line.push({ text: " " });
|
|
944
|
+
}
|
|
945
|
+
lines.push(line);
|
|
946
|
+
}
|
|
947
|
+
return lines;
|
|
948
|
+
}
|
|
949
|
+
var LOGOS = LOGO_SIZES.map(logoLines);
|
|
950
|
+
function startupScreen(ui, theme, { engineVersion, output, label = "Starting the game server\u2026", outputTitle = "Dev script" }) {
|
|
951
|
+
const lines = stripVTControlCharacters(output).split(NEWLINE_RE).filter((line) => line.trim() !== "");
|
|
952
|
+
const banner = bootBanner({ version: engineVersion ?? "unknown", mode: "development" });
|
|
953
|
+
ui.column({ size: "1fr" }, (column) => {
|
|
954
|
+
const { unicode, colors } = column.ctx.capabilities;
|
|
955
|
+
const logo = unicode && (colors === "truecolor" || colors === "ansi256") ? LOGOS.find((l) => l.length + banner.length + 3 <= column.height && l[0].length <= column.width) : void 0;
|
|
956
|
+
column.spacer("1fr");
|
|
957
|
+
if (logo) {
|
|
958
|
+
for (const line of logo)
|
|
959
|
+
column.text(line, { align: "center", size: 1 });
|
|
960
|
+
column.spacer(1);
|
|
961
|
+
}
|
|
962
|
+
for (const line of banner)
|
|
963
|
+
column.text([{ text: line, fg: theme.primary }], { align: "center", size: 1 });
|
|
964
|
+
column.spacer(1);
|
|
965
|
+
column.row({ size: 1 }, (row) => {
|
|
966
|
+
row.spacer("1fr");
|
|
967
|
+
row.spinner({ label, size: label.length + 2 });
|
|
968
|
+
row.spacer("1fr");
|
|
969
|
+
});
|
|
970
|
+
column.spacer("1fr");
|
|
971
|
+
});
|
|
972
|
+
ui.panel({ title: outputTitle, size: STARTUP_OUTPUT_LINES + 2 }, (panel) => {
|
|
973
|
+
if (lines.length === 0) {
|
|
974
|
+
panel.label("No output yet");
|
|
975
|
+
return;
|
|
976
|
+
}
|
|
977
|
+
panel.log({ entries: lines.slice(-STARTUP_OUTPUT_LINES).map((message) => ({ message })), size: "1fr" });
|
|
978
|
+
});
|
|
979
|
+
}
|
|
980
|
+
function waiting(ui, theme, state) {
|
|
981
|
+
ui.panel({ title: "Game server", size: "1fr" }, (panel) => {
|
|
982
|
+
if (state.status === "connecting") {
|
|
983
|
+
panel.spinner({ label: `Connecting to ${state.source}` });
|
|
984
|
+
return;
|
|
985
|
+
}
|
|
986
|
+
panel.text([{ text: STATUS_LABELS[state.status], fg: statusColor(state.status, theme), bold: true }]);
|
|
987
|
+
panel.text(state.error ?? "", { wrap: true });
|
|
988
|
+
panel.label(`Retrying (attempt ${state.failures})\u2026`);
|
|
989
|
+
});
|
|
990
|
+
}
|
|
991
|
+
function wideLayout(ui, theme, state, snapshot, view, transparent) {
|
|
992
|
+
ui.grid({ columns: ["1fr", "1fr"], rows: [11, 11], size: 22 }, (grid) => {
|
|
993
|
+
grid.panel({ title: "CPU" }, (p) => cpuPanel(p, theme, snapshot));
|
|
994
|
+
grid.panel({ title: "Network" }, (p) => networkPanel(p, theme, snapshot));
|
|
995
|
+
grid.panel({ title: "Tick" }, (p) => tickPanel(p, theme, snapshot));
|
|
996
|
+
grid.panel({ title: "World" }, (p) => worldPanel(p, theme, snapshot));
|
|
997
|
+
});
|
|
998
|
+
ui.row({ size: "1fr" }, (row) => {
|
|
999
|
+
playersPanel(row, theme, snapshot, view, transparent);
|
|
1000
|
+
logsPanel(row, theme, state, view);
|
|
1001
|
+
});
|
|
1002
|
+
}
|
|
1003
|
+
function narrowLayout(ui, theme, state, snapshot, view, transparent) {
|
|
1004
|
+
ui.panel({ title: "CPU", size: 9 }, (p) => cpuPanel(p, theme, snapshot));
|
|
1005
|
+
ui.panel({ title: "Network", size: 9 }, (p) => networkPanel(p, theme, snapshot));
|
|
1006
|
+
ui.panel({ title: "Tick", size: 9 }, (p) => tickPanel(p, theme, snapshot));
|
|
1007
|
+
ui.panel({ title: "World", size: 11 }, (p) => worldPanel(p, theme, snapshot));
|
|
1008
|
+
playersPanel(ui, theme, snapshot, view, transparent);
|
|
1009
|
+
logsPanel(ui, theme, state, view);
|
|
1010
|
+
}
|
|
1011
|
+
function cpuPanel(ui, theme, { cpu }) {
|
|
1012
|
+
const values = cpu.history.map((h) => h.cpuPercentage);
|
|
1013
|
+
ui.graph({
|
|
1014
|
+
values,
|
|
1015
|
+
min: 0,
|
|
1016
|
+
max: Math.max(100, ...values),
|
|
1017
|
+
fill: true,
|
|
1018
|
+
colors: theme.heat,
|
|
1019
|
+
axis: true,
|
|
1020
|
+
axisFormat: (v) => `${Math.round(v)}%`,
|
|
1021
|
+
size: "1fr"
|
|
1022
|
+
});
|
|
1023
|
+
ui.meters([
|
|
1024
|
+
{ label: "1s", value: cpu.avg1s, max: 100, text: formatPercent(cpu.avg1s) },
|
|
1025
|
+
{ label: "10s", value: cpu.avg10s, max: 100, text: formatPercent(cpu.avg10s) },
|
|
1026
|
+
{ label: "1m", value: cpu.avg60s, max: 100, text: formatPercent(cpu.avg60s) }
|
|
1027
|
+
], { columns: 3 });
|
|
1028
|
+
}
|
|
1029
|
+
function networkPanel(ui, theme, { bandwidth, sampleIntervalMs }) {
|
|
1030
|
+
const perSecond = 1e3 / sampleIntervalMs;
|
|
1031
|
+
ui.multiGraph([
|
|
1032
|
+
{ label: "sent", values: bandwidth.history.map((b) => b.sent * perSecond), color: theme.graph[0] },
|
|
1033
|
+
{ label: "received", values: bandwidth.history.map((b) => b.received * perSecond), color: theme.graph[1] }
|
|
1034
|
+
], {
|
|
1035
|
+
min: 0,
|
|
1036
|
+
axis: true,
|
|
1037
|
+
axisFormat: formatRate,
|
|
1038
|
+
legend: true,
|
|
1039
|
+
legendAlign: "right",
|
|
1040
|
+
size: "1fr"
|
|
1041
|
+
});
|
|
1042
|
+
ui.keyValues([
|
|
1043
|
+
{ label: "Sent", value: formatRate(bandwidth.sentPerSecond), color: theme.graph[0] },
|
|
1044
|
+
{ label: "Received", value: formatRate(bandwidth.receivedPerSecond), color: theme.graph[1] }
|
|
1045
|
+
], { spread: true });
|
|
1046
|
+
}
|
|
1047
|
+
function tickPanel(ui, theme, { tick, tickRate }) {
|
|
1048
|
+
const avg = tick.history.map((t) => t.avgMs);
|
|
1049
|
+
const max = tick.history.map((t) => t.maxMs);
|
|
1050
|
+
const latest = tick.latest;
|
|
1051
|
+
ui.multiGraph([
|
|
1052
|
+
{ label: "avg", values: avg, color: theme.graph[0] },
|
|
1053
|
+
{ label: "max", values: max, color: theme.graph[2] ?? theme.warning }
|
|
1054
|
+
], {
|
|
1055
|
+
min: 0,
|
|
1056
|
+
max: Math.max(tick.budgetMs * 1.25, ...max, 1),
|
|
1057
|
+
axis: true,
|
|
1058
|
+
axisFormat: (v) => `${v.toFixed(0)}ms`,
|
|
1059
|
+
legend: true,
|
|
1060
|
+
legendAlign: "right",
|
|
1061
|
+
size: "1fr"
|
|
1062
|
+
});
|
|
1063
|
+
ui.meter({
|
|
1064
|
+
label: "Budget",
|
|
1065
|
+
value: latest?.avgMs ?? 0,
|
|
1066
|
+
max: tick.budgetMs || 1,
|
|
1067
|
+
text: `${formatMs(latest?.avgMs ?? 0)} / ${formatMs(tick.budgetMs)}`
|
|
1068
|
+
});
|
|
1069
|
+
ui.keyValues([
|
|
1070
|
+
{ label: "Ticks/s", value: `${latest?.ticksPerSecond ?? 0} / ${tickRate}`, color: (latest?.ticksPerSecond ?? 0) < tickRate * 0.9 ? theme.danger : void 0 },
|
|
1071
|
+
{ label: "Commands/s", value: `${latest?.commandsPerSecond ?? 0} (max ${latest?.maxCommandsPerTick ?? 0}/tick)` }
|
|
1072
|
+
], { spread: true });
|
|
1073
|
+
}
|
|
1074
|
+
function worldPanel(ui, theme, { players, world, physics, memory }) {
|
|
1075
|
+
ui.keyValues([
|
|
1076
|
+
{ label: "Players", value: String(players.length), color: theme.primary },
|
|
1077
|
+
{ label: "Entities", value: String(world.entities) },
|
|
1078
|
+
{ label: "Physics bodies", value: physics ? String(physics.bodies) : "\u2014" },
|
|
1079
|
+
{ label: "Colliders", value: physics ? String(physics.colliders) : "\u2014" },
|
|
1080
|
+
{ label: "RSS", value: formatBytes(memory.rss) },
|
|
1081
|
+
{ label: "External", value: formatBytes(memory.external) }
|
|
1082
|
+
], { spread: true });
|
|
1083
|
+
ui.spacer("1fr");
|
|
1084
|
+
ui.meter({
|
|
1085
|
+
label: "Heap",
|
|
1086
|
+
value: memory.heapUsed,
|
|
1087
|
+
max: memory.heapTotal || 1,
|
|
1088
|
+
text: `${formatBytes(memory.heapUsed)} / ${formatBytes(memory.heapTotal)}`
|
|
1089
|
+
});
|
|
1090
|
+
}
|
|
1091
|
+
function playersTab(ui, theme, snapshot, view, transparent) {
|
|
1092
|
+
const selected = snapshot.players.find((p) => p.id === view.selectedPlayerId);
|
|
1093
|
+
if (!selected) {
|
|
1094
|
+
view.selectedPlayerId = null;
|
|
1095
|
+
playersPanel(ui, theme, snapshot, view, transparent, true);
|
|
1096
|
+
return;
|
|
1097
|
+
}
|
|
1098
|
+
ui.responsive({
|
|
1099
|
+
[WIDE_LAYOUT]: (c) => c.row({ size: "1fr" }, (row) => {
|
|
1100
|
+
playersPanel(row, theme, snapshot, view, transparent, true);
|
|
1101
|
+
playerDetails(row, theme, selected, 42);
|
|
1102
|
+
}),
|
|
1103
|
+
0: (c) => {
|
|
1104
|
+
playersPanel(c, theme, snapshot, view, transparent, true);
|
|
1105
|
+
playerDetails(c, theme, selected, 13);
|
|
1106
|
+
}
|
|
1107
|
+
});
|
|
1108
|
+
}
|
|
1109
|
+
function playerDetails(ui, theme, player, size) {
|
|
1110
|
+
const { connection, position } = player;
|
|
1111
|
+
ui.panel({ title: player.displayName, size }, (panel) => {
|
|
1112
|
+
panel.keyValues([
|
|
1113
|
+
{ label: "ID", value: player.id },
|
|
1114
|
+
{ label: "Connected", value: formatDuration(player.connectedFor) },
|
|
1115
|
+
{ label: "IP", value: connection ? connection.address ?? "pending" : "\u2014", color: theme.primary },
|
|
1116
|
+
{ label: "RTT", value: connection?.rttMs != null ? formatMs(connection.rttMs) : "\u2014" },
|
|
1117
|
+
{ label: "Sent", value: connection ? formatBytes(connection.bytesSent) : "\u2014" },
|
|
1118
|
+
{ label: "Received", value: connection ? formatBytes(connection.bytesReceived) : "\u2014" },
|
|
1119
|
+
{ label: "Position", value: [position.x, position.y, position.z].map((n) => n.toFixed(2)).join(", ") },
|
|
1120
|
+
{ label: "Last seq", value: String(player.lastProcessedSequenceId) },
|
|
1121
|
+
{ label: "Tracked", value: String(player.trackedEntities) }
|
|
1122
|
+
], { spread: true });
|
|
1123
|
+
if (!connection)
|
|
1124
|
+
panel.label("No live connection (a bot, or an older server)");
|
|
1125
|
+
});
|
|
1126
|
+
}
|
|
1127
|
+
function playersPanel(ui, theme, { players }, view, transparent, selectable = false) {
|
|
1128
|
+
const rows = sortedPlayers(players);
|
|
1129
|
+
const maxOffset = Math.max(0, rows.length - 1);
|
|
1130
|
+
view.playerOffset = Math.min(view.playerOffset, maxOffset);
|
|
1131
|
+
const showNames = rows.some((p) => p.displayName !== p.id);
|
|
1132
|
+
ui.panel({ title: `Players (${rows.length})`, size: "1fr" }, (panel) => {
|
|
1133
|
+
if (rows.length === 0) {
|
|
1134
|
+
panel.label("No players connected");
|
|
1135
|
+
return;
|
|
1136
|
+
}
|
|
1137
|
+
panel.table({
|
|
1138
|
+
rows,
|
|
1139
|
+
offset: view.playerOffset,
|
|
1140
|
+
selected: selectable ? rows.findIndex((p) => p.id === view.selectedPlayerId) : -1,
|
|
1141
|
+
followSelection: selectable && view.followSelectedPlayer,
|
|
1142
|
+
// Following the selection can move the window, so keep our offset in step with what is drawn
|
|
1143
|
+
onRow: (_row, index, y) => {
|
|
1144
|
+
if (y === 1)
|
|
1145
|
+
view.playerOffset = index;
|
|
1146
|
+
},
|
|
1147
|
+
onSelectRow: selectable ? (visibleRow) => {
|
|
1148
|
+
const row = rows[view.playerOffset + visibleRow];
|
|
1149
|
+
if (!row)
|
|
1150
|
+
return;
|
|
1151
|
+
view.selectedPlayerId = row.id === view.selectedPlayerId ? null : row.id;
|
|
1152
|
+
ui.ctx.invalidate();
|
|
1153
|
+
} : void 0,
|
|
1154
|
+
scrollbar: true,
|
|
1155
|
+
// Zebra stripes are solid fills, which would break transparency
|
|
1156
|
+
zebra: !transparent,
|
|
1157
|
+
size: "1fr",
|
|
1158
|
+
onScroll: (delta) => {
|
|
1159
|
+
view.playerOffset = Math.max(0, Math.min(maxOffset, view.playerOffset + delta));
|
|
1160
|
+
view.followSelectedPlayer = false;
|
|
1161
|
+
ui.ctx.invalidate();
|
|
1162
|
+
},
|
|
1163
|
+
columns: [
|
|
1164
|
+
...showNames ? [{ key: "displayName", title: "Name" }] : [],
|
|
1165
|
+
{ key: "id", title: "ID" },
|
|
1166
|
+
{ key: "x", title: "X", width: 9, align: "right", render: (p) => p.position.x.toFixed(2) },
|
|
1167
|
+
{ key: "y", title: "Y", width: 9, align: "right", render: (p) => p.position.y.toFixed(2) },
|
|
1168
|
+
{ key: "z", title: "Z", width: 9, align: "right", render: (p) => p.position.z.toFixed(2) },
|
|
1169
|
+
{ key: "lastProcessedSequenceId", title: "Seq", width: 10, align: "right" },
|
|
1170
|
+
{ key: "trackedEntities", title: "Tracked", width: 8, align: "right" }
|
|
1171
|
+
]
|
|
1172
|
+
});
|
|
1173
|
+
});
|
|
1174
|
+
}
|
|
1175
|
+
function formatTime(timestamp) {
|
|
1176
|
+
const date = new Date(timestamp);
|
|
1177
|
+
return [date.getHours(), date.getMinutes(), date.getSeconds()].map((n) => n.toString().padStart(2, "0")).join(":");
|
|
1178
|
+
}
|
|
1179
|
+
function toLogRows(lines) {
|
|
1180
|
+
return lines.flatMap(({ timestamp, level, message }) => {
|
|
1181
|
+
const parts = message.split(NEWLINE_RE);
|
|
1182
|
+
while (parts.length > 1 && parts[0].trim() === "")
|
|
1183
|
+
parts.shift();
|
|
1184
|
+
while (parts.length > 1 && parts.at(-1).trim() === "")
|
|
1185
|
+
parts.pop();
|
|
1186
|
+
return parts.map((part, i) => i === 0 ? { time: formatTime(timestamp), level, message: part } : { message: `${" ".repeat(15)}${part}` });
|
|
1187
|
+
});
|
|
1188
|
+
}
|
|
1189
|
+
function logsPanel(ui, theme, state, view) {
|
|
1190
|
+
const rows = toLogRows(state.logs);
|
|
1191
|
+
view.logFromEnd = Math.min(view.logFromEnd, Math.max(0, rows.length - 1));
|
|
1192
|
+
const title = view.logFromEnd > 0 ? `Logs (scrolled back ${view.logFromEnd}, scroll down to follow)` : "Logs";
|
|
1193
|
+
ui.panel({ title, size: "1fr" }, (panel) => {
|
|
1194
|
+
if (state.logsAvailable === false) {
|
|
1195
|
+
panel.label("Not available: the server runs a @mavonengine/core release without GET /api/game/logs, or has metrics.logBufferSize set to 0");
|
|
1196
|
+
return;
|
|
1197
|
+
}
|
|
1198
|
+
if (rows.length === 0) {
|
|
1199
|
+
panel.label("No log output yet");
|
|
1200
|
+
return;
|
|
1201
|
+
}
|
|
1202
|
+
panel.log({
|
|
1203
|
+
entries: rows,
|
|
1204
|
+
fromEnd: view.logFromEnd,
|
|
1205
|
+
scrollbar: true,
|
|
1206
|
+
size: "1fr",
|
|
1207
|
+
levelColors: {
|
|
1208
|
+
VERBOSE: theme.muted,
|
|
1209
|
+
SILLY: theme.muted,
|
|
1210
|
+
HTTP: theme.info
|
|
1211
|
+
},
|
|
1212
|
+
onScroll: (delta) => {
|
|
1213
|
+
view.logFromEnd = Math.max(0, Math.min(rows.length - 1, view.logFromEnd - delta * 3));
|
|
1214
|
+
ui.ctx.invalidate();
|
|
1215
|
+
}
|
|
1216
|
+
});
|
|
1217
|
+
});
|
|
1218
|
+
}
|
|
1219
|
+
function statusCodeColor(status, theme) {
|
|
1220
|
+
if (status === null || status >= 500)
|
|
1221
|
+
return theme.danger;
|
|
1222
|
+
if (status >= 400)
|
|
1223
|
+
return theme.warning;
|
|
1224
|
+
if (status >= 300)
|
|
1225
|
+
return theme.info;
|
|
1226
|
+
return theme.success;
|
|
1227
|
+
}
|
|
1228
|
+
function requestsPanel(ui, theme, state, view, transparent) {
|
|
1229
|
+
const rows = [...state.requests].reverse();
|
|
1230
|
+
const anchor = view.requestTopId === null ? 0 : rows.findIndex((r) => r.id <= view.requestTopId);
|
|
1231
|
+
const offset = Math.max(0, anchor);
|
|
1232
|
+
const errors = rows.filter((r) => r.status === null || r.status >= 400).length;
|
|
1233
|
+
const title = `HTTP requests (${rows.length}${errors ? `, ${errors} error${errors === 1 ? "" : "s"}` : ""})${offset > 0 ? ", scroll up to follow" : ""}`;
|
|
1234
|
+
ui.panel({ title, size: "1fr" }, (panel) => {
|
|
1235
|
+
if (state.requestsAvailable === false) {
|
|
1236
|
+
panel.label("Not available: the server runs a @mavonengine/core release without GET /api/game/requests, or has metrics.requestLogSize set to 0");
|
|
1237
|
+
return;
|
|
1238
|
+
}
|
|
1239
|
+
if (rows.length === 0) {
|
|
1240
|
+
panel.label("No requests yet");
|
|
1241
|
+
return;
|
|
1242
|
+
}
|
|
1243
|
+
panel.table({
|
|
1244
|
+
rows,
|
|
1245
|
+
offset,
|
|
1246
|
+
scrollbar: true,
|
|
1247
|
+
// Zebra stripes are solid fills, which would break transparency
|
|
1248
|
+
zebra: !transparent,
|
|
1249
|
+
size: "1fr",
|
|
1250
|
+
// The table won't scroll past the last full page, so keep the anchor on the row it actually drew
|
|
1251
|
+
onRow: (_row, index, y) => {
|
|
1252
|
+
if (y === 1)
|
|
1253
|
+
view.requestTopId = index === 0 ? null : rows[index].id;
|
|
1254
|
+
},
|
|
1255
|
+
onScroll: (delta) => {
|
|
1256
|
+
const next = Math.max(0, Math.min(rows.length - 1, offset + delta));
|
|
1257
|
+
view.requestTopId = next === 0 ? null : rows[next].id;
|
|
1258
|
+
ui.ctx.invalidate();
|
|
1259
|
+
},
|
|
1260
|
+
columns: [
|
|
1261
|
+
{ key: "time", title: "Time", width: 8, color: theme.muted, render: (r) => formatTime(r.timestamp) },
|
|
1262
|
+
{ key: "method", title: "Method", width: 7, color: (r) => r.method === "WS" ? theme.primary : void 0 },
|
|
1263
|
+
{ key: "status", title: "Status", width: 6, align: "right", color: (r) => statusCodeColor(r.status, theme), render: (r) => r.status === null ? "\u2014" : String(r.status) },
|
|
1264
|
+
{ key: "durationMs", title: "Duration", width: 10, align: "right", render: (r) => formatMs(r.durationMs) },
|
|
1265
|
+
{ key: "bytes", title: "Size", width: 10, align: "right", render: (r) => formatBytes(r.bytes) },
|
|
1266
|
+
{ key: "address", title: "Client", width: 18, color: theme.muted, render: (r) => r.address ?? "\u2014" },
|
|
1267
|
+
{ key: "path", title: "Path" }
|
|
1268
|
+
]
|
|
1269
|
+
});
|
|
1270
|
+
});
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
// src/commands/dashboard.ts
|
|
1274
|
+
var DASHBOARD_HELP = `Usage: mavonengine dashboard [options]
|
|
1275
|
+
|
|
1276
|
+
Live terminal dashboard for a running MavonEngine game server.
|
|
1277
|
+
|
|
1278
|
+
Options:
|
|
1279
|
+
--host <host> Server host (default: localhost)
|
|
1280
|
+
--port <port> Server HTTP port (default: 8050)
|
|
1281
|
+
--url <url> Full metrics URL, overrides --host and --port
|
|
1282
|
+
--token <token> Metrics token (or set MAVONENGINE_METRICS_TOKEN)
|
|
1283
|
+
--interval <ms> Poll interval in ms (default: 1000)
|
|
1284
|
+
--theme <name> dark, dracula, nord, tokyoNight, gruvbox, matrix, light, \u2026
|
|
1285
|
+
--transparent Let a transparent terminal background show through
|
|
1286
|
+
(text can be hard to read over a light wallpaper)
|
|
1287
|
+
--sim Show simulated data instead of connecting to a server
|
|
1288
|
+
-h, --help Show this help
|
|
1289
|
+
`;
|
|
1290
|
+
var STARTUP_POLL_MS = 250;
|
|
1291
|
+
var UsageError = class extends Error {
|
|
1292
|
+
};
|
|
1293
|
+
function parseDashboardArgs(args, env = process3.env) {
|
|
1294
|
+
const { values } = parseArgs({
|
|
1295
|
+
args,
|
|
1296
|
+
options: {
|
|
1297
|
+
host: { type: "string", default: "localhost" },
|
|
1298
|
+
port: { type: "string", default: "8050" },
|
|
1299
|
+
url: { type: "string" },
|
|
1300
|
+
token: { type: "string" },
|
|
1301
|
+
interval: { type: "string", default: "1000" },
|
|
1302
|
+
theme: { type: "string" },
|
|
1303
|
+
transparent: { type: "boolean", default: false },
|
|
1304
|
+
sim: { type: "boolean", default: false },
|
|
1305
|
+
help: { type: "boolean", short: "h", default: false }
|
|
1306
|
+
},
|
|
1307
|
+
strict: true,
|
|
1308
|
+
allowPositionals: false
|
|
1309
|
+
});
|
|
1310
|
+
if (values.help)
|
|
1311
|
+
return "help";
|
|
1312
|
+
const intervalMs = Number(values.interval);
|
|
1313
|
+
if (!Number.isInteger(intervalMs) || intervalMs < 100)
|
|
1314
|
+
throw new UsageError("--interval must be a whole number of ms, at least 100");
|
|
1315
|
+
if (values.sim)
|
|
1316
|
+
return { source: simulatedSource(), intervalMs, theme: values.theme, transparent: values.transparent };
|
|
1317
|
+
let url = values.url;
|
|
1318
|
+
if (!url) {
|
|
1319
|
+
const port = Number(values.port);
|
|
1320
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535)
|
|
1321
|
+
throw new UsageError("--port must be between 1 and 65535");
|
|
1322
|
+
const host = values.host.includes(":") && !values.host.startsWith("[") ? `[${values.host}]` : values.host;
|
|
1323
|
+
url = `http://${host}:${port}/api/game/metrics`;
|
|
1324
|
+
}
|
|
1325
|
+
try {
|
|
1326
|
+
void new URL(url);
|
|
1327
|
+
} catch {
|
|
1328
|
+
throw new UsageError(`Invalid --url: ${url}`);
|
|
1329
|
+
}
|
|
1330
|
+
const token = values.token ?? env.MAVONENGINE_METRICS_TOKEN;
|
|
1331
|
+
return { source: httpSource({ url, token }), clientUrl: new URL("/", url).href, intervalMs, theme: values.theme, transparent: values.transparent };
|
|
1332
|
+
}
|
|
1333
|
+
async function runDashboard(options, signal) {
|
|
1334
|
+
const { createApp } = await import("@profullstack/hqtui");
|
|
1335
|
+
const client = new MetricsClient(options.source, {
|
|
1336
|
+
intervalMs: options.intervalMs,
|
|
1337
|
+
startupIntervalMs: options.startup ? STARTUP_POLL_MS : void 0
|
|
1338
|
+
});
|
|
1339
|
+
const view = { tab: 0, playerOffset: 0, logFromEnd: 0, requestTopId: null, selectedPlayerId: null, followSelectedPlayer: true };
|
|
1340
|
+
const app = await createApp({ fps: 15, theme: readableTheme(options.theme), paintBackground: !options.transparent });
|
|
1341
|
+
app.render(({ ui, theme }) => renderDashboard(ui, theme, client.state, view, {
|
|
1342
|
+
transparent: options.transparent,
|
|
1343
|
+
startup: options.startup?.(),
|
|
1344
|
+
openKey: !!options.clientUrl
|
|
1345
|
+
}));
|
|
1346
|
+
client.onChange(() => app.invalidate());
|
|
1347
|
+
const { clientUrl } = options;
|
|
1348
|
+
if (clientUrl && options.openBrowser) {
|
|
1349
|
+
const off = client.onChange(async (state) => {
|
|
1350
|
+
if (state.status !== "live")
|
|
1351
|
+
return;
|
|
1352
|
+
off();
|
|
1353
|
+
if (await servesPage(clientUrl))
|
|
1354
|
+
openBrowser(clientUrl);
|
|
1355
|
+
});
|
|
1356
|
+
}
|
|
1357
|
+
app.on("key", (event) => {
|
|
1358
|
+
if (event.key === "r")
|
|
1359
|
+
void client.refresh();
|
|
1360
|
+
if (event.key === "o" && options.clientUrl)
|
|
1361
|
+
openBrowser(options.clientUrl);
|
|
1362
|
+
const tab = tabForKey(event.key);
|
|
1363
|
+
if (tab !== null) {
|
|
1364
|
+
view.tab = tab;
|
|
1365
|
+
app.invalidate();
|
|
1366
|
+
}
|
|
1367
|
+
if (handlePlayersKey(view, client.state.snapshot, event.key))
|
|
1368
|
+
app.invalidate();
|
|
1369
|
+
});
|
|
1370
|
+
app.on("exit", () => client.stop());
|
|
1371
|
+
const onAbort = () => app.stop();
|
|
1372
|
+
signal?.addEventListener("abort", onAbort);
|
|
1373
|
+
client.start();
|
|
1374
|
+
if (!signal?.aborted)
|
|
1375
|
+
await app.start();
|
|
1376
|
+
signal?.removeEventListener("abort", onAbort);
|
|
1377
|
+
client.stop();
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
// src/commands/dev.ts
|
|
1381
|
+
import { execFileSync, spawn as spawn3 } from "child_process";
|
|
1382
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
1383
|
+
import { constants } from "os";
|
|
1384
|
+
import { dirname as dirname2, join as join2, resolve as resolve2 } from "path";
|
|
1385
|
+
import process4 from "process";
|
|
1386
|
+
import { setTimeout as delay } from "timers/promises";
|
|
1387
|
+
import { parseArgs as parseArgs2 } from "util";
|
|
1388
|
+
var DEV_HELP = `Usage: mavonengine dev [options]
|
|
1389
|
+
|
|
1390
|
+
Run the dev script of the MavonEngine project you are in (game server and client)
|
|
1391
|
+
and show the dashboard for it. Once the game server is up, the game opens in the
|
|
1392
|
+
browser. Quitting the dashboard stops the dev servers.
|
|
1393
|
+
|
|
1394
|
+
Options:
|
|
1395
|
+
--cwd <dir> Start looking for the project here (default: current directory)
|
|
1396
|
+
--no-dashboard Only run the dev script, with its output in the terminal
|
|
1397
|
+
--port <port> Game server HTTP port for the dashboard (default: 8050)
|
|
1398
|
+
--theme <name> Dashboard theme, see 'mavonengine dashboard --help'
|
|
1399
|
+
--transparent Let a transparent terminal background show through
|
|
1400
|
+
--no-open Don't open the game in the browser once the server is up
|
|
1401
|
+
-h, --help Show this help
|
|
1402
|
+
`;
|
|
1403
|
+
var MAX_CAPTURED_OUTPUT = 64 * 1024;
|
|
1404
|
+
var STOP_TIMEOUT_MS = 3e3;
|
|
1405
|
+
var KILL_TIMEOUT_MS = 2e3;
|
|
1406
|
+
var LOCKFILES = [
|
|
1407
|
+
["pnpm-lock.yaml", "pnpm"],
|
|
1408
|
+
["yarn.lock", "yarn"],
|
|
1409
|
+
["bun.lock", "bun"],
|
|
1410
|
+
["bun.lockb", "bun"],
|
|
1411
|
+
["package-lock.json", "npm"]
|
|
1412
|
+
];
|
|
1413
|
+
var WHITESPACE_RE = /\s+/;
|
|
1414
|
+
var DEV_FLAGS = {
|
|
1415
|
+
cwd: { type: "string" },
|
|
1416
|
+
dashboard: { type: "boolean", default: true },
|
|
1417
|
+
port: { type: "string" },
|
|
1418
|
+
theme: { type: "string" },
|
|
1419
|
+
transparent: { type: "boolean", default: false },
|
|
1420
|
+
open: { type: "boolean", default: true },
|
|
1421
|
+
help: { type: "boolean", short: "h", default: false }
|
|
1422
|
+
};
|
|
1423
|
+
function parseDevArgs(args, cwd = process4.cwd(), env = process4.env) {
|
|
1424
|
+
const { values } = parseArgs2({
|
|
1425
|
+
args,
|
|
1426
|
+
options: DEV_FLAGS,
|
|
1427
|
+
strict: true,
|
|
1428
|
+
allowPositionals: false,
|
|
1429
|
+
allowNegative: true
|
|
1430
|
+
});
|
|
1431
|
+
if (values.help)
|
|
1432
|
+
return "help";
|
|
1433
|
+
return toDevOptions(values, cwd, env);
|
|
1434
|
+
}
|
|
1435
|
+
function toDevOptions(values, cwd, env) {
|
|
1436
|
+
let dashboard = false;
|
|
1437
|
+
if (values.dashboard) {
|
|
1438
|
+
const dashboardArgs = [
|
|
1439
|
+
...values.port ? ["--port", values.port] : [],
|
|
1440
|
+
...values.theme ? ["--theme", values.theme] : [],
|
|
1441
|
+
...values.transparent ? ["--transparent"] : []
|
|
1442
|
+
];
|
|
1443
|
+
dashboard = parseDashboardArgs(dashboardArgs, env);
|
|
1444
|
+
dashboard.openBrowser = values.open && canOpenBrowserHere(env);
|
|
1445
|
+
}
|
|
1446
|
+
return { cwd: resolve2(cwd, values.cwd ?? "."), dashboard };
|
|
1447
|
+
}
|
|
1448
|
+
function readPackageJson(dir) {
|
|
1449
|
+
const path = join2(dir, "package.json");
|
|
1450
|
+
if (!existsSync2(path))
|
|
1451
|
+
return void 0;
|
|
1452
|
+
try {
|
|
1453
|
+
return JSON.parse(readFileSync2(path, "utf8"));
|
|
1454
|
+
} catch {
|
|
1455
|
+
return void 0;
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
function findProjectRoot(start) {
|
|
1459
|
+
for (let dir = resolve2(start); ; dir = dirname2(dir)) {
|
|
1460
|
+
const pkg = readPackageJson(dir);
|
|
1461
|
+
const deps = { ...pkg?.dependencies, ...pkg?.devDependencies };
|
|
1462
|
+
if ("@mavonengine/core" in deps && typeof pkg?.scripts?.dev === "string")
|
|
1463
|
+
return dir;
|
|
1464
|
+
if (dirname2(dir) === dir)
|
|
1465
|
+
return void 0;
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
function installedEngineVersion(root) {
|
|
1469
|
+
return readPackageJson(join2(root, "node_modules", "@mavonengine", "core"))?.version;
|
|
1470
|
+
}
|
|
1471
|
+
function detectPackageManager(root) {
|
|
1472
|
+
return LOCKFILES.find(([file]) => existsSync2(join2(root, file)))?.[1] ?? "npm";
|
|
1473
|
+
}
|
|
1474
|
+
function descendants(root, psOutput) {
|
|
1475
|
+
const children = /* @__PURE__ */ new Map();
|
|
1476
|
+
for (const line of psOutput.trim().split("\n")) {
|
|
1477
|
+
const [pid, ppid] = line.trim().split(WHITESPACE_RE).map(Number);
|
|
1478
|
+
if (!children.has(ppid))
|
|
1479
|
+
children.set(ppid, []);
|
|
1480
|
+
children.get(ppid).push(pid);
|
|
1481
|
+
}
|
|
1482
|
+
const found = [];
|
|
1483
|
+
const queue = [root];
|
|
1484
|
+
while (queue.length) {
|
|
1485
|
+
for (const child of children.get(queue.shift()) ?? []) {
|
|
1486
|
+
found.push(child);
|
|
1487
|
+
queue.push(child);
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
return found;
|
|
1491
|
+
}
|
|
1492
|
+
function processTree(pid) {
|
|
1493
|
+
if (process4.platform === "win32")
|
|
1494
|
+
return [];
|
|
1495
|
+
try {
|
|
1496
|
+
return [pid, ...descendants(pid, execFileSync("ps", ["-A", "-o", "pid=,ppid="], { encoding: "utf8" }))];
|
|
1497
|
+
} catch {
|
|
1498
|
+
return [pid];
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
function signalAll(pids, signal) {
|
|
1502
|
+
for (const p of pids) {
|
|
1503
|
+
try {
|
|
1504
|
+
process4.kill(p, signal);
|
|
1505
|
+
} catch {
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
function isAlive(pid) {
|
|
1510
|
+
try {
|
|
1511
|
+
process4.kill(pid, 0);
|
|
1512
|
+
return true;
|
|
1513
|
+
} catch {
|
|
1514
|
+
return false;
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
async function waitForExit(pids, timeoutMs) {
|
|
1518
|
+
const deadline = Date.now() + timeoutMs;
|
|
1519
|
+
while (pids.some(isAlive) && Date.now() < deadline)
|
|
1520
|
+
await delay(50);
|
|
1521
|
+
}
|
|
1522
|
+
function killTree(pid, signal) {
|
|
1523
|
+
if (process4.platform === "win32") {
|
|
1524
|
+
try {
|
|
1525
|
+
execFileSync("taskkill", ["/pid", String(pid), "/T", "/F"], { stdio: "ignore" });
|
|
1526
|
+
} catch {
|
|
1527
|
+
}
|
|
1528
|
+
return;
|
|
1529
|
+
}
|
|
1530
|
+
signalAll(processTree(pid), signal);
|
|
1531
|
+
}
|
|
1532
|
+
function startDev(root, { capture = false } = {}) {
|
|
1533
|
+
const pm = detectPackageManager(root);
|
|
1534
|
+
const child = spawn3(pm, ["run", "dev"], {
|
|
1535
|
+
cwd: root,
|
|
1536
|
+
stdio: capture ? ["ignore", "pipe", "pipe"] : "inherit",
|
|
1537
|
+
// npm, pnpm and yarn are .cmd shims on Windows
|
|
1538
|
+
shell: process4.platform === "win32"
|
|
1539
|
+
});
|
|
1540
|
+
let output = "";
|
|
1541
|
+
const keep = (chunk) => {
|
|
1542
|
+
output = (output + chunk).slice(-MAX_CAPTURED_OUTPUT);
|
|
1543
|
+
};
|
|
1544
|
+
const announce = `mavonengine: running '${pm} run dev' in ${root}
|
|
1545
|
+
`;
|
|
1546
|
+
if (capture)
|
|
1547
|
+
keep(announce);
|
|
1548
|
+
else
|
|
1549
|
+
process4.stdout.write(announce);
|
|
1550
|
+
child.stdout?.setEncoding("utf8").on("data", keep);
|
|
1551
|
+
child.stderr?.setEncoding("utf8").on("data", keep);
|
|
1552
|
+
let running = true;
|
|
1553
|
+
const onInterrupt = () => {
|
|
1554
|
+
};
|
|
1555
|
+
const forward = (signal) => child.pid && killTree(child.pid, signal);
|
|
1556
|
+
const onExit = () => running && child.pid && killTree(child.pid, "SIGTERM");
|
|
1557
|
+
process4.on("SIGINT", onInterrupt);
|
|
1558
|
+
process4.on("SIGTERM", forward);
|
|
1559
|
+
process4.on("SIGHUP", forward);
|
|
1560
|
+
process4.on("exit", onExit);
|
|
1561
|
+
const exited = new Promise((resolvePromise, reject) => {
|
|
1562
|
+
const cleanup = () => {
|
|
1563
|
+
running = false;
|
|
1564
|
+
process4.off("SIGINT", onInterrupt);
|
|
1565
|
+
process4.off("SIGTERM", forward);
|
|
1566
|
+
process4.off("SIGHUP", forward);
|
|
1567
|
+
process4.off("exit", onExit);
|
|
1568
|
+
};
|
|
1569
|
+
child.on("error", (err) => {
|
|
1570
|
+
cleanup();
|
|
1571
|
+
reject(err.code === "ENOENT" ? new Error(`'${pm}' was not found on your PATH`) : err);
|
|
1572
|
+
});
|
|
1573
|
+
child.on("exit", (code, signal) => {
|
|
1574
|
+
cleanup();
|
|
1575
|
+
resolvePromise(code ?? 128 + (signal ? constants.signals[signal] : 0));
|
|
1576
|
+
});
|
|
1577
|
+
});
|
|
1578
|
+
const stop = async () => {
|
|
1579
|
+
if (running && child.pid) {
|
|
1580
|
+
if (process4.platform === "win32") {
|
|
1581
|
+
killTree(child.pid, "SIGTERM");
|
|
1582
|
+
} else {
|
|
1583
|
+
const tree = processTree(child.pid);
|
|
1584
|
+
signalAll(tree, "SIGTERM");
|
|
1585
|
+
await waitForExit(tree, STOP_TIMEOUT_MS);
|
|
1586
|
+
signalAll(tree.filter(isAlive), "SIGKILL");
|
|
1587
|
+
await waitForExit(tree, KILL_TIMEOUT_MS);
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1590
|
+
return exited;
|
|
1591
|
+
};
|
|
1592
|
+
return { exited, stop, output: () => output };
|
|
1593
|
+
}
|
|
1594
|
+
function runDev(root) {
|
|
1595
|
+
return startDev(root).exited;
|
|
1596
|
+
}
|
|
1597
|
+
async function runDevWithDashboard(root, dashboard) {
|
|
1598
|
+
const dev = startDev(root, { capture: true });
|
|
1599
|
+
const engineVersion = installedEngineVersion(root);
|
|
1600
|
+
const startup = () => ({ engineVersion, output: dev.output() });
|
|
1601
|
+
const devExited = new AbortController();
|
|
1602
|
+
dev.exited.then(() => devExited.abort(), () => devExited.abort());
|
|
1603
|
+
let quit = false;
|
|
1604
|
+
try {
|
|
1605
|
+
await runDashboard({ ...dashboard, startup }, devExited.signal);
|
|
1606
|
+
} finally {
|
|
1607
|
+
quit = !devExited.signal.aborted;
|
|
1608
|
+
if (quit)
|
|
1609
|
+
await dev.stop().catch(() => {
|
|
1610
|
+
});
|
|
1611
|
+
}
|
|
1612
|
+
if (quit)
|
|
1613
|
+
return 0;
|
|
1614
|
+
const exitCode = await dev.exited;
|
|
1615
|
+
process4.stderr.write(`${dev.output()}
|
|
1616
|
+
mavonengine: the dev script exited with code ${exitCode}
|
|
1617
|
+
`);
|
|
1618
|
+
return exitCode;
|
|
1619
|
+
}
|
|
1620
|
+
async function runDevCommand(options) {
|
|
1621
|
+
const root = findProjectRoot(options.cwd);
|
|
1622
|
+
if (!root)
|
|
1623
|
+
throw new UsageError(`no MavonEngine project found in ${options.cwd} or its parents (looking for a package.json with @mavonengine/core and a dev script)`);
|
|
1624
|
+
if (options.dashboard && process4.stdout.isTTY && process4.stdin.isTTY)
|
|
1625
|
+
return runDevWithDashboard(root, options.dashboard);
|
|
1626
|
+
return runDev(root);
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
// src/commands/create.ts
|
|
1630
|
+
var CREATE_HELP = `Usage: mavonengine create [name] [options]
|
|
1631
|
+
|
|
1632
|
+
Create a MavonEngine project from the multiplayer template, install its
|
|
1633
|
+
dependencies and start it like 'mavonengine dev'. Asks for a name if none is given.
|
|
1634
|
+
|
|
1635
|
+
Options:
|
|
1636
|
+
--cwd <dir> Create the project in here (default: current directory)
|
|
1637
|
+
--no-dashboard Print the progress and the dev script's output instead
|
|
1638
|
+
--port <port> Game server HTTP port for the dashboard (default: 8050)
|
|
1639
|
+
--theme <name> Dashboard theme, see 'mavonengine dashboard --help'
|
|
1640
|
+
--transparent Let a transparent terminal background show through
|
|
1641
|
+
--no-open Don't open the game in the browser once the server is up
|
|
1642
|
+
-h, --help Show this help
|
|
1643
|
+
`;
|
|
1644
|
+
var MAX_SETUP_OUTPUT = 64 * 1024;
|
|
1645
|
+
function parseCreateArgs(args, cwd = process5.cwd(), env = process5.env) {
|
|
1646
|
+
const { values, positionals } = parseArgs3({
|
|
1647
|
+
args,
|
|
1648
|
+
options: DEV_FLAGS,
|
|
1649
|
+
strict: true,
|
|
1650
|
+
allowPositionals: true,
|
|
1651
|
+
allowNegative: true
|
|
1652
|
+
});
|
|
1653
|
+
if (values.help)
|
|
1654
|
+
return "help";
|
|
1655
|
+
if (positionals.length > 1)
|
|
1656
|
+
throw new UsageError(`expected one project name, got ${positionals.length}`);
|
|
1657
|
+
const projectName = positionals[0]?.trim() || void 0;
|
|
1658
|
+
if (projectName)
|
|
1659
|
+
checkProjectName(projectName);
|
|
1660
|
+
return { ...toDevOptions(values, cwd, env), projectName };
|
|
1661
|
+
}
|
|
1662
|
+
function checkProjectName(name) {
|
|
1663
|
+
if (!PROJECT_NAME_RE.test(name))
|
|
1664
|
+
throw new UsageError(`invalid project name '${name}'`);
|
|
1665
|
+
}
|
|
1666
|
+
async function promptProjectName() {
|
|
1667
|
+
const rl = createInterface({ input: process5.stdin, output: process5.stdout });
|
|
1668
|
+
try {
|
|
1669
|
+
return (await rl.question("Project name: ")).trim();
|
|
1670
|
+
} finally {
|
|
1671
|
+
rl.close();
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
function gatedSource(source, ready) {
|
|
1675
|
+
return {
|
|
1676
|
+
...source,
|
|
1677
|
+
fetch: () => ready() ? source.fetch() : Promise.reject(new MetricsSourceError("Creating the project", "connecting"))
|
|
1678
|
+
};
|
|
1679
|
+
}
|
|
1680
|
+
async function runCreateWithDashboard(projectName, parentDir, dashboard) {
|
|
1681
|
+
let label = "Creating the project\u2026";
|
|
1682
|
+
let output = "";
|
|
1683
|
+
let engineVersion;
|
|
1684
|
+
let dev;
|
|
1685
|
+
const report = {
|
|
1686
|
+
step: (text) => {
|
|
1687
|
+
label = text;
|
|
1688
|
+
},
|
|
1689
|
+
output: (chunk) => {
|
|
1690
|
+
output = (output + chunk).slice(-MAX_SETUP_OUTPUT);
|
|
1691
|
+
}
|
|
1692
|
+
};
|
|
1693
|
+
const setup = new AbortController();
|
|
1694
|
+
const done = new AbortController();
|
|
1695
|
+
const work = (async () => {
|
|
1696
|
+
const dir = await createProject({ projectName, parentDir, report, signal: setup.signal });
|
|
1697
|
+
setup.signal.throwIfAborted();
|
|
1698
|
+
engineVersion = installedEngineVersion(dir);
|
|
1699
|
+
label = "Starting the game server\u2026";
|
|
1700
|
+
dev = startDev(dir, { capture: true });
|
|
1701
|
+
return dev.exited;
|
|
1702
|
+
})();
|
|
1703
|
+
work.then(() => done.abort(), () => done.abort());
|
|
1704
|
+
const startup = () => dev ? { engineVersion, output: dev.output() } : { engineVersion, label, output, outputTitle: "Setup" };
|
|
1705
|
+
let quit = false;
|
|
1706
|
+
try {
|
|
1707
|
+
await runDashboard({ ...dashboard, source: gatedSource(dashboard.source, () => dev !== void 0), startup }, done.signal);
|
|
1708
|
+
} finally {
|
|
1709
|
+
quit = !done.signal.aborted;
|
|
1710
|
+
if (quit) {
|
|
1711
|
+
setup.abort();
|
|
1712
|
+
await dev?.stop().catch(() => {
|
|
1713
|
+
});
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
if (quit) {
|
|
1717
|
+
if (dev)
|
|
1718
|
+
return 0;
|
|
1719
|
+
await work.catch(() => {
|
|
1720
|
+
});
|
|
1721
|
+
process5.stderr.write(`mavonengine: stopped creating the project, what was set up so far is in ${projectDirFor(projectName, parentDir)}
|
|
1722
|
+
`);
|
|
1723
|
+
return 130;
|
|
1724
|
+
}
|
|
1725
|
+
try {
|
|
1726
|
+
const exitCode = await work;
|
|
1727
|
+
process5.stderr.write(`${dev.output()}
|
|
1728
|
+
mavonengine: the dev script exited with code ${exitCode}
|
|
1729
|
+
`);
|
|
1730
|
+
return exitCode;
|
|
1731
|
+
} catch (err) {
|
|
1732
|
+
process5.stderr.write(output);
|
|
1733
|
+
throw err;
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
async function runCreatePlain(projectName, parentDir) {
|
|
1737
|
+
const report = {
|
|
1738
|
+
step: () => {
|
|
1739
|
+
},
|
|
1740
|
+
output: (chunk) => process5.stdout.write(chunk)
|
|
1741
|
+
};
|
|
1742
|
+
const dir = await createProject({ projectName, parentDir, report });
|
|
1743
|
+
process5.stdout.write(`
|
|
1744
|
+
mavonengine: your project is ready at ${dir}
|
|
1745
|
+
`);
|
|
1746
|
+
return runDev(dir);
|
|
1747
|
+
}
|
|
1748
|
+
async function runCreateCommand(options) {
|
|
1749
|
+
const interactive = !!process5.stdout.isTTY && !!process5.stdin.isTTY;
|
|
1750
|
+
let projectName = options.projectName;
|
|
1751
|
+
if (!projectName) {
|
|
1752
|
+
if (!interactive)
|
|
1753
|
+
throw new UsageError("a project name is required, e.g. mavonengine create my-game");
|
|
1754
|
+
projectName = await promptProjectName();
|
|
1755
|
+
if (!projectName)
|
|
1756
|
+
throw new UsageError("a project name is required");
|
|
1757
|
+
checkProjectName(projectName);
|
|
1758
|
+
}
|
|
1759
|
+
const dir = projectDirFor(projectName, options.cwd);
|
|
1760
|
+
if (existsSync3(dir))
|
|
1761
|
+
throw new UsageError(`${dir} already exists`);
|
|
1762
|
+
if (options.dashboard && interactive)
|
|
1763
|
+
return runCreateWithDashboard(projectName, options.cwd, options.dashboard);
|
|
1764
|
+
return runCreatePlain(projectName, options.cwd);
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
// src/index.ts
|
|
1768
|
+
var HELP = `Usage: mavonengine <command> [options]
|
|
1769
|
+
|
|
1770
|
+
Commands:
|
|
1771
|
+
create Create a project from the multiplayer template and start it
|
|
1772
|
+
dashboard Live terminal dashboard for a running game server
|
|
1773
|
+
dev Run the project's dev servers (game server and client)
|
|
1774
|
+
|
|
1775
|
+
Options:
|
|
1776
|
+
-h, --help Show help
|
|
1777
|
+
-v, --version Show version
|
|
1778
|
+
|
|
1779
|
+
Run 'mavonengine <command> --help' for command options.
|
|
1780
|
+
`;
|
|
1781
|
+
async function main(argv) {
|
|
1782
|
+
const [command, ...args] = argv;
|
|
1783
|
+
if (!command || command === "-h" || command === "--help" || command === "help") {
|
|
1784
|
+
process6.stdout.write(HELP);
|
|
1785
|
+
return 0;
|
|
1786
|
+
}
|
|
1787
|
+
if (command === "-v" || command === "--version") {
|
|
1788
|
+
process6.stdout.write(`${version}
|
|
1789
|
+
`);
|
|
1790
|
+
return 0;
|
|
1791
|
+
}
|
|
1792
|
+
if (command === "create") {
|
|
1793
|
+
const options = parseCreateArgs(args);
|
|
1794
|
+
if (options === "help") {
|
|
1795
|
+
process6.stdout.write(CREATE_HELP);
|
|
1796
|
+
return 0;
|
|
1797
|
+
}
|
|
1798
|
+
return runCreateCommand(options);
|
|
1799
|
+
}
|
|
1800
|
+
if (command === "dashboard") {
|
|
1801
|
+
const options = parseDashboardArgs(args);
|
|
1802
|
+
if (options === "help") {
|
|
1803
|
+
process6.stdout.write(DASHBOARD_HELP);
|
|
1804
|
+
return 0;
|
|
1805
|
+
}
|
|
1806
|
+
if (!process6.stdout.isTTY || !process6.stdin.isTTY) {
|
|
1807
|
+
process6.stderr.write("mavonengine dashboard needs an interactive terminal\n");
|
|
1808
|
+
return 1;
|
|
1809
|
+
}
|
|
1810
|
+
await runDashboard(options);
|
|
1811
|
+
return 0;
|
|
1812
|
+
}
|
|
1813
|
+
if (command === "dev") {
|
|
1814
|
+
const options = parseDevArgs(args);
|
|
1815
|
+
if (options === "help") {
|
|
1816
|
+
process6.stdout.write(DEV_HELP);
|
|
1817
|
+
return 0;
|
|
1818
|
+
}
|
|
1819
|
+
return runDevCommand(options);
|
|
1820
|
+
}
|
|
1821
|
+
throw new UsageError(`Unknown command '${command}'
|
|
1822
|
+
|
|
1823
|
+
${HELP}`);
|
|
1824
|
+
}
|
|
1825
|
+
main(process6.argv.slice(2)).then((code) => process6.exit(code)).catch((err) => {
|
|
1826
|
+
const isUsage = err instanceof UsageError || err.code?.startsWith("ERR_PARSE_ARGS");
|
|
1827
|
+
process6.stderr.write(`mavonengine: ${err.message}
|
|
1828
|
+
`);
|
|
1829
|
+
process6.exit(isUsage ? 2 : 1);
|
|
1830
|
+
});
|