@serverkgg/bridge 0.1.1 → 0.1.3
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 +3 -2
- package/rules/package.md +60 -0
- package/src/blog/articleFrontmatter.test.ts +10 -10
- package/src/blog/compile.test.ts +9 -9
- package/src/bot/botDeploy.test.ts +16 -16
- package/src/bot/botEnvironment.test.ts +8 -8
- package/src/bot/botIdentity.test.ts +12 -12
- package/src/bot/botPanel.test.ts +41 -11
- package/src/bot/botPanel.ts +13 -5
- package/src/bot/botState.test.ts +3 -3
- package/src/bot/botTerminal.test.ts +5 -5
- package/src/bot/botVariables.test.ts +8 -8
- package/src/bot/detect/botToken.test.ts +6 -6
- package/src/bot/detect/detect.test.ts +21 -21
- package/src/bot/detect/detectTable.test.ts +5 -5
- package/src/bot/detect/envExample.test.ts +2 -2
- package/src/bot/detect/envScan.test.ts +4 -4
- package/src/bot/detect/inferBotPlatform.test.ts +4 -4
- package/src/bot/detect/keys.test.ts +3 -3
- package/src/bot/detect/sensitiveKey.test.ts +5 -5
- package/src/bot/detect/serverkJson.test.ts +5 -5
- package/src/catalogs/curseforge.test.ts +35 -35
- package/src/catalogs/thunderstore.test.ts +9 -9
- package/src/cli/detectTable.test.ts +3 -3
- package/src/cli/link.test.ts +11 -11
- package/src/cli/main.ts +8 -0
- package/src/cli/rules.test.ts +198 -0
- package/src/cli/rules.ts +188 -0
- package/src/cli/validate.test.ts +123 -29
- package/src/cli/validate.ts +37 -1
- package/src/codecs/ini.test.ts +13 -13
- package/src/codecs/javaProperties.test.ts +11 -11
- package/src/codecs/sourceCfg.test.ts +14 -14
- package/src/codecs/xmlProperties.test.ts +10 -10
- package/src/context/bridgeContext.ts +6 -1
- package/src/core/bridgeAction.ts +2 -0
- package/src/core/bridgeActivity.ts +9 -0
- package/src/core/bridgeAvailability.ts +10 -0
- package/src/core/bridgePort.test.ts +6 -6
- package/src/core/bridgeUserError.test.ts +13 -3
- package/src/core/bridgeUserError.ts +6 -2
- package/src/core/bridgeValue.test.ts +5 -5
- package/src/core/describeDownloadFailure.test.ts +58 -0
- package/src/core/describeDownloadFailure.ts +69 -0
- package/src/core/index.ts +3 -0
- package/src/guides/markdown.test.ts +31 -31
- package/src/install/installStamp.test.ts +9 -9
- package/src/manifest/extension.ts +36 -0
- package/src/manifest/index.ts +1 -0
- package/src/manifest/manifest.test.ts +208 -90
- package/src/manifest/manifest.ts +34 -0
- package/src/modules/bridge.ts +8 -1
- package/src/modules/bridgeModule.ts +13 -1
- package/src/net/loopback.test.ts +10 -10
- package/src/panel/bridgeDetail.ts +2 -1
- package/src/presence/rosterSync.test.ts +7 -7
- package/src/probes/a2s.test.ts +5 -5
- package/src/probes/minecraftPing.test.ts +17 -17
- package/src/probes/raknetPing.test.ts +13 -13
- package/src/protocol/bridgeActivity.ts +54 -0
- package/src/protocol/bridgeEvents.ts +30 -0
- package/src/protocol/bridgeFrame.ts +10 -1
- package/src/protocol/index.ts +1 -0
- package/src/rcon/rconAccess.test.ts +20 -20
- package/src/rcon/rconAccess.ts +13 -13
- package/src/rcon/rconClient.test.ts +49 -49
- package/src/rcon/rconFrame.test.ts +14 -14
- package/src/rcon/rconIds.test.ts +4 -4
- package/src/rcon/rconWriter.test.ts +6 -6
- package/src/runtime/bridgeActions.ts +19 -2
- package/src/runtime/bridgeActivity.test.ts +318 -0
- package/src/runtime/bridgeActivity.ts +268 -0
- package/src/runtime/bridgeBackupLease.test.ts +9 -9
- package/src/runtime/bridgeConfigReader.test.ts +5 -5
- package/src/runtime/bridgeConsoleHandler.test.ts +8 -8
- package/src/runtime/bridgeContext.test.ts +11 -5
- package/src/runtime/bridgeContext.ts +8 -1
- package/src/runtime/bridgeDriver.test.ts +114 -93
- package/src/runtime/bridgeDriver.ts +10 -7
- package/src/runtime/bridgeEvents.test.ts +15 -15
- package/src/runtime/bridgeFiles.test.ts +54 -54
- package/src/runtime/bridgeInstall.test.ts +10 -10
- package/src/runtime/bridgeLiveConfig.test.ts +6 -6
- package/src/runtime/bridgeLog.ts +10 -0
- package/src/runtime/bridgePending.test.ts +6 -6
- package/src/runtime/bridgeRedaction.test.ts +16 -16
- package/src/runtime/bridgeRefusalError.ts +13 -0
- package/src/runtime/bridgeRegistry.test.ts +1077 -34
- package/src/runtime/bridgeRegistry.ts +464 -65
- package/src/runtime/bridgeScopedContext.ts +55 -0
- package/src/runtime/bridgeServer.test.ts +424 -24
- package/src/runtime/bridgeServer.ts +125 -35
- package/src/runtime/bridgeSetup.test.ts +27 -27
- package/src/runtime/bridgeSetup.ts +1 -1
- package/src/runtime/bridgeSetupHandlers.test.ts +80 -17
- package/src/runtime/bridgeSetupHandlers.ts +45 -10
- package/src/runtime/bridgeSignal.test.ts +7 -7
- package/src/runtime/bridgeSupervisor.test.ts +42 -42
- package/src/runtime/serve.ts +45 -3
- package/src/steam/avatars.test.ts +13 -13
- package/src/steam/steamApp.test.ts +8 -8
- package/src/steam/steamcmd.test.ts +44 -13
- package/src/steam/steamcmd.ts +27 -4
- package/src/steam/steamcmdNarrator.test.ts +4 -4
- package/src/steam/workshop.test.ts +5 -5
- package/src/utils/changedSettingKeys.test.ts +65 -0
- package/src/utils/changedSettingKeys.ts +6 -0
- package/src/utils/describeExecFailure.test.ts +34 -0
- package/src/utils/describeExecFailure.ts +9 -0
- package/src/utils/index.ts +3 -0
- package/src/utils/sameSettingValue.test.ts +35 -0
- package/src/utils/sameSettingValue.ts +40 -0
- package/src/utils/token.test.ts +7 -7
- package/src/wine/protonCommand.test.ts +5 -5
- package/src/wine/wineCommand.test.ts +6 -6
- package/src/wine/winePrefix.test.ts +6 -6
- package/src/wire/activity.test.ts +95 -0
- package/src/wire/activity.ts +41 -0
- package/src/wire/index.ts +1 -0
- package/src/wire/panel.test.ts +91 -25
- package/src/wire/panel.ts +11 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { describe, expect,
|
|
2
|
-
import { steamcmdArgv, steamcmdScript, steamcmdUpdateArgs } from "./steamcmd";
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { steamcmdArgv, steamcmdFailureText, steamcmdScript, steamcmdUpdateArgs } from "./steamcmd";
|
|
3
3
|
import { SteamcmdLoginKind, steamcmdLoginArgs } from "./steamcmdLogin";
|
|
4
4
|
import { SteamcmdPlatform } from "./steamcmdPlatform";
|
|
5
5
|
|
|
@@ -9,13 +9,13 @@ const OPTIONS = {
|
|
|
9
9
|
};
|
|
10
10
|
|
|
11
11
|
describe("steamcmdLoginArgs", () => {
|
|
12
|
-
|
|
12
|
+
it("logs in anonymously by default", () => {
|
|
13
13
|
expect(steamcmdLoginArgs()).toEqual([
|
|
14
14
|
"anonymous",
|
|
15
15
|
]);
|
|
16
16
|
});
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
it("passes the account credentials through", () => {
|
|
19
19
|
expect(
|
|
20
20
|
steamcmdLoginArgs({
|
|
21
21
|
kind: SteamcmdLoginKind.Account,
|
|
@@ -30,14 +30,14 @@ describe("steamcmdLoginArgs", () => {
|
|
|
30
30
|
});
|
|
31
31
|
|
|
32
32
|
describe("steamcmdScript", () => {
|
|
33
|
-
|
|
33
|
+
it("lives inside the copied steamcmd directory", () => {
|
|
34
34
|
expect(steamcmdScript()).toBe(".steamcmd/steamcmd.sh");
|
|
35
35
|
expect(steamcmdScript(".sc")).toBe(".sc/steamcmd.sh");
|
|
36
36
|
});
|
|
37
37
|
});
|
|
38
38
|
|
|
39
39
|
describe("steamcmdArgv", () => {
|
|
40
|
-
|
|
40
|
+
it("forces the install directory and logs in before the command", () => {
|
|
41
41
|
expect(
|
|
42
42
|
steamcmdArgv(OPTIONS, "/home/container", [
|
|
43
43
|
"+app_update",
|
|
@@ -55,7 +55,7 @@ describe("steamcmdArgv", () => {
|
|
|
55
55
|
]);
|
|
56
56
|
});
|
|
57
57
|
|
|
58
|
-
|
|
58
|
+
it("forces the windows platform before the login", () => {
|
|
59
59
|
const argv = steamcmdArgv(
|
|
60
60
|
{
|
|
61
61
|
...OPTIONS,
|
|
@@ -70,7 +70,7 @@ describe("steamcmdArgv", () => {
|
|
|
70
70
|
expect(argv.indexOf("+@sSteamCmdForcePlatformType")).toBeLessThan(argv.indexOf("+login"));
|
|
71
71
|
});
|
|
72
72
|
|
|
73
|
-
|
|
73
|
+
it("leaves the platform alone on linux", () => {
|
|
74
74
|
expect(
|
|
75
75
|
steamcmdArgv(
|
|
76
76
|
{
|
|
@@ -85,7 +85,7 @@ describe("steamcmdArgv", () => {
|
|
|
85
85
|
});
|
|
86
86
|
|
|
87
87
|
describe("steamcmdUpdateArgs", () => {
|
|
88
|
-
|
|
88
|
+
it("updates without validating by default", () => {
|
|
89
89
|
expect(
|
|
90
90
|
steamcmdUpdateArgs("258550", {
|
|
91
91
|
validate: false,
|
|
@@ -96,7 +96,7 @@ describe("steamcmdUpdateArgs", () => {
|
|
|
96
96
|
]);
|
|
97
97
|
});
|
|
98
98
|
|
|
99
|
-
|
|
99
|
+
it("adds validate last of the update flags", () => {
|
|
100
100
|
expect(
|
|
101
101
|
steamcmdUpdateArgs("258550", {
|
|
102
102
|
validate: true,
|
|
@@ -108,7 +108,7 @@ describe("steamcmdUpdateArgs", () => {
|
|
|
108
108
|
]);
|
|
109
109
|
});
|
|
110
110
|
|
|
111
|
-
|
|
111
|
+
it("passes a branch and its password", () => {
|
|
112
112
|
expect(
|
|
113
113
|
steamcmdUpdateArgs("90", {
|
|
114
114
|
validate: true,
|
|
@@ -126,7 +126,7 @@ describe("steamcmdUpdateArgs", () => {
|
|
|
126
126
|
]);
|
|
127
127
|
});
|
|
128
128
|
|
|
129
|
-
|
|
129
|
+
it("ignores a branch password without a branch", () => {
|
|
130
130
|
expect(
|
|
131
131
|
steamcmdUpdateArgs("90", {
|
|
132
132
|
validate: false,
|
|
@@ -138,7 +138,7 @@ describe("steamcmdUpdateArgs", () => {
|
|
|
138
138
|
]);
|
|
139
139
|
});
|
|
140
140
|
|
|
141
|
-
|
|
141
|
+
it("appends the extra arguments", () => {
|
|
142
142
|
expect(
|
|
143
143
|
steamcmdUpdateArgs("376030", {
|
|
144
144
|
validate: false,
|
|
@@ -155,3 +155,34 @@ describe("steamcmdUpdateArgs", () => {
|
|
|
155
155
|
]);
|
|
156
156
|
});
|
|
157
157
|
});
|
|
158
|
+
|
|
159
|
+
describe("steamcmdFailureText", () => {
|
|
160
|
+
it("says the download account does not own the app when steam answers no subscription", () => {
|
|
161
|
+
expect(
|
|
162
|
+
steamcmdFailureText(
|
|
163
|
+
{
|
|
164
|
+
stdout: "No subscription",
|
|
165
|
+
stderr: "",
|
|
166
|
+
code: 8,
|
|
167
|
+
},
|
|
168
|
+
"assetto",
|
|
169
|
+
).en,
|
|
170
|
+
).toBe("Steam refused to download assetto because the download account doesn't own it. Contact support.");
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("says the download failed for any other exit", () => {
|
|
174
|
+
expect(
|
|
175
|
+
steamcmdFailureText(
|
|
176
|
+
{
|
|
177
|
+
stdout: "",
|
|
178
|
+
stderr: "",
|
|
179
|
+
code: 8,
|
|
180
|
+
},
|
|
181
|
+
"palworld",
|
|
182
|
+
),
|
|
183
|
+
).toEqual({
|
|
184
|
+
ar: "تعذّر تنزيل palworld من Steam، جرب مرة ثانية.",
|
|
185
|
+
en: "We couldn't download palworld from Steam. Try again.",
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
});
|
package/src/steam/steamcmd.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { BridgeContext } from "../context";
|
|
1
|
+
import type { BridgeContext, BridgeExecResult } from "../context";
|
|
2
|
+
import { BridgeUserError, type LocalizedText } from "../core";
|
|
2
3
|
import { execDetail } from "../utils";
|
|
3
4
|
import { installedBuildId } from "./steamApp";
|
|
4
5
|
import { type SteamcmdLogin, SteamcmdLoginKind, steamcmdLoginArgs } from "./steamcmdLogin";
|
|
@@ -25,6 +26,8 @@ const ATTEMPTS = 3;
|
|
|
25
26
|
|
|
26
27
|
const RETRY_DELAY_MS = 5000;
|
|
27
28
|
|
|
29
|
+
const NO_SUBSCRIPTION = "No subscription";
|
|
30
|
+
|
|
28
31
|
export interface SteamcmdOptions {
|
|
29
32
|
appId: string;
|
|
30
33
|
label: string;
|
|
@@ -100,6 +103,20 @@ export const steamcmdUpdateArgs = (appId: string, update: SteamcmdUpdateOptions)
|
|
|
100
103
|
return args;
|
|
101
104
|
};
|
|
102
105
|
|
|
106
|
+
export const steamcmdFailureText = (result: BridgeExecResult, label: string): LocalizedText => {
|
|
107
|
+
if (`${result.stdout}\n${result.stderr}`.includes(NO_SUBSCRIPTION)) {
|
|
108
|
+
return {
|
|
109
|
+
ar: `رفض Steam تنزيل ${label} لأن حساب التنزيل لا يملكه. تواصل مع الدعم.`,
|
|
110
|
+
en: `Steam refused to download ${label} because the download account doesn't own it. Contact support.`,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
ar: `تعذّر تنزيل ${label} من Steam، جرب مرة ثانية.`,
|
|
116
|
+
en: `We couldn't download ${label} from Steam. Try again.`,
|
|
117
|
+
};
|
|
118
|
+
};
|
|
119
|
+
|
|
103
120
|
const installRoot = async (context: BridgeContext) => {
|
|
104
121
|
const result = await context.exec([
|
|
105
122
|
"pwd",
|
|
@@ -162,7 +179,7 @@ export const createSteamcmd = (context: BridgeContext, options: SteamcmdOptions)
|
|
|
162
179
|
|
|
163
180
|
const attempts = runOptions.attempts ?? ATTEMPTS;
|
|
164
181
|
|
|
165
|
-
let failure =
|
|
182
|
+
let failure: BridgeExecResult | null = null;
|
|
166
183
|
|
|
167
184
|
for (let attempt = 1; attempt <= attempts; attempt++) {
|
|
168
185
|
const result = await context.exec(argv, {
|
|
@@ -178,7 +195,7 @@ export const createSteamcmd = (context: BridgeContext, options: SteamcmdOptions)
|
|
|
178
195
|
return;
|
|
179
196
|
}
|
|
180
197
|
|
|
181
|
-
failure =
|
|
198
|
+
failure = result;
|
|
182
199
|
|
|
183
200
|
if (attempt < attempts) {
|
|
184
201
|
context.log("steamcmd did not finish, trying again", {
|
|
@@ -191,7 +208,13 @@ export const createSteamcmd = (context: BridgeContext, options: SteamcmdOptions)
|
|
|
191
208
|
}
|
|
192
209
|
}
|
|
193
210
|
|
|
194
|
-
|
|
211
|
+
if (!failure) {
|
|
212
|
+
throw new Error("steamcmd was not run");
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
throw new BridgeUserError(steamcmdFailureText(failure, options.label), {
|
|
216
|
+
cause: new Error(`steamcmd exited with code ${failure.code} — ${execDetail(failure)}`),
|
|
217
|
+
});
|
|
195
218
|
};
|
|
196
219
|
|
|
197
220
|
return {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, expect,
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
2
|
import type { BridgeLog } from "../context";
|
|
3
3
|
import type { BridgeValues } from "../core";
|
|
4
4
|
import { steamcmdNarrator } from "./steamcmdNarrator";
|
|
@@ -20,7 +20,7 @@ const recorder = () => {
|
|
|
20
20
|
};
|
|
21
21
|
|
|
22
22
|
describe("steamcmdNarrator", () => {
|
|
23
|
-
|
|
23
|
+
it("reports download progress in ten percent steps", () => {
|
|
24
24
|
const { log, lines, fields } = recorder();
|
|
25
25
|
const narrate = steamcmdNarrator(log, "palworld");
|
|
26
26
|
|
|
@@ -57,7 +57,7 @@ describe("steamcmdNarrator", () => {
|
|
|
57
57
|
]);
|
|
58
58
|
});
|
|
59
59
|
|
|
60
|
-
|
|
60
|
+
it("starts counting again when the phase changes", () => {
|
|
61
61
|
const { log, fields } = recorder();
|
|
62
62
|
const narrate = steamcmdNarrator(log, "rust");
|
|
63
63
|
|
|
@@ -74,7 +74,7 @@ describe("steamcmdNarrator", () => {
|
|
|
74
74
|
]);
|
|
75
75
|
});
|
|
76
76
|
|
|
77
|
-
|
|
77
|
+
it("passes notable lines through with the ansi stripped", () => {
|
|
78
78
|
const { log, lines } = recorder();
|
|
79
79
|
const narrate = steamcmdNarrator(log, "rust");
|
|
80
80
|
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { describe, expect,
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
2
|
import { workshopContentPath, workshopDownloadArgs, workshopFormBody } from "./workshop";
|
|
3
3
|
|
|
4
4
|
describe("workshopFormBody", () => {
|
|
5
|
-
|
|
5
|
+
it("writes the count and the indexed ids", () => {
|
|
6
6
|
expect(
|
|
7
7
|
workshopFormBody("itemcount", [
|
|
8
8
|
"450814997",
|
|
@@ -11,7 +11,7 @@ describe("workshopFormBody", () => {
|
|
|
11
11
|
).toBe("itemcount=2&publishedfileids%5B0%5D=450814997&publishedfileids%5B1%5D=1234");
|
|
12
12
|
});
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
it("names the count field the collection api expects", () => {
|
|
15
15
|
expect(
|
|
16
16
|
workshopFormBody("collectioncount", [
|
|
17
17
|
"1",
|
|
@@ -21,7 +21,7 @@ describe("workshopFormBody", () => {
|
|
|
21
21
|
});
|
|
22
22
|
|
|
23
23
|
describe("workshopDownloadArgs", () => {
|
|
24
|
-
|
|
24
|
+
it("asks steamcmd for every item", () => {
|
|
25
25
|
expect(
|
|
26
26
|
workshopDownloadArgs("440900", [
|
|
27
27
|
"1",
|
|
@@ -39,7 +39,7 @@ describe("workshopDownloadArgs", () => {
|
|
|
39
39
|
});
|
|
40
40
|
|
|
41
41
|
describe("workshopContentPath", () => {
|
|
42
|
-
|
|
42
|
+
it("points at where steamcmd unpacks an item", () => {
|
|
43
43
|
expect(workshopContentPath("440900", "12345")).toBe("steamapps/workshop/content/440900/12345");
|
|
44
44
|
});
|
|
45
45
|
});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { changedSettingKeys } from "./changedSettingKeys";
|
|
3
|
+
|
|
4
|
+
describe("changedSettingKeys", () => {
|
|
5
|
+
it("keeps only the keys whose value really changed", () => {
|
|
6
|
+
expect(
|
|
7
|
+
changedSettingKeys(
|
|
8
|
+
{
|
|
9
|
+
difficulty: "easy",
|
|
10
|
+
expRate: "1.000000",
|
|
11
|
+
pvp: "True",
|
|
12
|
+
name: "My server",
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
difficulty: "hard",
|
|
16
|
+
expRate: 1,
|
|
17
|
+
pvp: true,
|
|
18
|
+
name: "My server",
|
|
19
|
+
},
|
|
20
|
+
),
|
|
21
|
+
).toEqual([
|
|
22
|
+
"difficulty",
|
|
23
|
+
]);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("counts a key the stored values do not hold as changed", () => {
|
|
27
|
+
expect(
|
|
28
|
+
changedSettingKeys(
|
|
29
|
+
{},
|
|
30
|
+
{
|
|
31
|
+
seed: "42",
|
|
32
|
+
},
|
|
33
|
+
),
|
|
34
|
+
).toEqual([
|
|
35
|
+
"seed",
|
|
36
|
+
]);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("counts every written key as changed when nothing could be read first", () => {
|
|
40
|
+
const written = {
|
|
41
|
+
difficulty: "hard",
|
|
42
|
+
pvp: false,
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
expect(changedSettingKeys(null, written)).toEqual([
|
|
46
|
+
"difficulty",
|
|
47
|
+
"pvp",
|
|
48
|
+
]);
|
|
49
|
+
expect(changedSettingKeys(undefined, written)).toEqual([
|
|
50
|
+
"difficulty",
|
|
51
|
+
"pvp",
|
|
52
|
+
]);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("answers nothing for an empty write", () => {
|
|
56
|
+
expect(
|
|
57
|
+
changedSettingKeys(
|
|
58
|
+
{
|
|
59
|
+
difficulty: "easy",
|
|
60
|
+
},
|
|
61
|
+
{},
|
|
62
|
+
),
|
|
63
|
+
).toEqual([]);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { BridgeValues } from "../core";
|
|
2
|
+
import { sameSettingValue } from "./sameSettingValue";
|
|
3
|
+
|
|
4
|
+
export const changedSettingKeys = (before: BridgeValues | null | undefined, written: BridgeValues): string[] => {
|
|
5
|
+
return Object.keys(written).filter((key) => !before || !sameSettingValue(before[key], written[key]));
|
|
6
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { describeExecFailure } from "./describeExecFailure";
|
|
3
|
+
|
|
4
|
+
describe("describeExecFailure", () => {
|
|
5
|
+
it("names the subject and the exit code in both languages", () => {
|
|
6
|
+
const text = describeExecFailure(
|
|
7
|
+
{
|
|
8
|
+
stdout: "",
|
|
9
|
+
stderr: "java.lang.RuntimeException",
|
|
10
|
+
code: 1,
|
|
11
|
+
},
|
|
12
|
+
"Forge installer",
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
expect(text).toEqual({
|
|
16
|
+
ar: "فشل Forge installer بكود الخروج 1.",
|
|
17
|
+
en: "Forge installer failed with exit code 1.",
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("keeps the process output out of the customer text", () => {
|
|
22
|
+
const text = describeExecFailure(
|
|
23
|
+
{
|
|
24
|
+
stdout: "token=abc",
|
|
25
|
+
stderr: "at /home/game/secret",
|
|
26
|
+
code: 2,
|
|
27
|
+
},
|
|
28
|
+
"unzip",
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
expect(JSON.stringify(text)).not.toContain("secret");
|
|
32
|
+
expect(JSON.stringify(text)).not.toContain("token");
|
|
33
|
+
});
|
|
34
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { BridgeExecResult } from "../context";
|
|
2
|
+
import type { LocalizedText } from "../core";
|
|
3
|
+
|
|
4
|
+
export const describeExecFailure = (result: BridgeExecResult, subject: string): LocalizedText => {
|
|
5
|
+
return {
|
|
6
|
+
ar: `فشل ${subject} بكود الخروج ${result.code}.`,
|
|
7
|
+
en: `${subject} failed with exit code ${result.code}.`,
|
|
8
|
+
};
|
|
9
|
+
};
|
package/src/utils/index.ts
CHANGED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { sameSettingValue } from "./sameSettingValue";
|
|
3
|
+
|
|
4
|
+
describe("sameSettingValue", () => {
|
|
5
|
+
it("treats a float the ini codec wrote as equal to the integer the form sends", () => {
|
|
6
|
+
expect(sameSettingValue("1.000000", 1)).toBe(true);
|
|
7
|
+
expect(sameSettingValue("1", "1.000000")).toBe(true);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it("treats decimals that differ only in trailing zeros as equal", () => {
|
|
11
|
+
expect(sameSettingValue("0.5", "0.50")).toBe(true);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("treats boolean text as equal whatever its case", () => {
|
|
15
|
+
expect(sameSettingValue("True", "true")).toBe(true);
|
|
16
|
+
expect(sameSettingValue("False", false)).toBe(true);
|
|
17
|
+
expect(sameSettingValue(true, "FALSE")).toBe(false);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("never treats an empty value as zero", () => {
|
|
21
|
+
expect(sameSettingValue("", "0")).toBe(false);
|
|
22
|
+
expect(sameSettingValue(null, 0)).toBe(false);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("keeps a leading-zero string different from its number, since it may be an id", () => {
|
|
26
|
+
expect(sameSettingValue("01", "1")).toBe(false);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("compares the trimmed text of anything else", () => {
|
|
30
|
+
expect(sameSettingValue(" easy ", "easy")).toBe(true);
|
|
31
|
+
expect(sameSettingValue("easy", "hard")).toBe(false);
|
|
32
|
+
expect(sameSettingValue("2", "3")).toBe(false);
|
|
33
|
+
expect(sameSettingValue(undefined, "")).toBe(true);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { BridgeValue } from "../core";
|
|
2
|
+
|
|
3
|
+
const PLAIN_NUMBER = /^-?(0|[1-9]\d*)(\.\d+)?$/;
|
|
4
|
+
|
|
5
|
+
const BOOLEAN_TEXT: Readonly<Record<string, boolean>> = {
|
|
6
|
+
true: true,
|
|
7
|
+
false: false,
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const settingText = (value: BridgeValue | undefined) => {
|
|
11
|
+
return value === undefined || value === null ? "" : String(value).trim();
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const numberOf = (text: string) => {
|
|
15
|
+
return PLAIN_NUMBER.test(text) ? Number(text) : null;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const booleanOf = (text: string) => {
|
|
19
|
+
return BOOLEAN_TEXT[text.toLowerCase()] ?? null;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const sameSettingValue = (left: BridgeValue | undefined, right: BridgeValue | undefined): boolean => {
|
|
23
|
+
const a = settingText(left);
|
|
24
|
+
const b = settingText(right);
|
|
25
|
+
|
|
26
|
+
if (a === b) {
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const leftNumber = numberOf(a);
|
|
31
|
+
const rightNumber = numberOf(b);
|
|
32
|
+
|
|
33
|
+
if (leftNumber !== null && rightNumber !== null) {
|
|
34
|
+
return leftNumber === rightNumber;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const leftBoolean = booleanOf(a);
|
|
38
|
+
|
|
39
|
+
return leftBoolean !== null && leftBoolean === booleanOf(b);
|
|
40
|
+
};
|
package/src/utils/token.test.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { describe, expect,
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
2
|
import { generateToken, TOKEN_ALPHABET } from "./token";
|
|
3
3
|
|
|
4
4
|
describe("generateToken", () => {
|
|
5
|
-
|
|
5
|
+
it("returns a token of the requested length from the default alphabet", () => {
|
|
6
6
|
const token = generateToken(40);
|
|
7
7
|
|
|
8
8
|
expect(token).toHaveLength(40);
|
|
@@ -13,15 +13,15 @@ describe("generateToken", () => {
|
|
|
13
13
|
).toBe(true);
|
|
14
14
|
});
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
it("uses the alphabet it is given", () => {
|
|
17
17
|
expect(generateToken(32, "ab")).toMatch(/^[ab]{32}$/);
|
|
18
18
|
});
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
it("does not repeat itself", () => {
|
|
21
21
|
expect(generateToken(24)).not.toBe(generateToken(24));
|
|
22
22
|
});
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
it("draws every character of a small alphabet", () => {
|
|
25
25
|
const seen = new Set([
|
|
26
26
|
...generateToken(2000, "abc"),
|
|
27
27
|
]);
|
|
@@ -37,13 +37,13 @@ describe("generateToken", () => {
|
|
|
37
37
|
]);
|
|
38
38
|
});
|
|
39
39
|
|
|
40
|
-
|
|
40
|
+
it("refuses a length that is not a positive integer", () => {
|
|
41
41
|
expect(() => generateToken(0)).toThrow();
|
|
42
42
|
expect(() => generateToken(-4)).toThrow();
|
|
43
43
|
expect(() => generateToken(1.5)).toThrow();
|
|
44
44
|
});
|
|
45
45
|
|
|
46
|
-
|
|
46
|
+
it("refuses an unusable alphabet", () => {
|
|
47
47
|
expect(() => generateToken(8, "")).toThrow();
|
|
48
48
|
expect(() => generateToken(8, "x".repeat(257))).toThrow();
|
|
49
49
|
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, expect,
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
2
|
import type { BridgeContext } from "../context";
|
|
3
3
|
import { protonCommand, protonCompatPath } from "./protonCommand";
|
|
4
4
|
|
|
@@ -13,13 +13,13 @@ const context = {
|
|
|
13
13
|
} as unknown as BridgeContext;
|
|
14
14
|
|
|
15
15
|
describe("protonCompatPath", () => {
|
|
16
|
-
|
|
16
|
+
it("gives every app its own compat data directory", () => {
|
|
17
17
|
expect(protonCompatPath("2430930")).toBe(".proton/2430930");
|
|
18
18
|
});
|
|
19
19
|
});
|
|
20
20
|
|
|
21
21
|
describe("protonCommand", () => {
|
|
22
|
-
|
|
22
|
+
it("builds an env prefix pointing inside the container home", async () => {
|
|
23
23
|
expect(
|
|
24
24
|
await protonCommand(context, {
|
|
25
25
|
appId: "2430930",
|
|
@@ -41,7 +41,7 @@ describe("protonCommand", () => {
|
|
|
41
41
|
]);
|
|
42
42
|
});
|
|
43
43
|
|
|
44
|
-
|
|
44
|
+
it("ensures the compat and steam directories", async () => {
|
|
45
45
|
ensured.length = 0;
|
|
46
46
|
|
|
47
47
|
await protonCommand(context, {
|
|
@@ -55,7 +55,7 @@ describe("protonCommand", () => {
|
|
|
55
55
|
]);
|
|
56
56
|
});
|
|
57
57
|
|
|
58
|
-
|
|
58
|
+
it("honours another proton build", async () => {
|
|
59
59
|
expect(
|
|
60
60
|
await protonCommand(context, {
|
|
61
61
|
appId: "1",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, expect,
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
2
|
import type { BridgeContext } from "../context";
|
|
3
3
|
import { wineCommand, wineDllOverrides } from "./wineCommand";
|
|
4
4
|
|
|
@@ -13,7 +13,7 @@ const context = {
|
|
|
13
13
|
} as unknown as BridgeContext;
|
|
14
14
|
|
|
15
15
|
describe("wineDllOverrides", () => {
|
|
16
|
-
|
|
16
|
+
it("joins the overrides the way wine reads them", () => {
|
|
17
17
|
expect(
|
|
18
18
|
wineDllOverrides({
|
|
19
19
|
winhttp: "n,b",
|
|
@@ -24,7 +24,7 @@ describe("wineDllOverrides", () => {
|
|
|
24
24
|
});
|
|
25
25
|
|
|
26
26
|
describe("wineCommand", () => {
|
|
27
|
-
|
|
27
|
+
it("builds an env prefix pointing inside the container home", async () => {
|
|
28
28
|
expect(
|
|
29
29
|
await wineCommand(context, {
|
|
30
30
|
exe: "VRisingServer.exe",
|
|
@@ -45,7 +45,7 @@ describe("wineCommand", () => {
|
|
|
45
45
|
]);
|
|
46
46
|
});
|
|
47
47
|
|
|
48
|
-
|
|
48
|
+
it("ensures the prefix directory it points at", async () => {
|
|
49
49
|
ensured.length = 0;
|
|
50
50
|
|
|
51
51
|
await wineCommand(context, {
|
|
@@ -58,7 +58,7 @@ describe("wineCommand", () => {
|
|
|
58
58
|
]);
|
|
59
59
|
});
|
|
60
60
|
|
|
61
|
-
|
|
61
|
+
it("wraps the game in xvfb-run when asked", async () => {
|
|
62
62
|
const argv = await wineCommand(context, {
|
|
63
63
|
exe: "a.exe",
|
|
64
64
|
xvfb: true,
|
|
@@ -72,7 +72,7 @@ describe("wineCommand", () => {
|
|
|
72
72
|
]);
|
|
73
73
|
});
|
|
74
74
|
|
|
75
|
-
|
|
75
|
+
it("adds the dll overrides and extra environment", async () => {
|
|
76
76
|
const argv = await wineCommand(context, {
|
|
77
77
|
exe: "a.exe",
|
|
78
78
|
dllOverrides: {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, expect,
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
2
|
import type { BridgeContext, BridgeExecOptions } from "../context";
|
|
3
3
|
import { prepareWinePrefix, WINE_PREFIX_STAMP } from "./winePrefix";
|
|
4
4
|
|
|
@@ -45,7 +45,7 @@ const testContext = (code = 0) => {
|
|
|
45
45
|
};
|
|
46
46
|
|
|
47
47
|
describe("prepareWinePrefix", () => {
|
|
48
|
-
|
|
48
|
+
it("boots a wine prefix once and stamps it", async () => {
|
|
49
49
|
const { context, runs, stored } = testContext();
|
|
50
50
|
|
|
51
51
|
await prepareWinePrefix(context);
|
|
@@ -63,7 +63,7 @@ describe("prepareWinePrefix", () => {
|
|
|
63
63
|
expect(runs).toHaveLength(1);
|
|
64
64
|
});
|
|
65
65
|
|
|
66
|
-
|
|
66
|
+
it("boots the prefix through proton when asked", async () => {
|
|
67
67
|
const { context, runs } = testContext();
|
|
68
68
|
|
|
69
69
|
await prepareWinePrefix(context, {
|
|
@@ -79,7 +79,7 @@ describe("prepareWinePrefix", () => {
|
|
|
79
79
|
]);
|
|
80
80
|
});
|
|
81
81
|
|
|
82
|
-
|
|
82
|
+
it("boots again when the runtime changed from wine to proton", async () => {
|
|
83
83
|
const { context, runs } = testContext();
|
|
84
84
|
|
|
85
85
|
await prepareWinePrefix(context);
|
|
@@ -91,7 +91,7 @@ describe("prepareWinePrefix", () => {
|
|
|
91
91
|
expect(runs).toHaveLength(2);
|
|
92
92
|
});
|
|
93
93
|
|
|
94
|
-
|
|
94
|
+
it("refuses a proton prefix with no app id", async () => {
|
|
95
95
|
const { context } = testContext();
|
|
96
96
|
|
|
97
97
|
await expect(
|
|
@@ -101,7 +101,7 @@ describe("prepareWinePrefix", () => {
|
|
|
101
101
|
).rejects.toThrow("steam app id");
|
|
102
102
|
});
|
|
103
103
|
|
|
104
|
-
|
|
104
|
+
it("raises when wineboot fails and leaves no stamp", async () => {
|
|
105
105
|
const { context, stored } = testContext(1);
|
|
106
106
|
|
|
107
107
|
await expect(prepareWinePrefix(context)).rejects.toThrow("wineboot exploded");
|