omegga 1.8.0 → 1.9.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.
@@ -4,6 +4,7 @@ const logger = require("../logger.js");
4
4
  const softconfig = require("../softconfig.js");
5
5
  const version = require("../version.js");
6
6
  const index$2 = require("../util/index.js");
7
+ const brdb = require("../util/brdb.js");
7
8
  const file = require("../util/file.js");
8
9
  const index = require("../webserver/backend/index.js");
9
10
  const brs = require("brs-js");
@@ -17,6 +18,26 @@ const index$1 = require("./matchers/index.js");
17
18
  const plugin = require("./plugin.js");
18
19
  const wrapper = require("./wrapper.js");
19
20
  const MISSING_CMD = '"Command not found. Type <color=\\"ffff00\\">/help</> for a list of commands or <color=\\"ffff00\\">/plugins</> for plugin information."';
21
+ const WHOLE_WORLD_EXTENT = 1e9;
22
+ function brsRemoved(version2) {
23
+ return version2 > 0 && version2 >= commands.PREFAB_VERSION;
24
+ }
25
+ function warnRemoved(version2, method, since, release, replacement) {
26
+ if (!(version2 > 0 && version2 >= since)) return false;
27
+ logger.default.warnp(
28
+ `omegga.${method}() uses a console command removed in Brickadia ${release}. This call has no effect - use ${replacement} instead.`
29
+ );
30
+ return true;
31
+ }
32
+ function writeTempPrefab(omegga, saveData) {
33
+ const ref = omegga._tempSavePrefix + Date.now() + "_" + omegga._tempCounter.save++;
34
+ const file$1 = path.join(omegga.prefabPath, ref + ".brz");
35
+ if (!file$1.startsWith(omegga.prefabPath))
36
+ throw "prefab file not in Saved/Prefabs directory";
37
+ file.mkdir(omegga.prefabPath);
38
+ fs.writeFileSync(file$1, new Uint8Array(brs.writeBrzLegacy(saveData)));
39
+ return { ref, file: file$1 };
40
+ }
20
41
  class Omegga extends wrapper.default {
21
42
  /**
22
43
  * Omegga instance
@@ -53,6 +74,7 @@ class Omegga extends wrapper.default {
53
74
  const savedDir = cfg.server.savedDir ?? softconfig.CONFIG_SAVED_DIR;
54
75
  this.savePath = path.join(this.path, softconfig.DATA_PATH, savedDir, "Builds");
55
76
  this.worldPath = path.join(this.path, softconfig.DATA_PATH, savedDir, "Worlds");
77
+ this.prefabPath = path.join(this.path, softconfig.DATA_PATH, savedDir, "Prefabs");
56
78
  this.presetPath = path.join(this.path, softconfig.DATA_PATH, savedDir, "Presets");
57
79
  this.configPath = path.join(this.path, softconfig.DATA_PATH, savedDir, "Server");
58
80
  logger.default.verbose("Creating directories");
@@ -455,22 +477,49 @@ class Omegga extends wrapper.default {
455
477
  }
456
478
  clearRegion(region, options) {
457
479
  let target = "";
458
- if (options && typeof options.target === "object")
459
- target = " " + options.target.id;
460
- if (typeof target === "string" && !index$2.uuid.match(target)) {
461
- const player = this.getPlayer(target);
462
- if (player) target = " " + player.id;
480
+ const rawTarget = options?.target;
481
+ if (rawTarget) {
482
+ if (typeof rawTarget === "object") target = rawTarget.id;
483
+ else if (index$2.uuid.match(rawTarget)) target = rawTarget;
484
+ else target = this.getPlayer(rawTarget)?.id ?? "";
485
+ }
486
+ const center = region.center.join(" ");
487
+ const extent = region.extent.join(" ");
488
+ if (brsRemoved(this.version)) {
489
+ const bricks = options?.bricks ?? true ? 1 : 0;
490
+ const entities = options?.entities ?? false ? 1 : 0;
491
+ this.writeln(
492
+ `${this.Console.World.ClearRegion} ${center} ${extent} ${bricks} ${entities}${target ? " " + target : ""}`
493
+ );
494
+ return;
463
495
  }
464
496
  this.writeln(
465
- `${this.Console.Bricks.ClearRegion} ${region.center.join(" ")} ${region.extent.join(
466
- " "
467
- )}${target}`
497
+ `${this.Console.Bricks.ClearRegion} ${center} ${extent}${target ? " " + target : ""}`
468
498
  );
469
499
  }
470
- clearAllBricks(quiet = false) {
500
+ clearAllBricks(options = {}) {
501
+ const {
502
+ quiet = false,
503
+ bricks = true,
504
+ entities = false
505
+ } = typeof options === "boolean" ? { quiet: options } : options;
506
+ if (brsRemoved(this.version)) {
507
+ this.writeln(
508
+ `${this.Console.World.ClearAll} ${bricks ? 1 : 0} ${entities ? 1 : 0} ${quiet ? 1 : 0}`
509
+ );
510
+ return;
511
+ }
471
512
  this.writeln(`${this.Console.Bricks.ClearAll} ${quiet ? 1 : ""}`);
472
513
  }
473
514
  saveBricks(saveName, region) {
515
+ if (warnRemoved(
516
+ this.version,
517
+ "saveBricks",
518
+ commands.PREFAB_VERSION,
519
+ "EA3",
520
+ "savePrefab"
521
+ ))
522
+ return;
474
523
  if (!saveName) return;
475
524
  if (!(saveName.startsWith('"') && saveName.endsWith('"')))
476
525
  saveName = `"${saveName}"`;
@@ -483,6 +532,14 @@ class Omegga extends wrapper.default {
483
532
  else this.writeln(`${this.Console.Bricks.Save} ${saveName}`);
484
533
  }
485
534
  async saveBricksAsync(saveName, region) {
535
+ if (warnRemoved(
536
+ this.version,
537
+ "saveBricksAsync",
538
+ commands.PREFAB_VERSION,
539
+ "EA3",
540
+ "savePrefabAsync"
541
+ ))
542
+ return;
486
543
  if (!saveName) return;
487
544
  let saveNameClean = saveName;
488
545
  if (!(saveName.startsWith('"') && saveName.endsWith('"')))
@@ -509,6 +566,14 @@ class Omegga extends wrapper.default {
509
566
  correctPalette = false,
510
567
  correctCustom = false
511
568
  } = {}) {
569
+ if (warnRemoved(
570
+ this.version,
571
+ "loadBricks",
572
+ commands.PREFAB_VERSION,
573
+ "EA3",
574
+ "loadPrefab"
575
+ ))
576
+ return;
512
577
  if (!(saveName.startsWith('"') && saveName.endsWith('"')))
513
578
  saveName = `"${saveName}"`;
514
579
  this.writeln(
@@ -522,6 +587,14 @@ class Omegga extends wrapper.default {
522
587
  correctPalette = false,
523
588
  correctCustom = false
524
589
  } = {}) {
590
+ if (warnRemoved(
591
+ this.version,
592
+ "loadBricksOnPlayer",
593
+ commands.EA2_VERSION,
594
+ "EA2",
595
+ "loadPrefabOnPlayer"
596
+ ))
597
+ return;
525
598
  player = typeof player === "string" ? this.getPlayer(player) : player;
526
599
  if (!player) return;
527
600
  if (!(saveName.startsWith('"') && saveName.endsWith('"')))
@@ -536,6 +609,9 @@ class Omegga extends wrapper.default {
536
609
  getWorlds() {
537
610
  return fs.existsSync(this.worldPath) ? glob.sync(this.worldPath + "/**/*.brdb") : [];
538
611
  }
