@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.
Files changed (39) hide show
  1. package/package.json +1 -1
  2. package/src/cli/validate.test.ts +194 -2
  3. package/src/cli/validate.ts +83 -1
  4. package/src/context/bridgeContext.ts +9 -2
  5. package/src/guides/variables.ts +1 -0
  6. package/src/manifest/manifest.ts +1 -1
  7. package/src/modules/bridgeModule.ts +2 -1
  8. package/src/panel/bridgePanel.ts +11 -0
  9. package/src/panel/bridgePlace.ts +4 -0
  10. package/src/panel/index.ts +1 -0
  11. package/src/rcon/index.ts +3 -2
  12. package/src/rcon/rconAccess.test.ts +304 -0
  13. package/src/rcon/rconAccess.ts +260 -0
  14. package/src/rcon/rconClient.test.ts +1544 -0
  15. package/src/rcon/rconClient.ts +275 -0
  16. package/src/rcon/rconError.ts +21 -0
  17. package/src/rcon/rconFrame.test.ts +269 -0
  18. package/src/rcon/{sourceRconFrame.ts → rconFrame.ts} +23 -14
  19. package/src/rcon/rconIds.test.ts +59 -0
  20. package/src/rcon/rconIds.ts +33 -0
  21. package/src/rcon/rconLink.ts +427 -0
  22. package/src/rcon/rconWriter.test.ts +95 -0
  23. package/src/rcon/rconWriter.ts +63 -0
  24. package/src/runtime/bridgeConsoleHandler.test.ts +196 -0
  25. package/src/runtime/bridgeConsoleHandler.ts +26 -0
  26. package/src/runtime/bridgeDriver.test.ts +145 -0
  27. package/src/runtime/bridgeDriver.ts +44 -2
  28. package/src/runtime/bridgeRcon.ts +6 -2
  29. package/src/runtime/bridgeRegistry.test.ts +96 -0
  30. package/src/runtime/bridgeRegistry.ts +33 -1
  31. package/src/runtime/serve.ts +10 -1
  32. package/src/steam/avatars.test.ts +306 -0
  33. package/src/steam/avatars.ts +107 -0
  34. package/src/steam/index.ts +1 -0
  35. package/src/wire/panel.test.ts +79 -0
  36. package/src/wire/panel.ts +11 -0
  37. package/src/rcon/sourceRcon.test.ts +0 -308
  38. package/src/rcon/sourceRcon.ts +0 -276
  39. package/src/rcon/sourceRconFrame.test.ts +0 -152
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@serverkgg/bridge",
3
- "version": "0.0.13",
3
+ "version": "0.0.15",
4
4
  "description": "Serverk game bridge and driver runtime, wire protocol, manifest schema, and maintainer CLI",
5
5
  "homepage": "https://serverk.gg",
6
6
  "license": "SEE LICENSE IN LICENSE.md",
@@ -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 { runValidate, setupReportLines } from "./validate";
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
+ });
@@ -15,7 +15,15 @@ import {
15
15
  readPackageDocument,
16
16
  } from "../manifest";
17
17
  import type { BridgeDriver, BridgeSetupModule } from "../modules";
18
- import { type BridgeSetupStep, BridgeSetupStepKind, isBridgeEventName } from "../protocol";
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
- command(text: string): Promise<string>;
130
- close(): void;
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 {
@@ -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,
@@ -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 the game to listen on this port. The port is still allocated; the panel hides the address otherwise.",
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 {
@@ -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;
@@ -0,0 +1,4 @@
1
+ export enum BridgePlace {
2
+ Overview = "overview",
3
+ Players = "players",
4
+ }
@@ -6,4 +6,5 @@ export * from "./bridgeFormTarget";
6
6
  export * from "./bridgeIcon";
7
7
  export * from "./bridgeLayout";
8
8
  export * from "./bridgePanel";
9
+ export * from "./bridgePlace";
9
10
  export * from "./bridgeUploadMode";
package/src/rcon/index.ts CHANGED
@@ -1,3 +1,4 @@
1
1
  export { BridgeRconCode, BridgeRconError } from "../core";
2
- export * from "./sourceRcon";
3
- export * from "./sourceRconFrame";
2
+ export * from "./rconAccess";
3
+ export * from "./rconClient";
4
+ export * from "./rconFrame";