Rewrite save data functionality (now with type safety and migration)

This commit is contained in:
EliteMasterEric 2023-10-03 19:14:46 -04:00
parent ed6bc553ed
commit 490b2f18d0
13 changed files with 1274 additions and 458 deletions

View file

@ -4,6 +4,7 @@ import flixel.FlxGame;
import flixel.FlxState; import flixel.FlxState;
import funkin.util.logging.CrashHandler; import funkin.util.logging.CrashHandler;
import funkin.MemoryCounter; import funkin.MemoryCounter;
import funkin.save.Save;
import haxe.ui.Toolkit; import haxe.ui.Toolkit;
import openfl.display.FPS; import openfl.display.FPS;
import openfl.display.Sprite; import openfl.display.Sprite;
@ -84,6 +85,9 @@ class Main extends Sprite
initHaxeUI(); initHaxeUI();
// George recommends binding the save before FlxGame is created.
Save.load();
addChild(new FlxGame(gameWidth, gameHeight, initialState, framerate, framerate, skipSplash, startFullscreen)); addChild(new FlxGame(gameWidth, gameHeight, initialState, framerate, framerate, skipSplash, startFullscreen));
#if hxcpp_debug_server #if hxcpp_debug_server

View file

@ -21,6 +21,8 @@ import flixel.tweens.FlxEase;
import flixel.tweens.FlxTween; import flixel.tweens.FlxTween;
import flixel.util.FlxColor; import flixel.util.FlxColor;
import funkin.data.song.SongRegistry; import funkin.data.song.SongRegistry;
import funkin.save.Save;
import funkin.save.Save.SaveScoreData;
import flixel.util.FlxSpriteUtil; import flixel.util.FlxSpriteUtil;
import flixel.util.FlxTimer; import flixel.util.FlxTimer;
import funkin.Controls.Control; import funkin.Controls.Control;
@ -623,11 +625,6 @@ class FreeplayState extends MusicBeatSubState
fp.updateScore(Std.int(lerpScore)); fp.updateScore(Std.int(lerpScore));
txtCompletion.text = Math.floor(lerpCompletion * 100) + "%"; 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 upP = controls.UI_UP_P;
var downP = controls.UI_DOWN_P; var downP = controls.UI_DOWN_P;
@ -774,8 +771,6 @@ class FreeplayState extends MusicBeatSubState
FlxG.sound.play(Paths.sound('cancelMenu')); FlxG.sound.play(Paths.sound('cancelMenu'));
// FlxTween.tween(dj, {x: -dj.width}, 0.2, {ease: FlxEase.quartOut});
var longestTimer:Float = 0; var longestTimer:Float = 0;
for (grpSpr in exitMovers.keys()) for (grpSpr in exitMovers.keys())
@ -909,9 +904,20 @@ class FreeplayState extends MusicBeatSubState
if (curDifficulty < 0) curDifficulty = 2; if (curDifficulty < 0) curDifficulty = 2;
if (curDifficulty > 2) curDifficulty = 0; if (curDifficulty > 2) curDifficulty = 0;
// intendedScore = Highscore.getScore(songs[curSelected].songName, curDifficulty); var targetDifficulty:String = switch (curDifficulty)
intendedScore = Highscore.getScore(songs[curSelected].songName, curDifficulty); {
intendedCompletion = Highscore.getCompletion(songs[curSelected].songName, 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) { grpDifficulties.group.forEach(function(spr) {
spr.visible = false; spr.visible = false;
@ -955,12 +961,20 @@ class FreeplayState extends MusicBeatSubState
if (curSelected < 0) curSelected = grpCapsules.members.length - 1; if (curSelected < 0) curSelected = grpCapsules.members.length - 1;
if (curSelected >= grpCapsules.members.length) curSelected = 0; 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); var songScore:SaveScoreData = Save.get().getSongScore(songs[curSelected].songName, targetDifficulty);
intendedScore = Highscore.getScore(songs[curSelected].songName, curDifficulty); intendedScore = songScore.score;
intendedCompletion = Highscore.getCompletion(songs[curSelected].songName, curDifficulty); intendedCompletion = songScore.accuracy;
// lerpScore = 0;
#if PRELOAD_ALL #if PRELOAD_ALL
// FlxG.sound.playMusic(Paths.inst(songs[curSelected].songName), 0); // FlxG.sound.playMusic(Paths.inst(songs[curSelected].songName), 0);

View file

@ -2,183 +2,9 @@ package funkin;
class Highscore class Highscore
{ {
#if (haxe >= "4.0.0")
public static var songScores:Map<String, Int> = new Map();
#else
public static var songScores:Map<String, Int> = new Map<String, Int>();
#end
#if (haxe >= "4.0.0")
public static var songCompletion:Map<String, Float> = new Map();
#else
public static var songCompletion:Map<String, Float> = new Map<String, Float>();
#end
public static var tallies:Tallies = new Tallies(); 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 @:forward
abstract Tallies(RawTallies) abstract Tallies(RawTallies)
{ {

View file

@ -46,7 +46,11 @@ class InitState extends FlxState
{ {
setupShit(); setupShit();
loadSaveData(); // loadSaveData(); // Moved to Main.hx
// Load player options from save data.
PreferencesMenu.initPrefs();
// Load controls from save data.
PlayerSettings.init();
startGame(); startGame();
} }
@ -73,10 +77,6 @@ class InitState extends FlxState
FlxG.sound.volumeDownKeys = []; FlxG.sound.volumeDownKeys = [];
FlxG.sound.muteKeys = []; 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. // Set the game to a lower frame rate while it is in the background.
FlxG.game.focusLostFramerate = 30; FlxG.game.focusLostFramerate = 30;
@ -212,24 +212,6 @@ class InitState extends FlxState
ModuleHandler.callOnCreate(); 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. * Start the game.
* *

View file

@ -86,10 +86,10 @@ class NGio
#end #end
var onSessionFail:Error->Void = null; 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"); trace("using stored session id");
sessionId = FlxG.save.data.sessionId; sessionId = Save.get().ngSessionId;
onSessionFail = function(error) savedSessionFailed = true; onSessionFail = function(error) savedSessionFailed = true;
} }
#end #end
@ -159,8 +159,8 @@ class NGio
static function onNGLogin():Void static function onNGLogin():Void
{ {
trace('logged in! user:${NG.core.user.name}'); trace('logged in! user:${NG.core.user.name}');
FlxG.save.data.sessionId = NG.core.sessionId; Save.get().ngSessionId = NG.core.sessionId;
FlxG.save.flush(); Save.get().flush();
// Load medals then call onNGMedalFetch() // Load medals then call onNGMedalFetch()
NG.core.requestMedals(onNGMedalFetch); NG.core.requestMedals(onNGMedalFetch);
@ -174,8 +174,8 @@ class NGio
{ {
NG.core.logOut(); NG.core.logOut();
FlxG.save.data.sessionId = null; Save.get().ngSessionId = null;
FlxG.save.flush(); Save.get().flush();
} }
// --- MEDALS // --- MEDALS

View file

@ -1,5 +1,6 @@
package funkin; package funkin;
import funkin.save.Save;
import funkin.Controls; import funkin.Controls;
import flixel.FlxCamera; import flixel.FlxCamera;
import funkin.input.PreciseInputManager; import funkin.input.PreciseInputManager;
@ -11,121 +12,36 @@ import flixel.util.FlxSignal;
// import props.Player; // import props.Player;
class PlayerSettings class PlayerSettings
{ {
static public var numPlayers(default, null) = 0; public static var numPlayers(default, null) = 0;
static public var numAvatars(default, null) = 0; public static var numAvatars(default, null) = 0;
static public var player1(default, null):PlayerSettings; public static var player1(default, null):PlayerSettings;
static public var player2(default, null):PlayerSettings; public static var player2(default, null):PlayerSettings;
static public var onAvatarAdd(default, null) = new FlxTypedSignal<PlayerSettings->Void>(); public static var onAvatarAdd(default, null) = new FlxTypedSignal<PlayerSettings->Void>();
static public var onAvatarRemove(default, null) = new FlxTypedSignal<PlayerSettings->Void>(); public static var onAvatarRemove(default, null) = new FlxTypedSignal<PlayerSettings->Void>();
public var id(default, null):Int; public var id(default, null):Int;
public var controls(default, null):Controls; public var controls(default, null):Controls;
// public var avatar:Player; /**
// public var camera(get, never):PlayCamera; * Return the PlayerSettings for the given player number, or `null` if that player isn't active.
*/
function new(id:Int) public static function get(id:Int):Null<PlayerSettings>
{ {
trace('loading player settings for id: $id'); return switch (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)
{ {
var keyData:Dynamic = null; case 1: player1;
if (id == 0 && controlData.p1 != null && controlData.p1.keys != null) keyData = controlData.p1.keys; case 2: player2;
else if (id == 1 && controlData.p2 != null && controlData.p2.keys != null) keyData = controlData.p2.keys; default: null;
};
if (keyData != null)
{
useDefault = false;
trace("loaded key data: " + haxe.Json.stringify(keyData));
controls.fromSaveData(keyData, Keys);
}
} }
if (useDefault) public static function init():Void
{
trace("falling back to default control scheme");
controls.setKeyboardScheme(Solo);
}
// Apply loaded settings.
PreciseInputManager.instance.initializeKeys(controls);
}
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
{ {
if (player1 == null) if (player1 == null)
{ {
player1 = new PlayerSettings(0); player1 = new PlayerSettings(1);
++numPlayers; ++numPlayers;
} }
@ -137,26 +53,13 @@ class PlayerSettings
var gamepad = FlxG.gamepads.getByID(i); var gamepad = FlxG.gamepads.getByID(i);
if (gamepad != null) onGamepadAdded(gamepad); if (gamepad != null) onGamepadAdded(gamepad);
} }
}
// player1.controls.addDefaultGamepad(0); public static function reset()
// } {
player1 = null;
// if (numGamepads > 1) player2 = null;
// { numPlayers = 0;
// 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();
} }
static function onGamepadAdded(gamepad:FlxGamepad) static function onGamepadAdded(gamepad:FlxGamepad)
@ -164,86 +67,85 @@ class PlayerSettings
player1.addGamepad(gamepad); player1.addGamepad(gamepad);
} }
/* /**
public function setKeyboardScheme(scheme) * @param id The player number this represents. This was refactored to START AT `1`.
{
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);
}
*/ */
static public function reset() private function new(id:Int)
{ {
player1 = null; trace('loading player settings for id: $id');
player2 = null;
numPlayers = 0; 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);
}
}
} }
} }

