diff --git a/hmm.json b/hmm.json index 0dfe88ded..641ef1bbd 100644 --- a/hmm.json +++ b/hmm.json @@ -139,7 +139,7 @@ "name": "openfl", "type": "git", "dir": null, - "ref": "f229d76361c7e31025a048fe7909847f75bb5d5e", + "ref": "228c1b5063911e2ad75cef6e3168ef0a4b9f9134", "url": "https://github.com/FunkinCrew/openfl" }, { diff --git a/source/funkin/save/Save.hx b/source/funkin/save/Save.hx index dc7c5f989..af2730ddd 100644 --- a/source/funkin/save/Save.hx +++ b/source/funkin/save/Save.hx @@ -9,6 +9,7 @@ import funkin.save.migrator.SaveDataMigrator; import funkin.ui.debug.charting.ChartEditorState.ChartEditorLiveInputStyle; import funkin.ui.debug.charting.ChartEditorState.ChartEditorTheme; import thx.semver.Version; +import funkin.util.SerializerUtil; @:nullSafety class Save @@ -391,6 +392,22 @@ class Save */ public function getLevelScore(levelId:String, difficultyId:String = 'normal'):Null { + if (data.scores?.levels == null) + { + if (data.scores == null) + { + data.scores = + { + songs: [], + levels: [] + }; + } + else + { + data.scores.levels = []; + } + } + var level = data.scores.levels.get(levelId); if (level == null) { @@ -641,6 +658,9 @@ class Save { trace("[SAVE] Loading save from slot " + slot + "..."); + // Prevent crashes if the save data is corrupted. + SerializerUtil.initSerializer(); + FlxG.save.bind('$SAVE_NAME${slot}', SAVE_PATH); if (FlxG.save.isEmpty()) diff --git a/source/funkin/util/SerializerUtil.hx b/source/funkin/util/SerializerUtil.hx index c87d3f6c0..fa602cc73 100644 --- a/source/funkin/util/SerializerUtil.hx +++ b/source/funkin/util/SerializerUtil.hx @@ -63,6 +63,31 @@ class SerializerUtil } } + public static function initSerializer():Void + { + haxe.Unserializer.DEFAULT_RESOLVER = new FunkinTypeResolver(); + } + + /** + * Serialize a Haxe object using the built-in Serializer. + * @param input The object to serialize + * @return The serialized object as a string + */ + public static function fromHaxeObject(input:Dynamic):String + { + return haxe.Serializer.run(input); + } + + /** + * Convert a serialized Haxe object back into a Haxe object. + * @param input The serialized object as a string + * @return The deserialized object + */ + public static function toHaxeObject(input:String):Dynamic + { + return haxe.Unserializer.run(input); + } + /** * Customize how certain types are serialized when converting to JSON. */ @@ -90,3 +115,26 @@ class SerializerUtil return result; } } + +class FunkinTypeResolver +{ + public function new() + { + // Blank constructor. + } + + public function resolveClass(name:String):Class + { + if (name == 'Dynamic') + { + FlxG.log.warn('Found invalid class type in save data, indicates partial save corruption.'); + return null; + } + return Type.resolveClass(name); + }; + + public function resolveEnum(name:String):Enum + { + return Type.resolveEnum(name); + }; +} diff --git a/source/funkin/util/StructureUtil.hx b/source/funkin/util/StructureUtil.hx index 351d0e0a8..f94de4652 100644 --- a/source/funkin/util/StructureUtil.hx +++ b/source/funkin/util/StructureUtil.hx @@ -1,5 +1,6 @@ package funkin.util; +import funkin.util.tools.MapTools; import haxe.DynamicAccess; /** @@ -26,6 +27,57 @@ class StructureUtil return result; } + public static function toMap(a:Dynamic):haxe.ds.Map + { + var result:haxe.ds.Map = []; + + 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); + } + + public static function isObject(a:Dynamic):Bool + { + switch (Type.typeof(a)) + { + case TObject: + return true; + default: + return false; + } + } + + 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. @@ -37,6 +89,18 @@ class StructureUtil { if (a == null) return b; if (b == null) return null; + 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 (!Reflect.isObject(a) || !Reflect.isObject(b)) return b; var result:DynamicAccess = Reflect.copy(a); diff --git a/source/funkin/util/tools/MapTools.hx b/source/funkin/util/tools/MapTools.hx index 1399fb791..b98cb0adf 100644 --- a/source/funkin/util/tools/MapTools.hx +++ b/source/funkin/util/tools/MapTools.hx @@ -33,6 +33,24 @@ class MapTools return map.copy(); } + /** + * Create a new map which is a combination of the two given maps. + * @param a The base map. + * @param b The other map. The values from this take precedence. + * @return The combined map. + */ + public static function merge(a:Map, b:Map):Map + { + var result = a.copy(); + + for (pair in b.keyValueIterator()) + { + result.set(pair.key, pair.value); + } + + return result; + } + /** * Create a new array with clones of all elements of the given array, to prevent modifying the original. */