From f3868c2ee8d9dcf9488c349d8509545f0113f010 Mon Sep 17 00:00:00 2001
From: EliteMasterEric <ericmyllyoja@gmail.com>
Date: Tue, 21 May 2024 02:23:21 -0400
Subject: [PATCH 1/3] An attempt at an HTML5 save data fix

---
 hmm.json                                      |  2 +-
 source/funkin/save/Save.hx                    |  6 ++--
 .../funkin/save/migrator/SaveDataMigrator.hx  |  7 ++--
 source/funkin/util/StructureUtil.hx           | 32 ++++++++++++++++---
 source/funkin/util/VersionUtil.hx             | 19 +++++++++++
 5 files changed, 55 insertions(+), 11 deletions(-)

diff --git a/hmm.json b/hmm.json
index c359d7a51..1fe5a923d 100644
--- a/hmm.json
+++ b/hmm.json
@@ -153,7 +153,7 @@
       "name": "polymod",
       "type": "git",
       "dir": null,
-      "ref": "8553b800965f225bb14c7ab8f04bfa9cdec362ac",
+      "ref": "bfbe30d81601b3543d80dce580108ad6b7e182c7",
       "url": "https://github.com/larsiusprime/polymod"
     },
     {
diff --git a/source/funkin/save/Save.hx b/source/funkin/save/Save.hx
index acbe59edd..dbba4a4c4 100644
--- a/source/funkin/save/Save.hx
+++ b/source/funkin/save/Save.hx
@@ -53,7 +53,8 @@ class Save
   public function new(?data:RawSaveData)
   {
     if (data == null) this.data = Save.getDefault();
-    else this.data = data;
+    else
+      this.data = data;
   }
 
   public static function getDefault():RawSaveData
@@ -714,6 +715,7 @@ class Save
 
 /**
  * An anonymous structure containingg all the user's save data.
+ * Isn't stored with JSON, stored with some sort of Haxe built-in serialization?
  */
 typedef RawSaveData =
 {
@@ -724,8 +726,6 @@ typedef RawSaveData =
   /**
    * A semantic versioning string for the save data format.
    */
-  @:jcustomparse(funkin.data.DataParse.semverVersion)
-  @:jcustomwrite(funkin.data.DataWrite.semverVersion)
   var version:Version;
 
   var api:SaveApiData;
diff --git a/source/funkin/save/migrator/SaveDataMigrator.hx b/source/funkin/save/migrator/SaveDataMigrator.hx
index 3ed59e726..7f597b4ec 100644
--- a/source/funkin/save/migrator/SaveDataMigrator.hx
+++ b/source/funkin/save/migrator/SaveDataMigrator.hx
@@ -24,6 +24,8 @@ class SaveDataMigrator
     }
     else
     {
+      // Sometimes the Haxe serializer has issues with the version so we fix it here.
+      version = VersionUtil.repairVersion(version);
       if (VersionUtil.validateVersion(version, Save.SAVE_DATA_VERSION_RULE))
       {
         // Simply import the structured data.
@@ -32,8 +34,9 @@ class SaveDataMigrator
       }
       else
       {
-        trace('[SAVE] Invalid save data version! Returning blank data.');
-        trace(inputData);
+        var message:String = 'Error migrating save data, expected ${Save.SAVE_DATA_VERSION}.';
+        lime.app.Application.current.window.alert(message, "Save Data Failure");
+        trace('[SAVE] ' + message);
         return new Save(Save.getDefault());
       }
     }
diff --git a/source/funkin/util/StructureUtil.hx b/source/funkin/util/StructureUtil.hx
index 2f0c3818a..2a6b345d3 100644
--- a/source/funkin/util/StructureUtil.hx
+++ b/source/funkin/util/StructureUtil.hx
@@ -44,8 +44,15 @@ class StructureUtil
     return Std.isOfType(a, haxe.Constraints.IMap);
   }
 
-  public static function isObject(a:Dynamic):Bool
+  /**
+   * Returns `true` if `a` is an anonymous structure.
+   * I believe this returns `false` even for class instances and arrays.
+   */
+  public static function isStructure(a:Dynamic):Bool
   {
+    // TODO: Is there a difference?
+    // return Reflect.isObject(foo);
+
     switch (Type.typeof(a))
     {
       case TObject:
@@ -55,6 +62,22 @@ class StructureUtil
     }
   }
 
