From 8589c10c750159973004e90711119a0e8c0f0642 Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Wed, 11 Oct 2023 17:33:55 -0400 Subject: [PATCH 01/12] Fixes to Pause screen and Results screen --- source/funkin/PauseSubState.hx | 49 ++++++++++++------ source/funkin/play/PlayState.hx | 13 +++-- source/funkin/play/PlayStatePlaylist.hx | 7 +-- source/funkin/play/ResultState.hx | 65 +++++++++++++++--------- source/funkin/play/song/Song.hx | 32 +++++++----- source/funkin/ui/story/StoryMenuState.hx | 3 +- 6 files changed, 107 insertions(+), 62 deletions(-) diff --git a/source/funkin/PauseSubState.hx b/source/funkin/PauseSubState.hx index f93e5a450..48be31226 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') { @@ -196,18 +203,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; @@ -237,6 +232,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/play/PlayState.hx b/source/funkin/play/PlayState.hx index d7c2a2a4c..9e1c89c8a 100644 --- a/source/funkin/play/PlayState.hx +++ b/source/funkin/play/PlayState.hx @@ -2419,7 +2419,7 @@ class PlayState extends MusicBeatSubState if (currentSong.validScore) { NGio.unlockMedal(60961); - Highscore.saveWeekScoreForDifficulty(PlayStatePlaylist.campaignId, PlayStatePlaylist.campaignScore, currentDifficulty); + Highscore.saveWeekScoreForDifficulty(PlayStatePlaylist.campaignId, PlayStatePlaylist.campaignScore, PlayStatePlaylist.campaignDifficulty); } // FlxG.save.data.weekUnlocked = StoryMenuState.weekUnlocked; @@ -2466,7 +2466,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 +2482,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 +2608,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/song/Song.hx b/source/funkin/play/song/Song.hx index d11c7744b..96eb434d2 100644 --- a/source/funkin/play/song/Song.hx +++ b/source/funkin/play/song/Song.hx @@ -56,8 +56,6 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry; - public var songName(get, never):String; function get_songName():String @@ -85,7 +83,6 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry(); _data = _fetchData(id); @@ -127,8 +124,7 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry chartData in charts) @@ -162,8 +158,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; + + return 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; + }); } - 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/ui/story/StoryMenuState.hx b/source/funkin/ui/story/StoryMenuState.hx index 3a5a388a8..35a5b6479 100644 --- a/source/funkin/ui/story/StoryMenuState.hx +++ b/source/funkin/ui/story/StoryMenuState.hx @@ -521,6 +521,7 @@ class StoryMenuState extends MusicBeatState PlayStatePlaylist.campaignId = currentLevel.id; PlayStatePlaylist.campaignTitle = currentLevel.getTitle(); + PlayStatePlaylist.campaignDifficulty = currentDifficultyId; if (targetSong != null) { @@ -536,7 +537,7 @@ class StoryMenuState extends MusicBeatState LoadingState.loadAndSwitchState(new PlayState( { targetSong: targetSong, - targetDifficulty: currentDifficultyId, + targetDifficulty: PlayStatePlaylist.campaignDifficulty, }), true); }); } From 7a9b4d6bf028c053d334b263398c4b764de5e3d4 Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Wed, 11 Oct 2023 01:04:56 -0400 Subject: [PATCH 02/12] Add the "death.cameraOffset" attribute to character data --- assets | 2 +- source/funkin/play/GameOverSubState.hx | 3 +++ source/funkin/play/character/BaseCharacter.hx | 5 +++++ source/funkin/play/character/CharacterData.hx | 16 +++++++++++++++- 4 files changed, 24 insertions(+), 2 deletions(-) diff --git a/assets b/assets index 6e5ed4602..c1ef5f683 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit 6e5ed46026a2eb1e575c5accf9192b90c13ff857 +Subproject commit c1ef5f68303b8069702193078c8228ffc9dc551e diff --git a/source/funkin/play/GameOverSubState.hx b/source/funkin/play/GameOverSubState.hx index 15ed0421e..cb350f028 100644 --- a/source/funkin/play/GameOverSubState.hx +++ b/source/funkin/play/GameOverSubState.hx @@ -103,6 +103,9 @@ class GameOverSubState extends MusicBeatSubState cameraFollowPoint = new FlxObject(PlayState.instance.cameraFollowPoint.x, PlayState.instance.cameraFollowPoint.y, 1, 1); cameraFollowPoint.x = boyfriend.getGraphicMidpoint().x; cameraFollowPoint.y = boyfriend.getGraphicMidpoint().y; + var offsets:Array = boyfriend.getDeathCameraOffsets(); + cameraFollowPoint.x += offsets[0]; + cameraFollowPoint.y += offsets[1]; add(cameraFollowPoint); FlxG.camera.target = null; diff --git a/source/funkin/play/character/BaseCharacter.hx b/source/funkin/play/character/BaseCharacter.hx index 30b549fd3..3a34144bf 100644 --- a/source/funkin/play/character/BaseCharacter.hx +++ b/source/funkin/play/character/BaseCharacter.hx @@ -188,6 +188,11 @@ class BaseCharacter extends Bopper shouldBop = false; } + public function getDeathCameraOffsets():Array + { + return _data.death?.cameraOffsets ?? [0.0, 0.0]; + } + /** * Gets the value of flipX from the character data. * `!getFlipX()` is the direction Boyfriend should face. diff --git a/source/funkin/play/character/CharacterData.hx b/source/funkin/play/character/CharacterData.hx index f1b316b7f..8be9f25c7 100644 --- a/source/funkin/play/character/CharacterData.hx +++ b/source/funkin/play/character/CharacterData.hx @@ -19,8 +19,10 @@ class CharacterDataParser * The current version string for the stage data format. * Handle breaking changes by incrementing this value * and adding migration to the `migrateStageData()` function. + * + * - Version 1.0.1 adds `death.cameraOffsets` */ - public static final CHARACTER_DATA_VERSION:String = '1.0.0'; + public static final CHARACTER_DATA_VERSION:String = '1.0.1'; /** * The current version rule check for the stage data format. @@ -603,6 +605,8 @@ typedef CharacterData = */ var healthIcon:Null; + var death:Null; + /** * The global offset to the character's position, in pixels. * @default [0, 0] @@ -695,3 +699,13 @@ typedef HealthIconData = */ var offsets:Null>; } + +typedef DeathData = +{ + /** + * The amount to offset the camera by while focusing on this character as they die. + * Default value focuses on the character's graphic midpoint. + * @default [0, 0] + */ + var ?cameraOffsets:Array; +} From 0ce82e49d0f522181f98e70cc638d384434e277b Mon Sep 17 00:00:00 2001 From: Cameron Taylor Date: Mon, 16 Oct 2023 23:43:59 -0400 Subject: [PATCH 03/12] story menu diff sorted --- source/funkin/ui/story/Level.hx | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/source/funkin/ui/story/Level.hx b/source/funkin/ui/story/Level.hx index fd2e3ea49..271039f00 100644 --- a/source/funkin/ui/story/Level.hx +++ b/source/funkin/ui/story/Level.hx @@ -155,6 +155,31 @@ class Level implements IRegistryEntry } } + // sort the difficulties, since they may be out of order in the chart JSON + var diffMap:Map = new Map(); + for (difficulty in difficulties) + { + var num:Int = 0; + switch (difficulty) + { + case 'easy': + num = 0; + case 'normal': + num = 1; + case 'hard': + num = 2; + case 'erect': + num = 3; + case 'nightmare': + num = 4; + } + diffMap.set(difficulty, num); + } + + difficulties.sort(function(a:String, b:String) { + return diffMap.get(a) - diffMap.get(b); + }); + // Filter to only include difficulties that are present in all songs for (songIndex in 1...songList.length) { From 9b8fc872617cf55b9d41a0fb1a436e69995f1d53 Mon Sep 17 00:00:00 2001 From: Cameron Taylor Date: Tue, 17 Oct 2023 00:07:36 -0400 Subject: [PATCH 04/12] song diff menu sort --- source/funkin/play/song/Song.hx | 31 ++++++++++++++++++++++++++++++- source/funkin/ui/story/Level.hx | 1 + 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/source/funkin/play/song/Song.hx b/source/funkin/play/song/Song.hx index 96eb434d2..33363e1ff 100644 --- a/source/funkin/play/song/Song.hx +++ b/source/funkin/play/song/Song.hx @@ -245,12 +245,41 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry = 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; }); + + // sort the difficulties, since they may be out of order in the chart JSON + // maybe be careful of lowercase/uppercase? + // also used in Level.listDifficulties()!! + var diffMap:Map = new Map(); + for (difficulty in diffFiltered) + { + var num:Int = 0; + switch (difficulty) + { + case 'easy': + num = 0; + case 'normal': + num = 1; + case 'hard': + num = 2; + case 'erect': + num = 3; + case 'nightmare': + num = 4; + } + diffMap.set(difficulty, num); + } + + diffFiltered.sort(function(a:String, b:String) { + return (diffMap.get(a) ?? 0) - (diffMap.get(b) ?? 0); + }); + + return diffFiltered; } public function hasDifficulty(diffId:String, ?variationId:String):Bool diff --git a/source/funkin/ui/story/Level.hx b/source/funkin/ui/story/Level.hx index 271039f00..c976cbfce 100644 --- a/source/funkin/ui/story/Level.hx +++ b/source/funkin/ui/story/Level.hx @@ -156,6 +156,7 @@ class Level implements IRegistryEntry } // sort the difficulties, since they may be out of order in the chart JSON + // also copy/pasted to Song.listDifficulties()! var diffMap:Map = new Map(); for (difficulty in difficulties) { From afcb677facc0b7e05644f65b52b45d2c210ab7e0 Mon Sep 17 00:00:00 2001 From: Cameron Taylor Date: Tue, 17 Oct 2023 00:38:28 -0400 Subject: [PATCH 05/12] index on rewrite/bugfix/pause-and-results-fixes: 9b8fc872 song diff menu sort --- .github/actions/setup-haxeshit/action.yml | 2 - .github/workflows/build-shit.yml | 9 +- Project.xml | 17 +- assets | 2 +- hmm.json | 4 +- source/Main.hx | 20 +- source/funkin/Controls.hx | 10 + source/funkin/FreeplayState.hx | 268 +++---- source/funkin/Highscore.hx | 174 ----- source/funkin/InitState.hx | 28 +- source/funkin/MainMenuState.hx | 18 +- source/funkin/NGio.hx | 12 +- source/funkin/PauseSubState.hx | 12 +- source/funkin/PlayerSettings.hx | 307 +++----- source/funkin/Preferences.hx | 138 ++++ source/funkin/api/newgrounds/NGUtil.hx | 12 +- source/funkin/audiovis/ABotVis.hx | 1 - source/funkin/data/song/SongData.hx | 10 +- source/funkin/data/song/SongDataUtils.hx | 37 +- source/funkin/data/song/SongRegistry.hx | 4 +- source/funkin/import.hx | 1 + source/funkin/input/PreciseInputManager.hx | 205 ++++- source/funkin/modding/PolymodHandler.hx | 29 +- source/funkin/play/GameOverSubState.hx | 3 +- source/funkin/play/PlayState.hx | 67 +- source/funkin/play/notes/Strumline.hx | 14 +- source/funkin/play/notes/SustainTrail.hx | 2 +- source/funkin/save/Save.hx | 700 ++++++++++++++++++ .../save/migrator/RawSaveData_v1_0_0.hx | 52 ++ .../funkin/save/migrator/SaveDataMigrator.hx | 322 ++++++++ source/funkin/ui/ControlsMenu.hx | 51 +- source/funkin/ui/PreferencesMenu.hx | 157 ++-- source/funkin/ui/StickerSubState.hx | 12 +- source/funkin/ui/TextMenuList.hx | 6 +- .../debug/charting/ChartEditorAudioHandler.hx | 309 +++++--- .../ui/debug/charting/ChartEditorCommand.hx | 28 + .../charting/ChartEditorDialogHandler.hx | 208 +++++- .../ChartEditorImportExportHandler.hx | 56 +- .../ui/debug/charting/ChartEditorState.hx | 174 +++-- source/funkin/ui/story/StoryMenuState.hx | 7 +- source/funkin/util/Constants.hx | 7 +- source/funkin/util/FlxGamepadUtil.hx | 44 ++ source/funkin/util/macro/GitCommit.hx | 2 +- 43 files changed, 2527 insertions(+), 1014 deletions(-) create mode 100644 source/funkin/Preferences.hx create mode 100644 source/funkin/save/Save.hx create mode 100644 source/funkin/save/migrator/RawSaveData_v1_0_0.hx create mode 100644 source/funkin/save/migrator/SaveDataMigrator.hx create mode 100644 source/funkin/util/FlxGamepadUtil.hx diff --git a/.github/actions/setup-haxeshit/action.yml b/.github/actions/setup-haxeshit/action.yml index 0cc544cf7..dcf5fd0a7 100644 --- a/.github/actions/setup-haxeshit/action.yml +++ b/.github/actions/setup-haxeshit/action.yml @@ -23,8 +23,6 @@ runs: with: path: .haxelib key: ${{ runner.os }}-hmm-${{ hashFiles('**/hmm.json') }} - restore-keys: | - ${{ runner.os }}-hmm- - if: ${{ steps.cache-hmm.outputs.cache-hit != 'true' }} name: hmm install run: | diff --git a/.github/workflows/build-shit.yml b/.github/workflows/build-shit.yml index 809a8b94b..ed10cbdc2 100644 --- a/.github/workflows/build-shit.yml +++ b/.github/workflows/build-shit.yml @@ -53,9 +53,8 @@ jobs: token: ${{ secrets.GH_RO_PAT }} - uses: ./.github/actions/setup-haxeshit - name: Make HXCPP cache dir - shell: bash run: | - mkdir -p ${{ runner.temp }}\\hxcpp_cache + mkdir -p ${{ runner.temp }}\hxcpp_cache - name: Restore build cache id: cache-build-win uses: actions/cache@v3 @@ -63,10 +62,8 @@ jobs: path: | .haxelib export - ${{ runner.temp }}\\hxcpp_cache - key: ${{ runner.os }}-build-win-${{ github.ref_name }} - restore-keys: | - ${{ runner.os }}-build-win- + ${{ runner.temp }}\hxcpp_cache + key: ${{ runner.os }}-build-win-${{ github.ref_name }}-${{ hashFiles('**/hmm.json') }} - name: Build game run: | haxelib run lime build windows -release -DNO_REDIRECT_ASSETS_FOLDER diff --git a/Project.xml b/Project.xml index ccf6c83a3..69400d8b1 100644 --- a/Project.xml +++ b/Project.xml @@ -156,7 +156,6 @@ - @@ -196,6 +195,22 @@ + +
+ + +
+ +
+ + + + +
+ --> --> diff --git a/assets b/assets index 6e5ed4602..8104d43e5 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit 6e5ed46026a2eb1e575c5accf9192b90c13ff857 +Subproject commit 8104d43e584a1f25e574438d7b21a7e671358969 diff --git a/hmm.json b/hmm.json index 3f420ac48..070d96cd0 100644 --- a/hmm.json +++ b/hmm.json @@ -104,7 +104,7 @@ "name": "lime", "type": "git", "dir": null, - "ref": "f195121ebec688b417e38ab115185c8d93c349d3", + "ref": "737b86f121cdc90358d59e2e527934f267c94a2c", "url": "https://github.com/EliteMasterEric/lime" }, { @@ -139,7 +139,7 @@ "name": "openfl", "type": "git", "dir": null, - "ref": "de9395d2f367a80f93f082e1b639b9cde2258bf1", + "ref": "f229d76361c7e31025a048fe7909847f75bb5d5e", "url": "https://github.com/EliteMasterEric/openfl" }, { diff --git a/source/Main.hx b/source/Main.hx index 72209cd30..dffe666b7 100644 --- a/source/Main.hx +++ b/source/Main.hx @@ -4,6 +4,7 @@ import flixel.FlxGame; import flixel.FlxState; import funkin.util.logging.CrashHandler; import funkin.MemoryCounter; +import funkin.save.Save; import haxe.ui.Toolkit; import openfl.display.FPS; import openfl.display.Sprite; @@ -84,20 +85,21 @@ class Main extends Sprite initHaxeUI(); + fpsCounter = new FPS(10, 3, 0xFFFFFF); + // addChild(fpsCounter); // Handled by Preferences.init + #if !html5 + memoryCounter = new MemoryCounter(10, 13, 0xFFFFFF); + // addChild(memoryCounter); + #end + + // George recommends binding the save before FlxGame is created. + Save.load(); + addChild(new FlxGame(gameWidth, gameHeight, initialState, framerate, framerate, skipSplash, startFullscreen)); #if hxcpp_debug_server trace('hxcpp_debug_server is enabled! You can now connect to the game with a debugger.'); #end - - #if debug - fpsCounter = new FPS(10, 3, 0xFFFFFF); - addChild(fpsCounter); - #if !html5 - memoryCounter = new MemoryCounter(10, 13, 0xFFFFFF); - addChild(memoryCounter); - #end - #end } function initHaxeUI():Void diff --git a/source/funkin/Controls.hx b/source/funkin/Controls.hx index 81055fb34..9372c4dc6 100644 --- a/source/funkin/Controls.hx +++ b/source/funkin/Controls.hx @@ -1,5 +1,7 @@ + package funkin; +import flixel.input.gamepad.FlxGamepad; import flixel.util.FlxDirectionFlags; import flixel.FlxObject; import flixel.input.FlxInput; @@ -832,6 +834,14 @@ class Controls extends FlxActionSet fromSaveData(padData, Gamepad(id)); } + public function getGamepadIds():Array { + return gamepadsAdded; + } + + public function getGamepads():Array { + return [for (id in gamepadsAdded) FlxG.gamepads.getByID(id)]; + } + inline function addGamepadLiteral(id:Int, ?buttonMap:Map>):Void { gamepadsAdded.push(id); diff --git a/source/funkin/FreeplayState.hx b/source/funkin/FreeplayState.hx index 3ae32c2e4..4e7674e93 100644 --- a/source/funkin/FreeplayState.hx +++ b/source/funkin/FreeplayState.hx @@ -1,54 +1,55 @@ package funkin; -import funkin.shaderslmfao.HSVShader; -import funkin.ui.StickerSubState; import flash.text.TextField; +import flixel.addons.display.FlxGridOverlay; +import flixel.addons.transition.FlxTransitionableState; +import flixel.addons.ui.FlxInputText; import flixel.FlxCamera; import flixel.FlxGame; import flixel.FlxSprite; import flixel.FlxState; -import flixel.addons.display.FlxGridOverlay; -import flixel.addons.transition.FlxTransitionableState; -import flixel.addons.ui.FlxInputText; -import flixel.group.FlxGroup.FlxTypedGroup; import flixel.group.FlxGroup; +import flixel.group.FlxGroup.FlxTypedGroup; import flixel.group.FlxSpriteGroup; -import flixel.system.debug.watch.Tracker.TrackerProfile; import flixel.input.touch.FlxTouch; import flixel.math.FlxAngle; import flixel.math.FlxMath; import flixel.math.FlxPoint; +import flixel.system.debug.watch.Tracker.TrackerProfile; import flixel.text.FlxText; import flixel.tweens.FlxEase; import flixel.tweens.FlxTween; import flixel.util.FlxColor; -import funkin.data.song.SongRegistry; -import funkin.data.level.LevelRegistry; import flixel.util.FlxSpriteUtil; import flixel.util.FlxTimer; import funkin.Controls.Control; +import funkin.data.level.LevelRegistry; +import funkin.data.song.SongRegistry; import funkin.freeplayStuff.BGScrollingText; +import funkin.freeplayStuff.DifficultyStars; import funkin.freeplayStuff.DJBoyfriend; import funkin.freeplayStuff.FreeplayScore; import funkin.freeplayStuff.LetterSort; import funkin.freeplayStuff.SongMenuItem; -import funkin.freeplayStuff.DifficultyStars; +import funkin.graphics.adobeanimate.FlxAtlasSprite; import funkin.play.HealthIcon; import funkin.play.PlayState; -import funkin.shaderslmfao.AngleMask; -import funkin.shaderslmfao.PureColor; -import funkin.shaderslmfao.StrokeShader; import funkin.play.PlayStatePlaylist; import funkin.play.song.Song; +import funkin.save.Save; +import funkin.save.Save.SaveScoreData; +import funkin.shaderslmfao.AngleMask; +import funkin.shaderslmfao.HSVShader; +import funkin.shaderslmfao.PureColor; +import funkin.shaderslmfao.StrokeShader; +import funkin.ui.StickerSubState; import lime.app.Future; import lime.utils.Assets; -import funkin.graphics.adobeanimate.FlxAtlasSprite; class FreeplayState extends MusicBeatSubState { var songs:Array = []; - // var selector:FlxText; var curSelected:Int = 0; var curDifficulty:Int = 1; @@ -107,8 +108,6 @@ class FreeplayState extends MusicBeatSubState openSubState(stickerSubState); stickerSubState.degenStickers(); - - // resetSubState(); } #if discord_rpc @@ -120,31 +119,25 @@ class FreeplayState extends MusicBeatSubState #if debug isDebug = true; - // addSong('Test', 'tutorial', 'bf-pixel'); - // addSong('Pyro', 'weekend1', 'darnell'); #end - // var initSonglist = CoolUtil.coolTextFile(Paths.txt('freeplaySonglist')); - - // for (i in 0...initSonglist.length) - // { - // songs.push(new FreeplaySongData(initSonglist[i], 'tutorial', 'gf')); - // } - if (FlxG.sound.music != null) { if (!FlxG.sound.music.playing) FlxG.sound.playMusic(Paths.music('freakyMenu/freakyMenu')); } + // Add a null entry that represents the RANDOM option + songs.push(null); + // programmatically adds the songs via LevelRegistry and SongRegistry for (coolWeek in LevelRegistry.instance.listBaseGameLevelIds()) { - for (coolSong in LevelRegistry.instance.parseEntryData(coolWeek).songs) + for (songId in LevelRegistry.instance.parseEntryData(coolWeek).songs) { - var metadata = SongRegistry.instance.parseEntryMetadata(coolSong); + var metadata = SongRegistry.instance.parseEntryMetadata(songId); var char = metadata.playData.characters.opponent; var songName = metadata.songName; - addSong(songName, coolWeek, char); + addSong(songId, songName, coolWeek, char); } } @@ -254,7 +247,6 @@ class FreeplayState extends MusicBeatSubState speed: 0.3 }); - // dj = new DJBoyfriend(0, -100); dj = new DJBoyfriend(640, 366); exitMovers.set([dj], { @@ -427,7 +419,6 @@ class FreeplayState extends MusicBeatSubState dj.onIntroDone.add(function() { // when boyfriend hits dat shiii - // albumArt.visible = true; albumArt.anim.play(""); @@ -485,40 +476,15 @@ class FreeplayState extends MusicBeatSubState generateSongList(null, false); - // FlxG.sound.playMusic(Paths.music('title'), 0); - // FlxG.sound.music.fadeIn(2, 0, 0.8); - // selector = new FlxText(); - - // selector.size = 40; - // selector.text = ">"; - // add(selector); - var swag:Alphabet = new Alphabet(1, 0, "swag"); - // JUST DOIN THIS SHIT FOR TESTING!!! - /* - var md:String = Markdown.markdownToHtml(Assets.getText('CHANGELOG.md')); - - var texFel:TextField = new TextField(); - texFel.width = FlxG.width; - texFel.height = FlxG.height; - // texFel. - texFel.htmlText = md; - - FlxG.stage.addChild(texFel); - - trace(md); - */ - var funnyCam = new FlxCamera(0, 0, FlxG.width, FlxG.height); funnyCam.bgColor = FlxColor.TRANSPARENT; FlxG.cameras.add(funnyCam); typing = new FlxInputText(100, 100); - // add(typing); typing.callback = function(txt, action) { - // generateSongList(new EReg(txt.trim(), "ig")); trace(action); }; @@ -534,9 +500,6 @@ class FreeplayState extends MusicBeatSubState for (cap in grpCapsules.members) cap.kill(); - // grpCapsules.clear(); - - // var regexp:EReg = regexp; var tempSongs:Array = songs; if (filterStuff != null) @@ -570,7 +533,7 @@ class FreeplayState extends MusicBeatSubState var randomCapsule:SongMenuItem = grpCapsules.recycle(SongMenuItem); randomCapsule.init(FlxG.width, 0, "Random"); randomCapsule.onConfirm = function() { - trace("RANDOM SELECTED"); + capsuleOnConfirmRandom(randomCapsule); }; randomCapsule.y = randomCapsule.intendedY(0) + 10; randomCapsule.targetPos.x = randomCapsule.x; @@ -583,7 +546,10 @@ class FreeplayState extends MusicBeatSubState for (i in 0...tempSongs.length) { + if (tempSongs[i] == null) continue; + var funnyMenu:SongMenuItem = grpCapsules.recycle(SongMenuItem); + funnyMenu.init(FlxG.width, 0, tempSongs[i].songName); if (tempSongs[i].songCharacter != null) funnyMenu.setCharacter(tempSongs[i].songCharacter); funnyMenu.onConfirm = function() { @@ -596,7 +562,6 @@ class FreeplayState extends MusicBeatSubState funnyMenu.songText.visible = false; funnyMenu.favIcon.visible = tempSongs[i].isFav; funnyMenu.hsvShader = hsvShader; - // fp.updateScore(0); if (i < 8) funnyMenu.initJumpIn(Math.min(i, 4), force); else @@ -611,22 +576,9 @@ class FreeplayState extends MusicBeatSubState changeDiff(); } - public function addSong(songName:String, levelId:String, songCharacter:String) + public function addSong(songId:String, songName:String, levelId:String, songCharacter:String) { - songs.push(new FreeplaySongData(songName, levelId, songCharacter)); - } - - public function addWeek(songs:Array, levelId:String, ?songCharacters:Array) - { - if (songCharacters == null) songCharacters = ['bf']; - - var num:Int = 0; - for (song in songs) - { - addSong(song, levelId, songCharacters[num]); - - if (songCharacters.length != 1) num++; - } + songs.push(new FreeplaySongData(songId, songName, levelId, songCharacter)); } var touchY:Float = 0; @@ -643,34 +595,39 @@ class FreeplayState extends MusicBeatSubState var spamTimer:Float = 0; var spamming:Bool = false; + var busy:Bool = false; // Set to true once the user has pressed enter to select a song. + override function update(elapsed:Float) { super.update(elapsed); if (FlxG.keys.justPressed.F) { - var realShit = curSelected; - songs[curSelected].isFav = !songs[curSelected].isFav; - if (songs[curSelected].isFav) + if (songs[curSelected] != null) { - FlxTween.tween(grpCapsules.members[realShit], {angle: 360}, 0.4, - { - ease: FlxEase.elasticOut, - onComplete: _ -> { - grpCapsules.members[realShit].favIcon.visible = true; - grpCapsules.members[realShit].favIcon.animation.play("fav"); - } + var realShit = curSelected; + songs[curSelected].isFav = !songs[curSelected].isFav; + if (songs[curSelected].isFav) + { + FlxTween.tween(grpCapsules.members[realShit], {angle: 360}, 0.4, + { + ease: FlxEase.elasticOut, + onComplete: _ -> { + grpCapsules.members[realShit].favIcon.visible = true; + grpCapsules.members[realShit].favIcon.animation.play("fav"); + } + }); + } + else + { + grpCapsules.members[realShit].favIcon.animation.play('fav', false, true); + new FlxTimer().start((1 / 24) * 14, _ -> { + grpCapsules.members[realShit].favIcon.visible = false; }); - } - else - { - grpCapsules.members[realShit].favIcon.animation.play('fav', false, true); - new FlxTimer().start((1 / 24) * 14, _ -> { - grpCapsules.members[realShit].favIcon.visible = false; - }); - new FlxTimer().start((1 / 24) * 24, _ -> { - FlxTween.tween(grpCapsules.members[realShit], {angle: 0}, 0.4, {ease: FlxEase.elasticOut}); - }); + new FlxTimer().start((1 / 24) * 24, _ -> { + FlxTween.tween(grpCapsules.members[realShit], {angle: 0}, 0.4, {ease: FlxEase.elasticOut}); + }); + } } } @@ -690,11 +647,13 @@ class FreeplayState extends MusicBeatSubState fp.updateScore(Std.int(lerpScore)); txtCompletion.text = Math.floor(lerpCompletion * 100) + "%"; - // trace(Highscore.getCompletion(songs[curSelected].songName, curDifficulty)); - // trace(intendedScore); - // trace(lerpScore); - // Highscore.getAllScores(); + handleInputs(elapsed); + } + + function handleInputs(elapsed:Float):Void + { + if (busy) return; var upP = controls.UI_UP_P; var downP = controls.UI_DOWN_P; @@ -718,16 +677,7 @@ class FreeplayState extends MusicBeatSubState FlxG.watch.addQuick("LENGTH", length); FlxG.watch.addQuick("ANGLE", Math.round(FlxAngle.asDegrees(angle))); - // trace("ANGLE", Math.round(FlxAngle.asDegrees(angle))); } - - /* switch (inputID) - { - case FlxObject.UP: - return - case FlxObject.DOWN: - } - */ } if (FlxG.touches.getFirst() != null) @@ -763,7 +713,6 @@ class FreeplayState extends MusicBeatSubState touchY = touch.screenY; if (dyTouch != 0) dyTouch < 0 ? changeSelection(1) : changeSelection(-1); - // changeSelection(1); } } else @@ -841,8 +790,6 @@ class FreeplayState extends MusicBeatSubState FlxG.sound.play(Paths.sound('cancelMenu')); - // FlxTween.tween(dj, {x: -dj.width}, 0.2, {ease: FlxEase.quartOut}); - var longestTimer:Float = 0; for (grpSpr in exitMovers.keys()) @@ -888,15 +835,11 @@ class FreeplayState extends MusicBeatSubState { FlxG.switchState(new MainMenuState()); } - // - // close(); }); } if (accepted) { - // if (Assets.exists()) - grpCapsules.members[curSelected].onConfirm(); } } @@ -904,7 +847,11 @@ class FreeplayState extends MusicBeatSubState @:haxe.warning("-WDeprecated") override function switchTo(nextState:FlxState):Bool { - clearDaCache(songs[curSelected].songName); + var daSong = songs[curSelected]; + if (daSong != null) + { + clearDaCache(daSong.songName); + } return super.switchTo(nextState); } @@ -917,9 +864,29 @@ class FreeplayState extends MusicBeatSubState if (curDifficulty < 0) curDifficulty = 2; if (curDifficulty > 2) curDifficulty = 0; - // intendedScore = Highscore.getScore(songs[curSelected].songName, curDifficulty); - intendedScore = Highscore.getScore(songs[curSelected].songName, curDifficulty); - intendedCompletion = Highscore.getCompletion(songs[curSelected].songName, curDifficulty); + var targetDifficulty:String = switch (curDifficulty) + { + case 0: + 'easy'; + case 1: + 'normal'; + case 2: + 'hard'; + default: 'normal'; + }; + + var daSong = songs[curSelected]; + if (daSong != null) + { + var songScore:SaveScoreData = Save.get().getSongScore(songs[curSelected].songId, targetDifficulty); + intendedScore = songScore?.score ?? 0; + intendedCompletion = songScore?.accuracy ?? 0.0; + } + else + { + intendedScore = 0; + intendedCompletion = 0.0; + } grpDifficulties.group.forEach(function(spr) { spr.visible = false; @@ -941,6 +908,7 @@ class FreeplayState extends MusicBeatSubState { for (song in songs) { + if (song == null) continue; if (song.songName != actualSongTho) { trace('trying to remove: ' + song.songName); @@ -949,19 +917,16 @@ class FreeplayState extends MusicBeatSubState } } + function capsuleOnConfirmRandom(cap:SongMenuItem):Void + { + trace("RANDOM SELECTED"); + + busy = true; + } + function capsuleOnConfirmDefault(cap:SongMenuItem):Void { - // var poop:String = songs[curSelected].songName.toLowerCase(); - - // does not work properly, always just accidentally sets it to normal anyways! - /* if (!Assets.exists(Paths.json(songs[curSelected].songName + '/' + poop))) - { - // defaults to normal if HARD / EASY doesn't exist - // does not account if NORMAL doesn't exist! - FlxG.log.warn("CURRENT DIFFICULTY IS NOT CHARTED, DEFAULTING TO NORMAL!"); - poop = Highscore.formatSong(songs[curSelected].songName.toLowerCase(), 1); - curDifficulty = 1; - }*/ + busy = true; PlayStatePlaylist.isStoryMode = false; @@ -1002,6 +967,7 @@ class FreeplayState extends MusicBeatSubState targetSong.cacheCharts(true); new FlxTimer().start(1, function(tmr:FlxTimer) { + Paths.setCurrentLevel(songs[curSelected].levelId); LoadingState.loadAndSwitchState(new PlayState( { targetSong: targetSong, @@ -1013,8 +979,6 @@ class FreeplayState extends MusicBeatSubState function changeSelection(change:Int = 0) { - // fp.updateScore(12345); - // NGio.logEvent('Fresh'); FlxG.sound.play(Paths.sound('scrollMenu'), 0.4); // FlxG.sound.playMusic(Paths.inst(songs[curSelected].songName)); @@ -1024,27 +988,30 @@ class FreeplayState extends MusicBeatSubState if (curSelected < 0) curSelected = grpCapsules.countLiving() - 1; if (curSelected >= grpCapsules.countLiving()) curSelected = 0; - // selector.y = (70 * curSelected) + 30; - - // intendedScore = Highscore.getScore(songs[curSelected].songName, curDifficulty); - - if (songs[curSelected] != null) + var targetDifficulty:String = switch (curDifficulty) { - intendedScore = Highscore.getScore(songs[curSelected].songName, curDifficulty); - intendedCompletion = Highscore.getCompletion(songs[curSelected].songName, curDifficulty); + case 0: + 'easy'; + case 1: + 'normal'; + case 2: + 'hard'; + default: 'normal'; + }; + + var daSong = songs[curSelected]; + if (daSong != null) + { + var songScore:SaveScoreData = Save.get().getSongScore(daSong.songId, targetDifficulty); + intendedScore = songScore?.score ?? 0; + intendedCompletion = songScore?.accuracy ?? 0.0; } else { intendedScore = 0; - intendedCompletion = 0; + intendedCompletion = 0.0; } - // lerpScore = 0; - - #if PRELOAD_ALL - // FlxG.sound.playMusic(Paths.inst(songs[curSelected].songName), 0); - #end - for (index => capsule in grpCapsules.members) { index += 1; @@ -1053,7 +1020,6 @@ class FreeplayState extends MusicBeatSubState capsule.targetPos.y = capsule.intendedY(index - curSelected); capsule.targetPos.x = 270 + (60 * (Math.sin(index - curSelected))); - // capsule.targetPos.x = 320 + (40 * (index - curSelected)); if (index < curSelected) capsule.targetPos.y -= 100; // another 100 for good measure } @@ -1132,14 +1098,16 @@ enum abstract FilterType(String) class FreeplaySongData { + public var songId:String = ""; public var songName:String = ""; public var levelId:String = ""; public var songCharacter:String = ""; public var isFav:Bool = false; - public function new(song:String, levelId:String, songCharacter:String, isFav:Bool = false) + public function new(songId:String, songName:String, levelId:String, songCharacter:String, isFav:Bool = false) { - this.songName = song; + this.songId = songId; + this.songName = songName; this.levelId = levelId; this.songCharacter = songCharacter; this.isFav = isFav; diff --git a/source/funkin/Highscore.hx b/source/funkin/Highscore.hx index 46e98d8dc..3c9fd82e4 100644 --- a/source/funkin/Highscore.hx +++ b/source/funkin/Highscore.hx @@ -2,183 +2,9 @@ package funkin; class Highscore { - #if (haxe >= "4.0.0") - public static var songScores:Map = new Map(); - #else - public static var songScores:Map = new Map(); - #end - - #if (haxe >= "4.0.0") - public static var songCompletion:Map = new Map(); - #else - public static var songCompletion:Map = new Map(); - #end - public static var tallies:Tallies = new Tallies(); - - public static function saveScore(song:String, score:Int = 0, ?diff:Int = 0):Bool - { - var formattedSong:String = formatSong(song, diff); - - #if newgrounds - NGio.postScore(score, song); - #end - - if (songScores.exists(formattedSong)) - { - if (songScores.get(formattedSong) < score) - { - setScore(formattedSong, score); - return true; - // new highscore - } - } - else - setScore(formattedSong, score); - - return false; - } - - public static function saveScoreForDifficulty(song:String, score:Int = 0, diff:String = 'normal'):Bool - { - var diffInt:Int = 1; - - if (diff == 'easy') diffInt = 0; - else if (diff == 'hard') diffInt = 2; - - return saveScore(song, score, diffInt); - } - - public static function saveCompletion(song:String, completion:Float, diff:Int = 0):Bool - { - var formattedSong:String = formatSong(song, diff); - - if (songCompletion.exists(formattedSong)) - { - if (songCompletion.get(formattedSong) < completion) - { - setCompletion(formattedSong, completion); - return true; - } - } - else - setCompletion(formattedSong, completion); - - return false; - } - - public static function saveCompletionForDifficulty(song:String, completion:Float, diff:String = 'normal'):Bool - { - var diffInt:Int = 1; - - if (diff == 'easy') diffInt = 0; - else if (diff == 'hard') diffInt = 2; - - return saveCompletion(song, completion, diffInt); - } - - public static function saveWeekScore(week:String, score:Int = 0, diff:Int = 0):Void - { - #if newgrounds - NGio.postScore(score, 'Campaign ID $week'); - #end - - var formattedSong:String = formatSong(week, diff); - - if (songScores.exists(formattedSong)) - { - if (songScores.get(formattedSong) < score) setScore(formattedSong, score); - } - else - { - setScore(formattedSong, score); - } - } - - public static function saveWeekScoreForDifficulty(week:String, score:Int = 0, diff:String = 'normal'):Void - { - var diffInt:Int = 1; - - if (diff == 'easy') diffInt = 0; - else if (diff == 'hard') diffInt = 2; - - saveWeekScore(week, score, diffInt); - } - - static function setCompletion(formattedSong:String, completion:Float):Void - { - songCompletion.set(formattedSong, completion); - FlxG.save.data.songCompletion = songCompletion; - FlxG.save.flush(); - } - - /** - * YOU SHOULD FORMAT SONG WITH formatSong() BEFORE TOSSING IN SONG VARIABLE - */ - static function setScore(formattedSong:String, score:Int):Void - { - /** GeoKureli - * References to Highscore were wrapped in `#if !switch` blocks. I wasn't sure if this - * is because switch doesn't use NGio, or because switch has a different saving method. - * I moved the compiler flag here, rather than using it everywhere else. - */ - #if ! switch - // Reminder that I don't need to format this song, it should come formatted! - songScores.set(formattedSong, score); - FlxG.save.data.songScores = songScores; - FlxG.save.flush(); - #end - } - - public static function formatSong(song:String, diff:Int):String - { - var daSong:String = song; - - if (diff == 0) daSong += '-easy'; - else if (diff == 2) daSong += '-hard'; - - return daSong; - } - - public static function getScore(song:String, diff:Int):Int - { - if (!songScores.exists(formatSong(song, diff))) setScore(formatSong(song, diff), 0); - - return songScores.get(formatSong(song, diff)); - } - - public static function getCompletion(song, diff):Float - { - if (!songCompletion.exists(formatSong(song, diff))) setCompletion(formatSong(song, diff), 0); - - return songCompletion.get(formatSong(song, diff)); - } - - public static function getAllScores():Void - { - trace(songScores.toString()); - } - - public static function getWeekScore(week:Int, diff:Int):Int - { - if (!songScores.exists(formatSong('week' + week, diff))) setScore(formatSong('week' + week, diff), 0); - - return songScores.get(formatSong('week' + week, diff)); - } - - public static function load():Void - { - if (FlxG.save.data.songScores != null) - { - songScores = FlxG.save.data.songScores; - } - - if (FlxG.save.data.songCompletion != null) songCompletion = FlxG.save.data.songCompletion; - } } -// i only do forward metadata cuz george did! - @:forward abstract Tallies(RawTallies) { diff --git a/source/funkin/InitState.hx b/source/funkin/InitState.hx index e7060abd7..ecfa32eb3 100644 --- a/source/funkin/InitState.hx +++ b/source/funkin/InitState.hx @@ -46,7 +46,11 @@ class InitState extends FlxState { setupShit(); - loadSaveData(); + // loadSaveData(); // Moved to Main.hx + // Load player options from save data. + Preferences.init(); + // Load controls from save data. + PlayerSettings.init(); startGame(); } @@ -73,10 +77,6 @@ class InitState extends FlxState FlxG.sound.volumeDownKeys = []; FlxG.sound.muteKeys = []; - // TODO: Make sure volume still saves/loads properly. - // if (FlxG.save.data.volume != null) FlxG.sound.volume = FlxG.save.data.volume; - // if (FlxG.save.data.mute != null) FlxG.sound.muted = FlxG.save.data.mute; - // Set the game to a lower frame rate while it is in the background. FlxG.game.focusLostFramerate = 30; @@ -212,24 +212,6 @@ class InitState extends FlxState ModuleHandler.callOnCreate(); } - /** - * Retrive and parse data from the user's save. - */ - function loadSaveData() - { - // Bind save data. - // TODO: Migrate save data to a better format. - FlxG.save.bind('funkin', 'ninjamuffin99'); - - // Load player options from save data. - PreferencesMenu.initPrefs(); - // Load controls from save data. - PlayerSettings.init(); - // Load highscores from save data. - Highscore.load(); - // TODO: Load level/character/cosmetic unlocks from save data. - } - /** * Start the game. * diff --git a/source/funkin/MainMenuState.hx b/source/funkin/MainMenuState.hx index 7c54357bb..7267a6da8 100644 --- a/source/funkin/MainMenuState.hx +++ b/source/funkin/MainMenuState.hx @@ -13,23 +13,13 @@ import flixel.input.touch.FlxTouch; import flixel.text.FlxText; import flixel.tweens.FlxEase; import flixel.tweens.FlxTween; -import flixel.util.FlxColor; import flixel.util.FlxTimer; -import funkin.NGio; -import funkin.modding.events.ScriptEvent.UpdateScriptEvent; -import funkin.modding.module.ModuleHandler; -import funkin.shaderslmfao.ScreenWipeShader; import funkin.ui.AtlasMenuList; -import funkin.ui.MenuList.MenuItem; import funkin.ui.MenuList; import funkin.ui.title.TitleState; import funkin.ui.story.StoryMenuState; -import funkin.ui.OptionsState; -import funkin.ui.PreferencesMenu; import funkin.ui.Prompt; import funkin.util.WindowUtil; -import lime.app.Application; -import openfl.filters.ShaderFilter; #if discord_rpc import Discord.DiscordClient; #end @@ -82,8 +72,10 @@ class MainMenuState extends MusicBeatState magenta.y = bg.y; magenta.visible = false; magenta.color = 0xFFfd719b; - if (PreferencesMenu.preferences.get('flashing-menu')) add(magenta); - // magenta.scrollFactor.set(); + + // TODO: Why doesn't this line compile I'm going fucking feral + + if (Preferences.flashingLights) add(magenta); menuItems = new MenuTypedList(); add(menuItems); @@ -116,7 +108,7 @@ class MainMenuState extends MusicBeatState #end createMenuItem('options', 'mainmenu/options', function() { - startExitState(new OptionsState()); + startExitState(new funkin.ui.OptionsState()); }); // Reset position of menu items. diff --git a/source/funkin/NGio.hx b/source/funkin/NGio.hx index f2afe84db..e5f60c8b5 100644 --- a/source/funkin/NGio.hx +++ b/source/funkin/NGio.hx @@ -86,10 +86,10 @@ class NGio #end var onSessionFail:Error->Void = null; - if (sessionId == null && FlxG.save.data.sessionId != null) + if (sessionId == null && Save.get().ngSessionId != null) { trace("using stored session id"); - sessionId = FlxG.save.data.sessionId; + sessionId = Save.get().ngSessionId; onSessionFail = function(error) savedSessionFailed = true; } #end @@ -159,8 +159,8 @@ class NGio static function onNGLogin():Void { trace('logged in! user:${NG.core.user.name}'); - FlxG.save.data.sessionId = NG.core.sessionId; - FlxG.save.flush(); + Save.get().ngSessionId = NG.core.sessionId; + Save.get().flush(); // Load medals then call onNGMedalFetch() NG.core.requestMedals(onNGMedalFetch); @@ -174,8 +174,8 @@ class NGio { NG.core.logOut(); - FlxG.save.data.sessionId = null; - FlxG.save.flush(); + Save.get().ngSessionId = null; + Save.get().flush(); } // --- MEDALS diff --git a/source/funkin/PauseSubState.hx b/source/funkin/PauseSubState.hx index 48be31226..a074410ea 100644 --- a/source/funkin/PauseSubState.hx +++ b/source/funkin/PauseSubState.hx @@ -157,6 +157,11 @@ class PauseSubState extends MusicBeatSubState super.update(elapsed); + handleInputs(); + } + + function handleInputs():Void + { var upP = controls.UI_UP_P; var downP = controls.UI_DOWN_P; var accepted = controls.ACCEPT; @@ -224,9 +229,14 @@ class PauseSubState extends MusicBeatSubState FlxTransitionableState.skipNextTransIn = true; FlxTransitionableState.skipNextTransOut = true; - if (PlayStatePlaylist.isStoryMode) openSubState(new funkin.ui.StickerSubState(null, STORY)); + if (PlayStatePlaylist.isStoryMode) + { + openSubState(new funkin.ui.StickerSubState(null, STORY)); + } else + { openSubState(new funkin.ui.StickerSubState(null, FREEPLAY)); + } case 'Exit to Chart Editor': this.close(); diff --git a/source/funkin/PlayerSettings.hx b/source/funkin/PlayerSettings.hx index 54fd559fb..e97cfe384 100644 --- a/source/funkin/PlayerSettings.hx +++ b/source/funkin/PlayerSettings.hx @@ -1,5 +1,6 @@ package funkin; +import funkin.save.Save; import funkin.Controls; import flixel.FlxCamera; import funkin.input.PreciseInputManager; @@ -11,121 +12,36 @@ import flixel.util.FlxSignal; // import props.Player; class PlayerSettings { - static public var numPlayers(default, null) = 0; - static public var numAvatars(default, null) = 0; - static public var player1(default, null):PlayerSettings; - static public var player2(default, null):PlayerSettings; + public static var numPlayers(default, null) = 0; + public static var numAvatars(default, null) = 0; + public static var player1(default, null):PlayerSettings; + public static var player2(default, null):PlayerSettings; - static public var onAvatarAdd(default, null) = new FlxTypedSignalVoid>(); - static public var onAvatarRemove(default, null) = new FlxTypedSignalVoid>(); + public static var onAvatarAdd(default, null) = new FlxTypedSignalVoid>(); + public static var onAvatarRemove(default, null) = new FlxTypedSignalVoid>(); public var id(default, null):Int; public var controls(default, null):Controls; - // public var avatar:Player; - // public var camera(get, never):PlayCamera; - - function new(id:Int) + /** + * Return the PlayerSettings for the given player number, or `null` if that player isn't active. + */ + public static function get(id:Int):Null { - trace('loading player settings for id: $id'); - - this.id = id; - this.controls = new Controls('player$id', None); - - #if CLEAR_INPUT_SAVE - FlxG.save.data.controls = null; - FlxG.save.flush(); - #end - - var useDefault = true; - var controlData = FlxG.save.data.controls; - if (controlData != null) + return switch (id) { - var keyData:Dynamic = null; - if (id == 0 && controlData.p1 != null && controlData.p1.keys != null) keyData = controlData.p1.keys; - else if (id == 1 && controlData.p2 != null && controlData.p2.keys != null) keyData = controlData.p2.keys; - - if (keyData != null) - { - useDefault = false; - trace("loaded key data: " + haxe.Json.stringify(keyData)); - controls.fromSaveData(keyData, Keys); - } - } - - if (useDefault) - { - trace("falling back to default control scheme"); - controls.setKeyboardScheme(Solo); - } - - // Apply loaded settings. - PreciseInputManager.instance.initializeKeys(controls); + case 1: player1; + case 2: player2; + default: null; + }; } - function addGamepad(gamepad:FlxGamepad) - { - var useDefault = true; - var controlData = FlxG.save.data.controls; - if (controlData != null) - { - var padData:Dynamic = null; - if (id == 0 && controlData.p1 != null && controlData.p1.pad != null) padData = controlData.p1.pad; - else if (id == 1 && controlData.p2 != null && controlData.p2.pad != null) padData = controlData.p2.pad; - - if (padData != null) - { - useDefault = false; - trace("loaded pad data: " + haxe.Json.stringify(padData)); - controls.addGamepadWithSaveData(gamepad.id, padData); - } - } - - if (useDefault) controls.addDefaultGamepad(gamepad.id); - } - - public function saveControls() - { - if (FlxG.save.data.controls == null) FlxG.save.data.controls = {}; - - var playerData:{?keys:Dynamic, ?pad:Dynamic} - if (id == 0) - { - if (FlxG.save.data.controls.p1 == null) FlxG.save.data.controls.p1 = {}; - playerData = FlxG.save.data.controls.p1; - } - else - { - if (FlxG.save.data.controls.p2 == null) FlxG.save.data.controls.p2 = {}; - playerData = FlxG.save.data.controls.p2; - } - - var keyData = controls.createSaveData(Keys); - if (keyData != null) - { - playerData.keys = keyData; - trace("saving key data: " + haxe.Json.stringify(keyData)); - } - - if (controls.gamepadsAdded.length > 0) - { - var padData = controls.createSaveData(Gamepad(controls.gamepadsAdded[0])); - if (padData != null) - { - trace("saving pad data: " + haxe.Json.stringify(padData)); - playerData.pad = padData; - } - } - - FlxG.save.flush(); - } - - static public function init():Void + public static function init():Void { if (player1 == null) { - player1 = new PlayerSettings(0); + player1 = new PlayerSettings(1); ++numPlayers; } @@ -137,26 +53,13 @@ class PlayerSettings var gamepad = FlxG.gamepads.getByID(i); if (gamepad != null) onGamepadAdded(gamepad); } + } - // player1.controls.addDefaultGamepad(0); - // } - - // if (numGamepads > 1) - // { - // if (player2 == null) - // { - // player2 = new PlayerSettings(1, None); - // ++numPlayers; - // } - - // var gamepad = FlxG.gamepads.getByID(1); - // if (gamepad == null) - // throw 'Unexpected null gamepad. id:0'; - - // player2.controls.addDefaultGamepad(1); - // } - - // DeviceManager.init(); + public static function reset() + { + player1 = null; + player2 = null; + numPlayers = 0; } static function onGamepadAdded(gamepad:FlxGamepad) @@ -164,86 +67,90 @@ class PlayerSettings player1.addGamepad(gamepad); } - /* - public function setKeyboardScheme(scheme) - { - controls.setKeyboardScheme(scheme); - } - - static public function addAvatar(avatar:Player):PlayerSettings - { - var settings:PlayerSettings; - - if (player1 == null) - { - player1 = new PlayerSettings(0, Solo); - ++numPlayers; - } - - if (player1.avatar == null) - settings = player1; - else - { - if (player2 == null) - { - if (player1.controls.keyboardScheme.match(Duo(true))) - player2 = new PlayerSettings(1, Duo(false)); - else - player2 = new PlayerSettings(1, None); - ++numPlayers; - } - - if (player2.avatar == null) - settings = player2; - else - throw throw 'Invalid number of players: ${numPlayers + 1}'; - } - ++numAvatars; - settings.avatar = avatar; - avatar.settings = settings; - - splitCameras(); - - onAvatarAdd.dispatch(settings); - - return settings; - } - - static public function removeAvatar(avatar:Player):Void - { - var settings:PlayerSettings; - - if (player1 != null && player1.avatar == avatar) - settings = player1; - else if (player2 != null && player2.avatar == avatar) - { - settings = player2; - if (player1.controls.keyboardScheme.match(Duo(_))) - player1.setKeyboardScheme(Solo); - } - else - throw "Cannot remove avatar that is not for a player"; - - settings.avatar = null; - while (settings.controls.gamepadsAdded.length > 0) - { - final id = settings.controls.gamepadsAdded.shift(); - settings.controls.removeGamepad(id); - DeviceManager.releaseGamepad(FlxG.gamepads.getByID(id)); - } - - --numAvatars; - - splitCameras(); - - onAvatarRemove.dispatch(avatar.settings); - } - + /** + * @param id The player number this represents. This was refactored to START AT `1`. */ - static public function reset() + private function new(id:Int) { - player1 = null; - player2 = null; - numPlayers = 0; + trace('loading player settings for id: $id'); + + this.id = id; + this.controls = new Controls('player$id', None); + + addKeyboard(); + } + + function addKeyboard():Void + { + var useDefault = true; + if (Save.get().hasControls(id, Keys)) + { + var keyControlData = Save.get().getControls(id, Keys); + trace("keyControlData: " + haxe.Json.stringify(keyControlData)); + useDefault = false; + controls.fromSaveData(keyControlData, Keys); + } + else + { + useDefault = true; + } + + if (useDefault) + { + trace("Loading default keyboard control scheme"); + controls.setKeyboardScheme(Solo); + } + + PreciseInputManager.instance.initializeKeys(controls); + } + + /** + * Called after an FlxGamepad has been detected. + * @param gamepad The gamepad that was detected. + */ + function addGamepad(gamepad:FlxGamepad) + { + var useDefault = true; + if (Save.get().hasControls(id, Gamepad(gamepad.id))) + { + var padControlData = Save.get().getControls(id, Gamepad(gamepad.id)); + trace("padControlData: " + haxe.Json.stringify(padControlData)); + useDefault = false; + controls.addGamepadWithSaveData(gamepad.id, padControlData); + } + else + { + useDefault = true; + } + + if (useDefault) + { + trace("Loading gamepad control scheme"); + controls.addDefaultGamepad(gamepad.id); + } + PreciseInputManager.instance.initializeButtons(controls, gamepad); + } + + /** + * Save this player's controls to the game's persistent save. + */ + public function saveControls() + { + var keyData = controls.createSaveData(Keys); + if (keyData != null) + { + trace("saving key data: " + haxe.Json.stringify(keyData)); + Save.get().setControls(id, Keys, keyData); + } + + if (controls.gamepadsAdded.length > 0) + { + var padData = controls.createSaveData(Gamepad(controls.gamepadsAdded[0])); + if (padData != null) + { + trace("saving pad data: " + haxe.Json.stringify(padData)); + Save.get().setControls(id, Gamepad(controls.gamepadsAdded[0]), padData); + } + } } } diff --git a/source/funkin/Preferences.hx b/source/funkin/Preferences.hx new file mode 100644 index 000000000..7e3c3c6d7 --- /dev/null +++ b/source/funkin/Preferences.hx @@ -0,0 +1,138 @@ +package funkin; + +import funkin.save.Save; + +/** + * A store of user-configurable, globally relevant values. + */ +class Preferences +{ + /** + * Whether some particularly fowl language is displayed. + * @default `true` + */ + public static var naughtyness(get, set):Bool; + + static function get_naughtyness():Bool + { + return Save.get().options.naughtyness; + } + + static function set_naughtyness(value:Bool):Bool + { + return Save.get().options.naughtyness = value; + } + + /** + * If enabled, the strumline is at the bottom of the screen rather than the top. + * @default `false` + */ + public static var downscroll(get, set):Bool; + + static function get_downscroll():Bool + { + return Save.get().options.downscroll; + } + + static function set_downscroll(value:Bool):Bool + { + return Save.get().options.downscroll = value; + } + + /** + * If disabled, flashing lights in the main menu and other areas will be less intense. + * @default `true` + */ + public static var flashingLights(get, set):Bool; + + static function get_flashingLights():Bool + { + return Save.get().options.flashingLights; + } + + static function set_flashingLights(value:Bool):Bool + { + return Save.get().options.flashingLights = value; + } + + /** + * If disabled, the camera bump synchronized to the beat. + * @default `false` + */ + public static var zoomCamera(get, set):Bool; + + static function get_zoomCamera():Bool + { + return Save.get().options.zoomCamera; + } + + static function set_zoomCamera(value:Bool):Bool + { + return Save.get().options.zoomCamera = value; + } + + /** + * If enabled, an FPS and memory counter will be displayed even if this is not a debug build. + * @default `false` + */ + public static var debugDisplay(get, set):Bool; + + static function get_debugDisplay():Bool + { + return Save.get().options.debugDisplay; + } + + static function set_debugDisplay(value:Bool):Bool + { + if (value != Save.get().options.debugDisplay) + { + toggleDebugDisplay(value); + } + + return Save.get().options.debugDisplay = value; + } + + /** + * If enabled, the game will automatically pause when tabbing out. + * @default `true` + */ + public static var autoPause(get, set):Bool; + + static function get_autoPause():Bool + { + return Save.get().options.autoPause; + } + + static function set_autoPause(value:Bool):Bool + { + if (value != Save.get().options.autoPause) FlxG.autoPause = value; + + return Save.get().options.autoPause = value; + } + + public static function init():Void + { + FlxG.autoPause = Preferences.autoPause; + toggleDebugDisplay(Preferences.debugDisplay); + } + + static function toggleDebugDisplay(show:Bool):Void + { + if (show) + { + // Enable the debug display. + FlxG.stage.addChild(Main.fpsCounter); + #if !html5 + FlxG.stage.addChild(Main.memoryCounter); + #end + } + else + { + // Disable the debug display. + FlxG.stage.removeChild(Main.fpsCounter); + #if !html5 + FlxG.stage.removeChild(Main.memoryCounter); + #end + } + } +} diff --git a/source/funkin/api/newgrounds/NGUtil.hx b/source/funkin/api/newgrounds/NGUtil.hx index 773e2f98f..ba7d5f916 100644 --- a/source/funkin/api/newgrounds/NGUtil.hx +++ b/source/funkin/api/newgrounds/NGUtil.hx @@ -86,10 +86,10 @@ class NGUtil #end var onSessionFail:Error->Void = null; - if (sessionId == null && FlxG.save.data.sessionId != null) + if (sessionId == null && Save.get().ngSessionId != null) { trace("using stored session id"); - sessionId = FlxG.save.data.sessionId; + sessionId = Save.get().ngSessionId; onSessionFail = function(error) savedSessionFailed = true; } #end @@ -159,8 +159,8 @@ class NGUtil static function onNGLogin():Void { trace('logged in! user:${NG.core.user.name}'); - FlxG.save.data.sessionId = NG.core.sessionId; - FlxG.save.flush(); + Save.get().ngSessionId = NG.core.sessionId; + Save.get().flush(); // Load medals then call onNGMedalFetch() NG.core.requestMedals(onNGMedalFetch); @@ -174,8 +174,8 @@ class NGUtil { NG.core.logOut(); - FlxG.save.data.sessionId = null; - FlxG.save.flush(); + Save.get().ngSessionId = null; + Save.get().flush(); } // --- MEDALS diff --git a/source/funkin/audiovis/ABotVis.hx b/source/funkin/audiovis/ABotVis.hx index 2018a99b3..060bddcf7 100644 --- a/source/funkin/audiovis/ABotVis.hx +++ b/source/funkin/audiovis/ABotVis.hx @@ -7,7 +7,6 @@ import flixel.graphics.frames.FlxAtlasFrames; import flixel.group.FlxSpriteGroup.FlxTypedSpriteGroup; import flixel.math.FlxMath; import flixel.sound.FlxSound; -import funkin.ui.PreferencesMenu.CheckboxThingie; using Lambda; diff --git a/source/funkin/data/song/SongData.hx b/source/funkin/data/song/SongData.hx index d557bd39c..9340e46c9 100644 --- a/source/funkin/data/song/SongData.hx +++ b/source/funkin/data/song/SongData.hx @@ -4,6 +4,7 @@ import flixel.util.typeLimit.OneOfTwo; import funkin.data.song.SongRegistry; import thx.semver.Version; +@:nullSafety class SongMetadata { /** @@ -42,7 +43,7 @@ class SongMetadata public var timeChanges:Array; /** - * Defaults to `default` or `''`. Populated later. + * Defaults to `Constants.DEFAULT_VARIATION`. Populated later. */ @:jignored public var variation:String; @@ -228,10 +229,10 @@ class SongMusicData public var timeChanges:Array; /** - * Defaults to `default` or `''`. Populated later. + * Defaults to `Constants.DEFAULT_VARIATION`. Populated later. */ @:jignored - public var variation:String = Constants.DEFAULT_VARIATION; + public var variation:String; public function new(songName:String, artist:String, variation:String = 'default') { @@ -375,6 +376,9 @@ class SongChartData @:default(funkin.data.song.SongRegistry.DEFAULT_GENERATEDBY) public var generatedBy:String; + /** + * Defaults to `Constants.DEFAULT_VARIATION`. Populated later. + */ @:jignored public var variation:String; diff --git a/source/funkin/data/song/SongDataUtils.hx b/source/funkin/data/song/SongDataUtils.hx index 4b9318df2..ee3dfe98c 100644 --- a/source/funkin/data/song/SongDataUtils.hx +++ b/source/funkin/data/song/SongDataUtils.hx @@ -21,11 +21,21 @@ class SongDataUtils * @param notes The notes to modify. * @param offset The time difference to apply in milliseconds. */ - public static function offsetSongNoteData(notes:Array, offset:Int):Array + public static function offsetSongNoteData(notes:Array, offset:Float):Array { - return notes.map(function(note:SongNoteData):SongNoteData { - return new SongNoteData(note.time + offset, note.data, note.length, note.kind); - }); + var offsetNote = function(note:SongNoteData):SongNoteData { + var time:Float = note.time + offset; + var data:Int = note.data; + var length:Float = note.length; + var kind:String = note.kind; + return new SongNoteData(time, data, length, kind); + }; + + trace(notes); + trace(notes[0]); + var result = [for (i in 0...notes.length) offsetNote(notes[i])]; + trace(result); + return result; } /** @@ -36,7 +46,7 @@ class SongDataUtils * @param events The events to modify. * @param offset The time difference to apply in milliseconds. */ - public static function offsetSongEventData(events:Array, offset:Int):Array + public static function offsetSongEventData(events:Array, offset:Float):Array { return events.map(function(event:SongEventData):SongEventData { return new SongEventData(event.time + offset, event.event, event.value); @@ -152,7 +162,8 @@ class SongDataUtils */ public static function writeItemsToClipboard(data:SongClipboardItems):Void { - var dataString = SerializerUtil.toJSON(data); + var writer = new json2object.JsonWriter(); + var dataString:String = writer.write(data, ' '); ClipboardUtil.setClipboard(dataString); @@ -170,19 +181,24 @@ class SongDataUtils trace('Read ${notesString.length} characters from clipboard.'); - var data:SongClipboardItems = notesString.parseJSON(); - - if (data == null) + var parser = new json2object.JsonParser(); + parser.fromJson(notesString, 'clipboard'); + if (parser.errors.length > 0) { - trace('Failed to parse notes from clipboard.'); + trace('[SongDataUtils] Error parsing note JSON data from clipboard.'); + for (error in parser.errors) + DataError.printError(error); return { + valid: false, notes: [], events: [] }; } else { + var data:SongClipboardItems = parser.value; trace('Parsed ' + data.notes.length + ' notes and ' + data.events.length + ' from clipboard.'); + data.valid = true; return data; } } @@ -230,6 +246,7 @@ class SongDataUtils typedef SongClipboardItems = { + ?valid:Bool, notes:Array, events:Array } diff --git a/source/funkin/data/song/SongRegistry.hx b/source/funkin/data/song/SongRegistry.hx index cf2da14f7..889fca707 100644 --- a/source/funkin/data/song/SongRegistry.hx +++ b/source/funkin/data/song/SongRegistry.hx @@ -156,7 +156,7 @@ class SongRegistry extends BaseRegistry return cleanMetadata(parser.value, variation); } - public function parseEntryMetadataWithMigration(id:String, ?variation:String, version:thx.semver.Version):Null + public function parseEntryMetadataWithMigration(id:String, variation:String, version:thx.semver.Version):Null { variation = variation == null ? Constants.DEFAULT_VARIATION : variation; @@ -192,7 +192,7 @@ class SongRegistry extends BaseRegistry } } - function parseEntryMetadata_v2_0_0(id:String, variation:String = ""):Null + function parseEntryMetadata_v2_0_0(id:String, ?variation:String):Null { variation = variation == null ? Constants.DEFAULT_VARIATION : variation; diff --git a/source/funkin/import.hx b/source/funkin/import.hx index 06fe2bfa8..1c3a0fdb4 100644 --- a/source/funkin/import.hx +++ b/source/funkin/import.hx @@ -4,6 +4,7 @@ package; // Only import these when we aren't in a macro. import funkin.util.Constants; import funkin.Paths; +import funkin.Preferences; import flixel.FlxG; // This one in particular causes a compile error if you're using macros. // These are great. diff --git a/source/funkin/input/PreciseInputManager.hx b/source/funkin/input/PreciseInputManager.hx index 4cce0964d..897f738fb 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,17 +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(); - FlxG.stage.removeEventListener(KeyboardEvent.KEY_DOWN, onKeyDown); - FlxG.stage.removeEventListener(KeyboardEvent.KEY_UP, onKeyUp); + _buttonList = []; + _buttonListMap = []; + _buttonListArray = []; + _buttonListDir = new Map>(); + + _dirPressTimestamps = new Map(); + _dirReleaseTimestamps = new Map(); + + // Keyboard FlxG.stage.application.window.onKeyDownPrecise.add(handleKeyDown); FlxG.stage.application.window.onKeyUpPrecise.add(handleKeyUp); @@ -84,6 +120,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 +185,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 +249,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 +312,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 +328,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 +390,31 @@ class PreciseInputManager extends FlxKeyManager _keyListMap.clear(); _keyListDir.clear(); } + + function clearButtons():Void + { + _buttonListArray = []; + _buttonListDir.clear(); + + for (gamepad in _deviceBinds.keys()) + { + var callbacks = _deviceBinds.get(gamepad); + var limeGamepad = FlxGamepadUtil.getLimeGamepad(gamepad); + limeGamepad.onButtonDownPrecise.remove(callbacks.onButtonDown); + limeGamepad.onButtonUpPrecise.remove(callbacks.onButtonUp); + } + _deviceBinds.clear(); + } + + public override function destroy():Void + { + // Keyboard + FlxG.stage.application.window.onKeyDownPrecise.remove(handleKeyDown); + FlxG.stage.application.window.onKeyUpPrecise.remove(handleKeyUp); + + clearKeys(); + clearButtons(); + } } class PreciseInputList extends FlxKeyList diff --git a/source/funkin/modding/PolymodHandler.hx b/source/funkin/modding/PolymodHandler.hx index f7f69428b..7716f0f02 100644 --- a/source/funkin/modding/PolymodHandler.hx +++ b/source/funkin/modding/PolymodHandler.hx @@ -14,6 +14,7 @@ import funkin.data.level.LevelRegistry; import funkin.data.notestyle.NoteStyleRegistry; import funkin.play.cutscene.dialogue.ConversationDataParser; import funkin.play.cutscene.dialogue.DialogueBoxDataParser; +import funkin.save.Save; import funkin.play.cutscene.dialogue.SpeakerDataParser; import funkin.data.song.SongRegistry; @@ -59,7 +60,7 @@ class PolymodHandler createModRoot(); trace("Initializing Polymod (using configured mods)..."); - loadModsById(getEnabledModIds()); + loadModsById(Save.get().enabledModIds); } /** @@ -232,33 +233,9 @@ class PolymodHandler return modIds; } - public static function setEnabledMods(newModList:Array):Void - { - FlxG.save.data.enabledMods = newModList; - // Make sure to COMMIT the changes. - FlxG.save.flush(); - } - - /** - * Returns the list of enabled mods. - * @return Array - */ - public static function getEnabledModIds():Array - { - if (FlxG.save.data.enabledMods == null) - { - // NOTE: If the value is null, the enabled mod list is unconfigured. - // Currently, we default to disabling newly installed mods. - // If we want to auto-enable new mods, but otherwise leave the configured list in place, - // we will need some custom logic. - FlxG.save.data.enabledMods = []; - } - return FlxG.save.data.enabledMods; - } - public static function getEnabledMods():Array { - var modIds = getEnabledModIds(); + var modIds = Save.get().enabledModIds; var modMetadata = getAllMods(); var enabledMods = []; for (item in modMetadata) diff --git a/source/funkin/play/GameOverSubState.hx b/source/funkin/play/GameOverSubState.hx index 15ed0421e..a3aeb4139 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. @@ -292,7 +291,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 9e1c89c8a..cafb79a76 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, PlayStatePlaylist.campaignDifficulty); - } - // FlxG.save.data.weekUnlocked = StoryMenuState.weekUnlocked; - FlxG.save.flush(); + var data:SaveScoreData = + { + 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) { 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/save/Save.hx b/source/funkin/save/Save.hx new file mode 100644 index 000000000..2666d2bff --- /dev/null +++ b/source/funkin/save/Save.hx @@ -0,0 +1,700 @@ +package funkin.save; + +import flixel.util.FlxSave; +import funkin.save.migrator.SaveDataMigrator; +import thx.semver.Version; +import funkin.Controls.Device; +import funkin.save.migrator.RawSaveData_v1_0_0; + +@:nullSafety +@:forward(volume, mute) +abstract Save(RawSaveData) +{ + public static final SAVE_DATA_VERSION:thx.semver.Version = "2.0.0"; + public static final SAVE_DATA_VERSION_RULE:thx.semver.VersionRule = "2.0.x"; + + // We load this version's saves from a new save path, to maintain SOME level of backwards compatibility. + static final SAVE_PATH:String = 'FunkinCrew'; + static final SAVE_NAME:String = 'Funkin'; + + static final SAVE_PATH_LEGACY:String = 'ninjamuffin99'; + static final SAVE_NAME_LEGACY:String = 'funkin'; + + public static function load():Void + { + trace("[SAVE] Loading save..."); + + // Bind save data. + loadFromSlot(1); + } + + public static function get():Save + { + return FlxG.save.data; + } + + /** + * Constructing a new Save will load the default values. + */ + public function new() + { + this = + { + version: Save.SAVE_DATA_VERSION, + + volume: 1.0, + mute: false, + + api: + { + newgrounds: + { + sessionId: null, + } + }, + scores: + { + // No saved scores. + levels: [], + songs: [], + }, + options: + { + // Reasonable defaults. + naughtyness: true, + downscroll: false, + flashingLights: true, + zoomCamera: true, + debugDisplay: false, + autoPause: true, + + controls: + { + // Leave controls blank so defaults are loaded. + p1: + { + keyboard: {}, + gamepad: {}, + }, + p2: + { + keyboard: {}, + gamepad: {}, + }, + }, + }, + + mods: + { + // No mods enabled. + enabledMods: [], + modOptions: [], + }, + + optionsChartEditor: + { + // Reasonable defaults. + }, + }; + } + + public var options(get, never):SaveDataOptions; + + function get_options():SaveDataOptions + { + return this.options; + } + + public var modOptions(get, never):Map; + + function get_modOptions():Map + { + return this.mods.modOptions; + } + + /** + * The current session ID for the logged-in Newgrounds user, or null if the user is cringe. + */ + public var ngSessionId(get, set):Null; + + function get_ngSessionId():Null + { + return this.api.newgrounds.sessionId; + } + + function set_ngSessionId(value:Null):Null + { + return this.api.newgrounds.sessionId = value; + } + + public var enabledModIds(get, set):Array; + + function get_enabledModIds():Array + { + return this.mods.enabledMods; + } + + function set_enabledModIds(value:Array):Array + { + return this.mods.enabledMods = value; + } + + /** + * Return the score the user achieved for a given level on a given difficulty. + * + * @param levelId The ID of the level/week. + * @param difficultyId The difficulty to check. + * @return A data structure containing score, judgement counts, and accuracy. Returns `null` if no score is saved. + */ + public function getLevelScore(levelId:String, difficultyId:String = 'normal'):Null + { + var level = this.scores.levels.get(levelId); + if (level == null) + { + level = []; + this.scores.levels.set(levelId, level); + } + + return level.get(difficultyId); + } + + /** + * Apply the score the user achieved for a given level on a given difficulty. + */ + public function setLevelScore(levelId:String, difficultyId:String, score:SaveScoreData):Void + { + var level = this.scores.levels.get(levelId); + if (level == null) + { + level = []; + this.scores.levels.set(levelId, level); + } + level.set(difficultyId, score); + + flush(); + } + + public function isLevelHighScore(levelId:String, difficultyId:String = 'normal', score:SaveScoreData):Bool + { + var level = this.scores.levels.get(levelId); + if (level == null) + { + level = []; + this.scores.levels.set(levelId, level); + } + + var currentScore = level.get(difficultyId); + if (currentScore == null) + { + return true; + } + + return score.score > currentScore.score; + } + + public function hasBeatenLevel(levelId:String, ?difficultyList:Array):Bool + { + if (difficultyList == null) + { + difficultyList = ['easy', 'normal', 'hard']; + } + for (difficulty in difficultyList) + { + var score:Null = getLevelScore(levelId, difficulty); + // TODO: Do we need to check accuracy/score here? + if (score != null) + { + return true; + } + } + return false; + } + + /** + * Return the score the user achieved for a given song on a given difficulty. + * + * @param songId The ID of the song. + * @param difficultyId The difficulty to check. + * @return A data structure containing score, judgement counts, and accuracy. Returns `null` if no score is saved. + */ + public function getSongScore(songId:String, difficultyId:String = 'normal'):Null + { + var song = this.scores.songs.get(songId); + if (song == null) + { + song = []; + this.scores.songs.set(songId, song); + } + return song.get(difficultyId); + } + + /** + * Apply the score the user achieved for a given song on a given difficulty. + */ + public function setSongScore(songId:String, difficultyId:String, score:SaveScoreData):Void + { + var song = this.scores.songs.get(songId); + if (song == null) + { + song = []; + this.scores.songs.set(songId, song); + } + song.set(difficultyId, score); + + flush(); + } + + /** + * Is the provided score data better than the current high score for the given song? + * @param songId The song ID to check. + * @param difficultyId The difficulty to check. + * @param score The score to check. + * @return Whether the score is better than the current high score. + */ + public function isSongHighScore(songId:String, difficultyId:String = 'normal', score:SaveScoreData):Bool + { + var song = this.scores.songs.get(songId); + if (song == null) + { + song = []; + this.scores.songs.set(songId, song); + } + + var currentScore = song.get(difficultyId); + if (currentScore == null) + { + return true; + } + + return score.score > currentScore.score; + } + + /** + * Has the provided song been beaten on one of the listed difficulties? + * @param songId The song ID to check. + * @param difficultyList The difficulties to check. Defaults to `easy`, `normal`, and `hard`. + * @return Whether the song has been beaten on any of the listed difficulties. + */ + public function hasBeatenSong(songId:String, ?difficultyList:Array):Bool + { + if (difficultyList == null) + { + difficultyList = ['easy', 'normal', 'hard']; + } + for (difficulty in difficultyList) + { + var score:Null = getSongScore(songId, difficulty); + // TODO: Do we need to check accuracy/score here? + if (score != null) + { + return true; + } + } + return false; + } + + public function getControls(playerId:Int, inputType:Device):SaveControlsData + { + switch (inputType) + { + case Keys: + return (playerId == 0) ? this.options.controls.p1.keyboard : this.options.controls.p2.keyboard; + case Gamepad(_): + return (playerId == 0) ? this.options.controls.p1.gamepad : this.options.controls.p2.gamepad; + } + } + + public function hasControls(playerId:Int, inputType:Device):Bool + { + var controls = getControls(playerId, inputType); + var controlsFields = Reflect.fields(controls); + return controlsFields.length > 0; + } + + public function setControls(playerId:Int, inputType:Device, controls:SaveControlsData):Void + { + switch (inputType) + { + case Keys: + if (playerId == 0) + { + this.options.controls.p1.keyboard = controls; + } + else + { + this.options.controls.p2.keyboard = controls; + } + case Gamepad(_): + if (playerId == 0) + { + this.options.controls.p1.gamepad = controls; + } + else + { + this.options.controls.p2.gamepad = controls; + } + } + + flush(); + } + + public function isCharacterUnlocked(characterId:String):Bool + { + switch (characterId) + { + case 'bf': + return true; + case 'pico': + return hasBeatenLevel('weekend1'); + default: + trace('Unknown character ID: ' + characterId); + return true; + } + } + + /** + * Call this to make sure the save data is written to disk. + */ + public function flush():Void + { + FlxG.save.flush(); + } + + /** + * If you set slot to `2`, it will load an independe + * @param slot + */ + static function loadFromSlot(slot:Int):Void + { + trace("[SAVE] Loading save from slot " + slot + "..."); + + FlxG.save.bind('$SAVE_NAME${slot}', SAVE_PATH); + + if (FlxG.save.isEmpty()) + { + trace('[SAVE] Save data is empty, checking for legacy save data...'); + var legacySaveData = fetchLegacySaveData(); + if (legacySaveData != null) + { + trace('[SAVE] Found legacy save data, converting...'); + FlxG.save.mergeData(SaveDataMigrator.migrateFromLegacy(legacySaveData)); + } + } + else + { + trace('[SAVE] Loaded save data.'); + FlxG.save.mergeData(SaveDataMigrator.migrate(FlxG.save.data)); + } + + trace('[SAVE] Done loading save data.'); + trace(FlxG.save.data); + } + + static function fetchLegacySaveData():Null + { + trace("[SAVE] Checking for legacy save data..."); + var legacySave:FlxSave = new FlxSave(); + legacySave.bind(SAVE_NAME_LEGACY, SAVE_PATH_LEGACY); + if (legacySave?.data == null) + { + trace("[SAVE] No legacy save data found."); + return null; + } + else + { + trace("[SAVE] Legacy save data found."); + trace(legacySave.data); + return cast legacySave.data; + } + } +} + +/** + * An anonymous structure containingg all the user's save data. + */ +typedef RawSaveData = +{ + // Flixel save data. + var volume:Float; + var mute:Bool; + + /** + * A semantic versioning string for the save data format. + */ + var version:Version; + + var api:SaveApiData; + + /** + * The user's saved scores. + */ + var scores:SaveHighScoresData; + + /** + * The user's preferences. + */ + var options:SaveDataOptions; + + var mods:SaveDataMods; + + /** + * The user's preferences specific to the Chart Editor. + */ + var optionsChartEditor:SaveDataChartEditorOptions; +}; + +typedef SaveApiData = +{ + var newgrounds:SaveApiNewgroundsData; +} + +typedef SaveApiNewgroundsData = +{ + var sessionId:Null; +} + +/** + * An anoymous structure containing options about the user's high scores. + */ +typedef SaveHighScoresData = +{ + /** + * Scores for each level (or week). + */ + var levels:SaveScoreLevelsData; + + /** + * Scores for individual songs. + */ + var songs:SaveScoreSongsData; +}; + +typedef SaveDataMods = +{ + var enabledMods:Array; + var modOptions:Map; +} + +/** + * Key is the level ID, value is the SaveScoreLevelData. + */ +typedef SaveScoreLevelsData = Map; + +/** + * Key is the song ID, value is the data for each difficulty. + */ +typedef SaveScoreSongsData = Map; + +/** + * Key is the difficulty ID, value is the score. + */ +typedef SaveScoreDifficultiesData = Map; + +/** + * An individual score. Contains the score, accuracy, and count of each judgement hit. + */ +typedef SaveScoreData = +{ + /** + * The score achieved. + */ + var score:Int; + + /** + * The count of each judgement hit. + */ + var tallies:SaveScoreTallyData; + + /** + * The accuracy percentage. + */ + var accuracy:Float; +} + +typedef SaveScoreTallyData = +{ + var killer:Int; + var sick:Int; + var good:Int; + var bad:Int; + var shit:Int; + var missed:Int; + var combo:Int; + var maxCombo:Int; + var totalNotesHit:Int; + var totalNotes:Int; +} + +/** + * An anonymous structure containing all the user's options and preferences for the main game. + * Every time you add a new option, it needs to be added here. + */ +typedef SaveDataOptions = +{ + /** + * Whether some particularly fowl language is displayed. + * @default `true` + */ + var naughtyness:Bool; + + /** + * If enabled, the strumline is at the bottom of the screen rather than the top. + * @default `false` + */ + var downscroll:Bool; + + /** + * If disabled, flashing lights in the main menu and other areas will be less intense. + * @default `true` + */ + var flashingLights:Bool; + + /** + * If disabled, the camera bump synchronized to the beat. + * @default `false` + */ + var zoomCamera:Bool; + + /** + * If enabled, an FPS and memory counter will be displayed even if this is not a debug build. + * @default `false` + */ + var debugDisplay:Bool; + + /** + * If enabled, the game will automatically pause when tabbing out. + * @default `true` + */ + var autoPause:Bool; + + var controls: + { + var p1: + { + var keyboard:SaveControlsData; + var gamepad:SaveControlsData; + }; + var p2: + { + var keyboard:SaveControlsData; + var gamepad:SaveControlsData; + }; + }; +}; + +/** + * An anonymous structure containing a specific player's bound keys. + * Each key is an action name and each value is an array of keycodes. + * + * If a keybind is `null`, it needs to be reinitialized to the default. + * If a keybind is `[]`, it is UNBOUND by the user and should not be rebound. + */ +typedef SaveControlsData = +{ + /** + * Keybind for navigating in the menu. + * @default `Up Arrow` + */ + var ?UI_UP:Array; + + /** + * Keybind for navigating in the menu. + * @default `Left Arrow` + */ + var ?UI_LEFT:Array; + + /** + * Keybind for navigating in the menu. + * @default `Right Arrow` + */ + var ?UI_RIGHT:Array; + + /** + * Keybind for navigating in the menu. + * @default `Down Arrow` + */ + var ?UI_DOWN:Array; + + /** + * Keybind for hitting notes. + * @default `A` and `Left Arrow` + */ + var ?NOTE_LEFT:Array; + + /** + * Keybind for hitting notes. + * @default `W` and `Up Arrow` + */ + var ?NOTE_UP:Array; + + /** + * Keybind for hitting notes. + * @default `S` and `Down Arrow` + */ + var ?NOTE_DOWN:Array; + + /** + * Keybind for hitting notes. + * @default `D` and `Right Arrow` + */ + var ?NOTE_RIGHT:Array; + + /** + * Keybind for continue/OK in menus. + * @default `Enter` and `Space` + */ + var ?ACCEPT:Array; + + /** + * Keybind for back/cancel in menus. + * @default `Escape` + */ + var ?BACK:Array; + + /** + * Keybind for pausing the game. + * @default `Escape` + */ + var ?PAUSE:Array; + + /** + * Keybind for advancing cutscenes. + * @default `Z` and `Space` and `Enter` + */ + var ?CUTSCENE_ADVANCE:Array; + + /** + * Keybind for skipping a cutscene. + * @default `Escape` + */ + var ?CUTSCENE_SKIP:Array; + + /** + * Keybind for increasing volume. + * @default `Plus` + */ + var ?VOLUME_UP:Array; + + /** + * Keybind for decreasing volume. + * @default `Minus` + */ + var ?VOLUME_DOWN:Array; + + /** + * Keybind for muting/unmuting volume. + * @default `Zero` + */ + var ?VOLUME_MUTE:Array; + + /** + * Keybind for restarting a song. + * @default `R` + */ + var ?RESET:Array; +} + +/** + * An anonymous structure containing all the user's options and preferences, specific to the Chart Editor. + */ +typedef SaveDataChartEditorOptions = {}; diff --git a/source/funkin/save/migrator/RawSaveData_v1_0_0.hx b/source/funkin/save/migrator/RawSaveData_v1_0_0.hx new file mode 100644 index 000000000..b71102cce --- /dev/null +++ b/source/funkin/save/migrator/RawSaveData_v1_0_0.hx @@ -0,0 +1,52 @@ +package funkin.save.migrator; + +import thx.semver.Version; + +typedef RawSaveData_v1_0_0 = +{ + var seenVideo:Bool; + var mute:Bool; + var volume:Float; + + var sessionId:String; + + var songCompletion:Map; + + var songScores:Map; + + var ?controls: + { + ?p1:SavePlayerControlsData_v1_0_0, + ?p2:SavePlayerControlsData_v1_0_0 + }; + var enabledMods:Array; + var weeksUnlocked:Array; + var windowSettings:Array; +} + +typedef SavePlayerControlsData_v1_0_0 = +{ + var keys:SaveControlsData_v1_0_0; + var pad:SaveControlsData_v1_0_0; +}; + +typedef SaveControlsData_v1_0_0 = +{ + var ?ACCEPT:Array; + var ?BACK:Array; + var ?CUTSCENE_ADVANCE:Array; + var ?CUTSCENE_SKIP:Array; + var ?NOTE_DOWN:Array; + var ?NOTE_LEFT:Array; + var ?NOTE_RIGHT:Array; + var ?NOTE_UP:Array; + var ?PAUSE:Array; + var ?RESET:Array; + var ?UI_DOWN:Array; + var ?UI_LEFT:Array; + var ?UI_RIGHT:Array; + var ?UI_UP:Array; + var ?VOLUME_DOWN:Array; + var ?VOLUME_MUTE:Array; + var ?VOLUME_UP:Array; +}; diff --git a/source/funkin/save/migrator/SaveDataMigrator.hx b/source/funkin/save/migrator/SaveDataMigrator.hx new file mode 100644 index 000000000..e7b7c7583 --- /dev/null +++ b/source/funkin/save/migrator/SaveDataMigrator.hx @@ -0,0 +1,322 @@ +package funkin.save.migrator; + +import funkin.save.Save; +import funkin.save.migrator.RawSaveData_v1_0_0; +import thx.semver.Version; +import funkin.util.VersionUtil; + +@:nullSafety +class SaveDataMigrator +{ + /** + * Migrate from one 2.x version to another. + */ + public static function migrate(inputData:Dynamic):Save + { + // This deserializes directly into a `Version` object, not a `String`. + var version:Null = inputData?.version ?? null; + + if (version == null) + { + trace('[SAVE] No version found in save data! Returning blank data.'); + trace(inputData); + return new Save(); + } + else + { + if (VersionUtil.validateVersionStr(version, Save.SAVE_DATA_VERSION_RULE)) + { + // Simply cast the structured data. + var save:Save = inputData; + return save; + } + else + { + trace('[SAVE] Invalid save data version! Returning blank data.'); + trace(inputData); + return new Save(); + } + } + } + + /** + * Migrate from 1.x to the latest version. + */ + public static function migrateFromLegacy(inputData:Dynamic):Save + { + var inputSaveData:RawSaveData_v1_0_0 = cast inputData; + + var result:Save = new Save(); + + result.volume = inputSaveData.volume; + result.mute = inputSaveData.mute; + + result.ngSessionId = inputSaveData.sessionId; + + // TODO: Port over the save data from the legacy save data format. + migrateLegacyScores(result, inputSaveData); + + migrateLegacyControls(result, inputSaveData); + + return result; + } + + static function migrateLegacyScores(result:Save, inputSaveData:RawSaveData_v1_0_0):Void + { + if (inputSaveData.songCompletion == null) + { + inputSaveData.songCompletion = []; + } + + if (inputSaveData.songScores == null) + { + inputSaveData.songScores = []; + } + + migrateLegacyLevelScore(result, inputSaveData, 'week0'); + migrateLegacyLevelScore(result, inputSaveData, 'week1'); + migrateLegacyLevelScore(result, inputSaveData, 'week2'); + migrateLegacyLevelScore(result, inputSaveData, 'week3'); + migrateLegacyLevelScore(result, inputSaveData, 'week4'); + migrateLegacyLevelScore(result, inputSaveData, 'week5'); + migrateLegacyLevelScore(result, inputSaveData, 'week6'); + migrateLegacyLevelScore(result, inputSaveData, 'week7'); + + migrateLegacySongScore(result, inputSaveData, ['tutorial', 'Tutorial']); + + migrateLegacySongScore(result, inputSaveData, ['bopeebo', 'Bopeebo']); + migrateLegacySongScore(result, inputSaveData, ['fresh', 'Fresh']); + migrateLegacySongScore(result, inputSaveData, ['dadbattle', 'Dadbattle']); + + migrateLegacySongScore(result, inputSaveData, ['monster', 'Monster']); + migrateLegacySongScore(result, inputSaveData, ['south', 'South']); + migrateLegacySongScore(result, inputSaveData, ['spookeez', 'Spookeez']); + + migrateLegacySongScore(result, inputSaveData, ['pico', 'Pico']); + migrateLegacySongScore(result, inputSaveData, ['philly-nice', 'Philly', 'philly', 'Philly-Nice']); + migrateLegacySongScore(result, inputSaveData, ['blammed', 'Blammed']); + + migrateLegacySongScore(result, inputSaveData, ['satin-panties', 'Satin-Panties']); + migrateLegacySongScore(result, inputSaveData, ['high', 'High']); + migrateLegacySongScore(result, inputSaveData, ['milf', 'Milf', 'MILF']); + + migrateLegacySongScore(result, inputSaveData, ['cocoa', 'Cocoa']); + migrateLegacySongScore(result, inputSaveData, ['eggnog', 'Eggnog']); + migrateLegacySongScore(result, inputSaveData, ['winter-horrorland', 'Winter-Horrorland']); + + migrateLegacySongScore(result, inputSaveData, ['senpai', 'Senpai']); + migrateLegacySongScore(result, inputSaveData, ['roses', 'Roses']); + migrateLegacySongScore(result, inputSaveData, ['thorns', 'Thorns']); + + migrateLegacySongScore(result, inputSaveData, ['ugh', 'Ugh']); + migrateLegacySongScore(result, inputSaveData, ['guns', 'Guns']); + migrateLegacySongScore(result, inputSaveData, ['stress', 'Stress']); + } + + static function migrateLegacyLevelScore(result:Save, inputSaveData:RawSaveData_v1_0_0, levelId:String):Void + { + var scoreDataEasy:SaveScoreData = + { + score: inputSaveData.songScores.get('${levelId}-easy') ?? 0, + accuracy: inputSaveData.songCompletion.get('${levelId}-easy') ?? 0.0, + tallies: + { + killer: 0, + sick: 0, + good: 0, + bad: 0, + shit: 0, + missed: 0, + combo: 0, + maxCombo: 0, + totalNotesHit: 0, + totalNotes: 0, + } + }; + result.setLevelScore(levelId, 'easy', scoreDataEasy); + + var scoreDataNormal:SaveScoreData = + { + score: inputSaveData.songScores.get('${levelId}') ?? 0, + accuracy: inputSaveData.songCompletion.get('${levelId}') ?? 0.0, + tallies: + { + killer: 0, + sick: 0, + good: 0, + bad: 0, + shit: 0, + missed: 0, + combo: 0, + maxCombo: 0, + totalNotesHit: 0, + totalNotes: 0, + } + }; + result.setLevelScore(levelId, 'normal', scoreDataNormal); + + var scoreDataHard:SaveScoreData = + { + score: inputSaveData.songScores.get('${levelId}-hard') ?? 0, + accuracy: inputSaveData.songCompletion.get('${levelId}-hard') ?? 0.0, + tallies: + { + killer: 0, + sick: 0, + good: 0, + bad: 0, + shit: 0, + missed: 0, + combo: 0, + maxCombo: 0, + totalNotesHit: 0, + totalNotes: 0, + } + }; + result.setLevelScore(levelId, 'hard', scoreDataHard); + } + + static function migrateLegacySongScore(result:Save, inputSaveData:RawSaveData_v1_0_0, songIds:Array):Void + { + var scoreDataEasy:SaveScoreData = + { + score: 0, + accuracy: 0, + tallies: + { + killer: 0, + sick: 0, + good: 0, + bad: 0, + shit: 0, + missed: 0, + combo: 0, + maxCombo: 0, + totalNotesHit: 0, + totalNotes: 0, + } + }; + + for (songId in songIds) + { + scoreDataEasy.score = Std.int(Math.max(scoreDataEasy.score, inputSaveData.songScores.get('${songId}-easy') ?? 0)); + scoreDataEasy.accuracy = Math.max(scoreDataEasy.accuracy, inputSaveData.songCompletion.get('${songId}-easy') ?? 0.0); + } + result.setSongScore(songIds[0], 'easy', scoreDataEasy); + + var scoreDataNormal:SaveScoreData = + { + score: 0, + accuracy: 0, + tallies: + { + killer: 0, + sick: 0, + good: 0, + bad: 0, + shit: 0, + missed: 0, + combo: 0, + maxCombo: 0, + totalNotesHit: 0, + totalNotes: 0, + } + }; + + for (songId in songIds) + { + scoreDataNormal.score = Std.int(Math.max(scoreDataNormal.score, inputSaveData.songScores.get('${songId}') ?? 0)); + scoreDataNormal.accuracy = Math.max(scoreDataNormal.accuracy, inputSaveData.songCompletion.get('${songId}') ?? 0.0); + } + result.setSongScore(songIds[0], 'normal', scoreDataNormal); + + var scoreDataHard:SaveScoreData = + { + score: 0, + accuracy: 0, + tallies: + { + killer: 0, + sick: 0, + good: 0, + bad: 0, + shit: 0, + missed: 0, + combo: 0, + maxCombo: 0, + totalNotesHit: 0, + totalNotes: 0, + } + }; + + for (songId in songIds) + { + scoreDataHard.score = Std.int(Math.max(scoreDataHard.score, inputSaveData.songScores.get('${songId}-hard') ?? 0)); + scoreDataHard.accuracy = Math.max(scoreDataHard.accuracy, inputSaveData.songCompletion.get('${songId}-hard') ?? 0.0); + } + result.setSongScore(songIds[0], 'hard', scoreDataHard); + } + + static function migrateLegacyControls(result:Save, inputSaveData:RawSaveData_v1_0_0):Void + { + var p1Data = inputSaveData?.controls?.p1; + if (p1Data != null) + { + migrateLegacyPlayerControls(result, 1, p1Data); + } + + var p2Data = inputSaveData?.controls?.p2; + if (p2Data != null) + { + migrateLegacyPlayerControls(result, 2, p2Data); + } + } + + static function migrateLegacyPlayerControls(result:Save, playerId:Int, controlsData:SavePlayerControlsData_v1_0_0):Void + { + var outputKeyControls:SaveControlsData = + { + ACCEPT: controlsData?.keys?.ACCEPT ?? null, + BACK: controlsData?.keys?.BACK ?? null, + CUTSCENE_ADVANCE: controlsData?.keys?.CUTSCENE_ADVANCE ?? null, + CUTSCENE_SKIP: controlsData?.keys?.CUTSCENE_SKIP ?? null, + NOTE_DOWN: controlsData?.keys?.NOTE_DOWN ?? null, + NOTE_LEFT: controlsData?.keys?.NOTE_LEFT ?? null, + NOTE_RIGHT: controlsData?.keys?.NOTE_RIGHT ?? null, + NOTE_UP: controlsData?.keys?.NOTE_UP ?? null, + PAUSE: controlsData?.keys?.PAUSE ?? null, + RESET: controlsData?.keys?.RESET ?? null, + UI_DOWN: controlsData?.keys?.UI_DOWN ?? null, + UI_LEFT: controlsData?.keys?.UI_LEFT ?? null, + UI_RIGHT: controlsData?.keys?.UI_RIGHT ?? null, + UI_UP: controlsData?.keys?.UI_UP ?? null, + VOLUME_DOWN: controlsData?.keys?.VOLUME_DOWN ?? null, + VOLUME_MUTE: controlsData?.keys?.VOLUME_MUTE ?? null, + VOLUME_UP: controlsData?.keys?.VOLUME_UP ?? null, + }; + + var outputPadControls:SaveControlsData = + { + ACCEPT: controlsData?.pad?.ACCEPT ?? null, + BACK: controlsData?.pad?.BACK ?? null, + CUTSCENE_ADVANCE: controlsData?.pad?.CUTSCENE_ADVANCE ?? null, + CUTSCENE_SKIP: controlsData?.pad?.CUTSCENE_SKIP ?? null, + NOTE_DOWN: controlsData?.pad?.NOTE_DOWN ?? null, + NOTE_LEFT: controlsData?.pad?.NOTE_LEFT ?? null, + NOTE_RIGHT: controlsData?.pad?.NOTE_RIGHT ?? null, + NOTE_UP: controlsData?.pad?.NOTE_UP ?? null, + PAUSE: controlsData?.pad?.PAUSE ?? null, + RESET: controlsData?.pad?.RESET ?? null, + UI_DOWN: controlsData?.pad?.UI_DOWN ?? null, + UI_LEFT: controlsData?.pad?.UI_LEFT ?? null, + UI_RIGHT: controlsData?.pad?.UI_RIGHT ?? null, + UI_UP: controlsData?.pad?.UI_UP ?? null, + VOLUME_DOWN: controlsData?.pad?.VOLUME_DOWN ?? null, + VOLUME_MUTE: controlsData?.pad?.VOLUME_MUTE ?? null, + VOLUME_UP: controlsData?.pad?.VOLUME_UP ?? null, + }; + + result.setControls(playerId, Keys, outputKeyControls); + result.setControls(playerId, Gamepad(0), outputPadControls); + } +} diff --git a/source/funkin/ui/ControlsMenu.hx b/source/funkin/ui/ControlsMenu.hx index 0d9db5b34..8197424ee 100644 --- a/source/funkin/ui/ControlsMenu.hx +++ b/source/funkin/ui/ControlsMenu.hx @@ -163,7 +163,17 @@ class ControlsMenu extends funkin.ui.OptionsState.Page function onSelect():Void { - keyUsedToEnterPrompt = FlxG.keys.firstJustPressed(); + switch (currentDevice) + { + case Keys: + { + keyUsedToEnterPrompt = FlxG.keys.firstJustPressed(); + } + case Gamepad(id): + { + buttonUsedToEnterPrompt = FlxG.gamepads.getByID(id).firstJustPressedID(); + } + } controlGrid.enabled = false; canExit = false; @@ -204,6 +214,7 @@ class ControlsMenu extends funkin.ui.OptionsState.Page } var keyUsedToEnterPrompt:Null = null; + var buttonUsedToEnterPrompt:Null = null; override function update(elapsed:Float):Void { @@ -246,19 +257,49 @@ class ControlsMenu extends funkin.ui.OptionsState.Page case Gamepad(id): { var button = FlxG.gamepads.getByID(id).firstJustReleasedID(); - if (button != NONE && button != keyUsedToEnterPrompt) + if (button != NONE && button != buttonUsedToEnterPrompt) { if (button != BACK) onInputSelect(button); closePrompt(); } + + var key = FlxG.keys.firstJustReleased(); + if (key != NONE && key != keyUsedToEnterPrompt) + { + if (key == ESCAPE) + { + closePrompt(); + } + else if (key == BACKSPACE) + { + onInputSelect(NONE); + closePrompt(); + } + } } } } - var keyJustReleased:Int = FlxG.keys.firstJustReleased(); - if (keyJustReleased != NONE && keyJustReleased == keyUsedToEnterPrompt) + switch (currentDevice) { - keyUsedToEnterPrompt = null; + case Keys: + { + var keyJustReleased:Int = FlxG.keys.firstJustReleased(); + if (keyJustReleased != NONE && keyJustReleased == keyUsedToEnterPrompt) + { + keyUsedToEnterPrompt = null; + } + buttonUsedToEnterPrompt = null; + } + case Gamepad(id): + { + var buttonJustReleased:Int = FlxG.gamepads.getByID(id).firstJustReleasedID(); + if (buttonJustReleased != NONE && buttonJustReleased == buttonUsedToEnterPrompt) + { + buttonUsedToEnterPrompt = null; + } + keyUsedToEnterPrompt = null; + } } } diff --git a/source/funkin/ui/PreferencesMenu.hx b/source/funkin/ui/PreferencesMenu.hx index 4fa8f7f5b..812d0ab49 100644 --- a/source/funkin/ui/PreferencesMenu.hx +++ b/source/funkin/ui/PreferencesMenu.hx @@ -3,17 +3,16 @@ package funkin.ui; import flixel.FlxCamera; import flixel.FlxObject; import flixel.FlxSprite; +import flixel.group.FlxSpriteGroup.FlxTypedSpriteGroup; import funkin.ui.AtlasText.AtlasFont; import funkin.ui.OptionsState.Page; import funkin.ui.TextMenuList.TextMenuItem; class PreferencesMenu extends Page { - public static var preferences:Map = new Map(); - var items:TextMenuList; + var preferenceItems:FlxTypedSpriteGroup; - var checkboxes:Array = []; var menuCamera:FlxCamera; var camFollow:FlxObject; @@ -27,13 +26,9 @@ class PreferencesMenu extends Page camera = menuCamera; add(items = new TextMenuList()); + add(preferenceItems = new FlxTypedSpriteGroup()); - createPrefItem('naughtyness', 'censor-naughty', true); - createPrefItem('downscroll', 'downscroll', false); - createPrefItem('flashing menu', 'flashing-menu', true); - createPrefItem('Camera Zooming on Beat', 'camera-zoom', true); - createPrefItem('FPS Counter', 'fps-counter', true); - createPrefItem('Auto Pause', 'auto-pause', false); + createPrefItems(); camFollow = new FlxObject(FlxG.width / 2, 0, 140, 70); if (items != null) camFollow.y = items.selectedItem.y; @@ -48,128 +43,63 @@ class PreferencesMenu extends Page }); } - public static function getPref(pref:String):Dynamic + /** + * Create the menu items for each of the preferences. + */ + function createPrefItems():Void { - return preferences.get(pref); + createPrefItemCheckbox('Naughtyness', 'Toggle displaying raunchy content', function(value:Bool):Void { + Preferences.naughtyness = value; + }, Preferences.naughtyness); + createPrefItemCheckbox('Downscroll', 'Enable to make notes move downwards', function(value:Bool):Void { + Preferences.downscroll = value; + }, Preferences.downscroll); + createPrefItemCheckbox('Flashing Lights', 'Disable to dampen flashing effects', function(value:Bool):Void { + Preferences.flashingLights = value; + }, Preferences.flashingLights); + createPrefItemCheckbox('Camera Zooming on Beat', 'Disable to stop the camera bouncing to the song', function(value:Bool):Void { + Preferences.zoomCamera = value; + }, Preferences.zoomCamera); + createPrefItemCheckbox('Debug Display', 'Enable to show FPS and other debug stats', function(value:Bool):Void { + Preferences.debugDisplay = value; + }, Preferences.debugDisplay); + createPrefItemCheckbox('Auto Pause', 'Automatically pause the game when it loses focus', function(value:Bool):Void { + Preferences.autoPause = value; + }, Preferences.autoPause); } - // easy shorthand? - public static function setPref(pref:String, value:Dynamic):Void + function createPrefItemCheckbox(prefName:String, prefDesc:String, onChange:Bool->Void, defaultValue:Bool):Void { - preferences.set(pref, value); - } + var checkbox:CheckboxPreferenceItem = new CheckboxPreferenceItem(0, 120 * (items.length - 1 + 1), defaultValue); - public static function initPrefs():Void - { - preferenceCheck('censor-naughty', true); - preferenceCheck('downscroll', false); - preferenceCheck('flashing-menu', true); - preferenceCheck('camera-zoom', true); - preferenceCheck('fps-counter', true); - preferenceCheck('auto-pause', false); - preferenceCheck('master-volume', 1); - - #if muted - setPref('master-volume', 0); - FlxG.sound.muted = true; - #end - - if (!getPref('fps-counter')) FlxG.stage.removeChild(Main.fpsCounter); - - FlxG.autoPause = getPref('auto-pause'); - } - - function createPrefItem(prefName:String, prefString:String, prefValue:Dynamic):Void - { items.createItem(120, (120 * items.length) + 30, prefName, AtlasFont.BOLD, function() { - preferenceCheck(prefString, prefValue); - - switch (Type.typeof(prefValue).getName()) - { - case 'TBool': - prefToggle(prefString); - - default: - trace('swag'); - } + var value = !checkbox.currentValue; + onChange(value); + checkbox.currentValue = value; }); - switch (Type.typeof(prefValue).getName()) - { - case 'TBool': - createCheckbox(prefString); - - default: - trace('swag'); - } - - trace(Type.typeof(prefValue).getName()); - } - - function createCheckbox(prefString:String) - { - var checkbox:CheckboxThingie = new CheckboxThingie(0, 120 * (items.length - 1), preferences.get(prefString)); - checkboxes.push(checkbox); - add(checkbox); - } - - /** - * Assumes that the preference has already been checked/set? - */ - function prefToggle(prefName:String) - { - var daSwap:Bool = preferences.get(prefName); - daSwap = !daSwap; - preferences.set(prefName, daSwap); - checkboxes[items.selectedIndex].daValue = daSwap; - trace('toggled? ' + preferences.get(prefName)); - - switch (prefName) - { - case 'fps-counter': - if (getPref('fps-counter')) FlxG.stage.addChild(Main.fpsCounter); - else - FlxG.stage.removeChild(Main.fpsCounter); - case 'auto-pause': - FlxG.autoPause = getPref('auto-pause'); - } - - if (prefName == 'fps-counter') {} + preferenceItems.add(checkbox); } override function update(elapsed:Float) { super.update(elapsed); - // menuCamera.followLerp = CoolUtil.camLerpShit(0.05); - + // Indent the selected item. + // TODO: Only do this on menu change? items.forEach(function(daItem:TextMenuItem) { if (items.selectedItem == daItem) daItem.x = 150; else daItem.x = 120; }); } - - static function preferenceCheck(prefString:String, defaultValue:Dynamic):Void - { - if (preferences.get(prefString) == null) - { - // Set the value to default. - preferences.set(prefString, defaultValue); - trace('Set preference to default: ${prefString} = ${defaultValue}'); - } - else - { - trace('Found preference: ${prefString} = ${preferences.get(prefString)}'); - } - } } -class CheckboxThingie extends FlxSprite +class CheckboxPreferenceItem extends FlxSprite { - public var daValue(default, set):Bool; + public var currentValue(default, set):Bool; - public function new(x:Float, y:Float, daValue:Bool = false) + public function new(x:Float, y:Float, defaultValue:Bool = false) { super(x, y); @@ -180,7 +110,7 @@ class CheckboxThingie extends FlxSprite setGraphicSize(Std.int(width * 0.7)); updateHitbox(); - this.daValue = daValue; + this.currentValue = defaultValue; } override function update(elapsed:Float) @@ -196,12 +126,17 @@ class CheckboxThingie extends FlxSprite } } - function set_daValue(value:Bool):Bool + function set_currentValue(value:Bool):Bool { - if (value) animation.play('checked', true); + if (value) + { + animation.play('checked', true); + } else + { animation.play('static'); + } - return value; + return currentValue = value; } } diff --git a/source/funkin/ui/StickerSubState.hx b/source/funkin/ui/StickerSubState.hx index bde36b160..a4e3a6acb 100644 --- a/source/funkin/ui/StickerSubState.hx +++ b/source/funkin/ui/StickerSubState.hx @@ -122,7 +122,7 @@ class StickerSubState extends MusicBeatSubState var daSound:String = FlxG.random.getObject(sounds); FlxG.sound.play(Paths.sound(daSound)); - if (ind == grpStickers.members.length - 1) + if (grpStickers == null || ind == grpStickers.members.length - 1) { switchingState = false; close(); @@ -206,6 +206,8 @@ class StickerSubState extends MusicBeatSubState sticker.timing = FlxMath.remapToRange(ind, 0, grpStickers.members.length, 0, 0.9); new FlxTimer().start(sticker.timing, _ -> { + if (grpStickers == null) return; + sticker.visible = true; var daSound:String = FlxG.random.getObject(sounds); FlxG.sound.play(Paths.sound(daSound)); @@ -269,10 +271,10 @@ class StickerSubState extends MusicBeatSubState { super.update(elapsed); - if (FlxG.keys.justPressed.ANY) - { - regenStickers(); - } + // if (FlxG.keys.justPressed.ANY) + // { + // regenStickers(); + // } } var switchingState:Bool = false; diff --git a/source/funkin/ui/TextMenuList.hx b/source/funkin/ui/TextMenuList.hx index 0c9f9eb8b..521f46faf 100644 --- a/source/funkin/ui/TextMenuList.hx +++ b/source/funkin/ui/TextMenuList.hx @@ -10,7 +10,7 @@ class TextMenuList extends MenuTypedList super(navControls, wrapMode); } - public function createItem(x = 0.0, y = 0.0, name:String, font:AtlasFont = BOLD, callback, fireInstantly = false) + public function createItem(x = 0.0, y = 0.0, name:String, font:AtlasFont = BOLD, ?callback:Void->Void, fireInstantly = false) { var item = new TextMenuItem(x, y, name, font, callback); item.fireInstantly = fireInstantly; @@ -20,7 +20,7 @@ class TextMenuList extends MenuTypedList class TextMenuItem extends TextTypedMenuItem { - public function new(x = 0.0, y = 0.0, name:String, font:AtlasFont = BOLD, callback) + public function new(x = 0.0, y = 0.0, name:String, font:AtlasFont = BOLD, ?callback:Void->Void) { super(x, y, new AtlasText(0, 0, name, font), name, callback); setEmptyBackground(); @@ -29,7 +29,7 @@ class TextMenuItem extends TextTypedMenuItem class TextTypedMenuItem extends MenuTypedItem { - public function new(x = 0.0, y = 0.0, label:T, name:String, callback) + public function new(x = 0.0, y = 0.0, label:T, name:String, ?callback:Void->Void) { super(x, y, label, name, callback); } diff --git a/source/funkin/ui/debug/charting/ChartEditorAudioHandler.hx b/source/funkin/ui/debug/charting/ChartEditorAudioHandler.hx index e852dff0a..b5a6f36be 100644 --- a/source/funkin/ui/debug/charting/ChartEditorAudioHandler.hx +++ b/source/funkin/ui/debug/charting/ChartEditorAudioHandler.hx @@ -1,11 +1,14 @@ package funkin.ui.debug.charting; -import openfl.utils.Assets; import flixel.system.FlxAssets.FlxSoundAsset; import flixel.system.FlxSound; -import funkin.play.character.BaseCharacter.CharacterType; import flixel.system.FlxSound; +import funkin.audio.VoicesGroup; +import funkin.play.character.BaseCharacter.CharacterType; +import funkin.util.FileUtil; +import haxe.io.Bytes; import haxe.io.Path; +import openfl.utils.Assets; /** * Functions for loading audio for the chart editor. @@ -17,16 +20,18 @@ import haxe.io.Path; class ChartEditorAudioHandler { /** - * Loads a vocal track from an absolute file path. + * Loads and stores byte data for a vocal track from an absolute file path + * * @param path The absolute path to the audio file. - * @param charKey The character to load the vocal track for. + * @param charId The character this vocal track will be for. + * @param instId The instrumental this vocal track will be for. * @return Success or failure. */ - static function loadVocalsFromPath(state:ChartEditorState, path:Path, charKey:String = 'default'):Bool + static function loadVocalsFromPath(state:ChartEditorState, path:Path, charId:String, instId:String = ''):Bool { #if sys - var fileBytes:haxe.io.Bytes = sys.io.File.getBytes(path.toString()); - return loadVocalsFromBytes(state, fileBytes, charKey); + var fileBytes:Bytes = sys.io.File.getBytes(path.toString()); + return loadVocalsFromBytes(state, fileBytes, charId, instId); #else trace("[WARN] This platform can't load audio from a file path, you'll need to fetch the bytes some other way."); return false; @@ -34,137 +39,235 @@ class ChartEditorAudioHandler } /** - * Load a vocal track for a given song and character and add it to the voices group. + * Loads and stores byte data for a vocal track from an asset * - * @param path ID of the asset. - * @param charKey Character to load the vocal track for. + * @param path The path to the asset. Use `Paths` to build this. + * @param charId The character this vocal track will be for. + * @param instId The instrumental this vocal track will be for. * @return Success or failure. */ - static function loadVocalsFromAsset(state:ChartEditorState, path:String, charType:CharacterType = OTHER):Bool + static function loadVocalsFromAsset(state:ChartEditorState, path:String, charId:String, instId:String = ''):Bool { - var vocalTrack:FlxSound = FlxG.sound.load(path, 1.0, false); + var trackData:Null = Assets.getBytes(path); + if (trackData != null) + { + return loadVocalsFromBytes(state, trackData, charId, instId); + } + return false; + } + + /** + * Loads and stores byte data for a vocal track + * + * @param bytes The audio byte data. + * @param charId The character this vocal track will be for. + * @param instId The instrumental this vocal track will be for. + */ + static function loadVocalsFromBytes(state:ChartEditorState, bytes:Bytes, charId:String, instId:String = ''):Bool + { + var trackId:String = '${charId}${instId == '' ? '' : '-${instId}'}'; + state.audioVocalTrackData.set(trackId, bytes); + return true; + } + + /** + * Loads and stores byte data for an instrumental track from an absolute file path + * + * @param path The absolute path to the audio file. + * @param instId The instrumental this vocal track will be for. + * @return Success or failure. + */ + static function loadInstFromPath(state:ChartEditorState, path:Path, instId:String = ''):Bool + { + #if sys + var fileBytes:Bytes = sys.io.File.getBytes(path.toString()); + return loadInstFromBytes(state, fileBytes, instId); + #else + trace("[WARN] This platform can't load audio from a file path, you'll need to fetch the bytes some other way."); + return false; + #end + } + + /** + * Loads and stores byte data for an instrumental track from an asset + * + * @param path The path to the asset. Use `Paths` to build this. + * @param instId The instrumental this vocal track will be for. + * @return Success or failure. + */ + static function loadInstFromAsset(state:ChartEditorState, path:String, instId:String = ''):Bool + { + var trackData:Null = Assets.getBytes(path); + if (trackData != null) + { + return loadInstFromBytes(state, trackData, instId); + } + return false; + } + + /** + * Loads and stores byte data for a vocal track + * + * @param bytes The audio byte data. + * @param charId The character this vocal track will be for. + * @param instId The instrumental this vocal track will be for. + */ + static function loadInstFromBytes(state:ChartEditorState, bytes:Bytes, instId:String = ''):Bool + { + if (instId == '') instId = 'default'; + state.audioInstTrackData.set(instId, bytes); + return true; + } + + public static function switchToInstrumental(state:ChartEditorState, instId:String = '', playerId:String, opponentId:String):Bool + { + var result:Bool = playInstrumental(state, instId); + if (!result) return false; + + stopExistingVocals(state); + result = playVocals(state, BF, playerId, instId); + if (!result) return false; + result = playVocals(state, DAD, opponentId, instId); + if (!result) return false; + + return true; + } + + /** + * Tell the Chart Editor to select a specific instrumental track, that is already loaded. + */ + static function playInstrumental(state:ChartEditorState, instId:String = ''):Bool + { + if (instId == '') instId = 'default'; + var instTrackData:Null = state.audioInstTrackData.get(instId); + var instTrack:Null = buildFlxSoundFromBytes(instTrackData); + if (instTrack == null) return false; + + stopExistingInstrumental(state); + state.audioInstTrack = instTrack; + state.postLoadInstrumental(); + return true; + } + + static function stopExistingInstrumental(state:ChartEditorState):Void + { + if (state.audioInstTrack != null) + { + state.audioInstTrack.stop(); + state.audioInstTrack.destroy(); + state.audioInstTrack = null; + } + } + + /** + * Tell the Chart Editor to select a specific vocal track, that is already loaded. + */ + static function playVocals(state:ChartEditorState, charType:CharacterType, charId:String, instId:String = ''):Bool + { + var trackId:String = '${charId}${instId == '' ? '' : '-${instId}'}'; + var vocalTrackData:Null = state.audioVocalTrackData.get(trackId); + var vocalTrack:Null = buildFlxSoundFromBytes(vocalTrackData); + + if (state.audioVocalTrackGroup == null) state.audioVocalTrackGroup = new VoicesGroup(); + if (vocalTrack != null) { switch (charType) { - case CharacterType.BF: - if (state.audioVocalTrackGroup != null) state.audioVocalTrackGroup.addPlayerVoice(vocalTrack); - state.audioVocalTrackData.set(state.currentSongCharacterPlayer, Assets.getBytes(path)); - case CharacterType.DAD: - if (state.audioVocalTrackGroup != null) state.audioVocalTrackGroup.addOpponentVoice(vocalTrack); - state.audioVocalTrackData.set(state.currentSongCharacterOpponent, Assets.getBytes(path)); + case BF: + state.audioVocalTrackGroup.addPlayerVoice(vocalTrack); + return true; + case DAD: + state.audioVocalTrackGroup.addOpponentVoice(vocalTrack); + return true; + case OTHER: + state.audioVocalTrackGroup.add(vocalTrack); + return true; default: - if (state.audioVocalTrackGroup != null) state.audioVocalTrackGroup.add(vocalTrack); - state.audioVocalTrackData.set('default', Assets.getBytes(path)); + // Do nothing. } - - return true; } return false; } - /** - * Loads a vocal track from audio byte data. - */ - static function loadVocalsFromBytes(state:ChartEditorState, bytes:haxe.io.Bytes, charKey:String = ''):Bool + static function stopExistingVocals(state:ChartEditorState):Void { - var openflSound:openfl.media.Sound = new openfl.media.Sound(); - openflSound.loadCompressedDataFromByteArray(openfl.utils.ByteArray.fromBytes(bytes), bytes.length); - var vocalTrack:FlxSound = FlxG.sound.load(openflSound, 1.0, false); - if (state.audioVocalTrackGroup != null) state.audioVocalTrackGroup.add(vocalTrack); - state.audioVocalTrackData.set(charKey, bytes); - return true; - } - - /** - * Loads an instrumental from an absolute file path, replacing the current instrumental. - * - * @param path The absolute path to the audio file. - * - * @return Success or failure. - */ - static function loadInstrumentalFromPath(state:ChartEditorState, path:Path):Bool - { - #if sys - // Validate file extension. - if (path.ext != null && !ChartEditorState.SUPPORTED_MUSIC_FORMATS.contains(path.ext)) + if (state.audioVocalTrackGroup != null) { - return false; + state.audioVocalTrackGroup.clear(); } - - var fileBytes:haxe.io.Bytes = sys.io.File.getBytes(path.toString()); - return loadInstrumentalFromBytes(state, fileBytes, '${path.file}.${path.ext}'); - #else - trace("[WARN] This platform can't load audio from a file path, you'll need to fetch the bytes some other way."); - return false; - #end - } - - /** - * Loads an instrumental from audio byte data, replacing the current instrumental. - * @param bytes The audio byte data. - * @param fileName The name of the file, if available. Used for notifications. - * @return Success or failure. - */ - static function loadInstrumentalFromBytes(state:ChartEditorState, bytes:haxe.io.Bytes, fileName:String = null):Bool - { - if (bytes == null) - { - return false; - } - - var openflSound:openfl.media.Sound = new openfl.media.Sound(); - openflSound.loadCompressedDataFromByteArray(openfl.utils.ByteArray.fromBytes(bytes), bytes.length); - state.audioInstTrack = FlxG.sound.load(openflSound, 1.0, false); - state.audioInstTrack.autoDestroy = false; - state.audioInstTrack.pause(); - - state.audioInstTrackData = bytes; - - state.postLoadInstrumental(); - - return true; - } - - /** - * Loads an instrumental from an OpenFL asset, replacing the current instrumental. - * @param path The path to the asset. Use `Paths` to build this. - * @return Success or failure. - */ - static function loadInstrumentalFromAsset(state:ChartEditorState, path:String):Bool - { - var instTrack:FlxSound = FlxG.sound.load(path, 1.0, false); - if (instTrack != null) - { - state.audioInstTrack = instTrack; - - state.audioInstTrackData = Assets.getBytes(path); - - state.postLoadInstrumental(); - return true; - } - - return false; } /** * Play a sound effect. * Automatically cleans up after itself and recycles previous FlxSound instances if available, for performance. + * @param path The path to the sound effect. Use `Paths` to build this. */ public static function playSound(path:String):Void { var snd:FlxSound = FlxG.sound.list.recycle(FlxSound) ?? new FlxSound(); - var asset:Null = FlxG.sound.cache(path); if (asset == null) { trace('WARN: Failed to play sound $path, asset not found.'); return; } - snd.loadEmbedded(asset); snd.autoDestroy = true; FlxG.sound.list.add(snd); snd.play(); } + + /** + * Convert byte data into a playable sound. + * + * @param input The byte data. + * @return The playable sound, or `null` if loading failed. + */ + public static function buildFlxSoundFromBytes(input:Null):Null + { + if (input == null) return null; + + var openflSound:openfl.media.Sound = new openfl.media.Sound(); + openflSound.loadCompressedDataFromByteArray(openfl.utils.ByteArray.fromBytes(input), input.length); + var output:FlxSound = FlxG.sound.load(openflSound, 1.0, false); + return output; + } + + static function makeZIPEntriesFromInstrumentals(state:ChartEditorState):Array + { + var zipEntries = []; + + for (key in state.audioInstTrackData.keys()) + { + if (key == 'default') + { + var data:Null = state.audioInstTrackData.get('default'); + if (data == null) continue; + zipEntries.push(FileUtil.makeZIPEntryFromBytes('Inst.ogg', data)); + } + else + { + var data:Null = state.audioInstTrackData.get(key); + if (data == null) continue; + zipEntries.push(FileUtil.makeZIPEntryFromBytes('Inst-${key}.ogg', data)); + } + } + + return zipEntries; + } + + static function makeZIPEntriesFromVocals(state:ChartEditorState):Array + { + var zipEntries = []; + + for (key in state.audioVocalTrackData.keys()) + { + var data:Null = state.audioVocalTrackData.get(key); + if (data == null) continue; + zipEntries.push(FileUtil.makeZIPEntryFromBytes('Vocals-${key}.ogg', data)); + } + + return zipEntries; + } } diff --git a/source/funkin/ui/debug/charting/ChartEditorCommand.hx b/source/funkin/ui/debug/charting/ChartEditorCommand.hx index 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 736851d16..30f0381c6 100644 --- a/source/funkin/ui/debug/charting/ChartEditorDialogHandler.hx +++ b/source/funkin/ui/debug/charting/ChartEditorDialogHandler.hx @@ -83,7 +83,7 @@ class ChartEditorDialogHandler var dialog:Null = openDialog(state, CHART_EDITOR_DIALOG_WELCOME_LAYOUT, true, closable); if (dialog == null) throw 'Could not locate Welcome dialog'; - // Add handlers to the "Create From Song" section. + // Create New Song "Easy/Normal/Hard" var linkCreateBasic:Null = dialog.findComponent('splashCreateFromSongBasic', Link); if (linkCreateBasic == null) throw 'Could not locate splashCreateFromSongBasic link in Welcome dialog'; linkCreateBasic.onClick = function(_event) { @@ -94,7 +94,20 @@ class ChartEditorDialogHandler // // Create Song Wizard // - openCreateSongWizard(state, false); + openCreateSongWizardBasic(state, false); + } + + // Create New Song "Erect/Nightmare" + var linkCreateErect:Null = dialog.findComponent('splashCreateFromSongErect', Link); + if (linkCreateErect == null) throw 'Could not locate splashCreateFromSongErect link in Welcome dialog'; + linkCreateErect.onClick = function(_event) { + // Hide the welcome dialog + dialog.hideDialog(DialogButton.CANCEL); + + // + // Create Song Wizard + // + openCreateSongWizardErect(state, false); } var linkImportChartLegacy:Null = dialog.findComponent('splashImportChartLegacy', Link); @@ -237,34 +250,112 @@ class ChartEditorDialogHandler }; } - public static function openCreateSongWizard(state:ChartEditorState, closable:Bool):Void + public static function openCreateSongWizardBasic(state:ChartEditorState, closable:Bool):Void { - // Step 1. Upload Instrumental - var uploadInstDialog:Dialog = openUploadInstDialog(state, closable); - uploadInstDialog.onDialogClosed = function(_event) { + // Step 1. Song Metadata + var songMetadataDialog:Dialog = openSongMetadataDialog(state); + songMetadataDialog.onDialogClosed = function(_event) { state.isHaxeUIDialogOpen = false; if (_event.button == DialogButton.APPLY) { - // Step 2. Song Metadata - var songMetadataDialog:Dialog = openSongMetadataDialog(state); - songMetadataDialog.onDialogClosed = function(_event) { + // Step 2. Upload Instrumental + var uploadInstDialog:Dialog = openUploadInstDialog(state, closable); + uploadInstDialog.onDialogClosed = function(_event) { state.isHaxeUIDialogOpen = false; if (_event.button == DialogButton.APPLY) { // Step 3. Upload Vocals // NOTE: Uploading vocals is optional, so we don't need to check if the user cancelled the wizard. - openUploadVocalsDialog(state, false); // var uploadVocalsDialog:Dialog + var uploadVocalsDialog:Dialog = openUploadVocalsDialog(state, closable); // var uploadVocalsDialog:Dialog + uploadVocalsDialog.onDialogClosed = function(_event) { + state.isHaxeUIDialogOpen = false; + state.switchToCurrentInstrumental(); + state.postLoadInstrumental(); + } } else { - // User cancelled the wizard! Back to the welcome dialog. + // User cancelled the wizard at Step 2! Back to the welcome dialog. openWelcomeDialog(state); } }; } else { - // User cancelled the wizard! Back to the welcome dialog. + // User cancelled the wizard at Step 1! Back to the welcome dialog. + openWelcomeDialog(state); + } + }; + } + + public static function openCreateSongWizardErect(state:ChartEditorState, closable:Bool):Void + { + // Step 1. Song Metadata + var songMetadataDialog:Dialog = openSongMetadataDialog(state); + songMetadataDialog.onDialogClosed = function(_event) { + state.isHaxeUIDialogOpen = false; + if (_event.button == DialogButton.APPLY) + { + // Step 2. Upload Instrumental + var uploadInstDialog:Dialog = openUploadInstDialog(state, closable); + uploadInstDialog.onDialogClosed = function(_event) { + state.isHaxeUIDialogOpen = false; + if (_event.button == DialogButton.APPLY) + { + // Step 3. Upload Vocals + // NOTE: Uploading vocals is optional, so we don't need to check if the user cancelled the wizard. + var uploadVocalsDialog:Dialog = openUploadVocalsDialog(state, closable); // var uploadVocalsDialog:Dialog + uploadVocalsDialog.onDialogClosed = function(_event) { + state.switchToCurrentInstrumental(); + // Step 4. Song Metadata (Erect) + var songMetadataDialogErect:Dialog = openSongMetadataDialog(state, 'erect'); + songMetadataDialogErect.onDialogClosed = function(_event) { + state.isHaxeUIDialogOpen = false; + if (_event.button == DialogButton.APPLY) + { + // Switch to the Erect variation so uploading the instrumental applies properly. + state.selectedVariation = 'erect'; + + // Step 5. Upload Instrumental (Erect) + var uploadInstDialogErect:Dialog = openUploadInstDialog(state, closable); + uploadInstDialogErect.onDialogClosed = function(_event) { + state.isHaxeUIDialogOpen = false; + if (_event.button == DialogButton.APPLY) + { + // Step 6. Upload Vocals (Erect) + // NOTE: Uploading vocals is optional, so we don't need to check if the user cancelled the wizard. + var uploadVocalsDialogErect:Dialog = openUploadVocalsDialog(state, closable); // var uploadVocalsDialog:Dialog + uploadVocalsDialogErect.onDialogClosed = function(_event) { + state.isHaxeUIDialogOpen = false; + state.switchToCurrentInstrumental(); + state.postLoadInstrumental(); + } + } + else + { + // User cancelled the wizard at Step 5! Back to the welcome dialog. + openWelcomeDialog(state); + } + }; + } + else + { + // User cancelled the wizard at Step 4! Back to the welcome dialog. + openWelcomeDialog(state); + } + } + } + } + else + { + // User cancelled the wizard at Step 2! Back to the welcome dialog. + openWelcomeDialog(state); + } + }; + } + else + { + // User cancelled the wizard at Step 1! Back to the welcome dialog. openWelcomeDialog(state); } }; @@ -302,6 +393,8 @@ class ChartEditorDialogHandler Cursor.cursorMode = Default; } + var instId:String = state.currentInstrumentalId; + var onDropFile:String->Void; instrumentalBox.onClick = function(_event) { @@ -309,14 +402,14 @@ class ChartEditorDialogHandler {label: 'Audio File (.ogg)', extension: 'ogg'}], function(selectedFile:SelectedFileInfo) { if (selectedFile != null && selectedFile.bytes != null) { - if (ChartEditorAudioHandler.loadInstrumentalFromBytes(state, selectedFile.bytes)) + if (ChartEditorAudioHandler.loadInstFromBytes(state, selectedFile.bytes, instId)) { trace('Selected file: ' + selectedFile.fullPath); #if !mac NotificationManager.instance.addNotification( { title: 'Success', - body: 'Loaded instrumental track (${selectedFile.name})', + body: 'Loaded instrumental track (${selectedFile.name}) for variation (${state.selectedVariation})', type: NotificationType.Success, expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME }); @@ -333,7 +426,7 @@ class ChartEditorDialogHandler NotificationManager.instance.addNotification( { title: 'Failure', - body: 'Failed to load instrumental track (${selectedFile.name})', + body: 'Failed to load instrumental track (${selectedFile.name}) for variation (${state.selectedVariation})', type: NotificationType.Error, expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME }); @@ -346,14 +439,14 @@ class ChartEditorDialogHandler onDropFile = function(pathStr:String) { var path:Path = new Path(pathStr); trace('Dropped file (${path})'); - if (ChartEditorAudioHandler.loadInstrumentalFromPath(state, path)) + if (ChartEditorAudioHandler.loadInstFromPath(state, path, instId)) { // Tell the user the load was successful. #if !mac NotificationManager.instance.addNotification( { title: 'Success', - body: 'Loaded instrumental track (${path.file}.${path.ext})', + body: 'Loaded instrumental track (${path.file}.${path.ext}) for variation (${state.selectedVariation})', type: NotificationType.Success, expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME }); @@ -370,7 +463,7 @@ class ChartEditorDialogHandler } else { - 'Failed to load instrumental track (${path.file}.${path.ext})'; + 'Failed to load instrumental track (${path.file}.${path.ext}) for variation (${state.selectedVariation})'; } // Tell the user the load was successful. @@ -457,11 +550,18 @@ class ChartEditorDialogHandler * @return The dialog to open. */ @:haxe.warning("-WVarInit") - public static function openSongMetadataDialog(state:ChartEditorState):Dialog + public static function openSongMetadataDialog(state:ChartEditorState, ?targetVariation:String):Dialog { + if (targetVariation == null) targetVariation = Constants.DEFAULT_VARIATION; + var dialog:Null = openDialog(state, CHART_EDITOR_DIALOG_SONG_METADATA_LAYOUT, true, false); if (dialog == null) throw 'Could not locate Song Metadata dialog'; + if (targetVariation != Constants.DEFAULT_VARIATION) + { + dialog.title = 'New Chart - Provide Song Metadata (${targetVariation.toTitleCase()})'; + } + var buttonCancel:Null