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/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 ccb89a7ca..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) @@ -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; @@ -651,28 +603,31 @@ class FreeplayState extends MusicBeatSubState if (FlxG.keys.justPressed.F) { - var realShit = curSelected; - songs[curSelected].isFav = !songs[curSelected].isFav; - if (songs[curSelected].isFav) + if (songs[curSelected] != null) { - FlxTween.tween(grpCapsules.members[realShit], {angle: 360}, 0.4, - { - ease: FlxEase.elasticOut, - onComplete: _ -> { - grpCapsules.members[realShit].favIcon.visible = true; - grpCapsules.members[realShit].favIcon.animation.play("fav"); - } + var realShit = curSelected; + songs[curSelected].isFav = !songs[curSelected].isFav; + if (songs[curSelected].isFav) + { + FlxTween.tween(grpCapsules.members[realShit], {angle: 360}, 0.4, + { + ease: FlxEase.elasticOut, + onComplete: _ -> { + grpCapsules.members[realShit].favIcon.visible = true; + grpCapsules.members[realShit].favIcon.animation.play("fav"); + } + }); + } + else + { + grpCapsules.members[realShit].favIcon.animation.play('fav', false, true); + new FlxTimer().start((1 / 24) * 14, _ -> { + grpCapsules.members[realShit].favIcon.visible = false; }); - } - else - { - grpCapsules.members[realShit].favIcon.animation.play('fav', false, true); - new FlxTimer().start((1 / 24) * 14, _ -> { - grpCapsules.members[realShit].favIcon.visible = false; - }); - new FlxTimer().start((1 / 24) * 24, _ -> { - FlxTween.tween(grpCapsules.members[realShit], {angle: 0}, 0.4, {ease: FlxEase.elasticOut}); - }); + new FlxTimer().start((1 / 24) * 24, _ -> { + FlxTween.tween(grpCapsules.members[realShit], {angle: 0}, 0.4, {ease: FlxEase.elasticOut}); + }); + } } } @@ -722,16 +677,7 @@ class FreeplayState extends MusicBeatSubState FlxG.watch.addQuick("LENGTH", length); FlxG.watch.addQuick("ANGLE", Math.round(FlxAngle.asDegrees(angle))); - // trace("ANGLE", Math.round(FlxAngle.asDegrees(angle))); } - - /* switch (inputID) - { - case FlxObject.UP: - return - case FlxObject.DOWN: - } - */ } if (FlxG.touches.getFirst() != null) @@ -767,7 +713,6 @@ class FreeplayState extends MusicBeatSubState touchY = touch.screenY; if (dyTouch != 0) dyTouch < 0 ? changeSelection(1) : changeSelection(-1); - // changeSelection(1); } } else @@ -845,8 +790,6 @@ class FreeplayState extends MusicBeatSubState FlxG.sound.play(Paths.sound('cancelMenu')); - // FlxTween.tween(dj, {x: -dj.width}, 0.2, {ease: FlxEase.quartOut}); - var longestTimer:Float = 0; for (grpSpr in exitMovers.keys()) @@ -892,15 +835,11 @@ class FreeplayState extends MusicBeatSubState { FlxG.switchState(new MainMenuState()); } - // - // close(); }); } if (accepted) { - // if (Assets.exists()) - grpCapsules.members[curSelected].onConfirm(); } } @@ -908,7 +847,11 @@ class FreeplayState extends MusicBeatSubState @:haxe.warning("-WDeprecated") override function switchTo(nextState:FlxState):Bool { - clearDaCache(songs[curSelected].songName); + var daSong = songs[curSelected]; + if (daSong != null) + { + clearDaCache(daSong.songName); + } return super.switchTo(nextState); } @@ -921,9 +864,29 @@ class FreeplayState extends MusicBeatSubState if (curDifficulty < 0) curDifficulty = 2; if (curDifficulty > 2) curDifficulty = 0; - // intendedScore = Highscore.getScore(songs[curSelected].songName, curDifficulty); - intendedScore = Highscore.getScore(songs[curSelected].songName, curDifficulty); - intendedCompletion = Highscore.getCompletion(songs[curSelected].songName, curDifficulty); + var targetDifficulty:String = switch (curDifficulty) + { + case 0: + 'easy'; + case 1: + 'normal'; + case 2: + 'hard'; + default: 'normal'; + }; + + var daSong = songs[curSelected]; + if (daSong != null) + { + var songScore:SaveScoreData = Save.get().getSongScore(songs[curSelected].songId, targetDifficulty); + intendedScore = songScore?.score ?? 0; + intendedCompletion = songScore?.accuracy ?? 0.0; + } + else + { + intendedScore = 0; + intendedCompletion = 0.0; + } grpDifficulties.group.forEach(function(spr) { spr.visible = false; @@ -945,6 +908,7 @@ class FreeplayState extends MusicBeatSubState { for (song in songs) { + if (song == null) continue; if (song.songName != actualSongTho) { trace('trying to remove: ' + song.songName); @@ -962,18 +926,6 @@ class FreeplayState extends MusicBeatSubState function capsuleOnConfirmDefault(cap:SongMenuItem):Void { - // var poop:String = songs[curSelected].songName.toLowerCase(); - - // does not work properly, always just accidentally sets it to normal anyways! - /* if (!Assets.exists(Paths.json(songs[curSelected].songName + '/' + poop))) - { - // defaults to normal if HARD / EASY doesn't exist - // does not account if NORMAL doesn't exist! - FlxG.log.warn("CURRENT DIFFICULTY IS NOT CHARTED, DEFAULTING TO NORMAL!"); - poop = Highscore.formatSong(songs[curSelected].songName.toLowerCase(), 1); - curDifficulty = 1; - }*/ - busy = true; PlayStatePlaylist.isStoryMode = false; @@ -1015,6 +967,7 @@ class FreeplayState extends MusicBeatSubState targetSong.cacheCharts(true); new FlxTimer().start(1, function(tmr:FlxTimer) { + Paths.setCurrentLevel(songs[curSelected].levelId); LoadingState.loadAndSwitchState(new PlayState( { targetSong: targetSong, @@ -1026,8 +979,6 @@ class FreeplayState extends MusicBeatSubState function changeSelection(change:Int = 0) { - // fp.updateScore(12345); - // NGio.logEvent('Fresh'); FlxG.sound.play(Paths.sound('scrollMenu'), 0.4); // FlxG.sound.playMusic(Paths.inst(songs[curSelected].songName)); @@ -1037,27 +988,30 @@ class FreeplayState extends MusicBeatSubState if (curSelected < 0) curSelected = grpCapsules.countLiving() - 1; if (curSelected >= grpCapsules.countLiving()) curSelected = 0; - // selector.y = (70 * curSelected) + 30; - - // intendedScore = Highscore.getScore(songs[curSelected].songName, curDifficulty); - - if (songs[curSelected] != null) + var targetDifficulty:String = switch (curDifficulty) { - intendedScore = Highscore.getScore(songs[curSelected].songName, curDifficulty); - intendedCompletion = Highscore.getCompletion(songs[curSelected].songName, curDifficulty); + case 0: + 'easy'; + case 1: + 'normal'; + case 2: + 'hard'; + default: 'normal'; + }; + + var daSong = songs[curSelected]; + if (daSong != null) + { + var songScore:SaveScoreData = Save.get().getSongScore(daSong.songId, targetDifficulty); + intendedScore = songScore?.score ?? 0; + intendedCompletion = songScore?.accuracy ?? 0.0; } else { intendedScore = 0; - intendedCompletion = 0; + intendedCompletion = 0.0; } - // lerpScore = 0; - - #if PRELOAD_ALL - // FlxG.sound.playMusic(Paths.inst(songs[curSelected].songName), 0); - #end - for (index => capsule in grpCapsules.members) { index += 1; @@ -1066,7 +1020,6 @@ class FreeplayState extends MusicBeatSubState capsule.targetPos.y = capsule.intendedY(index - curSelected); capsule.targetPos.x = 270 + (60 * (Math.sin(index - curSelected))); - // capsule.targetPos.x = 320 + (40 * (index - curSelected)); if (index < curSelected) capsule.targetPos.y -= 100; // another 100 for good measure } @@ -1145,14 +1098,16 @@ enum abstract FilterType(String) class FreeplaySongData { + public var songId:String = ""; public var songName:String = ""; public var levelId:String = ""; public var songCharacter:String = ""; public var isFav:Bool = false; - public function new(song:String, levelId:String, songCharacter:String, isFav:Bool = false) + public function new(songId:String, songName:String, levelId:String, songCharacter:String, isFav:Bool = false) { - this.songName = song; + this.songId = songId; + this.songName = songName; this.levelId = levelId; this.songCharacter = songCharacter; this.isFav = isFav; 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 54c3a530b..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') { @@ -201,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; @@ -247,6 +242,30 @@ class PauseSubState extends MusicBeatSubState 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/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/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 cb350f028..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. @@ -295,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 e3aabc7a7..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]; @@ -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 3a34144bf..5346ced61 100644 --- a/source/funkin/play/character/BaseCharacter.hx +++ b/source/funkin/play/character/BaseCharacter.hx @@ -585,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/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 ebf75cb34..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(); 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/ChartEditorCommand.hx b/source/funkin/ui/debug/charting/ChartEditorCommand.hx index c358c1d3d..3328336e6 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; @@ -760,6 +762,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)); @@ -773,6 +791,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 30f0381c6..91576f2ee 100644 --- a/source/funkin/ui/debug/charting/ChartEditorDialogHandler.hx +++ b/source/funkin/ui/debug/charting/ChartEditorDialogHandler.hx @@ -667,8 +667,6 @@ class ChartEditorDialogHandler timeChanges[0].bpm = event.value; } - Conductor.forceBPM(event.value); - newSongMetadata.timeChanges = timeChanges; }; @@ -677,6 +675,8 @@ class ChartEditorDialogHandler dialogContinue.onClick = (_event) -> { state.songMetadata.set(targetVariation, newSongMetadata); + Conductor.mapTimeChanges(state.currentSongMetadata.timeChanges); + dialog.hideDialog(DialogButton.APPLY); } @@ -696,6 +696,8 @@ class ChartEditorDialogHandler var charData:SongCharacterData = state.currentSongMetadata.playData.characters; + var hasClearedVocals:Bool = false; + charIdsForVocals.push(charData.player); charIdsForVocals.push(charData.opponent); @@ -715,6 +717,7 @@ class ChartEditorDialogHandler if (dialogNoVocals == null) throw 'Could not locate dialogNoVocals button in Upload Vocals dialog'; dialogNoVocals.onClick = function(_event) { // Dismiss + ChartEditorAudioHandler.stopExistingVocals(state); dialog.hideDialog(DialogButton.APPLY); }; @@ -738,6 +741,12 @@ class ChartEditorDialogHandler trace('Selected file: $pathStr'); var path:Path = new Path(pathStr); + if (!hasClearedVocals) + { + hasClearedVocals = true; + ChartEditorAudioHandler.stopExistingVocals(state); + } + if (ChartEditorAudioHandler.loadVocalsFromPath(state, path, charKey, instId)) { // Tell the user the load was successful. @@ -788,6 +797,11 @@ class ChartEditorDialogHandler if (selectedFile != null && selectedFile.bytes != null) { trace('Selected file: ' + selectedFile.name); + if (!hasClearedVocals) + { + hasClearedVocals = true; + ChartEditorAudioHandler.stopExistingVocals(state); + } if (ChartEditorAudioHandler.loadVocalsFromBytes(state, selectedFile.bytes, charKey, instId)) { // Tell the user the load was successful. diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx index 2f5222cd5..c28c1c8c7 100644 --- a/source/funkin/ui/debug/charting/ChartEditorState.hx +++ b/source/funkin/ui/debug/charting/ChartEditorState.hx @@ -1547,11 +1547,11 @@ class ChartEditorState extends HaxeUIState renderedEvents.setPosition(gridTiledSprite.x, gridTiledSprite.y); add(renderedEvents); - renderedNotes.zIndex = 25; + renderedEvents.zIndex = 25; renderedSelectionSquares.setPosition(gridTiledSprite.x, gridTiledSprite.y); add(renderedSelectionSquares); - renderedNotes.zIndex = 26; + renderedSelectionSquares.zIndex = 26; } function buildAdditionalUI():Void @@ -1662,7 +1662,18 @@ class ChartEditorState extends HaxeUIState addUIClickListener('menubarItemCut', _ -> performCommand(new CutItemsCommand(currentNoteSelection, currentEventSelection))); - addUIClickListener('menubarItemPaste', _ -> performCommand(new PasteItemsCommand(scrollPositionInMs + playheadPositionInMs))); + addUIClickListener('menubarItemPaste', _ -> { + var targetMs:Float = scrollPositionInMs + playheadPositionInMs; + var targetStep:Float = Conductor.getTimeInSteps(targetMs); + var targetSnappedStep:Float = Math.floor(targetStep / noteSnapRatio) * noteSnapRatio; + var targetSnappedMs:Float = Conductor.getStepTimeInMs(targetSnappedStep); + performCommand(new PasteItemsCommand(targetSnappedMs)); + }); + + addUIClickListener('menubarItemPasteUnsnapped', _ -> { + var targetMs:Float = scrollPositionInMs + playheadPositionInMs; + performCommand(new PasteItemsCommand(targetMs)); + }); addUIClickListener('menubarItemDelete', function(_) { if (currentNoteSelection.length > 0 && currentEventSelection.length > 0) @@ -1886,6 +1897,16 @@ class ChartEditorState extends HaxeUIState handleViewKeybinds(); handleTestKeybinds(); handleHelpKeybinds(); + + #if debug + handleQuickWatch(); + #end + } + + function handleQuickWatch():Void + { + FlxG.watch.addQuick('scrollPosInPixels', scrollPositionInPixels); + FlxG.watch.addQuick('playheadPosInPixels', playheadPositionInPixels); } /** @@ -2335,7 +2356,6 @@ class ChartEditorState extends HaxeUIState // Scroll up. var diff:Float = MENU_BAR_HEIGHT - FlxG.mouse.screenY; scrollPositionInPixels -= diff * 0.5; // Too fast! - trace('Scroll up: ' + diff); moveSongToScrollPosition(); } else if (FlxG.mouse.screenY > (playbarHeadLayout?.y ?? 0.0)) @@ -2343,7 +2363,6 @@ class ChartEditorState extends HaxeUIState // Scroll down. var diff:Float = FlxG.mouse.screenY - (playbarHeadLayout?.y ?? 0.0); scrollPositionInPixels += diff * 0.5; // Too fast! - trace('Scroll down: ' + diff); moveSongToScrollPosition(); } @@ -2968,8 +2987,8 @@ class ChartEditorState extends HaxeUIState // Set the position and size (because we might be recycling one with bad values). selectionSquare.x = noteSprite.x; selectionSquare.y = noteSprite.y; - selectionSquare.width = noteSprite.width; - selectionSquare.height = noteSprite.height; + selectionSquare.width = GRID_SIZE; + selectionSquare.height = GRID_SIZE; } } @@ -3000,6 +3019,8 @@ class ChartEditorState extends HaxeUIState FlxG.watch.addQuick("tapNotesRendered", renderedNotes.members.length); FlxG.watch.addQuick("holdNotesRendered", renderedHoldNotes.members.length); FlxG.watch.addQuick("eventsRendered", renderedEvents.members.length); + FlxG.watch.addQuick("notesSelected", currentNoteSelection.length); + FlxG.watch.addQuick("eventsSelected", currentEventSelection.length); } /** @@ -3029,6 +3050,8 @@ class ChartEditorState extends HaxeUIState if (selectionSquareBitmap == null) throw "ERROR: Tried to build selection square, but selectionSquareBitmap is null! Check ChartEditorThemeHandler.updateSelectionSquare()"; + FlxG.bitmapLog.add(selectionSquareBitmap, "selectionSquareBitmap"); + return new FlxSprite().loadGraphic(selectionSquareBitmap); } @@ -3106,6 +3129,7 @@ class ChartEditorState extends HaxeUIState function quitChartEditor():Void { autoSave(); + stopWelcomeMusic(); FlxG.switchState(new MainMenuState()); } @@ -3148,8 +3172,20 @@ class ChartEditorState extends HaxeUIState // CTRL + V = Paste if (FlxG.keys.pressed.CONTROL && FlxG.keys.justPressed.V) { - // Paste notes from clipboard, at the playhead. - performCommand(new PasteItemsCommand(scrollPositionInMs + playheadPositionInMs)); + // CTRL + SHIFT + V = Paste Unsnapped. + var targetMs:Float = if (FlxG.keys.pressed.SHIFT) + { + scrollPositionInMs + playheadPositionInMs; + } + else + { + var targetMs:Float = scrollPositionInMs + playheadPositionInMs; + var targetStep:Float = Conductor.getTimeInSteps(targetMs); + var targetSnappedStep:Float = Math.floor(targetStep / noteSnapRatio) * noteSnapRatio; + var targetSnappedMs:Float = Conductor.getStepTimeInMs(targetSnappedStep); + targetSnappedMs; + } + performCommand(new PasteItemsCommand(targetMs)); } // DELETE = Delete @@ -3317,6 +3353,7 @@ class ChartEditorState extends HaxeUIState if (!isHaxeUIDialogOpen && !isCursorOverHaxeUI && FlxG.keys.justPressed.ENTER) { var minimal = FlxG.keys.pressed.SHIFT; + ChartEditorToolboxHandler.hideAllToolboxes(this); testSongInPlayState(minimal); } } @@ -4128,14 +4165,13 @@ class ChartEditorState extends HaxeUIState */ function moveSongToScrollPosition():Void { - // Update the songPosition in the Conductor. - var targetPos = scrollPositionInMs; - Conductor.update(targetPos); - // Update the songPosition in the audio tracks. if (audioInstTrack != null) audioInstTrack.time = scrollPositionInMs + playheadPositionInMs; if (audioVocalTrackGroup != null) audioVocalTrackGroup.time = scrollPositionInMs + playheadPositionInMs; + // Update the songPosition in the Conductor. + Conductor.update(audioInstTrack.time); + // We need to update the note sprites because we changed the scroll position. noteDisplayDirty = true; } diff --git a/source/funkin/ui/debug/charting/ChartEditorToolboxHandler.hx b/source/funkin/ui/debug/charting/ChartEditorToolboxHandler.hx index 6f89b6b63..d92e43cf2 100644 --- a/source/funkin/ui/debug/charting/ChartEditorToolboxHandler.hx +++ b/source/funkin/ui/debug/charting/ChartEditorToolboxHandler.hx @@ -136,6 +136,18 @@ class ChartEditorToolboxHandler } } + public static function rememberOpenToolboxes(state:ChartEditorState):Void {} + + public static function openRememberedToolboxes(state:ChartEditorState):Void {} + + public static function hideAllToolboxes(state:ChartEditorState):Void + { + for (toolbox in state.activeToolboxes.values()) + { + toolbox.hideDialog(DialogButton.CANCEL); + } + } + public static function minimizeToolbox(state:ChartEditorState, id:String):Void { var toolbox:Null = state.activeToolboxes.get(id); @@ -634,9 +646,9 @@ class ChartEditorToolboxHandler timeChanges[0].bpm = event.value; } - Conductor.forceBPM(event.value); - state.currentSongMetadata.timeChanges = timeChanges; + + Conductor.mapTimeChanges(state.currentSongMetadata.timeChanges); }; inputBPM.value = state.currentSongMetadata.timeChanges[0].bpm; diff --git a/source/funkin/ui/story/Level.hx b/source/funkin/ui/story/Level.hx index fd2e3ea49..e86241277 100644 --- a/source/funkin/ui/story/Level.hx +++ b/source/funkin/ui/story/Level.hx @@ -1,5 +1,6 @@ package funkin.ui.story; +import funkin.util.SortUtil; import flixel.FlxSprite; import flixel.util.FlxColor; import funkin.play.song.Song; @@ -155,6 +156,8 @@ class Level implements IRegistryEntry } } + difficulties.sort(SortUtil.defaultsThenAlphabetically.bind(Constants.DEFAULT_DIFFICULTY_LIST)); + // Filter to only include difficulties that are present in all songs for (songIndex in 1...songList.length) { diff --git a/source/funkin/ui/story/StoryMenuState.hx b/source/funkin/ui/story/StoryMenuState.hx index 3a5a388a8..53cf7e55b 100644 --- a/source/funkin/ui/story/StoryMenuState.hx +++ b/source/funkin/ui/story/StoryMenuState.hx @@ -1,5 +1,7 @@ package funkin.ui.story; +import funkin.save.Save; +import funkin.save.Save.SaveScoreData; import openfl.utils.Assets; import flixel.addons.transition.FlxTransitionableState; import flixel.FlxSprite; @@ -521,6 +523,7 @@ class StoryMenuState extends MusicBeatState PlayStatePlaylist.campaignId = currentLevel.id; PlayStatePlaylist.campaignTitle = currentLevel.getTitle(); + PlayStatePlaylist.campaignDifficulty = currentDifficultyId; if (targetSong != null) { @@ -536,7 +539,7 @@ class StoryMenuState extends MusicBeatState LoadingState.loadAndSwitchState(new PlayState( { targetSong: targetSong, - targetDifficulty: currentDifficultyId, + targetDifficulty: PlayStatePlaylist.campaignDifficulty, }), true); }); } @@ -623,7 +626,8 @@ class StoryMenuState extends MusicBeatState tracklistText.screenCenter(X); tracklistText.x -= FlxG.width * 0.35; - // TODO: Fix this. - highScore = Highscore.getWeekScore(0, 0); + var levelScore:Null = Save.get().getLevelScore(currentLevelId, currentDifficultyId); + highScore = levelScore?.score ?? 0; + // levelScore.accuracy } } diff --git a/source/funkin/util/Constants.hx b/source/funkin/util/Constants.hx index 28f864d0e..b27a7d2f5 100644 --- a/source/funkin/util/Constants.hx +++ b/source/funkin/util/Constants.hx @@ -4,6 +4,9 @@ import flixel.util.FlxColor; import lime.app.Application; import funkin.data.song.SongData.SongTimeFormat; +/** + * A store of unchanging, globally relevant values. + */ class Constants { /** @@ -118,6 +121,11 @@ class Constants */ public static final DEFAULT_DIFFICULTY:String = 'normal'; + /** + * Default list of difficulties for charts. + */ + public static final DEFAULT_DIFFICULTY_LIST:Array = ['easy', 'normal', 'hard']; + /** * Default player character for charts. */ diff --git a/source/funkin/util/FlxGamepadUtil.hx b/source/funkin/util/FlxGamepadUtil.hx new file mode 100644 index 000000000..d768b42c4 --- /dev/null +++ b/source/funkin/util/FlxGamepadUtil.hx @@ -0,0 +1,44 @@ +package funkin.util; + +import flixel.input.gamepad.FlxGamepad; +import flixel.input.gamepad.FlxGamepadInputID; +import lime.ui.Gamepad as LimeGamepad; +import lime.ui.GamepadAxis as LimeGamepadAxis; +import lime.ui.GamepadButton as LimeGamepadButton; + +class FlxGamepadUtil +{ + public static function getInputID(gamepad:FlxGamepad, button:LimeGamepadButton):FlxGamepadInputID + { + #if FLX_GAMEINPUT_API + // FLX_GAMEINPUT_API internally assigns 6 axes to IDs 0-5, which LimeGamepadButton doesn't account for, so we need to offset the ID by 6. + final OFFSET:Int = 6; + #else + final OFFSET:Int = 0; + #end + + var result:FlxGamepadInputID = gamepad.mapping.getID(button + OFFSET); + if (result == NONE) return NONE; + return result; + } + + public static function getLimeGamepad(input:FlxGamepad):Null + { + #if FLX_GAMEINPUT_API @:privateAccess + return input._device.getLimeGamepad(); + #else + return null; + #end + } + + @:privateAccess + public static function getFlxGamepadByLimeGamepad(gamepad:LimeGamepad):FlxGamepad + { + // Why is this so elaborate? + @:privateAccess + var gameInputDevice:openfl.ui.GameInputDevice = openfl.ui.GameInput.__getDevice(gamepad); + @:privateAccess + var gamepadIndex:Int = FlxG.gamepads.findGamepadIndex(gameInputDevice); + return FlxG.gamepads.getByID(gamepadIndex); + } +}