From c0314c85ecd5116641aff3de8e9153f7fe48e79c Mon Sep 17 00:00:00 2001 From: Cameron Taylor Date: Tue, 1 Oct 2024 20:32:48 -0400 Subject: [PATCH] fix(freeplay)!: Proper variation / difficulty loading for Freeplay Menu Previously the game would load variations in a `variation-difficulty` string format, but now we map it out better and filter it based on that, rather than messing around with suffixes and whatnot. If you have a mod that depended on the functionality of the `variation-difficulty` format, you should accomodate that functionality in another way re-add freeplay song preview song names and icons implemented again implement the scoring rank, bpm, and difficulty crud albumId loading fix --- source/funkin/play/song/Song.hx | 102 ++- source/funkin/ui/freeplay/FreeplayState.hx | 681 +++++++++------------ source/funkin/ui/freeplay/SongMenuItem.hx | 48 +- 3 files changed, 385 insertions(+), 446 deletions(-) diff --git a/source/funkin/play/song/Song.hx b/source/funkin/play/song/Song.hx index a6449bc10..91e158299 100644 --- a/source/funkin/play/song/Song.hx +++ b/source/funkin/play/song/Song.hx @@ -15,6 +15,7 @@ import funkin.data.song.SongRegistry; import funkin.modding.IScriptedClass.IPlayStateScriptedClass; import funkin.modding.events.ScriptEvent; import funkin.ui.freeplay.charselect.PlayableCharacter; +import funkin.data.freeplay.player.PlayerRegistry; import funkin.util.SortUtil; /** @@ -79,7 +80,12 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry; - final difficulties:Map; + + /** + * holds the difficulties (as in SongDifficulty) for each variation + * difficulties.get('default').get('easy') would return the easy difficulty for the default variation + */ + final difficulties:Map>; /** * The list of variations a song has. @@ -146,7 +152,7 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry(); + difficulties = new Map>(); _data = _fetchData(id); @@ -156,7 +162,8 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry + public function listAlbums(variation:String):Map { var result:Map = new Map(); - for (difficultyId in difficulties.keys()) + for (variationMap in difficulties) { - var meta:Null = difficulties.get(difficultyId); - if (meta != null && meta.album != null) + for (difficultyId in variationMap.keys()) { - result.set(difficultyId, meta.album); + var meta:Null = variationMap.get(difficultyId); + if (meta != null && meta.album != null) + { + result.set(difficultyId, meta.album); + } } } return result; } + /** + * Input a difficulty ID and a variation ID, and get the album ID. + * @param diffId + * @param variation + * @return String + */ + public function getAlbumId(diffId:String, variation:String):String + { + var diff:Null = getDifficulty(diffId, variation); + if (diff == null) return ''; + + return diff.album ?? ''; + } + /** * Populate the difficulty data from the provided metadata. * Does not load chart data (that is triggered later when we want to play the song). @@ -285,6 +309,9 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry = new Map(); + // There may be more difficulties in the chart file than in the metadata, // (i.e. non-playable charts like the one used for Pico on the speaker in Stress) // but all the difficulties in the metadata must be in the chart file. @@ -309,10 +336,9 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry = difficulties.get('$diffId$variationSuffix'); - if (difficulty == null) + // Retrieve the cached difficulty data. This one could potentially be null. + var nullDiff:Null = getDifficulty(diffId, variation); + + // if the difficulty doesn't exist, create a new one, and then proceed to fill it with data. + // I mostly do this since I don't wanna throw around ? everywhere for null check lol? + var difficulty:SongDifficulty = nullDiff ?? new SongDifficulty(this, diffId, variation); + + if (nullDiff == null) { trace('Fabricated new difficulty for $diffId.'); - difficulty = new SongDifficulty(this, diffId, variation); var metadata = _metadata.get(variation); - difficulties.set('$diffId$variationSuffix', difficulty); + difficulties.get(variation)?.set(diffId, difficulty); if (metadata != null) { @@ -396,11 +425,9 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry + * @see getVariationsByCharacter + */ + public function getVariationsByCharacterId(?charId:String):Array + { + var charPlayer = PlayerRegistry.instance.fetchEntry(charId ?? ''); + + return getVariationsByCharacter(charPlayer); + } + /** * List all the difficulties in this song. * @@ -501,6 +539,7 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry, ?showLocked:Bool, ?showHidden:Bool):Array { @@ -529,8 +568,7 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry> = []; - // List of available difficulties for the current song, without `-variation` at the end (no duplicates or nulls). - var diffIdsCurrent:Array = []; - // List of available difficulties for the total song list, without `-variation` at the end (no duplicates or nulls). - var diffIdsTotal:Array = []; - // List of available difficulties for the current song, with `-variation` at the end (no duplicates or nulls). - var suffixedDiffIdsCurrent:Array = []; - // List of available difficulties for the total song list, with `-variation` at the end (no duplicates or nulls). - var suffixedDiffIdsTotal:Array = []; - var curSelected:Int = 0; - var currentSuffixedDifficulty:String = Constants.DEFAULT_DIFFICULTY; - var currentUnsuffixedDifficulty(get, never):String; - function get_currentUnsuffixedDifficulty():String - { - if (Constants.DEFAULT_DIFFICULTY_LIST_FULL.contains(currentSuffixedDifficulty)) return currentSuffixedDifficulty; + /** + * Currently selected difficulty, in string form. + */ + var currentDifficulty:String = Constants.DEFAULT_DIFFICULTY; - // Else, we need to strip the suffix. - return currentSuffixedDifficulty.substring(0, currentSuffixedDifficulty.lastIndexOf('-')); - } - - var currentVariation(get, never):String; - - function get_currentVariation():String - { - if (Constants.DEFAULT_DIFFICULTY_LIST.contains(currentSuffixedDifficulty)) return Constants.DEFAULT_VARIATION; - if (Constants.DEFAULT_DIFFICULTY_LIST_ERECT.contains(currentSuffixedDifficulty)) return 'erect'; - - // Else, we need to isolate the suffix. - return currentSuffixedDifficulty.substring(currentSuffixedDifficulty.lastIndexOf('-') + 1, currentSuffixedDifficulty.length); - } + /** + * Current variation: default, erect, pico, bf, etc. + */ + var currentVariation:String = Constants.DEFAULT_VARIATION; public var fp:FreeplayScore; @@ -234,6 +171,11 @@ class FreeplayState extends MusicBeatSubState */ public static var rememberedCharacterId:String = Constants.DEFAULT_CHARACTER; + /** + * The remembered variation we were on when this menu was last accessed. + */ + public static var rememberedVariation:String = Constants.DEFAULT_VARIATION; + var funnyCam:FunkinCamera; var rankCamera:FunkinCamera; var rankBg:FunkinSprite; @@ -241,53 +183,43 @@ class FreeplayState extends MusicBeatSubState var backingCard:Null = null; + /** + * The backing card that has the toned dots, right now we just use that one dad graphic dave cooked up + */ public var bgDad:FlxSprite; + public var angleMaskShader:AngleMask = new AngleMask(); + + var fadeShader:BlueFade = new BlueFade(); + var fromResultsParams:Null = null; - var prepForNewRank:Bool = false; - var styleData:Null = null; - - var fromCharSelect:Null = null; + var fromCharSelect:Bool = false; public function new(?params:FreeplayStateParams, ?stickers:StickerSubState) { currentCharacterId = params?.character ?? rememberedCharacterId; styleData = FreeplayStyleRegistry.instance.fetchEntry(currentCharacterId); + var fetchPlayableCharacter = function():PlayableCharacter { var targetCharId = params?.character ?? rememberedCharacterId; var result = PlayerRegistry.instance.fetchEntry(targetCharId); if (result == null) throw 'No valid playable character with id ${targetCharId}'; return result; }; - currentCharacter = fetchPlayableCharacter(); + currentCharacter = fetchPlayableCharacter(); + currentVariation = rememberedVariation; styleData = FreeplayStyleRegistry.instance.fetchEntry(currentCharacter.getFreeplayStyleID()); rememberedCharacterId = currentCharacter?.id ?? Constants.DEFAULT_CHARACTER; - - fromCharSelect = params?.fromCharSelect; - + fromCharSelect = params?.fromCharSelect ?? false; fromResultsParams = params?.fromResults; - - // bf songs have no suffix, but we need to initalize the difficulty - // in case we begin playing as pico - if (currentCharacterId != 'bf') - { - currentSuffixedDifficulty = Constants.DEFAULT_DIFFICULTY + '-$currentCharacterId'; - } - - if (fromResultsParams?.playRankAnim == true) - { - prepForNewRank = true; - } + prepForNewRank = fromResultsParams?.playRankAnim ?? false; super(FlxColor.TRANSPARENT); - if (stickers?.members != null) - { - stickerSubState = stickers; - } + if (stickers?.members != null) stickerSubState = stickers; switch (currentCharacterId) { @@ -322,16 +254,11 @@ class FreeplayState extends MusicBeatSubState bgDad = new FlxSprite(backingCard.pinkBack.width * 0.74, 0).loadGraphic(styleData == null ? 'freeplay/freeplayBGdad' : styleData.getBgAssetGraphic()); } - var fadeShader:BlueFade = new BlueFade(); - - public var angleMaskShader:AngleMask = new AngleMask(); - override function create():Void { super.create(); FlxG.state.persistentUpdate = false; - FlxTransitionableState.skipNextTransIn = true; var fadeShaderFilter:ShaderFilter = new ShaderFilter(fadeShader); @@ -384,23 +311,7 @@ class FreeplayState extends MusicBeatSubState continue; } - // Only display songs which actually have available difficulties for the current character. - var displayedVariations = song.getVariationsByCharacter(currentCharacter); - trace('Displayed Variations (${songId}): $displayedVariations'); - var availableDifficultiesForSong:Array = song.listSuffixedDifficulties(displayedVariations, false, false); - var unsuffixedDifficulties = song.listDifficulties(displayedVariations, false, false); - trace('Available Difficulties: $availableDifficultiesForSong'); - if (availableDifficultiesForSong.length == 0) continue; - - songs.push(new FreeplaySongData(levelId, songId, song, currentCharacter, displayedVariations)); - for (difficulty in unsuffixedDifficulties) - { - diffIdsTotal.pushUnique(difficulty); - } - for (difficulty in availableDifficultiesForSong) - { - suffixedDiffIdsTotal.pushUnique(difficulty); - } + songs.push(new FreeplaySongData(song, level)); } } @@ -493,30 +404,21 @@ class FreeplayState extends MusicBeatSubState wait: 0.1 }); - for (diffId in suffixedDiffIdsTotal) + for (diffId in Constants.DEFAULT_DIFFICULTY_LIST_FULL) { var diffSprite:DifficultySprite = new DifficultySprite(diffId); diffSprite.difficultyId = diffId; + diffSprite.visible = diffId == Constants.DEFAULT_DIFFICULTY; grpDifficulties.add(diffSprite); } - grpDifficulties.group.forEach(function(spr) { - spr.visible = false; - }); - - for (diffSprite in grpDifficulties.group.members) - { - if (diffSprite == null) continue; - if (diffSprite.difficultyId == currentSuffixedDifficulty) diffSprite.visible = true; - } - albumRoll.albumId = null; add(albumRoll); var overhangStuff:FlxSprite = new FlxSprite().makeGraphic(FlxG.width, 164, FlxColor.BLACK); overhangStuff.y -= overhangStuff.height; - if (fromCharSelect == true) + if (fromCharSelect) { blackOverlayBullshitLOLXD.x = 387.76; overhangStuff.y = -100; @@ -602,6 +504,7 @@ class FreeplayState extends MusicBeatSubState wait: 0.1 }); + // Reminder, this is a callback function being set, rather than these being called here in create() letterSort.changeSelectionCallback = (str) -> { switch (str) { @@ -650,7 +553,7 @@ class FreeplayState extends MusicBeatSubState add(fnfFreeplay); add(ostName); - if (PlayerRegistry.instance.hasNewCharacter() == true) + if (PlayerRegistry.instance.hasNewCharacter()) { add(charSelectHint); } @@ -663,10 +566,10 @@ class FreeplayState extends MusicBeatSubState // when boyfriend hits dat shiii albumRoll.playIntro(); - var daSong = grpCapsules.members[curSelected].songData; - albumRoll.albumId = daSong?.albumId; + var daSong = grpCapsules.members[curSelected].freeplayData; + albumRoll.albumId = daSong?.data.getAlbumId(currentDifficulty, currentVariation); - if (fromCharSelect == null) + if (!fromCharSelect) { // render optimisation if (_parentState != null) _parentState.persistentDraw = false; @@ -735,6 +638,7 @@ class FreeplayState extends MusicBeatSubState onDJIntroDone(); } + // Generates song list with the starter params (who our current character is, last remembered difficulty, etc.) generateSongList(null, false); // dedicated camera for the state so we don't need to fuk around with camera scrolls from the mainmenu / elsewhere @@ -762,7 +666,7 @@ class FreeplayState extends MusicBeatSubState rankCamera.fade(0xFF000000, 0, false, null, true); } - if (fromCharSelect == true) + if (fromCharSelect) { enterFromCharSel(); onDJIntroDone(); @@ -773,7 +677,8 @@ class FreeplayState extends MusicBeatSubState var currentFilteredSongs:Array> = []; /** - * Given the current filter, rebuild the current song list. + * Given the current filter, rebuild the current song list and display it. + * Automatically takes into account currentDifficulty, character, and variation * * @param filterStuff A filter to apply to the song list (regex, startswith, all, favorite) * @param force Whether the capsules should "jump" back in or not using their animation @@ -785,27 +690,18 @@ class FreeplayState extends MusicBeatSubState if (filterStuff != null) tempSongs = sortSongs(tempSongs, filterStuff); - // Filter further by current selected difficulty. - if (currentSuffixedDifficulty != null) - { - tempSongs = tempSongs.filter(song -> { - if (song == null) return true; // Random + tempSongs = tempSongs.filter(song -> { + if (song == null) return true; // Random - // Check for character-specific difficulty first - var characterSuffixedDifficulty = '${currentUnsuffixedDifficulty}-${currentCharacterId}'; - if (song.suffixedSongDifficulties.contains(characterSuffixedDifficulty)) - { - return true; - } + // Available variations for current character. We get this since bf is usually `default` variation, and `pico` is `pico` + // but sometimes pico can be the default variation (weekend 1 songs), and bf can be `bf` variation (darnell) + var characterVariations:Array = song.data.getVariationsByCharacter(currentCharacter); - // Include songs that match the current suffixed difficulty (`normal-pico`) - // or the current unsuffixed difficulty, `normal` - // or songs specifically for the current character `normal` w/ songCharacter == `pico` - return (song.suffixedSongDifficulties.contains(currentSuffixedDifficulty) - || song.songDifficulties.contains(currentUnsuffixedDifficulty) - || song.songCharacter == currentCharacterId); - }); - } + // Gets all available difficulties for our character, via our available variations + var difficultiesAvailable:Array = song.data.listDifficulties(null, characterVariations); + + return difficultiesAvailable.contains(currentDifficulty); + }); if (onlyIfChanged) { @@ -814,28 +710,20 @@ class FreeplayState extends MusicBeatSubState } // Only now do we know that the filter is actually changing. - - // If curSelected is 0, the result will be null and fall back to the rememberedSongId. - rememberedSongId = grpCapsules.members[curSelected]?.songData?.songId ?? rememberedSongId; - - for (cap in grpCapsules.members) - { - cap.songText.resetText(); - cap.kill(); - } - currentFilter = filterStuff; currentFilteredSongs = tempSongs; curSelected = 0; - var hsvShader:HSVShader = new HSVShader(); + // If curSelected is 0, the result will be null and fall back to the rememberedSongId. + // We set this so if we change the filter, we'd remain on the same song if it's still in the list. + rememberedSongId = grpCapsules.members[curSelected]?.freeplayData?.data.id ?? rememberedSongId; + grpCapsules.killMembers(); + + // Initialize the random capsule, with empty/blank info (which we display once bf/pico does his hand) var randomCapsule:SongMenuItem = grpCapsules.recycle(SongMenuItem); randomCapsule.init(FlxG.width, 0, null, styleData); - randomCapsule.onConfirm = function() { - capsuleOnConfirmRandom(randomCapsule); - }; randomCapsule.y = randomCapsule.intendedY(0) + 10; randomCapsule.targetPos.x = randomCapsule.x; randomCapsule.alpha = 0; @@ -844,14 +732,15 @@ class FreeplayState extends MusicBeatSubState randomCapsule.favIconBlurred.visible = false; randomCapsule.ranking.visible = false; randomCapsule.blurredRanking.visible = false; - if (fromCharSelect == false) - { - randomCapsule.initJumpIn(0, force); - } + randomCapsule.onConfirm = function() { + capsuleOnConfirmRandom(randomCapsule); + }; + + if (fromCharSelect) randomCapsule.forcePosition(); else - { - randomCapsule.forcePosition(); - } + randomCapsule.initJumpIn(0, force); + + var hsvShader:HSVShader = new HSVShader(); randomCapsule.hsvShader = hsvShader; grpCapsules.add(randomCapsule); @@ -870,13 +759,8 @@ class FreeplayState extends MusicBeatSubState funnyMenu.targetPos.x = funnyMenu.x; funnyMenu.ID = i; funnyMenu.capsule.alpha = 0.5; - funnyMenu.songText.visible = false; - funnyMenu.favIcon.visible = tempSong.isFav; - funnyMenu.favIconBlurred.visible = tempSong.isFav; funnyMenu.hsvShader = hsvShader; - funnyMenu.newText.animation.curAnim.curFrame = 45 - ((i * 4) % 45); - funnyMenu.checkClip(); funnyMenu.forcePosition(); grpCapsules.add(funnyMenu); @@ -885,9 +769,8 @@ class FreeplayState extends MusicBeatSubState FlxG.console.registerFunction('changeSelection', changeSelection); rememberSelection(); - changeSelection(); - changeDiff(0, true); + refreshCapsuleDisplays(); } /** @@ -899,7 +782,7 @@ class FreeplayState extends MusicBeatSubState public function sortSongs(songsToFilter:Array>, songFilter:SongFilter):Array> { var filterAlphabetically = function(a:Null, b:Null):Int { - return SortUtil.alphabetically(a?.songName ?? '', b?.songName ?? ''); + return SortUtil.alphabetically(a?.data.songName ?? '', b?.data.songName ?? ''); }; switch (songFilter.filterType) @@ -911,9 +794,9 @@ class FreeplayState extends MusicBeatSubState // if filterData looks like "A-C", the regex should look something like this: ^[A-C].* // to get every song that starts between A and C var filterRegexp:EReg = new EReg('^[' + songFilter.filterData + '].*', 'i'); - songsToFilter = songsToFilter.filter(str -> { - if (str == null) return true; // Random - return filterRegexp.match(str.songName); + songsToFilter = songsToFilter.filter(filteredSong -> { + if (filteredSong == null) return true; // Random + return filterRegexp.match(filteredSong.data.songName); }); songsToFilter.sort(filterAlphabetically); @@ -921,16 +804,16 @@ class FreeplayState extends MusicBeatSubState case STARTSWITH: // extra note: this is essentially a "search" - songsToFilter = songsToFilter.filter(str -> { - if (str == null) return true; // Random - return str.songName.toLowerCase().startsWith(songFilter.filterData ?? ''); + songsToFilter = songsToFilter.filter(filteredSong -> { + if (filteredSong == null) return true; // Random + return filteredSong.data.songName.toLowerCase().startsWith(songFilter.filterData ?? ''); }); case ALL: // no filter! case FAVORITE: - songsToFilter = songsToFilter.filter(str -> { - if (str == null) return true; // Random - return str.isFav; + songsToFilter = songsToFilter.filter(filteredSong -> { + if (filteredSong == null) return true; // Random + return filteredSong.isFav; }); songsToFilter.sort(filterAlphabetically); @@ -1298,7 +1181,7 @@ class FreeplayState extends MusicBeatSubState } } fadeShader.fade(1.0, 0.0, 0.8, {ease: FlxEase.quadIn}); - FlxG.sound.music.fadeOut(0.9, 0); + FlxG.sound.music?.fadeOut(0.9, 0); new FlxTimer().start(0.9, _ -> { FlxG.switchState(new funkin.ui.charSelect.CharSelectSubState()); }); @@ -1461,7 +1344,7 @@ class FreeplayState extends MusicBeatSubState if (controls.FREEPLAY_FAVORITE && !busy) { - var targetSong = grpCapsules.members[curSelected]?.songData; + var targetSong = grpCapsules.members[curSelected]?.freeplayData; if (targetSong != null) { var realShit:Int = curSelected; @@ -1504,7 +1387,6 @@ class FreeplayState extends MusicBeatSubState busy = true; grpCapsules.members[realShit].doLerp = false; FlxTween.tween(grpCapsules.members[realShit], {y: grpCapsules.members[realShit].y + 5}, 0.1, {ease: FlxEase.expoOut}); - FlxTween.tween(grpCapsules.members[realShit], {y: grpCapsules.members[realShit].y - 5}, 0.1, { ease: FlxEase.expoIn, @@ -1792,74 +1674,72 @@ class FreeplayState extends MusicBeatSubState public override function destroy():Void { super.destroy(); - var daSong:Null = currentFilteredSongs[curSelected]; - if (daSong != null) - { - clearDaCache(daSong.songName); - } - // remove and destroy freeplay camera FlxG.cameras.remove(funnyCam); } + /** + * changeDiff is the root of both difficulty and variation changes/management. + * It will check the difficulty of the current variation, all available variations, and all available difficulties per variation. + * It's generally recommended that after calling this you re-sort the song list, however usually it's already on the way to being sorted. + * @param change + * @param force + */ function changeDiff(change:Int = 0, force:Bool = false):Void { touchTimer = 0; - var currentDifficultyIndex:Int = suffixedDiffIdsCurrent.indexOf(currentSuffixedDifficulty); + // Available variations for current character. We get this since bf is usually `default` variation, and `pico` is `pico` + // but sometimes pico can be the default variation (weekend 1 songs), and bf can be `bf` variation (darnell) + var characterVariations:Array = grpCapsules.members[curSelected].freeplayData?.data.getVariationsByCharacter(currentCharacter) ?? Constants.DEFAULT_VARIATION_LIST; - if (currentDifficultyIndex == -1) currentDifficultyIndex = suffixedDiffIdsCurrent.indexOf(Constants.DEFAULT_DIFFICULTY); + // Gets all available difficulties for our character, via our available variations + var difficultiesAvailable:Array = grpCapsules.members[curSelected].freeplayData?.data.listDifficulties(null, + characterVariations) ?? Constants.DEFAULT_DIFFICULTY_LIST; + + var currentDifficultyIndex:Int = difficultiesAvailable.indexOf(currentDifficulty); + + if (currentDifficultyIndex == -1) currentDifficultyIndex = difficultiesAvailable.indexOf(Constants.DEFAULT_DIFFICULTY); currentDifficultyIndex += change; - if (currentDifficultyIndex < 0) currentDifficultyIndex = suffixedDiffIdsCurrent.length - 1; - if (currentDifficultyIndex >= suffixedDiffIdsCurrent.length) currentDifficultyIndex = 0; + if (currentDifficultyIndex < 0) currentDifficultyIndex = Std.int(difficultiesAvailable.length - 1); + if (currentDifficultyIndex >= difficultiesAvailable.length) currentDifficultyIndex = 0; - var newSuffixedDifficulty = suffixedDiffIdsCurrent[currentDifficultyIndex]; - - // Always try to use the character-specific difficulty - var characterSuffixedDifficulty = '${newSuffixedDifficulty}-${currentCharacterId}'; - if (suffixedDiffIdsCurrent.contains(characterSuffixedDifficulty)) + // Update the current difficulty + currentDifficulty = difficultiesAvailable[currentDifficultyIndex]; + for (variation in characterVariations) { - newSuffixedDifficulty = characterSuffixedDifficulty; + if (grpCapsules.members[curSelected].freeplayData?.data.hasDifficulty(currentDifficulty, variation) ?? false) + { + currentVariation = variation; + rememberedVariation = variation; + break; + } } - currentSuffixedDifficulty = newSuffixedDifficulty; + trace('CURRENT VARIATION OF SONG: ${currentVariation}'); - trace('Switching to difficulty: ${currentSuffixedDifficulty}'); - trace(suffixedDiffIdsCurrent); - - var daSong:Null = grpCapsules.members[curSelected].songData; + var daSong:Null = grpCapsules.members[curSelected].freeplayData; if (daSong != null) { - var targetSong:Null = SongRegistry.instance.fetchEntry(daSong.songId); + var targetSong:Null = SongRegistry.instance.fetchEntry(daSong.data.id); if (targetSong == null) { - FlxG.log.warn('WARN: could not find song with id (${daSong.songId})'); + FlxG.log.warn('WARN: could not find song with id (${daSong.data.id})'); return; } - var suffixedDifficulty = suffixedDiffIdsCurrent[currentDifficultyIndex]; - var unsuffixedDifficulty = currentUnsuffixedDifficulty; - - // Check for character-specific difficulty - var characterSuffixedDifficulty = '${unsuffixedDifficulty}-${currentCharacterId}'; - if (daSong.suffixedSongDifficulties.contains(characterSuffixedDifficulty)) - { - suffixedDifficulty = characterSuffixedDifficulty; - } - - var songScore:Null = Save.instance.getSongScore(daSong.songId, suffixedDifficulty); - trace(songScore); + var songScore:Null = Save.instance.getSongScore(daSong.data.id, currentDifficulty); intendedScore = songScore?.score ?? 0; intendedCompletion = songScore == null ? 0.0 : ((songScore.tallies.sick + songScore.tallies.good) / songScore.tallies.totalNotes); - rememberedDifficulty = suffixedDifficulty; - currentSuffixedDifficulty = suffixedDifficulty; + rememberedDifficulty = currentDifficulty; + grpCapsules.members[curSelected].refreshDisplay(); } else { intendedScore = 0; intendedCompletion = 0.0; - rememberedDifficulty = currentSuffixedDifficulty; + rememberedDifficulty = currentDifficulty; } if (intendedCompletion == Math.POSITIVE_INFINITY || intendedCompletion == Math.NEGATIVE_INFINITY || Math.isNaN(intendedCompletion)) @@ -1867,15 +1747,15 @@ class FreeplayState extends MusicBeatSubState intendedCompletion = 0; } - grpDifficulties.group.forEach(function(diffSprite) { - diffSprite.visible = false; - }); - for (diffSprite in grpDifficulties.group.members) { if (diffSprite == null) continue; - if (diffSprite.difficultyId == currentSuffixedDifficulty) + diffSprite.visible = false; + + if (diffSprite.difficultyId == currentDifficulty) { + diffSprite.visible = true; + if (change != 0) { diffSprite.visible = true; @@ -1886,10 +1766,6 @@ class FreeplayState extends MusicBeatSubState diffSprite.updateHitbox(); }); } - else - { - diffSprite.visible = true; - } } } @@ -1899,18 +1775,12 @@ class FreeplayState extends MusicBeatSubState for (songCapsule in grpCapsules.members) { if (songCapsule == null) continue; - if (songCapsule.songData != null) + + if (songCapsule.freeplayData != null) { - songCapsule.songData.currentVariation = currentVariation; - songCapsule.songData.currentUnsuffixedDifficulty = currentUnsuffixedDifficulty; - songCapsule.songData.currentSuffixedDifficulty = currentSuffixedDifficulty; - songCapsule.init(null, null, songCapsule.songData); + songCapsule.init(null, null, songCapsule.freeplayData); songCapsule.checkClip(); } - else - { - songCapsule.init(null, null, null); - } } // Reset the song preview in case we changed variations (normal->erect etc) @@ -1918,7 +1788,7 @@ class FreeplayState extends MusicBeatSubState } // Set the album graphic and play the animation if relevant. - var newAlbumId:Null = daSong?.albumId; + var newAlbumId:Null = daSong?.data.getAlbumId(currentDifficulty, currentVariation); if (albumRoll.albumId != newAlbumId) { albumRoll.albumId = newAlbumId; @@ -1926,21 +1796,7 @@ class FreeplayState extends MusicBeatSubState } // Set difficulty star count. - albumRoll.setDifficultyStars(daSong?.difficultyRating); - } - - // Clears the cache of songs to free up memory, they'll have to be loaded in later tho - function clearDaCache(actualSongTho:String):Void - { - for (song in songs) - { - if (song == null) continue; - if (song.songName != actualSongTho) - { - trace('trying to remove: ' + song.songName); - // openfl.Assets.cache.clear(Paths.inst(song.songName)); - } - } + albumRoll.setDifficultyStars(daSong?.data.getDifficulty(currentDifficulty, currentVariation)?.difficultyRating ?? 0); } function capsuleOnConfirmRandom(randomCapsule:SongMenuItem):Void @@ -1952,11 +1808,11 @@ class FreeplayState extends MusicBeatSubState var availableSongCapsules:Array = grpCapsules.members.filter(function(cap:SongMenuItem) { // Dead capsules are ones which were removed from the list when changing filters. - return cap.alive && cap.songData != null; + return cap.alive && cap.freeplayData != null; }); trace('Available songs: ${availableSongCapsules.map(function(cap) { - return cap?.songData?.songName; + return cap?.freeplayData?.data.songName; })}'); if (availableSongCapsules.length == 0) @@ -1983,7 +1839,7 @@ class FreeplayState extends MusicBeatSubState */ function capsuleOnOpenDefault(cap:SongMenuItem):Void { - var targetSongId:String = cap?.songData?.songId ?? 'unknown'; + var targetSongId:String = cap?.freeplayData?.data.id ?? 'unknown'; var targetSongNullable:Null = SongRegistry.instance.fetchEntry(targetSongId); if (targetSongNullable == null) { @@ -1991,10 +1847,10 @@ class FreeplayState extends MusicBeatSubState return; } var targetSong:Song = targetSongNullable; - var targetDifficultyId:String = currentUnsuffixedDifficulty; + var targetDifficultyId:String = currentDifficulty; var targetVariation:Null = currentVariation; trace('target song: ${targetSongId} (${targetVariation})'); - var targetLevelId:Null = cap?.songData?.levelId; + var targetLevelId:Null = cap?.freeplayData?.levelId; PlayStatePlaylist.campaignId = targetLevelId ?? null; var targetDifficulty:Null = targetSong.getDifficulty(targetDifficultyId, targetVariation); @@ -2065,7 +1921,7 @@ class FreeplayState extends MusicBeatSubState PlayStatePlaylist.isStoryMode = false; - var targetSongId:String = cap?.songData?.songId ?? 'unknown'; + var targetSongId:String = cap?.freeplayData?.data.id ?? 'unknown'; var targetSongNullable:Null = SongRegistry.instance.fetchEntry(targetSongId); if (targetSongNullable == null) { @@ -2073,21 +1929,20 @@ class FreeplayState extends MusicBeatSubState return; } var targetSong:Song = targetSongNullable; - var targetDifficultyId:String = currentUnsuffixedDifficulty; var targetVariation:Null = currentVariation; - var targetLevelId:Null = cap?.songData?.levelId; + var targetLevelId:Null = cap?.freeplayData?.levelId; PlayStatePlaylist.campaignId = targetLevelId ?? null; - var targetDifficulty:Null = targetSong.getDifficulty(targetDifficultyId, targetVariation); + var targetDifficulty:Null = targetSong.getDifficulty(currentDifficulty, currentVariation); if (targetDifficulty == null) { - FlxG.log.warn('WARN: could not find difficulty with id (${targetDifficultyId})'); + FlxG.log.warn('WARN: could not find difficulty with id (${currentDifficulty})'); return; } if (targetInstId == null) { - var baseInstrumentalId:String = targetSong?.getBaseInstrumentalId(targetDifficultyId, targetDifficulty.variation ?? Constants.DEFAULT_VARIATION) ?? ''; + var baseInstrumentalId:String = targetSong?.getBaseInstrumentalId(currentDifficulty, targetDifficulty.variation ?? Constants.DEFAULT_VARIATION) ?? ''; targetInstId = baseInstrumentalId; } @@ -2103,12 +1958,12 @@ class FreeplayState extends MusicBeatSubState new FlxTimer().start(styleData?.getStartDelay(), function(tmr:FlxTimer) { FunkinSound.emptyPartialQueue(); - Paths.setCurrentLevel(cap?.songData?.levelId); + Paths.setCurrentLevel(cap?.freeplayData?.levelId); LoadingState.loadPlayState( { targetSong: targetSong, - targetDifficulty: targetDifficultyId, - targetVariation: targetVariation, + targetDifficulty: currentDifficulty, + targetVariation: currentVariation, targetInstrumental: targetInstId, practiceMode: false, minimalMode: false, @@ -2126,13 +1981,20 @@ class FreeplayState extends MusicBeatSubState }); } + function refreshCapsuleDisplays():Void + { + grpCapsules.forEachAlive((cap:SongMenuItem) -> { + cap.refreshDisplay(); + }); + } + function rememberSelection():Void { if (rememberedSongId != null) { curSelected = currentFilteredSongs.findIndex(function(song) { if (song == null) return false; - return song.songId == rememberedSongId; + return song.data.id == rememberedSongId; }); if (curSelected == -1) curSelected = 0; @@ -2140,7 +2002,12 @@ class FreeplayState extends MusicBeatSubState if (rememberedDifficulty != null) { - currentSuffixedDifficulty = rememberedDifficulty; + currentDifficulty = rememberedDifficulty; + } + + if (rememberedVariation != null) + { + currentVariation = rememberedVariation; } } @@ -2156,22 +2023,19 @@ class FreeplayState extends MusicBeatSubState if (curSelected >= grpCapsules.countLiving()) curSelected = 0; var daSongCapsule:SongMenuItem = grpCapsules.members[curSelected]; - if (daSongCapsule.songData != null) + if (daSongCapsule.freeplayData != null) { - var songScore:Null = Save.instance.getSongScore(daSongCapsule.songData.songId, currentSuffixedDifficulty); + var songScore:Null = Save.instance.getSongScore(daSongCapsule.freeplayData.data.id, currentDifficulty); intendedScore = songScore?.score ?? 0; intendedCompletion = songScore == null ? 0.0 : ((songScore.tallies.sick + songScore.tallies.good) / songScore.tallies.totalNotes); - diffIdsCurrent = daSongCapsule.songData.songDifficulties; - suffixedDiffIdsCurrent = daSongCapsule.songData.suffixedSongDifficulties; - rememberedSongId = daSongCapsule.songData.songId; + rememberedSongId = daSongCapsule.freeplayData.data.id; changeDiff(); + daSongCapsule.refreshDisplay(); } else { intendedScore = 0; intendedCompletion = 0.0; - diffIdsCurrent = diffIdsTotal; - suffixedDiffIdsCurrent = suffixedDiffIdsTotal; rememberedSongId = null; rememberedDifficulty = Constants.DEFAULT_DIFFICULTY; albumRoll.albumId = null; @@ -2199,7 +2063,6 @@ class FreeplayState extends MusicBeatSubState public function playCurSongPreview(?daSongCapsule:SongMenuItem):Void { if (daSongCapsule == null) daSongCapsule = grpCapsules.members[curSelected]; - if (curSelected == 0) { FunkinSound.playMusic('freeplayRandom', @@ -2212,43 +2075,25 @@ class FreeplayState extends MusicBeatSubState } else { - var previewSongId:Null = daSongCapsule?.songData?.songId; - if (previewSongId == null) return; - - var previewSong:Null = SongRegistry.instance.fetchEntry(previewSongId); + var previewSong:Null = daSongCapsule?.freeplayData?.data; if (previewSong == null) return; - var targetDifficultyId:String = currentUnsuffixedDifficulty; - var targetVariation:Null = currentVariation; - // Check if character-specific difficulty exists - var characterSuffixedDifficulty:String = '${targetDifficultyId}-${currentCharacterId}'; - var suffixedSongDifficulties:Array = daSongCapsule.songData?.suffixedSongDifficulties ?? Constants.DEFAULT_DIFFICULTY_LIST; - if (suffixedSongDifficulties != null && suffixedSongDifficulties.contains(characterSuffixedDifficulty)) - { - targetDifficultyId = characterSuffixedDifficulty; - } + var songDifficulty:Null = previewSong.getDifficulty(currentDifficulty, currentVariation); - var songDifficulty:Null = previewSong.getDifficulty(targetDifficultyId, targetVariation ?? Constants.DEFAULT_VARIATION); - - var baseInstrumentalId:String = previewSong.getBaseInstrumentalId(targetDifficultyId, songDifficulty?.variation ?? Constants.DEFAULT_VARIATION) ?? ''; - var altInstrumentalIds:Array = previewSong.listAltInstrumentalIds(targetDifficultyId, + var baseInstrumentalId:String = previewSong.getBaseInstrumentalId(currentDifficulty, songDifficulty?.variation ?? Constants.DEFAULT_VARIATION) ?? ''; + var altInstrumentalIds:Array = previewSong.listAltInstrumentalIds(currentDifficulty, songDifficulty?.variation ?? Constants.DEFAULT_VARIATION) ?? []; - var instSuffix:String = baseInstrumentalId; - #if FEATURE_DEBUG_FUNCTIONS if (altInstrumentalIds.length > 0 && FlxG.keys.pressed.CONTROL) { instSuffix = altInstrumentalIds[0]; } #end - instSuffix = (instSuffix != '') ? '-$instSuffix' : ''; - - trace('Attempting to play partial preview: ${previewSongId}:${instSuffix}'); - - FunkinSound.playMusic(previewSongId, + trace('Attempting to play partial preview: ${previewSong.id}:${instSuffix}'); + FunkinSound.playMusic(previewSong.id, { startingVolume: 0.0, overrideExisting: true, @@ -2266,7 +2111,6 @@ class FreeplayState extends MusicBeatSubState FlxG.sound.music.fadeIn(2, 0, 0.4); } }); - if (songDifficulty != null) { Conductor.instance.mapTimeChanges(songDifficulty.timeChanges); @@ -2382,58 +2226,67 @@ enum abstract FilterType(String) */ class FreeplaySongData { + /** + * We used to have a billion fields, but this SongMetadata variable should be all we need + * to be able to get most information about an available song. + * For example, you can get the artist via `data.songArtist` + * + * You can usually get various other particulars of a specific difficulty/variation by + * using data.getDifficulty(), and inputting specifics on your difficulty, variations, etc. + * See the getters here for songCharacter, fullSongName, and songStartingBpm for examples. + * + * @see Song + */ + public var data:Song; + + /** + * The level id of the song, useful for sorting from week1 -> week 7 + weekend1 + * and for properly loading PlayStatePlaylist for preloading on web + */ + public var levelId(get, never):Null; + + function get_levelId():Null + { + return _levelId; + } + + var _levelId:String; + /** * Whether or not the song has been favorited. */ public var isFav:Bool = false; + /** + * Whether the player has seen/played this song before within freeplay + */ public var isNew:Bool = false; - var song:Song; + /** + * The default opponent for the song. + * Does the getter stuff for you depending on your current (or rather, rememberd) variation and difficulty. + */ + public var songCharacter(get, never):String; - public var levelId(default, null):String = ''; - public var songId(default, null):String = ''; + /** + * The full song name, dynamically generated depending on your current (or rather, rememberd) variation and difficulty. + */ + public var fullSongName(get, never):String; - public var songDifficulties(default, null):Array = []; - public var suffixedSongDifficulties(default, null):Array = []; + /** + * The starting BPM of the song, dynamically generated depending on your current (or rather, rememberd) variation and difficulty. + */ + public var songStartingBpm(get, never):Float; - public var songName(default, null):String = ''; - public var songCharacter(default, null):String = ''; - public var songStartingBpm(default, null):Float = 0; - public var difficultyRating(default, null):Int = 0; - public var albumId(default, null):Null = null; + public var difficultyRating(get, never):Int; - public var currentCharacter:PlayableCharacter; - public var currentVariation:String = Constants.DEFAULT_VARIATION; - public var currentSuffixedDifficulty(default, set):String = Constants.DEFAULT_DIFFICULTY; - public var currentUnsuffixedDifficulty:String = Constants.DEFAULT_DIFFICULTY; + public var scoringRank(get, never):Null; - public var scoringRank:Null = null; - - var displayedVariations:Array = [Constants.DEFAULT_VARIATION]; - - function set_currentSuffixedDifficulty(value:String):String + public function new(data:Song, levelData:Level) { - if (currentSuffixedDifficulty == value) return value; - - currentSuffixedDifficulty = value; - updateValues(displayedVariations); - return value; - } - - public function new(levelId:String, songId:String, song:Song, currentCharacter:PlayableCharacter, ?displayedVariations:Array) - { - this.levelId = levelId; - this.songId = songId; - this.song = song; - - this.isFav = Save.instance.isSongFavorited(songId); - - this.currentCharacter = currentCharacter; - - if (displayedVariations != null) this.displayedVariations = displayedVariations; - - updateValues(displayedVariations); + this.data = data; + _levelId = levelData.id; + this.isFav = Save.instance.isSongFavorited(data.songName); } /** @@ -2445,72 +2298,96 @@ class FreeplaySongData isFav = !isFav; if (isFav) { - Save.instance.favoriteSong(this.songId); + Save.instance.favoriteSong(data.songName); } else { - Save.instance.unfavoriteSong(this.songId); + Save.instance.unfavoriteSong(data.songName); } return isFav; } function updateValues(variations:Array):Void { - trace('variations: ${variations}'); - this.songDifficulties = song.listDifficulties(null, variations, false, false); - this.suffixedSongDifficulties = song.listSuffixedDifficulties(variations, false, false); + // this.isNew = song.isSongNew(suffixedDifficulty); + } - // Add character-specific difficulties - for (difficulty in this.songDifficulties) - { - var characterDifficulty = '${difficulty}-${currentCharacter.id}'; - if (!this.suffixedSongDifficulties.contains(characterDifficulty)) - { - this.suffixedSongDifficulties.push(characterDifficulty); - } - } + function get_songCharacter():String + { + var variations:Array = data.getVariationsByCharacterId(FreeplayState.rememberedCharacterId); + return data.getDifficulty(FreeplayState.rememberedDifficulty, null, variations)?.characters.opponent ?? ''; + } - // Prioritize character-specific difficulty - var characterSuffixedDifficulty = '${currentUnsuffixedDifficulty}-${currentCharacter.id}'; - if (this.suffixedSongDifficulties.contains(characterSuffixedDifficulty)) - { - currentSuffixedDifficulty = characterSuffixedDifficulty; - } - else if (!this.songDifficulties.contains(currentUnsuffixedDifficulty) - && !this.suffixedSongDifficulties.contains(currentSuffixedDifficulty)) - { - currentSuffixedDifficulty = Constants.DEFAULT_DIFFICULTY; - // This method gets called again by the setter-method - // or the difficulty didn't change, so there's no need to continue. - return; - } + function get_fullSongName():String + { + var variations:Array = data.getVariationsByCharacterId(FreeplayState.rememberedCharacterId); - var targetVariation:Null = currentVariation; + return data.getDifficulty(FreeplayState.rememberedDifficulty, null, variations)?.songName ?? data.songName; + } - var songDifficulty:SongDifficulty = song.getDifficulty(currentUnsuffixedDifficulty, targetVariation); - if (songDifficulty == null) return; - this.songStartingBpm = songDifficulty.getStartingBPM(); - this.songName = songDifficulty.songName; - this.songCharacter = songDifficulty.characters.opponent; - this.difficultyRating = songDifficulty.difficultyRating; - if (songDifficulty.album == null) - { - FlxG.log.warn('No album for: ${songDifficulty.songName}'); - this.albumId = Constants.DEFAULT_ALBUM_ID; - } - else - { - this.albumId = songDifficulty.album; - } + function get_songStartingBpm():Float + { + var variations:Array = data.getVariationsByCharacterId(FreeplayState.rememberedCharacterId); - var suffixedDifficulty = currentSuffixedDifficulty; + return data.getDifficulty(FreeplayState.rememberedDifficulty, null, variations)?.getStartingBPM() ?? 0; + } - this.scoringRank = Save.instance.getSongRank(songId, suffixedDifficulty); + function get_difficultyRating():Int + { + var variations:Array = data.getVariationsByCharacterId(FreeplayState.rememberedCharacterId); + return data.getDifficulty(FreeplayState.rememberedDifficulty, null, variations)?.difficultyRating ?? 0; + } - this.isNew = song.isSongNew(suffixedDifficulty); + function get_scoringRank():Null + { + // TODO: Properly get/migrate the save data from the suffixed difficulty version to our new unsuffixed version + return Save.instance.getSongRank(data.songName, FreeplayState.rememberedDifficulty); } } +/** + * Parameters used to initialize the FreeplayState. + */ +typedef FreeplayStateParams = +{ + ?character:String, + + ?fromCharSelect:Bool, + + ?fromResults:FromResultsParams, +}; + +/** + * A set of parameters for transitioning to the FreeplayState from the ResultsState. + */ +typedef FromResultsParams = +{ + /** + * The previous rank the song hand, if any. Null if it had no score before. + */ + var ?oldRank:ScoringRank; + + /** + * Whether or not to play the rank animation on returning to freeplay. + */ + var playRankAnim:Bool; + + /** + * The new rank the song has. + */ + var newRank:ScoringRank; + + /** + * The song ID to play the animation on. + */ + var songId:String; + + /** + * The difficulty ID to play the animation on. + */ + var difficultyId:String; +}; + /** * The map storing information about the exit movers. */ diff --git a/source/funkin/ui/freeplay/SongMenuItem.hx b/source/funkin/ui/freeplay/SongMenuItem.hx index 864fa2d1d..3db958bb5 100644 --- a/source/funkin/ui/freeplay/SongMenuItem.hx +++ b/source/funkin/ui/freeplay/SongMenuItem.hx @@ -36,7 +36,7 @@ class SongMenuItem extends FlxSpriteGroup * Modify this by calling `init()` * If `null`, assume this SongMenuItem is for the "Random Song" option. */ - public var songData(default, null):Null = null; + public var freeplayData(default, null):Null = null; public var selected(default, set):Bool; @@ -422,6 +422,34 @@ class SongMenuItem extends FlxSpriteGroup return evilTrail.color; } + public function refreshDisplay():Void + { + if (freeplayData == null) + { + songText.text = 'Random'; + pixelIcon.visible = false; + ranking.visible = false; + blurredRanking.visible = false; + favIcon.visible = false; + favIconBlurred.visible = false; + newText.visible = false; + } + else + { + songText.text = freeplayData.fullSongName; + if (freeplayData.songCharacter != null) pixelIcon.setCharacter(freeplayData.songCharacter); + pixelIcon.visible = true; + updateBPM(Std.int(freeplayData.songStartingBpm) ?? 0); + updateDifficultyRating(freeplayData.difficultyRating ?? 0); + updateScoringRank(freeplayData.scoringRank); + newText.visible = freeplayData.isNew; + favIcon.visible = freeplayData.isFav; + favIconBlurred.visible = freeplayData.isFav; + checkClip(); + } + updateSelected(); + } + function updateDifficultyRating(newRating:Int):Void { var ratingPadded:String = newRating < 10 ? '0$newRating' : '$newRating'; @@ -500,11 +528,11 @@ class SongMenuItem extends FlxSpriteGroup updateSelected(); } - public function init(?x:Float, ?y:Float, songData:Null, ?styleData:FreeplayStyle = null):Void + public function init(?x:Float, ?y:Float, freeplayData:Null, ?styleData:FreeplayStyle = null):Void { if (x != null) this.x = x; if (y != null) this.y = y; - this.songData = songData; + this.freeplayData = freeplayData; // im so mad i have to do this but im pretty sure with the capsules recycling i cant call the new function properly :/ // if thats possible someone Please change the new function to be something like @@ -517,21 +545,13 @@ class SongMenuItem extends FlxSpriteGroup songText.applyStyle(styleData); } - // Update capsule text. - songText.text = songData?.songName ?? 'Random'; - // Update capsule character. - if (songData?.songCharacter != null) pixelIcon.setCharacter(songData.songCharacter); - updateBPM(Std.int(songData?.songStartingBpm) ?? 0); - updateDifficultyRating(songData?.difficultyRating ?? 0); - updateScoringRank(songData?.scoringRank); - newText.visible = songData?.isNew; + updateScoringRank(freeplayData?.scoringRank); favIcon.animation.curAnim.curFrame = favIcon.animation.curAnim.numFrames - 1; favIconBlurred.animation.curAnim.curFrame = favIconBlurred.animation.curAnim.numFrames - 1; - // Update opacity, offsets, etc. - updateSelected(); + refreshDisplay(); - checkWeek(songData?.songId); + checkWeek(freeplayData?.data.id); } var frameInTicker:Float = 0;