@serverkgg/bridge 0.1.13 → 0.1.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 (35) hide show
  1. package/package.json +1 -1
  2. package/rules/package.md +3 -0
  3. package/src/catalogs/curseforge.test.ts +46 -0
  4. package/src/catalogs/curseforge.ts +20 -0
  5. package/src/context/bridgeContext.ts +1 -0
  6. package/src/manifest/extension.ts +1 -0
  7. package/src/manifest/manifest.test.ts +18 -0
  8. package/src/modules/bridge.ts +13 -2
  9. package/src/modules/bridgeModule.ts +20 -4
  10. package/src/panel/bridgePanel.ts +2 -0
  11. package/src/protocol/bridgeCatalog.ts +7 -0
  12. package/src/protocol/bridgeCatalogSource.ts +9 -0
  13. package/src/protocol/bridgeConfig.ts +1 -0
  14. package/src/protocol/bridgeFrame.ts +1 -0
  15. package/src/protocol/bridgeLimits.ts +6 -0
  16. package/src/protocol/bridgeManifest.ts +2 -0
  17. package/src/protocol/index.ts +1 -0
  18. package/src/runtime/bridgeActions.ts +9 -2
  19. package/src/runtime/bridgeConfigReader.test.ts +31 -0
  20. package/src/runtime/bridgeConfigReader.ts +1 -0
  21. package/src/runtime/bridgeContext.test.ts +53 -0
  22. package/src/runtime/bridgeContext.ts +3 -0
  23. package/src/runtime/bridgeDriver.test.ts +347 -1
  24. package/src/runtime/bridgeDriver.ts +130 -18
  25. package/src/runtime/bridgeLiveConfig.test.ts +137 -1
  26. package/src/runtime/bridgeLiveConfig.ts +46 -2
  27. package/src/runtime/bridgeRegistry.test.ts +220 -0
  28. package/src/runtime/bridgeRegistry.ts +27 -1
  29. package/src/runtime/bridgeSample.test.ts +63 -0
  30. package/src/runtime/bridgeSample.ts +22 -0
  31. package/src/runtime/serve.ts +5 -12
  32. package/src/steam/workshop.test.ts +19 -1
  33. package/src/steam/workshop.ts +12 -0
  34. package/src/wire/panel.test.ts +177 -0
  35. package/src/wire/panel.ts +16 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@serverkgg/bridge",
3
- "version": "0.1.13",
3
+ "version": "0.1.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",
package/rules/package.md CHANGED
@@ -36,9 +36,12 @@ One Serverk package (a game or a bot runtime) on `@serverkgg/bridge`, with no pi
36
36
  - A game declares each tab as `const <name>Tab: Bridge.Tab`, listed in `export const panel: Bridge.Panel`; a bot's panel is the tab `createBotPanel()` from `@serverkgg/bridge/bot` builds. Reusable copy is a `Bridge.Text` constant.
37
37
  - Every label, title, help text and message is `{ ar, en }`, Arabic first.
38
38
  - A settings form targets `BridgeFormTarget.Settings` and module `settings`, with a one-sentence `help` and `restartHint: true`.
39
+ - A variables-form field may declare `default`, which the form shows and `context.variable` returns while the variable is unset or empty. A default redefines unset, so set the current value on existing servers by hand before their next start.
39
40
  - In a game, the roster and moderation tables go on `place: BridgePlace.Players`, metrics on `BridgePlace.Overview`.
40
41
  - A setup `Form` step points at an existing panel form by `tab`, `section` and field keys, never redeclaring a field; `Form` and `Open` steps state `required`. A game's setup ends with the `Open` step `invite` targeting `GuideOpenTab.Access`.
41
42
  - A preview module that knows the change cannot install answers `blocked: true`, with the reason in `lines`. The panel then shows the lines with a close button only, and the platform refuses the change before it rebuilds anything.
43
+ - A catalog whose items the platform downloads is a `Bridge.SourcedCatalog` declaring `source`, with `provided` naming what the image ships. The platform answers its `search` and `preview`; `install` reads the archive at `workshopArchivePath(appId, id)` from `@serverkgg/bridge/steam`, deletes it, and links an item with `workshopItemPageUrl(id)`.
44
+ - A `Catalog` section may carry `upload` when its module defines `add`, which installs the folder the platform extracted the upload into and returns its entry.
42
45
  - `BridgeConfirm.None` runs on the click: reads, downloads, start, stop and restart.
