mirror of
https://github.com/FunkinCrew/Funkin.git
synced 2024-11-27 01:55:52 -05:00
Rewrite save data functionality (now with type safety and migration)
This commit is contained in:
parent
ed6bc553ed
commit
490b2f18d0
13 changed files with 1274 additions and 458 deletions
|
@ -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
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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)
|
||||||
{
|
{
|
||||||
|
|
|
@ -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.
|
||||||
*
|
*
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
|
||||||
{
|
|
||||||
trace("falling back to default control scheme");
|
|
||||||
controls.setKeyboardScheme(Solo);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply loaded settings.
|
|
||||||
PreciseInputManager.instance.initializeKeys(controls);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function addGamepad(gamepad:FlxGamepad)
|
public static function init():Void
|
||||||
{
|
|
||||||
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
686
source/funkin/save/Save.hx
Normal 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 = {};
|
52
source/funkin/save/migrator/RawSaveData_v1_0_0.hx
Normal file
52
source/funkin/save/migrator/RawSaveData_v1_0_0.hx
Normal 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>;
|
||||||
|
};
|
322
source/funkin/save/migrator/SaveDataMigrator.hx
Normal file
322
source/funkin/save/migrator/SaveDataMigrator.hx
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue