@bf6mods/cli 1.1.1 → 1.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/README.md +1 -0
- package/dist/cli/index.d.ts +1 -1
- package/dist/cli/index.js +665 -781
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +1 -2
- package/dist/index.js +1 -1
- package/dist/resources/templates/All/package.json +3 -2
- package/package.json +6 -5
- package/dist/index.js.map +0 -1
package/dist/cli/index.js
CHANGED
|
@@ -1,853 +1,737 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
// src/cli/index.ts
|
|
4
2
|
import { Command } from "commander";
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
description: "CLI and library for bundling BF6 mods",
|
|
11
|
-
license: "MIT",
|
|
12
|
-
type: "module",
|
|
13
|
-
bin: {
|
|
14
|
-
bf6mods: "./dist/cli/index.js"
|
|
15
|
-
},
|
|
16
|
-
main: "./dist/index.js",
|
|
17
|
-
types: "./dist/index.d.ts",
|
|
18
|
-
scripts: {
|
|
19
|
-
build: "tsup",
|
|
20
|
-
start: "cross-env EXIT_CODE=none tsx ./src/cli/index.ts"
|
|
21
|
-
},
|
|
22
|
-
exports: {
|
|
23
|
-
".": {
|
|
24
|
-
types: "./dist/index.d.ts",
|
|
25
|
-
import: "./dist/index.js"
|
|
26
|
-
}
|
|
27
|
-
},
|
|
28
|
-
files: [
|
|
29
|
-
"dist/"
|
|
30
|
-
],
|
|
31
|
-
repository: {
|
|
32
|
-
type: "git",
|
|
33
|
-
url: "git+https://github.com/bf6mods/bf6mods.git"
|
|
34
|
-
},
|
|
35
|
-
dependencies: {
|
|
36
|
-
"@bf6mods/sdk": "1.0.3",
|
|
37
|
-
"@clack/prompts": "^0.11.0",
|
|
38
|
-
chokidar: "^4.0.3",
|
|
39
|
-
colors: "^1.4.0",
|
|
40
|
-
commander: "^14.0.1",
|
|
41
|
-
jiti: "^2.6.1",
|
|
42
|
-
knitwork: "^1.2.0",
|
|
43
|
-
rolldown: "^1.0.0-beta.43",
|
|
44
|
-
"stringify-object": "^6.0.0"
|
|
45
|
-
},
|
|
46
|
-
publishConfig: {
|
|
47
|
-
access: "public"
|
|
48
|
-
},
|
|
49
|
-
devDependencies: {
|
|
50
|
-
"@oxc-project/types": "^0.95.0",
|
|
51
|
-
"@types/stringify-object": "^4.0.5",
|
|
52
|
-
"cross-env": "^10.1.0",
|
|
53
|
-
tsx: "^4.20.6"
|
|
54
|
-
}
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
// src/cli/build/index.ts
|
|
58
|
-
import fs from "fs";
|
|
59
|
-
import path from "path";
|
|
60
|
-
import { fileURLToPath } from "url";
|
|
61
|
-
import {
|
|
62
|
-
AttachmentType
|
|
63
|
-
} from "@bf6mods/sdk";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { AttachmentType } from "@bf6mods/sdk";
|
|
7
|
+
import colors from "colors";
|
|
64
8
|
import { createJiti } from "jiti";
|
|
65
9
|
import { rolldown } from "rolldown";
|
|
10
|
+
import stripAnsi from "strip-ansi";
|
|
11
|
+
import { glob } from "node:fs/promises";
|
|
12
|
+
import chokidar from "chokidar";
|
|
13
|
+
import stringifyObject from "stringify-object";
|
|
14
|
+
import child_process from "node:child_process";
|
|
15
|
+
import * as prompts from "@clack/prompts";
|
|
16
|
+
import { genExport, genInlineTypeImport } from "knitwork";
|
|
66
17
|
|
|
67
|
-
|
|
68
|
-
var
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
MapId2["EmpireState"] = "MP_Aftermath-ModBuilderCustom0";
|
|
72
|
-
MapId2["IberianOffensive"] = "MP_Battery-ModBuilderCustom0";
|
|
73
|
-
MapId2["LiberationPeak"] = "MP_Capstone-ModBuilderCustom0";
|
|
74
|
-
MapId2["ManhattanBridge"] = "MP_Dumbo-ModBuilderCustom0";
|
|
75
|
-
MapId2["SaintsQuarter"] = "MP_Limestone-ModBuilderCustom0";
|
|
76
|
-
MapId2["NewSobekCity"] = "MP_Outskirts-ModBuilderCustom0";
|
|
77
|
-
MapId2["MirakValley"] = "MP_Tungsten-ModBuilderCustom0";
|
|
78
|
-
return MapId2;
|
|
79
|
-
})(MapId || {});
|
|
80
|
-
|
|
81
|
-
// src/cli/build/generated-strings.ts
|
|
82
|
-
function extractBf6Strings(bf6Strings, generateFromLiterals) {
|
|
83
|
-
return {
|
|
84
|
-
name: "extract-bf6-strings",
|
|
85
|
-
generateBundle(_options, bundle) {
|
|
86
|
-
if (!generateFromLiterals) return;
|
|
87
|
-
let id = 0;
|
|
88
|
-
const existingStrings = new Set(Object.values(bf6Strings));
|
|
89
|
-
for (const [_file, output] of Object.entries(bundle)) {
|
|
90
|
-
let walk2 = function(node) {
|
|
91
|
-
if (node.type === "Literal") {
|
|
92
|
-
if (typeof node.value !== "string" || node.value === "" || node.value === "----uniquename----")
|
|
93
|
-
return;
|
|
94
|
-
if (!(node.value in bf6Strings) && !existingStrings.has(node.value)) {
|
|
95
|
-
existingStrings.add(node.value);
|
|
96
|
-
bf6Strings[`__auto__${id++}`] = node.value;
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
for (const value of Object.values(node)) {
|
|
100
|
-
if (!value) continue;
|
|
101
|
-
if (Array.isArray(value)) {
|
|
102
|
-
for (const v of value) {
|
|
103
|
-
if (v && typeof v === "object" && "type" in v) walk2(v);
|
|
104
|
-
}
|
|
105
|
-
} else if (typeof value === "object" && "type" in value) {
|
|
106
|
-
walk2(value);
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
};
|
|
110
|
-
var walk = walk2;
|
|
111
|
-
if (output.type !== "chunk") continue;
|
|
112
|
-
const code = output.code;
|
|
113
|
-
const program2 = this.parse(code, {
|
|
114
|
-
lang: "ts",
|
|
115
|
-
astType: "js",
|
|
116
|
-
range: true
|
|
117
|
-
});
|
|
118
|
-
for (const item of program2.body) {
|
|
119
|
-
walk2(item);
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
},
|
|
123
|
-
moduleParsed(moduleInfo) {
|
|
124
|
-
if (moduleInfo.code && moduleInfo.exports.includes("bf6Strings")) {
|
|
125
|
-
const code = moduleInfo.code;
|
|
126
|
-
const getSnippet = (start, end, linesBefore = 2, linesAfter = 2) => {
|
|
127
|
-
const lines = code.split(/\r?\n/);
|
|
128
|
-
const startLine = Math.max(
|
|
129
|
-
0,
|
|
130
|
-
code.slice(0, start).split(/\r?\n/).length - 1 - linesBefore
|
|
131
|
-
);
|
|
132
|
-
const endLine = Math.min(
|
|
133
|
-
lines.length,
|
|
134
|
-
code.slice(0, end).split(/\r?\n/).length - 1 + linesAfter
|
|
135
|
-
);
|
|
136
|
-
return lines.slice(startLine, endLine).map((line, i) => {
|
|
137
|
-
const actualLine = startLine + i + 1;
|
|
138
|
-
return `${actualLine.toString().padStart(3)} | ${line}`;
|
|
139
|
-
}).join("\n");
|
|
140
|
-
};
|
|
141
|
-
const program2 = this.parse(code, {
|
|
142
|
-
lang: "ts",
|
|
143
|
-
astType: "js",
|
|
144
|
-
range: true
|
|
145
|
-
});
|
|
146
|
-
const extractObjectLiteral = (declaration) => {
|
|
147
|
-
if (declaration.init?.type !== "ObjectExpression") {
|
|
148
|
-
const snippet = getSnippet(...declaration.range ?? [0, 0]);
|
|
149
|
-
this.error(
|
|
150
|
-
`\u274C Invalid bf6Strings export: expected an object literal.
|
|
151
|
-
|
|
152
|
-
${snippet}`
|
|
153
|
-
);
|
|
154
|
-
}
|
|
155
|
-
for (const property of declaration.init.properties) {
|
|
156
|
-
if (property.type !== "Property") {
|
|
157
|
-
const snippet = getSnippet(...property.range ?? [0, 0]);
|
|
158
|
-
this.error(
|
|
159
|
-
`\u274C Invalid bf6Strings property: expected a key/value pair.
|
|
160
|
-
Found type: ${property.type}
|
|
161
|
-
|
|
162
|
-
${snippet}`
|
|
163
|
-
);
|
|
164
|
-
}
|
|
165
|
-
let key = void 0;
|
|
166
|
-
if (property.key.type === "Literal") {
|
|
167
|
-
key = property.key.value;
|
|
168
|
-
} else if (property.key.type === "Identifier") {
|
|
169
|
-
key = property.key.name;
|
|
170
|
-
} else {
|
|
171
|
-
const snippet = getSnippet(...property.range ?? [0, 0]);
|
|
172
|
-
this.error(
|
|
173
|
-
`\u274C Invalid bf6Strings key: expected a string literal or identifier.
|
|
174
|
-
Found type: ${property.key.type}
|
|
175
|
-
|
|
176
|
-
${snippet}`
|
|
177
|
-
);
|
|
178
|
-
continue;
|
|
179
|
-
}
|
|
180
|
-
if (property.value.type !== "Literal") {
|
|
181
|
-
const snippet = getSnippet(...property.range ?? [0, 0]);
|
|
182
|
-
this.error(
|
|
183
|
-
`\u274C Invalid bf6Strings value: expected a string literal.
|
|
184
|
-
Found type: ${property.value.type}
|
|
18
|
+
//#region package.json
|
|
19
|
+
var version = "1.2.0";
|
|
20
|
+
var description = "CLI and library for bundling BF6 mods";
|
|
21
|
+
var bin = { "bf6mods": "./dist/cli/index.js" };
|
|
185
22
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
23
|
+
//#endregion
|
|
24
|
+
//#region src/resources/prepare/types/config.ts
|
|
25
|
+
let MapId = /* @__PURE__ */ function(MapId$1) {
|
|
26
|
+
MapId$1["FireStorm"] = "MP_FireStorm-ModBuilderCustom0";
|
|
27
|
+
MapId$1["SiegeOfCairo"] = "MP_Abbasid-ModBuilderCustom0";
|
|
28
|
+
MapId$1["EmpireState"] = "MP_Aftermath-ModBuilderCustom0";
|
|
29
|
+
MapId$1["IberianOffensive"] = "MP_Battery-ModBuilderCustom0";
|
|
30
|
+
MapId$1["LiberationPeak"] = "MP_Capstone-ModBuilderCustom0";
|
|
31
|
+
MapId$1["ManhattanBridge"] = "MP_Dumbo-ModBuilderCustom0";
|
|
32
|
+
MapId$1["SaintsQuarter"] = "MP_Limestone-ModBuilderCustom0";
|
|
33
|
+
MapId$1["NewSobekCity"] = "MP_Outskirts-ModBuilderCustom0";
|
|
34
|
+
MapId$1["MirakValley"] = "MP_Tungsten-ModBuilderCustom0";
|
|
35
|
+
return MapId$1;
|
|
36
|
+
}({});
|
|
194
37
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
38
|
+
//#endregion
|
|
39
|
+
//#region src/cli/utils.ts
|
|
40
|
+
/**
|
|
41
|
+
* Safely prints a message with a right-aligned timestamp.
|
|
42
|
+
*/
|
|
43
|
+
const printToConsole = (message, error = false) => {
|
|
44
|
+
const now = /* @__PURE__ */ new Date();
|
|
45
|
+
const formattedTime = colors.grey(now.toLocaleTimeString());
|
|
46
|
+
const terminalWidth = process.stdout.columns || 80;
|
|
47
|
+
const timeLength = stripAnsi(formattedTime).length;
|
|
48
|
+
const available = terminalWidth - (stripAnsi(message).length + timeLength);
|
|
49
|
+
const spacing = available > 1 ? " ".repeat(available) : " ";
|
|
50
|
+
if (error) console.error(`${message}${spacing}${formattedTime}`);
|
|
51
|
+
else console.log(`${message}${spacing}${formattedTime}`);
|
|
52
|
+
};
|
|
202
53
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
54
|
+
//#endregion
|
|
55
|
+
//#region src/cli/build/generated-strings.ts
|
|
56
|
+
function extractBf6Strings(bf6Strings, generateFromLiterals) {
|
|
57
|
+
return {
|
|
58
|
+
name: "extract-bf6-strings",
|
|
59
|
+
generateBundle(_options, bundle) {
|
|
60
|
+
if (!generateFromLiterals) return;
|
|
61
|
+
let id = 0;
|
|
62
|
+
const existingStrings = new Set(Object.values(bf6Strings));
|
|
63
|
+
for (const [_file, output] of Object.entries(bundle)) {
|
|
64
|
+
if (output.type !== "chunk") continue;
|
|
65
|
+
const code = output.code;
|
|
66
|
+
const program$1 = this.parse(code, {
|
|
67
|
+
lang: "ts",
|
|
68
|
+
astType: "js",
|
|
69
|
+
range: true
|
|
70
|
+
});
|
|
71
|
+
function walk(node) {
|
|
72
|
+
if (node.type === "Literal") {
|
|
73
|
+
if (typeof node.value !== "string" || node.value === "" || node.value === "----uniquename----") return;
|
|
74
|
+
if (!(node.value in bf6Strings) && !existingStrings.has(node.value)) {
|
|
75
|
+
existingStrings.add(node.value);
|
|
76
|
+
bf6Strings[`__auto__${id++}`] = node.value;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
for (const value of Object.values(node)) {
|
|
80
|
+
if (!value) continue;
|
|
81
|
+
if (Array.isArray(value)) {
|
|
82
|
+
for (const v of value) if (v && typeof v === "object" && "type" in v) walk(v);
|
|
83
|
+
} else if (typeof value === "object" && "type" in value) walk(value);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
for (const item of program$1.body) walk(item);
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
moduleParsed(moduleInfo) {
|
|
90
|
+
if (moduleInfo.code && moduleInfo.exports.includes("bf6Strings")) {
|
|
91
|
+
const code = moduleInfo.code;
|
|
92
|
+
const getSnippet = (start, end, linesBefore = 2, linesAfter = 2) => {
|
|
93
|
+
const lines = code.split(/\r?\n/);
|
|
94
|
+
const startLine = Math.max(0, code.slice(0, start).split(/\r?\n/).length - 1 - linesBefore);
|
|
95
|
+
const endLine = Math.min(lines.length, code.slice(0, end).split(/\r?\n/).length - 1 + linesAfter);
|
|
96
|
+
return lines.slice(startLine, endLine).map((line, i) => {
|
|
97
|
+
return `${(startLine + i + 1).toString().padStart(3)} | ${line}`;
|
|
98
|
+
}).join("\n");
|
|
99
|
+
};
|
|
100
|
+
const program$1 = this.parse(code, {
|
|
101
|
+
lang: "ts",
|
|
102
|
+
astType: "js",
|
|
103
|
+
range: true
|
|
104
|
+
});
|
|
105
|
+
const extractObjectLiteral = (declaration) => {
|
|
106
|
+
if (declaration.init?.type !== "ObjectExpression") {
|
|
107
|
+
const snippet = getSnippet(...declaration.range ?? [0, 0]);
|
|
108
|
+
this.error(`${colors.red.bold("✗")} Invalid bf6Strings export: expected an object literal.\n\n${snippet}`);
|
|
109
|
+
}
|
|
110
|
+
for (const property of declaration.init.properties) {
|
|
111
|
+
if (property.type !== "Property") {
|
|
112
|
+
const snippet = getSnippet(...property.range ?? [0, 0]);
|
|
113
|
+
this.error(`${colors.red.bold("✗")} Invalid bf6Strings property: expected a key/value pair.\nFound type: ${property.type}\n\n${snippet}`);
|
|
114
|
+
}
|
|
115
|
+
let key;
|
|
116
|
+
if (property.key.type === "Literal") key = property.key.value;
|
|
117
|
+
else if (property.key.type === "Identifier") key = property.key.name;
|
|
118
|
+
else {
|
|
119
|
+
const snippet = getSnippet(...property.range ?? [0, 0]);
|
|
120
|
+
this.error(`${colors.red.bold("✗")} Invalid bf6Strings key: expected a string literal or identifier.\nFound type: ${property.key.type}\n\n${snippet}`);
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (property.value.type !== "Literal") {
|
|
124
|
+
const snippet = getSnippet(...property.range ?? [0, 0]);
|
|
125
|
+
this.error(`${colors.red.bold("✗")} Invalid bf6Strings value: expected a string literal.\nFound type: ${property.value.type}\n\n${snippet}`);
|
|
126
|
+
}
|
|
127
|
+
const value = property.value.value;
|
|
128
|
+
if (typeof key !== "string" && typeof key !== "number" && typeof key !== "bigint" && typeof key !== "boolean") {
|
|
129
|
+
const snippet = getSnippet(...property.range ?? [0, 0]);
|
|
130
|
+
this.error(`${colors.red.bold("✗")} bf6Strings key must be a string, found: ${typeof key}\n\n${snippet}`);
|
|
131
|
+
}
|
|
132
|
+
if (typeof value !== "string" && typeof value !== "number" && typeof value !== "bigint" && typeof value !== "boolean") {
|
|
133
|
+
const snippet = getSnippet(...property.range ?? [0, 0]);
|
|
134
|
+
this.error(`${colors.red.bold("✗")} bf6Strings value must be a string, found: ${typeof value}\n\n${snippet}`);
|
|
135
|
+
}
|
|
136
|
+
bf6Strings[`${key}`] = `${value}`;
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
for (const item of program$1.body) {
|
|
140
|
+
if (item.type === "VariableDeclaration") {
|
|
141
|
+
for (const declaration of item.declarations) if (declaration.type === "VariableDeclarator" && declaration.id.type === "Identifier" && declaration.id.name === "bf6Strings") extractObjectLiteral(declaration);
|
|
142
|
+
}
|
|
143
|
+
if (item.type === "ExportNamedDeclaration" && item.declaration?.type === "VariableDeclaration") {
|
|
144
|
+
for (const declaration of item.declaration.declarations) if (declaration.type === "VariableDeclarator" && declaration.id.type === "Identifier" && declaration.id.name === "bf6Strings") extractObjectLiteral(declaration);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
};
|
|
228
150
|
}
|
|
229
151
|
|
|
230
|
-
|
|
152
|
+
//#endregion
|
|
153
|
+
//#region src/cli/build/index.ts
|
|
154
|
+
/**
|
|
155
|
+
* Dynamically imports and validates bf6.config.ts
|
|
156
|
+
*/
|
|
231
157
|
async function getBf6Config(rootDir) {
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
return result;
|
|
158
|
+
globalThis.defineBf6Config = (c) => c;
|
|
159
|
+
globalThis.MapId = MapId;
|
|
160
|
+
const result = await createJiti(rootDir, { interopDefault: true }).import("./bf6.config", { default: true });
|
|
161
|
+
delete globalThis.defineBf6Config;
|
|
162
|
+
delete globalThis.MapId;
|
|
163
|
+
return result;
|
|
239
164
|
}
|
|
165
|
+
/**
|
|
166
|
+
* Builds the entire mod project once (for production or single run).
|
|
167
|
+
*/
|
|
240
168
|
async function build() {
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
const { attachments, mapRotation } = await collectAttachments(
|
|
260
|
-
config,
|
|
261
|
-
workingDir,
|
|
262
|
-
tsAttachment,
|
|
263
|
-
generatedStrings
|
|
264
|
-
);
|
|
265
|
-
await writeModJson(config, outDir, attachments, mapRotation, minifyJson);
|
|
266
|
-
console.log(`\u2714 Built mod: ${config.name}`);
|
|
169
|
+
const workingDir = path.resolve(".");
|
|
170
|
+
const config = await getBf6Config(workingDir);
|
|
171
|
+
const outDir = path.resolve(workingDir, config.outDir);
|
|
172
|
+
if (fs.existsSync(outDir)) fs.rmSync(outDir, {
|
|
173
|
+
recursive: true,
|
|
174
|
+
force: true
|
|
175
|
+
});
|
|
176
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
177
|
+
const minifyJson = typeof config.minify === "boolean" ? config.minify : config.minify?.json ?? false;
|
|
178
|
+
const generatedStrings = {};
|
|
179
|
+
let tsAttachment;
|
|
180
|
+
if (config.entrypoint) {
|
|
181
|
+
const entryAbs = path.resolve(workingDir, config.entrypoint);
|
|
182
|
+
tsAttachment = createTsAttachment(entryAbs, await buildEntrypoint(entryAbs, generatedStrings, config.generateStrings ?? true));
|
|
183
|
+
}
|
|
184
|
+
const { attachments, mapRotation } = await collectAttachments(config, workingDir, tsAttachment, generatedStrings);
|
|
185
|
+
await writeModJson(config, outDir, attachments, mapRotation, minifyJson);
|
|
186
|
+
printToConsole(`${colors.green.bold("✓")} Built mod: ${config.name}`);
|
|
267
187
|
}
|
|
188
|
+
/**
|
|
189
|
+
* Compiles the TypeScript entrypoint using rolldown and returns the compiled code.
|
|
190
|
+
*/
|
|
268
191
|
async function buildEntrypoint(entry, bf6Strings, generateStringsFromLiterals) {
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
});
|
|
278
|
-
const code = result.output[0].code;
|
|
279
|
-
return code;
|
|
192
|
+
return (await (await rolldown({
|
|
193
|
+
input: entry,
|
|
194
|
+
plugins: [extractBf6Strings(bf6Strings, generateStringsFromLiterals)],
|
|
195
|
+
logLevel: "debug"
|
|
196
|
+
})).generate({
|
|
197
|
+
format: "esm",
|
|
198
|
+
inlineDynamicImports: true
|
|
199
|
+
})).output[0].code;
|
|
280
200
|
}
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
201
|
+
const __filename$1 = fileURLToPath(import.meta.url);
|
|
202
|
+
const __dirname$1 = path.dirname(__filename$1);
|
|
203
|
+
const spatialsDir = path.resolve(__dirname$1, "../resources/maps/spatial");
|
|
204
|
+
/**
|
|
205
|
+
* Collects all attachments (TS, scenes, strings) and returns them + mapRotation.
|
|
206
|
+
*/
|
|
284
207
|
async function collectAttachments(config, workingDir, tsAttachment, generatedStrings) {
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
208
|
+
const attachments = [];
|
|
209
|
+
const mapRotation = [];
|
|
210
|
+
if (tsAttachment) attachments.push(tsAttachment);
|
|
211
|
+
if (config.strings) {
|
|
212
|
+
const strPath = path.resolve(workingDir, config.strings);
|
|
213
|
+
if (!fs.existsSync(strPath)) throw new Error("Cannot find strings file");
|
|
214
|
+
const attachment = createStringsAttachment(strPath, await fs.promises.readFile(strPath, "utf8"), generatedStrings);
|
|
215
|
+
attachments.push(attachment);
|
|
216
|
+
}
|
|
217
|
+
if (config.scenes) {
|
|
218
|
+
let mapIdx = 0;
|
|
219
|
+
for (const map of config.scenes) {
|
|
220
|
+
let mapId;
|
|
221
|
+
let scene;
|
|
222
|
+
if (Array.isArray(map)) [mapId, scene] = map;
|
|
223
|
+
else {
|
|
224
|
+
mapId = map;
|
|
225
|
+
scene = path.resolve(spatialsDir, `${mapId}.spatial.json`);
|
|
226
|
+
}
|
|
227
|
+
const scenePath = path.resolve(workingDir, scene);
|
|
228
|
+
if (!fs.existsSync(scenePath)) throw new Error(`Cannot find spatial data file: ${scene}`);
|
|
229
|
+
const spatial = createSpatialAttachment(scenePath, await fs.promises.readFile(scenePath, "utf8"), mapIdx++);
|
|
230
|
+
attachments.push(spatial);
|
|
231
|
+
mapRotation.push({
|
|
232
|
+
id: mapId,
|
|
233
|
+
spatialAttachment: spatial
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return {
|
|
238
|
+
attachments,
|
|
239
|
+
mapRotation
|
|
240
|
+
};
|
|
316
241
|
}
|
|
242
|
+
/**
|
|
243
|
+
* Writes the mod.json output to disk.
|
|
244
|
+
*/
|
|
317
245
|
async function writeModJson(config, outDir, attachments, mapRotation, minify) {
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
recursive: true
|
|
337
|
-
});
|
|
338
|
-
await fs.promises.writeFile(
|
|
339
|
-
path.resolve(outDir, "attachments", attachment.filename),
|
|
340
|
-
atob(attachment.attachmentData.original)
|
|
341
|
-
);
|
|
342
|
-
}
|
|
343
|
-
}
|
|
246
|
+
const baseGame = config.game;
|
|
247
|
+
const finalJson = {
|
|
248
|
+
name: config.name,
|
|
249
|
+
description: config.description,
|
|
250
|
+
gameMode: "ModBuilderCustom",
|
|
251
|
+
mutators: baseGame.mutators ?? {},
|
|
252
|
+
assetRestrictions: baseGame.assetRestrictions ?? {},
|
|
253
|
+
teamComposition: baseGame.teamComposition ?? [],
|
|
254
|
+
mapRotation,
|
|
255
|
+
attachments
|
|
256
|
+
};
|
|
257
|
+
const jsonOutput = minify ? JSON.stringify(finalJson) : JSON.stringify(finalJson, null, 2);
|
|
258
|
+
await fs.promises.writeFile(path.resolve(outDir, "mod.json"), jsonOutput);
|
|
259
|
+
if (config.outputArtifacts && finalJson?.attachments) for (const attachment of finalJson.attachments) {
|
|
260
|
+
const attachmentsDir = path.resolve(outDir, "attachments");
|
|
261
|
+
if (!fs.existsSync(attachmentsDir)) fs.mkdirSync(attachmentsDir, { recursive: true });
|
|
262
|
+
await fs.promises.writeFile(path.resolve(outDir, "attachments", attachment.filename), atob(attachment.attachmentData.original));
|
|
263
|
+
}
|
|
344
264
|
}
|
|
345
265
|
function createTsAttachment(filePath, compiled) {
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
266
|
+
return {
|
|
267
|
+
id: crypto.randomUUID(),
|
|
268
|
+
version: "1.0",
|
|
269
|
+
filename: `${path.parse(filePath).name}.js`,
|
|
270
|
+
isProcessable: true,
|
|
271
|
+
processingStatus: 2,
|
|
272
|
+
attachmentType: AttachmentType.TypeScript,
|
|
273
|
+
attachmentData: {
|
|
274
|
+
original: toBase64(compiled),
|
|
275
|
+
compiled: ""
|
|
276
|
+
},
|
|
277
|
+
errors: []
|
|
278
|
+
};
|
|
356
279
|
}
|
|
357
280
|
function createStringsAttachment(filePath, raw, generatedStrings) {
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
attachmentData: { original: toBase64(result), compiled: "" },
|
|
377
|
-
errors: []
|
|
378
|
-
};
|
|
281
|
+
let result = raw;
|
|
282
|
+
if (generatedStrings) result = JSON.stringify({
|
|
283
|
+
...generatedStrings,
|
|
284
|
+
...JSON.parse(raw)
|
|
285
|
+
}, null, 4);
|
|
286
|
+
return {
|
|
287
|
+
id: crypto.randomUUID(),
|
|
288
|
+
version: "1.0",
|
|
289
|
+
filename: path.basename(filePath),
|
|
290
|
+
isProcessable: true,
|
|
291
|
+
processingStatus: 2,
|
|
292
|
+
attachmentType: AttachmentType.Strings,
|
|
293
|
+
attachmentData: {
|
|
294
|
+
original: toBase64(result),
|
|
295
|
+
compiled: ""
|
|
296
|
+
},
|
|
297
|
+
errors: []
|
|
298
|
+
};
|
|
379
299
|
}
|
|
380
300
|
function createSpatialAttachment(filePath, raw, mapIdx) {
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
301
|
+
return {
|
|
302
|
+
id: crypto.randomUUID(),
|
|
303
|
+
version: "1.0",
|
|
304
|
+
filename: path.basename(filePath),
|
|
305
|
+
isProcessable: true,
|
|
306
|
+
processingStatus: 2,
|
|
307
|
+
attachmentType: AttachmentType.SpatialData,
|
|
308
|
+
attachmentData: {
|
|
309
|
+
original: toBase64(raw),
|
|
310
|
+
compiled: ""
|
|
311
|
+
},
|
|
312
|
+
metadata: `mapIdx=${mapIdx}`,
|
|
313
|
+
errors: []
|
|
314
|
+
};
|
|
392
315
|
}
|
|
393
316
|
function toBase64(input) {
|
|
394
|
-
|
|
317
|
+
return Buffer.isBuffer(input) ? input.toString("base64") : Buffer.from(input, "utf8").toString("base64");
|
|
395
318
|
}
|
|
396
319
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
320
|
+
//#endregion
|
|
321
|
+
//#region src/cli/log.ts
|
|
322
|
+
var Bf6Logger = class Bf6Logger {
|
|
323
|
+
constructor(input) {
|
|
324
|
+
if (Bf6Logger.instance) {
|
|
325
|
+
printToConsole(colors.yellow("⚠ Existing logger found — closing it first..."));
|
|
326
|
+
Bf6Logger.instance.stop();
|
|
327
|
+
}
|
|
328
|
+
if (input) this.watchTarget = path.resolve(input);
|
|
329
|
+
else if (process.env.LOCALAPPDATA) this.watchTarget = path.resolve(process.env.LOCALAPPDATA, "temp", "Battlefieldâ„¢ 6", "PortalLog.txt");
|
|
330
|
+
else throw new Error(`${colors.red.bold("✗")} Env variable LOCALAPPDATA not defined, and input file to watch was not provided!`);
|
|
331
|
+
if (!fs.existsSync(this.watchTarget)) throw new Error(`${colors.red.bold("✗")} Cannot find file '${this.watchTarget}' to watch for changes`);
|
|
332
|
+
this.lastSize = fs.statSync(this.watchTarget).size;
|
|
333
|
+
Bf6Logger.instance = this;
|
|
334
|
+
}
|
|
335
|
+
async start() {
|
|
336
|
+
printToConsole(colors.cyan(`👀 Watching Battlefield logs at: ${colors.yellow(this.watchTarget)}`));
|
|
337
|
+
this.watcher = chokidar.watch(this.watchTarget, {
|
|
338
|
+
persistent: true,
|
|
339
|
+
ignoreInitial: true
|
|
340
|
+
});
|
|
341
|
+
this.watcher.on("change", this.handleChange.bind(this));
|
|
342
|
+
}
|
|
343
|
+
async stop() {
|
|
344
|
+
if (this.watcher) {
|
|
345
|
+
this.watcher.close();
|
|
346
|
+
this.watcher = void 0;
|
|
347
|
+
printToConsole(colors.yellow(`${colors.red.bold("✗")} Stopped watching: ${colors.gray(this.watchTarget)}`));
|
|
348
|
+
}
|
|
349
|
+
Bf6Logger.instance = void 0;
|
|
350
|
+
}
|
|
351
|
+
handleChange(file, stats) {
|
|
352
|
+
if (!stats) return;
|
|
353
|
+
if (stats.size > this.lastSize) {
|
|
354
|
+
const newBytes = stats.size - this.lastSize;
|
|
355
|
+
const fd = fs.openSync(file, "r");
|
|
356
|
+
const buffer = Buffer.alloc(newBytes);
|
|
357
|
+
fs.readSync(fd, buffer, 0, newBytes, this.lastSize);
|
|
358
|
+
fs.closeSync(fd);
|
|
359
|
+
const newContent = buffer.toString("utf8").split("QuickJS: ").filter((debug) => debug.trim() !== "").map((debug) => debug.trim()).map((debug) => {
|
|
360
|
+
if (debug.startsWith("console.log: ")) return {
|
|
361
|
+
type: "console.log",
|
|
362
|
+
text: debug.replace("console.log: ", "")
|
|
363
|
+
};
|
|
364
|
+
else if (debug.startsWith("Exception:")) return {
|
|
365
|
+
type: "exception",
|
|
366
|
+
text: debug.replace("Exception:", "")
|
|
367
|
+
};
|
|
368
|
+
return {
|
|
369
|
+
type: "unknown",
|
|
370
|
+
text: debug
|
|
371
|
+
};
|
|
372
|
+
});
|
|
373
|
+
for (const content of newContent) if (content.type === "console.log") printToConsole(colors.gray(content.text));
|
|
374
|
+
else if (content.type === "exception") printToConsole(colors.red(content.text), true);
|
|
375
|
+
else printToConsole(colors.bold(content.text));
|
|
376
|
+
}
|
|
377
|
+
this.lastSize = stats.size;
|
|
378
|
+
}
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
//#endregion
|
|
382
|
+
//#region src/cli/dev.ts
|
|
403
383
|
async function dev() {
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
384
|
+
const workingDir = path.resolve(".");
|
|
385
|
+
let config = await getBf6Config(workingDir);
|
|
386
|
+
const outDir = path.resolve(workingDir, config.outDir);
|
|
387
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
388
|
+
printToConsole(colors.cyan(`▶ Starting dev for ${config.name}`));
|
|
389
|
+
let watcher;
|
|
390
|
+
function debounce(fn, delay) {
|
|
391
|
+
let timer;
|
|
392
|
+
return (...args) => {
|
|
393
|
+
if (timer) clearTimeout(timer);
|
|
394
|
+
timer = setTimeout(() => fn(...args), delay);
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
const rebuild = async (trigger) => {
|
|
398
|
+
printToConsole(colors.yellow(`↻ Change detected in ${path.basename(trigger)}, rebuilding...`));
|
|
399
|
+
const start = performance.now();
|
|
400
|
+
try {
|
|
401
|
+
await build();
|
|
402
|
+
const duration = ((performance.now() - start) / 1e3).toFixed(2);
|
|
403
|
+
printToConsole(`${colors.green.bold("✓")} Updated mod.json (${duration}s)`);
|
|
404
|
+
} catch (err) {
|
|
405
|
+
printToConsole(colors.red(`${colors.red.bold("✗")} Rebuild failed: ${err.message}`), true);
|
|
406
|
+
}
|
|
407
|
+
};
|
|
408
|
+
const debouncedRebuild = debounce(rebuild, 200);
|
|
409
|
+
async function collectWatchTargets() {
|
|
410
|
+
const targets = [];
|
|
411
|
+
if (config.entrypoint) targets.push(path.resolve(workingDir, config.entrypoint));
|
|
412
|
+
if (config.scenes) for (const [, scene] of config.scenes) targets.push(path.resolve(workingDir, scene));
|
|
413
|
+
if (config.strings) targets.push(path.resolve(workingDir, config.strings));
|
|
414
|
+
for await (const entry of glob("bf6.config.*")) targets.push(entry);
|
|
415
|
+
for await (const entry of glob("src/**/*")) targets.push(entry);
|
|
416
|
+
return targets;
|
|
417
|
+
}
|
|
418
|
+
async function setupWatcher() {
|
|
419
|
+
if (watcher) {
|
|
420
|
+
await watcher.close();
|
|
421
|
+
printToConsole(colors.grey("♻ Reloading watcher due to config change..."));
|
|
422
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
423
|
+
}
|
|
424
|
+
const watchTargets = await collectWatchTargets();
|
|
425
|
+
printToConsole(colors.cyan(`👀 Watching ${watchTargets.length} files...`));
|
|
426
|
+
watcher = chokidar.watch(watchTargets, {
|
|
427
|
+
persistent: true,
|
|
428
|
+
ignoreInitial: true,
|
|
429
|
+
awaitWriteFinish: {
|
|
430
|
+
stabilityThreshold: 250,
|
|
431
|
+
pollInterval: 100
|
|
432
|
+
},
|
|
433
|
+
ignored: [
|
|
434
|
+
outDir,
|
|
435
|
+
`${outDir}/**`,
|
|
436
|
+
"node_modules/**",
|
|
437
|
+
".git/**"
|
|
438
|
+
]
|
|
439
|
+
});
|
|
440
|
+
watcher.on("error", (err) => {
|
|
441
|
+
printToConsole(colors.red(`⚠ Watcher error: ${err.message}`), true);
|
|
442
|
+
setTimeout(setupWatcher, 1e3);
|
|
443
|
+
});
|
|
444
|
+
watcher.on("change", async (file) => {
|
|
445
|
+
if (file.includes("bf6.config.")) {
|
|
446
|
+
printToConsole(colors.magenta("⚙ Config changed — reloading watcher..."));
|
|
447
|
+
config = await getBf6Config(workingDir);
|
|
448
|
+
await setupWatcher();
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
debouncedRebuild(file);
|
|
452
|
+
});
|
|
453
|
+
watcher.on("add", debouncedRebuild);
|
|
454
|
+
await rebuild("initial");
|
|
455
|
+
}
|
|
456
|
+
process.on("SIGINT", async () => {
|
|
457
|
+
printToConsole(colors.red(`\n${colors.red.bold("✗")} Exiting dev mode...`));
|
|
458
|
+
await watcher?.close();
|
|
459
|
+
process.exit(0);
|
|
460
|
+
});
|
|
461
|
+
await setupWatcher();
|
|
462
|
+
let logger;
|
|
463
|
+
try {
|
|
464
|
+
logger = new Bf6Logger();
|
|
465
|
+
logger.start();
|
|
466
|
+
} catch (error) {
|
|
467
|
+
printToConsole(colors.grey("Failed to start logging for the following reason (building will still work)"), true);
|
|
468
|
+
printToConsole(error.message, true);
|
|
469
|
+
}
|
|
476
470
|
}
|
|
477
471
|
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
// src/cli/init.ts
|
|
485
|
-
import child_process from "child_process";
|
|
486
|
-
import fs3 from "fs";
|
|
487
|
-
import path3 from "path";
|
|
488
|
-
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
489
|
-
import * as prompts from "@clack/prompts";
|
|
490
|
-
var __filename2 = fileURLToPath2(import.meta.url);
|
|
491
|
-
var __dirname2 = path3.dirname(__filename2);
|
|
492
|
-
var templatesDir = path3.resolve(__dirname2, "../resources/templates");
|
|
472
|
+
//#endregion
|
|
473
|
+
//#region src/cli/init.ts
|
|
474
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
475
|
+
const __dirname = path.dirname(__filename);
|
|
476
|
+
const templatesDir = path.resolve(__dirname, "../resources/templates");
|
|
493
477
|
function renameFilesRecursively(dir, modName) {
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
}
|
|
516
|
-
}
|
|
478
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
479
|
+
for (const entry of entries) {
|
|
480
|
+
const oldPath = path.join(dir, entry.name);
|
|
481
|
+
let newPath = oldPath;
|
|
482
|
+
if (entry.name.includes("{name}")) {
|
|
483
|
+
const newName = entry.name.replace("{name}", modName);
|
|
484
|
+
newPath = path.join(dir, newName);
|
|
485
|
+
fs.renameSync(oldPath, newPath);
|
|
486
|
+
}
|
|
487
|
+
if (entry.name === "bf6.config.ts") {
|
|
488
|
+
let content = fs.readFileSync(newPath, "utf-8");
|
|
489
|
+
content = content.replace(/{bf6ConfigName}/g, modName);
|
|
490
|
+
fs.writeFileSync(newPath, content, "utf-8");
|
|
491
|
+
}
|
|
492
|
+
if (entry.name === "package.json") {
|
|
493
|
+
const pkg = JSON.parse(fs.readFileSync(newPath, "utf-8"));
|
|
494
|
+
pkg.name = modName;
|
|
495
|
+
fs.writeFileSync(newPath, JSON.stringify(pkg, null, 2));
|
|
496
|
+
}
|
|
497
|
+
if (fs.statSync(newPath).isDirectory()) renameFilesRecursively(newPath, modName);
|
|
498
|
+
}
|
|
517
499
|
}
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
500
|
+
const templates = [
|
|
501
|
+
"Basic",
|
|
502
|
+
"AcePursuit",
|
|
503
|
+
"BombSquad",
|
|
504
|
+
"Exfil",
|
|
505
|
+
"Vertigo"
|
|
524
506
|
];
|
|
525
507
|
async function startProject(destination, template, name) {
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
508
|
+
if ([
|
|
509
|
+
"AcePursuit",
|
|
510
|
+
"BombSquad",
|
|
511
|
+
"Exfil",
|
|
512
|
+
"Vertigo"
|
|
513
|
+
].includes(template)) await importFile(path.resolve(templatesDir, `${template}.json`), destination, name);
|
|
514
|
+
else if (template === "None") {} else {
|
|
515
|
+
const templateDir = path.resolve(templatesDir, template);
|
|
516
|
+
fs.cpSync(templateDir, destination, { recursive: true });
|
|
517
|
+
}
|
|
518
|
+
fs.cpSync(path.resolve(templatesDir, "All"), destination, { recursive: true });
|
|
519
|
+
if (name) renameFilesRecursively(destination, name);
|
|
538
520
|
}
|
|
539
521
|
function installDependencies(projectDir) {
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
522
|
+
try {
|
|
523
|
+
child_process.execSync(`npm install`, {
|
|
524
|
+
stdio: "inherit",
|
|
525
|
+
cwd: projectDir
|
|
526
|
+
});
|
|
527
|
+
return true;
|
|
528
|
+
} catch (_err) {
|
|
529
|
+
return false;
|
|
530
|
+
}
|
|
549
531
|
}
|
|
550
|
-
|
|
551
|
-
function isEmpty(
|
|
552
|
-
|
|
553
|
-
|
|
532
|
+
const cancel = () => prompts.cancel("Operation cancelled");
|
|
533
|
+
function isEmpty(path$1) {
|
|
534
|
+
const files = fs.readdirSync(path$1);
|
|
535
|
+
return files.length === 0 || files.length === 1 && files[0] === ".git";
|
|
554
536
|
}
|
|
555
537
|
function emptyDir(dir) {
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
}
|
|
538
|
+
if (!fs.existsSync(dir)) return;
|
|
539
|
+
for (const file of fs.readdirSync(dir)) {
|
|
540
|
+
if (file === ".git") continue;
|
|
541
|
+
fs.rmSync(path.resolve(dir, file), {
|
|
542
|
+
recursive: true,
|
|
543
|
+
force: true
|
|
544
|
+
});
|
|
545
|
+
}
|
|
565
546
|
}
|
|
566
547
|
async function init(argTargetDir) {
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
npm run build`;
|
|
634
|
-
prompts.note(nextSteps, "Next steps.");
|
|
548
|
+
prompts.intro("Initialize Bf6 Mod");
|
|
549
|
+
const path$1 = argTargetDir ? argTargetDir : await prompts.text({
|
|
550
|
+
message: "Where should we create your project?",
|
|
551
|
+
placeholder: "./ace-pursuit",
|
|
552
|
+
validate: (value) => {
|
|
553
|
+
if (!value) return "Please enter a path.";
|
|
554
|
+
if (value[0] !== ".") return "Please enter a relative path.";
|
|
555
|
+
}
|
|
556
|
+
});
|
|
557
|
+
if (prompts.isCancel(path$1)) return cancel();
|
|
558
|
+
let name = await prompts.text({
|
|
559
|
+
message: "What is the name of your mod?",
|
|
560
|
+
placeholder: "Ace Pursuit",
|
|
561
|
+
validate: (value) => {
|
|
562
|
+
if (!value.trim()) return "Please enter a name.";
|
|
563
|
+
}
|
|
564
|
+
});
|
|
565
|
+
if (prompts.isCancel(name)) return cancel();
|
|
566
|
+
name = name.trim();
|
|
567
|
+
if (fs.existsSync(path$1) && !isEmpty(path$1)) {
|
|
568
|
+
let overwrite;
|
|
569
|
+
const res = await prompts.select({
|
|
570
|
+
message: (path$1 === "." ? "Current directory" : `Target directory "${path$1}"`) + ` is not empty. Please choose how to proceed:`,
|
|
571
|
+
options: [
|
|
572
|
+
{
|
|
573
|
+
label: "Cancel operation",
|
|
574
|
+
value: "no"
|
|
575
|
+
},
|
|
576
|
+
{
|
|
577
|
+
label: "Remove existing files and continue",
|
|
578
|
+
value: "yes"
|
|
579
|
+
},
|
|
580
|
+
{
|
|
581
|
+
label: "Ignore files and continue",
|
|
582
|
+
value: "ignore"
|
|
583
|
+
}
|
|
584
|
+
]
|
|
585
|
+
});
|
|
586
|
+
if (prompts.isCancel(res)) return cancel();
|
|
587
|
+
overwrite = res;
|
|
588
|
+
switch (overwrite) {
|
|
589
|
+
case "yes":
|
|
590
|
+
emptyDir(path$1);
|
|
591
|
+
break;
|
|
592
|
+
case "no":
|
|
593
|
+
cancel();
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
const template = await prompts.select({
|
|
598
|
+
message: "Select a template:",
|
|
599
|
+
options: templates.map((template$1) => {
|
|
600
|
+
return {
|
|
601
|
+
label: template$1,
|
|
602
|
+
value: template$1
|
|
603
|
+
};
|
|
604
|
+
})
|
|
605
|
+
});
|
|
606
|
+
if (prompts.isCancel(template)) return cancel();
|
|
607
|
+
await startProject(path$1, template, name);
|
|
608
|
+
const s = prompts.spinner();
|
|
609
|
+
s.start("Installing via npm");
|
|
610
|
+
if (installDependencies(path$1)) s.stop("Installed via npm");
|
|
611
|
+
else s.stop("Failed to install via npm", 1);
|
|
612
|
+
const nextSteps = `cd ${path$1}\nnpm run build`;
|
|
613
|
+
prompts.note(nextSteps, "Next steps.");
|
|
635
614
|
}
|
|
636
615
|
|
|
637
|
-
|
|
616
|
+
//#endregion
|
|
617
|
+
//#region src/cli/import.ts
|
|
638
618
|
async function writeFileSafe(filePath, data) {
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
619
|
+
const dir = path.dirname(filePath);
|
|
620
|
+
await fs.promises.mkdir(dir, { recursive: true });
|
|
621
|
+
await fs.promises.writeFile(filePath, data);
|
|
642
622
|
}
|
|
643
623
|
function getMapKeyByValue(value) {
|
|
644
|
-
|
|
645
|
-
(k) => MapId[k] === value
|
|
646
|
-
);
|
|
624
|
+
return Object.keys(MapId).find((k) => MapId[k] === value);
|
|
647
625
|
}
|
|
648
626
|
async function importFile(input, output, name) {
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
)
|
|
687
|
-
);
|
|
688
|
-
scenes.push([map.id, `src/scenes/${map.spatialAttachment.filename}`]);
|
|
689
|
-
}
|
|
690
|
-
}
|
|
691
|
-
const bf6Config = {
|
|
692
|
-
name: name ?? config.name,
|
|
693
|
-
description: config.description,
|
|
694
|
-
outDir: "dist",
|
|
695
|
-
entrypoint: typescriptFile ? `src/${typescriptFile}` : void 0,
|
|
696
|
-
scenes: scenes ? scenes.map(([id, path6]) => [
|
|
697
|
-
`MapId.${getMapKeyByValue(id) ?? id}`,
|
|
698
|
-
path6
|
|
699
|
-
]) : void 0,
|
|
700
|
-
strings: stringsFile ? `src/${stringsFile}` : void 0,
|
|
701
|
-
game: {
|
|
702
|
-
mutators: config.mutators,
|
|
703
|
-
assetRestrictions: config.assetRestrictions,
|
|
704
|
-
gameMode: config.gameMode,
|
|
705
|
-
teamComposition: config.teamComposition
|
|
706
|
-
}
|
|
707
|
-
};
|
|
708
|
-
const bf6ConfigContent = `export default defineBf6Config(${stringifyWithRaw(
|
|
709
|
-
bf6Config
|
|
710
|
-
)});
|
|
711
|
-
`;
|
|
712
|
-
promises.push(
|
|
713
|
-
writeFileSafe(path4.resolve(outDir, "bf6.config.ts"), bf6ConfigContent)
|
|
714
|
-
);
|
|
715
|
-
await Promise.all(promises);
|
|
627
|
+
const workingDir = path.resolve(".");
|
|
628
|
+
const entrypoint = path.resolve(workingDir, input);
|
|
629
|
+
const outDir = path.resolve(workingDir, output);
|
|
630
|
+
if (!fs.existsSync(entrypoint)) throw new Error("Cannot find strings file");
|
|
631
|
+
const config = JSON.parse(await fs.promises.readFile(entrypoint, { encoding: "utf8" }));
|
|
632
|
+
if (!fs.existsSync(outDir)) await fs.promises.mkdir(outDir, { recursive: true });
|
|
633
|
+
await startProject(outDir, "None", name ?? config.name);
|
|
634
|
+
let typescriptFile;
|
|
635
|
+
let stringsFile;
|
|
636
|
+
const scenes = [];
|
|
637
|
+
const promises = [];
|
|
638
|
+
if (config.attachments) for (const attachment of config.attachments) {
|
|
639
|
+
if (attachment.attachmentType === AttachmentType.TypeScript) typescriptFile = attachment.filename;
|
|
640
|
+
if (attachment.attachmentType === AttachmentType.Strings) stringsFile = attachment.filename;
|
|
641
|
+
if (attachment.attachmentType === AttachmentType.SpatialData) continue;
|
|
642
|
+
promises.push(writeFileSafe(path.resolve(outDir, "src", attachment.filename), atob(attachment.attachmentData.original)));
|
|
643
|
+
}
|
|
644
|
+
if (config.mapRotation?.length) for (const map of config.mapRotation) {
|
|
645
|
+
promises.push(writeFileSafe(path.resolve(outDir, "src", "scenes", map.spatialAttachment.filename), atob(map.spatialAttachment.attachmentData.original)));
|
|
646
|
+
scenes.push([map.id, `src/scenes/${map.spatialAttachment.filename}`]);
|
|
647
|
+
}
|
|
648
|
+
const bf6ConfigContent = `export default defineBf6Config(${stringifyWithRaw({
|
|
649
|
+
name: name ?? config.name,
|
|
650
|
+
description: config.description,
|
|
651
|
+
outDir: "dist",
|
|
652
|
+
entrypoint: typescriptFile ? `src/${typescriptFile}` : void 0,
|
|
653
|
+
scenes: scenes ? scenes.map(([id, path$1]) => [`MapId.${getMapKeyByValue(id) ?? id}`, path$1]) : void 0,
|
|
654
|
+
strings: stringsFile ? `src/${stringsFile}` : void 0,
|
|
655
|
+
game: {
|
|
656
|
+
mutators: config.mutators,
|
|
657
|
+
assetRestrictions: config.assetRestrictions,
|
|
658
|
+
gameMode: config.gameMode,
|
|
659
|
+
teamComposition: config.teamComposition
|
|
660
|
+
}
|
|
661
|
+
})});\n`;
|
|
662
|
+
promises.push(writeFileSafe(path.resolve(outDir, "bf6.config.ts"), bf6ConfigContent));
|
|
663
|
+
await Promise.all(promises);
|
|
716
664
|
}
|
|
717
665
|
function stringifyWithRaw(value, options = {}) {
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
},
|
|
728
|
-
...options
|
|
729
|
-
});
|
|
666
|
+
return stringifyObject(value, {
|
|
667
|
+
indent: " ",
|
|
668
|
+
singleQuotes: true,
|
|
669
|
+
transform: (_obj, _prop, originalResult) => {
|
|
670
|
+
if (/^['"]MapId\.[A-Za-z0-9_]+['"]$/.test(originalResult)) return originalResult.slice(1, -1);
|
|
671
|
+
return originalResult;
|
|
672
|
+
},
|
|
673
|
+
...options
|
|
674
|
+
});
|
|
730
675
|
}
|
|
731
676
|
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
import path5 from "path";
|
|
735
|
-
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
736
|
-
import colors3 from "colors";
|
|
737
|
-
import { genExport, genInlineTypeImport } from "knitwork";
|
|
738
|
-
|
|
739
|
-
// src/cli/utils.ts
|
|
740
|
-
import colors2 from "colors";
|
|
741
|
-
|
|
742
|
-
// ../../node_modules/ansi-regex/index.js
|
|
743
|
-
function ansiRegex({ onlyFirst = false } = {}) {
|
|
744
|
-
const ST = "(?:\\u0007|\\u001B\\u005C|\\u009C)";
|
|
745
|
-
const osc = `(?:\\u001B\\][\\s\\S]*?${ST})`;
|
|
746
|
-
const csi = "[\\u001B\\u009B][[\\]()#;?]*(?:\\d{1,4}(?:[;:]\\d{0,4})*)?[\\dA-PR-TZcf-nq-uy=><~]";
|
|
747
|
-
const pattern = `${osc}|${csi}`;
|
|
748
|
-
return new RegExp(pattern, onlyFirst ? void 0 : "g");
|
|
749
|
-
}
|
|
750
|
-
|
|
751
|
-
// ../../node_modules/strip-ansi/index.js
|
|
752
|
-
var regex = ansiRegex();
|
|
753
|
-
function stripAnsi(string) {
|
|
754
|
-
if (typeof string !== "string") {
|
|
755
|
-
throw new TypeError(`Expected a \`string\`, got \`${typeof string}\``);
|
|
756
|
-
}
|
|
757
|
-
return string.replace(regex, "");
|
|
758
|
-
}
|
|
759
|
-
|
|
760
|
-
// src/cli/utils.ts
|
|
761
|
-
var printToConsole = (message) => {
|
|
762
|
-
const now = /* @__PURE__ */ new Date();
|
|
763
|
-
const formattedTime = colors2.grey(now.toLocaleTimeString());
|
|
764
|
-
const terminalWidth = process.stdout.columns || 80;
|
|
765
|
-
const timeLength = stripAnsi(formattedTime).length;
|
|
766
|
-
const messageLength = stripAnsi(message).length;
|
|
767
|
-
const available = terminalWidth - (messageLength + timeLength);
|
|
768
|
-
const spacing = available > 1 ? " ".repeat(available) : " ";
|
|
769
|
-
console.log(`${message}${spacing}${formattedTime}`);
|
|
770
|
-
};
|
|
771
|
-
|
|
772
|
-
// src/cli/prepare.ts
|
|
677
|
+
//#endregion
|
|
678
|
+
//#region src/cli/prepare.ts
|
|
773
679
|
async function prepare() {
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
fs5.writeFileSync(
|
|
796
|
-
path5.resolve(buildDir, "imports.d.ts"),
|
|
797
|
-
`${ConfigFileExports}
|
|
798
|
-
`
|
|
799
|
-
);
|
|
800
|
-
const augmentations = {
|
|
801
|
-
defineBf6Config: genInlineTypeImport(
|
|
802
|
-
"./types/config.ts",
|
|
803
|
-
`defineBf6Config`
|
|
804
|
-
),
|
|
805
|
-
MapId: genInlineTypeImport("./types/config.ts", `MapId`)
|
|
806
|
-
};
|
|
807
|
-
const args = genNamespaceAugmentation("global", augmentations);
|
|
808
|
-
fs5.writeFileSync(
|
|
809
|
-
path5.resolve(buildDir, "globals.d.ts"),
|
|
810
|
-
`export {}
|
|
811
|
-
|
|
812
|
-
${args}
|
|
813
|
-
`
|
|
814
|
-
);
|
|
815
|
-
printToConsole(`${colors3.green("\u2714")} Types generated in .bf6`);
|
|
816
|
-
} catch (error) {
|
|
817
|
-
console.error(error);
|
|
818
|
-
printToConsole(`${colors3.red("\u2717")} Types failed to generate in .bf6`);
|
|
819
|
-
}
|
|
680
|
+
try {
|
|
681
|
+
const __filename$2 = fileURLToPath(import.meta.url);
|
|
682
|
+
const __dirname$2 = path.dirname(__filename$2);
|
|
683
|
+
path.resolve(".");
|
|
684
|
+
const buildDir = path.resolve(".bf6");
|
|
685
|
+
const resources = path.resolve(__dirname$2, "../resources/prepare");
|
|
686
|
+
fs.cpSync(path.resolve(resources, "tsconfig.json"), path.resolve(buildDir, "tsconfig.json"));
|
|
687
|
+
fs.cpSync(path.resolve(resources, "bf6.d.ts"), path.resolve(buildDir, "bf6.d.ts"));
|
|
688
|
+
fs.cpSync(path.resolve(resources, "types", "config.ts"), path.resolve(buildDir, "types", "config.ts"));
|
|
689
|
+
const ConfigFileExports = genExport("./types/config.ts", ["defineBf6Config"]);
|
|
690
|
+
fs.writeFileSync(path.resolve(buildDir, "imports.d.ts"), `${ConfigFileExports}\n`);
|
|
691
|
+
const args = genNamespaceAugmentation("global", {
|
|
692
|
+
defineBf6Config: genInlineTypeImport("./types/config.ts", `defineBf6Config`),
|
|
693
|
+
MapId: genInlineTypeImport("./types/config.ts", `MapId`)
|
|
694
|
+
});
|
|
695
|
+
fs.writeFileSync(path.resolve(buildDir, "globals.d.ts"), `export {}\n\n${args}\n`);
|
|
696
|
+
printToConsole(`${colors.green.bold("✓")} Types generated in .bf6`);
|
|
697
|
+
} catch (error) {
|
|
698
|
+
console.error(error);
|
|
699
|
+
printToConsole(`${colors.red.bold("✗")} Types failed to generate in .bf6`, true);
|
|
700
|
+
}
|
|
820
701
|
}
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
const decls = Object.entries(contents).map(([k, v]) => ` const ${k}: ${v};`).join("\n");
|
|
825
|
-
return `declare ${name} {
|
|
826
|
-
${decls}
|
|
827
|
-
}`;
|
|
702
|
+
const genNamespaceAugmentation = (name, contents) => {
|
|
703
|
+
if (!contents || Object.keys(contents).length === 0) return `declare ${name} {}`;
|
|
704
|
+
return `declare ${name} {\n${Object.entries(contents).map(([k, v]) => `\tconst ${k}: ${v};`).join("\n")}\n}`;
|
|
828
705
|
};
|
|
829
706
|
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
program
|
|
707
|
+
//#endregion
|
|
708
|
+
//#region src/cli/index.ts
|
|
709
|
+
const program = new Command();
|
|
710
|
+
program.name(Object.keys(bin)[0]).description(description).version(version);
|
|
833
711
|
program.command("init").argument("[directory]").description("Create a new bf6 mod").action(async (directory) => {
|
|
834
|
-
|
|
712
|
+
await init(directory);
|
|
835
713
|
});
|
|
836
714
|
program.command("build").description("build the bf6 mod").action(async () => {
|
|
837
|
-
|
|
715
|
+
await build();
|
|
838
716
|
});
|
|
839
717
|
program.command("prepare").description("prepare the types for bf6 mod").action(async () => {
|
|
840
|
-
|
|
718
|
+
await prepare();
|
|
841
719
|
});
|
|
842
720
|
program.command("dev").description("watch the changes in src, and recompile as needed").action(async () => {
|
|
843
|
-
|
|
721
|
+
await dev();
|
|
844
722
|
});
|
|
845
723
|
program.command("import").argument("<input>").argument("<output>").description("decompiles the json config of a mod into a new project").action(async (input, output) => {
|
|
846
|
-
|
|
847
|
-
|
|
724
|
+
await importFile(input, output);
|
|
725
|
+
installDependencies(output);
|
|
726
|
+
});
|
|
727
|
+
program.command("log").argument("[input]").description("logs the output from a locally running server").action(async (input) => {
|
|
728
|
+
new Bf6Logger(input).start();
|
|
848
729
|
});
|
|
849
730
|
program.exitOverride((_err) => {
|
|
850
|
-
|
|
731
|
+
if (process.env.EXIT_CODE === "none") process.exit(0);
|
|
851
732
|
});
|
|
852
733
|
program.parse();
|
|
734
|
+
|
|
735
|
+
//#endregion
|
|
736
|
+
export { };
|
|
853
737
|
//# sourceMappingURL=index.js.map
|