612
+ getPrefabs() {
613
+ return fs.existsSync(this.prefabPath) ? glob.sync(this.prefabPath + "/**/*.brz") : [];
614
+ }
539
615
  getSavePath(saveName) {
540
616
  const file2 = path.join(
541
617
  this.savePath,
@@ -551,42 +627,12 @@ class Omegga extends wrapper.default {
551
627
  return fs.existsSync(file2) ? file2 : void 0;
552
628
  }
553
629
  async getWorldRevisions(worldName) {
554
- if (!this.started || this.starting || this.stopping) {
555
- throw new Error("Server is not started");
556
- }
557
630
  worldName = worldName.replace(/\.brdb$/i, "");
558
- if (!worldName || !this.getWorldPath(worldName)) {
631
+ const path2 = this.getWorldPath(worldName);
632
+ if (!worldName || !path2) {
559
633
  throw new Error(`World "${worldName}" does not exist`);
560
634
  }
561
- let numRevisions = 0;
562
- const revisionsRaw = await this.watchLogChunk(
563
- `${this.Console.World.ListRevisions} "${worldName}"`,
564
- /^LogBRBundleManager: (There are (?<numRevisions>\d+) revisions|Revision (?<revision>\d+) - (?<date>[\d.-]+): (?<note>.+))$/,
565
- {
566
- last: (match) => Number(match.groups.reverse) === numRevisions,
567
- first: (match) => {
568
- if (match.groups?.numRevisions !== void 0) {
569
- numRevisions = Number(match.groups.numRevisions);
570
- return true;
571
- }
572
- return false;
573
- }
574
- }
575
- );
576
- if (!revisionsRaw) return [];
577
- return revisionsRaw.map((match) => {
578
- if (match.groups?.numRevisions !== void 0) {
579
- return null;
580
- }
581
- const revision = match.groups.revision ? Number(match.groups.revision) : 0;
582
- const dateStr = match.groups.date.replace(
583
- /(\d{4})\.(\d{2})\.(\d{2})-(\d{2})\.(\d{2})\.(\d{2})/,
584
- "$1-$2-$3T$4:$5:$6Z"
585
- );
586
- const date = new Date(dateStr);
587
- const note = match.groups.note || "";
588
- return { index: revision, date, note };
589
- }).filter(Boolean);
635
+ return brdb.readBrdbRevisions(path2) ?? [];
590
636
  }
591
637
  async loadWorld(worldName) {
592
638
  worldName = worldName.replace(/\.brdb$/i, "");
@@ -733,6 +779,14 @@ class Omegga extends wrapper.default {
733
779
  correctPalette = false,
734
780
  correctCustom = false
735
781
  } = {}) {
782
+ if (brsRemoved(this.version)) {
783
+ const { ref, file: file2 } = writeTempPrefab(this, saveData);
784
+ this.loadPrefab(ref, { offX, offY, offZ });
785
+ setTimeout(() => {
786
+ if (fs.existsSync(file2)) fs.unlinkSync(file2);
787
+ }, 5e3);
788
+ return;
789
+ }
736
790
  const saveFile = this._tempSavePrefix + Date.now() + "_" + this._tempCounter.save++;
737
791
  this.writeSaveData(saveFile, saveData);
738
792
  await this.watchLogChunk(
@@ -759,6 +813,22 @@ class Omegga extends wrapper.default {
759
813
  } = {}) {
760
814
  player = typeof player === "string" ? this.getPlayer(player) : player;
761
815
  if (!player) return;
816
+ if (brsRemoved(this.version)) {
817
+ const { ref, file: file2 } = writeTempPrefab(this, saveData);
818
+ this.givePrefabToPlayer(ref, player);
819
+ setTimeout(() => {
820
+ if (fs.existsSync(file2)) fs.unlinkSync(file2);
821
+ }, 5e3);
822
+ return;
823
+ }
824
+ if (warnRemoved(
825
+ this.version,
826
+ "loadSaveDataOnPlayer",
827
+ commands.EA2_VERSION,
828
+ "EA2",
829
+ "loadPrefabOnPlayer"
830
+ ))
831
+ return;
762
832
  const saveFile = this._tempSavePrefix + Date.now() + "_" + this._tempCounter.save++;
763
833
  this.writeSaveData(saveFile, saveData);
764
834
  await this.watchLogChunk(
@@ -777,6 +847,54 @@ class Omegga extends wrapper.default {
777
847
  }
778
848
  }
779
849
  async getSaveData(region) {
850
+ if (brsRemoved(this.version)) {
851
+ const ref = this._tempSavePrefix + Date.now() + "_" + this._tempCounter.save++;
852
+ const file2 = await this.savePrefabAsync(ref, { region });
853
+ if (!file2) return void 0;
854
+ try {
855
+ const MAIN_GRID = 1;
856
+ const reader = brs.WorldReader.from(new Uint8Array(fs.readFileSync(file2)));
857
+ const bricks = [...reader.bricks(MAIN_GRID)].map((b) => ({
858
+ ...b,
859
+ physical_index: 0,
860
+ components: {}
861
+ }));
862
+ let brickOffset = 0;
863
+ for (const chunk of reader.brickChunkIndex(MAIN_GRID)) {
864
+ if (chunk.numComponents > 0) {
865
+ const { components } = reader.componentChunk(
866
+ MAIN_GRID,
867
+ chunk.index
868
+ );
869
+ for (const c of components) {
870
+ const brick = bricks[brickOffset + c.brickIndex];
871
+ if (brick) brick.components[c.typeName] = c.data ?? {};
872
+ }
873
+ }
874
+ brickOffset += chunk.numBricks;
875
+ }
876
+ return {
877
+ version: 10,
878
+ map: this.currentMap ?? "Unknown",
879
+ author: { id: this.host?.id ?? "", name: this.host?.name ?? "" },
880
+ host: { id: this.host?.id ?? "", name: this.host?.name ?? "" },
881
+ description: "",
882
+ brick_count: bricks.length,
883
+ mods: [],
884
+ brick_assets: reader.brickAssets(),
885
+ colors: [],
886
+ materials: reader.materials(),
887
+ physical_materials: [],
888
+ brick_owners: reader.brickOwners(),
889
+ game_version: this.version,
890
+ save_time: new Uint8Array(),
891
+ bricks,
892
+ components: {}
893
+ };
894
+ } finally {
895
+ if (fs.existsSync(file2)) fs.unlinkSync(file2);
896
+ }
897
+ }
780
898
  const saveFile = this._tempSavePrefix + Date.now() + "_" + this._tempCounter.save++;
781
899
  await this.saveBricksAsync(saveFile, region);
782
900
  const savePath = this.getSavePath(saveFile);
@@ -787,6 +905,93 @@ class Omegga extends wrapper.default {
787
905
  }
788
906
  return void 0;
789
907
  }
908
+ /**
909
+ * Load a prefab into the world (EA3). `path` is a bundle
910
+ * path ref such as `Prefabs/Uploads/<hash>.brz`.
911
+ * br.Prefab.Load <Path> [Offset X Y Z] [At Original Position] [Orientation]
912
+ * [Root Entity Persistent Index] [Mirror Axes] [Override User Id]
913
+ */
914
+ loadPrefab(path2, {
915
+ offX = 0,
916
+ offY = 0,
917
+ offZ = 0,
918
+ atOriginalPosition = false,
919
+ orientation = 0,
920
+ rootEntityPersistentIndex = -1,
921
+ mirrorAxes = 0,
922
+ overrideUserId = ""
923
+ } = {}) {
924
+ if (!path2) return;
925
+ const rootPart = rootEntityPersistentIndex >= 0 ? ` ${rootEntityPersistentIndex} ${mirrorAxes}${overrideUserId ? ` "${overrideUserId}"` : ""}` : "";
926
+ this.writeln(
927
+ `${this.Console.Prefab.Load} "${path2}" ${offX} ${offY} ${offZ} ${atOriginalPosition ? 1 : 0} ${orientation}${rootPart}`
928
+ );
929
+ }
930
+ /**
931
+ * Load a prefab onto a player (EA3). Replaces the
932
+ * removed {@link loadBricksOnPlayer}; backed by `br.Prefab.GiveToPlayer`.
933
+ * @param path prefab bundle path ref
934
+ * @param player player name/id or player object
935
+ * @param options give options (preserve ownership)
936
+ */
937
+ loadPrefabOnPlayer(path2, player, { preserveOwnership = false } = {}) {
938
+ this.givePrefabToPlayer(path2, player, { preserveOwnership });
939
+ }
940
+ /**
941
+ * Save the world (or a region of it) as a prefab (EA3).
942
+ * `path` is the destination bundle path ref (e.g. `Prefabs/MyPrefab.brz`).
943
+ * br.Prefab.SaveRegion <Path> <Center X Y Z> <Extent X Y Z> [Include Entities]
944
+ * [Root Entity Persistent Index] [Filter User Id]
945
+ * @param path destination prefab bundle path ref
946
+ * @param options save options; omit `region` to capture the whole world
947
+ */
948
+ savePrefab(path2, {
949
+ region,
950
+ entities = true,
951
+ rootEntityPersistentIndex = -1,
952
+ userId = ""
953
+ } = {}) {
954
+ if (!path2) return;
955
+ const center = (region?.center ?? [0, 0, 0]).join(" ");
956
+ const extent = (region?.extent ?? [
957
+ WHOLE_WORLD_EXTENT,
958
+ WHOLE_WORLD_EXTENT,
959
+ WHOLE_WORLD_EXTENT
960
+ ]).join(" ");
961
+ this.writeln(
962
+ `${this.Console.Prefab.SaveRegion} "${path2}" ${center} ${extent} ${entities ? 1 : 0} ${rootEntityPersistentIndex}${userId ? ` "${userId}"` : ""}`
963
+ );
964
+ }
965
+ /**
966
+ * Save a prefab and resolve once the prefab file has been written to disk.
967
+ * @param path destination prefab bundle path ref
968
+ * @param options same options as {@link savePrefab}
969
+ * @returns the absolute path to the written prefab, or null on timeout
970
+ */
971
+ async savePrefabAsync(path$1, options) {
972
+ if (!path$1) return null;
973
+ this.savePrefab(path$1, options);
974
+ const name = path$1.replace(/^Prefabs[\\/]/i, "").replace(/\.brz$/i, "");
975
+ const file2 = path.join(this.prefabPath, name + ".brz");
976
+ const deadline = Date.now() + 3e4;
977
+ while (Date.now() < deadline) {
978
+ if (fs.existsSync(file2)) return file2;
979
+ await new Promise((resolve) => setTimeout(resolve, 100));
980
+ }
981
+ return null;
982
+ }
983
+ /**
984
+ * Give a prefab to a player's inventory (EA3).
985
+ * br.Prefab.GiveToPlayer <Path> <Player Name or User Id> [Preserve Ownership]
986
+ */
987
+ givePrefabToPlayer(path2, player, { preserveOwnership = false } = {}) {
988
+ if (!path2) return;
989
+ const target = typeof player === "object" ? player.id : player;
990
+ if (!target) return;
991
+ this.writeln(
992
+ `${this.Console.Prefab.GiveToPlayer} "${path2}" "${target}" ${preserveOwnership ? 1 : 0}`
993
+ );
994
+ }
790
995
  // TODO: switch this to use worlds...
791
996
  async changeMap(map) {
792
997
  if (!map) return;