1
0
Fork 0
mirror of https://github.com/ninjamuffin99/Funkin.git synced 2025-03-25 03:19:24 +00:00

Merge pull request #171 from FunkinCrew/rewrite/feature/save-data-rewrite

Rewrite Save functionality
This commit is contained in:
Cameron Taylor 2023-10-15 20:22:46 -04:00 committed by GitHub
commit a89a819590
13 changed files with 1346 additions and 589 deletions

View file

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

View file

@ -1,54 +1,55 @@
package funkin;
import funkin.shaderslmfao.HSVShader;
import funkin.ui.StickerSubState;
import flash.text.TextField;
import flixel.addons.display.FlxGridOverlay;
import flixel.addons.transition.FlxTransitionableState;
import flixel.addons.ui.FlxInputText;
import flixel.FlxCamera;
import flixel.FlxGame;
import flixel.FlxSprite;
import flixel.FlxState;
import flixel.addons.display.FlxGridOverlay;
import flixel.addons.transition.FlxTransitionableState;
import flixel.addons.ui.FlxInputText;
import flixel.group.FlxGroup.FlxTypedGroup;
import flixel.group.FlxGroup;
import flixel.group.FlxGroup.FlxTypedGroup;
import flixel.group.FlxSpriteGroup;
import flixel.system.debug.watch.Tracker.TrackerProfile;
import flixel.input.touch.FlxTouch;
import flixel.math.FlxAngle;
import flixel.math.FlxMath;
import flixel.math.FlxPoint;
import flixel.system.debug.watch.Tracker.TrackerProfile;
import flixel.text.FlxText;
import flixel.tweens.FlxEase;
import flixel.tweens.FlxTween;
import flixel.util.FlxColor;
import funkin.data.song.SongRegistry;
import funkin.data.level.LevelRegistry;
import flixel.util.FlxSpriteUtil;
import flixel.util.FlxTimer;
import funkin.Controls.Control;
import funkin.data.level.LevelRegistry;
import funkin.data.song.SongRegistry;
import funkin.freeplayStuff.BGScrollingText;
import funkin.freeplayStuff.DifficultyStars;
import funkin.freeplayStuff.DJBoyfriend;
import funkin.freeplayStuff.FreeplayScore;
import funkin.freeplayStuff.LetterSort;
import funkin.freeplayStuff.SongMenuItem;
import funkin.freeplayStuff.DifficultyStars;
import funkin.graphics.adobeanimate.FlxAtlasSprite;
import funkin.play.HealthIcon;
import funkin.play.PlayState;
import funkin.shaderslmfao.AngleMask;
import funkin.shaderslmfao.PureColor;
import funkin.shaderslmfao.StrokeShader;
import funkin.play.PlayStatePlaylist;
import funkin.play.song.Song;
import funkin.save.Save;
import funkin.save.Save.SaveScoreData;
import funkin.shaderslmfao.AngleMask;
import funkin.shaderslmfao.HSVShader;
import funkin.shaderslmfao.PureColor;
import funkin.shaderslmfao.StrokeShader;
import funkin.ui.StickerSubState;
import lime.app.Future;
import lime.utils.Assets;
import funkin.graphics.adobeanimate.FlxAtlasSprite;
class FreeplayState extends MusicBeatSubState
{
var songs:Array<FreeplaySongData> = [];
// var selector:FlxText;
var curSelected:Int = 0;
var curDifficulty:Int = 1;
@ -107,8 +108,6 @@ class FreeplayState extends MusicBeatSubState
openSubState(stickerSubState);
stickerSubState.degenStickers();
// resetSubState();
}
#if discord_rpc
@ -120,31 +119,25 @@ class FreeplayState extends MusicBeatSubState
#if debug
isDebug = true;
// addSong('Test', 'tutorial', 'bf-pixel');
// addSong('Pyro', 'weekend1', 'darnell');
#end
// var initSonglist = CoolUtil.coolTextFile(Paths.txt('freeplaySonglist'));
// for (i in 0...initSonglist.length)
// {
// songs.push(new FreeplaySongData(initSonglist[i], 'tutorial', 'gf'));
// }
if (FlxG.sound.music != null)
{
if (!FlxG.sound.music.playing) FlxG.sound.playMusic(Paths.music('freakyMenu/freakyMenu'));
}
// Add a null entry that represents the RANDOM option
songs.push(null);
// programmatically adds the songs via LevelRegistry and SongRegistry
for (coolWeek in LevelRegistry.instance.listBaseGameLevelIds())
{
for (coolSong in LevelRegistry.instance.parseEntryData(coolWeek).songs)
for (songId in LevelRegistry.instance.parseEntryData(coolWeek).songs)
{
var metadata = SongRegistry.instance.parseEntryMetadata(coolSong);
var metadata = SongRegistry.instance.parseEntryMetadata(songId);
var char = metadata.playData.characters.opponent;
var songName = metadata.songName;
addSong(songName, coolWeek, char);
addSong(songId, songName, coolWeek, char);
}
}
@ -254,7 +247,6 @@ class FreeplayState extends MusicBeatSubState
speed: 0.3
});
// dj = new DJBoyfriend(0, -100);
dj = new DJBoyfriend(640, 366);
exitMovers.set([dj],
{
@ -427,7 +419,6 @@ class FreeplayState extends MusicBeatSubState
dj.onIntroDone.add(function() {
// when boyfriend hits dat shiii
//
albumArt.visible = true;
albumArt.anim.play("");
@ -485,40 +476,15 @@ class FreeplayState extends MusicBeatSubState
generateSongList(null, false);
// FlxG.sound.playMusic(Paths.music('title'), 0);
// FlxG.sound.music.fadeIn(2, 0, 0.8);
// selector = new FlxText();
// selector.size = 40;
// selector.text = ">";
// add(selector);
var swag:Alphabet = new Alphabet(1, 0, "swag");
// JUST DOIN THIS SHIT FOR TESTING!!!
/*
var md:String = Markdown.markdownToHtml(Assets.getText('CHANGELOG.md'));
var texFel:TextField = new TextField();
texFel.width = FlxG.width;
texFel.height = FlxG.height;
// texFel.
texFel.htmlText = md;
FlxG.stage.addChild(texFel);
trace(md);
*/
var funnyCam = new FlxCamera(0, 0, FlxG.width, FlxG.height);
funnyCam.bgColor = FlxColor.TRANSPARENT;
FlxG.cameras.add(funnyCam);
typing = new FlxInputText(100, 100);
// add(typing);
typing.callback = function(txt, action) {
// generateSongList(new EReg(txt.trim(), "ig"));
trace(action);
};
@ -534,9 +500,6 @@ class FreeplayState extends MusicBeatSubState
for (cap in grpCapsules.members)
cap.kill();
// grpCapsules.clear();
// var regexp:EReg = regexp;
var tempSongs:Array<FreeplaySongData> = songs;
if (filterStuff != null)
@ -583,7 +546,10 @@ class FreeplayState extends MusicBeatSubState
for (i in 0...tempSongs.length)
{
if (tempSongs[i] == null) continue;
var funnyMenu:SongMenuItem = grpCapsules.recycle(SongMenuItem);
funnyMenu.init(FlxG.width, 0, tempSongs[i].songName);
if (tempSongs[i].songCharacter != null) funnyMenu.setCharacter(tempSongs[i].songCharacter);
funnyMenu.onConfirm = function() {
@ -596,7 +562,6 @@ class FreeplayState extends MusicBeatSubState
funnyMenu.songText.visible = false;
funnyMenu.favIcon.visible = tempSongs[i].isFav;
funnyMenu.hsvShader = hsvShader;
// fp.updateScore(0);
if (i < 8) funnyMenu.initJumpIn(Math.min(i, 4), force);
else
@ -611,22 +576,9 @@ class FreeplayState extends MusicBeatSubState
changeDiff();
}
public function addSong(songName:String, levelId:String, songCharacter:String)
public function addSong(songId:String, songName:String, levelId:String, songCharacter:String)
{
songs.push(new FreeplaySongData(songName, levelId, songCharacter));
}
public function addWeek(songs:Array<String>, levelId:String, ?songCharacters:Array<String>)
{
if (songCharacters == null) songCharacters = ['bf'];
var num:Int = 0;
for (song in songs)
{
addSong(song, levelId, songCharacters[num]);
if (songCharacters.length != 1) num++;
}
songs.push(new FreeplaySongData(songId, songName, levelId, songCharacter));
}
var touchY:Float = 0;
@ -651,28 +603,31 @@ class FreeplayState extends MusicBeatSubState
if (FlxG.keys.justPressed.F)
{
var realShit = curSelected;
songs[curSelected].isFav = !songs[curSelected].isFav;
if (songs[curSelected].isFav)
if (songs[curSelected] != null)
{
FlxTween.tween(grpCapsules.members[realShit], {angle: 360}, 0.4,
{
ease: FlxEase.elasticOut,
onComplete: _ -> {
grpCapsules.members[realShit].favIcon.visible = true;
grpCapsules.members[realShit].favIcon.animation.play("fav");
}
var realShit = curSelected;
songs[curSelected].isFav = !songs[curSelected].isFav;
if (songs[curSelected].isFav)
{
FlxTween.tween(grpCapsules.members[realShit], {angle: 360}, 0.4,
{
ease: FlxEase.elasticOut,
onComplete: _ -> {
grpCapsules.members[realShit].favIcon.visible = true;
grpCapsules.members[realShit].favIcon.animation.play("fav");
}
});
}
else
{
grpCapsules.members[realShit].favIcon.animation.play('fav', false, true);
new FlxTimer().start((1 / 24) * 14, _ -> {
grpCapsules.members[realShit].favIcon.visible = false;
});
}
else
{
grpCapsules.members[realShit].favIcon.animation.play('fav', false, true);
new FlxTimer().start((1 / 24) * 14, _ -> {
grpCapsules.members[realShit].favIcon.visible = false;
});
new FlxTimer().start((1 / 24) * 24, _ -> {
FlxTween.tween(grpCapsules.members[realShit], {angle: 0}, 0.4, {ease: FlxEase.elasticOut});
});
new FlxTimer().start((1 / 24) * 24, _ -> {
FlxTween.tween(grpCapsules.members[realShit], {angle: 0}, 0.4, {ease: FlxEase.elasticOut});
});
}
}
}
@ -722,16 +677,7 @@ class FreeplayState extends MusicBeatSubState
FlxG.watch.addQuick("LENGTH", length);
FlxG.watch.addQuick("ANGLE", Math.round(FlxAngle.asDegrees(angle)));
// trace("ANGLE", Math.round(FlxAngle.asDegrees(angle)));
}
/* switch (inputID)
{
case FlxObject.UP:
return
case FlxObject.DOWN:
}
*/
}
if (FlxG.touches.getFirst() != null)
@ -767,7 +713,6 @@ class FreeplayState extends MusicBeatSubState
touchY = touch.screenY;
if (dyTouch != 0) dyTouch < 0 ? changeSelection(1) : changeSelection(-1);
// changeSelection(1);
}
}
else
@ -845,8 +790,6 @@ class FreeplayState extends MusicBeatSubState
FlxG.sound.play(Paths.sound('cancelMenu'));
// FlxTween.tween(dj, {x: -dj.width}, 0.2, {ease: FlxEase.quartOut});
var longestTimer:Float = 0;
for (grpSpr in exitMovers.keys())
@ -892,15 +835,11 @@ class FreeplayState extends MusicBeatSubState
{
FlxG.switchState(new MainMenuState());
}
//
// close();
});
}
if (accepted)
{
// if (Assets.exists())
grpCapsules.members[curSelected].onConfirm();
}
}
@ -908,7 +847,11 @@ class FreeplayState extends MusicBeatSubState
@:haxe.warning("-WDeprecated")
override function switchTo(nextState:FlxState):Bool
{
clearDaCache(songs[curSelected].songName);
var daSong = songs[curSelected];
if (daSong != null)
{
clearDaCache(daSong.songName);
}
return super.switchTo(nextState);
}
@ -921,9 +864,29 @@ class FreeplayState extends MusicBeatSubState
if (curDifficulty < 0) curDifficulty = 2;
if (curDifficulty > 2) curDifficulty = 0;
// intendedScore = Highscore.getScore(songs[curSelected].songName, curDifficulty);
intendedScore = Highscore.getScore(songs[curSelected].songName, curDifficulty);
intendedCompletion = Highscore.getCompletion(songs[curSelected].songName, curDifficulty);
var targetDifficulty:String = switch (curDifficulty)
{
case 0:
'easy';
case 1:
'normal';
case 2:
'hard';
default: 'normal';
};
var daSong = songs[curSelected];
if (daSong != null)
{
var songScore:SaveScoreData = Save.get().getSongScore(songs[curSelected].songId, targetDifficulty);
intendedScore = songScore?.score ?? 0;
intendedCompletion = songScore?.accuracy ?? 0.0;
}
else
{
intendedScore = 0;
intendedCompletion = 0.0;
}
grpDifficulties.group.forEach(function(spr) {
spr.visible = false;
@ -945,6 +908,7 @@ class FreeplayState extends MusicBeatSubState
{
for (song in songs)
{
if (song == null) continue;
if (song.songName != actualSongTho)
{
trace('trying to remove: ' + song.songName);
@ -962,18 +926,6 @@ class FreeplayState extends MusicBeatSubState
function capsuleOnConfirmDefault(cap:SongMenuItem):Void
{
// var poop:String = songs[curSelected].songName.toLowerCase();
// does not work properly, always just accidentally sets it to normal anyways!
/* if (!Assets.exists(Paths.json(songs[curSelected].songName + '/' + poop)))
{
// defaults to normal if HARD / EASY doesn't exist
// does not account if NORMAL doesn't exist!
FlxG.log.warn("CURRENT DIFFICULTY IS NOT CHARTED, DEFAULTING TO NORMAL!");
poop = Highscore.formatSong(songs[curSelected].songName.toLowerCase(), 1);
curDifficulty = 1;
}*/
busy = true;
PlayStatePlaylist.isStoryMode = false;
@ -1015,6 +967,7 @@ class FreeplayState extends MusicBeatSubState
targetSong.cacheCharts(true);
new FlxTimer().start(1, function(tmr:FlxTimer) {
Paths.setCurrentLevel(songs[curSelected].levelId);
LoadingState.loadAndSwitchState(new PlayState(
{
targetSong: targetSong,
@ -1026,8 +979,6 @@ class FreeplayState extends MusicBeatSubState
function changeSelection(change:Int = 0)
{
// fp.updateScore(12345);
// NGio.logEvent('Fresh');
FlxG.sound.play(Paths.sound('scrollMenu'), 0.4);
// FlxG.sound.playMusic(Paths.inst(songs[curSelected].songName));
@ -1037,27 +988,30 @@ class FreeplayState extends MusicBeatSubState
if (curSelected < 0) curSelected = grpCapsules.countLiving() - 1;
if (curSelected >= grpCapsules.countLiving()) curSelected = 0;
// selector.y = (70 * curSelected) + 30;
// intendedScore = Highscore.getScore(songs[curSelected].songName, curDifficulty);
if (songs[curSelected] != null)
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 daSong = songs[curSelected];
if (daSong != null)
{
var songScore:SaveScoreData = Save.get().getSongScore(daSong.songId, targetDifficulty);
intendedScore = songScore?.score ?? 0;
intendedCompletion = songScore?.accuracy ?? 0.0;
}
else
{
intendedScore = 0;
intendedCompletion = 0;
intendedCompletion = 0.0;
}
// lerpScore = 0;
#if PRELOAD_ALL
// FlxG.sound.playMusic(Paths.inst(songs[curSelected].songName), 0);
#end
for (index => capsule in grpCapsules.members)
{
index += 1;
@ -1066,7 +1020,6 @@ class FreeplayState extends MusicBeatSubState
capsule.targetPos.y = capsule.intendedY(index - curSelected);
capsule.targetPos.x = 270 + (60 * (Math.sin(index - curSelected)));
// capsule.targetPos.x = 320 + (40 * (index - curSelected));
if (index < curSelected) capsule.targetPos.y -= 100; // another 100 for good measure
}
@ -1145,14 +1098,16 @@ enum abstract FilterType(String)
class FreeplaySongData
{
public var songId:String = "";
public var songName:String = "";
public var levelId:String = "";
public var songCharacter:String = "";
public var isFav:Bool = false;
public function new(song:String, levelId:String, songCharacter:String, isFav:Bool = false)
public function new(songId:String, songName:String, levelId:String, songCharacter:String, isFav:Bool = false)
{
this.songName = song;
this.songId = songId;
this.songName = songName;
this.levelId = levelId;
this.songCharacter = songCharacter;
this.isFav = isFav;

View file

@ -2,183 +2,9 @@ package funkin;
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 function saveScore(song:String, score:Int = 0, ?diff:Int = 0):Bool
{
var formattedSong:String = formatSong(song, diff);
#if newgrounds
NGio.postScore(score, song);
#end
if (songScores.exists(formattedSong))
{
if (songScores.get(formattedSong) < score)
{
setScore(formattedSong, score);
return true;
// new highscore
}
}
else
setScore(formattedSong, score);
return false;
}
public static function saveScoreForDifficulty(song:String, score:Int = 0, diff:String = 'normal'):Bool
{
var diffInt:Int = 1;
if (diff == 'easy') diffInt = 0;
else if (diff == 'hard') diffInt = 2;
return saveScore(song, score, diffInt);
}
public static function saveCompletion(song:String, completion:Float, diff:Int = 0):Bool
{
var formattedSong:String = formatSong(song, diff);
if (songCompletion.exists(formattedSong))
{
if (songCompletion.get(formattedSong) < completion)
{
setCompletion(formattedSong, completion);
return true;
}
}
else
setCompletion(formattedSong, completion);
return false;
}
public static function saveCompletionForDifficulty(song:String, completion:Float, diff:String = 'normal'):Bool
{
var diffInt:Int = 1;
if (diff == 'easy') diffInt = 0;
else if (diff == 'hard') diffInt = 2;
return saveCompletion(song, completion, diffInt);
}
public static function saveWeekScore(week:String, score:Int = 0, diff:Int = 0):Void
{
#if newgrounds
NGio.postScore(score, 'Campaign ID $week');
#end
var formattedSong:String = formatSong(week, diff);
if (songScores.exists(formattedSong))
{
if (songScores.get(formattedSong) < score) setScore(formattedSong, score);
}
else
{
setScore(formattedSong, score);
}
}
public static function saveWeekScoreForDifficulty(week:String, score:Int = 0, diff:String = 'normal'):Void
{
var diffInt:Int = 1;
if (diff == 'easy') diffInt = 0;
else if (diff == 'hard') diffInt = 2;
saveWeekScore(week, score, diffInt);
}
static function setCompletion(formattedSong:String, completion:Float):Void
{
songCompletion.set(formattedSong, completion);
FlxG.save.data.songCompletion = songCompletion;
FlxG.save.flush();
}
/**
* YOU SHOULD FORMAT SONG WITH formatSong() BEFORE TOSSING IN SONG VARIABLE
*/
static function setScore(formattedSong:String, score:Int):Void
{
/** GeoKureli
* References to Highscore were wrapped in `#if !switch` blocks. I wasn't sure if this
* is because switch doesn't use NGio, or because switch has a different saving method.
* I moved the compiler flag here, rather than using it everywhere else.
*/
#if ! switch
// Reminder that I don't need to format this song, it should come formatted!
songScores.set(formattedSong, score);
FlxG.save.data.songScores = songScores;
FlxG.save.flush();
#end
}
public static function formatSong(song:String, diff:Int):String
{
var daSong:String = song;
if (diff == 0) daSong += '-easy';
else if (diff == 2) daSong += '-hard';
return daSong;
}
public static function getScore(song:String, diff:Int):Int
{
if (!songScores.exists(formatSong(song, diff))) setScore(formatSong(song, diff), 0);
return songScores.get(formatSong(song, diff));
}
public static function getCompletion(song, diff):Float
{
if (!songCompletion.exists(formatSong(song, diff))) setCompletion(formatSong(song, diff), 0);
return songCompletion.get(formatSong(song, diff));
}
public static function getAllScores():Void
{
trace(songScores.toString());
}
public static function getWeekScore(week:Int, diff:Int):Int
{
if (!songScores.exists(formatSong('week' + week, diff))) setScore(formatSong('week' + week, diff), 0);
return songScores.get(formatSong('week' + week, diff));
}
public static function load():Void
{
if (FlxG.save.data.songScores != null)
{
songScores = FlxG.save.data.songScores;
}
if (FlxG.save.data.songCompletion != null) songCompletion = FlxG.save.data.songCompletion;
}
}
// i only do forward metadata cuz george did!
@:forward
abstract Tallies(RawTallies)
{

View file

@ -46,7 +46,11 @@ class InitState extends FlxState
{
setupShit();
loadSaveData();
// loadSaveData(); // Moved to Main.hx
// Load player options from save data.
PreferencesMenu.initPrefs();
// Load controls from save data.
PlayerSettings.init();
startGame();
}
@ -73,10 +77,6 @@ class InitState extends FlxState
FlxG.sound.volumeDownKeys = [];
FlxG.sound.muteKeys = [];
// TODO: Make sure volume still saves/loads properly.
// if (FlxG.save.data.volume != null) FlxG.sound.volume = FlxG.save.data.volume;
// if (FlxG.save.data.mute != null) FlxG.sound.muted = FlxG.save.data.mute;
// Set the game to a lower frame rate while it is in the background.
FlxG.game.focusLostFramerate = 30;
@ -212,24 +212,6 @@ class InitState extends FlxState
ModuleHandler.callOnCreate();
}
/**
* Retrive and parse data from the user's save.
*/
function loadSaveData()
{
// Bind save data.
// TODO: Migrate save data to a better format.
FlxG.save.bind('funkin', 'ninjamuffin99');
// Load player options from save data.
PreferencesMenu.initPrefs();
// Load controls from save data.
PlayerSettings.init();
// Load highscores from save data.
Highscore.load();
// TODO: Load level/character/cosmetic unlocks from save data.
}
/**
* Start the game.
*

View file

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

View file

@ -1,5 +1,6 @@
package funkin;
import funkin.save.Save;
import funkin.Controls;
import flixel.FlxCamera;
import funkin.input.PreciseInputManager;
@ -11,121 +12,36 @@ import flixel.util.FlxSignal;
// import props.Player;
class PlayerSettings
{
static public var numPlayers(default, null) = 0;
static public var numAvatars(default, null) = 0;
static public var player1(default, null):PlayerSettings;
static public var player2(default, null):PlayerSettings;
public static var numPlayers(default, null) = 0;
public static var numAvatars(default, null) = 0;
public static var player1(default, null):PlayerSettings;
public static var player2(default, null):PlayerSettings;
static public var onAvatarAdd(default, null) = new FlxTypedSignal<PlayerSettings->Void>();
static public var onAvatarRemove(default, null) = new FlxTypedSignal<PlayerSettings->Void>();
public static var onAvatarAdd(default, null) = new FlxTypedSignal<PlayerSettings->Void>();
public static var onAvatarRemove(default, null) = new FlxTypedSignal<PlayerSettings->Void>();
public var id(default, null):Int;
public var controls(default, null):Controls;
// public var avatar:Player;
// public var camera(get, never):PlayCamera;
function new(id:Int)
/**
* Return the PlayerSettings for the given player number, or `null` if that player isn't active.
*/
public static function get(id:Int):Null<PlayerSettings>
{
trace('loading player settings for id: $id');
this.id = id;
this.controls = new Controls('player$id', None);
#if CLEAR_INPUT_SAVE
FlxG.save.data.controls = null;
FlxG.save.flush();
#end
var useDefault = true;
var controlData = FlxG.save.data.controls;
if (controlData != null)
return switch (id)
{
var keyData:Dynamic = null;
if (id == 0 && controlData.p1 != null && controlData.p1.keys != null) keyData = controlData.p1.keys;
else if (id == 1 && controlData.p2 != null && controlData.p2.keys != null) keyData = controlData.p2.keys;
if (keyData != null)
{
useDefault = false;
trace("loaded key data: " + haxe.Json.stringify(keyData));
controls.fromSaveData(keyData, Keys);
}
}
if (useDefault)
{
trace("falling back to default control scheme");
controls.setKeyboardScheme(Solo);
}
// Apply loaded settings.
PreciseInputManager.instance.initializeKeys(controls);
case 1: player1;
case 2: player2;
default: null;
};
}
function addGamepad(gamepad:FlxGamepad)
{
var useDefault = true;
var controlData = FlxG.save.data.controls;
if (controlData != null)
{
var padData:Dynamic = null;
if (id == 0 && controlData.p1 != null && controlData.p1.pad != null) padData = controlData.p1.pad;
else if (id == 1 && controlData.p2 != null && controlData.p2.pad != null) padData = controlData.p2.pad;
if (padData != null)
{
useDefault = false;
trace("loaded pad data: " + haxe.Json.stringify(padData));
controls.addGamepadWithSaveData(gamepad.id, padData);
}
}
if (useDefault) controls.addDefaultGamepad(gamepad.id);
}
public function saveControls()
{
if (FlxG.save.data.controls == null) FlxG.save.data.controls = {};
var playerData:{?keys:Dynamic, ?pad:Dynamic}
if (id == 0)
{
if (FlxG.save.data.controls.p1 == null) FlxG.save.data.controls.p1 = {};
playerData = FlxG.save.data.controls.p1;
}
else
{
if (FlxG.save.data.controls.p2 == null) FlxG.save.data.controls.p2 = {};
playerData = FlxG.save.data.controls.p2;
}
var keyData = controls.createSaveData(Keys);
if (keyData != null)
{
playerData.keys = keyData;
trace("saving key data: " + haxe.Json.stringify(keyData));
}
if (controls.gamepadsAdded.length > 0)
{
var padData = controls.createSaveData(Gamepad(controls.gamepadsAdded[0]));
if (padData != null)
{
trace("saving pad data: " + haxe.Json.stringify(padData));
playerData.pad = padData;
}
}
FlxG.save.flush();
}
static public function init():Void
public static function init():Void
{
if (player1 == null)
{
player1 = new PlayerSettings(0);
player1 = new PlayerSettings(1);
++numPlayers;
}
@ -137,26 +53,13 @@ class PlayerSettings
var gamepad = FlxG.gamepads.getByID(i);
if (gamepad != null) onGamepadAdded(gamepad);
}
}
// player1.controls.addDefaultGamepad(0);
// }
// if (numGamepads > 1)
// {
// if (player2 == null)
// {
// player2 = new PlayerSettings(1, None);
// ++numPlayers;
// }
// var gamepad = FlxG.gamepads.getByID(1);
// if (gamepad == null)
// throw 'Unexpected null gamepad. id:0';
// player2.controls.addDefaultGamepad(1);
// }
// DeviceManager.init();
public static function reset()
{
player1 = null;
player2 = null;
numPlayers = 0;
}
static function onGamepadAdded(gamepad:FlxGamepad)
@ -164,86 +67,85 @@ class PlayerSettings
player1.addGamepad(gamepad);
}
/*
public function setKeyboardScheme(scheme)
{
controls.setKeyboardScheme(scheme);
}
static public function addAvatar(avatar:Player):PlayerSettings
{
var settings:PlayerSettings;
if (player1 == null)
{
player1 = new PlayerSettings(0, Solo);
++numPlayers;
}
if (player1.avatar == null)
settings = player1;
else
{
if (player2 == null)
{
if (player1.controls.keyboardScheme.match(Duo(true)))
player2 = new PlayerSettings(1, Duo(false));
else
player2 = new PlayerSettings(1, None);
++numPlayers;
}
if (player2.avatar == null)
settings = player2;
else
throw throw 'Invalid number of players: ${numPlayers + 1}';
}
++numAvatars;
settings.avatar = avatar;
avatar.settings = settings;
splitCameras();
onAvatarAdd.dispatch(settings);
return settings;
}
static public function removeAvatar(avatar:Player):Void
{
var settings:PlayerSettings;
if (player1 != null && player1.avatar == avatar)
settings = player1;
else if (player2 != null && player2.avatar == avatar)
{
settings = player2;
if (player1.controls.keyboardScheme.match(Duo(_)))
player1.setKeyboardScheme(Solo);
}
else
throw "Cannot remove avatar that is not for a player";
settings.avatar = null;
while (settings.controls.gamepadsAdded.length > 0)
{
final id = settings.controls.gamepadsAdded.shift();
settings.controls.removeGamepad(id);
DeviceManager.releaseGamepad(FlxG.gamepads.getByID(id));
}
--numAvatars;
splitCameras();
onAvatarRemove.dispatch(avatar.settings);
}
/**
* @param id The player number this represents. This was refactored to START AT `1`.
*/
static public function reset()
private function new(id:Int)
{
player1 = null;
player2 = null;
numPlayers = 0;
trace('loading player settings for id: $id');
this.id = id;
this.controls = new Controls('player$id', None);
var useDefault = true;
if (Save.get().hasControls(id, Keys))
{
var keyControlData = Save.get().getControls(id, Keys);
trace("keyControlData: " + haxe.Json.stringify(keyControlData));
useDefault = false;
controls.fromSaveData(keyControlData, Keys);
}
else
{
useDefault = true;
}
if (useDefault)
{
trace("Loading default keyboard control scheme");
controls.setKeyboardScheme(Solo);
}
// Apply loaded settings.
PreciseInputManager.instance.initializeKeys(controls);
}
/**
* Called after an FlxGamepad has been detected.
* @param gamepad The gamepad that was detected.
*/
function addGamepad(gamepad:FlxGamepad)
{
var useDefault = true;
if (Save.get().hasControls(id, Gamepad(gamepad.id)))
{
var padControlData = Save.get().getControls(id, Gamepad(gamepad.id));
trace("padControlData: " + haxe.Json.stringify(padControlData));
useDefault = false;
controls.addGamepadWithSaveData(gamepad.id, padControlData);
}
else
{
useDefault = true;
}
if (useDefault)
{
trace("Loading gamepad control scheme");
controls.addDefaultGamepad(gamepad.id);
}
}
/**
* Save this player's controls to the game's persistent save.
*/
public function saveControls()
{
var keyData = controls.createSaveData(Keys);
if (keyData != null)
{
trace("saving key data: " + haxe.Json.stringify(keyData));
Save.get().setControls(id, Keys, keyData);
}
if (controls.gamepadsAdded.length > 0)
{
var padData = controls.createSaveData(Gamepad(controls.gamepadsAdded[0]));
if (padData != null)
{
trace("saving pad data: " + haxe.Json.stringify(padData));
Save.get().setControls(id, Gamepad(controls.gamepadsAdded[0]), padData);
}
}
}
}

View file

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

View file

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

View file

@ -25,6 +25,7 @@ import flixel.ui.FlxBar;
import flixel.util.FlxColor;
import flixel.util.FlxTimer;
import funkin.audio.VoicesGroup;
import funkin.save.Save;
import funkin.Highscore.Tallies;
import funkin.input.PreciseInputManager;
import funkin.modding.events.ScriptEvent;
@ -2393,9 +2394,32 @@ class PlayState extends MusicBeatSubState
if (currentSong != null && currentSong.validScore)
{
// crackhead double thingie, sets whether was new highscore, AND saves the song!
Highscore.tallies.isNewHighscore = Highscore.saveScoreForDifficulty(currentSong.id, songScore, currentDifficulty);
var data =
{
score: songScore,
tallies:
{
killer: Highscore.tallies.killer,
sick: Highscore.tallies.sick,
good: Highscore.tallies.good,
bad: Highscore.tallies.bad,
shit: Highscore.tallies.shit,
missed: Highscore.tallies.missed,
combo: Highscore.tallies.combo,
maxCombo: Highscore.tallies.maxCombo,
totalNotesHit: Highscore.tallies.totalNotesHit,
totalNotes: Highscore.tallies.totalNotes,
},
accuracy: Highscore.tallies.totalNotesHit / Highscore.tallies.totalNotes,
};
Highscore.saveCompletionForDifficulty(currentSong.id, Highscore.tallies.totalNotesHit / Highscore.tallies.totalNotes, currentDifficulty);
if (Save.get().isSongHighScore(currentSong.id, currentDifficulty, data))
{
Save.get().setSongScore(currentSong.id, currentDifficulty, data);
#if newgrounds
NGio.postScore(score, currentSong.id);
#end
}
}
if (PlayStatePlaylist.isStoryMode)
@ -2419,11 +2443,35 @@ class PlayState extends MusicBeatSubState
if (currentSong.validScore)
{
NGio.unlockMedal(60961);
Highscore.saveWeekScoreForDifficulty(PlayStatePlaylist.campaignId, PlayStatePlaylist.campaignScore, currentDifficulty);
}
// FlxG.save.data.weekUnlocked = StoryMenuState.weekUnlocked;
FlxG.save.flush();
var data =
{
score: PlayStatePlaylist.campaignScore,
tallies:
{
// TODO: Sum up the values for the whole level!
killer: 0,
sick: 0,
good: 0,
bad: 0,
shit: 0,
missed: 0,
combo: 0,
maxCombo: 0,
totalNotesHit: 0,
totalNotes: 0,
},
accuracy: Highscore.tallies.totalNotesHit / Highscore.tallies.totalNotes,
};
if (Save.get().isLevelHighScore(PlayStatePlaylist.campaignId, currentDifficulty, data))
{
Save.get().setLevelScore(PlayStatePlaylist.campaignId, currentDifficulty, data);
#if newgrounds
NGio.postScore(score, 'Level ${PlayStatePlaylist.campaignId}');
#end
}
}
if (isSubState)
{

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