@idajs/create-mod 0.2.10 → 0.2.15-dev.19
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/_project.config/AGENTS.md +25 -0
- package/_project.config/CLAUDE.md +1 -0
- package/_project.config/extensions.json +1 -3
- package/_project.config/package.template.json +9 -9
- package/_project.config/project.js +120 -0
- package/_project.config/remote.js +171 -0
- package/_project.config/run-remote.js +75 -0
- package/_project.config/settings.json +2 -2
- package/_project.config/start.js +60 -0
- package/_project.config/sync.js +21 -60
- package/_project.config/watch.js +90 -24
- package/index.js +55 -18
- package/install.js +6 -0
- package/package.json +1 -1
|
@@ -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
|
|
@@ -1,14 +1,14 @@
|
|
|
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 %npm_package_name%",
|
|
10
|
-
"update:types": "npm update @idajs/types"
|
|
11
|
-
},
|
|
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 %npm_package_name%",
|
|
10
|
+
"update:types": "npm update @idajs/types"
|
|
11
|
+
},
|
|
12
12
|
"devDependencies": {
|
|
13
13
|
"archiver": "^7.0.1",
|
|
14
14
|
"chokidar-cli": "^3.0.0",
|
|
@@ -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,171 @@
|
|
|
1
|
+
const archiver = require("archiver");
|
|
2
|
+
const fs = require("fs");
|
|
3
|
+
const http = require("http");
|
|
4
|
+
const https = require("https");
|
|
5
|
+
const path = require("path");
|
|
6
|
+
const { spawn } = require("child_process");
|
|
7
|
+
|
|
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 new Promise((resolve, reject) => {
|
|
31
|
+
const output = fs.createWriteStream(zipPath);
|
|
32
|
+
const archive = archiver("zip", {
|
|
33
|
+
store: true,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
output.on("close", resolve);
|
|
37
|
+
output.on("error", reject);
|
|
38
|
+
archive.on("error", reject);
|
|
39
|
+
archive.on("warning", (error) => {
|
|
40
|
+
if (error.code !== "ENOENT") {
|
|
41
|
+
reject(error);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
archive.pipe(output);
|
|
46
|
+
archive.directory(sourceDir, rootName);
|
|
47
|
+
archive.finalize();
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function request(server, method, requestPath, body, headers = {}) {
|
|
52
|
+
return new Promise((resolve, reject) => {
|
|
53
|
+
const client = server.origin.startsWith("https://") ? https : http;
|
|
54
|
+
const options = {
|
|
55
|
+
method,
|
|
56
|
+
hostname: server.host,
|
|
57
|
+
port: server.port,
|
|
58
|
+
path: requestPath,
|
|
59
|
+
headers,
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const req = client.request(options, (res) => {
|
|
63
|
+
const chunks = [];
|
|
64
|
+
res.on("data", (chunk) => chunks.push(chunk));
|
|
65
|
+
res.on("end", () => {
|
|
66
|
+
const responseBody = Buffer.concat(chunks).toString("utf8");
|
|
67
|
+
|
|
68
|
+
if (res.statusCode >= 200 && res.statusCode < 300) {
|
|
69
|
+
if (!responseBody) {
|
|
70
|
+
resolve(null);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
resolve(JSON.parse(responseBody));
|
|
76
|
+
} catch (error) {
|
|
77
|
+
resolve(responseBody);
|
|
78
|
+
}
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
reject(
|
|
83
|
+
new Error(
|
|
84
|
+
`${method} ${requestPath} failed with ${res.statusCode}: ${responseBody || "no body"}`
|
|
85
|
+
)
|
|
86
|
+
);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
req.on("error", (error) => {
|
|
91
|
+
if (error.code === "EHOSTUNREACH" || error.code === "ECONNREFUSED" || error.code === "ETIMEDOUT") {
|
|
92
|
+
reject(
|
|
93
|
+
new Error(
|
|
94
|
+
`Cannot reach remote Ida listener at ${server.host}:${server.port}. ` +
|
|
95
|
+
`Start it on the Windows machine with 'npm run listen' in the Ida folder and verify the host/port is reachable. ` +
|
|
96
|
+
`Original error: ${error.code}`
|
|
97
|
+
)
|
|
98
|
+
);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
reject(error);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
if (body) {
|
|
106
|
+
req.write(body);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
req.end();
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function stageMod(targetRoot) {
|
|
114
|
+
const args = [];
|
|
115
|
+
if (targetRoot) {
|
|
116
|
+
args.push("--target-root", targetRoot);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
await runNodeScript("sync.js", args);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function stageAndZip(targetRoot) {
|
|
123
|
+
const modName = getPackageName();
|
|
124
|
+
const stagedModDir = path.join(targetRoot, modName);
|
|
125
|
+
const zipPath = path.join(targetRoot, `${modName}.zip`);
|
|
126
|
+
|
|
127
|
+
await stageMod(targetRoot);
|
|
128
|
+
if (fs.existsSync(zipPath)) {
|
|
129
|
+
fs.rmSync(zipPath, { force: true });
|
|
130
|
+
}
|
|
131
|
+
await createZip(zipPath, stagedModDir, modName);
|
|
132
|
+
|
|
133
|
+
return { modName, stagedModDir, zipPath };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async function uploadMod(server, modName, zipPath) {
|
|
137
|
+
const body = fs.readFileSync(zipPath);
|
|
138
|
+
return request(server, "POST", `/sync?modName=${encodeURIComponent(modName)}`, body, {
|
|
139
|
+
"Content-Type": "application/zip",
|
|
140
|
+
"Content-Length": Buffer.byteLength(body),
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async function startRemoteGame(server, modName) {
|
|
145
|
+
return request(
|
|
146
|
+
server,
|
|
147
|
+
"POST",
|
|
148
|
+
"/game/start",
|
|
149
|
+
Buffer.from(JSON.stringify({ modName }), "utf8"),
|
|
150
|
+
{
|
|
151
|
+
"Content-Type": "application/json",
|
|
152
|
+
}
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async function killRemoteGame(server) {
|
|
157
|
+
return request(server, "POST", "/game/kill");
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function getRemoteGameStatus(server) {
|
|
161
|
+
return request(server, "GET", "/game/status");
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
module.exports = {
|
|
165
|
+
getRemoteGameStatus,
|
|
166
|
+
killRemoteGame,
|
|
167
|
+
stageMod,
|
|
168
|
+
stageAndZip,
|
|
169
|
+
startRemoteGame,
|
|
170
|
+
uploadMod,
|
|
171
|
+
};
|
|
@@ -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
|
+
});
|
|
@@ -7,14 +7,14 @@
|
|
|
7
7
|
"*.ts": "typescript"
|
|
8
8
|
},
|
|
9
9
|
"[javascript]": {
|
|
10
|
-
"editor.defaultFormatter": "
|
|
10
|
+
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
|
11
11
|
"editor.tabSize": 2,
|
|
12
12
|
"editor.wordWrap": "wordWrapColumn",
|
|
13
13
|
"editor.wordWrapColumn": 100,
|
|
14
14
|
"editor.rulers": [100]
|
|
15
15
|
},
|
|
16
16
|
"[typescript]": {
|
|
17
|
-
"editor.defaultFormatter": "
|
|
17
|
+
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
|
18
18
|
"editor.tabSize": 2,
|
|
19
19
|
"editor.wordWrap": "wordWrapColumn",
|
|
20
20
|
"editor.wordWrapColumn": 100,
|
|
@@ -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
|
+
}
|
package/_project.config/sync.js
CHANGED
|
@@ -1,88 +1,50 @@
|
|
|
1
1
|
const { execSync } = require("child_process");
|
|
2
2
|
const path = require("path");
|
|
3
3
|
const fs = require("fs");
|
|
4
|
-
const os = require("os");
|
|
5
4
|
|
|
6
|
-
|
|
7
|
-
function getIdaJsPath() {
|
|
8
|
-
// Try local config first, then user home config
|
|
9
|
-
const configPaths = [path.join(__dirname, ".idajs.json"), path.join(os.homedir(), ".idajs.json")];
|
|
5
|
+
const { getArgValue, getIdaJsPath, getPackageName, isTypeScriptProject } = require("./project");
|
|
10
6
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
15
|
-
if (config.installDir) {
|
|
16
|
-
return config.installDir;
|
|
17
|
-
}
|
|
18
|
-
} catch (err) {
|
|
19
|
-
// Ignore parse errors and try next config
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
return null;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
// Get package name from command line argument
|
|
28
|
-
const packageName = process.argv[2];
|
|
29
|
-
|
|
30
|
-
if (!packageName) {
|
|
31
|
-
console.error("Error: Package name is required as an argument");
|
|
32
|
-
console.error("Usage: node sync.js <package-name>");
|
|
33
|
-
process.exit(1);
|
|
34
|
-
}
|
|
7
|
+
const args = process.argv.slice(2);
|
|
8
|
+
const packageName = getPackageName();
|
|
9
|
+
const targetRoot = getArgValue("--target-root", args);
|
|
35
10
|
|
|
36
|
-
|
|
37
|
-
const
|
|
38
|
-
if (!
|
|
39
|
-
|
|
40
|
-
console.error("Please build IdaJS first or create .idajs.json with 'installDir' property.");
|
|
41
|
-
process.exit(1);
|
|
42
|
-
}
|
|
11
|
+
let resolvedTargetRoot = targetRoot;
|
|
12
|
+
const typeScriptProject = isTypeScriptProject();
|
|
13
|
+
if (!resolvedTargetRoot) {
|
|
14
|
+
const idaJsPath = getIdaJsPath();
|
|
43
15
|
|
|
44
|
-
if (!
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const isTypeScriptProject = (() => {
|
|
51
|
-
// Check if tsconfig.json exists
|
|
52
|
-
if (!fs.existsSync("tsconfig.json")) {
|
|
53
|
-
return false;
|
|
16
|
+
if (!idaJsPath) {
|
|
17
|
+
console.error("Error: .idajs.json not found in current directory or user home directory.");
|
|
18
|
+
console.error("Please build IdaJS first or create .idajs.json with 'installDir' property.");
|
|
19
|
+
process.exit(1);
|
|
54
20
|
}
|
|
55
21
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
22
|
+
if (!fs.existsSync(idaJsPath)) {
|
|
23
|
+
console.error(`Error: IdaJS installation directory does not exist: ${idaJsPath}`);
|
|
24
|
+
console.error("Please build IdaJS first.");
|
|
25
|
+
process.exit(1);
|
|
59
26
|
}
|
|
60
27
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
})();
|
|
28
|
+
resolvedTargetRoot = path.join(idaJsPath, "GameRun", "mods");
|
|
29
|
+
}
|
|
64
30
|
|
|
65
|
-
const targetBase = path.join(
|
|
31
|
+
const targetBase = path.join(resolvedTargetRoot, packageName);
|
|
66
32
|
const deleteExcludeArgs = '--delete-exclude "*.ida" --delete-exclude "*.md5"';
|
|
67
33
|
|
|
68
34
|
console.log(`Syncing ${packageName}...`);
|
|
69
35
|
|
|
70
|
-
|
|
71
|
-
if (isTypeScriptProject) {
|
|
36
|
+
if (typeScriptProject) {
|
|
72
37
|
console.log("TypeScript project detected, compiling...");
|
|
73
38
|
|
|
74
|
-
// Clean dist folder
|
|
75
39
|
if (fs.existsSync("dist")) {
|
|
76
40
|
console.log("Cleaning dist folder...");
|
|
77
41
|
fs.rmSync("dist", { recursive: true, force: true });
|
|
78
42
|
}
|
|
79
43
|
|
|
80
|
-
// Compile TypeScript
|
|
81
44
|
execSync("tsc", { stdio: "inherit" });
|
|
82
45
|
console.log("Compilation complete!");
|
|
83
46
|
}
|
|
84
47
|
|
|
85
|
-
// Sync media folder if it exists
|
|
86
48
|
if (fs.existsSync("media")) {
|
|
87
49
|
const mediaSyncCmd = `idasync media "${path.join(targetBase, "media")}" ${deleteExcludeArgs}`;
|
|
88
50
|
console.log(`Running: ${mediaSyncCmd}`);
|
|
@@ -91,8 +53,7 @@ if (fs.existsSync("media")) {
|
|
|
91
53
|
console.log("No media folder found, skipping media sync");
|
|
92
54
|
}
|
|
93
55
|
|
|
94
|
-
|
|
95
|
-
const sourceFolder = isTypeScriptProject ? "dist" : "src";
|
|
56
|
+
const sourceFolder = typeScriptProject ? "dist" : "src";
|
|
96
57
|
const srcSyncCmd = `idasync "${sourceFolder}" "${targetBase}" ${deleteExcludeArgs} --delete-exclude "media/*"`;
|
|
97
58
|
console.log(`Running: ${srcSyncCmd}`);
|
|
98
59
|
execSync(srcSyncCmd, { stdio: "inherit" });
|
package/_project.config/watch.js
CHANGED
|
@@ -1,54 +1,121 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Watches for file changes in the src/ and media/ directories
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* Watches for file changes in the src/ and media/ directories.
|
|
3
|
+
* Local mode syncs directly into the game mod directory.
|
|
4
|
+
* Remote mode stages the mod into a session temp folder and uploads a full zip.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
const chokidar = require("chokidar");
|
|
8
|
-
const {
|
|
8
|
+
const { exec } = require("child_process");
|
|
9
9
|
const { promisify } = require("util");
|
|
10
10
|
|
|
11
|
+
const { getArgValue, parseServerAddress } = require("./project");
|
|
12
|
+
const { getRemoteGameStatus, killRemoteGame, stageAndZip, stageMod, uploadMod } = require("./remote");
|
|
13
|
+
|
|
11
14
|
const execAsync = promisify(exec);
|
|
12
15
|
const PROC_NAME = "LBA2.exe";
|
|
16
|
+
const args = process.argv.slice(2);
|
|
17
|
+
const serverArg = getArgValue("--server", args);
|
|
18
|
+
const sessionRoot = getArgValue("--session-root", args);
|
|
19
|
+
const server = serverArg ? parseServerAddress(serverArg) : null;
|
|
20
|
+
|
|
21
|
+
if (server && !sessionRoot) {
|
|
22
|
+
console.error("Remote watch mode requires --session-root <path>.");
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let syncing = false;
|
|
27
|
+
let syncQueued = false;
|
|
13
28
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
29
|
+
async function runLocalSync() {
|
|
30
|
+
if (syncing) {
|
|
31
|
+
syncQueued = true;
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
syncing = true;
|
|
36
|
+
do {
|
|
37
|
+
syncQueued = false;
|
|
38
|
+
try {
|
|
39
|
+
await stageMod();
|
|
40
|
+
} catch (error) {
|
|
41
|
+
console.error(error.message);
|
|
42
|
+
}
|
|
43
|
+
} while (syncQueued);
|
|
44
|
+
syncing = false;
|
|
19
45
|
}
|
|
20
46
|
|
|
21
|
-
|
|
47
|
+
async function runRemoteSync() {
|
|
48
|
+
if (syncing) {
|
|
49
|
+
syncQueued = true;
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
syncing = true;
|
|
54
|
+
do {
|
|
55
|
+
syncQueued = false;
|
|
56
|
+
try {
|
|
57
|
+
const { modName, zipPath } = await stageAndZip(sessionRoot);
|
|
58
|
+
await uploadMod(server, modName, zipPath);
|
|
59
|
+
} catch (error) {
|
|
60
|
+
console.error(error.message);
|
|
61
|
+
}
|
|
62
|
+
} while (syncQueued);
|
|
63
|
+
syncing = false;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const watcher = chokidar.watch(["src/**/*.js", "src/**/*.ts", "media/**/*.png"], {
|
|
22
67
|
ignoreInitial: true,
|
|
23
68
|
});
|
|
24
69
|
|
|
25
|
-
watcher.on("all", () =>
|
|
70
|
+
watcher.on("all", async () => {
|
|
71
|
+
if (server) {
|
|
72
|
+
await runRemoteSync();
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
26
75
|
|
|
27
|
-
|
|
76
|
+
await runLocalSync();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
async function isLocalProcRunning() {
|
|
28
80
|
try {
|
|
29
81
|
const { stdout } = await execAsync(
|
|
30
|
-
`
|
|
82
|
+
`tasklist /FI "IMAGENAME eq ${PROC_NAME}" /FO CSV /NH`,
|
|
31
83
|
{
|
|
32
84
|
timeout: 5000,
|
|
33
85
|
}
|
|
34
86
|
);
|
|
35
|
-
return stdout.
|
|
87
|
+
return stdout.toLowerCase().includes(`"${PROC_NAME.toLowerCase()}"`);
|
|
36
88
|
} catch (error) {
|
|
37
|
-
// Process not found or command failed
|
|
38
89
|
return false;
|
|
39
90
|
}
|
|
40
91
|
}
|
|
41
92
|
|
|
42
|
-
async function
|
|
93
|
+
async function killLocalGameProc() {
|
|
43
94
|
try {
|
|
44
|
-
await execAsync(
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
} catch (e) {
|
|
48
|
-
// Process may already be stopped
|
|
95
|
+
await execAsync(`taskkill /IM "${PROC_NAME}" /F /T`);
|
|
96
|
+
} catch (error) {
|
|
97
|
+
// Process may already be stopped.
|
|
49
98
|
}
|
|
50
99
|
}
|
|
51
100
|
|
|
101
|
+
async function isProcRunning() {
|
|
102
|
+
if (server) {
|
|
103
|
+
const status = await getRemoteGameStatus(server);
|
|
104
|
+
return Boolean(status && status.running);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return isLocalProcRunning();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function killGameProc() {
|
|
111
|
+
if (server) {
|
|
112
|
+
await killRemoteGame(server);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
await killLocalGameProc();
|
|
117
|
+
}
|
|
118
|
+
|
|
52
119
|
const interval = setInterval(async () => {
|
|
53
120
|
try {
|
|
54
121
|
const alive = await isProcRunning();
|
|
@@ -59,12 +126,11 @@ const interval = setInterval(async () => {
|
|
|
59
126
|
await watcher.close();
|
|
60
127
|
process.exit(0);
|
|
61
128
|
}
|
|
62
|
-
} catch (
|
|
63
|
-
|
|
129
|
+
} catch (error) {
|
|
130
|
+
console.error(error.message);
|
|
64
131
|
}
|
|
65
132
|
}, 1000);
|
|
66
133
|
|
|
67
|
-
// Ctrl+C cleanup
|
|
68
134
|
process.on("SIGINT", async () => {
|
|
69
135
|
console.log("Caught interrupt signal. Exiting watcher.");
|
|
70
136
|
|
package/index.js
CHANGED
|
@@ -70,6 +70,37 @@ function getIdaJsDirFromConfig() {
|
|
|
70
70
|
return null;
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
function normalizeServerAddress(value) {
|
|
74
|
+
const trimmed = String(value || "").trim();
|
|
75
|
+
const input = trimmed.includes("://") ? trimmed : `http://${trimmed}`;
|
|
76
|
+
const url = new URL(input);
|
|
77
|
+
return `${url.hostname}:${url.port || 7770}`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function parseIdaConnection(value) {
|
|
81
|
+
const trimmed = String(value || "").trim();
|
|
82
|
+
|
|
83
|
+
if (!trimmed) {
|
|
84
|
+
throw new Error("IdaJS installation directory or host[:port] is required");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (fs.existsSync(trimmed)) {
|
|
88
|
+
return {
|
|
89
|
+
installDir: trimmed,
|
|
90
|
+
server: null,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (trimmed.includes("/") || trimmed.includes("\\") || /^[A-Za-z]:/.test(trimmed)) {
|
|
95
|
+
throw new Error(`Directory does not exist: ${trimmed}`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
installDir: null,
|
|
100
|
+
server: normalizeServerAddress(trimmed),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
73
104
|
async function main() {
|
|
74
105
|
console.log("Welcome to IdaJS Mod Creator!\n");
|
|
75
106
|
|
|
@@ -124,16 +155,15 @@ async function main() {
|
|
|
124
155
|
if (!config.idajsDir) {
|
|
125
156
|
questions.push({
|
|
126
157
|
type: "text",
|
|
127
|
-
name: "
|
|
128
|
-
message: "IdaJS installation directory:",
|
|
158
|
+
name: "idajsConnection",
|
|
159
|
+
message: "IdaJS installation directory or host[:port]:",
|
|
129
160
|
validate: (value) => {
|
|
130
|
-
|
|
131
|
-
|
|
161
|
+
try {
|
|
162
|
+
parseIdaConnection(value);
|
|
163
|
+
return true;
|
|
164
|
+
} catch (error) {
|
|
165
|
+
return error.message;
|
|
132
166
|
}
|
|
133
|
-
if (!fs.existsSync(value)) {
|
|
134
|
-
return `Directory does not exist: ${value}`;
|
|
135
|
-
}
|
|
136
|
-
return true;
|
|
137
167
|
},
|
|
138
168
|
});
|
|
139
169
|
}
|
|
@@ -159,6 +189,13 @@ async function main() {
|
|
|
159
189
|
});
|
|
160
190
|
|
|
161
191
|
config = { ...config, ...response };
|
|
192
|
+
|
|
193
|
+
if (response.idajsConnection) {
|
|
194
|
+
const parsedConnection = parseIdaConnection(response.idajsConnection);
|
|
195
|
+
config.idajsDir = parsedConnection.installDir;
|
|
196
|
+
config.server = parsedConnection.server;
|
|
197
|
+
delete config.idajsConnection;
|
|
198
|
+
}
|
|
162
199
|
}
|
|
163
200
|
|
|
164
201
|
// Set target directory to project name if not specified
|
|
@@ -171,7 +208,7 @@ async function main() {
|
|
|
171
208
|
: path.join(process.cwd(), config.targetDirectory);
|
|
172
209
|
|
|
173
210
|
// Validate IdaJS directory exists
|
|
174
|
-
if (!fs.existsSync(config.idajsDir)) {
|
|
211
|
+
if (config.idajsDir && !fs.existsSync(config.idajsDir)) {
|
|
175
212
|
console.error(`\n❌ Error: IdaJS installation directory does not exist: ${config.idajsDir}`);
|
|
176
213
|
process.exit(1);
|
|
177
214
|
}
|
|
@@ -184,7 +221,7 @@ async function main() {
|
|
|
184
221
|
|
|
185
222
|
console.log(`\nCreating IdaJS mod in ${targetDir}...`);
|
|
186
223
|
console.log(`Language: ${config.language === "js" ? "JavaScript" : "TypeScript"}`);
|
|
187
|
-
console.log(`IdaJS: ${config.idajsDir}\n`);
|
|
224
|
+
console.log(`IdaJS: ${config.idajsDir || config.server}\n`);
|
|
188
225
|
|
|
189
226
|
try {
|
|
190
227
|
// Create project directory
|
|
@@ -218,14 +255,14 @@ async function main() {
|
|
|
218
255
|
fs.writeFileSync(packageTemplatePath, JSON.stringify(packageTemplate, null, 2) + "\n");
|
|
219
256
|
console.log("✓ Updated package.template.json");
|
|
220
257
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
installDir
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
);
|
|
258
|
+
const idajsConfig = {};
|
|
259
|
+
if (config.idajsDir) {
|
|
260
|
+
idajsConfig.installDir = config.idajsDir;
|
|
261
|
+
}
|
|
262
|
+
if (config.server) {
|
|
263
|
+
idajsConfig.server = config.server;
|
|
264
|
+
}
|
|
265
|
+
fs.writeFileSync(path.join(targetDir, ".idajs.json"), JSON.stringify(idajsConfig, null, 2) + "\n");
|
|
229
266
|
console.log("✓ Created .idajs.json");
|
|
230
267
|
|
|
231
268
|
// Call Samples/install.js to set up the project
|
package/install.js
CHANGED
|
@@ -141,10 +141,16 @@ function mergePackageJson() {
|
|
|
141
141
|
// Step 2: Copy necessary files
|
|
142
142
|
function copyFiles() {
|
|
143
143
|
const filesToCopy = [
|
|
144
|
+
{ source: "project.js" },
|
|
145
|
+
{ source: "remote.js" },
|
|
144
146
|
{ source: "run.ps1" },
|
|
147
|
+
{ source: "run-remote.js" },
|
|
148
|
+
{ source: "start.js" },
|
|
145
149
|
{ source: "watch.js" },
|
|
146
150
|
{ source: "sync.js" },
|
|
147
151
|
{ source: "build.js" },
|
|
152
|
+
{ source: "AGENTS.md" },
|
|
153
|
+
{ source: "CLAUDE.md" },
|
|
148
154
|
{ source: isTypeScriptProject ? "tsconfig.json" : "jsconfig.json" },
|
|
149
155
|
{ source: "settings.json", target: ".vscode/settings.json" },
|
|
150
156
|
{ source: "extensions.json", target: ".vscode/extensions.json" },
|
package/package.json
CHANGED