@idajs/create-mod 0.2.11 → 0.2.15-dev.37
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/README.md +6 -0
- package/_project.config/AGENTS.md +25 -0
- package/_project.config/CLAUDE.md +1 -0
- package/_project.config/archive.js +107 -0
- package/_project.config/build.js +22 -37
- package/_project.config/package.template.json +14 -14
- package/_project.config/project.js +120 -0
- package/_project.config/remote.js +157 -0
- package/_project.config/run-remote.js +75 -0
- package/_project.config/start.js +60 -0
- package/_project.config/sync.js +21 -60
- package/_project.config/watch.js +90 -24
- package/index.js +145 -19
- package/install.js +160 -45
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -24,6 +24,12 @@ npx @idajs/create-mod
|
|
|
24
24
|
|
|
25
25
|
The tool will guide you through the setup process with interactive prompts.
|
|
26
26
|
|
|
27
|
+
Refresh scaffolder-managed files in an existing mod project:
|
|
28
|
+
|
|
29
|
+
```pwsh
|
|
30
|
+
npx @idajs/create-mod --update
|
|
31
|
+
```
|
|
32
|
+
|
|
27
33
|
## Usage
|
|
28
34
|
|
|
29
35
|
```pwsh
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# AGENTS.md
|
|
2
|
+
|
|
3
|
+
## What is IdaJS
|
|
4
|
+
|
|
5
|
+
IdaJS is a JavaScript-powered modding engine for Little Big Adventure 2 (LBA2). It lets you create mods for the classic LBA2 game using JavaScript or TypeScript — modifying scenes, characters, behaviors, dialogs, and more through a modern scripting API.
|
|
6
|
+
|
|
7
|
+
## API Reference
|
|
8
|
+
|
|
9
|
+
The full IdaJS TypeScript API with JSDoc documentation is available in `./node_modules/@idajs/types/`, structured by entities. The global objects (such as `scene`, `ida`, `text`, `object`, etc.) are defined in `global.d.ts`. Always consult these type definitions when implementing any feature.
|
|
10
|
+
|
|
11
|
+
## Mod Execution Phases
|
|
12
|
+
|
|
13
|
+
Different phases of mod execution allow different APIs. Be aware which phase you are in:
|
|
14
|
+
|
|
15
|
+
- **Scene setup phase** (`scene.Events.afterLoadScene` handler): Configure the scene before gameplay starts. Use `scene`, `object`, `text`, zone and waypoint APIs to add/modify objects, zones, waypoints, register life script handlers, register coroutines, and set initial state. Do **not** call life or move script commands here.
|
|
16
|
+
- **Life script handler** (`handleLifeScript`): Runs every frame per actor. Use `ida.life()` and `ida.lifef()` for life script commands/functions (conditions, dialogs, state changes). Can start/pause/stop coroutines from here.
|
|
17
|
+
- **Coroutine (move script)** (generator function registered via `registerCoroutine`): Runs across multiple frames. Use `yield doMove()` for move commands (animations, movement, timing) and other `yield do...()` helpers (`doSceneStore`, `doGameStore`, etc.). Cannot check game conditions directly — that logic belongs in life scripts.
|
|
18
|
+
|
|
19
|
+
## Mod Examples
|
|
20
|
+
|
|
21
|
+
For examples of working mods, look at `Ida/Samples` in the IdaJS installation folder. The path to the installation folder is written in a JSON file either in the user's home directory: `~/.idajs.json` or in the root directory of this project.
|
|
22
|
+
|
|
23
|
+
## Web Documentation
|
|
24
|
+
|
|
25
|
+
Full readme, API reference, and guides are available at: https://ida.innerbytes.com
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@AGENTS.md
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const yazl = require("yazl");
|
|
4
|
+
|
|
5
|
+
function normalizeZipPath(value) {
|
|
6
|
+
return String(value || "")
|
|
7
|
+
.split(path.sep)
|
|
8
|
+
.join("/")
|
|
9
|
+
.replace(/\/+/g, "/")
|
|
10
|
+
.replace(/^\/+/, "")
|
|
11
|
+
.replace(/\/+$/, "");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async function collectDirectoryEntries(sourceDir, zipRoot) {
|
|
15
|
+
const dirents = await fs.promises.readdir(sourceDir, { withFileTypes: true });
|
|
16
|
+
const sortedDirents = dirents.slice().sort((a, b) => a.name.localeCompare(b.name));
|
|
17
|
+
const entries = [];
|
|
18
|
+
|
|
19
|
+
if (sortedDirents.length === 0 && zipRoot) {
|
|
20
|
+
entries.push({
|
|
21
|
+
type: "directory",
|
|
22
|
+
metadataPath: normalizeZipPath(zipRoot),
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
for (const dirent of sortedDirents) {
|
|
27
|
+
const sourcePath = path.join(sourceDir, dirent.name);
|
|
28
|
+
const metadataPath = normalizeZipPath(path.posix.join(zipRoot, dirent.name));
|
|
29
|
+
|
|
30
|
+
if (dirent.isDirectory()) {
|
|
31
|
+
entries.push(...(await collectDirectoryEntries(sourcePath, metadataPath)));
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (dirent.isFile()) {
|
|
36
|
+
entries.push({
|
|
37
|
+
type: "file",
|
|
38
|
+
sourcePath,
|
|
39
|
+
metadataPath,
|
|
40
|
+
});
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
throw new Error(`Unsupported entry type in archive source: ${sourcePath}`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return entries;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function createZipFromDirectories(zipPath, mappings, options = {}) {
|
|
51
|
+
const zipfile = new yazl.ZipFile();
|
|
52
|
+
const output = fs.createWriteStream(zipPath);
|
|
53
|
+
const zipOptions = {};
|
|
54
|
+
|
|
55
|
+
if (typeof options.compressionLevel === "number") {
|
|
56
|
+
zipOptions.compressionLevel = options.compressionLevel;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const collectedEntries = [];
|
|
60
|
+
for (const mapping of mappings) {
|
|
61
|
+
if (!fs.existsSync(mapping.sourceDir)) {
|
|
62
|
+
throw new Error(`Source directory not found: ${mapping.sourceDir}`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
collectedEntries.push(...(await collectDirectoryEntries(mapping.sourceDir, mapping.zipRoot)));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return new Promise((resolve, reject) => {
|
|
69
|
+
let settled = false;
|
|
70
|
+
|
|
71
|
+
function finish(error) {
|
|
72
|
+
if (settled) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
settled = true;
|
|
76
|
+
if (error) {
|
|
77
|
+
reject(error);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
resolve();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
output.on("close", () => finish());
|
|
84
|
+
output.on("error", finish);
|
|
85
|
+
zipfile.outputStream.on("error", finish);
|
|
86
|
+
|
|
87
|
+
zipfile.outputStream.pipe(output);
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
for (const entry of collectedEntries) {
|
|
91
|
+
if (entry.type === "directory") {
|
|
92
|
+
zipfile.addEmptyDirectory(entry.metadataPath);
|
|
93
|
+
} else {
|
|
94
|
+
zipfile.addFile(entry.sourcePath, entry.metadataPath, zipOptions);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
zipfile.end();
|
|
99
|
+
} catch (error) {
|
|
100
|
+
finish(error);
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
module.exports = {
|
|
106
|
+
createZipFromDirectories,
|
|
107
|
+
};
|
package/_project.config/build.js
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
const { execSync } = require("child_process");
|
|
2
2
|
const path = require("path");
|
|
3
3
|
const fs = require("fs");
|
|
4
|
-
const
|
|
4
|
+
const { createZipFromDirectories } = require("./archive");
|
|
5
|
+
const { getPackageName } = require("./project");
|
|
5
6
|
|
|
6
7
|
// Get mod name from command line argument
|
|
7
|
-
const modName = process.argv[2];
|
|
8
|
+
const modName = process.argv[2] || getPackageName();
|
|
8
9
|
|
|
9
10
|
if (!modName) {
|
|
10
11
|
console.error("Error: Mod name is required as an argument");
|
|
11
|
-
console.error("Usage: node build.js
|
|
12
|
+
console.error("Usage: node build.js [mod-name]");
|
|
12
13
|
process.exit(1);
|
|
13
14
|
}
|
|
14
15
|
|
|
@@ -61,47 +62,31 @@ if (!fs.existsSync(buildFolder)) {
|
|
|
61
62
|
|
|
62
63
|
// Create zip file
|
|
63
64
|
const zipPath = path.join(buildFolder, `${modName}.zip`);
|
|
64
|
-
const output = fs.createWriteStream(zipPath);
|
|
65
|
-
const archive = archiver("zip", {
|
|
66
|
-
zlib: { level: 9 }, // Maximum compression
|
|
67
|
-
});
|
|
68
65
|
|
|
69
|
-
|
|
66
|
+
async function main() {
|
|
67
|
+
console.log(`Creating ${zipPath}...`);
|
|
70
68
|
|
|
71
|
-
|
|
72
|
-
output.on("close", () => {
|
|
73
|
-
const sizeInKB = (archive.pointer() / 1024).toFixed(2);
|
|
74
|
-
console.log(`✓ Build complete: ${zipPath} (${sizeInKB} KB)`);
|
|
75
|
-
});
|
|
69
|
+
const mappings = [{ sourceDir: sourceFolder, zipRoot: modName }];
|
|
76
70
|
|
|
77
|
-
|
|
78
|
-
console.error("Error creating archive:", err);
|
|
79
|
-
process.exit(1);
|
|
80
|
-
});
|
|
71
|
+
console.log(`Adding ${sourceFolder}/ → ${modName}/`);
|
|
81
72
|
|
|
82
|
-
|
|
83
|
-
if (
|
|
84
|
-
console.
|
|
73
|
+
const mediaFolder = "media";
|
|
74
|
+
if (fs.existsSync(mediaFolder)) {
|
|
75
|
+
console.log(`Adding ${mediaFolder}/ → ${modName}/media/`);
|
|
76
|
+
mappings.push({ sourceDir: mediaFolder, zipRoot: `${modName}/media` });
|
|
85
77
|
} else {
|
|
86
|
-
|
|
78
|
+
console.log("No media folder found, skipping");
|
|
87
79
|
}
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
// Pipe archive data to the file
|
|
91
|
-
archive.pipe(output);
|
|
92
80
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
81
|
+
await createZipFromDirectories(zipPath, mappings, {
|
|
82
|
+
compressionLevel: 9,
|
|
83
|
+
});
|
|
96
84
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
if (fs.existsSync(mediaFolder)) {
|
|
100
|
-
console.log(`Adding ${mediaFolder}/ → ${modName}/media/`);
|
|
101
|
-
archive.directory(mediaFolder, `${modName}/media`);
|
|
102
|
-
} else {
|
|
103
|
-
console.log("No media folder found, skipping");
|
|
85
|
+
const sizeInKB = (fs.statSync(zipPath).size / 1024).toFixed(2);
|
|
86
|
+
console.log(`✓ Build complete: ${zipPath} (${sizeInKB} KB)`);
|
|
104
87
|
}
|
|
105
88
|
|
|
106
|
-
|
|
107
|
-
|
|
89
|
+
main().catch((error) => {
|
|
90
|
+
console.error("Error creating archive:", error);
|
|
91
|
+
process.exit(1);
|
|
92
|
+
});
|
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"author": "Andriy Tevelyev",
|
|
3
|
-
"private": true,
|
|
4
|
-
"scripts": {
|
|
5
|
-
"setup": "node ../install.js",
|
|
6
|
-
"watch": "node watch.js",
|
|
7
|
-
"sync": "node sync.js
|
|
8
|
-
"start": "
|
|
9
|
-
"build": "node build.js
|
|
10
|
-
"update:types": "npm update @idajs/types"
|
|
11
|
-
},
|
|
12
|
-
"devDependencies": {
|
|
13
|
-
"
|
|
14
|
-
"chokidar-cli": "^3.0.0",
|
|
15
|
-
"@idajs/types": "^0.2.0",
|
|
16
|
-
"@idajs/sync": "^1.1.0",
|
|
3
|
+
"private": true,
|
|
4
|
+
"scripts": {
|
|
5
|
+
"setup": "node ../install.js",
|
|
6
|
+
"watch": "node watch.js",
|
|
7
|
+
"sync": "node sync.js",
|
|
8
|
+
"start": "node start.js",
|
|
9
|
+
"build": "node build.js",
|
|
10
|
+
"update:types": "npm update @idajs/types"
|
|
11
|
+
},
|
|
12
|
+
"devDependencies": {
|
|
13
|
+
"yazl": "^3.3.1",
|
|
14
|
+
"chokidar-cli": "^3.0.0",
|
|
15
|
+
"@idajs/types": "^0.2.0",
|
|
16
|
+
"@idajs/sync": "^1.1.0",
|
|
17
17
|
"typescript": "^5.9.3"
|
|
18
18
|
}
|
|
19
19
|
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const os = require("os");
|
|
3
|
+
const path = require("path");
|
|
4
|
+
|
|
5
|
+
const DEFAULT_PORT = 7770;
|
|
6
|
+
|
|
7
|
+
function getArgs(argv = process.argv.slice(2)) {
|
|
8
|
+
return argv;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function getArgValue(name, argv = getArgs()) {
|
|
12
|
+
const index = argv.findIndex((arg) => arg === name || arg.startsWith(`${name}=`));
|
|
13
|
+
if (index === -1) {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const arg = argv[index];
|
|
18
|
+
if (arg.includes("=")) {
|
|
19
|
+
return arg.split("=").slice(1).join("=");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return argv[index + 1] || null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function getPackageName(projectDir = process.cwd()) {
|
|
26
|
+
const packagePath = path.join(projectDir, "package.json");
|
|
27
|
+
|
|
28
|
+
if (!fs.existsSync(packagePath)) {
|
|
29
|
+
throw new Error(`package.json not found in ${projectDir}`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const packageJson = JSON.parse(fs.readFileSync(packagePath, "utf8"));
|
|
33
|
+
if (!packageJson.name) {
|
|
34
|
+
throw new Error("package.json is missing the 'name' field");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return packageJson.name;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function getIdaJsPath(projectDir = process.cwd()) {
|
|
41
|
+
const configPaths = [path.join(projectDir, ".idajs.json"), path.join(os.homedir(), ".idajs.json")];
|
|
42
|
+
|
|
43
|
+
for (const configPath of configPaths) {
|
|
44
|
+
if (!fs.existsSync(configPath)) {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
50
|
+
if (config.installDir) {
|
|
51
|
+
return config.installDir;
|
|
52
|
+
}
|
|
53
|
+
} catch (error) {
|
|
54
|
+
// Ignore parse failures and keep looking.
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function getIdaJsServer(projectDir = process.cwd()) {
|
|
62
|
+
const configPaths = [path.join(projectDir, ".idajs.json"), path.join(os.homedir(), ".idajs.json")];
|
|
63
|
+
|
|
64
|
+
for (const configPath of configPaths) {
|
|
65
|
+
if (!fs.existsSync(configPath)) {
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
71
|
+
if (config.server) {
|
|
72
|
+
return config.server;
|
|
73
|
+
}
|
|
74
|
+
} catch (error) {
|
|
75
|
+
// Ignore parse failures and keep looking.
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function isTypeScriptProject(projectDir = process.cwd()) {
|
|
83
|
+
const tsConfigPath = path.join(projectDir, "tsconfig.json");
|
|
84
|
+
const srcDir = path.join(projectDir, "src");
|
|
85
|
+
|
|
86
|
+
if (!fs.existsSync(tsConfigPath) || !fs.existsSync(srcDir)) {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const files = fs.readdirSync(srcDir);
|
|
91
|
+
return files.some((file) => file.endsWith(".ts"));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function parseServerAddress(serverInput) {
|
|
95
|
+
if (!serverInput) {
|
|
96
|
+
throw new Error("Server address is required. Use --server <host[:port]>.");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const trimmedInput = String(serverInput).trim();
|
|
100
|
+
const withProtocol = trimmedInput.includes("://") ? trimmedInput : `http://${trimmedInput}`;
|
|
101
|
+
const url = new URL(withProtocol);
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
host: url.hostname,
|
|
105
|
+
port: Number(url.port || DEFAULT_PORT),
|
|
106
|
+
origin: `${url.protocol}//${url.hostname}:${url.port || DEFAULT_PORT}`,
|
|
107
|
+
value: `${url.hostname}:${url.port || DEFAULT_PORT}`,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
module.exports = {
|
|
112
|
+
DEFAULT_PORT,
|
|
113
|
+
getArgValue,
|
|
114
|
+
getArgs,
|
|
115
|
+
getIdaJsPath,
|
|
116
|
+
getIdaJsServer,
|
|
117
|
+
getPackageName,
|
|
118
|
+
isTypeScriptProject,
|
|
119
|
+
parseServerAddress,
|
|
120
|
+
};
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const http = require("http");
|
|
3
|
+
const https = require("https");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const { spawn } = require("child_process");
|
|
6
|
+
|
|
7
|
+
const { createZipFromDirectories } = require("./archive");
|
|
8
|
+
const { getPackageName } = require("./project");
|
|
9
|
+
|
|
10
|
+
function runNodeScript(scriptName, args = []) {
|
|
11
|
+
return new Promise((resolve, reject) => {
|
|
12
|
+
const child = spawn(process.execPath, [path.join(__dirname, scriptName), ...args], {
|
|
13
|
+
cwd: process.cwd(),
|
|
14
|
+
stdio: "inherit",
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
child.on("error", reject);
|
|
18
|
+
child.on("exit", (code) => {
|
|
19
|
+
if (code === 0) {
|
|
20
|
+
resolve();
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
reject(new Error(`${scriptName} exited with code ${code}`));
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function createZip(zipPath, sourceDir, rootName) {
|
|
30
|
+
return createZipFromDirectories(
|
|
31
|
+
zipPath,
|
|
32
|
+
[{ sourceDir, zipRoot: rootName }],
|
|
33
|
+
{ compressionLevel: 0 }
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function request(server, method, requestPath, body, headers = {}) {
|
|
38
|
+
return new Promise((resolve, reject) => {
|
|
39
|
+
const client = server.origin.startsWith("https://") ? https : http;
|
|
40
|
+
const options = {
|
|
41
|
+
method,
|
|
42
|
+
hostname: server.host,
|
|
43
|
+
port: server.port,
|
|
44
|
+
path: requestPath,
|
|
45
|
+
headers,
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const req = client.request(options, (res) => {
|
|
49
|
+
const chunks = [];
|
|
50
|
+
res.on("data", (chunk) => chunks.push(chunk));
|
|
51
|
+
res.on("end", () => {
|
|
52
|
+
const responseBody = Buffer.concat(chunks).toString("utf8");
|
|
53
|
+
|
|
54
|
+
if (res.statusCode >= 200 && res.statusCode < 300) {
|
|
55
|
+
if (!responseBody) {
|
|
56
|
+
resolve(null);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
resolve(JSON.parse(responseBody));
|
|
62
|
+
} catch (error) {
|
|
63
|
+
resolve(responseBody);
|
|
64
|
+
}
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
reject(
|
|
69
|
+
new Error(
|
|
70
|
+
`${method} ${requestPath} failed with ${res.statusCode}: ${responseBody || "no body"}`
|
|
71
|
+
)
|
|
72
|
+
);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
req.on("error", (error) => {
|
|
77
|
+
if (error.code === "EHOSTUNREACH" || error.code === "ECONNREFUSED" || error.code === "ETIMEDOUT") {
|
|
78
|
+
reject(
|
|
79
|
+
new Error(
|
|
80
|
+
`Cannot reach remote Ida listener at ${server.host}:${server.port}. ` +
|
|
81
|
+
`Start it on the Windows machine with 'npm run listen' in the Ida folder and verify the host/port is reachable. ` +
|
|
82
|
+
`Original error: ${error.code}`
|
|
83
|
+
)
|
|
84
|
+
);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
reject(error);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
if (body) {
|
|
92
|
+
req.write(body);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
req.end();
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function stageMod(targetRoot) {
|
|
100
|
+
const args = [];
|
|
101
|
+
if (targetRoot) {
|
|
102
|
+
args.push("--target-root", targetRoot);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
await runNodeScript("sync.js", args);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function stageAndZip(targetRoot) {
|
|
109
|
+
const modName = getPackageName();
|
|
110
|
+
const stagedModDir = path.join(targetRoot, modName);
|
|
111
|
+
const zipPath = path.join(targetRoot, `${modName}.zip`);
|
|
112
|
+
|
|
113
|
+
await stageMod(targetRoot);
|
|
114
|
+
if (fs.existsSync(zipPath)) {
|
|
115
|
+
fs.rmSync(zipPath, { force: true });
|
|
116
|
+
}
|
|
117
|
+
await createZip(zipPath, stagedModDir, modName);
|
|
118
|
+
|
|
119
|
+
return { modName, stagedModDir, zipPath };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function uploadMod(server, modName, zipPath) {
|
|
123
|
+
const body = fs.readFileSync(zipPath);
|
|
124
|
+
return request(server, "POST", `/sync?modName=${encodeURIComponent(modName)}`, body, {
|
|
125
|
+
"Content-Type": "application/zip",
|
|
126
|
+
"Content-Length": Buffer.byteLength(body),
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function startRemoteGame(server, modName) {
|
|
131
|
+
return request(
|
|
132
|
+
server,
|
|
133
|
+
"POST",
|
|
134
|
+
"/game/start",
|
|
135
|
+
Buffer.from(JSON.stringify({ modName }), "utf8"),
|
|
136
|
+
{
|
|
137
|
+
"Content-Type": "application/json",
|
|
138
|
+
}
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async function killRemoteGame(server) {
|
|
143
|
+
return request(server, "POST", "/game/kill");
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async function getRemoteGameStatus(server) {
|
|
147
|
+
return request(server, "GET", "/game/status");
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
module.exports = {
|
|
151
|
+
getRemoteGameStatus,
|
|
152
|
+
killRemoteGame,
|
|
153
|
+
stageMod,
|
|
154
|
+
stageAndZip,
|
|
155
|
+
startRemoteGame,
|
|
156
|
+
uploadMod,
|
|
157
|
+
};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const os = require("os");
|
|
3
|
+
const path = require("path");
|
|
4
|
+
const { spawn } = require("child_process");
|
|
5
|
+
|
|
6
|
+
const { getArgValue, parseServerAddress } = require("./project");
|
|
7
|
+
const { getRemoteGameStatus, stageAndZip, startRemoteGame, uploadMod } = require("./remote");
|
|
8
|
+
|
|
9
|
+
async function main() {
|
|
10
|
+
const args = process.argv.slice(2);
|
|
11
|
+
const server = parseServerAddress(getArgValue("--server", args));
|
|
12
|
+
const sessionRoot = fs.mkdtempSync(path.join(os.tmpdir(), "idajs-remote-"));
|
|
13
|
+
let cleanedUp = false;
|
|
14
|
+
|
|
15
|
+
const cleanup = () => {
|
|
16
|
+
if (cleanedUp) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
cleanedUp = true;
|
|
21
|
+
fs.rmSync(sessionRoot, { recursive: true, force: true });
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
console.log(`Checking remote Ida listener at ${server.origin}...`);
|
|
26
|
+
await getRemoteGameStatus(server);
|
|
27
|
+
|
|
28
|
+
console.log(`Using remote staging directory: ${sessionRoot}`);
|
|
29
|
+
const { modName, zipPath } = await stageAndZip(sessionRoot);
|
|
30
|
+
await uploadMod(server, modName, zipPath);
|
|
31
|
+
await startRemoteGame(server, modName);
|
|
32
|
+
|
|
33
|
+
const child = spawn(
|
|
34
|
+
process.execPath,
|
|
35
|
+
[
|
|
36
|
+
path.join(__dirname, "watch.js"),
|
|
37
|
+
"--server",
|
|
38
|
+
server.value,
|
|
39
|
+
"--session-root",
|
|
40
|
+
sessionRoot,
|
|
41
|
+
],
|
|
42
|
+
{
|
|
43
|
+
cwd: process.cwd(),
|
|
44
|
+
stdio: "inherit",
|
|
45
|
+
}
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
child.on("exit", (code) => {
|
|
49
|
+
cleanup();
|
|
50
|
+
process.exit(code ?? 0);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
child.on("error", (error) => {
|
|
54
|
+
cleanup();
|
|
55
|
+
console.error(error.message);
|
|
56
|
+
process.exit(1);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
process.on("SIGINT", () => {
|
|
60
|
+
child.kill("SIGINT");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
process.on("SIGTERM", () => {
|
|
64
|
+
child.kill("SIGTERM");
|
|
65
|
+
});
|
|
66
|
+
} catch (error) {
|
|
67
|
+
cleanup();
|
|
68
|
+
throw error;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
main().catch((error) => {
|
|
73
|
+
console.error(error.message);
|
|
74
|
+
process.exit(1);
|
|
75
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
const path = require("path");
|
|
2
|
+
const { execFileSync, spawn } = require("child_process");
|
|
3
|
+
|
|
4
|
+
const { getArgValue, getIdaJsPath, getIdaJsServer, getPackageName } = require("./project");
|
|
5
|
+
|
|
6
|
+
function runRemote(args) {
|
|
7
|
+
const child = spawn(process.execPath, [path.join(__dirname, "run-remote.js"), ...args], {
|
|
8
|
+
cwd: process.cwd(),
|
|
9
|
+
stdio: "inherit",
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
child.on("exit", (code) => {
|
|
13
|
+
process.exit(code ?? 0);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
child.on("error", (error) => {
|
|
17
|
+
console.error(error.message);
|
|
18
|
+
process.exit(1);
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function runLocal() {
|
|
23
|
+
try {
|
|
24
|
+
execFileSync(
|
|
25
|
+
"powershell",
|
|
26
|
+
[
|
|
27
|
+
"-ExecutionPolicy",
|
|
28
|
+
"Bypass",
|
|
29
|
+
"-File",
|
|
30
|
+
path.join(__dirname, "run.ps1"),
|
|
31
|
+
getPackageName(),
|
|
32
|
+
],
|
|
33
|
+
{
|
|
34
|
+
cwd: process.cwd(),
|
|
35
|
+
stdio: "inherit",
|
|
36
|
+
}
|
|
37
|
+
);
|
|
38
|
+
process.exit(0);
|
|
39
|
+
} catch (error) {
|
|
40
|
+
if (typeof error.status === "number") {
|
|
41
|
+
process.exit(error.status);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
console.error(error.message);
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const args = process.argv.slice(2);
|
|
50
|
+
const explicitServer = getArgValue("--server", args);
|
|
51
|
+
const installDir = getIdaJsPath();
|
|
52
|
+
const configuredServer = getIdaJsServer();
|
|
53
|
+
|
|
54
|
+
if (explicitServer) {
|
|
55
|
+
runRemote(args);
|
|
56
|
+
} else if (!installDir && configuredServer) {
|
|
57
|
+
runRemote(["--server", configuredServer]);
|
|
58
|
+
} else {
|
|
59
|
+
runLocal();
|
|
60
|
+
}
|