@bf6mods/cli 1.3.0 → 1.4.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 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,27 @@ 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.3.0";
23
+ var version$1 = "1.4.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";
35
44
  return MapId$1;
36
45
  }({});
37
46
 
@@ -160,12 +169,16 @@ function extractBf6Strings(bf6Strings, generateFromLiterals) {
160
169
  * Dynamically imports and validates bf6.config.ts
161
170
  */
162
171
  async function getBf6Config(rootDir) {
163
- globalThis.defineBf6Config = (c) => c;
164
- globalThis.MapId = MapId;
165
- const result = await createJiti(rootDir, { interopDefault: true }).import("./bf6.config", { default: true });
166
- delete globalThis.defineBf6Config;
167
- delete globalThis.MapId;
168
- return result;
172
+ try {
173
+ globalThis.defineBf6Config = (c) => c;
174
+ globalThis.MapId = MapId;
175
+ const result = await createJiti(rootDir, { interopDefault: true }).import("./bf6.config", { default: true });
176
+ delete globalThis.defineBf6Config;
177
+ delete globalThis.MapId;
178
+ return result;
179
+ } catch (_error) {
180
+ return;
181
+ }
169
182
  }
170
183
  /**
171
184
  * Builds the entire mod project once (for production or single run).
@@ -173,6 +186,7 @@ async function getBf6Config(rootDir) {
173
186
  async function build() {
174
187
  const workingDir = path.resolve(".");
175
188
  const config = await getBf6Config(workingDir);
189
+ if (!config) throw new Error("Cannot find bf6.config.ts!");
176
190
  const outDir = path.resolve(workingDir, config.outDir);
177
191
  if (fs.existsSync(outDir)) fs.rmSync(outDir, {
178
192
  recursive: true,
@@ -326,9 +340,133 @@ function toBase64(input) {
326
340
  }
327
341
 
328
342
  //#endregion
329
- //#region src/cli/deploy.ts
330
- async function deploy(input, _authCode) {
331
- printToConsole(`Coming soon...`);
343
+ //#region src/cli/deploy/authenticate.ts
344
+ async function getSessionIdFromCookies() {
345
+ let puppeteer;
346
+ try {
347
+ puppeteer = await importGlobal("puppeteer");
348
+ } catch (_error) {
349
+ printToConsole(`${colors.red.bold("✗")} Puppeteer could not be loaded.`);
350
+ console.error("It looks like Puppeteer is not installed in this environment.");
351
+ console.error(`${colors.green.bold("✓")} Fix: install Puppeteer locally in the CLI package:`);
352
+ console.error(" npm i -g puppeteer");
353
+ console.error("\nIf you prefer not to install a browser automation tool,");
354
+ console.error("you can instead provide an auth code using the --auth-code option:");
355
+ console.error(" bf6mods deploy <input> --auth-code=<code>\n");
356
+ process.exit(0);
357
+ }
358
+ const profileDir = path.join(os.homedir(), ".bf6mods-browser");
359
+ const browser = await puppeteer.launch({
360
+ headless: false,
361
+ defaultViewport: null,
362
+ userDataDir: profileDir
363
+ });
364
+ const page = await browser.newPage();
365
+ await page.goto("https://portal.battlefield.com/bf6/experiences");
366
+ console.log("Please log in to Battlefield Portal if needed...");
367
+ await page.waitForFunction(() => {
368
+ return document.cookie.includes("bf6sessionId");
369
+ }, { timeout: 0 });
370
+ console.log(`${colors.green.bold("✓")} Login detected!`);
371
+ const cookies = await page.cookies();
372
+ await browser.close();
373
+ return cookies.find((cookie) => cookie.name === "bf6sessionId")?.value;
374
+ }
375
+ async function tryAuthenticateWithSessionId(sessionId) {
376
+ await clients.authenticate({ sessionId });
377
+ try {
378
+ await clients.play.getProgressionTypes({});
379
+ } catch (error) {
380
+ if (!(error instanceof Error)) return false;
381
+ if (error.message === "[unauthenticated]") return false;
382
+ }
383
+ return true;
384
+ }
385
+ async function authenticate(sessionIdParam) {
386
+ printToConsole(`🔑 Attempting authentication…`);
387
+ if (sessionIdParam) {
388
+ if (await tryAuthenticateWithSessionId(sessionIdParam)) {
389
+ await keytar.setPassword("bf6mods", "sessionId", sessionIdParam);
390
+ return true;
391
+ }
392
+ printToConsole(`${colors.red("✗")} Provided sessionId is invalid, ignoring…`);
393
+ }
394
+ const storedSessionId = await keytar.getPassword("bf6mods", "sessionId");
395
+ if (storedSessionId) {
396
+ if (await tryAuthenticateWithSessionId(storedSessionId)) return true;
397
+ printToConsole(`${colors.yellow("⚠")} Stored sessionId expired, removing…`);
398
+ await keytar.deletePassword("bf6mods", "sessionId");
399
+ }
400
+ printToConsole(`🌐 Launching browser to authenticate…`);
401
+ const cookieSessionId = await getSessionIdFromCookies();
402
+ if (!cookieSessionId) {
403
+ printToConsole(`${colors.red("✗")} Failed to retrieve Battlefield session, exiting.`, true);
404
+ return false;
405
+ }
406
+ if (await tryAuthenticateWithSessionId(cookieSessionId)) {
407
+ await keytar.setPassword("bf6mods", "sessionId", cookieSessionId);
408
+ return true;
409
+ }
410
+ printToConsole(`${colors.red("✗")} Authentication failed. Session may have expired immediately.`, true);
411
+ return false;
412
+ }
413
+ async function alwaysAuthenticatedRequest(method, request, sessionId, options) {
414
+ const fn = clients.play[method];
415
+ try {
416
+ return await fn(request, options);
417
+ } catch (error) {
418
+ if (error instanceof Error && error.message === "[unauthenticated]") {
419
+ printToConsole("🔄 Session expired. Re-authenticating…");
420
+ if (!await authenticate(sessionId)) throw error;
421
+ return await fn(request, options);
422
+ }
423
+ throw error;
424
+ }
425
+ }
426
+
427
+ //#endregion
428
+ //#region src/cli/deploy/index.ts
429
+ const clients = new Clients();
430
+ async function deploy({ input, sessionIdParam, modId }) {
431
+ printToConsole("To be implemented in a future release");
432
+ process.exit(0);
433
+ const rootDir = path.resolve(".");
434
+ if (!input) input = path.resolve(rootDir, "dist", "mod.json");
435
+ printToConsole(`🚀 Starting deploy for ${colors.cyan(input)}…`);
436
+ if (!fs.existsSync(input)) return printToConsole(`${colors.red.bold("✗")} File ${colors.cyan(input)} does not exist!`, true);
437
+ const mod = JSON.parse(fs.readFileSync(input, { encoding: "utf-8" }));
438
+ const blueprints = await alwaysAuthenticatedRequest("getScheduledBlueprints", {}, sessionIdParam);
439
+ console.log("blueprints", blueprints.blueprintIds);
440
+ await alwaysAuthenticatedRequest("getBlueprintsById", { blueprintIds: blueprints.blueprintIds }, sessionIdParam);
441
+ const config = await getBf6Config(rootDir);
442
+ let id = modId;
443
+ if (!config) printToConsole(`Cannot find bf6mods config in dir ${rootDir}, proceeding without it.`);
444
+ else if (config?.id) id = config.id;
445
+ else printToConsole(`No id specified in bf6.config.ts! Defaulting to experience with same name!`);
446
+ if (!id) {
447
+ const owned = await alwaysAuthenticatedRequest("getOwnedPlayElementsV2", {
448
+ includeDenied: true,
449
+ publishStates: [
450
+ Generated_pb.PublishStateType.Draft,
451
+ Generated_pb.PublishStateType.Published,
452
+ Generated_pb.PublishStateType.Error
453
+ ]
454
+ }, sessionIdParam);
455
+ console.log("owned:", owned.playElements.map((owned$1) => owned$1.playElement));
456
+ const found = owned.playElements.find((element) => element?.playElement?.name === mod.name);
457
+ if (found?.playElement?.id) id = found.playElement.id;
458
+ }
459
+ if (!id) {
460
+ 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!`);
461
+ process.exit(0);
462
+ }
463
+ const playElementResponse = await alwaysAuthenticatedRequest("getPlayElement", {
464
+ id,
465
+ includeDenied: true
466
+ }, sessionIdParam);
467
+ if (!playElementResponse.playElement || !playElementResponse.playElementDesign || !playElementResponse.progressionMode) throw new Error("Cannot find essential attribute in mod, please report to GitHub!");
468
+ if (!mod.mapRotation) throw new Error("You must specify at least one map!");
469
+ console.log("playElementResponse:", playElementResponse);
332
470
  }
333
471
 
334
472
  //#endregion
@@ -396,7 +534,8 @@ var Bf6Logger = class Bf6Logger {
396
534
  //#region src/cli/dev.ts
397
535
  async function dev() {
398
536
  const workingDir = path.resolve(".");
399
- let config = await getBf6Config(workingDir);
537
+ const config = await getBf6Config(workingDir);
538
+ if (!config) throw new Error("Cannot find bf6.config.ts!");
400
539
  const outDir = path.resolve(workingDir, config.outDir);
401
540
  fs.mkdirSync(outDir, { recursive: true });
402
541
  printToConsole(colors.cyan(`▶ Starting dev for ${config.name}`));
@@ -420,22 +559,22 @@ async function dev() {
420
559
  }
421
560
  };
422
561
  const debouncedRebuild = debounce(rebuild, 200);
423
- async function collectWatchTargets() {
562
+ async function collectWatchTargets(config$1) {
424
563
  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));
564
+ if (config$1.entrypoint) targets.push(path.resolve(workingDir, config$1.entrypoint));
565
+ if (config$1.scenes) for (const [, scene] of config$1.scenes) targets.push(path.resolve(workingDir, scene));
566
+ if (config$1.strings) targets.push(path.resolve(workingDir, config$1.strings));
428
567
  for await (const entry of glob("bf6.config.*")) targets.push(entry);
429
568
  for await (const entry of glob("src/**/*")) targets.push(entry);
430
569
  return targets;
431
570
  }
