@serverkgg/bridge 0.0.12 → 0.0.13
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 +9 -1
- package/src/catalogs/curseforge.test.ts +580 -0
- package/src/catalogs/curseforge.ts +461 -0
- package/src/catalogs/index.ts +2 -0
- package/src/catalogs/thunderstore.test.ts +94 -0
- package/src/catalogs/thunderstore.ts +239 -0
- package/src/codecs/index.ts +3 -0
- package/src/codecs/ini.test.ts +152 -0
- package/src/codecs/ini.ts +158 -0
- package/src/codecs/sourceCfg.test.ts +100 -0
- package/src/codecs/sourceCfg.ts +111 -0
- package/src/codecs/xmlProperties.test.ts +104 -0
- package/src/codecs/xmlProperties.ts +133 -0
- package/src/context/bridgeContext.ts +20 -0
- package/src/core/bridgeRconError.ts +19 -0
- package/src/core/index.ts +1 -0
- package/src/install/index.ts +1 -0
- package/src/install/installStamp.test.ts +125 -0
- package/src/install/installStamp.ts +40 -0
- package/src/main.ts +1 -0
- package/src/manifest/index.ts +1 -0
- package/src/manifest/manifest.test.ts +427 -2
- package/src/manifest/manifest.ts +141 -13
- package/src/manifest/primitives.ts +6 -0
- package/src/manifest/runtime.ts +89 -0
- package/src/net/index.ts +2 -0
- package/src/net/loopback.test.ts +152 -0
- package/src/net/loopback.ts +109 -0
- package/src/net/loopbackMethod.ts +7 -0
- package/src/presence/index.ts +1 -0
- package/src/presence/rosterSync.test.ts +179 -0
- package/src/presence/rosterSync.ts +57 -0
- package/src/protocol/bridgeEvents.ts +4 -0
- package/src/rcon/index.ts +3 -0
- package/src/rcon/sourceRcon.test.ts +308 -0
- package/src/rcon/sourceRcon.ts +276 -0
- package/src/rcon/sourceRconFrame.test.ts +152 -0
- package/src/rcon/sourceRconFrame.ts +99 -0
- package/src/runtime/bridgeCodec.ts +45 -2
- package/src/runtime/bridgeContext.ts +13 -6
- package/src/runtime/bridgeDriver.test.ts +1 -0
- package/src/runtime/bridgeLiveConfig.test.ts +149 -0
- package/src/runtime/bridgeLiveConfig.ts +63 -0
- package/src/runtime/bridgeRcon.ts +15 -0
- package/src/runtime/bridgeRegistry.ts +1 -0
- package/src/steam/index.ts +7 -0
- package/src/steam/steamApp.test.ts +78 -0
- package/src/steam/steamApp.ts +35 -0
- package/src/steam/steamcmd.test.ts +157 -0
- package/src/steam/steamcmd.ts +273 -0
- package/src/steam/steamcmdLogin.ts +34 -0
- package/src/steam/steamcmdNarrator.test.ts +90 -0
- package/src/steam/steamcmdNarrator.ts +43 -0
- package/src/steam/steamcmdPlatform.ts +4 -0
- package/src/steam/webApi.ts +70 -0
- package/src/steam/workshop.test.ts +45 -0
- package/src/steam/workshop.ts +148 -0
- package/src/utils/execDetail.ts +16 -0
- package/src/utils/index.ts +2 -0
- package/src/utils/token.test.ts +50 -0
- package/src/utils/token.ts +35 -0
- package/src/wine/index.ts +3 -0
- package/src/wine/protonCommand.test.ts +67 -0
- package/src/wine/protonCommand.ts +49 -0
- package/src/wine/wineCommand.test.ts +92 -0
- package/src/wine/wineCommand.ts +54 -0
- package/src/wine/winePrefix.test.ts +111 -0
- package/src/wine/winePrefix.ts +88 -0
- package/src/runtime/bridgeSecrets.test.ts +0 -121
- package/src/runtime/bridgeSecrets.ts +0 -43
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { BridgeRconCode, BridgeRconError } from "../core";
|
|
2
|
+
|
|
3
|
+
export enum SourceRconRequest {
|
|
4
|
+
Command = 2,
|
|
5
|
+
Auth = 3,
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export enum SourceRconResponse {
|
|
9
|
+
Value = 0,
|
|
10
|
+
Auth = 2,
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const SOURCE_RCON_BODY_LIMIT = 4096;
|
|
14
|
+
|
|
15
|
+
export const SOURCE_RCON_AUTH_FAILED_ID = -1;
|
|
16
|
+
|
|
17
|
+
const HEADER_LENGTH = 4;
|
|
18
|
+
|
|
19
|
+
const MIN_FRAME_SIZE = 10;
|
|
20
|
+
|
|
21
|
+
const MAX_FRAME_SIZE = 65_536;
|
|
22
|
+
|
|
23
|
+
const ENCODER = new TextEncoder();
|
|
24
|
+
|
|
25
|
+
const DECODER = new TextDecoder();
|
|
26
|
+
|
|
27
|
+
export interface SourceRconFrame {
|
|
28
|
+
id: number;
|
|
29
|
+
type: number;
|
|
30
|
+
body: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const encodeSourceRconFrame = (frame: SourceRconFrame) => {
|
|
34
|
+
const body = ENCODER.encode(frame.body);
|
|
35
|
+
|
|
36
|
+
if (body.length > SOURCE_RCON_BODY_LIMIT) {
|
|
37
|
+
throw new BridgeRconError(
|
|
38
|
+
BridgeRconCode.Oversized,
|
|
39
|
+
`an rcon body of ${body.length} bytes exceeds the ${SOURCE_RCON_BODY_LIMIT} byte limit`,
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const size = 4 + 4 + body.length + 2;
|
|
44
|
+
const packet = new Uint8Array(HEADER_LENGTH + size);
|
|
45
|
+
const view = new DataView(packet.buffer);
|
|
46
|
+
|
|
47
|
+
view.setInt32(0, size, true);
|
|
48
|
+
view.setInt32(4, frame.id, true);
|
|
49
|
+
view.setInt32(8, frame.type, true);
|
|
50
|
+
packet.set(body, 12);
|
|
51
|
+
|
|
52
|
+
return packet;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export const decodeSourceRconFrames = (buffer: Uint8Array) => {
|
|
56
|
+
const frames: SourceRconFrame[] = [];
|
|
57
|
+
|
|
58
|
+
let offset = 0;
|
|
59
|
+
|
|
60
|
+
while (buffer.length - offset >= HEADER_LENGTH) {
|
|
61
|
+
const size = new DataView(buffer.buffer, buffer.byteOffset + offset, HEADER_LENGTH).getInt32(0, true);
|
|
62
|
+
|
|
63
|
+
if (size < MIN_FRAME_SIZE || size > MAX_FRAME_SIZE) {
|
|
64
|
+
throw new BridgeRconError(BridgeRconCode.Malformed, `an rcon packet declares an impossible size of ${size}`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (buffer.length - offset - HEADER_LENGTH < size) {
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const head = new DataView(buffer.buffer, buffer.byteOffset + offset + HEADER_LENGTH, 8);
|
|
72
|
+
|
|
73
|
+
frames.push({
|
|
74
|
+
id: head.getInt32(0, true),
|
|
75
|
+
type: head.getInt32(4, true),
|
|
76
|
+
body: DECODER.decode(buffer.subarray(offset + 12, offset + HEADER_LENGTH + size - 2)),
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
offset += HEADER_LENGTH + size;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
frames,
|
|
84
|
+
rest: buffer.subarray(offset),
|
|
85
|
+
};
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
export const concatBytes = (left: Uint8Array, right: Uint8Array) => {
|
|
89
|
+
if (left.length === 0) {
|
|
90
|
+
return right;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const joined = new Uint8Array(left.length + right.length);
|
|
94
|
+
|
|
95
|
+
joined.set(left);
|
|
96
|
+
joined.set(right, left.length);
|
|
97
|
+
|
|
98
|
+
return joined;
|
|
99
|
+
};
|
|
@@ -1,4 +1,17 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
mergeIni,
|
|
3
|
+
mergeJson,
|
|
4
|
+
mergeProperties,
|
|
5
|
+
mergeSourceCfg,
|
|
6
|
+
mergeUeIni,
|
|
7
|
+
mergeXmlProperties,
|
|
8
|
+
parseIni,
|
|
9
|
+
parseJson,
|
|
10
|
+
parseProperties,
|
|
11
|
+
parseSourceCfg,
|
|
12
|
+
parseUeIni,
|
|
13
|
+
parseXmlProperties,
|
|
14
|
+
} from "../codecs";
|
|
2
15
|
import type { BridgeCodec, BridgeCodecOptions, BridgeCodecs, BridgeFiles } from "../context";
|
|
3
16
|
import type { BridgeValue, BridgeValues } from "../core";
|
|
4
17
|
|
|
@@ -70,9 +83,39 @@ export const createBridgeCodecs = (files: BridgeFiles): BridgeCodecs => {
|
|
|
70
83
|
},
|
|
71
84
|
};
|
|
72
85
|
|
|
86
|
+
const ini: BridgeCodec = {
|
|
87
|
+
async read(path, options) {
|
|
88
|
+
return widen(parseIni(await readOr(files, path), options?.section));
|
|
89
|
+
},
|
|
90
|
+
async merge(path, values, options) {
|
|
91
|
+
await files.write(path, mergeIni(await readOr(files, path), toStrings(values), options?.section));
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const sourceCfg: BridgeCodec = {
|
|
96
|
+
async read(path) {
|
|
97
|
+
return parseSourceCfg(await readOr(files, path));
|
|
98
|
+
},
|
|
99
|
+
async merge(path, values) {
|
|
100
|
+
await files.write(path, mergeSourceCfg(await readOr(files, path), values));
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const xmlProperties: BridgeCodec = {
|
|
105
|
+
async read(path) {
|
|
106
|
+
return widen(parseXmlProperties(await readOr(files, path)));
|
|
107
|
+
},
|
|
108
|
+
async merge(path, values) {
|
|
109
|
+
await files.write(path, mergeXmlProperties(await readOr(files, path), toStrings(values)));
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
|
|
73
113
|
return {
|
|
74
|
-
|
|
114
|
+
ini,
|
|
75
115
|
json,
|
|
116
|
+
properties,
|
|
117
|
+
sourceCfg,
|
|
76
118
|
ueIni,
|
|
119
|
+
xmlProperties,
|
|
77
120
|
};
|
|
78
121
|
};
|
|
@@ -7,10 +7,11 @@ import { configPath } from "./bridgeConfigReader";
|
|
|
7
7
|
import { createBridgeEgress } from "./bridgeEgress";
|
|
8
8
|
import { createBridgeExec } from "./bridgeExec";
|
|
9
9
|
import { createBridgeFiles } from "./bridgeFiles";
|
|
10
|
+
import { createBridgeLiveConfig } from "./bridgeLiveConfig";
|
|
10
11
|
import type { BridgeLeveledLogger } from "./bridgeLog";
|
|
11
12
|
import { createBridgeNet } from "./bridgeNet";
|
|
12
13
|
import { createBridgeProbes } from "./bridgeProbe";
|
|
13
|
-
import {
|
|
14
|
+
import { createBridgeRcons } from "./bridgeRcon";
|
|
14
15
|
import type { BridgeSupervisor } from "./bridgeSupervisor";
|
|
15
16
|
|
|
16
17
|
export interface ContextHooks {
|
|
@@ -26,7 +27,7 @@ export const createContext = (
|
|
|
26
27
|
): BridgeContext => {
|
|
27
28
|
const egress = createBridgeEgress(config);
|
|
28
29
|
|
|
29
|
-
const
|
|
30
|
+
const live = createBridgeLiveConfig(configPath(), config);
|
|
30
31
|
|
|
31
32
|
const files = createBridgeFiles({
|
|
32
33
|
root: config.cwd,
|
|
@@ -39,19 +40,23 @@ export const createContext = (
|
|
|
39
40
|
return {
|
|
40
41
|
server: {
|
|
41
42
|
code: config.code,
|
|
42
|
-
memoryMb
|
|
43
|
-
|
|
43
|
+
get memoryMb() {
|
|
44
|
+
return live.memoryMb();
|
|
45
|
+
},
|
|
46
|
+
get cpuPercent() {
|
|
47
|
+
return live.cpuPercent();
|
|
48
|
+
},
|
|
44
49
|
get running() {
|
|
45
50
|
return supervisor.running;
|
|
46
51
|
},
|
|
47
52
|
},
|
|
48
53
|
|
|
49
54
|
variable(key) {
|
|
50
|
-
return
|
|
55
|
+
return live.variable(key);
|
|
51
56
|
},
|
|
52
57
|
|
|
53
58
|
secret(key) {
|
|
54
|
-
return
|
|
59
|
+
return live.secret(key);
|
|
55
60
|
},
|
|
56
61
|
|
|
57
62
|
port(key) {
|
|
@@ -72,6 +77,8 @@ export const createContext = (
|
|
|
72
77
|
|
|
73
78
|
probe: createBridgeProbes(),
|
|
74
79
|
|
|
80
|
+
rcon: createBridgeRcons(),
|
|
81
|
+
|
|
75
82
|
async command(input, options) {
|
|
76
83
|
return await supervisor.command(input, options);
|
|
77
84
|
},
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import type { BridgeConfig } from "../protocol";
|
|
6
|
+
import { createBridgeLiveConfig } from "./bridgeLiveConfig";
|
|
7
|
+
|
|
8
|
+
const configWith = (secrets: Record<string, string>, variables: Record<string, string> = {}): BridgeConfig => ({
|
|
9
|
+
code: "wsvv8fakxm",
|
|
10
|
+
token: "token",
|
|
11
|
+
cwd: "/data",
|
|
12
|
+
run: "/run/bridge",
|
|
13
|
+
ports: {
|
|
14
|
+
game: 25_565,
|
|
15
|
+
},
|
|
16
|
+
memoryMb: 2048,
|
|
17
|
+
cpuPercent: 100,
|
|
18
|
+
gameUid: 1000,
|
|
19
|
+
gameGid: 1000,
|
|
20
|
+
variables: {
|
|
21
|
+
VERSION: "1.21",
|
|
22
|
+
...variables,
|
|
23
|
+
},
|
|
24
|
+
secrets,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
let directory: string;
|
|
28
|
+
|
|
29
|
+
let path: string;
|
|
30
|
+
|
|
31
|
+
const write = async (config: BridgeConfig) => {
|
|
32
|
+
await writeFile(path, `${JSON.stringify(config, null, 2)}\n`);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
beforeEach(async () => {
|
|
36
|
+
directory = await mkdtemp(join(tmpdir(), "bridge-live-config-"));
|
|
37
|
+
path = join(directory, "config.json");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
afterEach(async () => {
|
|
41
|
+
await rm(directory, {
|
|
42
|
+
force: true,
|
|
43
|
+
recursive: true,
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe("the bridge live config reader", () => {
|
|
48
|
+
test("answers from the boot snapshot", async () => {
|
|
49
|
+
const config = configWith({
|
|
50
|
+
CURSEFORGE_API_KEY: "boot",
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
await write(config);
|
|
54
|
+
|
|
55
|
+
const secrets = createBridgeLiveConfig(path, config, 0);
|
|
56
|
+
|
|
57
|
+
expect(secrets.secret("CURSEFORGE_API_KEY")).toBe("boot");
|
|
58
|
+
expect(secrets.secret("MODRINTH_API_KEY")).toBeNull();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("sees a value the agent rewrote into the config", async () => {
|
|
62
|
+
const config = configWith({
|
|
63
|
+
CURSEFORGE_API_KEY: "boot",
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
await write(config);
|
|
67
|
+
|
|
68
|
+
const secrets = createBridgeLiveConfig(path, config, 0);
|
|
69
|
+
|
|
70
|
+
expect(secrets.secret("CURSEFORGE_API_KEY")).toBe("boot");
|
|
71
|
+
|
|
72
|
+
await write(
|
|
73
|
+
configWith({
|
|
74
|
+
CURSEFORGE_API_KEY: "rotated",
|
|
75
|
+
MODRINTH_API_KEY: "added",
|
|
76
|
+
}),
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
expect(secrets.secret("CURSEFORGE_API_KEY")).toBe("rotated");
|
|
80
|
+
expect(secrets.secret("MODRINTH_API_KEY")).toBe("added");
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("keeps the last good values while the config is mid-write", async () => {
|
|
84
|
+
const config = configWith({
|
|
85
|
+
CURSEFORGE_API_KEY: "boot",
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
await write(config);
|
|
89
|
+
|
|
90
|
+
const secrets = createBridgeLiveConfig(path, config, 0);
|
|
91
|
+
|
|
92
|
+
expect(secrets.secret("CURSEFORGE_API_KEY")).toBe("boot");
|
|
93
|
+
|
|
94
|
+
await writeFile(path, '{"code": "wsvv8fak');
|
|
95
|
+
|
|
96
|
+
expect(secrets.secret("CURSEFORGE_API_KEY")).toBe("boot");
|
|
97
|
+
|
|
98
|
+
await rm(path);
|
|
99
|
+
|
|
100
|
+
expect(secrets.secret("CURSEFORGE_API_KEY")).toBe("boot");
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("re-reads the file at most once per refresh window", async () => {
|
|
104
|
+
const config = configWith({
|
|
105
|
+
CURSEFORGE_API_KEY: "boot",
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
await write(config);
|
|
109
|
+
|
|
110
|
+
const secrets = createBridgeLiveConfig(path, config, 60_000);
|
|
111
|
+
|
|
112
|
+
expect(secrets.secret("CURSEFORGE_API_KEY")).toBe("boot");
|
|
113
|
+
|
|
114
|
+
await write(
|
|
115
|
+
configWith({
|
|
116
|
+
CURSEFORGE_API_KEY: "rotated",
|
|
117
|
+
}),
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
expect(secrets.secret("CURSEFORGE_API_KEY")).toBe("boot");
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("sees a variable the customer changed after boot, and the new plan resources", async () => {
|
|
124
|
+
const config = configWith({});
|
|
125
|
+
|
|
126
|
+
await write(config);
|
|
127
|
+
|
|
128
|
+
const live = createBridgeLiveConfig(path, config, 0);
|
|
129
|
+
|
|
130
|
+
expect(live.variable("CROSSPLAY")).toBeNull();
|
|
131
|
+
expect(live.memoryMb()).toBe(2048);
|
|
132
|
+
|
|
133
|
+
await write({
|
|
134
|
+
...configWith(
|
|
135
|
+
{},
|
|
136
|
+
{
|
|
137
|
+
CROSSPLAY: "true",
|
|
138
|
+
},
|
|
139
|
+
),
|
|
140
|
+
memoryMb: 4096,
|
|
141
|
+
cpuPercent: 200,
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
expect(live.variable("CROSSPLAY")).toBe("true");
|
|
145
|
+
expect(live.variable("VERSION")).toBe("1.21");
|
|
146
|
+
expect(live.memoryMb()).toBe(4096);
|
|
147
|
+
expect(live.cpuPercent()).toBe(200);
|
|
148
|
+
});
|
|
149
|
+
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import type { BridgeConfig } from "../protocol";
|
|
3
|
+
import { parseConfig } from "./bridgeConfigReader";
|
|
4
|
+
|
|
5
|
+
export const BRIDGE_CONFIG_REFRESH_MS = 2000;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The agent rewrites the bridge config in place when the customer changes a
|
|
9
|
+
* variable, a secret or the plan, and then restarts the game rather than the
|
|
10
|
+
* container. The bridge process outlives that rewrite, so anything answered
|
|
11
|
+
* from the boot snapshot would be stale. Every read here re-checks the file,
|
|
12
|
+
* throttled to one read per refresh window, and keeps the last good values
|
|
13
|
+
* while the agent is mid-write.
|
|
14
|
+
*/
|
|
15
|
+
export interface BridgeLiveConfig {
|
|
16
|
+
variable: (key: string) => string | null;
|
|
17
|
+
secret: (key: string) => string | null;
|
|
18
|
+
memoryMb: () => number;
|
|
19
|
+
cpuPercent: () => number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const createBridgeLiveConfig = (
|
|
23
|
+
path: string,
|
|
24
|
+
config: BridgeConfig,
|
|
25
|
+
refreshMs: number = BRIDGE_CONFIG_REFRESH_MS,
|
|
26
|
+
): BridgeLiveConfig => {
|
|
27
|
+
let current = config;
|
|
28
|
+
|
|
29
|
+
let checkedAt: number | null = null;
|
|
30
|
+
|
|
31
|
+
const refresh = () => {
|
|
32
|
+
const stamp = performance.now();
|
|
33
|
+
|
|
34
|
+
if (checkedAt !== null && stamp - checkedAt < refreshMs) {
|
|
35
|
+
return current;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
checkedAt = stamp;
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
current = parseConfig(readFileSync(path, "utf8"));
|
|
42
|
+
} catch {
|
|
43
|
+
return current;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return current;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
variable(key) {
|
|
51
|
+
return refresh().variables[key] ?? null;
|
|
52
|
+
},
|
|
53
|
+
secret(key) {
|
|
54
|
+
return refresh().secrets[key] ?? null;
|
|
55
|
+
},
|
|
56
|
+
memoryMb() {
|
|
57
|
+
return refresh().memoryMb;
|
|
58
|
+
},
|
|
59
|
+
cpuPercent() {
|
|
60
|
+
return refresh().cpuPercent;
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { BridgeRcons } from "../context";
|
|
2
|
+
import { sourceRcon } from "../rcon";
|
|
3
|
+
|
|
4
|
+
const RCON_HOST = "127.0.0.1";
|
|
5
|
+
|
|
6
|
+
export const createBridgeRcons = (): BridgeRcons => {
|
|
7
|
+
return {
|
|
8
|
+
async source(options) {
|
|
9
|
+
return await sourceRcon({
|
|
10
|
+
...options,
|
|
11
|
+
host: RCON_HOST,
|
|
12
|
+
});
|
|
13
|
+
},
|
|
14
|
+
};
|
|
15
|
+
};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import type { BridgeContext } from "../context";
|
|
3
|
+
import { appManifestPath, buildIdOf, installedBuildId, missingGameRoots } from "./steamApp";
|
|
4
|
+
|
|
5
|
+
const MANIFEST = [
|
|
6
|
+
'"AppState"',
|
|
7
|
+
"{",
|
|
8
|
+
'\t"appid"\t\t"2394010"',
|
|
9
|
+
'\t"buildid"\t\t"19582821"',
|
|
10
|
+
"}",
|
|
11
|
+
].join("\n");
|
|
12
|
+
|
|
13
|
+
const testContext = (files: Record<string, string>) => {
|
|
14
|
+
return {
|
|
15
|
+
files: {
|
|
16
|
+
async exists(path: string) {
|
|
17
|
+
return path in files;
|
|
18
|
+
},
|
|
19
|
+
async read(path: string) {
|
|
20
|
+
return files[path] ?? "";
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
} as unknown as BridgeContext;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
describe("appManifestPath", () => {
|
|
27
|
+
test("points at the steamapps manifest", () => {
|
|
28
|
+
expect(appManifestPath("2394010")).toBe("steamapps/appmanifest_2394010.acf");
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("accepts another steamapps directory", () => {
|
|
32
|
+
expect(appManifestPath("90", ".steam/steamapps")).toBe(".steam/steamapps/appmanifest_90.acf");
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe("buildIdOf", () => {
|
|
37
|
+
test("reads the build id out of an acf manifest", () => {
|
|
38
|
+
expect(buildIdOf(MANIFEST)).toBe("19582821");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("returns null when there is no build id", () => {
|
|
42
|
+
expect(buildIdOf('"AppState"\n{\n}\n')).toBeNull();
|
|
43
|
+
expect(buildIdOf("")).toBeNull();
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe("installedBuildId", () => {
|
|
48
|
+
test("returns null when the app was never installed", async () => {
|
|
49
|
+
expect(await installedBuildId(testContext({}), "2394010")).toBeNull();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("reads the installed build id", async () => {
|
|
53
|
+
const context = testContext({
|
|
54
|
+
"steamapps/appmanifest_2394010.acf": MANIFEST,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
expect(await installedBuildId(context, "2394010")).toBe("19582821");
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe("missingGameRoots", () => {
|
|
62
|
+
test("names only the paths that are absent", async () => {
|
|
63
|
+
const context = testContext({
|
|
64
|
+
"PalServer.sh": "",
|
|
65
|
+
Engine: "",
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
expect(
|
|
69
|
+
await missingGameRoots(context, [
|
|
70
|
+
"PalServer.sh",
|
|
71
|
+
"Pal/Content",
|
|
72
|
+
"Engine",
|
|
73
|
+
]),
|
|
74
|
+
).toEqual([
|
|
75
|
+
"Pal/Content",
|
|
76
|
+
]);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { BridgeContext } from "../context";
|
|
2
|
+
|
|
3
|
+
export const STEAMAPPS_DIRECTORY = "steamapps";
|
|
4
|
+
|
|
5
|
+
const BUILD_ID = /"buildid"\s+"(?<buildId>\d+)"/;
|
|
6
|
+
|
|
7
|
+
export const appManifestPath = (appId: string, directory: string = STEAMAPPS_DIRECTORY) => {
|
|
8
|
+
return `${directory}/appmanifest_${appId}.acf`;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export const buildIdOf = (manifest: string) => {
|
|
12
|
+
return manifest.match(BUILD_ID)?.groups?.buildId ?? null;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export const installedBuildId = async (context: BridgeContext, appId: string, directory?: string) => {
|
|
16
|
+
const path = appManifestPath(appId, directory);
|
|
17
|
+
|
|
18
|
+
if (!(await context.files.exists(path))) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return buildIdOf(await context.files.read(path));
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const missingGameRoots = async (context: BridgeContext, roots: string[]) => {
|
|
26
|
+
const missing: string[] = [];
|
|
27
|
+
|
|
28
|
+
for (const path of roots) {
|
|
29
|
+
if (!(await context.files.exists(path))) {
|
|
30
|
+
missing.push(path);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return missing;
|
|
35
|
+
};
|