View file

@ -86,10 +86,10 @@ class NGUtil
#end #end
var onSessionFail:Error->Void = null; 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"); trace("using stored session id");
sessionId = FlxG.save.data.sessionId; sessionId = Save.get().ngSessionId;
onSessionFail = function(error) savedSessionFailed = true; onSessionFail = function(error) savedSessionFailed = true;
} }
#end #end
@ -159,8 +159,8 @@ class NGUtil
static function onNGLogin():Void static function onNGLogin():Void
{ {
trace('logged in! user:${NG.core.user.name}'); trace('logged in! user:${NG.core.user.name}');
FlxG.save.data.sessionId = NG.core.sessionId; Save.get().ngSessionId = NG.core.sessionId;
FlxG.save.flush(); Save.get().flush();
// Load medals then call onNGMedalFetch() // Load medals then call onNGMedalFetch()
NG.core.requestMedals(onNGMedalFetch); NG.core.requestMedals(onNGMedalFetch);
@ -174,8 +174,8 @@ class NGUtil
{ {
NG.core.logOut(); NG.core.logOut();
FlxG.save.data.sessionId = null; Save.get().ngSessionId = null;
FlxG.save.flush(); Save.get().flush();
} }
// --- MEDALS // --- MEDALS

View file

@ -14,6 +14,7 @@ import funkin.data.level.LevelRegistry;
import funkin.data.notestyle.NoteStyleRegistry; import funkin.data.notestyle.NoteStyleRegistry;
import funkin.play.cutscene.dialogue.ConversationDataParser; import funkin.play.cutscene.dialogue.ConversationDataParser;
import funkin.play.cutscene.dialogue.DialogueBoxDataParser; import funkin.play.cutscene.dialogue.DialogueBoxDataParser;
import funkin.save.Save;
import funkin.play.cutscene.dialogue.SpeakerDataParser; import funkin.play.cutscene.dialogue.SpeakerDataParser;
import funkin.data.song.SongRegistry; import funkin.data.song.SongRegistry;
@ -59,7 +60,7 @@ class PolymodHandler
createModRoot(); createModRoot();
trace("Initializing Polymod (using configured mods)..."); trace("Initializing Polymod (using configured mods)...");
loadModsById(getEnabledModIds()); loadModsById(Save.get().enabledModIds);
} }
/** /**
@ -232,33 +233,9 @@ class PolymodHandler
return modIds; return modIds;
} }
public static function setEnabledMods(newModList:Array<String>):Void
{
FlxG.save.data.enabledMods = newModList;
// Make sure to COMMIT the changes.
FlxG.save.flush();
}
/**
* Returns the list of enabled mods.
* @return Array<String>
*/
public static function getEnabledModIds():Array<String>
{
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<ModMetadata> public static function getEnabledMods():Array<ModMetadata>
{ {
var modIds = getEnabledModIds(); var modIds = Save.get().enabledModIds;
var modMetadata = getAllMods(); var modMetadata = getAllMods();
var enabledMods = []; var enabledMods = [];
for (item in modMetadata) for (item in modMetadata)

View file

@ -25,6 +25,7 @@ import flixel.ui.FlxBar;
import flixel.util.FlxColor; import flixel.util.FlxColor;
import flixel.util.FlxTimer; import flixel.util.FlxTimer;
import funkin.audio.VoicesGroup; import funkin.audio.VoicesGroup;
import funkin.save.Save;
import funkin.Highscore.Tallies; import funkin.Highscore.Tallies;
import funkin.input.PreciseInputManager; import funkin.input.PreciseInputManager;
import funkin.modding.events.ScriptEvent; import funkin.modding.events.ScriptEvent;
@ -2495,9 +2496,32 @@ class PlayState extends MusicBeatSubState
if (currentSong != null && currentSong.validScore) if (currentSong != null && currentSong.validScore)
{ {
// crackhead double thingie, sets whether was new highscore, AND saves the song! // 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) if (PlayStatePlaylist.isStoryMode)
@ -2521,11 +2545,35 @@ class PlayState extends MusicBeatSubState
if (currentSong.validScore) if (currentSong.validScore)
{ {
NGio.unlockMedal(60961); NGio.unlockMedal(60961);
Highscore.saveWeekScoreForDifficulty(PlayStatePlaylist.campaignId, PlayStatePlaylist.campaignScore, currentDifficulty);
}
// FlxG.save.data.weekUnlocked = StoryMenuState.weekUnlocked; var data =
FlxG.save.flush(); {
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) if (isSubState)
{ {

686
source/funkin/save/Save.hx Normal file
View file

@ -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<String>;
function get_ngSessionId():Null<String>
{
return this.api.newgrounds.sessionId;
}
function set_ngSessionId(value:Null<String>):Null<String>
{
return this.api.newgrounds.sessionId = value;
}
public var enabledModIds(get, set):Array<String>;
function get_enabledModIds():Array<String>
{
return this.mods.enabledMods;
}
function set_enabledModIds(value:Array<String>):Array<String>
{
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<SaveScoreData>
{
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<String>):Bool
{
if (difficultyList == null)
{
difficultyList = ['easy', 'normal', 'hard'];
}
for (difficulty in difficultyList)
{
var score:Null<SaveScoreData> = 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<SaveScoreData>
{
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<String>):Bool
{
if (difficultyList == null)
{
difficultyList = ['easy', 'normal', 'hard'];
}
for (difficulty in difficultyList)
{
var score:Null<SaveScoreData> = 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<RawSaveData_v1_0_0>
{
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<String>;
}
/**
* 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<String>;
var modSettings:Map<String, Dynamic>;
}
/**
* Key is the level ID, value is the SaveScoreLevelData.
*/
typedef SaveScoreLevelsData = Map<String, SaveScoreDifficultiesData>;
/**
* Key is the song ID, value is the data for each difficulty.
*/
typedef SaveScoreSongsData = Map<String, SaveScoreDifficultiesData>;
/**
* Key is the difficulty ID, value is the score.
*/
typedef SaveScoreDifficultiesData = Map<String, SaveScoreData>;
/**
* 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<Int>;
/**
* Keybind for navigating in the menu.
* @default `Left Arrow`
*/
var ?UI_LEFT:Array<Int>;
/**
* Keybind for navigating in the menu.
* @default `Right Arrow`
*/
var ?UI_RIGHT:Array<Int>;
/**
* Keybind for navigating in the menu.
* @default `Down Arrow`
*/
var ?UI_DOWN:Array<Int>;
/**
* Keybind for hitting notes.
* @default `A` and `Left Arrow`
*/
var ?NOTE_LEFT:Array<Int>;
/**
* Keybind for hitting notes.
* @default `W` and `Up Arrow`
*/
var ?NOTE_UP:Array<Int>;
/**
* Keybind for hitting notes.
* @default `S` and `Down Arrow`
*/
var ?NOTE_DOWN:Array<Int>;
/**
* Keybind for hitting notes.
* @default `D` and `Right Arrow`
*/
var ?NOTE_RIGHT:Array<Int>;
/**
* Keybind for continue/OK in menus.
* @default `Enter` and `Space`
*/
var ?ACCEPT:Array<Int>;
/**
* Keybind for back/cancel in menus.
* @default `Escape`
*/
var ?BACK:Array<Int>;
/**
* Keybind for pausing the game.
* @default `Escape`
*/
var ?PAUSE:Array<Int>;
/**
* Keybind for advancing cutscenes.
* @default `Z` and `Space` and `Enter`
*/
var ?CUTSCENE_ADVANCE:Array<Int>;
/**
* Keybind for skipping a cutscene.
* @default `Escape`
*/
var ?CUTSCENE_SKIP:Array<Int>;
/**
* Keybind for increasing volume.
* @default `Plus`
*/
var ?VOLUME_UP:Array<Int>;
/**
* Keybind for decreasing volume.
* @default `Minus`
*/
var ?VOLUME_DOWN:Array<Int>;
/**
* Keybind for muting/unmuting volume.
* @default `Zero`
*/
var ?VOLUME_MUTE:Array<Int>;
/**
* Keybind for restarting a song.
* @default `R`
*/
var ?RESET:Array<Int>;
}
/**
* An anonymous structure containing all the user's options and preferences, specific to the Chart Editor.
*/
typedef SaveDataChartEditorOptions = {};

View file

@ -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<String, Float>;
var songScores:Map<String, Int>;
var ?controls:
{
?p1:SavePlayerControlsData_v1_0_0,
?p2:SavePlayerControlsData_v1_0_0
};
var enabledMods:Array<String>;
var weeksUnlocked:Array<Bool>;
var windowSettings:Array<Bool>;
}
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<Int>;
var ?BACK:Array<Int>;
var ?CUTSCENE_ADVANCE:Array<Int>;
var ?CUTSCENE_SKIP:Array<Int>;
var ?NOTE_DOWN:Array<Int>;
var ?NOTE_LEFT:Array<Int>;
var ?NOTE_RIGHT:Array<Int>;
var ?NOTE_UP:Array<Int>;
var ?PAUSE:Array<Int>;
var ?RESET:Array<Int>;
var ?UI_DOWN:Array<Int>;
var ?UI_LEFT:Array<Int>;
var ?UI_RIGHT:Array<Int>;
var ?UI_UP:Array<Int>;
var ?VOLUME_DOWN:Array<Int>;
var ?VOLUME_MUTE:Array<Int>;
var ?VOLUME_UP:Array<Int>;
};

View file

@ -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<Version> = 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<String>):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);
}
}

View file

@ -1,5 +1,7 @@
package funkin.ui.story; package funkin.ui.story;
import funkin.save.Save;
import funkin.save.Save.SaveScoreData;
import openfl.utils.Assets; import openfl.utils.Assets;
import flixel.addons.transition.FlxTransitionableState; import flixel.addons.transition.FlxTransitionableState;
import flixel.FlxSprite; import flixel.FlxSprite;
@ -623,7 +625,8 @@ class StoryMenuState extends MusicBeatState
tracklistText.screenCenter(X); tracklistText.screenCenter(X);
tracklistText.x -= FlxG.width * 0.35; tracklistText.x -= FlxG.width * 0.35;
// TODO: Fix this. var levelScore:Null<SaveScoreData> = Save.get().getLevelScore(currentLevelId, currentDifficultyId);
highScore = Highscore.getWeekScore(0, 0); highScore = levelScore?.score ?? 0;
// levelScore.accuracy
} }
} }