43
46
  - `BridgeConfirm.Normal` asks once, for anything the player can come back from: a settings form, a variable flip, a reinstall, a version or type switch, a modpack or mod install, op and deop, kick, ban, whitelist, rotating a password.
44
47
  - `BridgeConfirm.Strong` is only for an action with no way back, where no restore point is taken and nothing in the panel brings the state back: deleting a world, save, map or character data, resetting a dimension, a wipe. A protected action takes no restore point.
@@ -9,6 +9,7 @@ import {
9
9
  type CurseforgeCatalogOptions,
10
10
  type CurseforgeFile,
11
11
  CurseforgeHash,
12
+ CurseforgeModLoader,
12
13
  CurseforgeReleaseType,
13
14
  CurseforgeSort,
14
15
  CurseforgeSortOrder,
@@ -536,6 +537,51 @@ describe("createCurseforgeCatalog", () => {
536
537
  expect(second.at(-1)).toBe(150);
537
538
  });
538
539
 
540
+ it("posts the mod ids in one sorted body inside the project window and keeps the latest file indexes", async () => {
541
+ const mod = {
542
+ id: 238_222,
543
+ latestFilesIndexes: [
544
+ {
545
+ gameVersion: "26.3",
546
+ fileId: 7_100_200,
547
+ filename: "jei-26.3-neoforge-31.16.0.90.jar",
548
+ releaseType: CurseforgeReleaseType.Release,
549
+ modLoader: CurseforgeModLoader.NeoForge,
550
+ },
551
+ ],
552
+ };
553
+ const { catalog, calls } = catalogContext({
554
+ result: {
555
+ data: [
556
+ mod,
557
+ ],
558
+ },
559
+ options: {
560
+ projectCacheSeconds: 600,
561
+ },
562
+ });
563
+
564
+ const mods = await catalog.mods([
565
+ 238_222,
566
+ 32_274,
567
+ 238_222,
568
+ ]);
569
+
570
+ expect(calls).toHaveLength(1);
571
+ expect(calls[0]?.url).toBe(`${CURSEFORGE_API}/mods`);
572
+ expect(calls[0]?.httpMethod).toBe(BridgeHttpMethod.Post);
573
+ expect(calls[0]?.cacheSeconds).toBe(600);
574
+ expect(JSON.parse(calls[0]?.body ?? "null")).toEqual({
575
+ modIds: [
576
+ 32_274,
577
+ 238_222,
578
+ ],
579
+ });
580
+ expect(mods).toEqual([
581
+ mod,
582
+ ] as unknown as typeof mods);
583
+ });
584
+
539
585
  it("refuses to work without a key", async () => {
540
586
  const { catalog, calls } = catalogContext({
541
587
  key: null,
@@ -51,6 +51,16 @@ export enum CurseforgeReleaseType {
51
51
  Release = 1,
52
52
  }
53
53
 
54
+ export enum CurseforgeModLoader {
55
+ Any = 0,
56
+ Cauldron = 2,
57
+ Fabric = 4,
58
+ Forge = 1,
59
+ LiteLoader = 3,
60
+ NeoForge = 6,
61
+ Quilt = 5,
62
+ }
63
+
54
64
  export enum CurseforgeDependency {
55
65
  EmbeddedLibrary = 1,
56
66
  Include = 6,
@@ -85,6 +95,14 @@ export interface CurseforgeCategory {
85
95
  isClass?: boolean | null;
86
96
  }
87
97
 
98
+ export interface CurseforgeFileIndex {
99
+ gameVersion: string;
100
+ fileId: number;
101
+ filename: string;
102
+ releaseType: number;
103
+ modLoader: number | null;
104
+ }
105
+
88
106
  export interface CurseforgeMod {
89
107
  id: number;
90
108
  classId: number | null;
@@ -102,6 +120,8 @@ export interface CurseforgeMod {
102
120
  links: {
103
121
  websiteUrl: string | null;
104
122
  } | null;
123
+ latestFiles?: CurseforgeFile[];
124
+ latestFilesIndexes?: CurseforgeFileIndex[];
105
125
  }
106
126
 
107
127
  export interface CurseforgeHashEntry {
@@ -7,6 +7,7 @@ export interface BridgeServer {
7
7
  cpuPercent: number;
8
8
  running: boolean;
9
9
  readonly publicAddress: string | null;
10
+ readonly playerAddress: string | null;
10
11
  }
11
12
 
12
13
  export interface BridgePortBlock {
@@ -12,6 +12,7 @@ export enum ManifestExtensionSource {
12
12
  CurseForge = "curseforge",
13
13
  Hangar = "hangar",
14
14
  Thunderstore = "thunderstore",
15
+ Workshop = "workshop",
15
16
  }
16
17
 
17
18
  export const EXTENSIONS_LIMIT = Object.values(ManifestExtensionKind).length;
@@ -322,6 +322,24 @@ describe("extensions", () => {
322
322
  expect(rendered).toContain(`"${value}"`);
323
323
  }
324
324
  });
325
+
326
+ it("carry the Steam Workshop as a catalog and render it into the JSON schema", () => {
327
+ const manifest = manifestSchema.parse(
328
+ withExtensions([
329
+ {
330
+ kind: ManifestExtensionKind.Mods,
331
+ sources: [
332
+ ManifestExtensionSource.Workshop,
333
+ ],
334
+ },
335
+ ]),
336
+ );
337
+
338
+ expect(manifest.metadata.extensions[0]?.sources).toEqual([
339
+ ManifestExtensionSource.Workshop,
340
+ ]);
341
+ expect(JSON.stringify(buildJsonSchema())).toContain('"workshop"');
342
+ });
325
343
  });
326
344
 
327
345
  describe("port presentation", () => {
@@ -38,7 +38,11 @@ import type {
38
38
  BridgeCatalogPage,
39
39
  BridgeCatalogProvider,
40
40
  BridgeCatalogQuery,
41
+ BridgeCatalogRelease,
42
+ BridgeCatalogUpdate,
41
43
  } from "../protocol/bridgeCatalog";
44
+ import type { BridgeCatalogSource } from "../protocol/bridgeCatalogSource";
45
+ import { BridgeCatalogSourceKind } from "../protocol/bridgeCatalogSource";
42
46
  import type { BridgeCrashReport } from "../protocol/bridgeCrash";
43
47
  import type { BridgePendingFile } from "../protocol/bridgeFrame";
44
48
  import type { BridgePreview } from "../protocol/bridgePreview";
@@ -57,7 +61,6 @@ import type {
57
61
  BridgeActionsModule,
58
62
  BridgeAnnounceModule,
59
63
  BridgeBackupModule,
60
- BridgeCatalogModule,
61
64
  BridgeCollectionModule,
62
65
  BridgeDetailAction,
63
66
  BridgeDetailModule,
@@ -71,9 +74,11 @@ import type {
71
74
  BridgeQueryModule,
72
75
  BridgeRowAction,
73
76
  BridgeSample,
77
+ BridgeSearchCatalogModule,
74
78
  BridgeServerAction,
75
79
  BridgeSettingsModule,
76
80
  BridgeSetupModule,
81
+ BridgeSourcedCatalogModule,
77
82
  BridgeTerminalModule,
78
83
  } from "./bridgeModule";
79
84
 
@@ -85,13 +90,18 @@ export namespace Bridge {
85
90
  export type Announce = BridgeAnnounceModule;
86
91
  export type Availability = BridgeAvailability;
87
92
  export type Backup = BridgeBackupModule;
88
- export type Catalog = BridgeCatalogModule;
93
+ export type Catalog = BridgeSearchCatalogModule;
89
94
  export type CatalogEntry = BridgeCatalogEntry;
90
95
  export type CatalogFacet = BridgeCatalogFacet;
91
96
  export type CatalogHit = BridgeCatalogHit;
92
97
  export type CatalogPage = BridgeCatalogPage;
93
98
  export type CatalogProvider = BridgeCatalogProvider;
94
99
  export type CatalogQuery = BridgeCatalogQuery;
100
+ export type CatalogRelease = BridgeCatalogRelease;
101
+ export type CatalogSource = BridgeCatalogSource;
102
+ export const CatalogSourceKind = BridgeCatalogSourceKind;
103
+ export type CatalogSourceKind = BridgeCatalogSourceKind;
104
+ export type CatalogUpdate = BridgeCatalogUpdate;
95
105
  export type Cell = BridgeCell;
96
106
  export type Cells = BridgeCells;
97
107
  export type Collection<Row extends BridgeRow = BridgeRow> = BridgeCollectionModule<Row>;
@@ -140,6 +150,7 @@ export namespace Bridge {
140
150
  export type SetupStep = BridgeSetupStep;
141
151
  export type SetupStepStatus = BridgeSetupStepStatus;
142
152
  export type SetupSummary = BridgeSetupSummary;
153
+ export type SourcedCatalog = BridgeSourcedCatalogModule;
143
154
  export type Terminal = BridgeTerminalModule;
144
155
  export type TerminalArg = BridgeTerminalArg;
145
156
  export type TerminalCommand = BridgeTerminalCommand;
@@ -15,6 +15,7 @@ import type {
15
15
  BridgeCatalogQuery,
16
16
  BridgeCatalogRelease,
17
17
  } from "../protocol/bridgeCatalog";
18
+ import type { BridgeCatalogSource } from "../protocol/bridgeCatalogSource";
18
19
  import type { BridgeCrashReport } from "../protocol/bridgeCrash";
19
20
  import type { BridgeBackupSnapshot, BridgeDescribeResult, BridgePendingFile } from "../protocol/bridgeFrame";
20
21
  import type { BridgePreview } from "../protocol/bridgePreview";
@@ -83,6 +84,7 @@ export interface BridgeSetupModule {
83
84
  export interface BridgeSample {
84
85
  online: number | null;
85
86
  max: number | null;
87
+ tick?: number | null;
86
88
  }
87
89
 
88
90
  export interface BridgeQueryModule {
@@ -170,20 +172,34 @@ export interface BridgeDetailModule {
170
172
  actions?: Record<string, BridgeDetailAction>;
171
173
  }
172
174
 
173
- export interface BridgeCatalogModule {
175
+ interface BridgeCatalogModuleBase {
174
176
  kind: BridgeKind.Catalog;
175
177
  protectedActions?: string[];
176
178
  pageSize?: number;
177
- search(context: BridgeContext, query: BridgeCatalogQuery): Promise<BridgeCatalogPage>;
178
179
  installed(context: BridgeContext): Promise<BridgeCatalogEntry[]>;
179
- releases?(context: BridgeContext, id: string): Promise<BridgeCatalogRelease[]>;
180
- preview?(context: BridgeContext, id: string, releaseId?: string): Promise<BridgePreview>;
181
180
  install(context: BridgeContext, id: string, releaseId?: string): Promise<BridgeCatalogEntry>;
181
+ add?(context: BridgeContext, input: string): Promise<BridgeCatalogEntry>;
182
182
  remove(context: BridgeContext, id: string): Promise<void>;
183
183
  toggle?(context: BridgeContext, id: string, enabled: boolean): Promise<void>;
184
184
  unavailable?(context: BridgeContext, entry: BridgeCatalogEntry): Promise<BridgeUnavailable>;
185
185
  }
186
186
 
187
+ export interface BridgeSearchCatalogModule extends BridgeCatalogModuleBase {
188
+ source?: never;
189
+ search(context: BridgeContext, query: BridgeCatalogQuery): Promise<BridgeCatalogPage>;
190
+ releases?(context: BridgeContext, id: string): Promise<BridgeCatalogRelease[]>;
191
+ preview?(context: BridgeContext, id: string, releaseId?: string): Promise<BridgePreview>;
192
+ }
193
+
194
+ export interface BridgeSourcedCatalogModule extends BridgeCatalogModuleBase {
195
+ source: BridgeCatalogSource;
196
+ search?: never;
197
+ releases?: never;
198
+ preview?: never;
199
+ }
200
+
201
+ export type BridgeCatalogModule = BridgeSearchCatalogModule | BridgeSourcedCatalogModule;
202
+
187
203
  export type BridgePanelModule =
188
204
  | BridgeActionsModule
189
205
  | BridgeCatalogModule
@@ -29,6 +29,7 @@ export interface BridgeField {
29
29
  help?: LocalizedText;
30
30
  warning?: LocalizedText;
31
31
  placeholder?: string;
32
+ default?: string;
32
33
  min?: number;
33
34
  max?: number;
34
35
  step?: number;
@@ -149,6 +150,7 @@ export interface BridgeCatalogSection {
149
150
  variable?: string;
150
151
  confirm?: BridgeConfirm;
151
152
  confirmText?: LocalizedText;
153
+ upload?: BridgeSectionUpload;
152
154
  empty?: LocalizedText;
153
155
  visibleWhen?: BridgeVisibleWhen;
154
156
  access?: BridgeAccess;
@@ -39,6 +39,11 @@ export interface BridgeCatalogPage {
39
39
  sorts: BridgeCatalogFacet[];
40
40
  }
41
41
 
42
+ export interface BridgeCatalogUpdate {
43
+ version: string;
44
+ releaseId: string;
45
+ }
46
+
42
47
  export interface BridgeCatalogEntry {
43
48
  id: string;
44
49
  provider: string | null;
@@ -52,6 +57,8 @@ export interface BridgeCatalogEntry {
52
57
  pageUrl: string | null;
53
58
  icon: string | null;
54
59
  variables?: Record<string, string> | null;
60
+ update?: BridgeCatalogUpdate | null;
61
+ note?: LocalizedText;
55
62
  }
56
63
 
57
64
  export interface BridgeCatalogQuery {
@@ -0,0 +1,9 @@
1
+ export enum BridgeCatalogSourceKind {
2
+ Workshop = "workshop",
3
+ }
4
+
5
+ export interface BridgeCatalogSource {
6
+ kind: BridgeCatalogSourceKind;
7
+ appId: number;
8
+ provided?: string[];
9
+ }
@@ -6,6 +6,7 @@ export interface BridgeConfig {
6
6
  ports: Record<string, number>;
7
7
  portCounts?: Record<string, number>;
8
8
  publicAddress?: string;
9
+ playerAddress?: string | null;
9
10
  memoryMb: number;
10
11
  cpuPercent: number;
11
12
  gameUid: number;
@@ -98,6 +98,7 @@ export interface BridgeStatus {
98
98
  export interface BridgeSampleResult {
99
99
  online: number | null;
100
100
  max: number | null;
101
+ tick?: number | null;
101
102
  }
102
103
 
103
104
  export interface BridgeAnnounceResult {
@@ -28,6 +28,8 @@ export const BRIDGE_FORM_CLEARS_LIMIT = 8;
28
28
 
29
29
  export const BRIDGE_UPLOAD_EXTENSIONS_LIMIT = 8;
30
30
 
31
+ export const BRIDGE_CATALOG_PROVIDED_LIMIT = 16;
32
+
31
33
  export const BRIDGE_MODULES_LIMIT = 32;
32
34
 
33
35
  export const BRIDGE_MODULE_ACTIONS_LIMIT = 16;
@@ -52,10 +54,14 @@ export const BRIDGE_OPTION_VALUE_LENGTH_LIMIT = 128;
52
54
 
53
55
  export const BRIDGE_PLACEHOLDER_LENGTH_LIMIT = 256;
54
56
 
57
+ export const BRIDGE_DEFAULT_LENGTH_LIMIT = 256;
58
+
55
59
  export const BRIDGE_STAGING_LENGTH_LIMIT = 128;
56
60
 
57
61
  export const BRIDGE_EXTENSION_LENGTH_LIMIT = 16;
58
62
 
63
+ export const BRIDGE_PROVIDED_NAME_LENGTH_LIMIT = 128;
64
+
59
65
  export const BRIDGE_PATTERN_LENGTH_LIMIT = 256;
60
66
 
61
67
  export const BRIDGE_SETUP_CODE_LENGTH_LIMIT = 64;
@@ -1,6 +1,7 @@
1
1
  import type { BridgeKind, BridgeStream } from "../core";
2
2
  import type { BridgePanel } from "../panel";
3
3
  import type { BridgeTerminalCommand, BridgeTerminalLevel } from "../terminal";
4
+ import type { BridgeCatalogSource } from "./bridgeCatalogSource";
4
5
  import type { BridgeSetupManifest } from "./bridgeSetup";
5
6
 
6
7
  export interface BridgeTerminalRuleManifest {
@@ -25,6 +26,7 @@ export interface BridgeModuleManifest {
25
26
  dependsOn: string[];
26
27
  actions: string[];
27
28
  protectedActions?: string[];
29
+ source?: BridgeCatalogSource;
28
30
  }
29
31
 
30
32
  export interface BridgeManifest {
@@ -3,6 +3,7 @@ export * from "../panel";
3
3
  export * from "../terminal";
4
4
  export * from "./bridgeActivity";
5
5
  export * from "./bridgeCatalog";
6
+ export * from "./bridgeCatalogSource";
6
7
  export * from "./bridgeConfig";
7
8
  export * from "./bridgeCrash";
8
9
  export * from "./bridgeEgress";
@@ -66,13 +66,15 @@ export const actionsOf = (module: BridgePanelModule): string[] => {
66
66
  }
67
67
 
68
68
  case BridgeKind.Catalog: {
69
+ const sourced = module.source !== undefined;
70
+
69
71
  return [
70
- ...(module.releases
72
+ ...(module.releases && !sourced
71
73
  ? [
72
74
  BridgeAction.Releases,
73
75
  ]
74
76
  : []),
75
- ...(module.preview
77
+ ...(module.preview || sourced
76
78
  ? [
77
79
  BridgeAction.Preview,
78
80
  ]
@@ -81,6 +83,11 @@ export const actionsOf = (module: BridgePanelModule): string[] => {
81
83
  BridgeAction.Installed,
82
84
  BridgeAction.Remove,
83
85
  BridgeAction.Search,
86
+ ...(module.add
87
+ ? [
88
+ BridgeAction.Add,
89
+ ]
90
+ : []),
84
91
  ...(module.toggle
85
92
  ? [
86
93
  BridgeAction.Toggle,
@@ -68,6 +68,37 @@ describe("parseConfig", () => {
68
68
  });
69
69
  });
70
70
 
71
+ it("reads a file an older agent wrote, with no player address", () => {
72
+ expect(parseConfig(written()).playerAddress).toBeUndefined();
73
+ });
74
+
75
+ it("reads the player address when the agent wrote it", () => {
76
+ expect(
77
+ parseConfig(
78
+ written({
79
+ playerAddress: "lobby.srvk.gg",
80
+ }),
81
+ ).playerAddress,
82
+ ).toBe("lobby.srvk.gg");
83
+ });
84
+
85
+ it("ignores a malformed or empty player address", () => {
86
+ expect(
87
+ parseConfig(
88
+ written({
89
+ playerAddress: 25_565,
90
+ }),
91
+ ).playerAddress,
92
+ ).toBeUndefined();
93
+ expect(
94
+ parseConfig(
95
+ written({
96
+ playerAddress: "",
97
+ }),
98
+ ).playerAddress,
99
+ ).toBeUndefined();
100
+ });
101
+
71
102
  it("still refuses a game that would run as root", () => {
72
103
  expect(() =>
73
104
  parseConfig(
@@ -117,6 +117,7 @@ export const parseConfig = (text: string): BridgeConfig => {
117
117
  ports: numberRecord(source, "ports"),
118
118
  portCounts: optionalNumberRecord(source, "portCounts"),
119
119
  publicAddress: optionalString(source, "publicAddress"),
120
+ playerAddress: optionalString(source, "playerAddress"),
120
121
  memoryMb: requireNumber(source, "memoryMb"),
121
122
  cpuPercent: requireNumber(source, "cpuPercent"),
122
123
  gameUid: requireNumber(source, "gameUid"),
@@ -250,4 +250,57 @@ describe("the context's ports and address", () => {
250
250
 
251
251
  expect(context.server.publicAddress).toBe("198.51.100.4");
252
252
  });
253
+
254
+ it("hand a driver the address players type", async () => {
255
+ const boot = configWith({
256
+ publicAddress: "203.0.113.7",
257
+ playerAddress: "lobby.srvk.gg",
258
+ });
259
+
260
+ await writeFile(path, JSON.stringify(boot));
261
+
262
+ const { context } = contextOf(boot);
263
+
264
+ expect(context.server.playerAddress).toBe("lobby.srvk.gg");
265
+ expect(context.server.publicAddress).toBe("203.0.113.7");
266
+ });
267
+
268
+ it("treat the player address as unknown for a file an older agent wrote", async () => {
269
+ const boot = configWith({});
270
+
271
+ await writeFile(path, JSON.stringify(boot));
272
+
273
+ const { context } = contextOf(boot);
274
+
275
+ expect(context.server.playerAddress).toBeNull();
276
+ });
277
+
278
+ it("follow a rename and a clear of the player address", async () => {
279
+ const boot = configWith({
280
+ playerAddress: "lobby.srvk.gg",
281
+ });
282
+
283
+ await writeFile(path, JSON.stringify(boot));
284
+
285
+ const { context, live } = contextOf(boot);
286
+
287
+ await writeFile(
288
+ path,
289
+ JSON.stringify(
290
+ configWith({
291
+ playerAddress: "arena.srvk.gg",
292
+ }),
293
+ ),
294
+ );
295
+
296
+ live.reload();
297
+
298
+ expect(context.server.playerAddress).toBe("arena.srvk.gg");
299
+
300
+ await writeFile(path, JSON.stringify(configWith({})));
301
+
302
+ live.reload();
303
+
304
+ expect(context.server.playerAddress).toBeNull();
305
+ });
253
306
  });
@@ -64,6 +64,9 @@ export const createContext = (
64
64
  get publicAddress() {
65
65
  return live.publicAddress();
66
66
  },
67
+ get playerAddress() {
68
+ return live.playerAddress();
69
+ },
67
70
  },
68
71
 
69
72
  variable(key) {