@bf6mods/cli 1.3.0 → 1.5.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 +11 -0
- package/dist/cli/index.js +176 -24
- package/dist/cli/index.js.map +1 -1
- package/dist/resources/maps/spatial/MP_Badlands-ModBuilderCustom0.spatial.json +197 -0
- package/dist/resources/maps/spatial/MP_Eastwood-ModBuilderCustom0.spatial.json +221 -0
- package/dist/resources/maps/spatial/MP_Granite_ClubHouse_Portal-ModBuilderCustom0.spatial.json +226 -0
- package/dist/resources/maps/spatial/MP_Granite_MainStreet_Portal-ModBuilderCustom0.json +199 -0
- package/dist/resources/maps/spatial/MP_Granite_Marina_Portal-ModBuilderCustom0.json +199 -0
- package/dist/resources/maps/spatial/MP_Granite_MilitaryRnD_Portal-ModBuilderCustom0.json +199 -0
- package/dist/resources/maps/spatial/MP_Granite_MilitaryStorage_Portal-ModBuilderCustom0.json +199 -0
- package/dist/resources/maps/spatial/MP_Granite_TechCampus_Portal-ModBuilderCustom0.spatial.json +199 -0
- package/dist/resources/maps/spatial/MP_Portal_Sand-ModBuilderCustom0.spatial.json +191 -0
- package/dist/resources/prepare/types/config.ts +21 -1
- package/dist/resources/templates/All/_gitignore +3 -0
- package/dist/resources/templates/BombSquad.json +1 -1
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -22,8 +22,19 @@ npx @bf6mods/cli import <export file> <output directory>
|
|
|
22
22
|
|
|
23
23
|
## Deploying Project to Portal
|
|
24
24
|
|
|
25
|
+
There are two different ways of deploying a project to portal.
|
|
26
|
+
|
|
27
|
+
### Manually Import
|
|
28
|
+
|
|
25
29
|
Just run `npm run build` in your project dir, open [portal.battlefield.com](https://portal.battlefield.com), click import, and select the `dist/mod.json` file.
|
|
26
30
|
|
|
31
|
+
### `npx @bf6mods/cli deploy`
|
|
32
|
+
|
|
33
|
+
To use this, you must first install puppeteer via `npm -g i puppeteer`, but after doing so you can just run this command, and you will
|
|
34
|
+
have your project deploy automatically for you.
|
|
35
|
+
|
|
36
|
+
Important note on this. The [portal.battlefield.com](https://portal.battlefield.com) will not update showing the changes from the deployed code. This is due to the browsers cache.
|
|
37
|
+
|
|
27
38
|
## Features
|
|
28
39
|
|
|
29
40
|
- Use multiple different files, instead of just one large TypeScript file!
|
package/dist/cli/index.js
CHANGED
|
@@ -8,6 +8,10 @@ import { AttachmentType } from "@bf6mods/sdk";
|
|
|
8
8
|
import { createJiti } from "jiti";
|
|
9
9
|
import { rolldown } from "rolldown";
|
|
10
10
|
import stripAnsi from "strip-ansi";
|
|
11
|
+
import { Clients, Generated_pb } from "@bf6mods/portal";
|
|
12
|
+
import os from "node:os";
|
|
13
|
+
import { importGlobal } from "import-global";
|
|
14
|
+
import keytar from "keytar";
|
|
11
15
|
import { glob } from "node:fs/promises";
|
|
12
16
|
import chokidar from "chokidar";
|
|
13
17
|
import stringifyObject from "stringify-object";
|
|
@@ -16,22 +20,31 @@ import * as prompts from "@clack/prompts";
|
|
|
16
20
|
import { genExport, genInlineTypeImport } from "knitwork";
|
|
17
21
|
|
|
18
22
|
//#region package.json
|
|
19
|
-
var version$1 = "1.
|
|
23
|
+
var version$1 = "1.5.0";
|
|
20
24
|
var description = "CLI and library for bundling BF6 mods";
|
|
21
25
|
var bin = { "bf6mods": "./dist/cli/index.js" };
|
|
22
26
|
|
|
23
27
|
//#endregion
|
|
24
28
|
//#region src/resources/prepare/types/config.ts
|
|
25
29
|
let MapId = /* @__PURE__ */ function(MapId$1) {
|
|
26
|
-
MapId$1["FireStorm"] = "MP_FireStorm-ModBuilderCustom0";
|
|
27
30
|
MapId$1["SiegeOfCairo"] = "MP_Abbasid-ModBuilderCustom0";
|
|
28
31
|
MapId$1["EmpireState"] = "MP_Aftermath-ModBuilderCustom0";
|
|
32
|
+
MapId$1["BlackwellFields"] = "MP_Badlands-ModBuilderCustom0";
|
|
29
33
|
MapId$1["IberianOffensive"] = "MP_Battery-ModBuilderCustom0";
|
|
30
34
|
MapId$1["LiberationPeak"] = "MP_Capstone-ModBuilderCustom0";
|
|
31
35
|
MapId$1["ManhattanBridge"] = "MP_Dumbo-ModBuilderCustom0";
|
|
36
|
+
MapId$1["Eastwoord"] = "MP_Eastwood-ModBuilderCustom0";
|
|
37
|
+
MapId$1["FireStorm"] = "MP_FireStorm-ModBuilderCustom0";
|
|
32
38
|
MapId$1["SaintsQuarter"] = "MP_Limestone-ModBuilderCustom0";
|
|
33
39
|
MapId$1["NewSobekCity"] = "MP_Outskirts-ModBuilderCustom0";
|
|
34
40
|
MapId$1["MirakValley"] = "MP_Tungsten-ModBuilderCustom0";
|
|
41
|
+
MapId$1["GolfCourse"] = "MP_Granite_ClubHouse_Portal-ModBuilderCustom0";
|
|
42
|
+
MapId$1["DefenseNexus"] = "MP_Granite_TechCampus_Portal-ModBuilderCustom0";
|
|
43
|
+
MapId$1["PortalSandbox"] = "MP_Portal_Sand-ModBuilderCustom0";
|
|
44
|
+
MapId$1["Marina"] = "MP_Granite_Marina_Portal-ModBuilderCustom0";
|
|
45
|
+
MapId$1["Downtown"] = "MP_Granite_MainStreet_Portal-ModBuilderCustom0";
|
|
46
|
+
MapId$1["Area22B"] = "MP_Granite_MilitaryRnD_Portal-ModBuilderCustom0";
|
|
47
|
+
MapId$1["RedlineStorage"] = "MP_Granite_MilitaryStorage_Portal-ModBuilderCustom0";
|
|
35
48
|
return MapId$1;
|
|
36
49
|
}({});
|
|
37
50
|
|
|
@@ -160,12 +173,16 @@ function extractBf6Strings(bf6Strings, generateFromLiterals) {
|
|
|
160
173
|
* Dynamically imports and validates bf6.config.ts
|
|
161
174
|
*/
|
|
162
175
|
async function getBf6Config(rootDir) {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
176
|
+
try {
|
|
177
|
+
globalThis.defineBf6Config = (c) => c;
|
|
178
|
+
globalThis.MapId = MapId;
|
|
179
|
+
const result = await createJiti(rootDir, { interopDefault: true }).import("./bf6.config", { default: true });
|
|
180
|
+
delete globalThis.defineBf6Config;
|
|
181
|
+
delete globalThis.MapId;
|
|
182
|
+
return result;
|
|
183
|
+
} catch (_error) {
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
169
186
|
}
|
|
170
187
|
/**
|
|
171
188
|
* Builds the entire mod project once (for production or single run).
|
|
@@ -173,6 +190,7 @@ async function getBf6Config(rootDir) {
|
|
|
173
190
|
async function build() {
|
|
174
191
|
const workingDir = path.resolve(".");
|
|
175
192
|
const config = await getBf6Config(workingDir);
|
|
193
|
+
if (!config) throw new Error("Cannot find bf6.config.ts!");
|
|
176
194
|
const outDir = path.resolve(workingDir, config.outDir);
|
|
177
195
|
if (fs.existsSync(outDir)) fs.rmSync(outDir, {
|
|
178
196
|
recursive: true,
|
|
@@ -326,9 +344,133 @@ function toBase64(input) {
|
|
|
326
344
|
}
|
|
327
345
|
|
|
328
346
|
//#endregion
|
|
329
|
-
//#region src/cli/deploy.ts
|
|
330
|
-
async function
|
|
331
|
-
|
|
347
|
+
//#region src/cli/deploy/authenticate.ts
|
|
348
|
+
async function getSessionIdFromCookies() {
|
|
349
|
+
let puppeteer;
|
|
350
|
+
try {
|
|
351
|
+
puppeteer = await importGlobal("puppeteer");
|
|
352
|
+
} catch (_error) {
|
|
353
|
+
printToConsole(`${colors.red.bold("✗")} Puppeteer could not be loaded.`);
|
|
354
|
+
console.error("It looks like Puppeteer is not installed in this environment.");
|
|
355
|
+
console.error(`${colors.green.bold("✓")} Fix: install Puppeteer locally in the CLI package:`);
|
|
356
|
+
console.error(" npm i -g puppeteer");
|
|
357
|
+
console.error("\nIf you prefer not to install a browser automation tool,");
|
|
358
|
+
console.error("you can instead provide an auth code using the --auth-code option:");
|
|
359
|
+
console.error(" bf6mods deploy <input> --auth-code=<code>\n");
|
|
360
|
+
process.exit(0);
|
|
361
|
+
}
|
|
362
|
+
const profileDir = path.join(os.homedir(), ".bf6mods-browser");
|
|
363
|
+
const browser = await puppeteer.launch({
|
|
364
|
+
headless: false,
|
|
365
|
+
defaultViewport: null,
|
|
366
|
+
userDataDir: profileDir
|
|
367
|
+
});
|
|
368
|
+
const page = await browser.newPage();
|
|
369
|
+
await page.goto("https://portal.battlefield.com/bf6/experiences");
|
|
370
|
+
console.log("Please log in to Battlefield Portal if needed...");
|
|
371
|
+
await page.waitForFunction(() => {
|
|
372
|
+
return document.cookie.includes("bf6sessionId");
|
|
373
|
+
}, { timeout: 0 });
|
|
374
|
+
console.log(`${colors.green.bold("✓")} Login detected!`);
|
|
375
|
+
const cookies = await page.cookies();
|
|
376
|
+
await browser.close();
|
|
377
|
+
return cookies.find((cookie) => cookie.name === "bf6sessionId")?.value;
|
|
378
|
+
}
|
|
379
|
+
async function tryAuthenticateWithSessionId(sessionId) {
|
|
380
|
+
await clients.authenticate({ sessionId });
|
|
381
|
+
try {
|
|
382
|
+
await clients.play.getProgressionTypes({});
|
|
383
|
+
} catch (error) {
|
|
384
|
+
if (!(error instanceof Error)) return false;
|
|
385
|
+
if (error.message === "[unauthenticated]") return false;
|
|
386
|
+
}
|
|
387
|
+
return true;
|
|
388
|
+
}
|
|
389
|
+
async function authenticate(sessionIdParam) {
|
|
390
|
+
printToConsole(`🔑 Attempting authentication…`);
|
|
391
|
+
if (sessionIdParam) {
|
|
392
|
+
if (await tryAuthenticateWithSessionId(sessionIdParam)) {
|
|
393
|
+
await keytar.setPassword("bf6mods", "sessionId", sessionIdParam);
|
|
394
|
+
return true;
|
|
395
|
+
}
|
|
396
|
+
printToConsole(`${colors.red("✗")} Provided sessionId is invalid, ignoring…`);
|
|
397
|
+
}
|
|
398
|
+
const storedSessionId = await keytar.getPassword("bf6mods", "sessionId");
|
|
399
|
+
if (storedSessionId) {
|
|
400
|
+
if (await tryAuthenticateWithSessionId(storedSessionId)) return true;
|
|
401
|
+
printToConsole(`${colors.yellow("⚠")} Stored sessionId expired, removing…`);
|
|
402
|
+
await keytar.deletePassword("bf6mods", "sessionId");
|
|
403
|
+
}
|
|
404
|
+
printToConsole(`🌐 Launching browser to authenticate…`);
|
|
405
|
+
const cookieSessionId = await getSessionIdFromCookies();
|
|
406
|
+
if (!cookieSessionId) {
|
|
407
|
+
printToConsole(`${colors.red("✗")} Failed to retrieve Battlefield session, exiting.`, true);
|
|
408
|
+
return false;
|
|
409
|
+
}
|
|
410
|
+
if (await tryAuthenticateWithSessionId(cookieSessionId)) {
|
|
411
|
+
await keytar.setPassword("bf6mods", "sessionId", cookieSessionId);
|
|
412
|
+
return true;
|
|
413
|
+
}
|
|
414
|
+
printToConsole(`${colors.red("✗")} Authentication failed. Session may have expired immediately.`, true);
|
|
415
|
+
return false;
|
|
416
|
+
}
|
|
417
|
+
async function alwaysAuthenticatedRequest(method, request, sessionId, options) {
|
|
418
|
+
const fn = clients.play[method];
|
|
419
|
+
try {
|
|
420
|
+
return await fn(request, options);
|
|
421
|
+
} catch (error) {
|
|
422
|
+
if (error instanceof Error && error.message === "[unauthenticated]") {
|
|
423
|
+
printToConsole("🔄 Session expired. Re-authenticating…");
|
|
424
|
+
if (!await authenticate(sessionId)) throw error;
|
|
425
|
+
return await fn(request, options);
|
|
426
|
+
}
|
|
427
|
+
throw error;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
//#endregion
|
|
432
|
+
//#region src/cli/deploy/index.ts
|
|
433
|
+
const clients = new Clients();
|
|
434
|
+
async function deploy({ input, sessionIdParam, modId }) {
|
|
435
|
+
printToConsole("To be implemented in a future release");
|
|
436
|
+
process.exit(0);
|
|
437
|
+
const rootDir = path.resolve(".");
|
|
438
|
+
if (!input) input = path.resolve(rootDir, "dist", "mod.json");
|
|
439
|
+
printToConsole(`🚀 Starting deploy for ${colors.cyan(input)}…`);
|
|
440
|
+
if (!fs.existsSync(input)) return printToConsole(`${colors.red.bold("✗")} File ${colors.cyan(input)} does not exist!`, true);
|
|
441
|
+
const mod = JSON.parse(fs.readFileSync(input, { encoding: "utf-8" }));
|
|
442
|
+
const blueprints = await alwaysAuthenticatedRequest("getScheduledBlueprints", {}, sessionIdParam);
|
|
443
|
+
console.log("blueprints", blueprints.blueprintIds);
|
|
444
|
+
await alwaysAuthenticatedRequest("getBlueprintsById", { blueprintIds: blueprints.blueprintIds }, sessionIdParam);
|
|
445
|
+
const config = await getBf6Config(rootDir);
|
|
446
|
+
let id = modId;
|
|
447
|
+
if (!config) printToConsole(`Cannot find bf6mods config in dir ${rootDir}, proceeding without it.`);
|
|
448
|
+
else if (config?.id) id = config.id;
|
|
449
|
+
else printToConsole(`No id specified in bf6.config.ts! Defaulting to experience with same name!`);
|
|
450
|
+
if (!id) {
|
|
451
|
+
const owned = await alwaysAuthenticatedRequest("getOwnedPlayElementsV2", {
|
|
452
|
+
includeDenied: true,
|
|
453
|
+
publishStates: [
|
|
454
|
+
Generated_pb.PublishStateType.Draft,
|
|
455
|
+
Generated_pb.PublishStateType.Published,
|
|
456
|
+
Generated_pb.PublishStateType.Error
|
|
457
|
+
]
|
|
458
|
+
}, sessionIdParam);
|
|
459
|
+
console.log("owned:", owned.playElements.map((owned$1) => owned$1.playElement));
|
|
460
|
+
const found = owned.playElements.find((element) => element?.playElement?.name === mod.name);
|
|
461
|
+
if (found?.playElement?.id) id = found.playElement.id;
|
|
462
|
+
}
|
|
463
|
+
if (!id) {
|
|
464
|
+
printToConsole(`${colors.red.bold("✗")} Cannot find an id for your mod to deploy with! Please specify the id explicitly with \`--id\` or by having a mod with the same name!`);
|
|
465
|
+
process.exit(0);
|
|
466
|
+
}
|
|
467
|
+
const playElementResponse = await alwaysAuthenticatedRequest("getPlayElement", {
|
|
468
|
+
id,
|
|
469
|
+
includeDenied: true
|
|
470
|
+
}, sessionIdParam);
|
|
471
|
+
if (!playElementResponse.playElement || !playElementResponse.playElementDesign || !playElementResponse.progressionMode) throw new Error("Cannot find essential attribute in mod, please report to GitHub!");
|
|
472
|
+
if (!mod.mapRotation) throw new Error("You must specify at least one map!");
|
|
473
|
+
console.log("playElementResponse:", playElementResponse);
|
|
332
474
|
}
|
|
333
475
|
|
|
334
476
|
//#endregion
|
|
@@ -396,7 +538,8 @@ var Bf6Logger = class Bf6Logger {
|
|
|
396
538
|
//#region src/cli/dev.ts
|
|
397
539
|
async function dev() {
|
|
398
540
|
const workingDir = path.resolve(".");
|
|
399
|
-
|
|
541
|
+
const config = await getBf6Config(workingDir);
|
|
542
|
+
if (!config) throw new Error("Cannot find bf6.config.ts!");
|
|
400
543
|
const outDir = path.resolve(workingDir, config.outDir);
|
|
401
544
|
fs.mkdirSync(outDir, { recursive: true });
|
|
402
545
|
printToConsole(colors.cyan(`▶ Starting dev for ${config.name}`));
|
|
@@ -420,22 +563,22 @@ async function dev() {
|
|
|
420
563
|
}
|
|
421
564
|
};
|
|
422
565
|
const debouncedRebuild = debounce(rebuild, 200);
|
|
423
|
-
async function collectWatchTargets() {
|
|
566
|
+
async function collectWatchTargets(config$1) {
|
|
424
567
|
const targets = [];
|
|
425
|
-
if (config.entrypoint) targets.push(path.resolve(workingDir, config.entrypoint));
|
|
426
|
-
if (config.scenes) for (const [, scene] of config.scenes) targets.push(path.resolve(workingDir, scene));
|
|
427
|
-
if (config.strings) targets.push(path.resolve(workingDir, config.strings));
|
|
568
|
+
if (config$1.entrypoint) targets.push(path.resolve(workingDir, config$1.entrypoint));
|
|
569
|
+
if (config$1.scenes) for (const [, scene] of config$1.scenes) targets.push(path.resolve(workingDir, scene));
|
|
570
|
+
if (config$1.strings) targets.push(path.resolve(workingDir, config$1.strings));
|
|
428
571
|
for await (const entry of glob("bf6.config.*")) targets.push(entry);
|
|
429
572
|
for await (const entry of glob("src/**/*")) targets.push(entry);
|
|
430
573
|
return targets;
|
|
431
574
|
}
|
|
432
|
-
async function setupWatcher() {
|
|
575
|
+
async function setupWatcher(config$1) {
|
|
433
576
|
if (watcher) {
|
|
434
577
|
await watcher.close();
|
|
435
578
|
printToConsole(colors.grey("♻ Reloading watcher due to config change..."));
|
|
436
579
|
await new Promise((r) => setTimeout(r, 10));
|
|
437
580
|
}
|
|
438
|
-
const watchTargets = await collectWatchTargets();
|
|
581
|
+
const watchTargets = await collectWatchTargets(config$1);
|
|
439
582
|
printToConsole(colors.cyan(`👀 Watching ${watchTargets.length} files...`));
|
|
440
583
|
watcher = chokidar.watch(watchTargets, {
|
|
441
584
|
persistent: true,
|
|
@@ -458,8 +601,9 @@ async function dev() {
|
|
|
458
601
|
watcher.on("change", async (file) => {
|
|
459
602
|
if (file.includes("bf6.config.")) {
|
|
460
603
|
printToConsole(colors.magenta("⚙ Config changed — reloading watcher..."));
|
|
461
|
-
|
|
462
|
-
|
|
604
|
+
const newConfig = await getBf6Config(workingDir);
|
|
605
|
+
if (!newConfig) throw new Error("Cannot find bf6.config.ts!");
|
|
606
|
+
await setupWatcher(newConfig);
|
|
463
607
|
return;
|
|
464
608
|
}
|
|
465
609
|
debouncedRebuild(file);
|
|
@@ -472,7 +616,7 @@ async function dev() {
|
|
|
472
616
|
await watcher?.close();
|
|
473
617
|
process.exit(0);
|
|
474
618
|
});
|
|
475
|
-
await setupWatcher();
|
|
619
|
+
await setupWatcher(config);
|
|
476
620
|
let logger;
|
|
477
621
|
try {
|
|
478
622
|
logger = new Bf6Logger();
|
|
@@ -485,7 +629,7 @@ async function dev() {
|
|
|
485
629
|
|
|
486
630
|
//#endregion
|
|
487
631
|
//#region ../sdk/package.json
|
|
488
|
-
var version = "1.
|
|
632
|
+
var version = "1.5.0";
|
|
489
633
|
|
|
490
634
|
//#endregion
|
|
491
635
|
//#region src/cli/init.ts
|
|
@@ -537,6 +681,9 @@ async function startProject(destination, template, name) {
|
|
|
537
681
|
fs.cpSync(templateDir, destination, { recursive: true });
|
|
538
682
|
}
|
|
539
683
|
fs.cpSync(path.resolve(templatesDir, "All"), destination, { recursive: true });
|
|
684
|
+
const gitignorePath = path.join(destination, "_gitignore");
|
|
685
|
+
const dotGitignorePath = path.join(destination, ".gitignore");
|
|
686
|
+
if (fs.existsSync(gitignorePath)) fs.renameSync(gitignorePath, dotGitignorePath);
|
|
540
687
|
if (name) renameFilesRecursively(destination, name);
|
|
541
688
|
}
|
|
542
689
|
function installDependencies(projectDir) {
|
|
@@ -804,8 +951,13 @@ program.command("import").argument("<input>").argument("<output>").option("--no-
|
|
|
804
951
|
program.command("log").argument("[input]").description("logs the output from a locally running server").action(async (input) => {
|
|
805
952
|
new Bf6Logger(input).start();
|
|
806
953
|
});
|
|
807
|
-
program.command("deploy").argument("
|
|
808
|
-
await deploy(
|
|
954
|
+
program.command("deploy").argument("[input]").option("--session-id <session id>", `The session id to use when deploying, should be web-xxxx`).option("--publish", `Whether to publish your mod`).option("--id <id>", `The id of your mod`).description("deploys your mod for you, if unauthenticated, it requests reauthentication").action(async (input, options) => {
|
|
955
|
+
await deploy({
|
|
956
|
+
input,
|
|
957
|
+
sessionIdParam: options?.sessionId,
|
|
958
|
+
publish: options?.publish,
|
|
959
|
+
modId: options?.id
|
|
960
|
+
});
|
|
809
961
|
});
|
|
810
962
|
program.exitOverride((_err) => {
|
|
811
963
|
if (process.env.EXIT_CODE === "none") process.exit(0);
|