+  /**
+   * Returns true if `a` is an array.
+   *
+   * NOTE: isObject and isInstance also return true,
+   * since they're objects of the Array<> class, so check this first!
+   */
+  public static function isArray(a:Dynamic):Bool
+  {
+    return Std.is(a, Array);
+  }
+
+  public static function isInstance(a:Dynamic):Bool
+  {
+    return Type.getClass(a) != null;
+  }
+
   public static function isPrimitive(a:Dynamic):Bool
   {
     switch (Type.typeof(a))
@@ -89,6 +112,7 @@ class StructureUtil
   {
     if (a == null) return b;
     if (b == null) return null;
+    if (isArray(a) && isArray(b)) return b;
     if (isPrimitive(a) && isPrimitive(b)) return b;
     if (isMap(b))
     {
@@ -101,7 +125,6 @@ class StructureUtil
         return StructureUtil.toMap(a).merge(b);
       }
     }
-    if (!Reflect.isObject(a) || !Reflect.isObject(b)) return b;
     if (Std.isOfType(b, haxe.ds.StringMap))
     {
       if (Std.isOfType(a, haxe.ds.StringMap))
@@ -113,15 +136,14 @@ class StructureUtil
         return StructureUtil.toMap(a).merge(b);
       }
     }
+    if (!isStructure(a) || !isStructure(b)) return b;
 
     var result:DynamicAccess<Dynamic> = Reflect.copy(a);
 
     for (field in Reflect.fields(b))
     {
-      if (Reflect.isObject(b))
+      if (isStructure(b))
       {
-        // Note that isObject also returns true for class instances,
-        // but we just assume that's not a problem here.
         result.set(field, deepMerge(Reflect.field(result, field), Reflect.field(b, field)));
       }
       else
diff --git a/source/funkin/util/VersionUtil.hx b/source/funkin/util/VersionUtil.hx
index 247ba19db..8f5550662 100644
--- a/source/funkin/util/VersionUtil.hx
+++ b/source/funkin/util/VersionUtil.hx
@@ -32,6 +32,25 @@ class VersionUtil
     }
   }
 
