From 490b2f18d0b8437dddc1fa6ee68a3c3b8cd38e58 Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Tue, 3 Oct 2023 19:14:46 -0400 Subject: [PATCH] Rewrite save data functionality (now with type safety and migration) --- source/Main.hx | 4 + source/funkin/FreeplayState.hx | 44 +- source/funkin/Highscore.hx | 174 ----- source/funkin/InitState.hx | 28 +- source/funkin/NGio.hx | 12 +- source/funkin/PlayerSettings.hx | 302 +++----- source/funkin/api/newgrounds/NGUtil.hx | 12 +- source/funkin/modding/PolymodHandler.hx | 29 +- source/funkin/play/PlayState.hx | 60 +- source/funkin/save/Save.hx | 686 ++++++++++++++++++ .../save/migrator/RawSaveData_v1_0_0.hx | 52 ++ .../funkin/save/migrator/SaveDataMigrator.hx | 322 ++++++++ source/funkin/ui/story/StoryMenuState.hx | 7 +- 13 files changed, 1274 insertions(+), 458 deletions(-) create mode 100644 source/funkin/save/Save.hx create mode 100644 source/funkin/save/migrator/RawSaveData_v1_0_0.hx create mode 100644 source/funkin/save/migrator/SaveDataMigrator.hx diff --git a/source/Main.hx b/source/Main.hx index 72209cd30..8419d3fb4 100644 --- a/source/Main.hx +++ b/source/Main.hx @@ -4,6 +4,7 @@ import flixel.FlxGame; import flixel.FlxState; import funkin.util.logging.CrashHandler; import funkin.MemoryCounter; +import funkin.save.Save; import haxe.ui.Toolkit; import openfl.display.FPS; import openfl.display.Sprite; @@ -84,6 +85,9 @@ class Main extends Sprite initHaxeUI(); + // George recommends binding the save before FlxGame is created. + Save.load(); + addChild(new FlxGame(gameWidth, gameHeight, initialState, framerate, framerate, skipSplash, startFullscreen)); #if hxcpp_debug_server diff --git a/source/funkin/FreeplayState.hx b/source/funkin/FreeplayState.hx index 6cd353233..00d7422c8 100644 --- a/source/funkin/FreeplayState.hx +++ b/source/funkin/FreeplayState.hx @@ -21,6 +21,8 @@ import flixel.tweens.FlxEase; import flixel.tweens.FlxTween; import flixel.util.FlxColor; import funkin.data.song.SongRegistry; +import funkin.save.Save; +import funkin.save.Save.SaveScoreData; import flixel.util.FlxSpriteUtil; import flixel.util.FlxTimer; import funkin.Controls.Control; @@ -623,11 +625,6 @@ class FreeplayState extends MusicBeatSubState fp.updateScore(Std.int(lerpScore)); txtCompletion.text = Math.floor(lerpCompletion * 100) + "%"; - // trace(Highscore.getCompletion(songs[curSelected].songName, curDifficulty)); - - // trace(intendedScore); - // trace(lerpScore); - // Highscore.getAllScores(); var upP = controls.UI_UP_P; var downP = controls.UI_DOWN_P; @@ -774,8 +771,6 @@ class FreeplayState extends MusicBeatSubState FlxG.sound.play(Paths.sound('cancelMenu')); - // FlxTween.tween(dj, {x: -dj.width}, 0.2, {ease: FlxEase.quartOut}); - var longestTimer:Float = 0; for (grpSpr in exitMovers.keys()) @@ -909,9 +904,20 @@ class FreeplayState extends MusicBeatSubState if (curDifficulty < 0) curDifficulty = 2; if (curDifficulty > 2) curDifficulty = 0; - // intendedScore = Highscore.getScore(songs[curSelected].songName, curDifficulty); - intendedScore = Highscore.getScore(songs[curSelected].songName, curDifficulty); - intendedCompletion = Highscore.getCompletion(songs[curSelected].songName, curDifficulty); + var targetDifficulty:String = switch (curDifficulty) + { + case 0: + 'easy'; + case 1: + 'normal'; + case 2: + 'hard'; + default: 'normal'; + }; + + var songScore:SaveScoreData = Save.get().getSongScore(songs[curSelected].songName, targetDifficulty); + intendedScore = songScore.score; + intendedCompletion = songScore.accuracy; grpDifficulties.group.forEach(function(spr) { spr.visible = false; @@ -955,12 +961,20 @@ class FreeplayState extends MusicBeatSubState if (curSelected < 0) curSelected = grpCapsules.members.length - 1; if (curSelected >= grpCapsules.members.length) curSelected = 0; - // selector.y = (70 * curSelected) + 30; + var targetDifficulty:String = switch (curDifficulty) + { + case 0: + 'easy'; + case 1: + 'normal'; + case 2: + 'hard'; + default: 'normal'; + }; - // intendedScore = Highscore.getScore(songs[curSelected].songName, curDifficulty); - intendedScore = Highscore.getScore(songs[curSelected].songName, curDifficulty); - intendedCompletion = Highscore.getCompletion(songs[curSelected].songName, curDifficulty); - // lerpScore = 0; + var songScore:SaveScoreData = Save.get().getSongScore(songs[curSelected].songName, targetDifficulty); + intendedScore = songScore.score; + intendedCompletion = songScore.accuracy; #if PRELOAD_ALL // FlxG.sound.playMusic(Paths.inst(songs[curSelected].songName), 0); diff --git a/source/funkin/Highscore.hx b/source/funkin/Highscore.hx index 46e98d8dc..3c9fd82e4 100644 --- a/source/funkin/Highscore.hx +++ b/source/funkin/Highscore.hx @@ -2,183 +2,9 @@ package funkin; class Highscore { - #if (haxe >= "4.0.0") - public static var songScores:Map = new Map(); - #else - public static var songScores:Map = new Map(); - #end - - #if (haxe >= "4.0.0") - public static var songCompletion:Map = new Map(); - #else - public static var songCompletion:Map = new Map(); - #end - public static var tallies:Tallies = new Tallies(); - - public static function saveScore(song:String, score:Int = 0, ?diff:Int = 0):Bool - { - var formattedSong:String = formatSong(song, diff); - - #if newgrounds - NGio.postScore(score, song); - #end - - if (songScores.exists(formattedSong)) - { - if (songScores.get(formattedSong) < score) - { - setScore(formattedSong, score); - return true; - // new highscore - } - } - else - setScore(formattedSong, score); - - return false; - } - - public static function saveScoreForDifficulty(song:String, score:Int = 0, diff:String = 'normal'):Bool - { - var diffInt:Int = 1; - - if (diff == 'easy') diffInt = 0; - else if (diff == 'hard') diffInt = 2; - - return saveScore(song, score, diffInt); - } - - public static function saveCompletion(song:String, completion:Float, diff:Int = 0):Bool - { - var formattedSong:String = formatSong(song, diff); - - if (songCompletion.exists(formattedSong)) - { - if (songCompletion.get(formattedSong) < completion) - { - setCompletion(formattedSong, completion); - return true; - } - } - else - setCompletion(formattedSong, completion); - - return false; - } - - public static function saveCompletionForDifficulty(song:String, completion:Float, diff:String = 'normal'):Bool - { - var diffInt:Int = 1; - - if (diff == 'easy') diffInt = 0; - else if (diff == 'hard') diffInt = 2; - - return saveCompletion(song, completion, diffInt); - } - - public static function saveWeekScore(week:String, score:Int = 0, diff:Int = 0):Void - { - #if newgrounds - NGio.postScore(score, 'Campaign ID $week'); - #end - - var formattedSong:String = formatSong(week, diff); - - if (songScores.exists(formattedSong)) - { - if (songScores.get(formattedSong) < score) setScore(formattedSong, score); - } - else - { - setScore(formattedSong, score); - } - } - - public static function saveWeekScoreForDifficulty(week:String, score:Int = 0, diff:String = 'normal'):Void - { - var diffInt:Int = 1; - - if (diff == 'easy') diffInt = 0; - else if (diff == 'hard') diffInt = 2; - - saveWeekScore(week, score, diffInt); - } - - static function setCompletion(formattedSong:String, completion:Float):Void - { - songCompletion.set(formattedSong, completion); - FlxG.save.data.songCompletion = songCompletion; - FlxG.save.flush(); - } - - /** - * YOU SHOULD FORMAT SONG WITH formatSong() BEFORE TOSSING IN SONG VARIABLE - */ - static function setScore(formattedSong:String, score:Int):Void - { - /** GeoKureli - * References to Highscore were wrapped in `#if !switch` blocks. I wasn't sure if this - * is because switch doesn't use NGio, or because switch has a different saving method. - * I moved the compiler flag here, rather than using it everywhere else. - */ - #if ! switch - // Reminder that I don't need to format this song, it should come formatted! - songScores.set(formattedSong, score); - FlxG.save.data.songScores = songScores; - FlxG.save.flush(); - #end - } - - public static function formatSong(song:String, diff:Int):String - { - var daSong:String = song; - - if (diff == 0) daSong += '-easy'; - else if (diff == 2) daSong += '-hard'; - - return daSong; - } - - public static function getScore(song:String, diff:Int):Int - { - if (!songScores.exists(formatSong(song, diff))) setScore(formatSong(song, diff), 0); - - return songScores.get(formatSong(song, diff)); - } - - public static function getCompletion(song, diff):Float - { - if (!songCompletion.exists(formatSong(song, diff))) setCompletion(formatSong(song, diff), 0); - - return songCompletion.get(formatSong(song, diff)); - } - - public static function getAllScores():Void - { - trace(songScores.toString()); - } - - public static function getWeekScore(week:Int, diff:Int):Int - { - if (!songScores.exists(formatSong('week' + week, diff))) setScore(formatSong('week' + week, diff), 0); - - return songScores.get(formatSong('week' + week, diff)); - } - - public static function load():Void - { - if (FlxG.save.data.songScores != null) - { - songScores = FlxG.save.data.songScores; - } - - if (FlxG.save.data.songCompletion != null) songCompletion = FlxG.save.data.songCompletion; - } } -// i only do forward metadata cuz george did! - @:forward abstract Tallies(RawTallies) { diff --git a/source/funkin/InitState.hx b/source/funkin/InitState.hx index e7060abd7..0a7d413c1 100644 --- a/source/funkin/InitState.hx +++ b/source/funkin/InitState.hx @@ -46,7 +46,11 @@ class InitState extends FlxState { setupShit(); - loadSaveData(); + // loadSaveData(); // Moved to Main.hx + // Load player options from save data. + PreferencesMenu.initPrefs(); + // Load controls from save data. + PlayerSettings.init(); startGame(); } @@ -73,10 +77,6 @@ class InitState extends FlxState FlxG.sound.volumeDownKeys = []; FlxG.sound.muteKeys = []; - // TODO: Make sure volume still saves/loads properly. - // if (FlxG.save.data.volume != null) FlxG.sound.volume = FlxG.save.data.volume; - // if (FlxG.save.data.mute != null) FlxG.sound.muted = FlxG.save.data.mute; - // Set the game to a lower frame rate while it is in the background. FlxG.game.focusLostFramerate = 30; @@ -212,24 +212,6 @@ class InitState extends FlxState ModuleHandler.callOnCreate(); } - /** - * Retrive and parse data from the user's save. - */ - function loadSaveData() - { - // Bind save data. - // TODO: Migrate save data to a better format. - FlxG.save.bind('funkin', 'ninjamuffin99'); - - // Load player options from save data. - PreferencesMenu.initPrefs(); - // Load controls from save data. - PlayerSettings.init(); - // Load highscores from save data. - Highscore.load(); - // TODO: Load level/character/cosmetic unlocks from save data. - } - /** * Start the game. * diff --git a/source/funkin/NGio.hx b/source/funkin/NGio.hx index f2afe84db..e5f60c8b5 100644 --- a/source/funkin/NGio.hx +++ b/source/funkin/NGio.hx @@ -86,10 +86,10 @@ class NGio #end var onSessionFail:Error->Void = null; - if (sessionId == null && FlxG.save.data.sessionId != null) + if (sessionId == null && Save.get().ngSessionId != null) { trace("using stored session id"); - sessionId = FlxG.save.data.sessionId; + sessionId = Save.get().ngSessionId; onSessionFail = function(error) savedSessionFailed = true; } #end @@ -159,8 +159,8 @@ class NGio static function onNGLogin():Void { trace('logged in! user:${NG.core.user.name}'); - FlxG.save.data.sessionId = NG.core.sessionId; - FlxG.save.flush(); + Save.get().ngSessionId = NG.core.sessionId; + Save.get().flush(); // Load medals then call onNGMedalFetch() NG.core.requestMedals(onNGMedalFetch); @@ -174,8 +174,8 @@ class NGio { NG.core.logOut(); - FlxG.save.data.sessionId = null; - FlxG.save.flush(); + Save.get().ngSessionId = null; + Save.get().flush(); } // --- MEDALS diff --git a/source/funkin/PlayerSettings.hx b/source/funkin/PlayerSettings.hx index 54fd559fb..a4d8a3b5c 100644 --- a/source/funkin/PlayerSettings.hx +++ b/source/funkin/PlayerSettings.hx @@ -1,5 +1,6 @@ package funkin; +import funkin.save.Save; import funkin.Controls; import flixel.FlxCamera; import funkin.input.PreciseInputManager; @@ -11,121 +12,36 @@ import flixel.util.FlxSignal; // import props.Player; class PlayerSettings { - static public var numPlayers(default, null) = 0; - static public var numAvatars(default, null) = 0; - static public var player1(default, null):PlayerSettings; - static public var player2(default, null):PlayerSettings; + public static var numPlayers(default, null) = 0; + public static var numAvatars(default, null) = 0; + public static var player1(default, null):PlayerSettings; + public static var player2(default, null):PlayerSettings; - static public var onAvatarAdd(default, null) = new FlxTypedSignalVoid>(); - static public var onAvatarRemove(default, null) = new FlxTypedSignalVoid>(); + public static var onAvatarAdd(default, null) = new FlxTypedSignalVoid>(); + public static var onAvatarRemove(default, null) = new FlxTypedSignalVoid>(); public var id(default, null):Int; public var controls(default, null):Controls; - // public var avatar:Player; - // public var camera(get, never):PlayCamera; - - function new(id:Int) + /** + * Return the PlayerSettings for the given player number, or `null` if that player isn't active. + */ + public static function get(id:Int):Null { - trace('loading player settings for id: $id'); - - this.id = id; - this.controls = new Controls('player$id', None); - - #if CLEAR_INPUT_SAVE - FlxG.save.data.controls = null; - FlxG.save.flush(); - #end - - var useDefault = true; - var controlData = FlxG.save.data.controls; - if (controlData != null) + return switch (id) { - var keyData:Dynamic = null; - if (id == 0 && controlData.p1 != null && controlData.p1.keys != null) keyData = controlData.p1.keys; - else if (id == 1 && controlData.p2 != null && controlData.p2.keys != null) keyData = controlData.p2.keys; - - if (keyData != null) - { - useDefault = false; - trace("loaded key data: " + haxe.Json.stringify(keyData)); - controls.fromSaveData(keyData, Keys); - } - } - - if (useDefault) - { - trace("falling back to default control scheme"); - controls.setKeyboardScheme(Solo); - } - - // Apply loaded settings. - PreciseInputManager.instance.initializeKeys(controls); + case 1: player1; + case 2: player2; + default: null; + }; } - function addGamepad(gamepad:FlxGamepad) - { - var useDefault = true; - var controlData = FlxG.save.data.controls; - if (controlData != null) - { - var padData:Dynamic = null; - if (id == 0 && controlData.p1 != null && controlData.p1.pad != null) padData = controlData.p1.pad; - else if (id == 1 && controlData.p2 != null && controlData.p2.pad != null) padData = controlData.p2.pad; - - if (padData != null) - { - useDefault = false; - trace("loaded pad data: " + haxe.Json.stringify(padData)); - controls.addGamepadWithSaveData(gamepad.id, padData); - } - } - - if (useDefault) controls.addDefaultGamepad(gamepad.id); - } - - public function saveControls() - { - if (FlxG.save.data.controls == null) FlxG.save.data.controls = {}; - - var playerData:{?keys:Dynamic, ?pad:Dynamic} - if (id == 0) - { - if (FlxG.save.data.controls.p1 == null) FlxG.save.data.controls.p1 = {}; - playerData = FlxG.save.data.controls.p1; - } - else - { - if (FlxG.save.data.controls.p2 == null) FlxG.save.data.controls.p2 = {}; - playerData = FlxG.save.data.controls.p2; - } - - var keyData = controls.createSaveData(Keys); - if (keyData != null) - { - playerData.keys = keyData; - trace("saving key data: " + haxe.Json.stringify(keyData)); - } - - if (controls.gamepadsAdded.length > 0) - { - var padData = controls.createSaveData(Gamepad(controls.gamepadsAdded[0])); - if (padData != null) - { - trace("saving pad data: " + haxe.Json.stringify(padData)); - playerData.pad = padData; - } - } - - FlxG.save.flush(); - } - - static public function init():Void + public static function init():Void { if (player1 == null) { - player1 = new PlayerSettings(0); + player1 = new PlayerSettings(1); ++numPlayers; } @@ -137,26 +53,13 @@ class PlayerSettings var gamepad = FlxG.gamepads.getByID(i); if (gamepad != null) onGamepadAdded(gamepad); } + } - // player1.controls.addDefaultGamepad(0); - // } - - // if (numGamepads > 1) - // { - // if (player2 == null) - // { - // player2 = new PlayerSettings(1, None); - // ++numPlayers; - // } - - // var gamepad = FlxG.gamepads.getByID(1); - // if (gamepad == null) - // throw 'Unexpected null gamepad. id:0'; - - // player2.controls.addDefaultGamepad(1); - // } - - // DeviceManager.init(); + public static function reset() + { + player1 = null; + player2 = null; + numPlayers = 0; } static function onGamepadAdded(gamepad:FlxGamepad) @@ -164,86 +67,85 @@ class PlayerSettings player1.addGamepad(gamepad); } - /* - public function setKeyboardScheme(scheme) - { - controls.setKeyboardScheme(scheme); - } - - static public function addAvatar(avatar:Player):PlayerSettings - { - var settings:PlayerSettings; - - if (player1 == null) - { - player1 = new PlayerSettings(0, Solo); - ++numPlayers; - } - - if (player1.avatar == null) - settings = player1; - else - { - if (player2 == null) - { - if (player1.controls.keyboardScheme.match(Duo(true))) - player2 = new PlayerSettings(1, Duo(false)); - else - player2 = new PlayerSettings(1, None); - ++numPlayers; - } - - if (player2.avatar == null) - settings = player2; - else - throw throw 'Invalid number of players: ${numPlayers + 1}'; - } - ++numAvatars; - settings.avatar = avatar; - avatar.settings = settings; - - splitCameras(); - - onAvatarAdd.dispatch(settings); - - return settings; - } - - static public function removeAvatar(avatar:Player):Void - { - var settings:PlayerSettings; - - if (player1 != null && player1.avatar == avatar) - settings = player1; - else if (player2 != null && player2.avatar == avatar) - { - settings = player2; - if (player1.controls.keyboardScheme.match(Duo(_))) - player1.setKeyboardScheme(Solo); - } - else - throw "Cannot remove avatar that is not for a player"; - - settings.avatar = null; - while (settings.controls.gamepadsAdded.length > 0) - { - final id = settings.controls.gamepadsAdded.shift(); - settings.controls.removeGamepad(id); - DeviceManager.releaseGamepad(FlxG.gamepads.getByID(id)); - } - - --numAvatars; - - splitCameras(); - - onAvatarRemove.dispatch(avatar.settings); - } - + /** + * @param id The player number this represents. This was refactored to START AT `1`. */ - static public function reset() + private function new(id:Int) { - player1 = null; - player2 = null; - numPlayers = 0; + trace('loading player settings for id: $id'); + + this.id = id; + this.controls = new Controls('player$id', None); + + var useDefault = true; + if (Save.get().hasControls(id, Keys)) + { + var keyControlData = Save.get().getControls(id, Keys); + trace("keyControlData: " + haxe.Json.stringify(keyControlData)); + useDefault = false; + controls.fromSaveData(keyControlData, Keys); + } + else + { + useDefault = true; + } + + if (useDefault) + { + trace("Loading default keyboard control scheme"); + controls.setKeyboardScheme(Solo); + } + + // Apply loaded settings. + PreciseInputManager.instance.initializeKeys(controls); + } + + /** + * Called after an FlxGamepad has been detected. + * @param gamepad The gamepad that was detected. + */ + function addGamepad(gamepad:FlxGamepad) + { + var useDefault = true; + if (Save.get().hasControls(id, Gamepad(gamepad.id))) + { + var padControlData = Save.get().getControls(id, Gamepad(gamepad.id)); + trace("padControlData: " + haxe.Json.stringify(padControlData)); + useDefault = false; + controls.addGamepadWithSaveData(gamepad.id, padControlData); + } + else + { + useDefault = true; + } + + if (useDefault) + { + trace("Loading gamepad control scheme"); + controls.addDefaultGamepad(gamepad.id); + } + } + + /** + * Save this player's controls to the game's persistent save. + */ + public function saveControls() + { + var keyData = controls.createSaveData(Keys); + if (keyData != null) + { + trace("saving key data: " + haxe.Json.stringify(keyData)); + Save.get().setControls(id, Keys, keyData); + } + + if (controls.gamepadsAdded.length > 0) + { + var padData = controls.createSaveData(Gamepad(controls.gamepadsAdded[0])); + if (padData != null) + { + trace("saving pad data: " + haxe.Json.stringify(padData)); + Save.get().setControls(id, Gamepad(controls.gamepadsAdded[0]), padData); + } + } } } diff --git a/source/funkin/api/newgrounds/NGUtil.hx b/source/funkin/api/newgrounds/NGUtil.hx index 773e2f98f..ba7d5f916 100644 --- a/source/funkin/api/newgrounds/NGUtil.hx +++ b/source/funkin/api/newgrounds/NGUtil.hx @@ -86,10 +86,10 @@ class NGUtil #end var onSessionFail:Error->Void = null; - if (sessionId == null && FlxG.save.data.sessionId != null) + if (sessionId == null && Save.get().ngSessionId != null) { trace("using stored session id"); - sessionId = FlxG.save.data.sessionId; + sessionId = Save.get().ngSessionId; onSessionFail = function(error) savedSessionFailed = true; } #end @@ -159,8 +159,8 @@ class NGUtil static function onNGLogin():Void { trace('logged in! user:${NG.core.user.name}'); - FlxG.save.data.sessionId = NG.core.sessionId; - FlxG.save.flush(); + Save.get().ngSessionId = NG.core.sessionId; + Save.get().flush(); // Load medals then call onNGMedalFetch() NG.core.requestMedals(onNGMedalFetch); @@ -174,8 +174,8 @@ class NGUtil { NG.core.logOut(); - FlxG.save.data.sessionId = null; - FlxG.save.flush(); + Save.get().ngSessionId = null; + Save.get().flush(); } // --- MEDALS diff --git a/source/funkin/modding/PolymodHandler.hx b/source/funkin/modding/PolymodHandler.hx index f7f69428b..7716f0f02 100644 --- a/source/funkin/modding/PolymodHandler.hx +++ b/source/funkin/modding/PolymodHandler.hx @@ -14,6 +14,7 @@ import funkin.data.level.LevelRegistry; import funkin.data.notestyle.NoteStyleRegistry; import funkin.play.cutscene.dialogue.ConversationDataParser; import funkin.play.cutscene.dialogue.DialogueBoxDataParser; +import funkin.save.Save; import funkin.play.cutscene.dialogue.SpeakerDataParser; import funkin.data.song.SongRegistry; @@ -59,7 +60,7 @@ class PolymodHandler createModRoot(); trace("Initializing Polymod (using configured mods)..."); - loadModsById(getEnabledModIds()); + loadModsById(Save.get().enabledModIds); } /** @@ -232,33 +233,9 @@ class PolymodHandler return modIds; } - public static function setEnabledMods(newModList:Array):Void - { - FlxG.save.data.enabledMods = newModList; - // Make sure to COMMIT the changes. - FlxG.save.flush(); - } - - /** - * Returns the list of enabled mods. - * @return Array - */ - public static function getEnabledModIds():Array - { - if (FlxG.save.data.enabledMods == null) - { - // NOTE: If the value is null, the enabled mod list is unconfigured. - // Currently, we default to disabling newly installed mods. - // If we want to auto-enable new mods, but otherwise leave the configured list in place, - // we will need some custom logic. - FlxG.save.data.enabledMods = []; - } - return FlxG.save.data.enabledMods; - } - public static function getEnabledMods():Array { - var modIds = getEnabledModIds(); + var modIds = Save.get().enabledModIds; var modMetadata = getAllMods(); var enabledMods = []; for (item in modMetadata) diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx index 46938215b..8eff610c2 100644 --- a/source/funkin/play/PlayState.hx +++ b/source/funkin/play/PlayState.hx @@ -25,6 +25,7 @@ import flixel.ui.FlxBar; import flixel.util.FlxColor; import flixel.util.FlxTimer; import funkin.audio.VoicesGroup; +import funkin.save.Save; import funkin.Highscore.Tallies; import funkin.input.PreciseInputManager; import funkin.modding.events.ScriptEvent; @@ -2495,9 +2496,32 @@ class PlayState extends MusicBeatSubState if (currentSong != null && currentSong.validScore) { // crackhead double thingie, sets whether was new highscore, AND saves the song! - Highscore.tallies.isNewHighscore = Highscore.saveScoreForDifficulty(currentSong.id, songScore, currentDifficulty); + var data = + { + score: songScore, + tallies: + { + killer: Highscore.tallies.killer, + sick: Highscore.tallies.sick, + good: Highscore.tallies.good, + bad: Highscore.tallies.bad, + shit: Highscore.tallies.shit, + missed: Highscore.tallies.missed, + combo: Highscore.tallies.combo, + maxCombo: Highscore.tallies.maxCombo, + totalNotesHit: Highscore.tallies.totalNotesHit, + totalNotes: Highscore.tallies.totalNotes, + }, + accuracy: Highscore.tallies.totalNotesHit / Highscore.tallies.totalNotes, + }; - Highscore.saveCompletionForDifficulty(currentSong.id, Highscore.tallies.totalNotesHit / Highscore.tallies.totalNotes, currentDifficulty); + if (Save.get().isSongHighScore(currentSong.id, currentDifficulty, data)) + { + Save.get().setSongScore(currentSong.id, currentDifficulty, data); + #if newgrounds + NGio.postScore(score, currentSong.id); + #end + } } if (PlayStatePlaylist.isStoryMode) @@ -2521,11 +2545,35 @@ class PlayState extends MusicBeatSubState if (currentSong.validScore) { NGio.unlockMedal(60961); - Highscore.saveWeekScoreForDifficulty(PlayStatePlaylist.campaignId, PlayStatePlaylist.campaignScore, currentDifficulty); - } - // FlxG.save.data.weekUnlocked = StoryMenuState.weekUnlocked; - FlxG.save.flush(); + var data = + { + score: PlayStatePlaylist.campaignScore, + tallies: + { + // TODO: Sum up the values for the whole level! + killer: 0, + sick: 0, + good: 0, + bad: 0, + shit: 0, + missed: 0, + combo: 0, + maxCombo: 0, + totalNotesHit: 0, + totalNotes: 0, + }, + accuracy: Highscore.tallies.totalNotesHit / Highscore.tallies.totalNotes, + }; + + if (Save.get().isLevelHighScore(PlayStatePlaylist.campaignId, currentDifficulty, data)) + { + Save.get().setLevelScore(PlayStatePlaylist.campaignId, currentDifficulty, data); + #if newgrounds + NGio.postScore(score, 'Level ${PlayStatePlaylist.campaignId}'); + #end + } + } if (isSubState) { diff --git a/source/funkin/save/Save.hx b/source/funkin/save/Save.hx new file mode 100644 index 000000000..d1f9800ea --- /dev/null +++ b/source/funkin/save/Save.hx @@ -0,0 +1,686 @@ +package funkin.save; + +import flixel.util.FlxSave; +import funkin.save.migrator.SaveDataMigrator; +import thx.semver.Version; +import funkin.Controls.Device; +import funkin.save.migrator.RawSaveData_v1_0_0; + +@:nullSafety +@:forward(volume, mute) +abstract Save(RawSaveData) +{ + public static final SAVE_DATA_VERSION:thx.semver.Version = "2.0.0"; + 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. + static final SAVE_PATH:String = 'FunkinCrew'; + static final SAVE_NAME:String = 'Funkin'; + + static final SAVE_PATH_LEGACY:String = 'ninjamuffin99'; + static final SAVE_NAME_LEGACY:String = 'funkin'; + + public static function load():Void + { + trace("[SAVE] Loading save..."); + + // Bind save data. + loadFromSlot(1); + } + + public static function get():Save + { + return FlxG.save.data; + } + + /** + * Constructing a new Save will load the default values. + */ + public function new() + { + this = + { + version: Save.SAVE_DATA_VERSION, + + volume: 1.0, + mute: false, + + api: + { + newgrounds: + { + sessionId: null, + } + }, + scores: + { + // No saved scores. + levels: [], + songs: [], + }, + options: + { + // Reasonable defaults. + naughtyness: true, + downscroll: false, + flashingMenu: true, + zoomCamera: true, + debugDisplay: false, + pauseOnTabOut: true, + + controls: + { + // Leave controls blank so defaults are loaded. + p1: + { + keyboard: {}, + gamepad: {}, + }, + p2: + { + keyboard: {}, + gamepad: {}, + }, + }, + }, + + mods: + { + // No mods enabled. + enabledMods: [], + modSettings: [], + }, + + optionsChartEditor: + { + // Reasonable defaults. + }, + }; + } + + /** + * The current session ID for the logged-in Newgrounds user, or null if the user is cringe. + */ + public var ngSessionId(get, set):Null; + + function get_ngSessionId():Null + { + return this.api.newgrounds.sessionId; + } + + function set_ngSessionId(value:Null):Null + { + return this.api.newgrounds.sessionId = value; + } + + public var enabledModIds(get, set):Array; + + function get_enabledModIds():Array + { + return this.mods.enabledMods; + } + + function set_enabledModIds(value:Array):Array + { + return this.mods.enabledMods = value; + } + + /** + * Return the score the user achieved for a given level on a given difficulty. + * + * @param levelId The ID of the level/week. + * @param difficultyId The difficulty to check. + * @return A data structure containing score, judgement counts, and accuracy. Returns `null` if no score is saved. + */ + public function getLevelScore(levelId:String, difficultyId:String = 'normal'):Null + { + var level = this.scores.levels.get(levelId); + if (level == null) + { + level = []; + this.scores.levels.set(levelId, level); + } + + return level.get(difficultyId); + } + + /** + * Apply the score the user achieved for a given level on a given difficulty. + */ + public function setLevelScore(levelId:String, difficultyId:String, score:SaveScoreData):Void + { + var level = this.scores.levels.get(levelId); + if (level == null) + { + level = []; + this.scores.levels.set(levelId, level); + } + level.set(difficultyId, score); + + flush(); + } + + public function isLevelHighScore(levelId:String, difficultyId:String = 'normal', score:SaveScoreData):Bool + { + var level = this.scores.levels.get(levelId); + if (level == null) + { + level = []; + this.scores.levels.set(levelId, level); + } + + var currentScore = level.get(difficultyId); + if (currentScore == null) + { + return true; + } + + return score.score > currentScore.score; + } + + public function hasBeatenLevel(levelId:String, ?difficultyList:Array):Bool + { + if (difficultyList == null) + { + difficultyList = ['easy', 'normal', 'hard']; + } + for (difficulty in difficultyList) + { + var score:Null = getLevelScore(levelId, difficulty); + // TODO: Do we need to check accuracy/score here? + if (score != null) + { + return true; + } + } + return false; + } + + /** + * Return the score the user achieved for a given song on a given difficulty. + * + * @param songId The ID of the song. + * @param difficultyId The difficulty to check. + * @return A data structure containing score, judgement counts, and accuracy. Returns `null` if no score is saved. + */ + public function getSongScore(songId:String, difficultyId:String = 'normal'):Null + { + var song = this.scores.songs.get(songId); + if (song == null) + { + song = []; + this.scores.songs.set(songId, song); + } + return song.get(difficultyId); + } + + /** + * Apply the score the user achieved for a given song on a given difficulty. + */ + public function setSongScore(songId:String, difficultyId:String, score:SaveScoreData):Void + { + var song = this.scores.songs.get(songId); + if (song == null) + { + song = []; + this.scores.songs.set(songId, song); + } + song.set(difficultyId, score); + + flush(); + } + + /** + * Is the provided score data better than the current high score for the given song? + * @param songId The song ID to check. + * @param difficultyId The difficulty to check. + * @param score The score to check. + * @return Whether the score is better than the current high score. + */ + public function isSongHighScore(songId:String, difficultyId:String = 'normal', score:SaveScoreData):Bool + { + var song = this.scores.songs.get(songId); + if (song == null) + { + song = []; + this.scores.songs.set(songId, song); + } + + var currentScore = song.get(difficultyId); + if (currentScore == null) + { + return true; + } + + return score.score > currentScore.score; + } + + /** + * Has the provided song been beaten on one of the listed difficulties? + * @param songId The song ID to check. + * @param difficultyList The difficulties to check. Defaults to `easy`, `normal`, and `hard`. + * @return Whether the song has been beaten on any of the listed difficulties. + */ + public function hasBeatenSong(songId:String, ?difficultyList:Array):Bool + { + if (difficultyList == null) + { + difficultyList = ['easy', 'normal', 'hard']; + } + for (difficulty in difficultyList) + { + var score:Null = getSongScore(songId, difficulty); + // TODO: Do we need to check accuracy/score here? + if (score != null) + { + return true; + } + } + return false; + } + + public function getControls(playerId:Int, inputType:Device):SaveControlsData + { + switch (inputType) + { + case Keys: + return (playerId == 0) ? this.options.controls.p1.keyboard : this.options.controls.p2.keyboard; + case Gamepad(_): + return (playerId == 0) ? this.options.controls.p1.gamepad : this.options.controls.p2.gamepad; + } + } + + public function hasControls(playerId:Int, inputType:Device):Bool + { + var controls = getControls(playerId, inputType); + var controlsFields = Reflect.fields(controls); + return controlsFields.length > 0; + } + + public function setControls(playerId:Int, inputType:Device, controls:SaveControlsData):Void + { + switch (inputType) + { + case Keys: + if (playerId == 0) + { + this.options.controls.p1.keyboard = controls; + } + else + { + this.options.controls.p2.keyboard = controls; + } + case Gamepad(_): + if (playerId == 0) + { + this.options.controls.p1.gamepad = controls; + } + else + { + this.options.controls.p2.gamepad = controls; + } + } + + flush(); + } + + public function isCharacterUnlocked(characterId:String):Bool + { + switch (characterId) + { + case 'bf': + return true; + case 'pico': + return hasBeatenLevel('weekend1'); + default: + trace('Unknown character ID: ' + characterId); + return true; + } + } + + /** + * Call this to make sure the save data is written to disk. + */ + public function flush():Void + { + FlxG.save.flush(); + } + + /** + * If you set slot to `2`, it will load an independe + * @param slot + */ + static function loadFromSlot(slot:Int):Void + { + trace("[SAVE] Loading save from slot " + slot + "..."); + + FlxG.save.bind('$SAVE_NAME${slot}', SAVE_PATH); + + if (FlxG.save.isEmpty()) + { + trace('[SAVE] Save data is empty, checking for legacy save data...'); + var legacySaveData = fetchLegacySaveData(); + if (legacySaveData != null) + { + trace('[SAVE] Found legacy save data, converting...'); + FlxG.save.mergeData(SaveDataMigrator.migrateFromLegacy(legacySaveData)); + } + } + else + { + trace('[SAVE] Loaded save data.'); + FlxG.save.mergeData(SaveDataMigrator.migrate(FlxG.save.data)); + } + + trace('[SAVE] Done loading save data.'); + trace(FlxG.save.data); + } + + static function fetchLegacySaveData():Null + { + trace("[SAVE] Checking for legacy save data..."); + var legacySave:FlxSave = new FlxSave(); + legacySave.bind(SAVE_NAME_LEGACY, SAVE_PATH_LEGACY); + if (legacySave?.data == null) + { + trace("[SAVE] No legacy save data found."); + return null; + } + else + { + trace("[SAVE] Legacy save data found."); + trace(legacySave.data); + return cast legacySave.data; + } + } +} + +/** + * An anonymous structure containingg all the user's save data. + */ +typedef RawSaveData = +{ + // Flixel save data. + var volume:Float; + var mute:Bool; + + /** + * A semantic versioning string for the save data format. + */ + var version:Version; + + var api:SaveApiData; + + /** + * The user's saved scores. + */ + var scores:SaveHighScoresData; + + /** + * The user's preferences. + */ + var options:SaveDataOptions; + + var mods:SaveDataMods; + + /** + * The user's preferences specific to the Chart Editor. + */ + var optionsChartEditor:SaveDataChartEditorOptions; +}; + +typedef SaveApiData = +{ + var newgrounds:SaveApiNewgroundsData; +} + +typedef SaveApiNewgroundsData = +{ + var sessionId:Null; +} + +/** + * An anoymous structure containing options about the user's high scores. + */ +typedef SaveHighScoresData = +{ + /** + * Scores for each level (or week). + */ + var levels:SaveScoreLevelsData; + + /** + * Scores for individual songs. + */ + var songs:SaveScoreSongsData; +}; + +typedef SaveDataMods = +{ + var enabledMods:Array; + var modSettings:Map; +} + +/** + * Key is the level ID, value is the SaveScoreLevelData. + */ +typedef SaveScoreLevelsData = Map; + +/** + * Key is the song ID, value is the data for each difficulty. + */ +typedef SaveScoreSongsData = Map; + +/** + * Key is the difficulty ID, value is the score. + */ +typedef SaveScoreDifficultiesData = Map; + +/** + * An individual score. Contains the score, accuracy, and count of each judgement hit. + */ +typedef SaveScoreData = +{ + /** + * The score achieved. + */ + var score:Int; + + /** + * The count of each judgement hit. + */ + var tallies:SaveScoreTallyData; + + /** + * The accuracy percentage. + */ + var accuracy:Float; +} + +typedef SaveScoreTallyData = +{ + var killer:Int; + var sick:Int; + var good:Int; + var bad:Int; + var shit:Int; + var missed:Int; + var combo:Int; + var maxCombo:Int; + var totalNotesHit:Int; + var totalNotes:Int; +} + +/** + * An anonymous structure containing all the user's options and preferences for the main game. + * Every time you add a new option, it needs to be added here. + */ +typedef SaveDataOptions = +{ + /** + * Whether some particularly fowl language is displayed. + * @default `true` + */ + var naughtyness:Bool; + + /** + * If enabled, the strumline is at the bottom of the screen rather than the top. + * @default `false` + */ + var downscroll:Bool; + + /** + * If disabled, the main menu won't flash when entering a submenu. + * @default `true` + */ + var flashingMenu:Bool; + + /** + * If disabled, the camera bump synchronized to the beat. + * @default `false` + */ + var zoomCamera:Bool; + + /** + * If enabled, an FPS and memory counter will be displayed even if this is not a debug build. + * @default `false` + */ + var debugDisplay:Bool; + + /** + * If enabled, the game will automatically pause when tabbing out. + * @default `true` + */ + var pauseOnTabOut:Bool; + + var controls: + { + var p1: + { + var keyboard:SaveControlsData; + var gamepad:SaveControlsData; + }; + var p2: + { + var keyboard:SaveControlsData; + var gamepad:SaveControlsData; + }; + }; +}; + +/** + * An anonymous structure containing a specific player's bound keys. + * Each key is an action name and each value is an array of keycodes. + * + * If a keybind is `null`, it needs to be reinitialized to the default. + * If a keybind is `[]`, it is UNBOUND by the user and should not be rebound. + */ +typedef SaveControlsData = +{ + /** + * Keybind for navigating in the menu. + * @default `Up Arrow` + */ + var ?UI_UP:Array; + + /** + * Keybind for navigating in the menu. + * @default `Left Arrow` + */ + var ?UI_LEFT:Array; + + /** + * Keybind for navigating in the menu. + * @default `Right Arrow` + */ + var ?UI_RIGHT:Array; + + /** + * Keybind for navigating in the menu. + * @default `Down Arrow` + */ + var ?UI_DOWN:Array; + + /** + * Keybind for hitting notes. + * @default `A` and `Left Arrow` + */ + var ?NOTE_LEFT:Array; + + /** + * Keybind for hitting notes. + * @default `W` and `Up Arrow` + */ + var ?NOTE_UP:Array; + + /** + * Keybind for hitting notes. + * @default `S` and `Down Arrow` + */ + var ?NOTE_DOWN:Array; + + /** + * Keybind for hitting notes. + * @default `D` and `Right Arrow` + */ + var ?NOTE_RIGHT:Array; + + /** + * Keybind for continue/OK in menus. + * @default `Enter` and `Space` + */ + var ?ACCEPT:Array; + + /** + * Keybind for back/cancel in menus. + * @default `Escape` + */ + var ?BACK:Array; + + /** + * Keybind for pausing the game. + * @default `Escape` + */ + var ?PAUSE:Array; + + /** + * Keybind for advancing cutscenes. + * @default `Z` and `Space` and `Enter` + */ + var ?CUTSCENE_ADVANCE:Array; + + /** + * Keybind for skipping a cutscene. + * @default `Escape` + */ + var ?CUTSCENE_SKIP:Array; + + /** + * Keybind for increasing volume. + * @default `Plus` + */ + var ?VOLUME_UP:Array; + + /** + * Keybind for decreasing volume. + * @default `Minus` + */ + var ?VOLUME_DOWN:Array; + + /** + * Keybind for muting/unmuting volume. + * @default `Zero` + */ + var ?VOLUME_MUTE:Array; + + /** + * Keybind for restarting a song. + * @default `R` + */ + var ?RESET:Array; +} + +/** + * An anonymous structure containing all the user's options and preferences, specific to the Chart Editor. + */ +typedef SaveDataChartEditorOptions = {}; diff --git a/source/funkin/save/migrator/RawSaveData_v1_0_0.hx b/source/funkin/save/migrator/RawSaveData_v1_0_0.hx new file mode 100644 index 000000000..b71102cce --- /dev/null +++ b/source/funkin/save/migrator/RawSaveData_v1_0_0.hx @@ -0,0 +1,52 @@ +package funkin.save.migrator; + +import thx.semver.Version; + +typedef RawSaveData_v1_0_0 = +{ + var seenVideo:Bool; + var mute:Bool; + var volume:Float; + + var sessionId:String; + + var songCompletion:Map; + + var songScores:Map; + + var ?controls: + { + ?p1:SavePlayerControlsData_v1_0_0, + ?p2:SavePlayerControlsData_v1_0_0 + }; + var enabledMods:Array; + var weeksUnlocked:Array; + var windowSettings:Array; +} + +typedef SavePlayerControlsData_v1_0_0 = +{ + var keys:SaveControlsData_v1_0_0; + var pad:SaveControlsData_v1_0_0; +}; + +typedef SaveControlsData_v1_0_0 = +{ + var ?ACCEPT:Array; + var ?BACK:Array; + var ?CUTSCENE_ADVANCE:Array; + var ?CUTSCENE_SKIP:Array; + var ?NOTE_DOWN:Array; + var ?NOTE_LEFT:Array; + var ?NOTE_RIGHT:Array; + var ?NOTE_UP:Array; + var ?PAUSE:Array; + var ?RESET:Array; + var ?UI_DOWN:Array; + var ?UI_LEFT:Array; + var ?UI_RIGHT:Array; + var ?UI_UP:Array; + var ?VOLUME_DOWN:Array; + var ?VOLUME_MUTE:Array; + var ?VOLUME_UP:Array; +}; diff --git a/source/funkin/save/migrator/SaveDataMigrator.hx b/source/funkin/save/migrator/SaveDataMigrator.hx new file mode 100644 index 000000000..e7b7c7583 --- /dev/null +++ b/source/funkin/save/migrator/SaveDataMigrator.hx @@ -0,0 +1,322 @@ +package funkin.save.migrator; + +import funkin.save.Save; +import funkin.save.migrator.RawSaveData_v1_0_0; +import thx.semver.Version; +import funkin.util.VersionUtil; + +@:nullSafety +class SaveDataMigrator +{ + /** + * Migrate from one 2.x version to another. + */ + public static function migrate(inputData:Dynamic):Save + { + // This deserializes directly into a `Version` object, not a `String`. + var version:Null = inputData?.version ?? null; + + if (version == null) + { + trace('[SAVE] No version found in save data! Returning blank data.'); + trace(inputData); + return new Save(); + } + else + { + if (VersionUtil.validateVersionStr(version, Save.SAVE_DATA_VERSION_RULE)) + { + // Simply cast the structured data. + var save:Save = inputData; + return save; + } + else + { + trace('[SAVE] Invalid save data version! Returning blank data.'); + trace(inputData); + return new Save(); + } + } + } + + /** + * Migrate from 1.x to the latest version. + */ + public static function migrateFromLegacy(inputData:Dynamic):Save + { + var inputSaveData:RawSaveData_v1_0_0 = cast inputData; + + var result:Save = new Save(); + + result.volume = inputSaveData.volume; + result.mute = inputSaveData.mute; + + result.ngSessionId = inputSaveData.sessionId; + + // TODO: Port over the save data from the legacy save data format. + migrateLegacyScores(result, inputSaveData); + + migrateLegacyControls(result, inputSaveData); + + return result; + } + + static function migrateLegacyScores(result:Save, inputSaveData:RawSaveData_v1_0_0):Void + { + if (inputSaveData.songCompletion == null) + { + inputSaveData.songCompletion = []; + } + + if (inputSaveData.songScores == null) + { + inputSaveData.songScores = []; + } + + migrateLegacyLevelScore(result, inputSaveData, 'week0'); + migrateLegacyLevelScore(result, inputSaveData, 'week1'); + migrateLegacyLevelScore(result, inputSaveData, 'week2'); + migrateLegacyLevelScore(result, inputSaveData, 'week3'); + migrateLegacyLevelScore(result, inputSaveData, 'week4'); + migrateLegacyLevelScore(result, inputSaveData, 'week5'); + migrateLegacyLevelScore(result, inputSaveData, 'week6'); + migrateLegacyLevelScore(result, inputSaveData, 'week7'); + + migrateLegacySongScore(result, inputSaveData, ['tutorial', 'Tutorial']); + + migrateLegacySongScore(result, inputSaveData, ['bopeebo', 'Bopeebo']); + migrateLegacySongScore(result, inputSaveData, ['fresh', 'Fresh']); + migrateLegacySongScore(result, inputSaveData, ['dadbattle', 'Dadbattle']); + + migrateLegacySongScore(result, inputSaveData, ['monster', 'Monster']); + migrateLegacySongScore(result, inputSaveData, ['south', 'South']); + migrateLegacySongScore(result, inputSaveData, ['spookeez', 'Spookeez']); + + migrateLegacySongScore(result, inputSaveData, ['pico', 'Pico']); + migrateLegacySongScore(result, inputSaveData, ['philly-nice', 'Philly', 'philly', 'Philly-Nice']); + migrateLegacySongScore(result, inputSaveData, ['blammed', 'Blammed']); + + migrateLegacySongScore(result, inputSaveData, ['satin-panties', 'Satin-Panties']); + migrateLegacySongScore(result, inputSaveData, ['high', 'High']); + migrateLegacySongScore(result, inputSaveData, ['milf', 'Milf', 'MILF']); + + migrateLegacySongScore(result, inputSaveData, ['cocoa', 'Cocoa']); + migrateLegacySongScore(result, inputSaveData, ['eggnog', 'Eggnog']); + migrateLegacySongScore(result, inputSaveData, ['winter-horrorland', 'Winter-Horrorland']); + + migrateLegacySongScore(result, inputSaveData, ['senpai', 'Senpai']); + migrateLegacySongScore(result, inputSaveData, ['roses', 'Roses']); + migrateLegacySongScore(result, inputSaveData, ['thorns', 'Thorns']); + + migrateLegacySongScore(result, inputSaveData, ['ugh', 'Ugh']); + migrateLegacySongScore(result, inputSaveData, ['guns', 'Guns']); + migrateLegacySongScore(result, inputSaveData, ['stress', 'Stress']); + } + + static function migrateLegacyLevelScore(result:Save, inputSaveData:RawSaveData_v1_0_0, levelId:String):Void + { + var scoreDataEasy:SaveScoreData = + { + score: inputSaveData.songScores.get('${levelId}-easy') ?? 0, + accuracy: inputSaveData.songCompletion.get('${levelId}-easy') ?? 0.0, + tallies: + { + killer: 0, + sick: 0, + good: 0, + bad: 0, + shit: 0, + missed: 0, + combo: 0, + maxCombo: 0, + totalNotesHit: 0, + totalNotes: 0, + } + }; + result.setLevelScore(levelId, 'easy', scoreDataEasy); + + var scoreDataNormal:SaveScoreData = + { + score: inputSaveData.songScores.get('${levelId}') ?? 0, + accuracy: inputSaveData.songCompletion.get('${levelId}') ?? 0.0, + tallies: + { + killer: 0, + sick: 0, + good: 0, + bad: 0, + shit: 0, + missed: 0, + combo: 0, + maxCombo: 0, + totalNotesHit: 0, + totalNotes: 0, + } + }; + result.setLevelScore(levelId, 'normal', scoreDataNormal); + + var scoreDataHard:SaveScoreData = + { + score: inputSaveData.songScores.get('${levelId}-hard') ?? 0, + accuracy: inputSaveData.songCompletion.get('${levelId}-hard') ?? 0.0, + tallies: + { + killer: 0, + sick: 0, + good: 0, + bad: 0, + shit: 0, + missed: 0, + combo: 0, + maxCombo: 0, + totalNotesHit: 0, + totalNotes: 0, + } + }; + result.setLevelScore(levelId, 'hard', scoreDataHard); + } + + static function migrateLegacySongScore(result:Save, inputSaveData:RawSaveData_v1_0_0, songIds:Array):Void + { + var scoreDataEasy:SaveScoreData = + { + score: 0, + accuracy: 0, + tallies: + { + killer: 0, + sick: 0, + good: 0, + bad: 0, + shit: 0, + missed: 0, + combo: 0, + maxCombo: 0, + totalNotesHit: 0, + totalNotes: 0, + } + }; + + for (songId in songIds) + { + scoreDataEasy.score = Std.int(Math.max(scoreDataEasy.score, inputSaveData.songScores.get('${songId}-easy') ?? 0)); + scoreDataEasy.accuracy = Math.max(scoreDataEasy.accuracy, inputSaveData.songCompletion.get('${songId}-easy') ?? 0.0); + } + result.setSongScore(songIds[0], 'easy', scoreDataEasy); + + var scoreDataNormal:SaveScoreData = + { + score: 0, + accuracy: 0, + tallies: + { + killer: 0, + sick: 0, + good: 0, + bad: 0, + shit: 0, + missed: 0, + combo: 0, + maxCombo: 0, + totalNotesHit: 0, + totalNotes: 0, + } + }; + + for (songId in songIds) + { + scoreDataNormal.score = Std.int(Math.max(scoreDataNormal.score, inputSaveData.songScores.get('${songId}') ?? 0)); + scoreDataNormal.accuracy = Math.max(scoreDataNormal.accuracy, inputSaveData.songCompletion.get('${songId}') ?? 0.0); + } + result.setSongScore(songIds[0], 'normal', scoreDataNormal); + + var scoreDataHard:SaveScoreData = + { + score: 0, + accuracy: 0, + tallies: + { + killer: 0, + sick: 0, + good: 0, + bad: 0, + shit: 0, + missed: 0, + combo: 0, + maxCombo: 0, + totalNotesHit: 0, + totalNotes: 0, + } + }; + + for (songId in songIds) + { + scoreDataHard.score = Std.int(Math.max(scoreDataHard.score, inputSaveData.songScores.get('${songId}-hard') ?? 0)); + scoreDataHard.accuracy = Math.max(scoreDataHard.accuracy, inputSaveData.songCompletion.get('${songId}-hard') ?? 0.0); + } + result.setSongScore(songIds[0], 'hard', scoreDataHard); + } + + static function migrateLegacyControls(result:Save, inputSaveData:RawSaveData_v1_0_0):Void + { + var p1Data = inputSaveData?.controls?.p1; + if (p1Data != null) + { + migrateLegacyPlayerControls(result, 1, p1Data); + } + + var p2Data = inputSaveData?.controls?.p2; + if (p2Data != null) + { + migrateLegacyPlayerControls(result, 2, p2Data); + } + } + + static function migrateLegacyPlayerControls(result:Save, playerId:Int, controlsData:SavePlayerControlsData_v1_0_0):Void + { + var outputKeyControls:SaveControlsData = + { + ACCEPT: controlsData?.keys?.ACCEPT ?? null, + BACK: controlsData?.keys?.BACK ?? null, + CUTSCENE_ADVANCE: controlsData?.keys?.CUTSCENE_ADVANCE ?? null, + CUTSCENE_SKIP: controlsData?.keys?.CUTSCENE_SKIP ?? null, + NOTE_DOWN: controlsData?.keys?.NOTE_DOWN ?? null, + NOTE_LEFT: controlsData?.keys?.NOTE_LEFT ?? null, + NOTE_RIGHT: controlsData?.keys?.NOTE_RIGHT ?? null, + NOTE_UP: controlsData?.keys?.NOTE_UP ?? null, + PAUSE: controlsData?.keys?.PAUSE ?? null, + RESET: controlsData?.keys?.RESET ?? null, + UI_DOWN: controlsData?.keys?.UI_DOWN ?? null, + UI_LEFT: controlsData?.keys?.UI_LEFT ?? null, + UI_RIGHT: controlsData?.keys?.UI_RIGHT ?? null, + UI_UP: controlsData?.keys?.UI_UP ?? null, + VOLUME_DOWN: controlsData?.keys?.VOLUME_DOWN ?? null, + VOLUME_MUTE: controlsData?.keys?.VOLUME_MUTE ?? null, + VOLUME_UP: controlsData?.keys?.VOLUME_UP ?? null, + }; + + var outputPadControls:SaveControlsData = + { + ACCEPT: controlsData?.pad?.ACCEPT ?? null, + BACK: controlsData?.pad?.BACK ?? null, + CUTSCENE_ADVANCE: controlsData?.pad?.CUTSCENE_ADVANCE ?? null, + CUTSCENE_SKIP: controlsData?.pad?.CUTSCENE_SKIP ?? null, + NOTE_DOWN: controlsData?.pad?.NOTE_DOWN ?? null, + NOTE_LEFT: controlsData?.pad?.NOTE_LEFT ?? null, + NOTE_RIGHT: controlsData?.pad?.NOTE_RIGHT ?? null, + NOTE_UP: controlsData?.pad?.NOTE_UP ?? null, + PAUSE: controlsData?.pad?.PAUSE ?? null, + RESET: controlsData?.pad?.RESET ?? null, + UI_DOWN: controlsData?.pad?.UI_DOWN ?? null, + UI_LEFT: controlsData?.pad?.UI_LEFT ?? null, + UI_RIGHT: controlsData?.pad?.UI_RIGHT ?? null, + UI_UP: controlsData?.pad?.UI_UP ?? null, + VOLUME_DOWN: controlsData?.pad?.VOLUME_DOWN ?? null, + VOLUME_MUTE: controlsData?.pad?.VOLUME_MUTE ?? null, + VOLUME_UP: controlsData?.pad?.VOLUME_UP ?? null, + }; + + result.setControls(playerId, Keys, outputKeyControls); + result.setControls(playerId, Gamepad(0), outputPadControls); + } +} diff --git a/source/funkin/ui/story/StoryMenuState.hx b/source/funkin/ui/story/StoryMenuState.hx index 3a5a388a8..16c917e4c 100644 --- a/source/funkin/ui/story/StoryMenuState.hx +++ b/source/funkin/ui/story/StoryMenuState.hx @@ -1,5 +1,7 @@ package funkin.ui.story; +import funkin.save.Save; +import funkin.save.Save.SaveScoreData; import openfl.utils.Assets; import flixel.addons.transition.FlxTransitionableState; import flixel.FlxSprite; @@ -623,7 +625,8 @@ class StoryMenuState extends MusicBeatState tracklistText.screenCenter(X); tracklistText.x -= FlxG.width * 0.35; - // TODO: Fix this. - highScore = Highscore.getWeekScore(0, 0); + var levelScore:Null = Save.get().getLevelScore(currentLevelId, currentDifficultyId); + highScore = levelScore?.score ?? 0; + // levelScore.accuracy } }