quake2ts 0.0.43 → 0.0.45

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.
@@ -75,6 +75,9 @@ __export(index_exports, {
75
75
  clampAmmoCounts: () => clampAmmoCounts,
76
76
  classifyRange: () => classifyRange,
77
77
  clearExpiredPowerups: () => clearExpiredPowerups,
78
+ convertGameSaveToRereleaseLevel: () => convertGameSaveToRereleaseLevel,
79
+ convertRereleaseLevelToGameSave: () => convertRereleaseLevelToGameSave,
80
+ convertRereleaseSaveToGameSave: () => convertRereleaseSaveToGameSave,
78
81
  createAmmoInventory: () => createAmmoInventory,
79
82
  createBaseAmmoCaps: () => createBaseAmmoCaps,
80
83
  createDefaultSpawnRegistry: () => createDefaultSpawnRegistry,
@@ -103,6 +106,7 @@ __export(index_exports, {
103
106
  rangeTo: () => rangeTo,
104
107
  registerDefaultSpawns: () => registerDefaultSpawns,
105
108
  selectWeapon: () => selectWeapon,
109
+ serializeRereleaseSave: () => serializeRereleaseSave,
106
110
  setMovedir: () => setMovedir,
107
111
  spawnEntitiesFromText: () => spawnEntitiesFromText,
108
112
  spawnEntityFromDictionary: () => spawnEntityFromDictionary,
@@ -2739,6 +2743,150 @@ function summarizeRereleaseSave(raw) {
2739
2743
  highestEntityIndex: highestEntityIndex >= 0 ? highestEntityIndex : void 0
2740
2744
  };
2741
2745
  }
2746
+ var SERIALIZABLE_FIELD_NAMES = new Set(
2747
+ ENTITY_FIELD_METADATA.filter((field) => field.save).map((field) => field.name)
2748
+ );
2749
+ function toVec3(value, label) {
2750
+ if (!Array.isArray(value) || value.length !== 3) {
2751
+ throw new Error(`${label} must be a vec3 array`);
2752
+ }
2753
+ const [x, y, z] = value;
2754
+ return [ensureNumber2(x, `${label}[0]`), ensureNumber2(y, `${label}[1]`), ensureNumber2(z, `${label}[2]`)];
2755
+ }
2756
+ function normalizeEntityFields(raw, label) {
2757
+ const fields = {};
2758
+ for (const [rawName, value] of Object.entries(raw)) {
2759
+ const name = rawName;
2760
+ if (!SERIALIZABLE_FIELD_NAMES.has(name)) {
2761
+ continue;
2762
+ }
2763
+ if (value === null) {
2764
+ fields[name] = null;
2765
+ continue;
2766
+ }
2767
+ if (Array.isArray(value)) {
2768
+ fields[name] = toVec3(value, `${label}.${name}`);
2769
+ continue;
2770
+ }
2771
+ switch (typeof value) {
2772
+ case "number":
2773
+ case "string":
2774
+ case "boolean":
2775
+ fields[name] = value;
2776
+ break;
2777
+ default:
2778
+ throw new Error(`Unsupported field type for ${label}.${name}`);
2779
+ }
2780
+ }
2781
+ return fields;
2782
+ }
2783
+ function buildEntitySnapshot(entities, levelTimeSeconds, capacityHint) {
2784
+ let highestIndex = 0;
2785
+ const active = /* @__PURE__ */ new Set();
2786
+ const activeOrder = [];
2787
+ const serialized = [];
2788
+ for (const [index, entry] of entities.entries()) {
2789
+ if (index > highestIndex) {
2790
+ highestIndex = index;
2791
+ }
2792
+ const inUse = entry.inuse !== false;
2793
+ if (!inUse) {
2794
+ continue;
2795
+ }
2796
+ if (!active.has(index)) {
2797
+ activeOrder.push(index);
2798
+ }
2799
+ active.add(index);
2800
+ serialized.push({ index, fields: normalizeEntityFields(entry, `entities[${index}]`) });
2801
+ }
2802
+ if (!active.has(0)) {
2803
+ active.add(0);
2804
+ activeOrder.unshift(0);
2805
+ serialized.unshift({ index: 0, fields: { classname: "worldspawn" } });
2806
+ }
2807
+ const capacity = Math.max(capacityHint ?? 0, highestIndex + 1, Math.max(...active) + 1);
2808
+ const freeList = [];
2809
+ for (let i = 0; i < capacity; i += 1) {
2810
+ if (!active.has(i)) {
2811
+ freeList.push(i);
2812
+ }
2813
+ }
2814
+ return {
2815
+ timeSeconds: levelTimeSeconds,
2816
+ pool: { capacity, activeOrder, freeList, pendingFree: [] },
2817
+ entities: serialized,
2818
+ thinks: []
2819
+ };
2820
+ }
2821
+ function buildLevelState(level) {
2822
+ const timeSeconds = typeof level.time === "number" ? level.time : 0;
2823
+ const frameNumber = typeof level.framenum === "number" ? level.framenum : 0;
2824
+ const deltaSeconds = typeof level.frametime === "number" ? level.frametime : 0;
2825
+ const previousTimeSeconds = timeSeconds - deltaSeconds;
2826
+ return { frameNumber, timeSeconds, previousTimeSeconds, deltaSeconds };
2827
+ }
2828
+ function convertRereleaseLevelToGameSave(save, options = {}) {
2829
+ const { timestamp = Date.now(), defaultDifficulty = 1, defaultPlaytimeSeconds, rngState, configstrings = [] } = options;
2830
+ const levelName = save.level.mapname ?? "unknown";
2831
+ const level = buildLevelState(save.level);
2832
+ const playtimeSeconds = defaultPlaytimeSeconds ?? level.timeSeconds;
2833
+ return {
2834
+ version: SAVE_FORMAT_VERSION,
2835
+ timestamp,
2836
+ map: levelName,
2837
+ difficulty: defaultDifficulty,
2838
+ playtimeSeconds,
2839
+ gameState: { ...options.gameState ?? {}, rereleaseVersion: save.saveVersion },
2840
+ level,
2841
+ rng: rngState ? { mt: { index: rngState.mt.index, state: [...rngState.mt.state] } } : new RandomGenerator().getState(),
2842
+ entities: buildEntitySnapshot(save.entities, level.timeSeconds, save.level.maxentities ?? void 0),
2843
+ cvars: [],
2844
+ configstrings: [...configstrings]
2845
+ };
2846
+ }
2847
+ function convertRereleaseSaveToGameSave(save, options = {}) {
2848
+ if ("game" in save) {
2849
+ throw new Error("Game-wide rerelease saves are not currently supported for conversion");
2850
+ }
2851
+ return convertRereleaseLevelToGameSave(save, options);
2852
+ }
2853
+ function convertGameSaveToRereleaseLevel(save) {
2854
+ const active = new Set(save.entities.pool.activeOrder);
2855
+ const entities = /* @__PURE__ */ new Map();
2856
+ for (const entity of save.entities.entities) {
2857
+ if (!active.has(entity.index)) {
2858
+ continue;
2859
+ }
2860
+ const fields = { inuse: true };
2861
+ for (const [name, value] of Object.entries(entity.fields)) {
2862
+ if (value === void 0) {
2863
+ continue;
2864
+ }
2865
+ fields[name] = value;
2866
+ }
2867
+ entities.set(entity.index, fields);
2868
+ }
2869
+ return {
2870
+ saveVersion: save.version,
2871
+ level: {
2872
+ mapname: save.map,
2873
+ time: save.level.timeSeconds,
2874
+ framenum: save.level.frameNumber,
2875
+ frametime: save.level.deltaSeconds
2876
+ },
2877
+ entities
2878
+ };
2879
+ }
2880
+ function serializeRereleaseSave(save) {
2881
+ if ("game" in save) {
2882
+ return { save_version: save.saveVersion, game: save.game, clients: save.clients };
2883
+ }
2884
+ const entities = {};
2885
+ for (const [index, entity] of save.entities.entries()) {
2886
+ entities[index.toString(10)] = entity;
2887
+ }
2888
+ return { save_version: save.saveVersion, level: save.level, entities };
2889
+ }
2742
2890
 
2743
2891
  // src/save/storage.ts
2744
2892
  var TEXT_ENCODER_CTOR = globalThis.TextEncoder;
@@ -3709,6 +3857,9 @@ function createGame(engine, options) {
3709
3857
  clampAmmoCounts,
3710
3858
  classifyRange,
3711
3859
  clearExpiredPowerups,
3860
+ convertGameSaveToRereleaseLevel,
3861
+ convertRereleaseLevelToGameSave,
3862
+ convertRereleaseSaveToGameSave,
3712
3863
  createAmmoInventory,
3713
3864
  createBaseAmmoCaps,
3714
3865
  createDefaultSpawnRegistry,
@@ -3737,6 +3888,7 @@ function createGame(engine, options) {
3737
3888
  rangeTo,
3738
3889
  registerDefaultSpawns,
3739
3890
  selectWeapon,
3891
+ serializeRereleaseSave,
3740
3892
  setMovedir,
3741
3893
  spawnEntitiesFromText,
3742
3894
  spawnEntityFromDictionary,