+  public static function repairVersion(version:thx.semver.Version):thx.semver.Version
+  {
+    var versionData:thx.semver.Version.SemVer = version;
+
+    if (StructureUtil.isStructure(versionData.version))
+    {
+      // This is bad! versionData.version should be an array!
+      versionData.version = [versionData.version[0], versionData.version[1], versionData.version[2]];
+
+      var fixedVersion:thx.semver.Version = versionData;
+      return fixedVersion;
+    }
+    else
+    {
+      // No need for repair.
+      return version;
+    }
+  }
+
   /**
    * Checks that a given verison number satisisfies a given version rule.
    * Version rule can be complex, e.g. "1.0.x" or ">=1.0.0,<1.1.0", or anything NPM supports.

From fed6d1146c67b048f2c9b82d3b50a7dd9cd1748f Mon Sep 17 00:00:00 2001
From: EliteMasterEric <ericmyllyoja@gmail.com>
Date: Tue, 21 May 2024 04:02:32 -0400
Subject: [PATCH 2/3] Do some cleanup (replace several utility functions with a
 utility library we already depend on!)

---
 Project.xml                                   |   1 +
 source/funkin/import.hx                       |   1 +
 source/funkin/play/song/Song.hx               |  16 +-
 .../funkin/save/migrator/SaveDataMigrator.hx  |   5 +-
 source/funkin/ui/story/LevelProp.hx           |   5 +-
 source/funkin/util/StructureUtil.hx           | 158 ------------------
 source/funkin/util/tools/ArrayTools.hx        |  66 --------
 7 files changed, 17 insertions(+), 235 deletions(-)
 delete mode 100644 source/funkin/util/StructureUtil.hx

diff --git a/Project.xml b/Project.xml
index fcfcfb9f3..b5630a46a 100644
--- a/Project.xml
+++ b/Project.xml
@@ -128,6 +128,7 @@
 
 
 	<haxelib name="json2object" /> <!-- JSON parsing -->
+	<haxelib name="thx.core" /> <!-- General utility library, "the lodash of Haxe" -->
 	<haxelib name="thx.semver" /> <!-- Version string handling -->
 
 	<haxelib name="hxcpp-debug-server" if="desktop debug" /> <!-- VSCode debug support -->
diff --git a/source/funkin/import.hx b/source/funkin/import.hx
index 250de99cb..c8431be33 100644
--- a/source/funkin/import.hx
+++ b/source/funkin/import.hx
@@ -11,6 +11,7 @@ import flixel.system.debug.watch.Tracker;
 // These are great.
 using Lambda;
 using StringTools;
+using thx.Arrays;
 using funkin.util.tools.ArraySortTools;
 using funkin.util.tools.ArrayTools;
 using funkin.util.tools.FloatTools;
diff --git a/source/funkin/play/song/Song.hx b/source/funkin/play/song/Song.hx
index 23d8d2198..53408fb34 100644
--- a/source/funkin/play/song/Song.hx
+++ b/source/funkin/play/song/Song.hx
@@ -439,12 +439,16 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta
     // so we have to map it to the actual difficulty names.
     // We also filter out difficulties that don't match the variation or that don't exist.
 
-    var diffFiltered:Array<String> = difficulties.keys().array().map(function(diffId:String):Null<String> {
-      var difficulty:Null<SongDifficulty> = difficulties.get(diffId);
-      if (difficulty == null) return null;
-      if (variationIds.length > 0 && !variationIds.contains(difficulty.variation)) return null;
-      return difficulty.difficulty;
-    }).nonNull().unique();
+    var diffFiltered:Array<String> = difficulties.keys()
+      .array()
+      .map(function(diffId:String):Null<String> {
+        var difficulty:Null<SongDifficulty> = difficulties.get(diffId);
+        if (difficulty == null) return null;
+        if (variationIds.length > 0 && !variationIds.contains(difficulty.variation)) return null;
+        return difficulty.difficulty;
+      })
+      .filterNull()
+      .distinct();
 
     diffFiltered = diffFiltered.filter(function(diffId:String):Bool {
       if (showHidden) return true;
diff --git a/source/funkin/save/migrator/SaveDataMigrator.hx b/source/funkin/save/migrator/SaveDataMigrator.hx
index 7f597b4ec..5398b2119 100644
--- a/source/funkin/save/migrator/SaveDataMigrator.hx
+++ b/source/funkin/save/migrator/SaveDataMigrator.hx
@@ -28,8 +28,9 @@ class SaveDataMigrator
       version = VersionUtil.repairVersion(version);
       if (VersionUtil.validateVersion(version, Save.SAVE_DATA_VERSION_RULE))
       {
-        // Simply import the structured data.
-        var save:Save = new Save(StructureUtil.deepMerge(Save.getDefault(), inputData));
+        // Import the structured data.
+        var saveDataWithDefaults:RawSaveData = thx.Objects.deepCombine(Save.getDefault(), inputData);
+        var save:Save = new Save(saveDataWithDefaults);
         return save;
       }
       else
diff --git a/source/funkin/ui/story/LevelProp.hx b/source/funkin/ui/story/LevelProp.hx
index ffc756e1c..5a3efc36a 100644
--- a/source/funkin/ui/story/LevelProp.hx
+++ b/source/funkin/ui/story/LevelProp.hx
@@ -13,11 +13,10 @@ class LevelProp extends Bopper
     // Only reset the prop if the asset path has changed.
     if (propData == null || value?.assetPath != propData?.assetPath)
     {
-      this.visible = (value != null);
-      this.propData = value;
-      danceEvery = this.propData?.danceEvery ?? 0;
       applyData();
     }
+    this.visible = (value != null);
+    danceEvery = this.propData?.danceEvery ?? 0;
 
     return this.propData;
   }
diff --git a/source/funkin/util/StructureUtil.hx b/source/funkin/util/StructureUtil.hx
deleted file mode 100644
index 2a6b345d3..000000000
--- a/source/funkin/util/StructureUtil.hx
+++ /dev/null
@@ -1,158 +0,0 @@
-package funkin.util;
-
-import funkin.util.tools.MapTools;
-import haxe.DynamicAccess;
-
-/**
- * Utilities for working with anonymous structures.
- */
-class StructureUtil
-{
-  /**
-   * Merge two structures, with the second overwriting the first.
-   * Performs a SHALLOW clone, where child structures are not merged.
-   * @param a The base structure.
-   * @param b The new structure.
-   * @return The merged structure.
-   */
-  public static function merge(a:Dynamic, b:Dynamic):Dynamic
-  {
-    var result:DynamicAccess<Dynamic> = Reflect.copy(a);
-
-    for (field in Reflect.fields(b))
-    {
-      result.set(field, Reflect.field(b, field));
-    }
-
-    return result;
-  }
-
-  public static function toMap(a:Dynamic):haxe.ds.Map<String, Dynamic>
-  {
-    var result:haxe.ds.Map<String, Dynamic> = [];
-
-    for (field in Reflect.fields(a))
-    {
-      result.set(field, Reflect.field(a, field));
-    }
-
-    return result;
-  }
-
-  public static function isMap(a:Dynamic):Bool
-  {
-    return Std.isOfType(a, haxe.Constraints.IMap);
-  }
-
-  /**
-   * Returns `true` if `a` is an anonymous structure.
-   * I believe this returns `false` even for class instances and arrays.
-   */
-  public static function isStructure(a:Dynamic):Bool
-  {
-    // TODO: Is there a difference?
-    // return Reflect.isObject(foo);
-
-    switch (Type.typeof(a))
-    {
-      case TObject:
-        return true;
-      default:
-        return false;
-    }
-  }
-
-  /**
-   * Returns true if `a` is an array.
-   *
-   * NOTE: isObject and isInstance also return true,
-   * since they're objects of the Array<> class, so check this first!
-   */
-  public static function isArray(a:Dynamic):Bool
-  {
-    return Std.is(a, Array);
-  }
-
-  public static function isInstance(a:Dynamic):Bool
-  {
-    return Type.getClass(a) != null;
-  }
-
-  public static function isPrimitive(a:Dynamic):Bool
-  {
-    switch (Type.typeof(a))
-    {
-      case TInt | TFloat | TBool:
-        return true;
-      case TClass(c):
-        return false;
-      case TEnum(e):
-        return false;
-      case TObject:
-        return false;
-      case TFunction:
-        return false;
-      case TNull:
-        return true;
-      case TUnknown:
-        return false;
-      default:
-        return false;
-    }
-  }
-
-  /**
-   * Merge two structures, with the second overwriting the first.
-   * Performs a DEEP clone, where child structures are also merged recursively.
-   * @param a The base structure.
-   * @param b The new structure.
-   * @return The merged structure.
-   */
-  public static function deepMerge(a:Dynamic, b:Dynamic):Dynamic
-  {
-    if (a == null) return b;
-    if (b == null) return null;
-    if (isArray(a) && isArray(b)) return b;
-    if (isPrimitive(a) && isPrimitive(b)) return b;
-    if (isMap(b))
-    {
-      if (isMap(a))
-      {
-        return MapTools.merge(a, b);
-      }
-      else
-      {
-        return StructureUtil.toMap(a).merge(b);
-      }
-    }
-    if (Std.isOfType(b, haxe.ds.StringMap))
-    {
-      if (Std.isOfType(a, haxe.ds.StringMap))
-      {
-        return MapTools.merge(a, b);
-      }
-      else
-      {
-        return StructureUtil.toMap(a).merge(b);
-      }
-    }
-    if (!isStructure(a) || !isStructure(b)) return b;
-
-    var result:DynamicAccess<Dynamic> = Reflect.copy(a);
-
-    for (field in Reflect.fields(b))
-    {
-      if (isStructure(b))
-      {
-        result.set(field, deepMerge(Reflect.field(result, field), Reflect.field(b, field)));
-      }
-      else
-      {
-        // If we're here, b[field] is a primitive.
-        result.set(field, Reflect.field(b, field));
-      }
-    }
-
-    return result;
-  }
-}
diff --git a/source/funkin/util/tools/ArrayTools.hx b/source/funkin/util/tools/ArrayTools.hx
index caf8e8aab..0fe245e3a 100644
--- a/source/funkin/util/tools/ArrayTools.hx
+++ b/source/funkin/util/tools/ArrayTools.hx
@@ -5,72 +5,6 @@ package funkin.util.tools;
  */
 class ArrayTools
 {
-  /**
-   * Returns a copy of the array with all duplicate elements removed.
-   * @param array The array to remove duplicates from.
-   * @return A copy of the array with all duplicate elements removed.
-   */
-  public static function unique<T>(array:Array<T>):Array<T>
-  {
-    var result:Array<T> = [];
-    for (element in array)
-    {
-      if (!result.contains(element))
-      {
-        result.push(element);
-      }
-    }
-    return result;
-  }
-
-  /**
-   * Returns a copy of the array with all `null` elements removed.
-   * @param array The array to remove `null` elements from.
-   * @return A copy of the array with all `null` elements removed.
-   */
-  public static function nonNull<T>(array:Array<Null<T>>):Array<T>
-  {
-    var result:Array<T> = [];
-    for (element in array)
-    {
-      if (element != null)
-      {
-        result.push(element);
-      }
-    }
-    return result;
-  }
-
-  /**
-   * Return the first element of the array that satisfies the predicate, or null if none do.
-   * @param input The array to search
-   * @param predicate The predicate to call
-   * @return The result
-   */
-  public static function find<T>(input:Array<T>, predicate:T->Bool):Null<T>
-  {
-    for (element in input)
-    {
-      if (predicate(element)) return element;
-    }
-    return null;
-  }
-
-  /**
-   * Return the index of the first element of the array that satisfies the predicate, or `-1` if none do.
-   * @param input The array to search
-   * @param predicate The predicate to call
-   * @return The index of the result
-   */
-  public static function findIndex<T>(input:Array<T>, predicate:T->Bool):Int
-  {
-    for (index in 0...input.length)
-    {
-      if (predicate(input[index])) return index;
-    }
-    return -1;
-  }
-
   /*
    * Push an element to the array if it is not already present.
    * @param input The array to push to

From 6d3b58cecdb3b3881e261a3137518926835fd7a5 Mon Sep 17 00:00:00 2001
From: EliteMasterEric <ericmyllyoja@gmail.com>
Date: Tue, 21 May 2024 04:02:53 -0400
Subject: [PATCH 3/3] Fix some additional compiling issues.

---
 source/funkin/save/migrator/SaveDataMigrator.hx | 3 +--
 source/funkin/util/VersionUtil.hx               | 2 +-
 source/funkin/util/macro/InlineMacro.hx         | 2 +-
 3 files changed, 3 insertions(+), 4 deletions(-)

diff --git a/source/funkin/save/migrator/SaveDataMigrator.hx b/source/funkin/save/migrator/SaveDataMigrator.hx
index 5398b2119..650666c5c 100644
--- a/source/funkin/save/migrator/SaveDataMigrator.hx
+++ b/source/funkin/save/migrator/SaveDataMigrator.hx
@@ -3,7 +3,6 @@ package funkin.save.migrator;
 import funkin.save.Save;
 import funkin.save.migrator.RawSaveData_v1_0_0;
 import thx.semver.Version;
-import funkin.util.StructureUtil;
 import funkin.util.VersionUtil;
 
 @:nullSafety
@@ -29,7 +28,7 @@ class SaveDataMigrator
       if (VersionUtil.validateVersion(version, Save.SAVE_DATA_VERSION_RULE))
       {
         // Import the structured data.
-        var saveDataWithDefaults:RawSaveData = thx.Objects.deepCombine(Save.getDefault(), inputData);
+        var saveDataWithDefaults:RawSaveData = cast thx.Objects.deepCombine(Save.getDefault(), inputData);
         var save:Save = new Save(saveDataWithDefaults);
         return save;
       }
diff --git a/source/funkin/util/VersionUtil.hx b/source/funkin/util/VersionUtil.hx
index 8f5550662..18d7eafa6 100644
--- a/source/funkin/util/VersionUtil.hx
+++ b/source/funkin/util/VersionUtil.hx
@@ -36,7 +36,7 @@ class VersionUtil
   {
     var versionData:thx.semver.Version.SemVer = version;
 
-    if (StructureUtil.isStructure(versionData.version))
+    if (thx.Types.isAnonymousObject(versionData.version))
     {
       // This is bad! versionData.version should be an array!
       versionData.version = [versionData.version[0], versionData.version[1], versionData.version[2]];
diff --git a/source/funkin/util/macro/InlineMacro.hx b/source/funkin/util/macro/InlineMacro.hx
index b0e7ed184..c40257409 100644
--- a/source/funkin/util/macro/InlineMacro.hx
+++ b/source/funkin/util/macro/InlineMacro.hx
@@ -23,7 +23,7 @@ class InlineMacro
     var fields:Array<haxe.macro.Expr.Field> = haxe.macro.Context.getBuildFields();
 
     // Find the field with the given name.
-    var targetField:Null<haxe.macro.Expr.Field> = fields.find(function(f) return f.name == field
+    var targetField:Null<haxe.macro.Expr.Field> = thx.Arrays.find(fields, function(f) return f.name == field
       && (MacroUtil.isFieldStatic(f) == isStatic));
 
     // If the field was not found, throw an error.