diff --git a/.github/actions/setup-haxeshit/action.yml b/.github/actions/setup-haxeshit/action.yml
index 0cc544cf7..dcf5fd0a7 100644
--- a/.github/actions/setup-haxeshit/action.yml
+++ b/.github/actions/setup-haxeshit/action.yml
@@ -23,8 +23,6 @@ runs:
with:
path: .haxelib
key: ${{ runner.os }}-hmm-${{ hashFiles('**/hmm.json') }}
- restore-keys: |
- ${{ runner.os }}-hmm-
- if: ${{ steps.cache-hmm.outputs.cache-hit != 'true' }}
name: hmm install
run: |
diff --git a/.github/workflows/build-shit.yml b/.github/workflows/build-shit.yml
index 809a8b94b..ed10cbdc2 100644
--- a/.github/workflows/build-shit.yml
+++ b/.github/workflows/build-shit.yml
@@ -53,9 +53,8 @@ jobs:
token: ${{ secrets.GH_RO_PAT }}
- uses: ./.github/actions/setup-haxeshit
- name: Make HXCPP cache dir
- shell: bash
run: |
- mkdir -p ${{ runner.temp }}\\hxcpp_cache
+ mkdir -p ${{ runner.temp }}\hxcpp_cache
- name: Restore build cache
id: cache-build-win
uses: actions/cache@v3
@@ -63,10 +62,8 @@ jobs:
path: |
.haxelib
export
- ${{ runner.temp }}\\hxcpp_cache
- key: ${{ runner.os }}-build-win-${{ github.ref_name }}
- restore-keys: |
- ${{ runner.os }}-build-win-
+ ${{ runner.temp }}\hxcpp_cache
+ key: ${{ runner.os }}-build-win-${{ github.ref_name }}-${{ hashFiles('**/hmm.json') }}
- name: Build game
run: |
haxelib run lime build windows -release -DNO_REDIRECT_ASSETS_FOLDER
diff --git a/Project.xml b/Project.xml
index ccf6c83a3..69400d8b1 100644
--- a/Project.xml
+++ b/Project.xml
@@ -156,7 +156,6 @@
-
@@ -196,6 +195,22 @@
+
+
+
+
+
-->
-->
diff --git a/assets b/assets
index 05973b6bb..a66eb8353 160000
--- a/assets
+++ b/assets
@@ -1 +1 @@
-Subproject commit 05973b6bb816464b5cb46631285f17477d05cf08
+Subproject commit a66eb835318b7b4cf639d5add1de98a1a155d209
diff --git a/hmm.json b/hmm.json
index 3f420ac48..070d96cd0 100644
--- a/hmm.json
+++ b/hmm.json
@@ -104,7 +104,7 @@
"name": "lime",
"type": "git",
"dir": null,
- "ref": "f195121ebec688b417e38ab115185c8d93c349d3",
+ "ref": "737b86f121cdc90358d59e2e527934f267c94a2c",
"url": "https://github.com/EliteMasterEric/lime"
},
{
@@ -139,7 +139,7 @@
"name": "openfl",
"type": "git",
"dir": null,
- "ref": "de9395d2f367a80f93f082e1b639b9cde2258bf1",
+ "ref": "f229d76361c7e31025a048fe7909847f75bb5d5e",
"url": "https://github.com/EliteMasterEric/openfl"
},
{
diff --git a/source/Main.hx b/source/Main.hx
index 72209cd30..dffe666b7 100644
--- a/source/Main.hx
+++ b/source/Main.hx
@@ -4,6 +4,7 @@ import flixel.FlxGame;
import flixel.FlxState;
import funkin.util.logging.CrashHandler;
import funkin.MemoryCounter;
+import funkin.save.Save;
import haxe.ui.Toolkit;
import openfl.display.FPS;
import openfl.display.Sprite;
@@ -84,20 +85,21 @@ class Main extends Sprite
initHaxeUI();
+ fpsCounter = new FPS(10, 3, 0xFFFFFF);
+ // addChild(fpsCounter); // Handled by Preferences.init
+ #if !html5
+ memoryCounter = new MemoryCounter(10, 13, 0xFFFFFF);
+ // addChild(memoryCounter);
+ #end
+
+ // 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
trace('hxcpp_debug_server is enabled! You can now connect to the game with a debugger.');
#end
-
- #if debug
- fpsCounter = new FPS(10, 3, 0xFFFFFF);
- addChild(fpsCounter);
- #if !html5
- memoryCounter = new MemoryCounter(10, 13, 0xFFFFFF);
- addChild(memoryCounter);
- #end
- #end
}
function initHaxeUI():Void
diff --git a/source/funkin/Controls.hx b/source/funkin/Controls.hx
index 81055fb34..9372c4dc6 100644
--- a/source/funkin/Controls.hx
+++ b/source/funkin/Controls.hx
@@ -1,5 +1,7 @@
+
package funkin;
+import flixel.input.gamepad.FlxGamepad;
import flixel.util.FlxDirectionFlags;
import flixel.FlxObject;
import flixel.input.FlxInput;
@@ -832,6 +834,14 @@ class Controls extends FlxActionSet
fromSaveData(padData, Gamepad(id));
}
+ public function getGamepadIds():Array {
+ return gamepadsAdded;
+ }
+
+ public function getGamepads():Array {
+ return [for (id in gamepadsAdded) FlxG.gamepads.getByID(id)];
+ }
+
inline function addGamepadLiteral(id:Int, ?buttonMap:Map>):Void
{
gamepadsAdded.push(id);
diff --git a/source/funkin/FreeplayState.hx b/source/funkin/FreeplayState.hx
index 3ae32c2e4..4e7674e93 100644
--- a/source/funkin/FreeplayState.hx
+++ b/source/funkin/FreeplayState.hx
@@ -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 = [];
- // 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 = songs;
if (filterStuff != null)
@@ -570,7 +533,7 @@ class FreeplayState extends MusicBeatSubState
var randomCapsule:SongMenuItem = grpCapsules.recycle(SongMenuItem);
randomCapsule.init(FlxG.width, 0, "Random");
randomCapsule.onConfirm = function() {
- trace("RANDOM SELECTED");
+ capsuleOnConfirmRandom(randomCapsule);
};
randomCapsule.y = randomCapsule.intendedY(0) + 10;
randomCapsule.targetPos.x = randomCapsule.x;
@@ -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, levelId:String, ?songCharacters:Array)
- {
- 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;
@@ -643,34 +595,39 @@ class FreeplayState extends MusicBeatSubState
var spamTimer:Float = 0;
var spamming:Bool = false;
+ var busy:Bool = false; // Set to true once the user has pressed enter to select a song.
+
override function update(elapsed:Float)
{
super.update(elapsed);
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});
+ });
+ }
}
}
@@ -690,11 +647,13 @@ class FreeplayState extends MusicBeatSubState
fp.updateScore(Std.int(lerpScore));
txtCompletion.text = Math.floor(lerpCompletion * 100) + "%";
- // trace(Highscore.getCompletion(songs[curSelected].songName, curDifficulty));
- // trace(intendedScore);
- // trace(lerpScore);
- // Highscore.getAllScores();
+ handleInputs(elapsed);
+ }
+
+ function handleInputs(elapsed:Float):Void
+ {
+ if (busy) return;
var upP = controls.UI_UP_P;
var downP = controls.UI_DOWN_P;
@@ -718,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)
@@ -763,7 +713,6 @@ class FreeplayState extends MusicBeatSubState
touchY = touch.screenY;
if (dyTouch != 0) dyTouch < 0 ? changeSelection(1) : changeSelection(-1);
- // changeSelection(1);
}
}
else
@@ -841,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())
@@ -888,15 +835,11 @@ class FreeplayState extends MusicBeatSubState
{
FlxG.switchState(new MainMenuState());
}
- //
- // close();
});
}
if (accepted)
{
- // if (Assets.exists())
-
grpCapsules.members[curSelected].onConfirm();
}
}
@@ -904,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);
}
@@ -917,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;
@@ -941,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);
@@ -949,19 +917,16 @@ class FreeplayState extends MusicBeatSubState
}
}
+ function capsuleOnConfirmRandom(cap:SongMenuItem):Void
+ {
+ trace("RANDOM SELECTED");
+
+ busy = true;
+ }
+
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;
@@ -1002,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,
@@ -1013,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));
@@ -1024,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;
@@ -1053,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
}
@@ -1132,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;
diff --git a/source/funkin/Highscore.hx b/source/funkin/Highscore.hx
index 46e98d8dc..3c9fd82e4 100644
--- a/source/funkin/Highscore.hx
+++ b/source/funkin/Highscore.hx
@@ -2,183 +2,9 @@ package funkin;
class Highscore
{
- #if (haxe >= "4.0.0")
- public static var songScores:Map = new Map();
- #else
- public static var songScores:Map = new Map();
- #end
-
- #if (haxe >= "4.0.0")
- public static var songCompletion:Map = new Map();
- #else
- public static var songCompletion:Map = new Map();
- #end
-
public static var tallies:Tallies = new Tallies();
-
- public static function saveScore(song:String, score:Int = 0, ?diff:Int = 0):Bool
- {
- var formattedSong:String = formatSong(song, diff);
-
- #if newgrounds
- NGio.postScore(score, song);
- #end
-
- if (songScores.exists(formattedSong))
- {
- if (songScores.get(formattedSong) < score)
- {
- setScore(formattedSong, score);
- return true;
- // new highscore
- }
- }
- else
- setScore(formattedSong, score);
-
- return false;
- }
-
- public static function saveScoreForDifficulty(song:String, score:Int = 0, diff:String = 'normal'):Bool
- {
- var diffInt:Int = 1;
-
- if (diff == 'easy') diffInt = 0;
- else if (diff == 'hard') diffInt = 2;
-
- return saveScore(song, score, diffInt);
- }
-
- public static function saveCompletion(song:String, completion:Float, diff:Int = 0):Bool
- {
- var formattedSong:String = formatSong(song, diff);
-
- if (songCompletion.exists(formattedSong))
- {
- if (songCompletion.get(formattedSong) < completion)
- {
- setCompletion(formattedSong, completion);
- return true;
- }
- }
- else
- setCompletion(formattedSong, completion);
-
- return false;
- }
-
- public static function saveCompletionForDifficulty(song:String, completion:Float, diff:String = 'normal'):Bool
- {
- var diffInt:Int = 1;
-
- if (diff == 'easy') diffInt = 0;
- else if (diff == 'hard') diffInt = 2;
-
- return saveCompletion(song, completion, diffInt);
- }
-
- public static function saveWeekScore(week:String, score:Int = 0, diff:Int = 0):Void
- {
- #if newgrounds
- NGio.postScore(score, 'Campaign ID $week');
- #end
-
- var formattedSong:String = formatSong(week, diff);
-
- if (songScores.exists(formattedSong))
- {
- if (songScores.get(formattedSong) < score) setScore(formattedSong, score);
- }
- else
- {
- setScore(formattedSong, score);
- }
- }
-
- public static function saveWeekScoreForDifficulty(week:String, score:Int = 0, diff:String = 'normal'):Void
- {
- var diffInt:Int = 1;
-
- if (diff == 'easy') diffInt = 0;
- else if (diff == 'hard') diffInt = 2;
-
- saveWeekScore(week, score, diffInt);
- }
-
- static function setCompletion(formattedSong:String, completion:Float):Void
- {
- songCompletion.set(formattedSong, completion);
- FlxG.save.data.songCompletion = songCompletion;
- FlxG.save.flush();
- }
-
- /**
- * YOU SHOULD FORMAT SONG WITH formatSong() BEFORE TOSSING IN SONG VARIABLE
- */
- static function setScore(formattedSong:String, score:Int):Void
- {
- /** GeoKureli
- * References to Highscore were wrapped in `#if !switch` blocks. I wasn't sure if this
- * is because switch doesn't use NGio, or because switch has a different saving method.
- * I moved the compiler flag here, rather than using it everywhere else.
- */
- #if ! switch
- // Reminder that I don't need to format this song, it should come formatted!
- songScores.set(formattedSong, score);
- FlxG.save.data.songScores = songScores;
- FlxG.save.flush();
- #end
- }
-
- public static function formatSong(song:String, diff:Int):String
- {
- var daSong:String = song;
-
- if (diff == 0) daSong += '-easy';
- else if (diff == 2) daSong += '-hard';
-
- return daSong;
- }
-
- public static function getScore(song:String, diff:Int):Int
- {
- if (!songScores.exists(formatSong(song, diff))) setScore(formatSong(song, diff), 0);
-
- return songScores.get(formatSong(song, diff));
- }
-
- public static function getCompletion(song, diff):Float
- {
- if (!songCompletion.exists(formatSong(song, diff))) setCompletion(formatSong(song, diff), 0);
-
- return songCompletion.get(formatSong(song, diff));
- }
-
- public static function getAllScores():Void
- {
- trace(songScores.toString());
- }
-
- public static function getWeekScore(week:Int, diff:Int):Int
- {
- if (!songScores.exists(formatSong('week' + week, diff))) setScore(formatSong('week' + week, diff), 0);
-
- return songScores.get(formatSong('week' + week, diff));
- }
-
- public static function load():Void
- {
- if (FlxG.save.data.songScores != null)
- {
- songScores = FlxG.save.data.songScores;
- }
-
- if (FlxG.save.data.songCompletion != null) songCompletion = FlxG.save.data.songCompletion;
- }
}
-// i only do forward metadata cuz george did!
-
@:forward
abstract Tallies(RawTallies)
{
diff --git a/source/funkin/InitState.hx b/source/funkin/InitState.hx
index e7060abd7..ecfa32eb3 100644
--- a/source/funkin/InitState.hx
+++ b/source/funkin/InitState.hx
@@ -46,7 +46,11 @@ class InitState extends FlxState
{
setupShit();
- loadSaveData();
+ // loadSaveData(); // Moved to Main.hx
+ // Load player options from save data.
+ Preferences.init();
+ // Load controls from save data.
+ PlayerSettings.init();
startGame();
}
@@ -73,10 +77,6 @@ class InitState extends FlxState
FlxG.sound.volumeDownKeys = [];
FlxG.sound.muteKeys = [];
- // TODO: Make sure volume still saves/loads properly.
- // if (FlxG.save.data.volume != null) FlxG.sound.volume = FlxG.save.data.volume;
- // if (FlxG.save.data.mute != null) FlxG.sound.muted = FlxG.save.data.mute;
-
// Set the game to a lower frame rate while it is in the background.
FlxG.game.focusLostFramerate = 30;
@@ -212,24 +212,6 @@ class InitState extends FlxState
ModuleHandler.callOnCreate();
}
- /**
- * Retrive and parse data from the user's save.
- */
- function loadSaveData()
- {
- // Bind save data.
- // TODO: Migrate save data to a better format.
- FlxG.save.bind('funkin', 'ninjamuffin99');
-
- // Load player options from save data.
- PreferencesMenu.initPrefs();
- // Load controls from save data.
- PlayerSettings.init();
- // Load highscores from save data.
- Highscore.load();
- // TODO: Load level/character/cosmetic unlocks from save data.
- }
-
/**
* Start the game.
*
diff --git a/source/funkin/MainMenuState.hx b/source/funkin/MainMenuState.hx
index 7c54357bb..7267a6da8 100644
--- a/source/funkin/MainMenuState.hx
+++ b/source/funkin/MainMenuState.hx
@@ -13,23 +13,13 @@ import flixel.input.touch.FlxTouch;
import flixel.text.FlxText;
import flixel.tweens.FlxEase;
import flixel.tweens.FlxTween;
-import flixel.util.FlxColor;
import flixel.util.FlxTimer;
-import funkin.NGio;
-import funkin.modding.events.ScriptEvent.UpdateScriptEvent;
-import funkin.modding.module.ModuleHandler;
-import funkin.shaderslmfao.ScreenWipeShader;
import funkin.ui.AtlasMenuList;
-import funkin.ui.MenuList.MenuItem;
import funkin.ui.MenuList;
import funkin.ui.title.TitleState;
import funkin.ui.story.StoryMenuState;
-import funkin.ui.OptionsState;
-import funkin.ui.PreferencesMenu;
import funkin.ui.Prompt;
import funkin.util.WindowUtil;
-import lime.app.Application;
-import openfl.filters.ShaderFilter;
#if discord_rpc
import Discord.DiscordClient;
#end
@@ -82,8 +72,10 @@ class MainMenuState extends MusicBeatState
magenta.y = bg.y;
magenta.visible = false;
magenta.color = 0xFFfd719b;
- if (PreferencesMenu.preferences.get('flashing-menu')) add(magenta);
- // magenta.scrollFactor.set();
+
+ // TODO: Why doesn't this line compile I'm going fucking feral
+
+ if (Preferences.flashingLights) add(magenta);
menuItems = new MenuTypedList();
add(menuItems);
@@ -116,7 +108,7 @@ class MainMenuState extends MusicBeatState
#end
createMenuItem('options', 'mainmenu/options', function() {
- startExitState(new OptionsState());
+ startExitState(new funkin.ui.OptionsState());
});
// Reset position of menu items.
diff --git a/source/funkin/NGio.hx b/source/funkin/NGio.hx
index f2afe84db..e5f60c8b5 100644
--- a/source/funkin/NGio.hx
+++ b/source/funkin/NGio.hx
@@ -86,10 +86,10 @@ class NGio
#end
var onSessionFail:Error->Void = null;
- if (sessionId == null && FlxG.save.data.sessionId != null)
+ if (sessionId == null && Save.get().ngSessionId != null)
{
trace("using stored session id");
- sessionId = FlxG.save.data.sessionId;
+ sessionId = Save.get().ngSessionId;
onSessionFail = function(error) savedSessionFailed = true;
}
#end
@@ -159,8 +159,8 @@ class NGio
static function onNGLogin():Void
{
trace('logged in! user:${NG.core.user.name}');
- FlxG.save.data.sessionId = NG.core.sessionId;
- FlxG.save.flush();
+ Save.get().ngSessionId = NG.core.sessionId;
+ Save.get().flush();
// Load medals then call onNGMedalFetch()
NG.core.requestMedals(onNGMedalFetch);
@@ -174,8 +174,8 @@ class NGio
{
NG.core.logOut();
- FlxG.save.data.sessionId = null;
- FlxG.save.flush();
+ Save.get().ngSessionId = null;
+ Save.get().flush();
}
// --- MEDALS
diff --git a/source/funkin/PauseSubState.hx b/source/funkin/PauseSubState.hx
index f93e5a450..a074410ea 100644
--- a/source/funkin/PauseSubState.hx
+++ b/source/funkin/PauseSubState.hx
@@ -16,17 +16,18 @@ class PauseSubState extends MusicBeatSubState
{
var grpMenuShit:FlxTypedGroup;
- var pauseOptionsBase:Array = [
+ final pauseOptionsBase:Array = [
'Resume',
'Restart Song',
'Change Difficulty',
'Toggle Practice Mode',
'Exit to Menu'
];
+ final pauseOptionsCharting:Array = ['Resume', 'Restart Song', 'Exit to Chart Editor'];
- var pauseOptionsDifficulty:Array = ['EASY', 'NORMAL', 'HARD', 'ERECT', 'BACK'];
+ final pauseOptionsDifficultyBase:Array = ['BACK'];
- var pauseOptionsCharting:Array = ['Resume', 'Restart Song', 'Exit to Chart Editor'];
+ var pauseOptionsDifficulty:Array = []; // AUTO-POPULATED
var menuItems:Array = [];
var curSelected:Int = 0;
@@ -48,6 +49,12 @@ class PauseSubState extends MusicBeatSubState
this.isChartingMode = isChartingMode;
menuItems = this.isChartingMode ? pauseOptionsCharting : pauseOptionsBase;
+ var difficultiesInVariation = PlayState.instance.currentSong.listDifficulties(PlayState.instance.currentChart.variation);
+ trace('DIFFICULTIES: ${difficultiesInVariation}');
+
+ pauseOptionsDifficulty = difficultiesInVariation.map(function(item:String):String {
+ return item.toUpperCase();
+ }).concat(pauseOptionsDifficultyBase);
if (PlayStatePlaylist.campaignId == 'week6')
{
@@ -150,6 +157,11 @@ class PauseSubState extends MusicBeatSubState
super.update(elapsed);
+ handleInputs();
+ }
+
+ function handleInputs():Void
+ {
var upP = controls.UI_UP_P;
var downP = controls.UI_DOWN_P;
var accepted = controls.ACCEPT;
@@ -196,18 +208,6 @@ class PauseSubState extends MusicBeatSubState
menuItems = pauseOptionsDifficulty;
regenMenu();
- case 'EASY' | 'NORMAL' | 'HARD' | 'ERECT':
- PlayState.instance.currentSong = SongRegistry.instance.fetchEntry(PlayState.instance.currentSong.id.toLowerCase());
-
- PlayState.instance.currentDifficulty = daSelected.toLowerCase();
-
- PlayState.instance.needsReset = true;
-
- close();
- case 'BACK':
- menuItems = this.isChartingMode ? pauseOptionsCharting : pauseOptionsBase;
- regenMenu();
-
case 'Toggle Practice Mode':
PlayState.instance.isPracticeMode = true;
practiceText.visible = PlayState.instance.isPracticeMode;
@@ -229,14 +229,43 @@ class PauseSubState extends MusicBeatSubState
FlxTransitionableState.skipNextTransIn = true;
FlxTransitionableState.skipNextTransOut = true;
- if (PlayStatePlaylist.isStoryMode) openSubState(new funkin.ui.StickerSubState(null, STORY));
+ if (PlayStatePlaylist.isStoryMode)
+ {
+ openSubState(new funkin.ui.StickerSubState(null, STORY));
+ }
else
+ {
openSubState(new funkin.ui.StickerSubState(null, FREEPLAY));
+ }
case 'Exit to Chart Editor':
this.close();
if (FlxG.sound.music != null) FlxG.sound.music.stop();
PlayState.instance.close(); // This only works because PlayState is a substate!
+
+ case 'BACK':
+ menuItems = this.isChartingMode ? pauseOptionsCharting : pauseOptionsBase;
+ regenMenu();
+
+ default:
+ if (pauseOptionsDifficulty.contains(daSelected))
+ {
+ PlayState.instance.currentSong = SongRegistry.instance.fetchEntry(PlayState.instance.currentSong.id.toLowerCase());
+
+ // Reset campaign score when changing difficulty
+ // So if you switch difficulty on the last song of a week you get a really low overall score.
+ PlayStatePlaylist.campaignScore = 0;
+ PlayStatePlaylist.campaignDifficulty = daSelected.toLowerCase();
+ PlayState.instance.currentDifficulty = PlayStatePlaylist.campaignDifficulty;
+
+ PlayState.instance.needsReset = true;
+
+ close();
+ }
+ else
+ {
+ trace('[WARN] Unhandled pause menu option: ${daSelected}');
+ }
}
}
diff --git a/source/funkin/PlayerSettings.hx b/source/funkin/PlayerSettings.hx
index 54fd559fb..e97cfe384 100644
--- a/source/funkin/PlayerSettings.hx
+++ b/source/funkin/PlayerSettings.hx
@@ -1,5 +1,6 @@
package funkin;
+import funkin.save.Save;
import funkin.Controls;
import flixel.FlxCamera;
import funkin.input.PreciseInputManager;
@@ -11,121 +12,36 @@ import flixel.util.FlxSignal;
// import props.Player;
class PlayerSettings
{
- static public var numPlayers(default, null) = 0;
- static public var numAvatars(default, null) = 0;
- static public var player1(default, null):PlayerSettings;
- static public var player2(default, null):PlayerSettings;
+ public static var numPlayers(default, null) = 0;
+ public static var numAvatars(default, null) = 0;
+ public static var player1(default, null):PlayerSettings;
+ public static var player2(default, null):PlayerSettings;
- static public var onAvatarAdd(default, null) = new FlxTypedSignalVoid>();
- static public var onAvatarRemove(default, null) = new FlxTypedSignalVoid>();
+ public static var onAvatarAdd(default, null) = new FlxTypedSignalVoid>();
+ public static var onAvatarRemove(default, null) = new FlxTypedSignalVoid>();
public var id(default, null):Int;
public var controls(default, null):Controls;
- // public var avatar:Player;
- // public var camera(get, never):PlayCamera;
-
- function new(id:Int)
+ /**
+ * Return the PlayerSettings for the given player number, or `null` if that player isn't active.
+ */
+ public static function get(id:Int):Null
{
- trace('loading player settings for id: $id');
-
- this.id = id;
- this.controls = new Controls('player$id', None);
-
- #if CLEAR_INPUT_SAVE
- FlxG.save.data.controls = null;
- FlxG.save.flush();
- #end
-
- var useDefault = true;
- var controlData = FlxG.save.data.controls;
- if (controlData != null)
+ return switch (id)
{
- var keyData:Dynamic = null;
- if (id == 0 && controlData.p1 != null && controlData.p1.keys != null) keyData = controlData.p1.keys;
- else if (id == 1 && controlData.p2 != null && controlData.p2.keys != null) keyData = controlData.p2.keys;
-
- if (keyData != null)
- {
- useDefault = false;
- trace("loaded key data: " + haxe.Json.stringify(keyData));
- controls.fromSaveData(keyData, Keys);
- }
- }
-
- if (useDefault)
- {
- trace("falling back to default control scheme");
- controls.setKeyboardScheme(Solo);
- }
-
- // Apply loaded settings.
- PreciseInputManager.instance.initializeKeys(controls);
+ case 1: player1;
+ case 2: player2;
+ default: null;
+ };
}
- function addGamepad(gamepad:FlxGamepad)
- {
- var useDefault = true;
- var controlData = FlxG.save.data.controls;
- if (controlData != null)
- {
- var padData:Dynamic = null;
- if (id == 0 && controlData.p1 != null && controlData.p1.pad != null) padData = controlData.p1.pad;
- else if (id == 1 && controlData.p2 != null && controlData.p2.pad != null) padData = controlData.p2.pad;
-
- if (padData != null)
- {
- useDefault = false;
- trace("loaded pad data: " + haxe.Json.stringify(padData));
- controls.addGamepadWithSaveData(gamepad.id, padData);
- }
- }
-
- if (useDefault) controls.addDefaultGamepad(gamepad.id);
- }
-
- public function saveControls()
- {
- if (FlxG.save.data.controls == null) FlxG.save.data.controls = {};
-
- var playerData:{?keys:Dynamic, ?pad:Dynamic}
- if (id == 0)
- {
- if (FlxG.save.data.controls.p1 == null) FlxG.save.data.controls.p1 = {};
- playerData = FlxG.save.data.controls.p1;
- }
- else
- {
- if (FlxG.save.data.controls.p2 == null) FlxG.save.data.controls.p2 = {};
- playerData = FlxG.save.data.controls.p2;
- }
-
- var keyData = controls.createSaveData(Keys);
- if (keyData != null)
- {
- playerData.keys = keyData;
- trace("saving key data: " + haxe.Json.stringify(keyData));
- }
-
- if (controls.gamepadsAdded.length > 0)
- {
- var padData = controls.createSaveData(Gamepad(controls.gamepadsAdded[0]));
- if (padData != null)
- {
- trace("saving pad data: " + haxe.Json.stringify(padData));
- playerData.pad = padData;
- }
- }
-
- FlxG.save.flush();
- }
-
- static public function init():Void
+ public static function init():Void
{
if (player1 == null)
{
- player1 = new PlayerSettings(0);
+ player1 = new PlayerSettings(1);
++numPlayers;
}
@@ -137,26 +53,13 @@ class PlayerSettings
var gamepad = FlxG.gamepads.getByID(i);
if (gamepad != null) onGamepadAdded(gamepad);
}
+ }
- // player1.controls.addDefaultGamepad(0);
- // }
-
- // if (numGamepads > 1)
- // {
- // if (player2 == null)
- // {
- // player2 = new PlayerSettings(1, None);
- // ++numPlayers;
- // }
-
- // var gamepad = FlxG.gamepads.getByID(1);
- // if (gamepad == null)
- // throw 'Unexpected null gamepad. id:0';
-
- // player2.controls.addDefaultGamepad(1);
- // }
-
- // DeviceManager.init();
+ public static function reset()
+ {
+ player1 = null;
+ player2 = null;
+ numPlayers = 0;
}
static function onGamepadAdded(gamepad:FlxGamepad)
@@ -164,86 +67,90 @@ 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);
+
+ addKeyboard();
+ }
+
+ function addKeyboard():Void
+ {
+ 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);
+ }
+
+ 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);
+ }
+ PreciseInputManager.instance.initializeButtons(controls, gamepad);
+ }
+
+ /**
+ * Save this player's controls to the game's persistent save.
+ */
+ public function saveControls()
+ {
+ var keyData = controls.createSaveData(Keys);
+ if (keyData != null)
+ {
+ trace("saving key data: " + haxe.Json.stringify(keyData));
+ Save.get().setControls(id, Keys, keyData);
+ }
+
+ if (controls.gamepadsAdded.length > 0)
+ {
+ var padData = controls.createSaveData(Gamepad(controls.gamepadsAdded[0]));
+ if (padData != null)
+ {
+ trace("saving pad data: " + haxe.Json.stringify(padData));
+ Save.get().setControls(id, Gamepad(controls.gamepadsAdded[0]), padData);
+ }
+ }
}
}
diff --git a/source/funkin/Preferences.hx b/source/funkin/Preferences.hx
new file mode 100644
index 000000000..7e3c3c6d7
--- /dev/null
+++ b/source/funkin/Preferences.hx
@@ -0,0 +1,138 @@
+package funkin;
+
+import funkin.save.Save;
+
+/**
+ * A store of user-configurable, globally relevant values.
+ */
+class Preferences
+{
+ /**
+ * Whether some particularly fowl language is displayed.
+ * @default `true`
+ */
+ public static var naughtyness(get, set):Bool;
+
+ static function get_naughtyness():Bool
+ {
+ return Save.get().options.naughtyness;
+ }
+
+ static function set_naughtyness(value:Bool):Bool
+ {
+ return Save.get().options.naughtyness = value;
+ }
+
+ /**
+ * If enabled, the strumline is at the bottom of the screen rather than the top.
+ * @default `false`
+ */
+ public static var downscroll(get, set):Bool;
+
+ static function get_downscroll():Bool
+ {
+ return Save.get().options.downscroll;
+ }
+
+ static function set_downscroll(value:Bool):Bool
+ {
+ return Save.get().options.downscroll = value;
+ }
+
+ /**
+ * If disabled, flashing lights in the main menu and other areas will be less intense.
+ * @default `true`
+ */
+ public static var flashingLights(get, set):Bool;
+
+ static function get_flashingLights():Bool
+ {
+ return Save.get().options.flashingLights;
+ }
+
+ static function set_flashingLights(value:Bool):Bool
+ {
+ return Save.get().options.flashingLights = value;
+ }
+
+ /**
+ * If disabled, the camera bump synchronized to the beat.
+ * @default `false`
+ */
+ public static var zoomCamera(get, set):Bool;
+
+ static function get_zoomCamera():Bool
+ {
+ return Save.get().options.zoomCamera;
+ }
+
+ static function set_zoomCamera(value:Bool):Bool
+ {
+ return Save.get().options.zoomCamera = value;
+ }
+
+ /**
+ * If enabled, an FPS and memory counter will be displayed even if this is not a debug build.
+ * @default `false`
+ */
+ public static var debugDisplay(get, set):Bool;
+
+ static function get_debugDisplay():Bool
+ {
+ return Save.get().options.debugDisplay;
+ }
+
+ static function set_debugDisplay(value:Bool):Bool
+ {
+ if (value != Save.get().options.debugDisplay)
+ {
+ toggleDebugDisplay(value);
+ }
+
+ return Save.get().options.debugDisplay = value;
+ }
+
+ /**
+ * If enabled, the game will automatically pause when tabbing out.
+ * @default `true`
+ */
+ public static var autoPause(get, set):Bool;
+
+ static function get_autoPause():Bool
+ {
+ return Save.get().options.autoPause;
+ }
+
+ static function set_autoPause(value:Bool):Bool
+ {
+ if (value != Save.get().options.autoPause) FlxG.autoPause = value;
+
+ return Save.get().options.autoPause = value;
+ }
+
+ public static function init():Void
+ {
+ FlxG.autoPause = Preferences.autoPause;
+ toggleDebugDisplay(Preferences.debugDisplay);
+ }
+
+ static function toggleDebugDisplay(show:Bool):Void
+ {
+ if (show)
+ {
+ // Enable the debug display.
+ FlxG.stage.addChild(Main.fpsCounter);
+ #if !html5
+ FlxG.stage.addChild(Main.memoryCounter);
+ #end
+ }
+ else
+ {
+ // Disable the debug display.
+ FlxG.stage.removeChild(Main.fpsCounter);
+ #if !html5
+ FlxG.stage.removeChild(Main.memoryCounter);
+ #end
+ }
+ }
+}
diff --git a/source/funkin/api/newgrounds/NGUtil.hx b/source/funkin/api/newgrounds/NGUtil.hx
index 773e2f98f..ba7d5f916 100644
--- a/source/funkin/api/newgrounds/NGUtil.hx
+++ b/source/funkin/api/newgrounds/NGUtil.hx
@@ -86,10 +86,10 @@ class NGUtil
#end
var onSessionFail:Error->Void = null;
- if (sessionId == null && FlxG.save.data.sessionId != null)
+ if (sessionId == null && Save.get().ngSessionId != null)
{
trace("using stored session id");
- sessionId = FlxG.save.data.sessionId;
+ sessionId = Save.get().ngSessionId;
onSessionFail = function(error) savedSessionFailed = true;
}
#end
@@ -159,8 +159,8 @@ class NGUtil
static function onNGLogin():Void
{
trace('logged in! user:${NG.core.user.name}');
- FlxG.save.data.sessionId = NG.core.sessionId;
- FlxG.save.flush();
+ Save.get().ngSessionId = NG.core.sessionId;
+ Save.get().flush();
// Load medals then call onNGMedalFetch()
NG.core.requestMedals(onNGMedalFetch);
@@ -174,8 +174,8 @@ class NGUtil
{
NG.core.logOut();
- FlxG.save.data.sessionId = null;
- FlxG.save.flush();
+ Save.get().ngSessionId = null;
+ Save.get().flush();
}
// --- MEDALS
diff --git a/source/funkin/audiovis/ABotVis.hx b/source/funkin/audiovis/ABotVis.hx
index 2018a99b3..060bddcf7 100644
--- a/source/funkin/audiovis/ABotVis.hx
+++ b/source/funkin/audiovis/ABotVis.hx
@@ -7,7 +7,6 @@ import flixel.graphics.frames.FlxAtlasFrames;
import flixel.group.FlxSpriteGroup.FlxTypedSpriteGroup;
import flixel.math.FlxMath;
import flixel.sound.FlxSound;
-import funkin.ui.PreferencesMenu.CheckboxThingie;
using Lambda;
diff --git a/source/funkin/data/song/SongData.hx b/source/funkin/data/song/SongData.hx
index d557bd39c..9340e46c9 100644
--- a/source/funkin/data/song/SongData.hx
+++ b/source/funkin/data/song/SongData.hx
@@ -4,6 +4,7 @@ import flixel.util.typeLimit.OneOfTwo;
import funkin.data.song.SongRegistry;
import thx.semver.Version;
+@:nullSafety
class SongMetadata
{
/**
@@ -42,7 +43,7 @@ class SongMetadata
public var timeChanges:Array;
/**
- * Defaults to `default` or `''`. Populated later.
+ * Defaults to `Constants.DEFAULT_VARIATION`. Populated later.
*/
@:jignored
public var variation:String;
@@ -228,10 +229,10 @@ class SongMusicData
public var timeChanges:Array;
/**
- * Defaults to `default` or `''`. Populated later.
+ * Defaults to `Constants.DEFAULT_VARIATION`. Populated later.
*/
@:jignored
- public var variation:String = Constants.DEFAULT_VARIATION;
+ public var variation:String;
public function new(songName:String, artist:String, variation:String = 'default')
{
@@ -375,6 +376,9 @@ class SongChartData
@:default(funkin.data.song.SongRegistry.DEFAULT_GENERATEDBY)
public var generatedBy:String;
+ /**
+ * Defaults to `Constants.DEFAULT_VARIATION`. Populated later.
+ */
@:jignored
public var variation:String;
diff --git a/source/funkin/data/song/SongDataUtils.hx b/source/funkin/data/song/SongDataUtils.hx
index 4b9318df2..ee3dfe98c 100644
--- a/source/funkin/data/song/SongDataUtils.hx
+++ b/source/funkin/data/song/SongDataUtils.hx
@@ -21,11 +21,21 @@ class SongDataUtils
* @param notes The notes to modify.
* @param offset The time difference to apply in milliseconds.
*/
- public static function offsetSongNoteData(notes:Array, offset:Int):Array
+ public static function offsetSongNoteData(notes:Array, offset:Float):Array
{
- return notes.map(function(note:SongNoteData):SongNoteData {
- return new SongNoteData(note.time + offset, note.data, note.length, note.kind);
- });
+ var offsetNote = function(note:SongNoteData):SongNoteData {
+ var time:Float = note.time + offset;
+ var data:Int = note.data;
+ var length:Float = note.length;
+ var kind:String = note.kind;
+ return new SongNoteData(time, data, length, kind);
+ };
+
+ trace(notes);
+ trace(notes[0]);
+ var result = [for (i in 0...notes.length) offsetNote(notes[i])];
+ trace(result);
+ return result;
}
/**
@@ -36,7 +46,7 @@ class SongDataUtils
* @param events The events to modify.
* @param offset The time difference to apply in milliseconds.
*/
- public static function offsetSongEventData(events:Array, offset:Int):Array
+ public static function offsetSongEventData(events:Array, offset:Float):Array
{
return events.map(function(event:SongEventData):SongEventData {
return new SongEventData(event.time + offset, event.event, event.value);
@@ -152,7 +162,8 @@ class SongDataUtils
*/
public static function writeItemsToClipboard(data:SongClipboardItems):Void
{
- var dataString = SerializerUtil.toJSON(data);
+ var writer = new json2object.JsonWriter();
+ var dataString:String = writer.write(data, ' ');
ClipboardUtil.setClipboard(dataString);
@@ -170,19 +181,24 @@ class SongDataUtils
trace('Read ${notesString.length} characters from clipboard.');
- var data:SongClipboardItems = notesString.parseJSON();
-
- if (data == null)
+ var parser = new json2object.JsonParser();
+ parser.fromJson(notesString, 'clipboard');
+ if (parser.errors.length > 0)
{
- trace('Failed to parse notes from clipboard.');
+ trace('[SongDataUtils] Error parsing note JSON data from clipboard.');
+ for (error in parser.errors)
+ DataError.printError(error);
return {
+ valid: false,
notes: [],
events: []
};
}
else
{
+ var data:SongClipboardItems = parser.value;
trace('Parsed ' + data.notes.length + ' notes and ' + data.events.length + ' from clipboard.');
+ data.valid = true;
return data;
}
}
@@ -230,6 +246,7 @@ class SongDataUtils
typedef SongClipboardItems =
{
+ ?valid:Bool,
notes:Array,
events:Array
}
diff --git a/source/funkin/data/song/SongRegistry.hx b/source/funkin/data/song/SongRegistry.hx
index cf2da14f7..889fca707 100644
--- a/source/funkin/data/song/SongRegistry.hx
+++ b/source/funkin/data/song/SongRegistry.hx
@@ -156,7 +156,7 @@ class SongRegistry extends BaseRegistry
return cleanMetadata(parser.value, variation);
}
- public function parseEntryMetadataWithMigration(id:String, ?variation:String, version:thx.semver.Version):Null
+ public function parseEntryMetadataWithMigration(id:String, variation:String, version:thx.semver.Version):Null
{
variation = variation == null ? Constants.DEFAULT_VARIATION : variation;
@@ -192,7 +192,7 @@ class SongRegistry extends BaseRegistry
}
}
- function parseEntryMetadata_v2_0_0(id:String, variation:String = ""):Null
+ function parseEntryMetadata_v2_0_0(id:String, ?variation:String):Null
{
variation = variation == null ? Constants.DEFAULT_VARIATION : variation;
diff --git a/source/funkin/import.hx b/source/funkin/import.hx
index 06fe2bfa8..1c3a0fdb4 100644
--- a/source/funkin/import.hx
+++ b/source/funkin/import.hx
@@ -4,6 +4,7 @@ package;
// Only import these when we aren't in a macro.
import funkin.util.Constants;
import funkin.Paths;
+import funkin.Preferences;
import flixel.FlxG; // This one in particular causes a compile error if you're using macros.
// These are great.
diff --git a/source/funkin/input/PreciseInputManager.hx b/source/funkin/input/PreciseInputManager.hx
index 4cce0964d..59e6610a5 100644
--- a/source/funkin/input/PreciseInputManager.hx
+++ b/source/funkin/input/PreciseInputManager.hx
@@ -1,18 +1,25 @@
package funkin.input;
-import openfl.ui.Keyboard;
-import funkin.play.notes.NoteDirection;
-import flixel.input.keyboard.FlxKeyboard.FlxKeyInput;
-import openfl.events.KeyboardEvent;
import flixel.FlxG;
+import flixel.input.FlxInput;
import flixel.input.FlxInput.FlxInputState;
import flixel.input.FlxKeyManager;
+import flixel.input.gamepad.FlxGamepad;
+import flixel.input.gamepad.FlxGamepadInputID;
import flixel.input.keyboard.FlxKey;
+import flixel.input.keyboard.FlxKeyboard.FlxKeyInput;
import flixel.input.keyboard.FlxKeyList;
import flixel.util.FlxSignal.FlxTypedSignal;
+import funkin.play.notes.NoteDirection;
+import funkin.util.FlxGamepadUtil;
import haxe.Int64;
+import lime.ui.Gamepad as LimeGamepad;
+import lime.ui.GamepadAxis as LimeGamepadAxis;
+import lime.ui.GamepadButton as LimeGamepadButton;
import lime.ui.KeyCode;
import lime.ui.KeyModifier;
+import openfl.events.KeyboardEvent;
+import openfl.ui.Keyboard;
/**
* A precise input manager that:
@@ -43,6 +50,20 @@ class PreciseInputManager extends FlxKeyManager
*/
var _keyListDir:Map;
+ /**
+ * A FlxGamepadID->Array, with FlxGamepadInputID being the counterpart to FlxKey.
+ */
+ var _buttonList:Map>;
+
+ var _buttonListArray:Array>;
+
+ var _buttonListMap:Map>>;
+
+ /**
+ * A FlxGamepadID->FlxGamepadInputID->NoteDirection, with FlxGamepadInputID being the counterpart to FlxKey.
+ */
+ var _buttonListDir:Map>;
+
/**
* The timestamp at which a given note direction was last pressed.
*/
@@ -53,15 +74,32 @@ class PreciseInputManager extends FlxKeyManager
*/
var _dirReleaseTimestamps:Map;
+ var _deviceBinds:MapInt64->Void,
+ onButtonUp:LimeGamepadButton->Int64->Void
+ }>;
+
public function new()
{
super(PreciseInputList.new);
+ _deviceBinds = [];
+
_keyList = [];
- _dirPressTimestamps = new Map();
- _dirReleaseTimestamps = new Map();
+ // _keyListMap
+ // _keyListArray
_keyListDir = new Map();
+ _buttonList = [];
+ _buttonListMap = [];
+ _buttonListArray = [];
+ _buttonListDir = new Map>();
+
+ _dirPressTimestamps = new Map();
+ _dirReleaseTimestamps = new Map();
+
+ // Keyboard
FlxG.stage.removeEventListener(KeyboardEvent.KEY_DOWN, onKeyDown);
FlxG.stage.removeEventListener(KeyboardEvent.KEY_UP, onKeyUp);
FlxG.stage.application.window.onKeyDownPrecise.add(handleKeyDown);
@@ -84,6 +122,17 @@ class PreciseInputManager extends FlxKeyManager
};
}
+ public static function getButtonsForDirection(controls:Controls, noteDirection:NoteDirection)
+ {
+ return switch (noteDirection)
+ {
+ case NoteDirection.LEFT: controls.getButtonsForAction(NOTE_LEFT);
+ case NoteDirection.DOWN: controls.getButtonsForAction(NOTE_DOWN);
+ case NoteDirection.UP: controls.getButtonsForAction(NOTE_UP);
+ case NoteDirection.RIGHT: controls.getButtonsForAction(NOTE_RIGHT);
+ };
+ }
+
/**
* Convert from int to Int64.
*/
@@ -138,6 +187,43 @@ class PreciseInputManager extends FlxKeyManager
}
}
+ public function initializeButtons(controls:Controls, gamepad:FlxGamepad):Void
+ {
+ clearButtons();
+
+ var limeGamepad = FlxGamepadUtil.getLimeGamepad(gamepad);
+ var callbacks =
+ {
+ onButtonDown: handleButtonDown.bind(gamepad),
+ onButtonUp: handleButtonUp.bind(gamepad)
+ };
+ limeGamepad.onButtonDownPrecise.add(callbacks.onButtonDown);
+ limeGamepad.onButtonUpPrecise.add(callbacks.onButtonUp);
+
+ for (noteDirection in DIRECTIONS)
+ {
+ var buttons = getButtonsForDirection(controls, noteDirection);
+ for (button in buttons)
+ {
+ var input = new FlxInput(button);
+
+ var buttonListEntry = _buttonList.get(gamepad.id);
+ if (buttonListEntry == null) _buttonList.set(gamepad.id, buttonListEntry = []);
+ buttonListEntry.push(button);
+
+ _buttonListArray.push(input);
+
+ var buttonListMapEntry = _buttonListMap.get(gamepad.id);
+ if (buttonListMapEntry == null) _buttonListMap.set(gamepad.id, buttonListMapEntry = new Map>());
+ buttonListMapEntry.set(button, input);
+
+ var buttonListDirEntry = _buttonListDir.get(gamepad.id);
+ if (buttonListDirEntry == null) _buttonListDir.set(gamepad.id, buttonListDirEntry = new Map());
+ buttonListDirEntry.set(button, noteDirection);
+ }
+ }
+ }
+
/**
* Get the time, in nanoseconds, since the given note direction was last pressed.
* @param noteDirection The note direction to check.
@@ -165,11 +251,41 @@ class PreciseInputManager extends FlxKeyManager
return _keyListMap.get(key);
}
+ public function getInputByButton(gamepad:FlxGamepad, button:FlxGamepadInputID):FlxInput
+ {
+ return _buttonListMap.get(gamepad.id).get(button);
+ }
+
public function getDirectionForKey(key:FlxKey):NoteDirection
{
return _keyListDir.get(key);
}
+ public function getDirectionForButton(gamepad:FlxGamepad, button:FlxGamepadInputID):NoteDirection
+ {
+ return _buttonListDir.get(gamepad.id).get(button);
+ }
+
+ function getButton(gamepad:FlxGamepad, button:FlxGamepadInputID):FlxInput
+ {
+ return _buttonListMap.get(gamepad.id).get(button);
+ }
+
+ function updateButtonStates(gamepad:FlxGamepad, button:FlxGamepadInputID, down:Bool):Void
+ {
+ var input = getButton(gamepad, button);
+ if (input == null) return;
+
+ if (down)
+ {
+ input.press();
+ }
+ else
+ {
+ input.release();
+ }
+ }
+
function handleKeyDown(keyCode:KeyCode, _:KeyModifier, timestamp:Int64):Void
{
var key:FlxKey = convertKeyCode(keyCode);
@@ -198,7 +314,7 @@ class PreciseInputManager extends FlxKeyManager
if (_keyList.indexOf(key) == -1) return;
// TODO: Remove this line with SDL3 when timestamps change meaning.
- // This is because SDL3's timestamps are measured in nanoseconds, not milliseconds.
+ // This is because SDL3's timestamps ar e measured in nanoseconds, not milliseconds.
timestamp *= Constants.NS_PER_MS;
updateKeyStates(key, false);
@@ -214,6 +330,54 @@ class PreciseInputManager extends FlxKeyManager
}
}
+ function handleButtonDown(gamepad:FlxGamepad, button:LimeGamepadButton, timestamp:Int64):Void
+ {
+ var buttonId:FlxGamepadInputID = FlxGamepadUtil.getInputID(gamepad, button);
+
+ var buttonListEntry = _buttonList.get(gamepad.id);
+ if (buttonListEntry == null || buttonListEntry.indexOf(buttonId) == -1) return;
+
+ // TODO: Remove this line with SDL3 when timestamps change meaning.
+ // This is because SDL3's timestamps ar e measured in nanoseconds, not milliseconds.
+ timestamp *= Constants.NS_PER_MS;
+
+ updateButtonStates(gamepad, buttonId, true);
+
+ if (getInputByButton(gamepad, buttonId)?.justPressed ?? false)
+ {
+ onInputPressed.dispatch(
+ {
+ noteDirection: getDirectionForButton(gamepad, buttonId),
+ timestamp: timestamp
+ });
+ _dirPressTimestamps.set(getDirectionForButton(gamepad, buttonId), timestamp);
+ }
+ }
+
+ function handleButtonUp(gamepad:FlxGamepad, button:LimeGamepadButton, timestamp:Int64):Void
+ {
+ var buttonId:FlxGamepadInputID = FlxGamepadUtil.getInputID(gamepad, button);
+
+ var buttonListEntry = _buttonList.get(gamepad.id);
+ if (buttonListEntry == null || buttonListEntry.indexOf(buttonId) == -1) return;
+
+ // TODO: Remove this line with SDL3 when timestamps change meaning.
+ // This is because SDL3's timestamps ar e measured in nanoseconds, not milliseconds.
+ timestamp *= Constants.NS_PER_MS;
+
+ updateButtonStates(gamepad, buttonId, false);
+
+ if (getInputByButton(gamepad, buttonId)?.justReleased ?? false)
+ {
+ onInputReleased.dispatch(
+ {
+ noteDirection: getDirectionForButton(gamepad, buttonId),
+ timestamp: timestamp
+ });
+ _dirReleaseTimestamps.set(getDirectionForButton(gamepad, buttonId), timestamp);
+ }
+ }
+
static function convertKeyCode(input:KeyCode):FlxKey
{
@:privateAccess
@@ -228,6 +392,31 @@ class PreciseInputManager extends FlxKeyManager
_keyListMap.clear();
_keyListDir.clear();
}
+
+ function clearButtons():Void
+ {
+ _buttonListArray = [];
+ _buttonListDir.clear();
+
+ for (gamepad in _deviceBinds.keys())
+ {
+ var callbacks = _deviceBinds.get(gamepad);
+ var limeGamepad = FlxGamepadUtil.getLimeGamepad(gamepad);
+ limeGamepad.onButtonDownPrecise.remove(callbacks.onButtonDown);
+ limeGamepad.onButtonUpPrecise.remove(callbacks.onButtonUp);
+ }
+ _deviceBinds.clear();
+ }
+
+ public override function destroy():Void
+ {
+ // Keyboard
+ FlxG.stage.application.window.onKeyDownPrecise.remove(handleKeyDown);
+ FlxG.stage.application.window.onKeyUpPrecise.remove(handleKeyUp);
+
+ clearKeys();
+ clearButtons();
+ }
}
class PreciseInputList extends FlxKeyList
diff --git a/source/funkin/modding/PolymodHandler.hx b/source/funkin/modding/PolymodHandler.hx
index f7f69428b..7716f0f02 100644
--- a/source/funkin/modding/PolymodHandler.hx
+++ b/source/funkin/modding/PolymodHandler.hx
@@ -14,6 +14,7 @@ import funkin.data.level.LevelRegistry;
import funkin.data.notestyle.NoteStyleRegistry;
import funkin.play.cutscene.dialogue.ConversationDataParser;
import funkin.play.cutscene.dialogue.DialogueBoxDataParser;
+import funkin.save.Save;
import funkin.play.cutscene.dialogue.SpeakerDataParser;
import funkin.data.song.SongRegistry;
@@ -59,7 +60,7 @@ class PolymodHandler
createModRoot();
trace("Initializing Polymod (using configured mods)...");
- loadModsById(getEnabledModIds());
+ loadModsById(Save.get().enabledModIds);
}
/**
@@ -232,33 +233,9 @@ class PolymodHandler
return modIds;
}
- public static function setEnabledMods(newModList:Array):Void
- {
- FlxG.save.data.enabledMods = newModList;
- // Make sure to COMMIT the changes.
- FlxG.save.flush();
- }
-
- /**
- * Returns the list of enabled mods.
- * @return Array
- */
- public static function getEnabledModIds():Array
- {
- if (FlxG.save.data.enabledMods == null)
- {
- // NOTE: If the value is null, the enabled mod list is unconfigured.
- // Currently, we default to disabling newly installed mods.
- // If we want to auto-enable new mods, but otherwise leave the configured list in place,
- // we will need some custom logic.
- FlxG.save.data.enabledMods = [];
- }
- return FlxG.save.data.enabledMods;
- }
-
public static function getEnabledMods():Array
{
- var modIds = getEnabledModIds();
+ var modIds = Save.get().enabledModIds;
var modMetadata = getAllMods();
var enabledMods = [];
for (item in modMetadata)
diff --git a/source/funkin/play/GameOverSubState.hx b/source/funkin/play/GameOverSubState.hx
index 15ed0421e..c5d9b4b0b 100644
--- a/source/funkin/play/GameOverSubState.hx
+++ b/source/funkin/play/GameOverSubState.hx
@@ -11,7 +11,6 @@ import funkin.modding.events.ScriptEvent;
import funkin.modding.events.ScriptEventDispatcher;
import funkin.play.PlayState;
import funkin.play.character.BaseCharacter;
-import funkin.ui.PreferencesMenu;
/**
* A substate which renders over the PlayState when the player dies.
@@ -103,6 +102,9 @@ class GameOverSubState extends MusicBeatSubState
cameraFollowPoint = new FlxObject(PlayState.instance.cameraFollowPoint.x, PlayState.instance.cameraFollowPoint.y, 1, 1);
cameraFollowPoint.x = boyfriend.getGraphicMidpoint().x;
cameraFollowPoint.y = boyfriend.getGraphicMidpoint().y;
+ var offsets:Array = boyfriend.getDeathCameraOffsets();
+ cameraFollowPoint.x += offsets[0];
+ cameraFollowPoint.y += offsets[1];
add(cameraFollowPoint);
FlxG.camera.target = null;
@@ -292,7 +294,7 @@ class GameOverSubState extends MusicBeatSubState
{
var randomCensor:Array = [];
- if (PreferencesMenu.getPref('censor-naughty')) randomCensor = [1, 3, 8, 13, 17, 21];
+ if (!Preferences.naughtyness) randomCensor = [1, 3, 8, 13, 17, 21];
FlxG.sound.play(Paths.sound('jeffGameover/jeffGameover-' + FlxG.random.int(1, 25, randomCensor)), 1, false, null, true, function() {
// Once the quote ends, fade in the game over music.
diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx
index d7c2a2a4c..048b6ed6e 100644
--- a/source/funkin/play/PlayState.hx
+++ b/source/funkin/play/PlayState.hx
@@ -25,6 +25,7 @@ import flixel.ui.FlxBar;
import flixel.util.FlxColor;
import flixel.util.FlxTimer;
import funkin.audio.VoicesGroup;
+import funkin.save.Save;
import funkin.Highscore.Tallies;
import funkin.input.PreciseInputManager;
import funkin.modding.events.ScriptEvent;
@@ -919,7 +920,6 @@ class PlayState extends MusicBeatSubState
}
// Handle keybinds.
- // if (!isInCutscene && !disableKeys) keyShit(true);
processInputQueue();
if (!isInCutscene && !disableKeys) debugKeyShit();
if (isInCutscene && !disableKeys) handleCutsceneKeys(elapsed);
@@ -1267,7 +1267,7 @@ class PlayState extends MusicBeatSubState
*/
function initHealthBar():Void
{
- var healthBarYPos:Float = PreferencesMenu.getPref('downscroll') ? FlxG.height * 0.1 : FlxG.height * 0.9;
+ var healthBarYPos:Float = Preferences.downscroll ? FlxG.height * 0.1 : FlxG.height * 0.9;
healthBarBG = new FlxSprite(0, healthBarYPos).loadGraphic(Paths.image('healthBar'));
healthBarBG.screenCenter(X);
healthBarBG.scrollFactor.set(0, 0);
@@ -1476,13 +1476,13 @@ class PlayState extends MusicBeatSubState
// Position the player strumline on the right half of the screen
playerStrumline.x = FlxG.width / 2 + Constants.STRUMLINE_X_OFFSET; // Classic style
// playerStrumline.x = FlxG.width - playerStrumline.width - Constants.STRUMLINE_X_OFFSET; // Centered style
- playerStrumline.y = PreferencesMenu.getPref('downscroll') ? FlxG.height - playerStrumline.height - Constants.STRUMLINE_Y_OFFSET : Constants.STRUMLINE_Y_OFFSET;
+ playerStrumline.y = Preferences.downscroll ? FlxG.height - playerStrumline.height - Constants.STRUMLINE_Y_OFFSET : Constants.STRUMLINE_Y_OFFSET;
playerStrumline.zIndex = 200;
playerStrumline.cameras = [camHUD];
// Position the opponent strumline on the left half of the screen
opponentStrumline.x = Constants.STRUMLINE_X_OFFSET;
- opponentStrumline.y = PreferencesMenu.getPref('downscroll') ? FlxG.height - opponentStrumline.height - Constants.STRUMLINE_Y_OFFSET : Constants.STRUMLINE_Y_OFFSET;
+ opponentStrumline.y = Preferences.downscroll ? FlxG.height - opponentStrumline.height - Constants.STRUMLINE_Y_OFFSET : Constants.STRUMLINE_Y_OFFSET;
opponentStrumline.zIndex = 100;
opponentStrumline.cameras = [camHUD];
@@ -1641,7 +1641,7 @@ class PlayState extends MusicBeatSubState
*/
function onConversationComplete():Void
{
- isInCutscene = true;
+ isInCutscene = false;
remove(currentConversation);
currentConversation = null;
@@ -2393,9 +2393,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 +2442,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, PlayStatePlaylist.campaignDifficulty, data))
+ {
+ Save.get().setLevelScore(PlayStatePlaylist.campaignId, PlayStatePlaylist.campaignDifficulty, data);
+ #if newgrounds
+ NGio.postScore(score, 'Level ${PlayStatePlaylist.campaignId}');
+ #end
+ }
+ }
if (isSubState)
{
@@ -2466,7 +2513,7 @@ class PlayState extends MusicBeatSubState
var nextPlayState:PlayState = new PlayState(
{
targetSong: targetSong,
- targetDifficulty: currentDifficulty,
+ targetDifficulty: PlayStatePlaylist.campaignDifficulty,
targetCharacter: currentPlayerId,
});
nextPlayState.previousCameraFollowPoint = new FlxSprite(cameraFollowPoint.x, cameraFollowPoint.y);
@@ -2482,7 +2529,7 @@ class PlayState extends MusicBeatSubState
var nextPlayState:PlayState = new PlayState(
{
targetSong: targetSong,
- targetDifficulty: currentDifficulty,
+ targetDifficulty: PlayStatePlaylist.campaignDifficulty,
targetCharacter: currentPlayerId,
});
nextPlayState.previousCameraFollowPoint = new FlxSprite(cameraFollowPoint.x, cameraFollowPoint.y);
@@ -2608,7 +2655,12 @@ class PlayState extends MusicBeatSubState
persistentUpdate = false;
vocals.stop();
camHUD.alpha = 1;
- var res:ResultState = new ResultState();
+ var res:ResultState = new ResultState(
+ {
+ storyMode: PlayStatePlaylist.isStoryMode,
+ title: PlayStatePlaylist.isStoryMode ? ('${PlayStatePlaylist.campaignTitle}') : ('${currentChart.songName} by ${currentChart.songArtist}'),
+ tallies: Highscore.tallies,
+ });
res.camera = camHUD;
openSubState(res);
}
diff --git a/source/funkin/play/PlayStatePlaylist.hx b/source/funkin/play/PlayStatePlaylist.hx
index 6b754878c..3b0fb01f6 100644
--- a/source/funkin/play/PlayStatePlaylist.hx
+++ b/source/funkin/play/PlayStatePlaylist.hx
@@ -34,10 +34,7 @@ class PlayStatePlaylist
*/
public static var campaignId:String = 'unknown';
- /**
- * The current difficulty selected for this level (as a named ID).
- */
- public static var currentDifficulty(default, default):String = Constants.DEFAULT_DIFFICULTY;
+ public static var campaignDifficulty:String = Constants.DEFAULT_DIFFICULTY;
/**
* Resets the playlist to its default state.
@@ -49,6 +46,6 @@ class PlayStatePlaylist
campaignScore = 0;
campaignTitle = 'UNKNOWN';
campaignId = 'unknown';
- currentDifficulty = Constants.DEFAULT_DIFFICULTY;
+ campaignDifficulty = Constants.DEFAULT_DIFFICULTY;
}
}
diff --git a/source/funkin/play/ResultState.hx b/source/funkin/play/ResultState.hx
index 0c2984719..3f7231c2a 100644
--- a/source/funkin/play/ResultState.hx
+++ b/source/funkin/play/ResultState.hx
@@ -22,6 +22,8 @@ import flxanimate.FlxAnimate.Settings;
class ResultState extends MusicBeatSubState
{
+ final params:ResultsStateParams;
+
var resultsVariation:ResultVariations;
var songName:FlxBitmapText;
var difficulty:FlxSprite;
@@ -29,13 +31,18 @@ class ResultState extends MusicBeatSubState
var maskShaderSongName = new LeftMaskShader();
var maskShaderDifficulty = new LeftMaskShader();
+ public function new(params:ResultsStateParams)
+ {
+ super();
+
+ this.params = params;
+ }
+
override function create():Void
{
- if (Highscore.tallies.sick == Highscore.tallies.totalNotesHit
- && Highscore.tallies.maxCombo == Highscore.tallies.totalNotesHit) resultsVariation = PERFECT;
- else if (Highscore.tallies.missed
- + Highscore.tallies.bad
- + Highscore.tallies.shit >= Highscore.tallies.totalNotes * 0.50)
+ if (params.tallies.sick == params.tallies.totalNotesHit
+ && params.tallies.maxCombo == params.tallies.totalNotesHit) resultsVariation = PERFECT;
+ else if (params.tallies.missed + params.tallies.bad + params.tallies.shit >= params.tallies.totalNotes * 0.50)
resultsVariation = SHIT; // if more than half of your song was missed, bad, or shit notes, you get shit ending!
else
resultsVariation = NORMAL;
@@ -135,17 +142,7 @@ class ResultState extends MusicBeatSubState
var fontLetters:String = "AaBbCcDdEeFfGgHhiIJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz:1234567890";
songName = new FlxBitmapText(FlxBitmapFont.fromMonospace(Paths.image("resultScreen/tardlingSpritesheet"), fontLetters, FlxPoint.get(49, 62)));
-
- // stole this from PauseSubState, I think eric wrote it!!
- if (PlayState.instance.currentChart != null)
- {
- songName.text += '${PlayState.instance.currentChart.songName}:${PlayState.instance.currentChart.songArtist}';
- }
- else
- {
- songName.text += PlayState.instance.currentSong.id;
- }
-
+ songName.text = params.title;
songName.letterSpacing = -15;
songName.angle = -4.1;
add(songName);
@@ -194,27 +191,27 @@ class ResultState extends MusicBeatSubState
var ratingGrp:FlxTypedGroup = new FlxTypedGroup();
add(ratingGrp);
- var totalHit:TallyCounter = new TallyCounter(375, hStuf * 3, Highscore.tallies.totalNotesHit);
+ var totalHit:TallyCounter = new TallyCounter(375, hStuf * 3, params.tallies.totalNotesHit);
ratingGrp.add(totalHit);
- var maxCombo:TallyCounter = new TallyCounter(375, hStuf * 4, Highscore.tallies.maxCombo);
+ var maxCombo:TallyCounter = new TallyCounter(375, hStuf * 4, params.tallies.maxCombo);
ratingGrp.add(maxCombo);
hStuf += 2;
var extraYOffset:Float = 5;
- var tallySick:TallyCounter = new TallyCounter(230, (hStuf * 5) + extraYOffset, Highscore.tallies.sick, 0xFF89E59E);
+ var tallySick:TallyCounter = new TallyCounter(230, (hStuf * 5) + extraYOffset, params.tallies.sick, 0xFF89E59E);
ratingGrp.add(tallySick);
- var tallyGood:TallyCounter = new TallyCounter(210, (hStuf * 6) + extraYOffset, Highscore.tallies.good, 0xFF89C9E5);
+ var tallyGood:TallyCounter = new TallyCounter(210, (hStuf * 6) + extraYOffset, params.tallies.good, 0xFF89C9E5);
ratingGrp.add(tallyGood);
- var tallyBad:TallyCounter = new TallyCounter(190, (hStuf * 7) + extraYOffset, Highscore.tallies.bad, 0xffE6CF8A);
+ var tallyBad:TallyCounter = new TallyCounter(190, (hStuf * 7) + extraYOffset, params.tallies.bad, 0xffE6CF8A);
ratingGrp.add(tallyBad);
- var tallyShit:TallyCounter = new TallyCounter(220, (hStuf * 8) + extraYOffset, Highscore.tallies.shit, 0xFFE68C8A);
+ var tallyShit:TallyCounter = new TallyCounter(220, (hStuf * 8) + extraYOffset, params.tallies.shit, 0xFFE68C8A);
ratingGrp.add(tallyShit);
- var tallyMissed:TallyCounter = new TallyCounter(260, (hStuf * 9) + extraYOffset, Highscore.tallies.missed, 0xFFC68AE6);
+ var tallyMissed:TallyCounter = new TallyCounter(260, (hStuf * 9) + extraYOffset, params.tallies.missed, 0xFFC68AE6);
ratingGrp.add(tallyMissed);
for (ind => rating in ratingGrp.members)
@@ -275,7 +272,7 @@ class ResultState extends MusicBeatSubState
}
});
- if (Highscore.tallies.isNewHighscore) trace("ITS A NEW HIGHSCORE!!!");
+ if (params.tallies.isNewHighscore) trace("ITS A NEW HIGHSCORE!!!");
super.create();
}
@@ -351,7 +348,7 @@ class ResultState extends MusicBeatSubState
if (controls.PAUSE)
{
- if (PlayStatePlaylist.isStoryMode)
+ if (params.storyMode)
{
FlxG.switchState(new StoryMenuState());
}
@@ -372,3 +369,21 @@ enum abstract ResultVariations(String)
var NORMAL;
var SHIT;
}
+
+typedef ResultsStateParams =
+{
+ /**
+ * True if results are for a level, false if results are for a single song.
+ */
+ var storyMode:Bool;
+
+ /**
+ * Either "Song Name by Artist Name" or "Week Name"
+ */
+ var title:String;
+
+ /**
+ * The score, accuracy, and judgements.
+ */
+ var tallies:Highscore.Tallies;
+};
diff --git a/source/funkin/play/character/BaseCharacter.hx b/source/funkin/play/character/BaseCharacter.hx
index 30b549fd3..5346ced61 100644
--- a/source/funkin/play/character/BaseCharacter.hx
+++ b/source/funkin/play/character/BaseCharacter.hx
@@ -188,6 +188,11 @@ class BaseCharacter extends Bopper
shouldBop = false;
}
+ public function getDeathCameraOffsets():Array
+ {
+ return _data.death?.cameraOffsets ?? [0.0, 0.0];
+ }
+
/**
* Gets the value of flipX from the character data.
* `!getFlipX()` is the direction Boyfriend should face.
@@ -580,8 +585,7 @@ class BaseCharacter extends Bopper
public override function playAnimation(name:String, restart:Bool = false, ignoreOther:Bool = false, reversed:Bool = false):Void
{
- FlxG.watch.addQuick('playAnim(${characterName})', name);
- // trace('playAnim(${characterName}): ${name}');
+ // FlxG.watch.addQuick('playAnim(${characterName})', name);
super.playAnimation(name, restart, ignoreOther, reversed);
}
}
diff --git a/source/funkin/play/character/CharacterData.hx b/source/funkin/play/character/CharacterData.hx
index f1b316b7f..8be9f25c7 100644
--- a/source/funkin/play/character/CharacterData.hx
+++ b/source/funkin/play/character/CharacterData.hx
@@ -19,8 +19,10 @@ class CharacterDataParser
* The current version string for the stage data format.
* Handle breaking changes by incrementing this value
* and adding migration to the `migrateStageData()` function.
+ *
+ * - Version 1.0.1 adds `death.cameraOffsets`
*/
- public static final CHARACTER_DATA_VERSION:String = '1.0.0';
+ public static final CHARACTER_DATA_VERSION:String = '1.0.1';
/**
* The current version rule check for the stage data format.
@@ -603,6 +605,8 @@ typedef CharacterData =
*/
var healthIcon:Null;
+ var death:Null;
+
/**
* The global offset to the character's position, in pixels.
* @default [0, 0]
@@ -695,3 +699,13 @@ typedef HealthIconData =
*/
var offsets:Null>;
}
+
+typedef DeathData =
+{
+ /**
+ * The amount to offset the camera by while focusing on this character as they die.
+ * Default value focuses on the character's graphic midpoint.
+ * @default [0, 0]
+ */
+ var ?cameraOffsets:Array;
+}
diff --git a/source/funkin/play/notes/Strumline.hx b/source/funkin/play/notes/Strumline.hx
index 7bd6e7ae7..60b995c06 100644
--- a/source/funkin/play/notes/Strumline.hx
+++ b/source/funkin/play/notes/Strumline.hx
@@ -231,7 +231,7 @@ class Strumline extends FlxSpriteGroup
notesVwoosh.add(note);
var targetY:Float = FlxG.height + note.y;
- if (PreferencesMenu.getPref('downscroll')) targetY = 0 - note.height;
+ if (Preferences.downscroll) targetY = 0 - note.height;
FlxTween.tween(note, {y: targetY}, 0.5,
{
ease: FlxEase.expoIn,
@@ -252,7 +252,7 @@ class Strumline extends FlxSpriteGroup
holdNotesVwoosh.add(holdNote);
var targetY:Float = FlxG.height + holdNote.y;
- if (PreferencesMenu.getPref('downscroll')) targetY = 0 - holdNote.height;
+ if (Preferences.downscroll) targetY = 0 - holdNote.height;
FlxTween.tween(holdNote, {y: targetY}, 0.5,
{
ease: FlxEase.expoIn,
@@ -277,7 +277,7 @@ class Strumline extends FlxSpriteGroup
var vwoosh:Float = (strumTime < Conductor.songPosition) && vwoosh ? 2.0 : 1.0;
var scrollSpeed:Float = PlayState.instance?.currentChart?.scrollSpeed ?? 1.0;
- return Constants.PIXELS_PER_MS * (Conductor.songPosition - strumTime) * scrollSpeed * vwoosh * (PreferencesMenu.getPref('downscroll') ? 1 : -1);
+ return Constants.PIXELS_PER_MS * (Conductor.songPosition - strumTime) * scrollSpeed * vwoosh * (Preferences.downscroll ? 1 : -1);
}
function updateNotes():Void
@@ -321,7 +321,7 @@ class Strumline extends FlxSpriteGroup
note.y = this.y - INITIAL_OFFSET + calculateNoteYPos(note.strumTime, vwoosh);
// If the note is miss
- var isOffscreen = PreferencesMenu.getPref('downscroll') ? note.y > FlxG.height : note.y < -note.height;
+ var isOffscreen = Preferences.downscroll ? note.y > FlxG.height : note.y < -note.height;
if (note.handledMiss && isOffscreen)
{
killNote(note);
@@ -388,7 +388,7 @@ class Strumline extends FlxSpriteGroup
var vwoosh:Bool = false;
- if (PreferencesMenu.getPref('downscroll'))
+ if (Preferences.downscroll)
{
holdNote.y = this.y + calculateNoteYPos(holdNote.strumTime, vwoosh) - holdNote.height + STRUMLINE_SIZE / 2;
}
@@ -410,7 +410,7 @@ class Strumline extends FlxSpriteGroup
holdNote.visible = false;
}
- if (PreferencesMenu.getPref('downscroll'))
+ if (Preferences.downscroll)
{
holdNote.y = this.y - holdNote.height + STRUMLINE_SIZE / 2;
}
@@ -425,7 +425,7 @@ class Strumline extends FlxSpriteGroup
holdNote.visible = true;
var vwoosh:Bool = false;
- if (PreferencesMenu.getPref('downscroll'))
+ if (Preferences.downscroll)
{
holdNote.y = this.y + calculateNoteYPos(holdNote.strumTime, vwoosh) - holdNote.height + STRUMLINE_SIZE / 2;
}
diff --git a/source/funkin/play/notes/SustainTrail.hx b/source/funkin/play/notes/SustainTrail.hx
index 37bc674a5..f55799828 100644
--- a/source/funkin/play/notes/SustainTrail.hx
+++ b/source/funkin/play/notes/SustainTrail.hx
@@ -114,7 +114,7 @@ class SustainTrail extends FlxSprite
height = sustainHeight(sustainLength, getScrollSpeed());
// instead of scrollSpeed, PlayState.SONG.speed
- flipY = PreferencesMenu.getPref('downscroll');
+ flipY = Preferences.downscroll;
// alpha = 0.6;
alpha = 1.0;
diff --git a/source/funkin/play/song/Song.hx b/source/funkin/play/song/Song.hx
index d11c7744b..f996d75ef 100644
--- a/source/funkin/play/song/Song.hx
+++ b/source/funkin/play/song/Song.hx
@@ -1,5 +1,6 @@
package funkin.play.song;
+import funkin.util.SortUtil;
import flixel.sound.FlxSound;
import openfl.utils.Assets;
import funkin.modding.events.ScriptEvent;
@@ -56,8 +57,6 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry;
-
public var songName(get, never):String;
function get_songName():String
@@ -85,7 +84,6 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry();
_data = _fetchData(id);
@@ -127,8 +125,7 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry chartData in charts)
@@ -162,8 +159,6 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry
{
- if (diffId == null) diffId = difficulties.keys().array()[0];
+ if (diffId == null) diffId = listDifficulties()[0];
return difficulties.get(diffId);
}
- public function listDifficulties():Array
+ /**
+ * List all the difficulties in this song.
+ * @param variationId Optionally filter by variation.
+ * @return The list of difficulties.
+ */
+ public function listDifficulties(?variationId:String):Array
{
- return difficultyIds;
+ if (variationId == '') variationId = null;
+
+ var diffFiltered:Array = difficulties.keys().array().filter(function(diffId:String):Bool {
+ if (variationId == null) return true;
+ var difficulty:Null = difficulties.get(diffId);
+ if (difficulty == null) return false;
+ return difficulty.variation == variationId;
+ });
+
+ diffFiltered.sort(SortUtil.defaultsThenAlphabetically.bind(Constants.DEFAULT_DIFFICULTY_LIST));
+
+ return diffFiltered;
}
- public function hasDifficulty(diffId:String):Bool
+ public function hasDifficulty(diffId:String, ?variationId:String):Bool
{
- return difficulties.exists(diffId);
+ if (variationId == '') variationId = null;
+ var difficulty:Null = difficulties.get(diffId);
+ return variationId == null ? (difficulty != null) : (difficulty != null && difficulty.variation == variationId);
}
/**
diff --git a/source/funkin/save/Save.hx b/source/funkin/save/Save.hx
new file mode 100644
index 000000000..2666d2bff
--- /dev/null
+++ b/source/funkin/save/Save.hx
@@ -0,0 +1,700 @@
+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,
+ flashingLights: true,
+ zoomCamera: true,
+ debugDisplay: false,
+ autoPause: true,
+
+ controls:
+ {
+ // Leave controls blank so defaults are loaded.
+ p1:
+ {
+ keyboard: {},
+ gamepad: {},
+ },
+ p2:
+ {
+ keyboard: {},
+ gamepad: {},
+ },
+ },
+ },
+
+ mods:
+ {
+ // No mods enabled.
+ enabledMods: [],
+ modOptions: [],
+ },
+
+ optionsChartEditor:
+ {
+ // Reasonable defaults.
+ },
+ };
+ }
+
+ public var options(get, never):SaveDataOptions;
+
+ function get_options():SaveDataOptions
+ {
+ return this.options;
+ }
+
+ public var modOptions(get, never):Map;
+
+ function get_modOptions():Map
+ {
+ return this.mods.modOptions;
+ }
+
+ /**
+ * The current session ID for the logged-in Newgrounds user, or null if the user is cringe.
+ */
+ public var ngSessionId(get, set):Null;
+
+ function get_ngSessionId():Null
+ {
+ return this.api.newgrounds.sessionId;
+ }
+
+ function set_ngSessionId(value:Null):Null
+ {
+ return this.api.newgrounds.sessionId = value;
+ }
+
+ public var enabledModIds(get, set):Array;
+
+ function get_enabledModIds():Array
+ {
+ return this.mods.enabledMods;
+ }
+
+ function set_enabledModIds(value:Array):Array
+ {
+ return this.mods.enabledMods = value;
+ }
+
+ /**
+ * Return the score the user achieved for a given level on a given difficulty.
+ *
+ * @param levelId The ID of the level/week.
+ * @param difficultyId The difficulty to check.
+ * @return A data structure containing score, judgement counts, and accuracy. Returns `null` if no score is saved.
+ */
+ public function getLevelScore(levelId:String, difficultyId:String = 'normal'):Null
+ {
+ var level = this.scores.levels.get(levelId);
+ if (level == null)
+ {
+ level = [];
+ this.scores.levels.set(levelId, level);
+ }
+
+ return level.get(difficultyId);
+ }
+
+ /**
+ * Apply the score the user achieved for a given level on a given difficulty.
+ */
+ public function setLevelScore(levelId:String, difficultyId:String, score:SaveScoreData):Void
+ {
+ var level = this.scores.levels.get(levelId);
+ if (level == null)
+ {
+ level = [];
+ this.scores.levels.set(levelId, level);
+ }
+ level.set(difficultyId, score);
+
+ flush();
+ }
+
+ public function isLevelHighScore(levelId:String, difficultyId:String = 'normal', score:SaveScoreData):Bool
+ {
+ var level = this.scores.levels.get(levelId);
+ if (level == null)
+ {
+ level = [];
+ this.scores.levels.set(levelId, level);
+ }
+
+ var currentScore = level.get(difficultyId);
+ if (currentScore == null)
+ {
+ return true;
+ }
+
+ return score.score > currentScore.score;
+ }
+
+ public function hasBeatenLevel(levelId:String, ?difficultyList:Array):Bool
+ {
+ if (difficultyList == null)
+ {
+ difficultyList = ['easy', 'normal', 'hard'];
+ }
+ for (difficulty in difficultyList)
+ {
+ var score:Null = getLevelScore(levelId, difficulty);
+ // TODO: Do we need to check accuracy/score here?
+ if (score != null)
+ {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Return the score the user achieved for a given song on a given difficulty.
+ *
+ * @param songId The ID of the song.
+ * @param difficultyId The difficulty to check.
+ * @return A data structure containing score, judgement counts, and accuracy. Returns `null` if no score is saved.
+ */
+ public function getSongScore(songId:String, difficultyId:String = 'normal'):Null
+ {
+ var song = this.scores.songs.get(songId);
+ if (song == null)
+ {
+ song = [];
+ this.scores.songs.set(songId, song);
+ }
+ return song.get(difficultyId);
+ }
+
+ /**
+ * Apply the score the user achieved for a given song on a given difficulty.
+ */
+ public function setSongScore(songId:String, difficultyId:String, score:SaveScoreData):Void
+ {
+ var song = this.scores.songs.get(songId);
+ if (song == null)
+ {
+ song = [];
+ this.scores.songs.set(songId, song);
+ }
+ song.set(difficultyId, score);
+
+ flush();
+ }
+
+ /**
+ * Is the provided score data better than the current high score for the given song?
+ * @param songId The song ID to check.
+ * @param difficultyId The difficulty to check.
+ * @param score The score to check.
+ * @return Whether the score is better than the current high score.
+ */
+ public function isSongHighScore(songId:String, difficultyId:String = 'normal', score:SaveScoreData):Bool
+ {
+ var song = this.scores.songs.get(songId);
+ if (song == null)
+ {
+ song = [];
+ this.scores.songs.set(songId, song);
+ }
+
+ var currentScore = song.get(difficultyId);
+ if (currentScore == null)
+ {
+ return true;
+ }
+
+ return score.score > currentScore.score;
+ }
+
+ /**
+ * Has the provided song been beaten on one of the listed difficulties?
+ * @param songId The song ID to check.
+ * @param difficultyList The difficulties to check. Defaults to `easy`, `normal`, and `hard`.
+ * @return Whether the song has been beaten on any of the listed difficulties.
+ */
+ public function hasBeatenSong(songId:String, ?difficultyList:Array):Bool
+ {
+ if (difficultyList == null)
+ {
+ difficultyList = ['easy', 'normal', 'hard'];
+ }
+ for (difficulty in difficultyList)
+ {
+ var score:Null = getSongScore(songId, difficulty);
+ // TODO: Do we need to check accuracy/score here?
+ if (score != null)
+ {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public function getControls(playerId:Int, inputType:Device):SaveControlsData
+ {
+ switch (inputType)
+ {
+ case Keys:
+ return (playerId == 0) ? this.options.controls.p1.keyboard : this.options.controls.p2.keyboard;
+ case Gamepad(_):
+ return (playerId == 0) ? this.options.controls.p1.gamepad : this.options.controls.p2.gamepad;
+ }
+ }
+
+ public function hasControls(playerId:Int, inputType:Device):Bool
+ {
+ var controls = getControls(playerId, inputType);
+ var controlsFields = Reflect.fields(controls);
+ return controlsFields.length > 0;
+ }
+
+ public function setControls(playerId:Int, inputType:Device, controls:SaveControlsData):Void
+ {
+ switch (inputType)
+ {
+ case Keys:
+ if (playerId == 0)
+ {
+ this.options.controls.p1.keyboard = controls;
+ }
+ else
+ {
+ this.options.controls.p2.keyboard = controls;
+ }
+ case Gamepad(_):
+ if (playerId == 0)
+ {
+ this.options.controls.p1.gamepad = controls;
+ }
+ else
+ {
+ this.options.controls.p2.gamepad = controls;
+ }
+ }
+
+ flush();
+ }
+
+ public function isCharacterUnlocked(characterId:String):Bool
+ {
+ switch (characterId)
+ {
+ case 'bf':
+ return true;
+ case 'pico':
+ return hasBeatenLevel('weekend1');
+ default:
+ trace('Unknown character ID: ' + characterId);
+ return true;
+ }
+ }
+
+ /**
+ * Call this to make sure the save data is written to disk.
+ */
+ public function flush():Void
+ {
+ FlxG.save.flush();
+ }
+
+ /**
+ * If you set slot to `2`, it will load an independe
+ * @param slot
+ */
+ static function loadFromSlot(slot:Int):Void
+ {
+ trace("[SAVE] Loading save from slot " + slot + "...");
+
+ FlxG.save.bind('$SAVE_NAME${slot}', SAVE_PATH);
+
+ if (FlxG.save.isEmpty())
+ {
+ trace('[SAVE] Save data is empty, checking for legacy save data...');
+ var legacySaveData = fetchLegacySaveData();
+ if (legacySaveData != null)
+ {
+ trace('[SAVE] Found legacy save data, converting...');
+ FlxG.save.mergeData(SaveDataMigrator.migrateFromLegacy(legacySaveData));
+ }
+ }
+ else
+ {
+ trace('[SAVE] Loaded save data.');
+ FlxG.save.mergeData(SaveDataMigrator.migrate(FlxG.save.data));
+ }
+
+ trace('[SAVE] Done loading save data.');
+ trace(FlxG.save.data);
+ }
+
+ static function fetchLegacySaveData():Null
+ {
+ trace("[SAVE] Checking for legacy save data...");
+ var legacySave:FlxSave = new FlxSave();
+ legacySave.bind(SAVE_NAME_LEGACY, SAVE_PATH_LEGACY);
+ if (legacySave?.data == null)
+ {
+ trace("[SAVE] No legacy save data found.");
+ return null;
+ }
+ else
+ {
+ trace("[SAVE] Legacy save data found.");
+ trace(legacySave.data);
+ return cast legacySave.data;
+ }
+ }
+}
+
+/**
+ * An anonymous structure containingg all the user's save data.
+ */
+typedef RawSaveData =
+{
+ // Flixel save data.
+ var volume:Float;
+ var mute:Bool;
+
+ /**
+ * A semantic versioning string for the save data format.
+ */
+ var version:Version;
+
+ var api:SaveApiData;
+
+ /**
+ * The user's saved scores.
+ */
+ var scores:SaveHighScoresData;
+
+ /**
+ * The user's preferences.
+ */
+ var options:SaveDataOptions;
+
+ var mods:SaveDataMods;
+
+ /**
+ * The user's preferences specific to the Chart Editor.
+ */
+ var optionsChartEditor:SaveDataChartEditorOptions;
+};
+
+typedef SaveApiData =
+{
+ var newgrounds:SaveApiNewgroundsData;
+}
+
+typedef SaveApiNewgroundsData =
+{
+ var sessionId:Null;
+}
+
+/**
+ * An anoymous structure containing options about the user's high scores.
+ */
+typedef SaveHighScoresData =
+{
+ /**
+ * Scores for each level (or week).
+ */
+ var levels:SaveScoreLevelsData;
+
+ /**
+ * Scores for individual songs.
+ */
+ var songs:SaveScoreSongsData;
+};
+
+typedef SaveDataMods =
+{
+ var enabledMods:Array;
+ var modOptions:Map;
+}
+
+/**
+ * Key is the level ID, value is the SaveScoreLevelData.
+ */
+typedef SaveScoreLevelsData = Map;
+
+/**
+ * Key is the song ID, value is the data for each difficulty.
+ */
+typedef SaveScoreSongsData = Map;
+
+/**
+ * Key is the difficulty ID, value is the score.
+ */
+typedef SaveScoreDifficultiesData = Map;
+
+/**
+ * An individual score. Contains the score, accuracy, and count of each judgement hit.
+ */
+typedef SaveScoreData =
+{
+ /**
+ * The score achieved.
+ */
+ var score:Int;
+
+ /**
+ * The count of each judgement hit.
+ */
+ var tallies:SaveScoreTallyData;
+
+ /**
+ * The accuracy percentage.
+ */
+ var accuracy:Float;
+}
+
+typedef SaveScoreTallyData =
+{
+ var killer:Int;
+ var sick:Int;
+ var good:Int;
+ var bad:Int;
+ var shit:Int;
+ var missed:Int;
+ var combo:Int;
+ var maxCombo:Int;
+ var totalNotesHit:Int;
+ var totalNotes:Int;
+}
+
+/**
+ * An anonymous structure containing all the user's options and preferences for the main game.
+ * Every time you add a new option, it needs to be added here.
+ */
+typedef SaveDataOptions =
+{
+ /**
+ * Whether some particularly fowl language is displayed.
+ * @default `true`
+ */
+ var naughtyness:Bool;
+
+ /**
+ * If enabled, the strumline is at the bottom of the screen rather than the top.
+ * @default `false`
+ */
+ var downscroll:Bool;
+
+ /**
+ * If disabled, flashing lights in the main menu and other areas will be less intense.
+ * @default `true`
+ */
+ var flashingLights: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 autoPause:Bool;
+
+ var controls:
+ {
+ var p1:
+ {
+ var keyboard:SaveControlsData;
+ var gamepad:SaveControlsData;
+ };
+ var p2:
+ {
+ var keyboard:SaveControlsData;
+ var gamepad:SaveControlsData;
+ };
+ };
+};
+
+/**
+ * An anonymous structure containing a specific player's bound keys.
+ * Each key is an action name and each value is an array of keycodes.
+ *
+ * If a keybind is `null`, it needs to be reinitialized to the default.
+ * If a keybind is `[]`, it is UNBOUND by the user and should not be rebound.
+ */
+typedef SaveControlsData =
+{
+ /**
+ * Keybind for navigating in the menu.
+ * @default `Up Arrow`
+ */
+ var ?UI_UP:Array;
+
+ /**
+ * Keybind for navigating in the menu.
+ * @default `Left Arrow`
+ */
+ var ?UI_LEFT:Array;
+
+ /**
+ * Keybind for navigating in the menu.
+ * @default `Right Arrow`
+ */
+ var ?UI_RIGHT:Array;
+
+ /**
+ * Keybind for navigating in the menu.
+ * @default `Down Arrow`
+ */
+ var ?UI_DOWN:Array;
+
+ /**
+ * Keybind for hitting notes.
+ * @default `A` and `Left Arrow`
+ */
+ var ?NOTE_LEFT:Array;
+
+ /**
+ * Keybind for hitting notes.
+ * @default `W` and `Up Arrow`
+ */
+ var ?NOTE_UP:Array;
+
+ /**
+ * Keybind for hitting notes.
+ * @default `S` and `Down Arrow`
+ */
+ var ?NOTE_DOWN:Array;
+
+ /**
+ * Keybind for hitting notes.
+ * @default `D` and `Right Arrow`
+ */
+ var ?NOTE_RIGHT:Array;
+
+ /**
+ * Keybind for continue/OK in menus.
+ * @default `Enter` and `Space`
+ */
+ var ?ACCEPT:Array;
+
+ /**
+ * Keybind for back/cancel in menus.
+ * @default `Escape`
+ */
+ var ?BACK:Array;
+
+ /**
+ * Keybind for pausing the game.
+ * @default `Escape`
+ */
+ var ?PAUSE:Array;
+
+ /**
+ * Keybind for advancing cutscenes.
+ * @default `Z` and `Space` and `Enter`
+ */
+ var ?CUTSCENE_ADVANCE:Array;
+
+ /**
+ * Keybind for skipping a cutscene.
+ * @default `Escape`
+ */
+ var ?CUTSCENE_SKIP:Array;
+
+ /**
+ * Keybind for increasing volume.
+ * @default `Plus`
+ */
+ var ?VOLUME_UP:Array;
+
+ /**
+ * Keybind for decreasing volume.
+ * @default `Minus`
+ */
+ var ?VOLUME_DOWN:Array;
+
+ /**
+ * Keybind for muting/unmuting volume.
+ * @default `Zero`
+ */
+ var ?VOLUME_MUTE:Array;
+
+ /**
+ * Keybind for restarting a song.
+ * @default `R`
+ */
+ var ?RESET:Array;
+}
+
+/**
+ * An anonymous structure containing all the user's options and preferences, specific to the Chart Editor.
+ */
+typedef SaveDataChartEditorOptions = {};
diff --git a/source/funkin/save/migrator/RawSaveData_v1_0_0.hx b/source/funkin/save/migrator/RawSaveData_v1_0_0.hx
new file mode 100644
index 000000000..b71102cce
--- /dev/null
+++ b/source/funkin/save/migrator/RawSaveData_v1_0_0.hx
@@ -0,0 +1,52 @@
+package funkin.save.migrator;
+
+import thx.semver.Version;
+
+typedef RawSaveData_v1_0_0 =
+{
+ var seenVideo:Bool;
+ var mute:Bool;
+ var volume:Float;
+
+ var sessionId:String;
+
+ var songCompletion:Map;
+
+ var songScores:Map;
+
+ var ?controls:
+ {
+ ?p1:SavePlayerControlsData_v1_0_0,
+ ?p2:SavePlayerControlsData_v1_0_0
+ };
+ var enabledMods:Array;
+ var weeksUnlocked:Array;
+ var windowSettings:Array;
+}
+
+typedef SavePlayerControlsData_v1_0_0 =
+{
+ var keys:SaveControlsData_v1_0_0;
+ var pad:SaveControlsData_v1_0_0;
+};
+
+typedef SaveControlsData_v1_0_0 =
+{
+ var ?ACCEPT:Array;
+ var ?BACK:Array;
+ var ?CUTSCENE_ADVANCE:Array;
+ var ?CUTSCENE_SKIP:Array;
+ var ?NOTE_DOWN:Array;
+ var ?NOTE_LEFT:Array;
+ var ?NOTE_RIGHT:Array;
+ var ?NOTE_UP:Array;
+ var ?PAUSE:Array;
+ var ?RESET:Array;
+ var ?UI_DOWN:Array;
+ var ?UI_LEFT:Array;
+ var ?UI_RIGHT:Array;
+ var ?UI_UP:Array;
+ var ?VOLUME_DOWN:Array;
+ var ?VOLUME_MUTE:Array;
+ var ?VOLUME_UP:Array;
+};
diff --git a/source/funkin/save/migrator/SaveDataMigrator.hx b/source/funkin/save/migrator/SaveDataMigrator.hx
new file mode 100644
index 000000000..e7b7c7583
--- /dev/null
+++ b/source/funkin/save/migrator/SaveDataMigrator.hx
@@ -0,0 +1,322 @@
+package funkin.save.migrator;
+
+import funkin.save.Save;
+import funkin.save.migrator.RawSaveData_v1_0_0;
+import thx.semver.Version;
+import funkin.util.VersionUtil;
+
+@:nullSafety
+class SaveDataMigrator
+{
+ /**
+ * Migrate from one 2.x version to another.
+ */
+ public static function migrate(inputData:Dynamic):Save
+ {
+ // This deserializes directly into a `Version` object, not a `String`.
+ var version:Null = inputData?.version ?? null;
+
+ if (version == null)
+ {
+ trace('[SAVE] No version found in save data! Returning blank data.');
+ trace(inputData);
+ return new Save();
+ }
+ else
+ {
+ if (VersionUtil.validateVersionStr(version, Save.SAVE_DATA_VERSION_RULE))
+ {
+ // Simply cast the structured data.
+ var save:Save = inputData;
+ return save;
+ }
+ else
+ {
+ trace('[SAVE] Invalid save data version! Returning blank data.');
+ trace(inputData);
+ return new Save();
+ }
+ }
+ }
+
+ /**
+ * Migrate from 1.x to the latest version.
+ */
+ public static function migrateFromLegacy(inputData:Dynamic):Save
+ {
+ var inputSaveData:RawSaveData_v1_0_0 = cast inputData;
+
+ var result:Save = new Save();
+
+ result.volume = inputSaveData.volume;
+ result.mute = inputSaveData.mute;
+
+ result.ngSessionId = inputSaveData.sessionId;
+
+ // TODO: Port over the save data from the legacy save data format.
+ migrateLegacyScores(result, inputSaveData);
+
+ migrateLegacyControls(result, inputSaveData);
+
+ return result;
+ }
+
+ static function migrateLegacyScores(result:Save, inputSaveData:RawSaveData_v1_0_0):Void
+ {
+ if (inputSaveData.songCompletion == null)
+ {
+ inputSaveData.songCompletion = [];
+ }
+
+ if (inputSaveData.songScores == null)
+ {
+ inputSaveData.songScores = [];
+ }
+
+ migrateLegacyLevelScore(result, inputSaveData, 'week0');
+ migrateLegacyLevelScore(result, inputSaveData, 'week1');
+ migrateLegacyLevelScore(result, inputSaveData, 'week2');
+ migrateLegacyLevelScore(result, inputSaveData, 'week3');
+ migrateLegacyLevelScore(result, inputSaveData, 'week4');
+ migrateLegacyLevelScore(result, inputSaveData, 'week5');
+ migrateLegacyLevelScore(result, inputSaveData, 'week6');
+ migrateLegacyLevelScore(result, inputSaveData, 'week7');
+
+ migrateLegacySongScore(result, inputSaveData, ['tutorial', 'Tutorial']);
+
+ migrateLegacySongScore(result, inputSaveData, ['bopeebo', 'Bopeebo']);
+ migrateLegacySongScore(result, inputSaveData, ['fresh', 'Fresh']);
+ migrateLegacySongScore(result, inputSaveData, ['dadbattle', 'Dadbattle']);
+
+ migrateLegacySongScore(result, inputSaveData, ['monster', 'Monster']);
+ migrateLegacySongScore(result, inputSaveData, ['south', 'South']);
+ migrateLegacySongScore(result, inputSaveData, ['spookeez', 'Spookeez']);
+
+ migrateLegacySongScore(result, inputSaveData, ['pico', 'Pico']);
+ migrateLegacySongScore(result, inputSaveData, ['philly-nice', 'Philly', 'philly', 'Philly-Nice']);
+ migrateLegacySongScore(result, inputSaveData, ['blammed', 'Blammed']);
+
+ migrateLegacySongScore(result, inputSaveData, ['satin-panties', 'Satin-Panties']);
+ migrateLegacySongScore(result, inputSaveData, ['high', 'High']);
+ migrateLegacySongScore(result, inputSaveData, ['milf', 'Milf', 'MILF']);
+
+ migrateLegacySongScore(result, inputSaveData, ['cocoa', 'Cocoa']);
+ migrateLegacySongScore(result, inputSaveData, ['eggnog', 'Eggnog']);
+ migrateLegacySongScore(result, inputSaveData, ['winter-horrorland', 'Winter-Horrorland']);
+
+ migrateLegacySongScore(result, inputSaveData, ['senpai', 'Senpai']);
+ migrateLegacySongScore(result, inputSaveData, ['roses', 'Roses']);
+ migrateLegacySongScore(result, inputSaveData, ['thorns', 'Thorns']);
+
+ migrateLegacySongScore(result, inputSaveData, ['ugh', 'Ugh']);
+ migrateLegacySongScore(result, inputSaveData, ['guns', 'Guns']);
+ migrateLegacySongScore(result, inputSaveData, ['stress', 'Stress']);
+ }
+
+ static function migrateLegacyLevelScore(result:Save, inputSaveData:RawSaveData_v1_0_0, levelId:String):Void
+ {
+ var scoreDataEasy:SaveScoreData =
+ {
+ score: inputSaveData.songScores.get('${levelId}-easy') ?? 0,
+ accuracy: inputSaveData.songCompletion.get('${levelId}-easy') ?? 0.0,
+ tallies:
+ {
+ killer: 0,
+ sick: 0,
+ good: 0,
+ bad: 0,
+ shit: 0,
+ missed: 0,
+ combo: 0,
+ maxCombo: 0,
+ totalNotesHit: 0,
+ totalNotes: 0,
+ }
+ };
+ result.setLevelScore(levelId, 'easy', scoreDataEasy);
+
+ var scoreDataNormal:SaveScoreData =
+ {
+ score: inputSaveData.songScores.get('${levelId}') ?? 0,
+ accuracy: inputSaveData.songCompletion.get('${levelId}') ?? 0.0,
+ tallies:
+ {
+ killer: 0,
+ sick: 0,
+ good: 0,
+ bad: 0,
+ shit: 0,
+ missed: 0,
+ combo: 0,
+ maxCombo: 0,
+ totalNotesHit: 0,
+ totalNotes: 0,
+ }
+ };
+ result.setLevelScore(levelId, 'normal', scoreDataNormal);
+
+ var scoreDataHard:SaveScoreData =
+ {
+ score: inputSaveData.songScores.get('${levelId}-hard') ?? 0,
+ accuracy: inputSaveData.songCompletion.get('${levelId}-hard') ?? 0.0,
+ tallies:
+ {
+ killer: 0,
+ sick: 0,
+ good: 0,
+ bad: 0,
+ shit: 0,
+ missed: 0,
+ combo: 0,
+ maxCombo: 0,
+ totalNotesHit: 0,
+ totalNotes: 0,
+ }
+ };
+ result.setLevelScore(levelId, 'hard', scoreDataHard);
+ }
+
+ static function migrateLegacySongScore(result:Save, inputSaveData:RawSaveData_v1_0_0, songIds:Array):Void
+ {
+ var scoreDataEasy:SaveScoreData =
+ {
+ score: 0,
+ accuracy: 0,
+ tallies:
+ {
+ killer: 0,
+ sick: 0,
+ good: 0,
+ bad: 0,
+ shit: 0,
+ missed: 0,
+ combo: 0,
+ maxCombo: 0,
+ totalNotesHit: 0,
+ totalNotes: 0,
+ }
+ };
+
+ for (songId in songIds)
+ {
+ scoreDataEasy.score = Std.int(Math.max(scoreDataEasy.score, inputSaveData.songScores.get('${songId}-easy') ?? 0));
+ scoreDataEasy.accuracy = Math.max(scoreDataEasy.accuracy, inputSaveData.songCompletion.get('${songId}-easy') ?? 0.0);
+ }
+ result.setSongScore(songIds[0], 'easy', scoreDataEasy);
+
+ var scoreDataNormal:SaveScoreData =
+ {
+ score: 0,
+ accuracy: 0,
+ tallies:
+ {
+ killer: 0,
+ sick: 0,
+ good: 0,
+ bad: 0,
+ shit: 0,
+ missed: 0,
+ combo: 0,
+ maxCombo: 0,
+ totalNotesHit: 0,
+ totalNotes: 0,
+ }
+ };
+
+ for (songId in songIds)
+ {
+ scoreDataNormal.score = Std.int(Math.max(scoreDataNormal.score, inputSaveData.songScores.get('${songId}') ?? 0));
+ scoreDataNormal.accuracy = Math.max(scoreDataNormal.accuracy, inputSaveData.songCompletion.get('${songId}') ?? 0.0);
+ }
+ result.setSongScore(songIds[0], 'normal', scoreDataNormal);
+
+ var scoreDataHard:SaveScoreData =
+ {
+ score: 0,
+ accuracy: 0,
+ tallies:
+ {
+ killer: 0,
+ sick: 0,
+ good: 0,
+ bad: 0,
+ shit: 0,
+ missed: 0,
+ combo: 0,
+ maxCombo: 0,
+ totalNotesHit: 0,
+ totalNotes: 0,
+ }
+ };
+
+ for (songId in songIds)
+ {
+ scoreDataHard.score = Std.int(Math.max(scoreDataHard.score, inputSaveData.songScores.get('${songId}-hard') ?? 0));
+ scoreDataHard.accuracy = Math.max(scoreDataHard.accuracy, inputSaveData.songCompletion.get('${songId}-hard') ?? 0.0);
+ }
+ result.setSongScore(songIds[0], 'hard', scoreDataHard);
+ }
+
+ static function migrateLegacyControls(result:Save, inputSaveData:RawSaveData_v1_0_0):Void
+ {
+ var p1Data = inputSaveData?.controls?.p1;
+ if (p1Data != null)
+ {
+ migrateLegacyPlayerControls(result, 1, p1Data);
+ }
+
+ var p2Data = inputSaveData?.controls?.p2;
+ if (p2Data != null)
+ {
+ migrateLegacyPlayerControls(result, 2, p2Data);
+ }
+ }
+
+ static function migrateLegacyPlayerControls(result:Save, playerId:Int, controlsData:SavePlayerControlsData_v1_0_0):Void
+ {
+ var outputKeyControls:SaveControlsData =
+ {
+ ACCEPT: controlsData?.keys?.ACCEPT ?? null,
+ BACK: controlsData?.keys?.BACK ?? null,
+ CUTSCENE_ADVANCE: controlsData?.keys?.CUTSCENE_ADVANCE ?? null,
+ CUTSCENE_SKIP: controlsData?.keys?.CUTSCENE_SKIP ?? null,
+ NOTE_DOWN: controlsData?.keys?.NOTE_DOWN ?? null,
+ NOTE_LEFT: controlsData?.keys?.NOTE_LEFT ?? null,
+ NOTE_RIGHT: controlsData?.keys?.NOTE_RIGHT ?? null,
+ NOTE_UP: controlsData?.keys?.NOTE_UP ?? null,
+ PAUSE: controlsData?.keys?.PAUSE ?? null,
+ RESET: controlsData?.keys?.RESET ?? null,
+ UI_DOWN: controlsData?.keys?.UI_DOWN ?? null,
+ UI_LEFT: controlsData?.keys?.UI_LEFT ?? null,
+ UI_RIGHT: controlsData?.keys?.UI_RIGHT ?? null,
+ UI_UP: controlsData?.keys?.UI_UP ?? null,
+ VOLUME_DOWN: controlsData?.keys?.VOLUME_DOWN ?? null,
+ VOLUME_MUTE: controlsData?.keys?.VOLUME_MUTE ?? null,
+ VOLUME_UP: controlsData?.keys?.VOLUME_UP ?? null,
+ };
+
+ var outputPadControls:SaveControlsData =
+ {
+ ACCEPT: controlsData?.pad?.ACCEPT ?? null,
+ BACK: controlsData?.pad?.BACK ?? null,
+ CUTSCENE_ADVANCE: controlsData?.pad?.CUTSCENE_ADVANCE ?? null,
+ CUTSCENE_SKIP: controlsData?.pad?.CUTSCENE_SKIP ?? null,
+ NOTE_DOWN: controlsData?.pad?.NOTE_DOWN ?? null,
+ NOTE_LEFT: controlsData?.pad?.NOTE_LEFT ?? null,
+ NOTE_RIGHT: controlsData?.pad?.NOTE_RIGHT ?? null,
+ NOTE_UP: controlsData?.pad?.NOTE_UP ?? null,
+ PAUSE: controlsData?.pad?.PAUSE ?? null,
+ RESET: controlsData?.pad?.RESET ?? null,
+ UI_DOWN: controlsData?.pad?.UI_DOWN ?? null,
+ UI_LEFT: controlsData?.pad?.UI_LEFT ?? null,
+ UI_RIGHT: controlsData?.pad?.UI_RIGHT ?? null,
+ UI_UP: controlsData?.pad?.UI_UP ?? null,
+ VOLUME_DOWN: controlsData?.pad?.VOLUME_DOWN ?? null,
+ VOLUME_MUTE: controlsData?.pad?.VOLUME_MUTE ?? null,
+ VOLUME_UP: controlsData?.pad?.VOLUME_UP ?? null,
+ };
+
+ result.setControls(playerId, Keys, outputKeyControls);
+ result.setControls(playerId, Gamepad(0), outputPadControls);
+ }
+}
diff --git a/source/funkin/ui/ControlsMenu.hx b/source/funkin/ui/ControlsMenu.hx
index 0d9db5b34..8197424ee 100644
--- a/source/funkin/ui/ControlsMenu.hx
+++ b/source/funkin/ui/ControlsMenu.hx
@@ -163,7 +163,17 @@ class ControlsMenu extends funkin.ui.OptionsState.Page
function onSelect():Void
{
- keyUsedToEnterPrompt = FlxG.keys.firstJustPressed();
+ switch (currentDevice)
+ {
+ case Keys:
+ {
+ keyUsedToEnterPrompt = FlxG.keys.firstJustPressed();
+ }
+ case Gamepad(id):
+ {
+ buttonUsedToEnterPrompt = FlxG.gamepads.getByID(id).firstJustPressedID();
+ }
+ }
controlGrid.enabled = false;
canExit = false;
@@ -204,6 +214,7 @@ class ControlsMenu extends funkin.ui.OptionsState.Page
}
var keyUsedToEnterPrompt:Null = null;
+ var buttonUsedToEnterPrompt:Null = null;
override function update(elapsed:Float):Void
{
@@ -246,19 +257,49 @@ class ControlsMenu extends funkin.ui.OptionsState.Page
case Gamepad(id):
{
var button = FlxG.gamepads.getByID(id).firstJustReleasedID();
- if (button != NONE && button != keyUsedToEnterPrompt)
+ if (button != NONE && button != buttonUsedToEnterPrompt)
{
if (button != BACK) onInputSelect(button);
closePrompt();
}
+
+ var key = FlxG.keys.firstJustReleased();
+ if (key != NONE && key != keyUsedToEnterPrompt)
+ {
+ if (key == ESCAPE)
+ {
+ closePrompt();
+ }
+ else if (key == BACKSPACE)
+ {
+ onInputSelect(NONE);
+ closePrompt();
+ }
+ }
}
}
}
- var keyJustReleased:Int = FlxG.keys.firstJustReleased();
- if (keyJustReleased != NONE && keyJustReleased == keyUsedToEnterPrompt)
+ switch (currentDevice)
{
- keyUsedToEnterPrompt = null;
+ case Keys:
+ {
+ var keyJustReleased:Int = FlxG.keys.firstJustReleased();
+ if (keyJustReleased != NONE && keyJustReleased == keyUsedToEnterPrompt)
+ {
+ keyUsedToEnterPrompt = null;
+ }
+ buttonUsedToEnterPrompt = null;
+ }
+ case Gamepad(id):
+ {
+ var buttonJustReleased:Int = FlxG.gamepads.getByID(id).firstJustReleasedID();
+ if (buttonJustReleased != NONE && buttonJustReleased == buttonUsedToEnterPrompt)
+ {
+ buttonUsedToEnterPrompt = null;
+ }
+ keyUsedToEnterPrompt = null;
+ }
}
}
diff --git a/source/funkin/ui/PreferencesMenu.hx b/source/funkin/ui/PreferencesMenu.hx
index 4fa8f7f5b..812d0ab49 100644
--- a/source/funkin/ui/PreferencesMenu.hx
+++ b/source/funkin/ui/PreferencesMenu.hx
@@ -3,17 +3,16 @@ package funkin.ui;
import flixel.FlxCamera;
import flixel.FlxObject;
import flixel.FlxSprite;
+import flixel.group.FlxSpriteGroup.FlxTypedSpriteGroup;
import funkin.ui.AtlasText.AtlasFont;
import funkin.ui.OptionsState.Page;
import funkin.ui.TextMenuList.TextMenuItem;
class PreferencesMenu extends Page
{
- public static var preferences:Map = new Map();
-
var items:TextMenuList;
+ var preferenceItems:FlxTypedSpriteGroup;
- var checkboxes:Array = [];
var menuCamera:FlxCamera;
var camFollow:FlxObject;
@@ -27,13 +26,9 @@ class PreferencesMenu extends Page
camera = menuCamera;
add(items = new TextMenuList());
+ add(preferenceItems = new FlxTypedSpriteGroup());
- createPrefItem('naughtyness', 'censor-naughty', true);
- createPrefItem('downscroll', 'downscroll', false);
- createPrefItem('flashing menu', 'flashing-menu', true);
- createPrefItem('Camera Zooming on Beat', 'camera-zoom', true);
- createPrefItem('FPS Counter', 'fps-counter', true);
- createPrefItem('Auto Pause', 'auto-pause', false);
+ createPrefItems();
camFollow = new FlxObject(FlxG.width / 2, 0, 140, 70);
if (items != null) camFollow.y = items.selectedItem.y;
@@ -48,128 +43,63 @@ class PreferencesMenu extends Page
});
}
- public static function getPref(pref:String):Dynamic
+ /**
+ * Create the menu items for each of the preferences.
+ */
+ function createPrefItems():Void
{
- return preferences.get(pref);
+ createPrefItemCheckbox('Naughtyness', 'Toggle displaying raunchy content', function(value:Bool):Void {
+ Preferences.naughtyness = value;
+ }, Preferences.naughtyness);
+ createPrefItemCheckbox('Downscroll', 'Enable to make notes move downwards', function(value:Bool):Void {
+ Preferences.downscroll = value;
+ }, Preferences.downscroll);
+ createPrefItemCheckbox('Flashing Lights', 'Disable to dampen flashing effects', function(value:Bool):Void {
+ Preferences.flashingLights = value;
+ }, Preferences.flashingLights);
+ createPrefItemCheckbox('Camera Zooming on Beat', 'Disable to stop the camera bouncing to the song', function(value:Bool):Void {
+ Preferences.zoomCamera = value;
+ }, Preferences.zoomCamera);
+ createPrefItemCheckbox('Debug Display', 'Enable to show FPS and other debug stats', function(value:Bool):Void {
+ Preferences.debugDisplay = value;
+ }, Preferences.debugDisplay);
+ createPrefItemCheckbox('Auto Pause', 'Automatically pause the game when it loses focus', function(value:Bool):Void {
+ Preferences.autoPause = value;
+ }, Preferences.autoPause);
}
- // easy shorthand?
- public static function setPref(pref:String, value:Dynamic):Void
+ function createPrefItemCheckbox(prefName:String, prefDesc:String, onChange:Bool->Void, defaultValue:Bool):Void
{
- preferences.set(pref, value);
- }
+ var checkbox:CheckboxPreferenceItem = new CheckboxPreferenceItem(0, 120 * (items.length - 1 + 1), defaultValue);
- public static function initPrefs():Void
- {
- preferenceCheck('censor-naughty', true);
- preferenceCheck('downscroll', false);
- preferenceCheck('flashing-menu', true);
- preferenceCheck('camera-zoom', true);
- preferenceCheck('fps-counter', true);
- preferenceCheck('auto-pause', false);
- preferenceCheck('master-volume', 1);
-
- #if muted
- setPref('master-volume', 0);
- FlxG.sound.muted = true;
- #end
-
- if (!getPref('fps-counter')) FlxG.stage.removeChild(Main.fpsCounter);
-
- FlxG.autoPause = getPref('auto-pause');
- }
-
- function createPrefItem(prefName:String, prefString:String, prefValue:Dynamic):Void
- {
items.createItem(120, (120 * items.length) + 30, prefName, AtlasFont.BOLD, function() {
- preferenceCheck(prefString, prefValue);
-
- switch (Type.typeof(prefValue).getName())
- {
- case 'TBool':
- prefToggle(prefString);
-
- default:
- trace('swag');
- }
+ var value = !checkbox.currentValue;
+ onChange(value);
+ checkbox.currentValue = value;
});
- switch (Type.typeof(prefValue).getName())
- {
- case 'TBool':
- createCheckbox(prefString);
-
- default:
- trace('swag');
- }
-
- trace(Type.typeof(prefValue).getName());
- }
-
- function createCheckbox(prefString:String)
- {
- var checkbox:CheckboxThingie = new CheckboxThingie(0, 120 * (items.length - 1), preferences.get(prefString));
- checkboxes.push(checkbox);
- add(checkbox);
- }
-
- /**
- * Assumes that the preference has already been checked/set?
- */
- function prefToggle(prefName:String)
- {
- var daSwap:Bool = preferences.get(prefName);
- daSwap = !daSwap;
- preferences.set(prefName, daSwap);
- checkboxes[items.selectedIndex].daValue = daSwap;
- trace('toggled? ' + preferences.get(prefName));
-
- switch (prefName)
- {
- case 'fps-counter':
- if (getPref('fps-counter')) FlxG.stage.addChild(Main.fpsCounter);
- else
- FlxG.stage.removeChild(Main.fpsCounter);
- case 'auto-pause':
- FlxG.autoPause = getPref('auto-pause');
- }
-
- if (prefName == 'fps-counter') {}
+ preferenceItems.add(checkbox);
}
override function update(elapsed:Float)
{
super.update(elapsed);
- // menuCamera.followLerp = CoolUtil.camLerpShit(0.05);
-
+ // Indent the selected item.
+ // TODO: Only do this on menu change?
items.forEach(function(daItem:TextMenuItem) {
if (items.selectedItem == daItem) daItem.x = 150;
else
daItem.x = 120;
});
}
-
- static function preferenceCheck(prefString:String, defaultValue:Dynamic):Void
- {
- if (preferences.get(prefString) == null)
- {
- // Set the value to default.
- preferences.set(prefString, defaultValue);
- trace('Set preference to default: ${prefString} = ${defaultValue}');
- }
- else
- {
- trace('Found preference: ${prefString} = ${preferences.get(prefString)}');
- }
- }
}
-class CheckboxThingie extends FlxSprite
+class CheckboxPreferenceItem extends FlxSprite
{
- public var daValue(default, set):Bool;
+ public var currentValue(default, set):Bool;
- public function new(x:Float, y:Float, daValue:Bool = false)
+ public function new(x:Float, y:Float, defaultValue:Bool = false)
{
super(x, y);
@@ -180,7 +110,7 @@ class CheckboxThingie extends FlxSprite
setGraphicSize(Std.int(width * 0.7));
updateHitbox();
- this.daValue = daValue;
+ this.currentValue = defaultValue;
}
override function update(elapsed:Float)
@@ -196,12 +126,17 @@ class CheckboxThingie extends FlxSprite
}
}
- function set_daValue(value:Bool):Bool
+ function set_currentValue(value:Bool):Bool
{
- if (value) animation.play('checked', true);
+ if (value)
+ {
+ animation.play('checked', true);
+ }
else
+ {
animation.play('static');
+ }
- return value;
+ return currentValue = value;
}
}
diff --git a/source/funkin/ui/StickerSubState.hx b/source/funkin/ui/StickerSubState.hx
index bde36b160..a4e3a6acb 100644
--- a/source/funkin/ui/StickerSubState.hx
+++ b/source/funkin/ui/StickerSubState.hx
@@ -122,7 +122,7 @@ class StickerSubState extends MusicBeatSubState
var daSound:String = FlxG.random.getObject(sounds);
FlxG.sound.play(Paths.sound(daSound));
- if (ind == grpStickers.members.length - 1)
+ if (grpStickers == null || ind == grpStickers.members.length - 1)
{
switchingState = false;
close();
@@ -206,6 +206,8 @@ class StickerSubState extends MusicBeatSubState
sticker.timing = FlxMath.remapToRange(ind, 0, grpStickers.members.length, 0, 0.9);
new FlxTimer().start(sticker.timing, _ -> {
+ if (grpStickers == null) return;
+
sticker.visible = true;
var daSound:String = FlxG.random.getObject(sounds);
FlxG.sound.play(Paths.sound(daSound));
@@ -269,10 +271,10 @@ class StickerSubState extends MusicBeatSubState
{
super.update(elapsed);
- if (FlxG.keys.justPressed.ANY)
- {
- regenStickers();
- }
+ // if (FlxG.keys.justPressed.ANY)
+ // {
+ // regenStickers();
+ // }
}
var switchingState:Bool = false;
diff --git a/source/funkin/ui/TextMenuList.hx b/source/funkin/ui/TextMenuList.hx
index 0c9f9eb8b..521f46faf 100644
--- a/source/funkin/ui/TextMenuList.hx
+++ b/source/funkin/ui/TextMenuList.hx
@@ -10,7 +10,7 @@ class TextMenuList extends MenuTypedList
super(navControls, wrapMode);
}
- public function createItem(x = 0.0, y = 0.0, name:String, font:AtlasFont = BOLD, callback, fireInstantly = false)
+ public function createItem(x = 0.0, y = 0.0, name:String, font:AtlasFont = BOLD, ?callback:Void->Void, fireInstantly = false)
{
var item = new TextMenuItem(x, y, name, font, callback);
item.fireInstantly = fireInstantly;
@@ -20,7 +20,7 @@ class TextMenuList extends MenuTypedList
class TextMenuItem extends TextTypedMenuItem
{
- public function new(x = 0.0, y = 0.0, name:String, font:AtlasFont = BOLD, callback)
+ public function new(x = 0.0, y = 0.0, name:String, font:AtlasFont = BOLD, ?callback:Void->Void)
{
super(x, y, new AtlasText(0, 0, name, font), name, callback);
setEmptyBackground();
@@ -29,7 +29,7 @@ class TextMenuItem extends TextTypedMenuItem
class TextTypedMenuItem extends MenuTypedItem
{
- public function new(x = 0.0, y = 0.0, label:T, name:String, callback)
+ public function new(x = 0.0, y = 0.0, label:T, name:String, ?callback:Void->Void)
{
super(x, y, label, name, callback);
}
diff --git a/source/funkin/ui/debug/charting/ChartEditorAudioHandler.hx b/source/funkin/ui/debug/charting/ChartEditorAudioHandler.hx
index e852dff0a..b5a6f36be 100644
--- a/source/funkin/ui/debug/charting/ChartEditorAudioHandler.hx
+++ b/source/funkin/ui/debug/charting/ChartEditorAudioHandler.hx
@@ -1,11 +1,14 @@
package funkin.ui.debug.charting;
-import openfl.utils.Assets;
import flixel.system.FlxAssets.FlxSoundAsset;
import flixel.system.FlxSound;
-import funkin.play.character.BaseCharacter.CharacterType;
import flixel.system.FlxSound;
+import funkin.audio.VoicesGroup;
+import funkin.play.character.BaseCharacter.CharacterType;
+import funkin.util.FileUtil;
+import haxe.io.Bytes;
import haxe.io.Path;
+import openfl.utils.Assets;
/**
* Functions for loading audio for the chart editor.
@@ -17,16 +20,18 @@ import haxe.io.Path;
class ChartEditorAudioHandler
{
/**
- * Loads a vocal track from an absolute file path.
+ * Loads and stores byte data for a vocal track from an absolute file path
+ *
* @param path The absolute path to the audio file.
- * @param charKey The character to load the vocal track for.
+ * @param charId The character this vocal track will be for.
+ * @param instId The instrumental this vocal track will be for.
* @return Success or failure.
*/
- static function loadVocalsFromPath(state:ChartEditorState, path:Path, charKey:String = 'default'):Bool
+ static function loadVocalsFromPath(state:ChartEditorState, path:Path, charId:String, instId:String = ''):Bool
{
#if sys
- var fileBytes:haxe.io.Bytes = sys.io.File.getBytes(path.toString());
- return loadVocalsFromBytes(state, fileBytes, charKey);
+ var fileBytes:Bytes = sys.io.File.getBytes(path.toString());
+ return loadVocalsFromBytes(state, fileBytes, charId, instId);
#else
trace("[WARN] This platform can't load audio from a file path, you'll need to fetch the bytes some other way.");
return false;
@@ -34,137 +39,235 @@ class ChartEditorAudioHandler
}
/**
- * Load a vocal track for a given song and character and add it to the voices group.
+ * Loads and stores byte data for a vocal track from an asset
*
- * @param path ID of the asset.
- * @param charKey Character to load the vocal track for.
+ * @param path The path to the asset. Use `Paths` to build this.
+ * @param charId The character this vocal track will be for.
+ * @param instId The instrumental this vocal track will be for.
* @return Success or failure.
*/
- static function loadVocalsFromAsset(state:ChartEditorState, path:String, charType:CharacterType = OTHER):Bool
+ static function loadVocalsFromAsset(state:ChartEditorState, path:String, charId:String, instId:String = ''):Bool
{
- var vocalTrack:FlxSound = FlxG.sound.load(path, 1.0, false);
+ var trackData:Null = Assets.getBytes(path);
+ if (trackData != null)
+ {
+ return loadVocalsFromBytes(state, trackData, charId, instId);
+ }
+ return false;
+ }
+
+ /**
+ * Loads and stores byte data for a vocal track
+ *
+ * @param bytes The audio byte data.
+ * @param charId The character this vocal track will be for.
+ * @param instId The instrumental this vocal track will be for.
+ */
+ static function loadVocalsFromBytes(state:ChartEditorState, bytes:Bytes, charId:String, instId:String = ''):Bool
+ {
+ var trackId:String = '${charId}${instId == '' ? '' : '-${instId}'}';
+ state.audioVocalTrackData.set(trackId, bytes);
+ return true;
+ }
+
+ /**
+ * Loads and stores byte data for an instrumental track from an absolute file path
+ *
+ * @param path The absolute path to the audio file.
+ * @param instId The instrumental this vocal track will be for.
+ * @return Success or failure.
+ */
+ static function loadInstFromPath(state:ChartEditorState, path:Path, instId:String = ''):Bool
+ {
+ #if sys
+ var fileBytes:Bytes = sys.io.File.getBytes(path.toString());
+ return loadInstFromBytes(state, fileBytes, instId);
+ #else
+ trace("[WARN] This platform can't load audio from a file path, you'll need to fetch the bytes some other way.");
+ return false;
+ #end
+ }
+
+ /**
+ * Loads and stores byte data for an instrumental track from an asset
+ *
+ * @param path The path to the asset. Use `Paths` to build this.
+ * @param instId The instrumental this vocal track will be for.
+ * @return Success or failure.
+ */
+ static function loadInstFromAsset(state:ChartEditorState, path:String, instId:String = ''):Bool
+ {
+ var trackData:Null = Assets.getBytes(path);
+ if (trackData != null)
+ {
+ return loadInstFromBytes(state, trackData, instId);
+ }
+ return false;
+ }
+
+ /**
+ * Loads and stores byte data for a vocal track
+ *
+ * @param bytes The audio byte data.
+ * @param charId The character this vocal track will be for.
+ * @param instId The instrumental this vocal track will be for.
+ */
+ static function loadInstFromBytes(state:ChartEditorState, bytes:Bytes, instId:String = ''):Bool
+ {
+ if (instId == '') instId = 'default';
+ state.audioInstTrackData.set(instId, bytes);
+ return true;
+ }
+
+ public static function switchToInstrumental(state:ChartEditorState, instId:String = '', playerId:String, opponentId:String):Bool
+ {
+ var result:Bool = playInstrumental(state, instId);
+ if (!result) return false;
+
+ stopExistingVocals(state);
+ result = playVocals(state, BF, playerId, instId);
+ if (!result) return false;
+ result = playVocals(state, DAD, opponentId, instId);
+ if (!result) return false;
+
+ return true;
+ }
+
+ /**
+ * Tell the Chart Editor to select a specific instrumental track, that is already loaded.
+ */
+ static function playInstrumental(state:ChartEditorState, instId:String = ''):Bool
+ {
+ if (instId == '') instId = 'default';
+ var instTrackData:Null = state.audioInstTrackData.get(instId);
+ var instTrack:Null = buildFlxSoundFromBytes(instTrackData);
+ if (instTrack == null) return false;
+
+ stopExistingInstrumental(state);
+ state.audioInstTrack = instTrack;
+ state.postLoadInstrumental();
+ return true;
+ }
+
+ static function stopExistingInstrumental(state:ChartEditorState):Void
+ {
+ if (state.audioInstTrack != null)
+ {
+ state.audioInstTrack.stop();
+ state.audioInstTrack.destroy();
+ state.audioInstTrack = null;
+ }
+ }
+
+ /**
+ * Tell the Chart Editor to select a specific vocal track, that is already loaded.
+ */
+ static function playVocals(state:ChartEditorState, charType:CharacterType, charId:String, instId:String = ''):Bool
+ {
+ var trackId:String = '${charId}${instId == '' ? '' : '-${instId}'}';
+ var vocalTrackData:Null = state.audioVocalTrackData.get(trackId);
+ var vocalTrack:Null = buildFlxSoundFromBytes(vocalTrackData);
+
+ if (state.audioVocalTrackGroup == null) state.audioVocalTrackGroup = new VoicesGroup();
+
if (vocalTrack != null)
{
switch (charType)
{
- case CharacterType.BF:
- if (state.audioVocalTrackGroup != null) state.audioVocalTrackGroup.addPlayerVoice(vocalTrack);
- state.audioVocalTrackData.set(state.currentSongCharacterPlayer, Assets.getBytes(path));
- case CharacterType.DAD:
- if (state.audioVocalTrackGroup != null) state.audioVocalTrackGroup.addOpponentVoice(vocalTrack);
- state.audioVocalTrackData.set(state.currentSongCharacterOpponent, Assets.getBytes(path));
+ case BF:
+ state.audioVocalTrackGroup.addPlayerVoice(vocalTrack);
+ return true;
+ case DAD:
+ state.audioVocalTrackGroup.addOpponentVoice(vocalTrack);
+ return true;
+ case OTHER:
+ state.audioVocalTrackGroup.add(vocalTrack);
+ return true;
default:
- if (state.audioVocalTrackGroup != null) state.audioVocalTrackGroup.add(vocalTrack);
- state.audioVocalTrackData.set('default', Assets.getBytes(path));
+ // Do nothing.
}
-
- return true;
}
return false;
}
- /**
- * Loads a vocal track from audio byte data.
- */
- static function loadVocalsFromBytes(state:ChartEditorState, bytes:haxe.io.Bytes, charKey:String = ''):Bool
+ static function stopExistingVocals(state:ChartEditorState):Void
{
- var openflSound:openfl.media.Sound = new openfl.media.Sound();
- openflSound.loadCompressedDataFromByteArray(openfl.utils.ByteArray.fromBytes(bytes), bytes.length);
- var vocalTrack:FlxSound = FlxG.sound.load(openflSound, 1.0, false);
- if (state.audioVocalTrackGroup != null) state.audioVocalTrackGroup.add(vocalTrack);
- state.audioVocalTrackData.set(charKey, bytes);
- return true;
- }
-
- /**
- * Loads an instrumental from an absolute file path, replacing the current instrumental.
- *
- * @param path The absolute path to the audio file.
- *
- * @return Success or failure.
- */
- static function loadInstrumentalFromPath(state:ChartEditorState, path:Path):Bool
- {
- #if sys
- // Validate file extension.
- if (path.ext != null && !ChartEditorState.SUPPORTED_MUSIC_FORMATS.contains(path.ext))
+ if (state.audioVocalTrackGroup != null)
{
- return false;
+ state.audioVocalTrackGroup.clear();
}
-
- var fileBytes:haxe.io.Bytes = sys.io.File.getBytes(path.toString());
- return loadInstrumentalFromBytes(state, fileBytes, '${path.file}.${path.ext}');
- #else
- trace("[WARN] This platform can't load audio from a file path, you'll need to fetch the bytes some other way.");
- return false;
- #end
- }
-
- /**
- * Loads an instrumental from audio byte data, replacing the current instrumental.
- * @param bytes The audio byte data.
- * @param fileName The name of the file, if available. Used for notifications.
- * @return Success or failure.
- */
- static function loadInstrumentalFromBytes(state:ChartEditorState, bytes:haxe.io.Bytes, fileName:String = null):Bool
- {
- if (bytes == null)
- {
- return false;
- }
-
- var openflSound:openfl.media.Sound = new openfl.media.Sound();
- openflSound.loadCompressedDataFromByteArray(openfl.utils.ByteArray.fromBytes(bytes), bytes.length);
- state.audioInstTrack = FlxG.sound.load(openflSound, 1.0, false);
- state.audioInstTrack.autoDestroy = false;
- state.audioInstTrack.pause();
-
- state.audioInstTrackData = bytes;
-
- state.postLoadInstrumental();
-
- return true;
- }
-
- /**
- * Loads an instrumental from an OpenFL asset, replacing the current instrumental.
- * @param path The path to the asset. Use `Paths` to build this.
- * @return Success or failure.
- */
- static function loadInstrumentalFromAsset(state:ChartEditorState, path:String):Bool
- {
- var instTrack:FlxSound = FlxG.sound.load(path, 1.0, false);
- if (instTrack != null)
- {
- state.audioInstTrack = instTrack;
-
- state.audioInstTrackData = Assets.getBytes(path);
-
- state.postLoadInstrumental();
- return true;
- }
-
- return false;
}
/**
* Play a sound effect.
* Automatically cleans up after itself and recycles previous FlxSound instances if available, for performance.
+ * @param path The path to the sound effect. Use `Paths` to build this.
*/
public static function playSound(path:String):Void
{
var snd:FlxSound = FlxG.sound.list.recycle(FlxSound) ?? new FlxSound();
-
var asset:Null = FlxG.sound.cache(path);
if (asset == null)
{
trace('WARN: Failed to play sound $path, asset not found.');
return;
}
-
snd.loadEmbedded(asset);
snd.autoDestroy = true;
FlxG.sound.list.add(snd);
snd.play();
}
+
+ /**
+ * Convert byte data into a playable sound.
+ *
+ * @param input The byte data.
+ * @return The playable sound, or `null` if loading failed.
+ */
+ public static function buildFlxSoundFromBytes(input:Null):Null
+ {
+ if (input == null) return null;
+
+ var openflSound:openfl.media.Sound = new openfl.media.Sound();
+ openflSound.loadCompressedDataFromByteArray(openfl.utils.ByteArray.fromBytes(input), input.length);
+ var output:FlxSound = FlxG.sound.load(openflSound, 1.0, false);
+ return output;
+ }
+
+ static function makeZIPEntriesFromInstrumentals(state:ChartEditorState):Array
+ {
+ var zipEntries = [];
+
+ for (key in state.audioInstTrackData.keys())
+ {
+ if (key == 'default')
+ {
+ var data:Null = state.audioInstTrackData.get('default');
+ if (data == null) continue;
+ zipEntries.push(FileUtil.makeZIPEntryFromBytes('Inst.ogg', data));
+ }
+ else
+ {
+ var data:Null = state.audioInstTrackData.get(key);
+ if (data == null) continue;
+ zipEntries.push(FileUtil.makeZIPEntryFromBytes('Inst-${key}.ogg', data));
+ }
+ }
+
+ return zipEntries;
+ }
+
+ static function makeZIPEntriesFromVocals(state:ChartEditorState):Array
+ {
+ var zipEntries = [];
+
+ for (key in state.audioVocalTrackData.keys())
+ {
+ var data:Null = state.audioVocalTrackData.get(key);
+ if (data == null) continue;
+ zipEntries.push(FileUtil.makeZIPEntryFromBytes('Vocals-${key}.ogg', data));
+ }
+
+ return zipEntries;
+ }
}
diff --git a/source/funkin/ui/debug/charting/ChartEditorCommand.hx b/source/funkin/ui/debug/charting/ChartEditorCommand.hx
index e6caf61e7..1014e67c2 100644
--- a/source/funkin/ui/debug/charting/ChartEditorCommand.hx
+++ b/source/funkin/ui/debug/charting/ChartEditorCommand.hx
@@ -1,5 +1,7 @@
package funkin.ui.debug.charting;
+import haxe.ui.notifications.NotificationType;
+import haxe.ui.notifications.NotificationManager;
import funkin.data.song.SongData.SongEventData;
import funkin.data.song.SongData.SongNoteData;
import funkin.data.song.SongDataUtils;
@@ -762,6 +764,22 @@ class PasteItemsCommand implements ChartEditorCommand
{
var currentClipboard:SongClipboardItems = SongDataUtils.readItemsFromClipboard();
+ if (currentClipboard.valid != true)
+ {
+ #if !mac
+ NotificationManager.instance.addNotification(
+ {
+ title: 'Failed to Paste',
+ body: 'Could not parse clipboard contents.',
+ type: NotificationType.Error,
+ expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME
+ });
+ #end
+ return;
+ }
+
+ trace(currentClipboard.notes);
+
addedNotes = SongDataUtils.offsetSongNoteData(currentClipboard.notes, Std.int(targetTimestamp));
addedEvents = SongDataUtils.offsetSongEventData(currentClipboard.events, Std.int(targetTimestamp));
@@ -775,6 +793,16 @@ class PasteItemsCommand implements ChartEditorCommand
state.notePreviewDirty = true;
state.sortChartData();
+
+ #if !mac
+ NotificationManager.instance.addNotification(
+ {
+ title: 'Paste Successful',
+ body: 'Successfully pasted clipboard contents.',
+ type: NotificationType.Success,
+ expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME
+ });
+ #end
}
public function undo(state:ChartEditorState):Void
diff --git a/source/funkin/ui/debug/charting/ChartEditorDialogHandler.hx b/source/funkin/ui/debug/charting/ChartEditorDialogHandler.hx
index 736851d16..91576f2ee 100644
--- a/source/funkin/ui/debug/charting/ChartEditorDialogHandler.hx
+++ b/source/funkin/ui/debug/charting/ChartEditorDialogHandler.hx
@@ -83,7 +83,7 @@ class ChartEditorDialogHandler
var dialog:Null