From c056c7276285b847e4ea969ccb3b38dd23fd60b6 Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Tue, 4 Jun 2024 14:26:24 -0400 Subject: [PATCH 1/2] Implement advanced save data repair. --- source/funkin/save/Save.hx | 81 +++++++++++++++++++ .../funkin/save/migrator/SaveDataMigrator.hx | 1 + source/funkin/util/VersionUtil.hx | 5 +- 3 files changed, 86 insertions(+), 1 deletion(-) diff --git a/source/funkin/save/Save.hx b/source/funkin/save/Save.hx index 7f25a8e01..7634c1f51 100644 --- a/source/funkin/save/Save.hx +++ b/source/funkin/save/Save.hx @@ -732,6 +732,87 @@ class Save } } + public static function archiveBadSaveData(data:Dynamic):Void + { + // We want to save this somewhere so we can try to recover it for the user in the future! + + final RECOVERY_SLOT_START = 1000; + + writeToAvailableSlot(RECOVERY_SLOT_START, data); + } + + public static function debug_queryBadSaveData():Void + { + final RECOVERY_SLOT_START = 1000; + final RECOVERY_SLOT_END = 1100; + var firstBadSaveData = querySlotRange(RECOVERY_SLOT_START, RECOVERY_SLOT_END); + if (firstBadSaveData > 0) + { + trace('[SAVE] Found bad save data in slot ${firstBadSaveData}!'); + trace('We should look into recovery...'); + + trace(haxe.Json.stringify(fetchFromSlotRaw(firstBadSaveData))); + } + } + + static function fetchFromSlotRaw(slot:Int):Null + { + var targetSaveData = new FlxSave(); + targetSaveData.bind('$SAVE_NAME${slot}', SAVE_PATH); + if (targetSaveData.isEmpty()) return null; + return targetSaveData.data; + } + + static function writeToAvailableSlot(slot:Int, data:Dynamic):Void + { + trace('[SAVE] Finding slot to write data to (starting with ${slot})...'); + + var targetSaveData = new FlxSave(); + targetSaveData.bind('$SAVE_NAME${slot}', SAVE_PATH); + while (!targetSaveData.isEmpty()) + { + // Keep trying to bind to slots until we find an empty slot. + trace('[SAVE] Slot ${slot} is taken, continuing...'); + slot++; + targetSaveData.bind('$SAVE_NAME${slot}', SAVE_PATH); + } + + trace('[SAVE] Writing data to slot ${slot}...'); + targetSaveData.mergeData(data, true); + + trace('[SAVE] Data written to slot ${slot}!'); + } + + /** + * Return true if the given save slot is not empty. + * @param slot The slot number to check. + * @return Whether the slot is not empty. + */ + static function querySlot(slot:Int):Bool + { + var targetSaveData = new FlxSave(); + targetSaveData.bind('$SAVE_NAME${slot}', SAVE_PATH); + return !targetSaveData.isEmpty(); + } + + /** + * Return true if any of the slots in the given range is not empty. + * @param start The starting slot number to check. + * @param end The ending slot number to check. + * @return The first slot in the range that is not empty, or `-1` if none are. + */ + static function querySlotRange(start:Int, end:Int):Int + { + for (i in start...end) + { + if (querySlot(i)) + { + return i; + } + } + return -1; + } + static function fetchLegacySaveData():Null { trace("[SAVE] Checking for legacy save data..."); diff --git a/source/funkin/save/migrator/SaveDataMigrator.hx b/source/funkin/save/migrator/SaveDataMigrator.hx index 4fa9dd6b3..b7d278cc6 100644 --- a/source/funkin/save/migrator/SaveDataMigrator.hx +++ b/source/funkin/save/migrator/SaveDataMigrator.hx @@ -36,6 +36,7 @@ class SaveDataMigrator { var message:String = 'Error migrating save data, expected ${Save.SAVE_DATA_VERSION}.'; lime.app.Application.current.window.alert(message, "Save Data Failure"); + Save.archiveBadSaveData(inputData); trace('[SAVE] ' + message); return new Save(Save.getDefault()); } diff --git a/source/funkin/util/VersionUtil.hx b/source/funkin/util/VersionUtil.hx index 18d7eafa6..b84b66341 100644 --- a/source/funkin/util/VersionUtil.hx +++ b/source/funkin/util/VersionUtil.hx @@ -39,13 +39,16 @@ class VersionUtil 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]]; + trace('[SAVE] Version data repair required! (got ${versionData.version})'); + var fixedVersionData = [versionData.version[0], versionData.version[1], versionData.version[2]]; + versionData.version = fixedVersionData; var fixedVersion:thx.semver.Version = versionData; return fixedVersion; } else { + trace('[SAVE] Version data repair not required (got ${version})'); // No need for repair. return version; } From ae950c738214e20446cc8ded5b163544a5ee0280 Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Tue, 4 Jun 2024 19:44:00 -0400 Subject: [PATCH 2/2] Finish save data repair (you should be able to transfer your save now) --- CHANGELOG.md | 1 + source/funkin/save/Save.hx | 16 +++++++++------- source/funkin/save/changelog.md | 4 ++++ .../funkin/save/migrator/SaveDataMigrator.hx | 6 +++--- source/funkin/util/VersionUtil.hx | 18 ++++++++++++++++-- 5 files changed, 33 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f5aefb885..898978ca3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Senpai (increased the note speed) - Thorns (increased the note speed slightly) - Favorite songs marked in Freeplay are now stored between sessions. +- In the event that the game cannot load your save data, it will now perform a backup before clearing it, so that we can try to repair it in the future. - Custom note styles are now properly supported for songs; add new notestyles via JSON, then select it for use from the Chart Editor Metadata toolbox. (thanks Keoiki!) - Improved logic for NoteHitScriptEvents, allowing you to view the hit diff and modify whether a note hit is a combo break (thanks nebulazorua!) - Health icons now support a Winning frame without requiring a spritesheet, simply include a third frame in the icon file. (thanks gamerbross!) diff --git a/source/funkin/save/Save.hx b/source/funkin/save/Save.hx index 7634c1f51..2ff6b96cc 100644 --- a/source/funkin/save/Save.hx +++ b/source/funkin/save/Save.hx @@ -16,7 +16,7 @@ import thx.semver.Version; @:nullSafety class Save { - public static final SAVE_DATA_VERSION:thx.semver.Version = "2.0.4"; + public static final SAVE_DATA_VERSION:thx.semver.Version = "2.0.5"; public static final SAVE_DATA_VERSION_RULE:thx.semver.VersionRule = "2.0.x"; // We load this version's saves from a new save path, to maintain SOME level of backwards compatibility. @@ -56,6 +56,9 @@ class Save if (data == null) this.data = Save.getDefault(); else this.data = data; + + // Make sure the verison number is up to date before we flush. + this.data.version = Save.SAVE_DATA_VERSION; } public static function getDefault():RawSaveData @@ -713,7 +716,6 @@ class Save { trace('[SAVE] Found legacy save data, converting...'); var gameSave = SaveDataMigrator.migrateFromLegacy(legacySaveData); - @:privateAccess FlxG.save.mergeData(gameSave.data, true); } else @@ -725,20 +727,19 @@ class Save } else { - trace('[SAVE] Loaded save data.'); - @:privateAccess + trace('[SAVE] Found existing save data.'); var gameSave = SaveDataMigrator.migrate(FlxG.save.data); FlxG.save.mergeData(gameSave.data, true); } } - public static function archiveBadSaveData(data:Dynamic):Void + public static function archiveBadSaveData(data:Dynamic):Int { // We want to save this somewhere so we can try to recover it for the user in the future! final RECOVERY_SLOT_START = 1000; - writeToAvailableSlot(RECOVERY_SLOT_START, data); + return writeToAvailableSlot(RECOVERY_SLOT_START, data); } public static function debug_queryBadSaveData():Void @@ -763,7 +764,7 @@ class Save return targetSaveData.data; } - static function writeToAvailableSlot(slot:Int, data:Dynamic):Void + static function writeToAvailableSlot(slot:Int, data:Dynamic):Int { trace('[SAVE] Finding slot to write data to (starting with ${slot})...'); @@ -781,6 +782,7 @@ class Save targetSaveData.mergeData(data, true); trace('[SAVE] Data written to slot ${slot}!'); + return slot; } /** diff --git a/source/funkin/save/changelog.md b/source/funkin/save/changelog.md index 7c9094f2d..e3038373d 100644 --- a/source/funkin/save/changelog.md +++ b/source/funkin/save/changelog.md @@ -5,6 +5,10 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [2.0.5] - 2024-05-21 +### Fixed +- Resolved an issue where HTML5 wouldn't store the semantic version properly, causing the game to fail to load the save. + ## [2.0.4] - 2024-05-21 ### Added - `favoriteSongs:Array` to `Save` diff --git a/source/funkin/save/migrator/SaveDataMigrator.hx b/source/funkin/save/migrator/SaveDataMigrator.hx index b7d278cc6..7a929322a 100644 --- a/source/funkin/save/migrator/SaveDataMigrator.hx +++ b/source/funkin/save/migrator/SaveDataMigrator.hx @@ -35,9 +35,9 @@ class SaveDataMigrator else { var message:String = 'Error migrating save data, expected ${Save.SAVE_DATA_VERSION}.'; - lime.app.Application.current.window.alert(message, "Save Data Failure"); - Save.archiveBadSaveData(inputData); - trace('[SAVE] ' + message); + var slot:Int = Save.archiveBadSaveData(inputData); + var fullMessage:String = 'An error occurred migrating your save data.\n${message}\nInvalid data has been moved to save slot ${slot}.'; + lime.app.Application.current.window.alert(fullMessage, "Save Data Failure"); return new Save(Save.getDefault()); } } diff --git a/source/funkin/util/VersionUtil.hx b/source/funkin/util/VersionUtil.hx index b84b66341..832ce008a 100644 --- a/source/funkin/util/VersionUtil.hx +++ b/source/funkin/util/VersionUtil.hx @@ -23,6 +23,8 @@ class VersionUtil { try { + var versionRaw:thx.semver.Version.SemVer = version; + trace('${versionRaw} satisfies (${versionRule})? ${version.satisfies(versionRule)}'); return version.satisfies(versionRule); } catch (e) @@ -40,10 +42,22 @@ class VersionUtil { // This is bad! versionData.version should be an array! trace('[SAVE] Version data repair required! (got ${versionData.version})'); - var fixedVersionData = [versionData.version[0], versionData.version[1], versionData.version[2]]; - versionData.version = fixedVersionData; + // Turn the objects back into arrays. + // I'd use DynamicsT.values but IDK if it maintains order + versionData.version = [versionData.version[0], versionData.version[1], versionData.version[2]]; + + // This is so jank but it should work. + var buildData:Dynamic = cast versionData.build; + var buildDataFixed:Array = thx.Dynamics.DynamicsT.values(buildData) + .map(function(d:Dynamic) return StringId(d.toString())); + versionData.build = buildDataFixed; + + var preData:Dynamic = cast versionData.pre; + var preDataFixed:Array = thx.Dynamics.DynamicsT.values(preData).map(function(d:Dynamic) return StringId(d.toString())); + versionData.pre = preDataFixed; var fixedVersion:thx.semver.Version = versionData; + trace('[SAVE] Fixed version: ${fixedVersion}'); return fixedVersion; } else