spplice-api 0.1.0 → 0.2.0
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/dist/docs/example.d.ts +2 -0
- package/dist/docs/example.d.ts.map +1 -0
- package/{docs/example.ts → dist/docs/example.js} +10 -13
- package/dist/docs/example.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/src/ConsoleTools.d.ts +40 -0
- package/dist/src/ConsoleTools.d.ts.map +1 -0
- package/dist/src/ConsoleTools.js +116 -0
- package/dist/src/ConsoleTools.js.map +1 -0
- package/dist/src/PackageTools.d.ts +52 -0
- package/dist/src/PackageTools.d.ts.map +1 -0
- package/dist/src/PackageTools.js +160 -0
- package/dist/src/PackageTools.js.map +1 -0
- package/dist/src/SteamTools.d.ts +47 -0
- package/dist/src/SteamTools.d.ts.map +1 -0
- package/dist/src/SteamTools.js +166 -0
- package/dist/src/SteamTools.js.map +1 -0
- package/dist/test/extract.test.d.ts +2 -0
- package/dist/test/extract.test.d.ts.map +1 -0
- package/dist/test/extract.test.js +11 -0
- package/dist/test/extract.test.js.map +1 -0
- package/package.json +18 -5
- package/index.ts +0 -10
- package/src/ConsoleTools.ts +0 -120
- package/src/PackageTools.ts +0 -170
- package/src/SteamTools.ts +0 -195
- package/test/extract.test.ts +0 -11
- package/tsconfig.json +0 -29
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { SteamworksSDK } from "steamworks-ffi-node";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import { execSync, execFile } from "node:child_process";
|
|
6
|
+
import psList from "ps-list";
|
|
7
|
+
class SteamClient {
|
|
8
|
+
steamworksClient;
|
|
9
|
+
appID;
|
|
10
|
+
/**
|
|
11
|
+
* Initialize Steamworks API.
|
|
12
|
+
* @param appID Game's Steam AppID (default 620 = Portal 2)
|
|
13
|
+
* Note: This makes Steam think that the specified game (Portal 2) is running.
|
|
14
|
+
*/
|
|
15
|
+
constructor(appID = 620) {
|
|
16
|
+
this.appID = appID;
|
|
17
|
+
// HACK: Temporarily use AppID of Spacewar - Steam otherwise won't let us launch Portal 2
|
|
18
|
+
// Fixed by implementing manual startup of Portal 2
|
|
19
|
+
appID = 480;
|
|
20
|
+
// The Steamworks SDK requires steam_appid.txt in the working directory when
|
|
21
|
+
// the app is not launched through Steam, regardless of the envvar.
|
|
22
|
+
fs.writeFileSync(path.join(process.cwd(), "steam_appid.txt"), appID.toString());
|
|
23
|
+
this.steamworksClient = SteamworksSDK.getInstance();
|
|
24
|
+
const success = this.steamworksClient.init({ appId: appID });
|
|
25
|
+
if (!success)
|
|
26
|
+
throw "Failed to initialize Steam API";
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* @returns {string} Absolute path to game directory.
|
|
30
|
+
*/
|
|
31
|
+
getGameDir() {
|
|
32
|
+
const pathString = this.steamworksClient.apps.getAppInstallDir(this.appID);
|
|
33
|
+
return path.resolve(pathString);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* @returns {UserDetails} Current user's SteamID64 and username.
|
|
37
|
+
*/
|
|
38
|
+
getUserDetails() {
|
|
39
|
+
const steamid = BigInt(this.steamworksClient.getStatus().steamId);
|
|
40
|
+
const name = this.steamworksClient.friends.getPersonaName();
|
|
41
|
+
return { steamid, name };
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Checks whether the currently logged-in user has access to the game on Steam.
|
|
45
|
+
* @returns {boolean} true if current user has the game, false otherwise.
|
|
46
|
+
*/
|
|
47
|
+
doesUserOwnGame() {
|
|
48
|
+
return this.steamworksClient.apps.isSubscribed();
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Searches for Steam binaries and returns all that were found. Output is
|
|
52
|
+
* sorted by how relevant the results are. The most useful result is first.
|
|
53
|
+
*
|
|
54
|
+
* @returns {string[]} Array of absolute paths to found Steam executables,
|
|
55
|
+
* sorted by descending relevance.
|
|
56
|
+
*/
|
|
57
|
+
static async findSteam() {
|
|
58
|
+
const candidates = [];
|
|
59
|
+
/**
|
|
60
|
+
* First pass:
|
|
61
|
+
*
|
|
62
|
+
* Start by looking at the list of running system processes.
|
|
63
|
+
* The most relevant binary is likely one that is currently running.
|
|
64
|
+
*/
|
|
65
|
+
const processList = await psList();
|
|
66
|
+
// Sort the process list, putting the most recent processes first.
|
|
67
|
+
// The more relevant process is the one that was last opened.
|
|
68
|
+
processList.sort((a, b) => Number(b.startTime) - Number(a.startTime));
|
|
69
|
+
// Filter for processes with name "steam".
|
|
70
|
+
const steamProcesses = processList.filter(p => p.name === "steam" ||
|
|
71
|
+
p.name === "steam.exe");
|
|
72
|
+
// TODO: Read registry on Windows
|
|
73
|
+
/**
|
|
74
|
+
* Second pass:
|
|
75
|
+
*
|
|
76
|
+
* Look for processes listening on port 27036. Steam uses this port to do
|
|
77
|
+
* game streaming, making it a fairly reliable way to fingerprint Steam.
|
|
78
|
+
*/
|
|
79
|
+
try {
|
|
80
|
+
let steamPortPIDs = [];
|
|
81
|
+
// There's no neat package for this, so we use OS-dependent shells.
|
|
82
|
+
if (process.platform === "win32") {
|
|
83
|
+
const stdout = execSync("netstat -ano | findstr :27036");
|
|
84
|
+
const matches = stdout.toString().match(/\s+(\d+)$/gm);
|
|
85
|
+
if (matches)
|
|
86
|
+
steamPortPIDs = [...new Set(matches.map(m => Number(m.trim())))];
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
const stdout = execSync("lsof -i :27036 -t");
|
|
90
|
+
const result = stdout.toString();
|
|
91
|
+
steamPortPIDs = result.split("\n").map(Number);
|
|
92
|
+
}
|
|
93
|
+
// Filter for processes with the PIDs we found.
|
|
94
|
+
const steamPortProcesses = processList.filter(p => steamPortPIDs.includes(p.pid));
|
|
95
|
+
steamProcesses.push(...steamPortProcesses);
|
|
96
|
+
}
|
|
97
|
+
catch (_) { }
|
|
98
|
+
// Push process paths to candidate list.
|
|
99
|
+
for (const process of steamProcesses) {
|
|
100
|
+
if (process.path)
|
|
101
|
+
candidates.push(process.path);
|
|
102
|
+
else if (process.cmd)
|
|
103
|
+
candidates.push(process.cmd.split(" ")[0]);
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Third pass:
|
|
107
|
+
*
|
|
108
|
+
* Blindly push common Steam installation paths as a last resort.
|
|
109
|
+
* These will get validated in the final filtering step.
|
|
110
|
+
*/
|
|
111
|
+
candidates.push(`C:\\Program Files (x86)\\Steam\\steam.exe`, `${os.homedir()}/.steam/steam/ubuntu12_32/steam`, `${os.homedir()}/.local/share/Steam/ubuntu12_32/steam`, `${os.homedir()}/.var/app/com.valvesoftware.Steam/.local/share/Steam/ubuntu12_32/steam`);
|
|
112
|
+
// Filter and normalize candidate list.
|
|
113
|
+
for (let i = candidates.length - 1; i >= 0; i--) {
|
|
114
|
+
// Remove any empty entries
|
|
115
|
+
if (!candidates[i]?.trim()) {
|
|
116
|
+
candidates.splice(i, 1);
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
// Resolve candidate path
|
|
120
|
+
candidates[i] = path.resolve(candidates[i]);
|
|
121
|
+
const candidate = candidates[i];
|
|
122
|
+
// Check that the path actually points to something
|
|
123
|
+
if (!fs.existsSync(candidate)) {
|
|
124
|
+
candidates.splice(i, 1);
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
// Check that the destination is a file
|
|
128
|
+
const stat = fs.lstatSync(candidate);
|
|
129
|
+
if (!stat.isFile()) {
|
|
130
|
+
candidates.splice(i, 1);
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
// Return list of candidates without duplicates
|
|
135
|
+
return [...new Set(candidates)];
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Runs the Steam executable with the given command-line arguments.
|
|
139
|
+
* Can be used to start Steam, or to start a Steam game by using "-applaunch".
|
|
140
|
+
*
|
|
141
|
+
* TODO: Replace with manual game launch, hooking virtual file system.
|
|
142
|
+
*
|
|
143
|
+
* @param args Command-line arguments for Steam.
|
|
144
|
+
*/
|
|
145
|
+
static async launchSteam(args = []) {
|
|
146
|
+
// Get list of canidate Steam executables
|
|
147
|
+
// TODO: Use Steamworks for this somehow?
|
|
148
|
+
const steamExes = await SteamClient.findSteam();
|
|
149
|
+
// Try each executable until one works, i.e. doesn't fail before `timeout`
|
|
150
|
+
const timeout = 5000;
|
|
151
|
+
for (const steam of steamExes) {
|
|
152
|
+
const error = await Promise.all([
|
|
153
|
+
new Promise(resolve => {
|
|
154
|
+
execFile(steam, args, (error) => {
|
|
155
|
+
resolve(error);
|
|
156
|
+
});
|
|
157
|
+
}),
|
|
158
|
+
new Promise(resolve => setTimeout(resolve, timeout, false))
|
|
159
|
+
]);
|
|
160
|
+
if (!error)
|
|
161
|
+
break;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
export { SteamClient };
|
|
166
|
+
//# sourceMappingURL=SteamTools.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SteamTools.js","sourceRoot":"","sources":["../../src/SteamTools.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AACxD,OAAO,MAAM,MAAM,SAAS,CAAC;AAO7B,MAAM,WAAW;IAEf,gBAAgB,CAAgB;IAChC,KAAK,CAAS;IAEd;;;;OAIG;IACH,YAAa,QAAgB,GAAG;QAC9B,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;QACnB,yFAAyF;QACzF,mDAAmD;QACnD,KAAK,GAAG,GAAG,CAAC;QACZ,4EAA4E;QAC5E,mEAAmE;QACnE,EAAE,CAAC,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,iBAAiB,CAAC,EAAE,KAAK,CAAC,QAAQ,EAAE,CAAC,CAAC;QAChF,IAAI,CAAC,gBAAgB,GAAG,aAAa,CAAC,WAAW,EAAE,CAAC;QACpD,MAAM,OAAO,GAAG,IAAI,CAAC,gBAAgB,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC;QAC7D,IAAI,CAAC,OAAO;YAAE,MAAM,gCAAgC,CAAC;IACvD,CAAC;IAED;;OAEG;IACH,UAAU;QACR,MAAM,UAAU,GAAG,IAAI,CAAC,gBAAgB,CAAC,IAAI,CAAC,gBAAgB,CAAC,IAAI,CAAC,KAAK,CAAW,CAAC;QACrF,OAAO,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;IAClC,CAAC;IAED;;OAEG;IACH,cAAc;QACZ,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,CAAC,gBAAgB,CAAC,SAAS,EAAE,CAAC,OAAO,CAAC,CAAC;QAClE,MAAM,IAAI,GAAG,IAAI,CAAC,gBAAgB,CAAC,OAAO,CAAC,cAAc,EAAE,CAAC;QAC5D,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;IAC3B,CAAC;IAED;;;OAGG;IACH,eAAe;QACb,OAAO,IAAI,CAAC,gBAAgB,CAAC,IAAI,CAAC,YAAY,EAAE,CAAC;IACnD,CAAC;IAED;;;;;;OAMG;IACH,MAAM,CAAC,KAAK,CAAC,SAAS;QAEpB,MAAM,UAAU,GAAa,EAAE,CAAC;QAEhC;;;;;WAKG;QACH,MAAM,WAAW,GAAG,MAAM,MAAM,EAAE,CAAC;QACnC,kEAAkE;QAClE,6DAA6D;QAC7D,WAAW,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC;QACtE,0CAA0C;QAC1C,MAAM,cAAc,GAAG,WAAW,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAC5C,CAAC,CAAC,IAAI,KAAK,OAAO;YAClB,CAAC,CAAC,IAAI,KAAK,WAAW,CACvB,CAAC;QAEF,iCAAiC;QAEjC;;;;;WAKG;QACH,IAAI,CAAC;YACH,IAAI,aAAa,GAAa,EAAE,CAAC;YACjC,mEAAmE;YACnE,IAAI,OAAO,CAAC,QAAQ,KAAK,OAAO,EAAE,CAAC;gBACjC,MAAM,MAAM,GAAG,QAAQ,CAAC,+BAA+B,CAAC,CAAC;gBACzD,MAAM,OAAO,GAAG,MAAM,CAAC,QAAQ,EAAE,CAAC,KAAK,CAAC,aAAa,CAAC,CAAC;gBACvD,IAAI,OAAO;oBAAE,aAAa,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;YAChF,CAAC;iBAAM,CAAC;gBACN,MAAM,MAAM,GAAG,QAAQ,CAAC,mBAAmB,CAAC,CAAC;gBAC7C,MAAM,MAAM,GAAG,MAAM,CAAC,QAAQ,EAAE,CAAC;gBACjC,aAAa,GAAG,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;YACjD,CAAC;YACD,+CAA+C;YAC/C,MAAM,kBAAkB,GAAG,WAAW,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,aAAa,CAAC,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;YAClF,cAAc,CAAC,IAAI,CAAC,GAAG,kBAAkB,CAAC,CAAC;QAC7C,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC,CAAC,CAAC;QAEf,wCAAwC;QACxC,KAAK,MAAM,OAAO,IAAI,cAAc,EAAE,CAAC;YACrC,IAAI,OAAO,CAAC,IAAI;gBAAE,UAAU,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;iBAC3C,IAAI,OAAO,CAAC,GAAG;gBAAE,UAAU,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAE,CAAC,CAAC;QACpE,CAAC;QAED;;;;;WAKG;QACH,UAAU,CAAC,IAAI,CACb,2CAA2C,EAC3C,GAAG,EAAE,CAAC,OAAO,EAAE,iCAAiC,EAChD,GAAG,EAAE,CAAC,OAAO,EAAE,uCAAuC,EACtD,GAAG,EAAE,CAAC,OAAO,EAAE,wEAAwE,CACxF,CAAC;QAEF,uCAAuC;QACvC,KAAK,IAAI,CAAC,GAAG,UAAU,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;YAChD,2BAA2B;YAC3B,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,EAAE,CAAC;gBAC3B,UAAU,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;gBACxB,SAAS;YACX,CAAC;YACD,yBAAyB;YACzB,UAAU,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC,CAAE,CAAC,CAAC;YAC7C,MAAM,SAAS,GAAG,UAAU,CAAC,CAAC,CAAE,CAAC;YACjC,mDAAmD;YACnD,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;gBAC9B,UAAU,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;gBACxB,SAAS;YACX,CAAC;YACD,uCAAuC;YACvC,MAAM,IAAI,GAAG,EAAE,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC;YACrC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,CAAC;gBACnB,UAAU,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;gBACxB,SAAS;YACX,CAAC;QACH,CAAC;QAED,+CAA+C;QAC/C,OAAO,CAAC,GAAG,IAAI,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC;IAElC,CAAC;IAED;;;;;;;OAOG;IACH,MAAM,CAAC,KAAK,CAAC,WAAW,CAAE,OAAiB,EAAE;QAE3C,yCAAyC;QACzC,yCAAyC;QACzC,MAAM,SAAS,GAAG,MAAM,WAAW,CAAC,SAAS,EAAE,CAAC;QAEhD,0EAA0E;QAC1E,MAAM,OAAO,GAAG,IAAI,CAAC;QACrB,KAAK,MAAM,KAAK,IAAI,SAAS,EAAE,CAAC;YAC9B,MAAM,KAAK,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC;gBAC9B,IAAI,OAAO,CAAC,OAAO,CAAC,EAAE;oBACpB,QAAQ,CAAC,KAAK,EAAE,IAAI,EAAE,CAAC,KAAK,EAAE,EAAE;wBAC9B,OAAO,CAAC,KAAK,CAAC,CAAC;oBACjB,CAAC,CAAC,CAAC;gBACL,CAAC,CAAC;gBACF,IAAI,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,OAAO,EAAE,KAAK,CAAC,CAAC;aAC5D,CAAC,CAAC;YACH,IAAI,CAAC,KAAK;gBAAE,MAAM;QACpB,CAAC;IAEH,CAAC;CAEF;AAED,OAAO,EAEL,WAAW,EACZ,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"extract.test.d.ts","sourceRoot":"","sources":["../../test/extract.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { expect, test } from "bun:test";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import * as Spplice from "../index";
|
|
4
|
+
test("repository package extraction", async () => {
|
|
5
|
+
const repo = await Spplice.Repository.fromURL("https://p2r3.github.io/spplice-repo/index.json");
|
|
6
|
+
const pkg = Array.from(repo.packages)[0];
|
|
7
|
+
await pkg.extract(".tmp");
|
|
8
|
+
expect(await Bun.file(".tmp/scripts/vscripts/mapspawn.nut").exists()).toBeTrue();
|
|
9
|
+
await fs.rm(".tmp", { recursive: true, force: true });
|
|
10
|
+
});
|
|
11
|
+
//# sourceMappingURL=extract.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"extract.test.js","sourceRoot":"","sources":["../../test/extract.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,UAAU,CAAC;AACxC,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAClC,OAAO,KAAK,OAAO,MAAM,UAAU,CAAC;AAEpC,IAAI,CAAC,+BAA+B,EAAE,KAAK,IAAI,EAAE;IAC/C,MAAM,IAAI,GAAG,MAAM,OAAO,CAAC,UAAU,CAAC,OAAO,CAAC,gDAAgD,CAAC,CAAC;IAChG,MAAM,GAAG,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAE,CAAC;IAC1C,MAAM,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;IAC1B,MAAM,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,oCAAoC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,QAAQ,EAAE,CAAC;IACjF,MAAM,EAAE,CAAC,EAAE,CAAC,MAAM,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;AACxD,CAAC,CAAC,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,21 +1,34 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spplice-api",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"module": "index.ts",
|
|
3
|
+
"version": "0.2.0",
|
|
5
4
|
"type": "module",
|
|
6
5
|
"private": false,
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist"
|
|
16
|
+
],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "tsc"
|
|
19
|
+
},
|
|
7
20
|
"devDependencies": {
|
|
8
|
-
"@types/bun": "latest"
|
|
21
|
+
"@types/bun": "latest",
|
|
22
|
+
"typescript": "^5"
|
|
9
23
|
},
|
|
10
24
|
"peerDependencies": {
|
|
11
|
-
"
|
|
25
|
+
"steamworks-ffi-node": "^0.10.3"
|
|
12
26
|
},
|
|
13
27
|
"dependencies": {
|
|
14
28
|
"@types/lzma-native": "^4.0.4",
|
|
15
29
|
"@types/tar-stream": "^3.1.4",
|
|
16
30
|
"lzma-native": "^8.0.6",
|
|
17
31
|
"ps-list": "^9.0.0",
|
|
18
|
-
"steamworks-ffi-node": "^0.10.3",
|
|
19
32
|
"tar-stream": "^3.2.0"
|
|
20
33
|
},
|
|
21
34
|
"trustedDependencies": [
|
package/index.ts
DELETED
package/src/ConsoleTools.ts
DELETED
|
@@ -1,120 +0,0 @@
|
|
|
1
|
-
import net from "net";
|
|
2
|
-
|
|
3
|
-
export class Console {
|
|
4
|
-
|
|
5
|
-
// TCP socket handle
|
|
6
|
-
socket: net.Socket;
|
|
7
|
-
// Network port number
|
|
8
|
-
port: number;
|
|
9
|
-
// Last connection error, or null if none
|
|
10
|
-
error: Error | null = null;
|
|
11
|
-
// Connection state - true if connected, false otherwise
|
|
12
|
-
connected: boolean = false;
|
|
13
|
-
|
|
14
|
-
// Whether we expect the socket to close intentionally.
|
|
15
|
-
// Determines whether a repeat connection is attempted.
|
|
16
|
-
private closing: boolean = false;
|
|
17
|
-
// Stores unflushed writes, re-sent if the socket reconnects.
|
|
18
|
-
private writeBuffer: string = "";
|
|
19
|
-
// Stores output read from the socket, but not yet read by the client.
|
|
20
|
-
private readBuffer: string = "";
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Creates and configures TCP socket for Portal 2 netcon.
|
|
24
|
-
* @param port Network port number for connection.
|
|
25
|
-
*/
|
|
26
|
-
constructor (port: number) {
|
|
27
|
-
this.socket = new net.Socket();
|
|
28
|
-
this.socket.setNoDelay(true);
|
|
29
|
-
this.socket.setKeepAlive(false);
|
|
30
|
-
this.port = port;
|
|
31
|
-
// Listen for incoming data and log to read buffer
|
|
32
|
-
this.socket.on("data", data => {
|
|
33
|
-
this.readBuffer += data;
|
|
34
|
-
});
|
|
35
|
-
// Clear pending write buffer when data gets flushed
|
|
36
|
-
this.socket.on("drain", () => {
|
|
37
|
-
this.writeBuffer = "";
|
|
38
|
-
});
|
|
39
|
-
// Reconnect if the socket ever closes unexpectedly
|
|
40
|
-
this.socket.on("close", () => {
|
|
41
|
-
this.connected = false;
|
|
42
|
-
if (!this.closing) {
|
|
43
|
-
setTimeout(() => this.connect(), 200);
|
|
44
|
-
}
|
|
45
|
-
});
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Connects to the Portal 2 console TCP socket.
|
|
50
|
-
*/
|
|
51
|
-
async connect () {
|
|
52
|
-
// Attempt connection to the socket
|
|
53
|
-
this.error = await new Promise(resolve => {
|
|
54
|
-
let onConnect: any, onError: any;
|
|
55
|
-
this.socket.once("error", onError = (err: Error) => {
|
|
56
|
-
this.socket.off("connect", onConnect);
|
|
57
|
-
resolve(err);
|
|
58
|
-
});
|
|
59
|
-
this.socket.once("connect", onConnect = () => {
|
|
60
|
-
this.socket.off("error", onError);
|
|
61
|
-
resolve(null);
|
|
62
|
-
});
|
|
63
|
-
this.socket.connect(this.port, "127.0.0.1");
|
|
64
|
-
});
|
|
65
|
-
if (!this.error) this.connected = true;
|
|
66
|
-
// Re-send anything that remains in the write buffer from the last session
|
|
67
|
-
if (this.writeBuffer) {
|
|
68
|
-
const success = this.socket.write(this.writeBuffer);
|
|
69
|
-
if (success) this.writeBuffer = "";
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* Disconnects from the Portal 2 console socket.
|
|
75
|
-
*/
|
|
76
|
-
disconnect () {
|
|
77
|
-
this.closing = true;
|
|
78
|
-
this.writeBuffer = "";
|
|
79
|
-
this.readBuffer = "";
|
|
80
|
-
this.socket.end();
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Sends a console command to the game.
|
|
85
|
-
* @param command Command to execute.
|
|
86
|
-
*/
|
|
87
|
-
send (command: string) {
|
|
88
|
-
const data = command + "\n";
|
|
89
|
-
const success = this.socket.write(data);
|
|
90
|
-
if (!success) this.writeBuffer += data;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* Reads output from the game console.
|
|
95
|
-
* @param length Max amount of bytes to read. Default is -1 (read all).
|
|
96
|
-
*/
|
|
97
|
-
read (length: number = -1): string {
|
|
98
|
-
let output: string;
|
|
99
|
-
if (length < 0) {
|
|
100
|
-
output = this.readBuffer;
|
|
101
|
-
this.readBuffer = "";
|
|
102
|
-
} else {
|
|
103
|
-
output = this.readBuffer.slice(0, length);
|
|
104
|
-
this.readBuffer = this.readBuffer.slice(length);
|
|
105
|
-
}
|
|
106
|
-
return output;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* Reads and returns one full line of console output. Does not read
|
|
111
|
-
* incomplete lines. Removes newline and carriage return characters.
|
|
112
|
-
* @returns One full line of console output, or `null` if no output.
|
|
113
|
-
*/
|
|
114
|
-
readLine (): string | null {
|
|
115
|
-
const newlineIndex = this.readBuffer.indexOf("\n");
|
|
116
|
-
if (newlineIndex === -1) return null;
|
|
117
|
-
return this.read(newlineIndex + 1).slice(0, -1).replaceAll("\r", "");
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
}
|
package/src/PackageTools.ts
DELETED
|
@@ -1,170 +0,0 @@
|
|
|
1
|
-
import { Readable } from "node:stream";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import fs from "node:fs";
|
|
4
|
-
import * as tar from "tar-stream";
|
|
5
|
-
import * as lzma from "lzma-native";
|
|
6
|
-
|
|
7
|
-
class SpplicePackage {
|
|
8
|
-
|
|
9
|
-
title: string;
|
|
10
|
-
author: string;
|
|
11
|
-
description: string = "";
|
|
12
|
-
version: string = "1.0.0";
|
|
13
|
-
args: string[] = [];
|
|
14
|
-
file: string;
|
|
15
|
-
icon: string;
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* @param foreignPackage Untrusted "foreign" object (e.g. from an online repo),
|
|
19
|
-
* assumed to be shaped roughly like a Spplice package.
|
|
20
|
-
*/
|
|
21
|
-
constructor (foreignPackage: any) {
|
|
22
|
-
// Title is required
|
|
23
|
-
this.title = foreignPackage.title.toString().trim();
|
|
24
|
-
if (!this.title) throw "Invalid package title";
|
|
25
|
-
// Author is required
|
|
26
|
-
this.author = foreignPackage.author.toString().trim();
|
|
27
|
-
if (!this.author) throw "Invalid package author";
|
|
28
|
-
// Description is optional
|
|
29
|
-
if (typeof foreignPackage.description === "string") {
|
|
30
|
-
this.description = foreignPackage.description.trim();
|
|
31
|
-
}
|
|
32
|
-
// File URL is required
|
|
33
|
-
if (typeof foreignPackage.file !== "string") throw "Invalid package file URL";
|
|
34
|
-
this.file = foreignPackage.file;
|
|
35
|
-
// Icon URL is required
|
|
36
|
-
if (typeof foreignPackage.icon !== "string") throw "Invalid package icon URL";
|
|
37
|
-
this.icon = foreignPackage.icon;
|
|
38
|
-
// Version is optional
|
|
39
|
-
if (typeof foreignPackage.version === "string") {
|
|
40
|
-
this.version = foreignPackage.version;
|
|
41
|
-
}
|
|
42
|
-
// Command line arguments are optional
|
|
43
|
-
if (Array.isArray(foreignPackage.args)) {
|
|
44
|
-
this.args = foreignPackage.args;
|
|
45
|
-
} else if (typeof foreignPackage.args === "string") {
|
|
46
|
-
this.args = [foreignPackage.args];
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Extracts a readable stream of a tar.xz archive to the given folder.
|
|
52
|
-
* @param inputStream Readable stream of input tar.xz file.
|
|
53
|
-
* @param destinationPath Path to directory in which to extract the archive.
|
|
54
|
-
*/
|
|
55
|
-
static async extractFromStream (inputStream: Readable, destinationPath: string){
|
|
56
|
-
return await new Promise((resolve, reject) => {
|
|
57
|
-
try {
|
|
58
|
-
const lzmaDecompressor = lzma.createDecompressor();
|
|
59
|
-
const tarExtractor = tar.extract();
|
|
60
|
-
tarExtractor.on("entry", (header, stream, next) => {
|
|
61
|
-
// Get just entry name and type from header.
|
|
62
|
-
// It's probably best not to replicate permissions/users/groups.
|
|
63
|
-
const { name, type } = header;
|
|
64
|
-
const fullPath = path.join(destinationPath, name);
|
|
65
|
-
// Set up handler to continue when this entry has been parsed
|
|
66
|
-
stream.on("end", () => next());
|
|
67
|
-
// For files, pipe them to a write stream.
|
|
68
|
-
// For directories, quietly create them recursively.
|
|
69
|
-
if (type === "file") {
|
|
70
|
-
stream.pipe(fs.createWriteStream(fullPath));
|
|
71
|
-
} else if (type === "directory") {
|
|
72
|
-
fs.mkdirSync(fullPath, { recursive: true });
|
|
73
|
-
}
|
|
74
|
-
// Drain the rest of the stream if anything remains
|
|
75
|
-
stream.resume();
|
|
76
|
-
});
|
|
77
|
-
// Resolve promise once archive has been extracted
|
|
78
|
-
tarExtractor.on("finish", resolve);
|
|
79
|
-
// Pipe the input file - first decompress, then extract
|
|
80
|
-
inputStream.pipe(lzmaDecompressor).pipe(tarExtractor);
|
|
81
|
-
} catch (err) {
|
|
82
|
-
reject(err);
|
|
83
|
-
}
|
|
84
|
-
});
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Extracts a tar.xz archive file to the given folder.
|
|
89
|
-
* @param filePath Input tar.xz archive path.
|
|
90
|
-
* @param destinationPath Path to directory in which to extract the archive.
|
|
91
|
-
*/
|
|
92
|
-
static async extractFromFile (filePath: string, destinationPath: string) {
|
|
93
|
-
const inputStream = fs.createReadStream(filePath);
|
|
94
|
-
return await SpplicePackage.extractFromStream(inputStream, destinationPath);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* Downloads a tar.xz archive from a URL and extracts it to the given folder.
|
|
99
|
-
* @param fileURL Input tar.xz archive URL.
|
|
100
|
-
* @param destinationPath Path to directory in which to extract the archive.
|
|
101
|
-
*/
|
|
102
|
-
static async extractFromURL (fileURL: string, destinationPath: string) {
|
|
103
|
-
const response = await fetch(fileURL);
|
|
104
|
-
if (response.status !== 200) throw `Server returned response ${response.status}`;
|
|
105
|
-
if (!response.body) throw `Server did not return any content.`;
|
|
106
|
-
const inputStream = Readable.fromWeb(response.body);
|
|
107
|
-
return await SpplicePackage.extractFromStream(inputStream, destinationPath);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* Downloads and extracts the package's file archive to the given folder.
|
|
112
|
-
* @param destinationPath Path to directory in which to extract the archive.
|
|
113
|
-
*/
|
|
114
|
-
async extract (destinationPath: string) {
|
|
115
|
-
return await SpplicePackage.extractFromURL(this.file, destinationPath);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
class SppliceRepository {
|
|
121
|
-
|
|
122
|
-
packages: Set<SpplicePackage> = new Set();
|
|
123
|
-
|
|
124
|
-
addPackage (newPackage: SpplicePackage) {
|
|
125
|
-
this.packages.add(newPackage);
|
|
126
|
-
}
|
|
127
|
-
removePackage (removedPackage: SpplicePackage) {
|
|
128
|
-
this.packages.delete(removedPackage);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* Fetches a Spplice repository from its URL.
|
|
133
|
-
* @param repositoryURL Link to Spplice repository index (JSON).
|
|
134
|
-
* @param strict Whether to throw an error when package parsing fails.
|
|
135
|
-
* @returns A new Spplice repository.
|
|
136
|
-
*/
|
|
137
|
-
static async fromURL (repositoryURL: string, strict: boolean = false): Promise<SppliceRepository> {
|
|
138
|
-
|
|
139
|
-
// Get JSON data from input URL
|
|
140
|
-
const response = await fetch(repositoryURL.trim());
|
|
141
|
-
if (response.status !== 200) throw `Server returned response ${response.status}`;
|
|
142
|
-
const repositoryData = (await response.json()) as any;
|
|
143
|
-
|
|
144
|
-
// Validate repository structure
|
|
145
|
-
if (!Array.isArray(repositoryData.packages)) {
|
|
146
|
-
throw "Malformed repository index.";
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
const repository = new SppliceRepository();
|
|
150
|
-
|
|
151
|
-
// Try to convert each entry to a Spplice package, skip any that fail
|
|
152
|
-
for (const entry of repositoryData.packages) {
|
|
153
|
-
try {
|
|
154
|
-
const newPackage = new SpplicePackage(entry);
|
|
155
|
-
repository.addPackage(newPackage);
|
|
156
|
-
} catch (err) {
|
|
157
|
-
if (strict) throw err;
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
return repository;
|
|
162
|
-
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
export {
|
|
168
|
-
SpplicePackage,
|
|
169
|
-
SppliceRepository
|
|
170
|
-
};
|