@serverkgg/bridge 0.0.13 → 0.0.15
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/package.json +1 -1
- package/src/cli/validate.test.ts +194 -2
- package/src/cli/validate.ts +83 -1
- package/src/context/bridgeContext.ts +9 -2
- package/src/guides/variables.ts +1 -0
- package/src/manifest/manifest.ts +1 -1
- package/src/modules/bridgeModule.ts +2 -1
- package/src/panel/bridgePanel.ts +11 -0
- package/src/panel/bridgePlace.ts +4 -0
- package/src/panel/index.ts +1 -0
- package/src/rcon/index.ts +3 -2
- package/src/rcon/rconAccess.test.ts +304 -0
- package/src/rcon/rconAccess.ts +260 -0
- package/src/rcon/rconClient.test.ts +1544 -0
- package/src/rcon/rconClient.ts +275 -0
- package/src/rcon/rconError.ts +21 -0
- package/src/rcon/rconFrame.test.ts +269 -0
- package/src/rcon/{sourceRconFrame.ts → rconFrame.ts} +23 -14
- package/src/rcon/rconIds.test.ts +59 -0
- package/src/rcon/rconIds.ts +33 -0
- package/src/rcon/rconLink.ts +427 -0
- package/src/rcon/rconWriter.test.ts +95 -0
- package/src/rcon/rconWriter.ts +63 -0
- package/src/runtime/bridgeConsoleHandler.test.ts +196 -0
- package/src/runtime/bridgeConsoleHandler.ts +26 -0
- package/src/runtime/bridgeDriver.test.ts +145 -0
- package/src/runtime/bridgeDriver.ts +44 -2
- package/src/runtime/bridgeRcon.ts +6 -2
- package/src/runtime/bridgeRegistry.test.ts +96 -0
- package/src/runtime/bridgeRegistry.ts +33 -1
- package/src/runtime/serve.ts +10 -1
- package/src/steam/avatars.test.ts +306 -0
- package/src/steam/avatars.ts +107 -0
- package/src/steam/index.ts +1 -0
- package/src/wire/panel.test.ts +79 -0
- package/src/wire/panel.ts +11 -0
- package/src/rcon/sourceRcon.test.ts +0 -308
- package/src/rcon/sourceRcon.ts +0 -276
- package/src/rcon/sourceRconFrame.test.ts +0 -152
package/package.json
CHANGED
package/src/cli/validate.test.ts
CHANGED
|
@@ -4,8 +4,17 @@ import { tmpdir } from "node:os";
|
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { BridgeKind, BridgeSetupStepKind } from "../core";
|
|
6
6
|
import { GuideOpenTab } from "../guides";
|
|
7
|
-
import type { BridgeSetupModule } from "../modules";
|
|
8
|
-
import {
|
|
7
|
+
import type { BridgeDriver, BridgeSetupModule } from "../modules";
|
|
8
|
+
import {
|
|
9
|
+
BridgeControl,
|
|
10
|
+
type BridgeFormSection,
|
|
11
|
+
BridgeFormTarget,
|
|
12
|
+
BridgeLayout,
|
|
13
|
+
BridgePlace,
|
|
14
|
+
type BridgeTab,
|
|
15
|
+
type BridgeTableSection,
|
|
16
|
+
} from "../panel";
|
|
17
|
+
import { placementReportLines, runValidate, setupReportLines } from "./validate";
|
|
9
18
|
|
|
10
19
|
const text = (value: string) => ({
|
|
11
20
|
ar: value,
|
|
@@ -202,3 +211,186 @@ describe("the setup report", () => {
|
|
|
202
211
|
]);
|
|
203
212
|
});
|
|
204
213
|
});
|
|
214
|
+
|
|
215
|
+
describe("the placement report", () => {
|
|
216
|
+
const rosterTable = (place?: BridgePlace): BridgeTableSection => ({
|
|
217
|
+
layout: BridgeLayout.Table,
|
|
218
|
+
id: "online",
|
|
219
|
+
module: "players",
|
|
220
|
+
place,
|
|
221
|
+
columns: [
|
|
222
|
+
{
|
|
223
|
+
key: "name",
|
|
224
|
+
label: text("Name"),
|
|
225
|
+
},
|
|
226
|
+
],
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
const settingsForm = (): BridgeFormSection => ({
|
|
230
|
+
layout: BridgeLayout.Form,
|
|
231
|
+
id: "world",
|
|
232
|
+
target: BridgeFormTarget.Variables,
|
|
233
|
+
fields: [
|
|
234
|
+
{
|
|
235
|
+
key: "WORLD_NAME",
|
|
236
|
+
control: BridgeControl.Text,
|
|
237
|
+
label: text("World name"),
|
|
238
|
+
},
|
|
239
|
+
],
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
const driverWithTabs = (tabs: BridgeTab[]): BridgeDriver => ({
|
|
243
|
+
install: {
|
|
244
|
+
kind: BridgeKind.Install,
|
|
245
|
+
run: async () => {
|
|
246
|
+
return;
|
|
247
|
+
},
|
|
248
|
+
},
|
|
249
|
+
lifecycle: {
|
|
250
|
+
kind: BridgeKind.Lifecycle,
|
|
251
|
+
command: async () => [
|
|
252
|
+
"run",
|
|
253
|
+
],
|
|
254
|
+
},
|
|
255
|
+
panel: {
|
|
256
|
+
tabs,
|
|
257
|
+
},
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
test("says nothing about a roster table that declares where it belongs", () => {
|
|
261
|
+
expect(
|
|
262
|
+
placementReportLines(
|
|
263
|
+
driverWithTabs([
|
|
264
|
+
{
|
|
265
|
+
id: "roster",
|
|
266
|
+
title: text("Roster"),
|
|
267
|
+
sections: [
|
|
268
|
+
rosterTable(BridgePlace.Players),
|
|
269
|
+
],
|
|
270
|
+
},
|
|
271
|
+
]),
|
|
272
|
+
),
|
|
273
|
+
).toEqual([
|
|
274
|
+
' placement: tab "roster" is fully placed, it will not appear in the sidebar',
|
|
275
|
+
]);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
test("warns that the platform absorbs an unplaced players collection by convention", () => {
|
|
279
|
+
expect(
|
|
280
|
+
placementReportLines(
|
|
281
|
+
driverWithTabs([
|
|
282
|
+
{
|
|
283
|
+
id: "roster",
|
|
284
|
+
title: text("Roster"),
|
|
285
|
+
sections: [
|
|
286
|
+
rosterTable(),
|
|
287
|
+
],
|
|
288
|
+
},
|
|
289
|
+
]),
|
|
290
|
+
),
|
|
291
|
+
).toEqual([
|
|
292
|
+
' warning: tab "roster" section "online" is the players collection with no place — the platform absorbs it by convention today; declare `place: players` to make it explicit',
|
|
293
|
+
]);
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
test("warns about a tab that takes the name of a platform sidebar entry", () => {
|
|
297
|
+
expect(
|
|
298
|
+
placementReportLines(
|
|
299
|
+
driverWithTabs([
|
|
300
|
+
{
|
|
301
|
+
id: "analytics",
|
|
302
|
+
title: text("Analytics"),
|
|
303
|
+
sections: [
|
|
304
|
+
settingsForm(),
|
|
305
|
+
],
|
|
306
|
+
},
|
|
307
|
+
]),
|
|
308
|
+
),
|
|
309
|
+
).toEqual([
|
|
310
|
+
` warning: tab "analytics" takes the name of a platform sidebar entry — rename it, the platform's own entry owns that noun`,
|
|
311
|
+
]);
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
test("leaves a players tab alone while it is the roster tab", () => {
|
|
315
|
+
expect(
|
|
316
|
+
placementReportLines(
|
|
317
|
+
driverWithTabs([
|
|
318
|
+
{
|
|
319
|
+
id: "players",
|
|
320
|
+
title: text("Players"),
|
|
321
|
+
sections: [
|
|
322
|
+
rosterTable(BridgePlace.Players),
|
|
323
|
+
],
|
|
324
|
+
},
|
|
325
|
+
]),
|
|
326
|
+
),
|
|
327
|
+
).toEqual([
|
|
328
|
+
' placement: tab "players" is fully placed, it will not appear in the sidebar',
|
|
329
|
+
]);
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
test("warns about a players tab that holds no roster", () => {
|
|
333
|
+
expect(
|
|
334
|
+
placementReportLines(
|
|
335
|
+
driverWithTabs([
|
|
336
|
+
{
|
|
337
|
+
id: "players",
|
|
338
|
+
title: text("Players"),
|
|
339
|
+
sections: [
|
|
340
|
+
settingsForm(),
|
|
341
|
+
],
|
|
342
|
+
},
|
|
343
|
+
]),
|
|
344
|
+
),
|
|
345
|
+
).toEqual([
|
|
346
|
+
` warning: tab "players" takes the name of a platform sidebar entry — rename it, the platform's own entry owns that noun`,
|
|
347
|
+
]);
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
test("says nothing about a tab that declares no sections at all", () => {
|
|
351
|
+
expect(
|
|
352
|
+
placementReportLines(
|
|
353
|
+
driverWithTabs([
|
|
354
|
+
{
|
|
355
|
+
id: "empty",
|
|
356
|
+
title: text("Empty"),
|
|
357
|
+
sections: [],
|
|
358
|
+
},
|
|
359
|
+
]),
|
|
360
|
+
),
|
|
361
|
+
).toEqual([]);
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
test("still warns about an empty players tab, which holds no roster", () => {
|
|
365
|
+
expect(
|
|
366
|
+
placementReportLines(
|
|
367
|
+
driverWithTabs([
|
|
368
|
+
{
|
|
369
|
+
id: "players",
|
|
370
|
+
title: text("Players"),
|
|
371
|
+
sections: [],
|
|
372
|
+
},
|
|
373
|
+
]),
|
|
374
|
+
),
|
|
375
|
+
).toEqual([
|
|
376
|
+
` warning: tab "players" takes the name of a platform sidebar entry — rename it, the platform's own entry owns that noun`,
|
|
377
|
+
]);
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
test("says nothing about a tab that keeps a leftover section", () => {
|
|
381
|
+
expect(
|
|
382
|
+
placementReportLines(
|
|
383
|
+
driverWithTabs([
|
|
384
|
+
{
|
|
385
|
+
id: "world",
|
|
386
|
+
title: text("World"),
|
|
387
|
+
sections: [
|
|
388
|
+
rosterTable(BridgePlace.Players),
|
|
389
|
+
settingsForm(),
|
|
390
|
+
],
|
|
391
|
+
},
|
|
392
|
+
]),
|
|
393
|
+
),
|
|
394
|
+
).toEqual([]);
|
|
395
|
+
});
|
|
396
|
+
});
|
package/src/cli/validate.ts
CHANGED
|
@@ -15,7 +15,15 @@ import {
|
|
|
15
15
|
readPackageDocument,
|
|
16
16
|
} from "../manifest";
|
|
17
17
|
import type { BridgeDriver, BridgeSetupModule } from "../modules";
|
|
18
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
BridgeLayout,
|
|
20
|
+
BridgePlace,
|
|
21
|
+
type BridgeSection,
|
|
22
|
+
type BridgeSetupStep,
|
|
23
|
+
BridgeSetupStepKind,
|
|
24
|
+
type BridgeTab,
|
|
25
|
+
isBridgeEventName,
|
|
26
|
+
} from "../protocol";
|
|
19
27
|
import { validateDriver, validatePortActivation } from "../runtime/bridgeDriver";
|
|
20
28
|
import { directorySource } from "./directorySource";
|
|
21
29
|
|
|
@@ -193,6 +201,79 @@ const reportActionTargets = (driver: BridgeDriver) => {
|
|
|
193
201
|
}
|
|
194
202
|
};
|
|
195
203
|
|
|
204
|
+
const PLATFORM_TAB_IDS: ReadonlySet<string> = new Set([
|
|
205
|
+
"overview",
|
|
206
|
+
"console",
|
|
207
|
+
"files",
|
|
208
|
+
"backups",
|
|
209
|
+
"schedules",
|
|
210
|
+
"analytics",
|
|
211
|
+
"feed",
|
|
212
|
+
"access",
|
|
213
|
+
"integrations",
|
|
214
|
+
"needs",
|
|
215
|
+
"setup",
|
|
216
|
+
"change-game",
|
|
217
|
+
]);
|
|
218
|
+
|
|
219
|
+
const ROSTER_TAB_ID = "players";
|
|
220
|
+
|
|
221
|
+
const ROSTER_MODULE = "players";
|
|
222
|
+
|
|
223
|
+
const isCollectionSection = (section: BridgeSection) => {
|
|
224
|
+
return section.layout === BridgeLayout.Table || section.layout === BridgeLayout.Cards;
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
const placeOf = (section: BridgeSection) => {
|
|
228
|
+
return "place" in section ? section.place : undefined;
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
const isRosterTab = (tab: BridgeTab) => {
|
|
232
|
+
if (tab.sections.some((section) => isCollectionSection(section) && section.module === ROSTER_MODULE)) {
|
|
233
|
+
return true;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return tab.sections.length > 0 && tab.sections.every((section) => placeOf(section) === BridgePlace.Players);
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
export const placementReportLines = (driver: BridgeDriver): string[] => {
|
|
240
|
+
const lines: string[] = [];
|
|
241
|
+
|
|
242
|
+
for (const tab of driver.panel?.tabs ?? []) {
|
|
243
|
+
for (const section of tab.sections) {
|
|
244
|
+
if (!isCollectionSection(section) || section.module !== ROSTER_MODULE) {
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (placeOf(section) === BridgePlace.Players) {
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
lines.push(
|
|
253
|
+
` warning: tab "${tab.id}" section "${section.id}" is the players collection with no place — the platform absorbs it by convention today; declare \`place: players\` to make it explicit`,
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (PLATFORM_TAB_IDS.has(tab.id) || (tab.id === ROSTER_TAB_ID && !isRosterTab(tab))) {
|
|
258
|
+
lines.push(
|
|
259
|
+
` warning: tab "${tab.id}" takes the name of a platform sidebar entry — rename it, the platform's own entry owns that noun`,
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (tab.sections.length > 0 && tab.sections.every((section) => placeOf(section) !== undefined)) {
|
|
264
|
+
lines.push(` placement: tab "${tab.id}" is fully placed, it will not appear in the sidebar`);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return lines;
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
const reportPlacement = (driver: BridgeDriver) => {
|
|
272
|
+
for (const line of placementReportLines(driver)) {
|
|
273
|
+
console.log(line);
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
|
|
196
277
|
const openTargetOf = (target: GuideOpenTarget) => {
|
|
197
278
|
if (target.tab === GuideOpenTab.Panel) {
|
|
198
279
|
return `panel tab "${target.tabId}"`;
|
|
@@ -286,6 +367,7 @@ const validateGamePackage = async (source: PackageSource, manifest: Manifest, di
|
|
|
286
367
|
|
|
287
368
|
reportEvents(driver);
|
|
288
369
|
reportActionTargets(driver);
|
|
370
|
+
reportPlacement(driver);
|
|
289
371
|
};
|
|
290
372
|
|
|
291
373
|
export const runValidate = async (args: string[]) => {
|
|
@@ -123,11 +123,18 @@ export interface BridgeRconOptions {
|
|
|
123
123
|
password: string;
|
|
124
124
|
timeoutMs?: number;
|
|
125
125
|
multiPacket?: boolean;
|
|
126
|
+
mirroredIds?: boolean;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export interface BridgeRconCommandOptions {
|
|
130
|
+
timeoutMs?: number;
|
|
126
131
|
}
|
|
127
132
|
|
|
128
133
|
export interface BridgeRconClient {
|
|
129
|
-
|
|
130
|
-
|
|
134
|
+
readonly connected: boolean;
|
|
135
|
+
connect(): Promise<void>;
|
|
136
|
+
command(text: string, options?: BridgeRconCommandOptions): Promise<string>;
|
|
137
|
+
disconnect(): Promise<void>;
|
|
131
138
|
}
|
|
132
139
|
|
|
133
140
|
export interface BridgeRcons {
|
package/src/guides/variables.ts
CHANGED
|
@@ -12,6 +12,7 @@ export const GUIDE_VARIABLES = {
|
|
|
12
12
|
"server.code": GuideVariableFormat.Code,
|
|
13
13
|
"server.address": GuideVariableFormat.Code,
|
|
14
14
|
"server.host": GuideVariableFormat.Code,
|
|
15
|
+
"server.ip": GuideVariableFormat.Code,
|
|
15
16
|
"server.port": GuideVariableFormat.Code,
|
|
16
17
|
"server.game": GuideVariableFormat.Text,
|
|
17
18
|
"server.region": GuideVariableFormat.Text,
|
package/src/manifest/manifest.ts
CHANGED
|
@@ -90,7 +90,7 @@ const manifestPortSchema = z.object({
|
|
|
90
90
|
}),
|
|
91
91
|
activeWhen: z.array(manifestPortActivationSchema).max(4).default([]).meta({
|
|
92
92
|
description:
|
|
93
|
-
"Conditions on the server's variables that must all hold for
|
|
93
|
+
"Conditions on the server's variables that must all hold for this port to be published. The host port stays allocated either way; while a condition fails the container does not publish it and the panel hides the address.",
|
|
94
94
|
}),
|
|
95
95
|
mirror: z.boolean().default(false).meta({
|
|
96
96
|
description:
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { BridgeContext } from "../context";
|
|
1
|
+
import type { BridgeContext, BridgeReply } from "../context";
|
|
2
2
|
import type { BridgeInstallOutcome, BridgeKind, BridgeOption, BridgeRow, BridgeStream, BridgeValues } from "../core";
|
|
3
3
|
import type { BridgeDetail, BridgeDetailOutcome } from "../panel";
|
|
4
4
|
import type { BridgeCatalogEntry, BridgeCatalogPage, BridgeCatalogQuery } from "../protocol/bridgeCatalog";
|
|
@@ -78,6 +78,7 @@ export interface BridgeTerminalModule {
|
|
|
78
78
|
kind: BridgeKind.Terminal;
|
|
79
79
|
commands: BridgeTerminalCommand[];
|
|
80
80
|
rules?: BridgeTerminalRule[];
|
|
81
|
+
run?(context: BridgeContext, input: string): Promise<BridgeReply | null>;
|
|
81
82
|
}
|
|
82
83
|
|
|
83
84
|
export interface BridgeOptionsModule {
|
package/src/panel/bridgePanel.ts
CHANGED
|
@@ -5,6 +5,7 @@ import type { BridgeControl } from "./bridgeControl";
|
|
|
5
5
|
import type { BridgeFormTarget } from "./bridgeFormTarget";
|
|
6
6
|
import type { BridgeIcon } from "./bridgeIcon";
|
|
7
7
|
import type { BridgeLayout } from "./bridgeLayout";
|
|
8
|
+
import type { BridgePlace } from "./bridgePlace";
|
|
8
9
|
import type { BridgeUploadMode } from "./bridgeUploadMode";
|
|
9
10
|
|
|
10
11
|
export interface BridgeVisibleWhen {
|
|
@@ -44,6 +45,7 @@ export interface BridgeSectionAction {
|
|
|
44
45
|
confirm?: BridgeConfirm;
|
|
45
46
|
confirmText?: LocalizedText;
|
|
46
47
|
download?: boolean;
|
|
48
|
+
offline?: boolean;
|
|
47
49
|
fields?: BridgeField[];
|
|
48
50
|
}
|
|
49
51
|
|
|
@@ -74,6 +76,7 @@ export interface BridgeActionsSection {
|
|
|
74
76
|
id: string;
|
|
75
77
|
title?: LocalizedText;
|
|
76
78
|
help?: LocalizedText;
|
|
79
|
+
place?: BridgePlace;
|
|
77
80
|
module: string;
|
|
78
81
|
actions: BridgeSectionAction[];
|
|
79
82
|
visibleWhen?: BridgeVisibleWhen;
|
|
@@ -84,6 +87,7 @@ export interface BridgeFormSection {
|
|
|
84
87
|
layout: BridgeLayout.Form;
|
|
85
88
|
id: string;
|
|
86
89
|
title?: LocalizedText;
|
|
90
|
+
help?: LocalizedText;
|
|
87
91
|
target: BridgeFormTarget;
|
|
88
92
|
module?: string;
|
|
89
93
|
preview?: BridgePreviewRef;
|
|
@@ -106,6 +110,8 @@ export interface BridgeTableSection {
|
|
|
106
110
|
layout: BridgeLayout.Table;
|
|
107
111
|
id: string;
|
|
108
112
|
title?: LocalizedText;
|
|
113
|
+
help?: LocalizedText;
|
|
114
|
+
place?: BridgePlace;
|
|
109
115
|
module: string;
|
|
110
116
|
restartHint?: boolean;
|
|
111
117
|
columns: BridgeColumn[];
|
|
@@ -121,6 +127,8 @@ export interface BridgeCardsSection {
|
|
|
121
127
|
layout: BridgeLayout.Cards;
|
|
122
128
|
id: string;
|
|
123
129
|
title?: LocalizedText;
|
|
130
|
+
help?: LocalizedText;
|
|
131
|
+
place?: BridgePlace;
|
|
124
132
|
module: string;
|
|
125
133
|
titleKey: string;
|
|
126
134
|
subtitleKey?: string;
|
|
@@ -134,6 +142,7 @@ export interface BridgeCatalogSection {
|
|
|
134
142
|
layout: BridgeLayout.Catalog;
|
|
135
143
|
id: string;
|
|
136
144
|
title?: LocalizedText;
|
|
145
|
+
help?: LocalizedText;
|
|
137
146
|
module: string;
|
|
138
147
|
restartHint?: boolean;
|
|
139
148
|
reinstall?: boolean;
|
|
@@ -149,6 +158,8 @@ export interface BridgeDetailSection {
|
|
|
149
158
|
layout: BridgeLayout.Detail;
|
|
150
159
|
id: string;
|
|
151
160
|
title?: LocalizedText;
|
|
161
|
+
help?: LocalizedText;
|
|
162
|
+
place?: BridgePlace;
|
|
152
163
|
module: string;
|
|
153
164
|
actions?: BridgeSectionAction[];
|
|
154
165
|
reinstall?: boolean;
|
package/src/panel/index.ts
CHANGED
package/src/rcon/index.ts
CHANGED