432
- async function setupWatcher() {
571
+ async function setupWatcher(config$1) {
433
572
  if (watcher) {
434
573
  await watcher.close();
435
574
  printToConsole(colors.grey("♻ Reloading watcher due to config change..."));
436
575
  await new Promise((r) => setTimeout(r, 10));
437
576
  }
438
- const watchTargets = await collectWatchTargets();
577
+ const watchTargets = await collectWatchTargets(config$1);
439
578
  printToConsole(colors.cyan(`👀 Watching ${watchTargets.length} files...`));
440
579
  watcher = chokidar.watch(watchTargets, {
441
580
  persistent: true,
@@ -458,8 +597,9 @@ async function dev() {
458
597
  watcher.on("change", async (file) => {
459
598
  if (file.includes("bf6.config.")) {
460
599
  printToConsole(colors.magenta("⚙ Config changed — reloading watcher..."));
461
- config = await getBf6Config(workingDir);
462
- await setupWatcher();
600
+ const newConfig = await getBf6Config(workingDir);
601
+ if (!newConfig) throw new Error("Cannot find bf6.config.ts!");
602
+ await setupWatcher(newConfig);
463
603
  return;
464
604
  }
465
605
  debouncedRebuild(file);
@@ -472,7 +612,7 @@ async function dev() {
472
612
  await watcher?.close();
473
613
  process.exit(0);
474
614
  });
475
- await setupWatcher();
615
+ await setupWatcher(config);
476
616
  let logger;
477
617
  try {
478
618
  logger = new Bf6Logger();
@@ -485,7 +625,7 @@ async function dev() {
485
625
 
486
626
  //#endregion
487
627
  //#region ../sdk/package.json
488
- var version = "1.3.0";
628
+ var version = "1.4.0";
489
629
 
490
630
  //#endregion
491
631
  //#region src/cli/init.ts
@@ -537,6 +677,9 @@ async function startProject(destination, template, name) {
537
677
  fs.cpSync(templateDir, destination, { recursive: true });
538
678
  }
539
679
  fs.cpSync(path.resolve(templatesDir, "All"), destination, { recursive: true });
680
+ const gitignorePath = path.join(destination, "_gitignore");
681
+ const dotGitignorePath = path.join(destination, ".gitignore");
682
+ if (fs.existsSync(gitignorePath)) fs.renameSync(gitignorePath, dotGitignorePath);
540
683
  if (name) renameFilesRecursively(destination, name);
541
684
  }
542
685
  function installDependencies(projectDir) {
@@ -804,8 +947,13 @@ program.command("import").argument("<input>").argument("<output>").option("--no-
804
947
  program.command("log").argument("[input]").description("logs the output from a locally running server").action(async (input) => {
805
948
  new Bf6Logger(input).start();
806
949
  });
807
- program.command("deploy").argument("<input>").option("--auth-code", `The auth code to use when deploying`).description("deploys your mod for you, if unauthenticated, it requests reauthentication").action(async (input, options) => {
808
- await deploy(input, options?.authCode);
950
+ 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) => {
951
+ await deploy({
952
+ input,
953
+ sessionIdParam: options?.sessionId,
954
+ publish: options?.publish,
955
+ modId: options?.id
956
+ });
809
957
  });
810
958
  program.exitOverride((_err) => {
811
959
  if (process.env.EXIT_CODE === "none") process.exit(0);