@serverkgg/bridge 0.0.14 → 0.0.16
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/guides/variables.ts +1 -0
- package/src/panel/bridgePanel.ts +11 -0
- package/src/panel/bridgePlace.ts +4 -0
- package/src/panel/index.ts +1 -0
- package/src/protocol/bridgeCrash.ts +83 -0
- package/src/protocol/bridgeFrame.ts +1 -0
- package/src/protocol/index.ts +1 -0
- package/src/rcon/rconAccess.test.ts +20 -2
- package/src/rcon/rconAccess.ts +9 -1
- package/src/runtime/bridgeDriver.test.ts +145 -0
- package/src/runtime/bridgeDriver.ts +44 -2
- package/src/runtime/bridgeEvents.test.ts +411 -9
- package/src/runtime/bridgeEvents.ts +53 -3
- package/src/runtime/bridgeRegistry.test.ts +96 -0
- package/src/runtime/bridgeRegistry.ts +33 -1
- package/src/runtime/bridgeServer.test.ts +1 -0
- package/src/runtime/bridgeSupervisor.test.ts +289 -0
- package/src/runtime/bridgeSupervisor.ts +166 -9
- package/src/runtime/serve.ts +10 -0
- 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
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import type { BridgeContext } from "../context";
|
|
2
|
+
import { playerSummaries, steamWebApiReady } from "./webApi";
|
|
3
|
+
|
|
4
|
+
export const STEAM_AVATAR_CACHE_TTL_MS = 21_600_000;
|
|
5
|
+
|
|
6
|
+
const STEAM_AVATAR = /\/([0-9a-f]{40})(?:_[a-z]+)?\.jpg/;
|
|
7
|
+
|
|
8
|
+
export const steamAvatarHashOf = (url: string): string | null => {
|
|
9
|
+
return url.match(STEAM_AVATAR)?.at(1) ?? null;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export interface SteamAvatars {
|
|
13
|
+
resolve(context: BridgeContext, ids: string[]): Promise<Map<string, string>>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface SteamAvatarsOptions {
|
|
17
|
+
steamIdOf: (id: string) => string | null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface CachedAvatar {
|
|
21
|
+
hash: string;
|
|
22
|
+
storedAt: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const createSteamAvatars = (options: SteamAvatarsOptions): SteamAvatars => {
|
|
26
|
+
const { steamIdOf } = options;
|
|
27
|
+
const cached = new Map<string, CachedAvatar>();
|
|
28
|
+
|
|
29
|
+
let warned = false;
|
|
30
|
+
|
|
31
|
+
const hashOf = (steamId: string, now: number) => {
|
|
32
|
+
const entry = cached.get(steamId);
|
|
33
|
+
|
|
34
|
+
if (!entry) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (now - entry.storedAt >= STEAM_AVATAR_CACHE_TTL_MS) {
|
|
39
|
+
cached.delete(steamId);
|
|
40
|
+
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return entry.hash;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const known = (ids: string[], now: number) => {
|
|
48
|
+
const hashes = new Map<string, string>();
|
|
49
|
+
|
|
50
|
+
for (const id of ids) {
|
|
51
|
+
const steamId = steamIdOf(id);
|
|
52
|
+
const hash = steamId === null ? null : hashOf(steamId, now);
|
|
53
|
+
|
|
54
|
+
if (hash !== null) {
|
|
55
|
+
hashes.set(id, hash);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return hashes;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
async resolve(context, ids) {
|
|
64
|
+
const now = Date.now();
|
|
65
|
+
const missing = new Set(
|
|
66
|
+
ids.flatMap((id) => {
|
|
67
|
+
const steamId = steamIdOf(id);
|
|
68
|
+
|
|
69
|
+
return steamId === null || hashOf(steamId, now) !== null
|
|
70
|
+
? []
|
|
71
|
+
: [
|
|
72
|
+
steamId,
|
|
73
|
+
];
|
|
74
|
+
}),
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
if (missing.size === 0 || !steamWebApiReady(context)) {
|
|
78
|
+
return known(ids, now);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
for (const summary of await playerSummaries(context, [
|
|
83
|
+
...missing,
|
|
84
|
+
])) {
|
|
85
|
+
const hash = steamAvatarHashOf(summary.avatarfull);
|
|
86
|
+
|
|
87
|
+
if (hash !== null) {
|
|
88
|
+
cached.set(summary.steamid, {
|
|
89
|
+
hash,
|
|
90
|
+
storedAt: now,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
} catch (error) {
|
|
95
|
+
if (!warned) {
|
|
96
|
+
warned = true;
|
|
97
|
+
|
|
98
|
+
context.log.warn("the steam web api did not answer for the roster", {
|
|
99
|
+
error: error instanceof Error ? error.message : String(error),
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return known(ids, now);
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
};
|
package/src/steam/index.ts
CHANGED
package/src/wire/panel.test.ts
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
BridgeKind,
|
|
11
11
|
BridgeLayout,
|
|
12
12
|
type BridgeManifest,
|
|
13
|
+
BridgePlace,
|
|
13
14
|
type BridgeSetupManifest,
|
|
14
15
|
BridgeSetupStepKind,
|
|
15
16
|
BridgeUploadMode,
|
|
@@ -604,3 +605,81 @@ describe("the setup mirror", () => {
|
|
|
604
605
|
expect(bridgeManifestSchema.safeParse(source).success).toBe(false);
|
|
605
606
|
});
|
|
606
607
|
});
|
|
608
|
+
|
|
609
|
+
const placedManifest = (): BridgeManifest => ({
|
|
610
|
+
version: "1.0.0",
|
|
611
|
+
panel: {
|
|
612
|
+
tabs: [
|
|
613
|
+
{
|
|
614
|
+
id: "roster",
|
|
615
|
+
title: text("Roster"),
|
|
616
|
+
sections: [
|
|
617
|
+
{
|
|
618
|
+
layout: BridgeLayout.Table,
|
|
619
|
+
id: "online",
|
|
620
|
+
module: "players",
|
|
621
|
+
place: BridgePlace.Players,
|
|
622
|
+
columns: [
|
|
623
|
+
{
|
|
624
|
+
key: "name",
|
|
625
|
+
label: text("Name"),
|
|
626
|
+
},
|
|
627
|
+
],
|
|
628
|
+
actions: [
|
|
629
|
+
{
|
|
630
|
+
id: "ban",
|
|
631
|
+
label: text("Ban"),
|
|
632
|
+
confirm: BridgeConfirm.Strong,
|
|
633
|
+
offline: true,
|
|
634
|
+
},
|
|
635
|
+
],
|
|
636
|
+
},
|
|
637
|
+
],
|
|
638
|
+
},
|
|
639
|
+
{
|
|
640
|
+
id: "settings",
|
|
641
|
+
title: text("Settings"),
|
|
642
|
+
sections: [
|
|
643
|
+
{
|
|
644
|
+
layout: BridgeLayout.Form,
|
|
645
|
+
id: "world",
|
|
646
|
+
title: text("World"),
|
|
647
|
+
help: text("These settings apply the next time the server starts"),
|
|
648
|
+
target: BridgeFormTarget.Variables,
|
|
649
|
+
fields: [
|
|
650
|
+
{
|
|
651
|
+
key: "WORLD_NAME",
|
|
652
|
+
control: BridgeControl.Text,
|
|
653
|
+
label: text("World name"),
|
|
654
|
+
},
|
|
655
|
+
],
|
|
656
|
+
},
|
|
657
|
+
],
|
|
658
|
+
},
|
|
659
|
+
],
|
|
660
|
+
},
|
|
661
|
+
terminal: null,
|
|
662
|
+
setup: null,
|
|
663
|
+
modules: [
|
|
664
|
+
{
|
|
665
|
+
id: "players",
|
|
666
|
+
kind: BridgeKind.Collection,
|
|
667
|
+
requiresRunning: true,
|
|
668
|
+
refreshSeconds: 20,
|
|
669
|
+
pageSize: null,
|
|
670
|
+
dependsOn: [],
|
|
671
|
+
actions: [
|
|
672
|
+
"ban",
|
|
673
|
+
],
|
|
674
|
+
},
|
|
675
|
+
],
|
|
676
|
+
events: [],
|
|
677
|
+
});
|
|
678
|
+
|
|
679
|
+
describe("the placement mirror", () => {
|
|
680
|
+
test("carries place, an offline row action and a form help through unchanged", () => {
|
|
681
|
+
const source = placedManifest();
|
|
682
|
+
|
|
683
|
+
expect(bridgeManifestSchema.parse(source)).toEqual(source);
|
|
684
|
+
});
|
|
685
|
+
});
|
package/src/wire/panel.ts
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
BridgeLayout,
|
|
13
13
|
type BridgeManifest,
|
|
14
14
|
type BridgePanel,
|
|
15
|
+
BridgePlace,
|
|
15
16
|
type BridgeSection,
|
|
16
17
|
type BridgeSetupManifest,
|
|
17
18
|
type BridgeSetupStep,
|
|
@@ -118,6 +119,7 @@ export const bridgeSectionActionSchema = z.object({
|
|
|
118
119
|
confirm: z.enum(BridgeConfirm).optional(),
|
|
119
120
|
confirmText: panelTextSchema.optional(),
|
|
120
121
|
download: z.boolean().optional(),
|
|
122
|
+
offline: z.boolean().optional(),
|
|
121
123
|
fields: z.array(bridgeFieldSchema).max(ACTION_FIELDS_LIMIT).optional(),
|
|
122
124
|
});
|
|
123
125
|
|
|
@@ -148,6 +150,7 @@ export const bridgeActionsSectionSchema = z.object({
|
|
|
148
150
|
id: zBridgeId,
|
|
149
151
|
title: panelTextSchema.optional(),
|
|
150
152
|
help: panelTextSchema.optional(),
|
|
153
|
+
place: z.enum(BridgePlace).optional(),
|
|
151
154
|
module: zBridgeModuleId,
|
|
152
155
|
actions: z.array(bridgeSectionActionSchema).min(1).max(SECTION_ACTIONS_LIMIT),
|
|
153
156
|
visibleWhen: bridgeVisibleWhenSchema.optional(),
|
|
@@ -158,6 +161,7 @@ export const bridgeFormSectionSchema = z.object({
|
|
|
158
161
|
layout: z.literal(BridgeLayout.Form),
|
|
159
162
|
id: zBridgeId,
|
|
160
163
|
title: panelTextSchema.optional(),
|
|
164
|
+
help: panelTextSchema.optional(),
|
|
161
165
|
target: z.enum(BridgeFormTarget),
|
|
162
166
|
module: zBridgeModuleId.optional(),
|
|
163
167
|
preview: bridgePreviewRefSchema.optional(),
|
|
@@ -175,6 +179,8 @@ export const bridgeDetailSectionSchema = z.object({
|
|
|
175
179
|
layout: z.literal(BridgeLayout.Detail),
|
|
176
180
|
id: zBridgeId,
|
|
177
181
|
title: panelTextSchema.optional(),
|
|
182
|
+
help: panelTextSchema.optional(),
|
|
183
|
+
place: z.enum(BridgePlace).optional(),
|
|
178
184
|
module: zBridgeModuleId,
|
|
179
185
|
actions: z.array(bridgeSectionActionSchema).max(SECTION_ACTIONS_LIMIT).optional(),
|
|
180
186
|
reinstall: z.boolean().optional(),
|
|
@@ -196,6 +202,8 @@ export const bridgeTableSectionSchema = z.object({
|
|
|
196
202
|
layout: z.literal(BridgeLayout.Table),
|
|
197
203
|
id: zBridgeId,
|
|
198
204
|
title: panelTextSchema.optional(),
|
|
205
|
+
help: panelTextSchema.optional(),
|
|
206
|
+
place: z.enum(BridgePlace).optional(),
|
|
199
207
|
module: zBridgeModuleId,
|
|
200
208
|
restartHint: z.boolean().optional(),
|
|
201
209
|
columns: z.array(bridgeColumnSchema).min(1).max(8),
|
|
@@ -211,6 +219,8 @@ export const bridgeCardsSectionSchema = z.object({
|
|
|
211
219
|
layout: z.literal(BridgeLayout.Cards),
|
|
212
220
|
id: zBridgeId,
|
|
213
221
|
title: panelTextSchema.optional(),
|
|
222
|
+
help: panelTextSchema.optional(),
|
|
223
|
+
place: z.enum(BridgePlace).optional(),
|
|
214
224
|
module: zBridgeModuleId,
|
|
215
225
|
titleKey: zKey,
|
|
216
226
|
subtitleKey: zKey.optional(),
|
|
@@ -224,6 +234,7 @@ export const bridgeCatalogSectionSchema = z.object({
|
|
|
224
234
|
layout: z.literal(BridgeLayout.Catalog),
|
|
225
235
|
id: zBridgeId,
|
|
226
236
|
title: panelTextSchema.optional(),
|
|
237
|
+
help: panelTextSchema.optional(),
|
|
227
238
|
module: zBridgeModuleId,
|
|
228
239
|
restartHint: z.boolean().optional(),
|
|
229
240
|
reinstall: z.boolean().optional(),
|