1
0
Fork 0
mirror of https://github.com/ninjamuffin99/Funkin.git synced 2024-10-02 07:28:53 +00:00

index on rewrite/bugfix/pause-and-results-fixes: 9b8fc872 song diff menu sort

This commit is contained in:
Cameron Taylor 2023-10-17 00:38:28 -04:00
parent 9b8fc87261
commit afcb677fac
43 changed files with 2527 additions and 1014 deletions

View file

@ -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: |

View file

@ -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

View file

@ -156,7 +156,6 @@
<haxedef name="HXCPP_CHECK_POINTER" />
<haxedef name="HXCPP_STACK_LINE" />
<haxedef name="HXCPP_STACK_TRACE" />
<haxedef name="openfl-enable-handle-error" />
<!-- This macro allows addition of new functionality to existing Flixel. -->
<haxeflag name="--macro" value="addMetadata('@:build(funkin.util.macro.FlxMacro.buildFlxBasic())', 'flixel.FlxBasic')" />
<!--Place custom nodes like icons here (higher priority to override the HaxeFlixel icon)-->
@ -196,6 +195,22 @@
<haxedef name="REDIRECT_ASSETS_FOLDER" />
</section>
<section>
<!--
This flag enables the popup/crashlog error handler.
However, it also messes with breakpoints on some platforms.
-->
<haxedef name="openfl-enable-handle-error" />
</section>
<section>
<!-- TODO: Add a flag to Github Actions to turn this on or something. -->
<!-- Forces the version string to include the Git hash even on release builds (which are used for performance reasons). -->
<haxedef name="FORCE_DEBUG_VERSION" />
</section>
<!-- Run a script before and after building. -->
<postbuild haxe="source/Prebuild.hx"/> -->
<postbuild haxe="source/Postbuild.hx"/> -->

2
assets

@ -1 +1 @@
Subproject commit 6e5ed46026a2eb1e575c5accf9192b90c13ff857
Subproject commit 8104d43e584a1f25e574438d7b21a7e671358969

View file

@ -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"
},
{

View file

@ -4,6 +4,7 @@ import flixel.FlxGame;
import flixel.FlxState;
import funkin.util.logging.CrashHandler;
import funkin.MemoryCounter;
import funkin.save.Save;
import haxe.ui.Toolkit;
import openfl.display.FPS;
import openfl.display.Sprite;
@ -84,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

View file

@ -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<Int> {
return gamepadsAdded;
}
public function getGamepads():Array<FlxGamepad> {
return [for (id in gamepadsAdded) FlxG.gamepads.getByID(id)];
}
inline function addGamepadLiteral(id:Int, ?buttonMap:Map<Control, Array<FlxGamepadInputID>>):Void
{
gamepadsAdded.push(id);

View file

@ -1,54 +1,55 @@
package funkin;
import funkin.shaderslmfao.HSVShader;
import funkin.ui.StickerSubState;
import flash.text.TextField;
import flixel.addons.display.FlxGridOverlay;
import flixel.addons.transition.FlxTransitionableState;
import flixel.addons.ui.FlxInputText;
import flixel.FlxCamera;
import flixel.FlxGame;
import flixel.FlxSprite;
import flixel.FlxState;
import flixel.addons.display.FlxGridOverlay;
import flixel.addons.transition.FlxTransitionableState;
import flixel.addons.ui.FlxInputText;
import flixel.group.FlxGroup.FlxTypedGroup;
import flixel.group.FlxGroup;
import flixel.group.FlxGroup.FlxTypedGroup;
import flixel.group.FlxSpriteGroup;
import flixel.system.debug.watch.Tracker.TrackerProfile;
import flixel.input.touch.FlxTouch;
import flixel.math.FlxAngle;
import flixel.math.FlxMath;
import flixel.math.FlxPoint;
import flixel.system.debug.watch.Tracker.TrackerProfile;
import flixel.text.FlxText;
import flixel.tweens.FlxEase;
import flixel.tweens.FlxTween;
import flixel.util.FlxColor;
import funkin.data.song.SongRegistry;
import funkin.data.level.LevelRegistry;
import flixel.util.FlxSpriteUtil;
import flixel.util.FlxTimer;
import funkin.Controls.Control;
import funkin.data.level.LevelRegistry;
import funkin.data.song.SongRegistry;
import funkin.freeplayStuff.BGScrollingText;
import funkin.freeplayStuff.DifficultyStars;
import funkin.freeplayStuff.DJBoyfriend;
import funkin.freeplayStuff.FreeplayScore;
import funkin.freeplayStuff.LetterSort;
import funkin.freeplayStuff.SongMenuItem;
import funkin.freeplayStuff.DifficultyStars;
import funkin.graphics.adobeanimate.FlxAtlasSprite;
import funkin.play.HealthIcon;
import funkin.play.PlayState;
import funkin.shaderslmfao.AngleMask;
import funkin.shaderslmfao.PureColor;
import funkin.shaderslmfao.StrokeShader;
import funkin.play.PlayStatePlaylist;
import funkin.play.song.Song;
import funkin.save.Save;
import funkin.save.Save.SaveScoreData;
import funkin.shaderslmfao.AngleMask;
import funkin.shaderslmfao.HSVShader;
import funkin.shaderslmfao.PureColor;
import funkin.shaderslmfao.StrokeShader;
import funkin.ui.StickerSubState;
import lime.app.Future;
import lime.utils.Assets;
import funkin.graphics.adobeanimate.FlxAtlasSprite;
class FreeplayState extends MusicBeatSubState
{
var songs:Array<FreeplaySongData> = [];
// var selector:FlxText;
var curSelected:Int = 0;
var curDifficulty:Int = 1;
@ -107,8 +108,6 @@ class FreeplayState extends MusicBeatSubState
openSubState(stickerSubState);
stickerSubState.degenStickers();
// resetSubState();
}
#if discord_rpc
@ -120,31 +119,25 @@ class FreeplayState extends MusicBeatSubState
#if debug
isDebug = true;
// addSong('Test', 'tutorial', 'bf-pixel');
// addSong('Pyro', 'weekend1', 'darnell');
#end
// var initSonglist = CoolUtil.coolTextFile(Paths.txt('freeplaySonglist'));
// for (i in 0...initSonglist.length)
// {
// songs.push(new FreeplaySongData(initSonglist[i], 'tutorial', 'gf'));
// }
if (FlxG.sound.music != null)
{
if (!FlxG.sound.music.playing) FlxG.sound.playMusic(Paths.music('freakyMenu/freakyMenu'));
}
// Add a null entry that represents the RANDOM option
songs.push(null);
// programmatically adds the songs via LevelRegistry and SongRegistry
for (coolWeek in LevelRegistry.instance.listBaseGameLevelIds())
{
for (coolSong in LevelRegistry.instance.parseEntryData(coolWeek).songs)
for (songId in LevelRegistry.instance.parseEntryData(coolWeek).songs)
{
var metadata = SongRegistry.instance.parseEntryMetadata(coolSong);
var metadata = SongRegistry.instance.parseEntryMetadata(songId);
var char = metadata.playData.characters.opponent;
var songName = metadata.songName;
addSong(songName, coolWeek, char);
addSong(songId, songName, coolWeek, char);
}
}
@ -254,7 +247,6 @@ class FreeplayState extends MusicBeatSubState
speed: 0.3
});
// dj = new DJBoyfriend(0, -100);
dj = new DJBoyfriend(640, 366);
exitMovers.set([dj],
{
@ -427,7 +419,6 @@ class FreeplayState extends MusicBeatSubState
dj.onIntroDone.add(function() {
// when boyfriend hits dat shiii
//
albumArt.visible = true;
albumArt.anim.play("");
@ -485,40 +476,15 @@ class FreeplayState extends MusicBeatSubState
generateSongList(null, false);
// FlxG.sound.playMusic(Paths.music('title'), 0);
// FlxG.sound.music.fadeIn(2, 0, 0.8);
// selector = new FlxText();
// selector.size = 40;
// selector.text = ">";
// add(selector);
var swag:Alphabet = new Alphabet(1, 0, "swag");
// JUST DOIN THIS SHIT FOR TESTING!!!
/*
var md:String = Markdown.markdownToHtml(Assets.getText('CHANGELOG.md'));
var texFel:TextField = new TextField();
texFel.width = FlxG.width;
texFel.height = FlxG.height;
// texFel.
texFel.htmlText = md;
FlxG.stage.addChild(texFel);
trace(md);
*/
var funnyCam = new FlxCamera(0, 0, FlxG.width, FlxG.height);
funnyCam.bgColor = FlxColor.TRANSPARENT;
FlxG.cameras.add(funnyCam);
typing = new FlxInputText(100, 100);
// add(typing);
typing.callback = function(txt, action) {
// generateSongList(new EReg(txt.trim(), "ig"));
trace(action);
};
@ -534,9 +500,6 @@ class FreeplayState extends MusicBeatSubState
for (cap in grpCapsules.members)
cap.kill();
// grpCapsules.clear();
// var regexp:EReg = regexp;
var tempSongs:Array<FreeplaySongData> = songs;
if (filterStuff != null)
@ -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<String>, levelId:String, ?songCharacters:Array<String>)
{
if (songCharacters == null) songCharacters = ['bf'];
var num:Int = 0;
for (song in songs)
{
addSong(song, levelId, songCharacters[num]);
if (songCharacters.length != 1) num++;
}
songs.push(new FreeplaySongData(songId, songName, levelId, songCharacter));
}
var touchY:Float = 0;
@ -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;

View file

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

View file

@ -46,7 +46,11 @@ class InitState extends FlxState
{
setupShit();
loadSaveData();
// loadSaveData(); // Moved to Main.hx
// Load player options from save data.
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.
*

View file

@ -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<AtlasMenuItem>();
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.

View file

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

View file

@ -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();

View file

@ -1,5 +1,6 @@
package funkin;
import funkin.save.Save;
import funkin.Controls;
import flixel.FlxCamera;
import funkin.input.PreciseInputManager;
@ -11,121 +12,36 @@ import flixel.util.FlxSignal;
// import props.Player;
class PlayerSettings
{
static public var numPlayers(default, null) = 0;
static public var numAvatars(default, null) = 0;
static public var player1(default, null):PlayerSettings;
static public var player2(default, null):PlayerSettings;
public static var numPlayers(default, null) = 0;
public static var numAvatars(default, null) = 0;
public static var player1(default, null):PlayerSettings;
public static var player2(default, null):PlayerSettings;
static public var onAvatarAdd(default, null) = new FlxTypedSignal<PlayerSettings->Void>();
static public var onAvatarRemove(default, null) = new FlxTypedSignal<PlayerSettings->Void>();
public static var onAvatarAdd(default, null) = new FlxTypedSignal<PlayerSettings->Void>();
public static var onAvatarRemove(default, null) = new FlxTypedSignal<PlayerSettings->Void>();
public var id(default, null):Int;
public var controls(default, null):Controls;
// public var avatar:Player;
// public var camera(get, never):PlayCamera;
function new(id:Int)
/**
* Return the PlayerSettings for the given player number, or `null` if that player isn't active.
*/
public static function get(id:Int):Null<PlayerSettings>
{
trace('loading player settings for id: $id');
this.id = id;
this.controls = new Controls('player$id', None);
#if CLEAR_INPUT_SAVE
FlxG.save.data.controls = null;
FlxG.save.flush();
#end
var useDefault = true;
var controlData = FlxG.save.data.controls;
if (controlData != null)
return switch (id)
{
var keyData:Dynamic = null;
if (id == 0 && controlData.p1 != null && controlData.p1.keys != null) keyData = controlData.p1.keys;
else if (id == 1 && controlData.p2 != null && controlData.p2.keys != null) keyData = controlData.p2.keys;
if (keyData != null)
{
useDefault = false;
trace("loaded key data: " + haxe.Json.stringify(keyData));
controls.fromSaveData(keyData, Keys);
}
}
if (useDefault)
{
trace("falling back to default control scheme");
controls.setKeyboardScheme(Solo);
}
// Apply loaded settings.
PreciseInputManager.instance.initializeKeys(controls);
case 1: player1;
case 2: player2;
default: null;
};
}
function addGamepad(gamepad:FlxGamepad)
{
var useDefault = true;
var controlData = FlxG.save.data.controls;
if (controlData != null)
{
var padData:Dynamic = null;
if (id == 0 && controlData.p1 != null && controlData.p1.pad != null) padData = controlData.p1.pad;
else if (id == 1 && controlData.p2 != null && controlData.p2.pad != null) padData = controlData.p2.pad;
if (padData != null)
{
useDefault = false;
trace("loaded pad data: " + haxe.Json.stringify(padData));
controls.addGamepadWithSaveData(gamepad.id, padData);
}
}
if (useDefault) controls.addDefaultGamepad(gamepad.id);
}
public function saveControls()
{
if (FlxG.save.data.controls == null) FlxG.save.data.controls = {};
var playerData:{?keys:Dynamic, ?pad:Dynamic}
if (id == 0)
{
if (FlxG.save.data.controls.p1 == null) FlxG.save.data.controls.p1 = {};
playerData = FlxG.save.data.controls.p1;
}
else
{
if (FlxG.save.data.controls.p2 == null) FlxG.save.data.controls.p2 = {};
playerData = FlxG.save.data.controls.p2;
}
var keyData = controls.createSaveData(Keys);
if (keyData != null)
{
playerData.keys = keyData;
trace("saving key data: " + haxe.Json.stringify(keyData));
}
if (controls.gamepadsAdded.length > 0)
{
var padData = controls.createSaveData(Gamepad(controls.gamepadsAdded[0]));
if (padData != null)
{
trace("saving pad data: " + haxe.Json.stringify(padData));
playerData.pad = padData;
}
}
FlxG.save.flush();
}
static public function init():Void
public static function init():Void
{
if (player1 == null)
{
player1 = new PlayerSettings(0);
player1 = new PlayerSettings(1);
++numPlayers;
}
@ -137,26 +53,13 @@ class PlayerSettings
var gamepad = FlxG.gamepads.getByID(i);
if (gamepad != null) onGamepadAdded(gamepad);
}
}
// player1.controls.addDefaultGamepad(0);
// }
// if (numGamepads > 1)
// {
// if (player2 == null)
// {
// player2 = new PlayerSettings(1, None);
// ++numPlayers;
// }
// var gamepad = FlxG.gamepads.getByID(1);
// if (gamepad == null)
// throw 'Unexpected null gamepad. id:0';
// player2.controls.addDefaultGamepad(1);
// }
// DeviceManager.init();
public static function reset()
{
player1 = null;
player2 = null;
numPlayers = 0;
}
static function onGamepadAdded(gamepad:FlxGamepad)
@ -164,86 +67,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);
}
}
}
}

View file

@ -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
}
}
}

View file

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

View file

@ -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;

View file

@ -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<SongTimeChange>;
/**
* 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<SongTimeChange>;
/**
* 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;

View file

@ -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<SongNoteData>, offset:Int):Array<SongNoteData>
public static function offsetSongNoteData(notes:Array<SongNoteData>, offset:Float):Array<SongNoteData>
{
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<SongEventData>, offset:Int):Array<SongEventData>
public static function offsetSongEventData(events:Array<SongEventData>, offset:Float):Array<SongEventData>
{
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<SongClipboardItems>();
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<SongClipboardItems>();
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<SongNoteData>,
events:Array<SongEventData>
}

View file

@ -156,7 +156,7 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
return cleanMetadata(parser.value, variation);
}
public function parseEntryMetadataWithMigration(id:String, ?variation:String, version:thx.semver.Version):Null<SongMetadata>
public function parseEntryMetadataWithMigration(id:String, variation:String, version:thx.semver.Version):Null<SongMetadata>
{
variation = variation == null ? Constants.DEFAULT_VARIATION : variation;
@ -192,7 +192,7 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
}
}
function parseEntryMetadata_v2_0_0(id:String, variation:String = ""):Null<SongMetadata>
function parseEntryMetadata_v2_0_0(id:String, ?variation:String):Null<SongMetadata>
{
variation = variation == null ? Constants.DEFAULT_VARIATION : variation;

View file

@ -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.

View file

@ -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<FlxKey, PreciseInputList>
*/
var _keyListDir:Map<FlxKey, NoteDirection>;
/**
* A FlxGamepadID->Array<FlxGamepadInputID>, with FlxGamepadInputID being the counterpart to FlxKey.
*/
var _buttonList:Map<Int, Array<FlxGamepadInputID>>;
var _buttonListArray:Array<FlxInput<FlxGamepadInputID>>;
var _buttonListMap:Map<Int, Map<FlxGamepadInputID, FlxInput<FlxGamepadInputID>>>;
/**
* A FlxGamepadID->FlxGamepadInputID->NoteDirection, with FlxGamepadInputID being the counterpart to FlxKey.
*/
var _buttonListDir:Map<Int, Map<FlxGamepadInputID, NoteDirection>>;
/**
* The timestamp at which a given note direction was last pressed.
*/
@ -53,17 +74,32 @@ class PreciseInputManager extends FlxKeyManager<FlxKey, PreciseInputList>
*/
var _dirReleaseTimestamps:Map<NoteDirection, Int64>;
var _deviceBinds:Map<FlxGamepad,
{
onButtonDown:LimeGamepadButton->Int64->Void,
onButtonUp:LimeGamepadButton->Int64->Void
}>;
public function new()
{
super(PreciseInputList.new);
_deviceBinds = [];
_keyList = [];
_dirPressTimestamps = new Map<NoteDirection, Int64>();
_dirReleaseTimestamps = new Map<NoteDirection, Int64>();
// _keyListMap
// _keyListArray
_keyListDir = new Map<FlxKey, NoteDirection>();
FlxG.stage.removeEventListener(KeyboardEvent.KEY_DOWN, onKeyDown);
FlxG.stage.removeEventListener(KeyboardEvent.KEY_UP, onKeyUp);
_buttonList = [];
_buttonListMap = [];
_buttonListArray = [];
_buttonListDir = new Map<Int, Map<FlxGamepadInputID, NoteDirection>>();
_dirPressTimestamps = new Map<NoteDirection, Int64>();
_dirReleaseTimestamps = new Map<NoteDirection, Int64>();
// Keyboard
FlxG.stage.application.window.onKeyDownPrecise.add(handleKeyDown);
FlxG.stage.application.window.onKeyUpPrecise.add(handleKeyUp);
@ -84,6 +120,17 @@ class PreciseInputManager extends FlxKeyManager<FlxKey, PreciseInputList>
};
}
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<FlxKey, PreciseInputList>
}
}
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<FlxGamepadInputID>(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<FlxGamepadInputID, FlxInput<FlxGamepadInputID>>());
buttonListMapEntry.set(button, input);
var buttonListDirEntry = _buttonListDir.get(gamepad.id);
if (buttonListDirEntry == null) _buttonListDir.set(gamepad.id, buttonListDirEntry = new Map<FlxGamepadInputID, NoteDirection>());
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<FlxKey, PreciseInputList>
return _keyListMap.get(key);
}
public function getInputByButton(gamepad:FlxGamepad, button:FlxGamepadInputID):FlxInput<FlxGamepadInputID>
{
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<FlxGamepadInputID>
{
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<FlxKey, PreciseInputList>
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<FlxKey, PreciseInputList>
}
}
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<FlxKey, PreciseInputList>
_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

View file

@ -14,6 +14,7 @@ import funkin.data.level.LevelRegistry;
import funkin.data.notestyle.NoteStyleRegistry;
import funkin.play.cutscene.dialogue.ConversationDataParser;
import funkin.play.cutscene.dialogue.DialogueBoxDataParser;
import funkin.save.Save;
import funkin.play.cutscene.dialogue.SpeakerDataParser;
import funkin.data.song.SongRegistry;
@ -59,7 +60,7 @@ class PolymodHandler
createModRoot();
trace("Initializing Polymod (using configured mods)...");
loadModsById(getEnabledModIds());
loadModsById(Save.get().enabledModIds);
}
/**
@ -232,33 +233,9 @@ class PolymodHandler
return modIds;
}
public static function setEnabledMods(newModList:Array<String>):Void
{
FlxG.save.data.enabledMods = newModList;
// Make sure to COMMIT the changes.
FlxG.save.flush();
}
/**
* Returns the list of enabled mods.
* @return Array<String>
*/
public static function getEnabledModIds():Array<String>
{
if (FlxG.save.data.enabledMods == null)
{
// NOTE: If the value is null, the enabled mod list is unconfigured.
// Currently, we default to disabling newly installed mods.
// If we want to auto-enable new mods, but otherwise leave the configured list in place,
// we will need some custom logic.
FlxG.save.data.enabledMods = [];
}
return FlxG.save.data.enabledMods;
}
public static function getEnabledMods():Array<ModMetadata>
{
var modIds = getEnabledModIds();
var modIds = Save.get().enabledModIds;
var modMetadata = getAllMods();
var enabledMods = [];
for (item in modMetadata)

View file

@ -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<Int> = [];
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.

View file

@ -25,6 +25,7 @@ import flixel.ui.FlxBar;
import flixel.util.FlxColor;
import flixel.util.FlxTimer;
import funkin.audio.VoicesGroup;
import funkin.save.Save;
import funkin.Highscore.Tallies;
import funkin.input.PreciseInputManager;
import funkin.modding.events.ScriptEvent;
@ -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)
{

View file

@ -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;
}

View file

@ -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;

700
source/funkin/save/Save.hx Normal file
View file

@ -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<String, Dynamic>;
function get_modOptions():Map<String, Dynamic>
{
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<String>;
function get_ngSessionId():Null<String>
{
return this.api.newgrounds.sessionId;
}
function set_ngSessionId(value:Null<String>):Null<String>
{
return this.api.newgrounds.sessionId = value;
}
public var enabledModIds(get, set):Array<String>;
function get_enabledModIds():Array<String>
{
return this.mods.enabledMods;
}
function set_enabledModIds(value:Array<String>):Array<String>
{
return this.mods.enabledMods = value;
}
/**
* Return the score the user achieved for a given level on a given difficulty.
*
* @param levelId The ID of the level/week.
* @param difficultyId The difficulty to check.
* @return A data structure containing score, judgement counts, and accuracy. Returns `null` if no score is saved.
*/
public function getLevelScore(levelId:String, difficultyId:String = 'normal'):Null<SaveScoreData>
{
var level = this.scores.levels.get(levelId);
if (level == null)
{
level = [];
this.scores.levels.set(levelId, level);
}
return level.get(difficultyId);
}
/**
* Apply the score the user achieved for a given level on a given difficulty.
*/
public function setLevelScore(levelId:String, difficultyId:String, score:SaveScoreData):Void
{
var level = this.scores.levels.get(levelId);
if (level == null)
{
level = [];
this.scores.levels.set(levelId, level);
}
level.set(difficultyId, score);
flush();
}
public function isLevelHighScore(levelId:String, difficultyId:String = 'normal', score:SaveScoreData):Bool
{
var level = this.scores.levels.get(levelId);
if (level == null)
{
level = [];
this.scores.levels.set(levelId, level);
}
var currentScore = level.get(difficultyId);
if (currentScore == null)
{
return true;
}
return score.score > currentScore.score;
}
public function hasBeatenLevel(levelId:String, ?difficultyList:Array<String>):Bool
{
if (difficultyList == null)
{
difficultyList = ['easy', 'normal', 'hard'];
}
for (difficulty in difficultyList)
{
var score:Null<SaveScoreData> = getLevelScore(levelId, difficulty);
// TODO: Do we need to check accuracy/score here?
if (score != null)
{
return true;
}
}
return false;
}
/**
* Return the score the user achieved for a given song on a given difficulty.
*
* @param songId The ID of the song.
* @param difficultyId The difficulty to check.
* @return A data structure containing score, judgement counts, and accuracy. Returns `null` if no score is saved.
*/
public function getSongScore(songId:String, difficultyId:String = 'normal'):Null<SaveScoreData>
{
var song = this.scores.songs.get(songId);
if (song == null)
{
song = [];
this.scores.songs.set(songId, song);
}
return song.get(difficultyId);
}
/**
* Apply the score the user achieved for a given song on a given difficulty.
*/
public function setSongScore(songId:String, difficultyId:String, score:SaveScoreData):Void
{
var song = this.scores.songs.get(songId);
if (song == null)
{
song = [];
this.scores.songs.set(songId, song);
}
song.set(difficultyId, score);
flush();
}
/**
* Is the provided score data better than the current high score for the given song?
* @param songId The song ID to check.
* @param difficultyId The difficulty to check.
* @param score The score to check.
* @return Whether the score is better than the current high score.
*/
public function isSongHighScore(songId:String, difficultyId:String = 'normal', score:SaveScoreData):Bool
{
var song = this.scores.songs.get(songId);
if (song == null)
{
song = [];
this.scores.songs.set(songId, song);
}
var currentScore = song.get(difficultyId);
if (currentScore == null)
{
return true;
}
return score.score > currentScore.score;
}
/**
* Has the provided song been beaten on one of the listed difficulties?
* @param songId The song ID to check.
* @param difficultyList The difficulties to check. Defaults to `easy`, `normal`, and `hard`.
* @return Whether the song has been beaten on any of the listed difficulties.
*/
public function hasBeatenSong(songId:String, ?difficultyList:Array<String>):Bool
{
if (difficultyList == null)
{
difficultyList = ['easy', 'normal', 'hard'];
}
for (difficulty in difficultyList)
{
var score:Null<SaveScoreData> = getSongScore(songId, difficulty);
// TODO: Do we need to check accuracy/score here?
if (score != null)
{
return true;
}
}
return false;
}
public function getControls(playerId:Int, inputType:Device):SaveControlsData
{
switch (inputType)
{
case Keys:
return (playerId == 0) ? this.options.controls.p1.keyboard : this.options.controls.p2.keyboard;
case Gamepad(_):
return (playerId == 0) ? this.options.controls.p1.gamepad : this.options.controls.p2.gamepad;
}
}
public function hasControls(playerId:Int, inputType:Device):Bool
{
var controls = getControls(playerId, inputType);
var controlsFields = Reflect.fields(controls);
return controlsFields.length > 0;
}
public function setControls(playerId:Int, inputType:Device, controls:SaveControlsData):Void
{
switch (inputType)
{
case Keys:
if (playerId == 0)
{
this.options.controls.p1.keyboard = controls;
}
else
{
this.options.controls.p2.keyboard = controls;
}
case Gamepad(_):
if (playerId == 0)
{
this.options.controls.p1.gamepad = controls;
}
else
{
this.options.controls.p2.gamepad = controls;
}
}
flush();
}
public function isCharacterUnlocked(characterId:String):Bool
{
switch (characterId)
{
case 'bf':
return true;
case 'pico':
return hasBeatenLevel('weekend1');
default:
trace('Unknown character ID: ' + characterId);
return true;
}
}
/**
* Call this to make sure the save data is written to disk.
*/
public function flush():Void
{
FlxG.save.flush();
}
/**
* If you set slot to `2`, it will load an independe
* @param slot
*/
static function loadFromSlot(slot:Int):Void
{
trace("[SAVE] Loading save from slot " + slot + "...");
FlxG.save.bind('$SAVE_NAME${slot}', SAVE_PATH);
if (FlxG.save.isEmpty())
{
trace('[SAVE] Save data is empty, checking for legacy save data...');
var legacySaveData = fetchLegacySaveData();
if (legacySaveData != null)
{
trace('[SAVE] Found legacy save data, converting...');
FlxG.save.mergeData(SaveDataMigrator.migrateFromLegacy(legacySaveData));
}
}
else
{
trace('[SAVE] Loaded save data.');
FlxG.save.mergeData(SaveDataMigrator.migrate(FlxG.save.data));
}
trace('[SAVE] Done loading save data.');
trace(FlxG.save.data);
}
static function fetchLegacySaveData():Null<RawSaveData_v1_0_0>
{
trace("[SAVE] Checking for legacy save data...");
var legacySave:FlxSave = new FlxSave();
legacySave.bind(SAVE_NAME_LEGACY, SAVE_PATH_LEGACY);
if (legacySave?.data == null)
{
trace("[SAVE] No legacy save data found.");
return null;
}
else
{
trace("[SAVE] Legacy save data found.");
trace(legacySave.data);
return cast legacySave.data;
}
}
}
/**
* An anonymous structure containingg all the user's save data.
*/
typedef RawSaveData =
{
// Flixel save data.
var volume:Float;
var mute:Bool;
/**
* A semantic versioning string for the save data format.
*/
var version:Version;
var api:SaveApiData;
/**
* The user's saved scores.
*/
var scores:SaveHighScoresData;
/**
* The user's preferences.
*/
var options:SaveDataOptions;
var mods:SaveDataMods;
/**
* The user's preferences specific to the Chart Editor.
*/
var optionsChartEditor:SaveDataChartEditorOptions;
};
typedef SaveApiData =
{
var newgrounds:SaveApiNewgroundsData;
}
typedef SaveApiNewgroundsData =
{
var sessionId:Null<String>;
}
/**
* An anoymous structure containing options about the user's high scores.
*/
typedef SaveHighScoresData =
{
/**
* Scores for each level (or week).
*/
var levels:SaveScoreLevelsData;
/**
* Scores for individual songs.
*/
var songs:SaveScoreSongsData;
};
typedef SaveDataMods =
{
var enabledMods:Array<String>;
var modOptions:Map<String, Dynamic>;
}
/**
* Key is the level ID, value is the SaveScoreLevelData.
*/
typedef SaveScoreLevelsData = Map<String, SaveScoreDifficultiesData>;
/**
* Key is the song ID, value is the data for each difficulty.
*/
typedef SaveScoreSongsData = Map<String, SaveScoreDifficultiesData>;
/**
* Key is the difficulty ID, value is the score.
*/
typedef SaveScoreDifficultiesData = Map<String, SaveScoreData>;
/**
* An individual score. Contains the score, accuracy, and count of each judgement hit.
*/
typedef SaveScoreData =
{
/**
* The score achieved.
*/
var score:Int;
/**
* The count of each judgement hit.
*/
var tallies:SaveScoreTallyData;
/**
* The accuracy percentage.
*/
var accuracy:Float;
}
typedef SaveScoreTallyData =
{
var killer:Int;
var sick:Int;
var good:Int;
var bad:Int;
var shit:Int;
var missed:Int;
var combo:Int;
var maxCombo:Int;
var totalNotesHit:Int;
var totalNotes:Int;
}
/**
* An anonymous structure containing all the user's options and preferences for the main game.
* Every time you add a new option, it needs to be added here.
*/
typedef SaveDataOptions =
{
/**
* Whether some particularly fowl language is displayed.
* @default `true`
*/
var naughtyness:Bool;
/**
* If enabled, the strumline is at the bottom of the screen rather than the top.
* @default `false`
*/
var downscroll:Bool;
/**
* If disabled, 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<Int>;
/**
* Keybind for navigating in the menu.
* @default `Left Arrow`
*/
var ?UI_LEFT:Array<Int>;
/**
* Keybind for navigating in the menu.
* @default `Right Arrow`
*/
var ?UI_RIGHT:Array<Int>;
/**
* Keybind for navigating in the menu.
* @default `Down Arrow`
*/
var ?UI_DOWN:Array<Int>;
/**
* Keybind for hitting notes.
* @default `A` and `Left Arrow`
*/
var ?NOTE_LEFT:Array<Int>;
/**
* Keybind for hitting notes.
* @default `W` and `Up Arrow`
*/
var ?NOTE_UP:Array<Int>;
/**
* Keybind for hitting notes.
* @default `S` and `Down Arrow`
*/
var ?NOTE_DOWN:Array<Int>;
/**
* Keybind for hitting notes.
* @default `D` and `Right Arrow`
*/
var ?NOTE_RIGHT:Array<Int>;
/**
* Keybind for continue/OK in menus.
* @default `Enter` and `Space`
*/
var ?ACCEPT:Array<Int>;
/**
* Keybind for back/cancel in menus.
* @default `Escape`
*/
var ?BACK:Array<Int>;
/**
* Keybind for pausing the game.
* @default `Escape`
*/
var ?PAUSE:Array<Int>;
/**
* Keybind for advancing cutscenes.
* @default `Z` and `Space` and `Enter`
*/
var ?CUTSCENE_ADVANCE:Array<Int>;
/**
* Keybind for skipping a cutscene.
* @default `Escape`
*/
var ?CUTSCENE_SKIP:Array<Int>;
/**
* Keybind for increasing volume.
* @default `Plus`
*/
var ?VOLUME_UP:Array<Int>;
/**
* Keybind for decreasing volume.
* @default `Minus`
*/
var ?VOLUME_DOWN:Array<Int>;
/**
* Keybind for muting/unmuting volume.
* @default `Zero`
*/
var ?VOLUME_MUTE:Array<Int>;
/**
* Keybind for restarting a song.
* @default `R`
*/
var ?RESET:Array<Int>;
}
/**
* An anonymous structure containing all the user's options and preferences, specific to the Chart Editor.
*/
typedef SaveDataChartEditorOptions = {};

View file

@ -0,0 +1,52 @@
package funkin.save.migrator;
import thx.semver.Version;
typedef RawSaveData_v1_0_0 =
{
var seenVideo:Bool;
var mute:Bool;
var volume:Float;
var sessionId:String;
var songCompletion:Map<String, Float>;
var songScores:Map<String, Int>;
var ?controls:
{
?p1:SavePlayerControlsData_v1_0_0,
?p2:SavePlayerControlsData_v1_0_0
};
var enabledMods:Array<String>;
var weeksUnlocked:Array<Bool>;
var windowSettings:Array<Bool>;
}
typedef SavePlayerControlsData_v1_0_0 =
{
var keys:SaveControlsData_v1_0_0;
var pad:SaveControlsData_v1_0_0;
};
typedef SaveControlsData_v1_0_0 =
{
var ?ACCEPT:Array<Int>;
var ?BACK:Array<Int>;
var ?CUTSCENE_ADVANCE:Array<Int>;
var ?CUTSCENE_SKIP:Array<Int>;
var ?NOTE_DOWN:Array<Int>;
var ?NOTE_LEFT:Array<Int>;
var ?NOTE_RIGHT:Array<Int>;
var ?NOTE_UP:Array<Int>;
var ?PAUSE:Array<Int>;
var ?RESET:Array<Int>;
var ?UI_DOWN:Array<Int>;
var ?UI_LEFT:Array<Int>;
var ?UI_RIGHT:Array<Int>;
var ?UI_UP:Array<Int>;
var ?VOLUME_DOWN:Array<Int>;
var ?VOLUME_MUTE:Array<Int>;
var ?VOLUME_UP:Array<Int>;
};

View file

@ -0,0 +1,322 @@
package funkin.save.migrator;
import funkin.save.Save;
import funkin.save.migrator.RawSaveData_v1_0_0;
import thx.semver.Version;
import funkin.util.VersionUtil;
@:nullSafety
class SaveDataMigrator
{
/**
* Migrate from one 2.x version to another.
*/
public static function migrate(inputData:Dynamic):Save
{
// This deserializes directly into a `Version` object, not a `String`.
var version:Null<Version> = inputData?.version ?? null;
if (version == null)
{
trace('[SAVE] No version found in save data! Returning blank data.');
trace(inputData);
return new Save();
}
else
{
if (VersionUtil.validateVersionStr(version, Save.SAVE_DATA_VERSION_RULE))
{
// Simply cast the structured data.
var save:Save = inputData;
return save;
}
else
{
trace('[SAVE] Invalid save data version! Returning blank data.');
trace(inputData);
return new Save();
}
}
}
/**
* Migrate from 1.x to the latest version.
*/
public static function migrateFromLegacy(inputData:Dynamic):Save
{
var inputSaveData:RawSaveData_v1_0_0 = cast inputData;
var result:Save = new Save();
result.volume = inputSaveData.volume;
result.mute = inputSaveData.mute;
result.ngSessionId = inputSaveData.sessionId;
// TODO: Port over the save data from the legacy save data format.
migrateLegacyScores(result, inputSaveData);
migrateLegacyControls(result, inputSaveData);
return result;
}
static function migrateLegacyScores(result:Save, inputSaveData:RawSaveData_v1_0_0):Void
{
if (inputSaveData.songCompletion == null)
{
inputSaveData.songCompletion = [];
}
if (inputSaveData.songScores == null)
{
inputSaveData.songScores = [];
}
migrateLegacyLevelScore(result, inputSaveData, 'week0');
migrateLegacyLevelScore(result, inputSaveData, 'week1');
migrateLegacyLevelScore(result, inputSaveData, 'week2');
migrateLegacyLevelScore(result, inputSaveData, 'week3');
migrateLegacyLevelScore(result, inputSaveData, 'week4');
migrateLegacyLevelScore(result, inputSaveData, 'week5');
migrateLegacyLevelScore(result, inputSaveData, 'week6');
migrateLegacyLevelScore(result, inputSaveData, 'week7');
migrateLegacySongScore(result, inputSaveData, ['tutorial', 'Tutorial']);
migrateLegacySongScore(result, inputSaveData, ['bopeebo', 'Bopeebo']);
migrateLegacySongScore(result, inputSaveData, ['fresh', 'Fresh']);
migrateLegacySongScore(result, inputSaveData, ['dadbattle', 'Dadbattle']);
migrateLegacySongScore(result, inputSaveData, ['monster', 'Monster']);
migrateLegacySongScore(result, inputSaveData, ['south', 'South']);
migrateLegacySongScore(result, inputSaveData, ['spookeez', 'Spookeez']);
migrateLegacySongScore(result, inputSaveData, ['pico', 'Pico']);
migrateLegacySongScore(result, inputSaveData, ['philly-nice', 'Philly', 'philly', 'Philly-Nice']);
migrateLegacySongScore(result, inputSaveData, ['blammed', 'Blammed']);
migrateLegacySongScore(result, inputSaveData, ['satin-panties', 'Satin-Panties']);
migrateLegacySongScore(result, inputSaveData, ['high', 'High']);
migrateLegacySongScore(result, inputSaveData, ['milf', 'Milf', 'MILF']);
migrateLegacySongScore(result, inputSaveData, ['cocoa', 'Cocoa']);
migrateLegacySongScore(result, inputSaveData, ['eggnog', 'Eggnog']);
migrateLegacySongScore(result, inputSaveData, ['winter-horrorland', 'Winter-Horrorland']);
migrateLegacySongScore(result, inputSaveData, ['senpai', 'Senpai']);
migrateLegacySongScore(result, inputSaveData, ['roses', 'Roses']);
migrateLegacySongScore(result, inputSaveData, ['thorns', 'Thorns']);
migrateLegacySongScore(result, inputSaveData, ['ugh', 'Ugh']);
migrateLegacySongScore(result, inputSaveData, ['guns', 'Guns']);
migrateLegacySongScore(result, inputSaveData, ['stress', 'Stress']);
}
static function migrateLegacyLevelScore(result:Save, inputSaveData:RawSaveData_v1_0_0, levelId:String):Void
{
var scoreDataEasy:SaveScoreData =
{
score: inputSaveData.songScores.get('${levelId}-easy') ?? 0,
accuracy: inputSaveData.songCompletion.get('${levelId}-easy') ?? 0.0,
tallies:
{
killer: 0,
sick: 0,
good: 0,
bad: 0,
shit: 0,
missed: 0,
combo: 0,
maxCombo: 0,
totalNotesHit: 0,
totalNotes: 0,
}
};
result.setLevelScore(levelId, 'easy', scoreDataEasy);
var scoreDataNormal:SaveScoreData =
{
score: inputSaveData.songScores.get('${levelId}') ?? 0,
accuracy: inputSaveData.songCompletion.get('${levelId}') ?? 0.0,
tallies:
{
killer: 0,
sick: 0,
good: 0,
bad: 0,
shit: 0,
missed: 0,
combo: 0,
maxCombo: 0,
totalNotesHit: 0,
totalNotes: 0,
}
};
result.setLevelScore(levelId, 'normal', scoreDataNormal);
var scoreDataHard:SaveScoreData =
{
score: inputSaveData.songScores.get('${levelId}-hard') ?? 0,
accuracy: inputSaveData.songCompletion.get('${levelId}-hard') ?? 0.0,
tallies:
{
killer: 0,
sick: 0,
good: 0,
bad: 0,
shit: 0,
missed: 0,
combo: 0,
maxCombo: 0,
totalNotesHit: 0,
totalNotes: 0,
}
};
result.setLevelScore(levelId, 'hard', scoreDataHard);
}
static function migrateLegacySongScore(result:Save, inputSaveData:RawSaveData_v1_0_0, songIds:Array<String>):Void
{
var scoreDataEasy:SaveScoreData =
{
score: 0,
accuracy: 0,
tallies:
{
killer: 0,
sick: 0,
good: 0,
bad: 0,
shit: 0,
missed: 0,
combo: 0,
maxCombo: 0,
totalNotesHit: 0,
totalNotes: 0,
}
};
for (songId in songIds)
{
scoreDataEasy.score = Std.int(Math.max(scoreDataEasy.score, inputSaveData.songScores.get('${songId}-easy') ?? 0));
scoreDataEasy.accuracy = Math.max(scoreDataEasy.accuracy, inputSaveData.songCompletion.get('${songId}-easy') ?? 0.0);
}
result.setSongScore(songIds[0], 'easy', scoreDataEasy);
var scoreDataNormal:SaveScoreData =
{
score: 0,
accuracy: 0,
tallies:
{
killer: 0,
sick: 0,
good: 0,
bad: 0,
shit: 0,
missed: 0,
combo: 0,
maxCombo: 0,
totalNotesHit: 0,
totalNotes: 0,
}
};
for (songId in songIds)
{
scoreDataNormal.score = Std.int(Math.max(scoreDataNormal.score, inputSaveData.songScores.get('${songId}') ?? 0));
scoreDataNormal.accuracy = Math.max(scoreDataNormal.accuracy, inputSaveData.songCompletion.get('${songId}') ?? 0.0);
}
result.setSongScore(songIds[0], 'normal', scoreDataNormal);
var scoreDataHard:SaveScoreData =
{
score: 0,
accuracy: 0,
tallies:
{
killer: 0,
sick: 0,
good: 0,
bad: 0,
shit: 0,
missed: 0,
combo: 0,
maxCombo: 0,
totalNotesHit: 0,
totalNotes: 0,
}
};
for (songId in songIds)
{
scoreDataHard.score = Std.int(Math.max(scoreDataHard.score, inputSaveData.songScores.get('${songId}-hard') ?? 0));
scoreDataHard.accuracy = Math.max(scoreDataHard.accuracy, inputSaveData.songCompletion.get('${songId}-hard') ?? 0.0);
}
result.setSongScore(songIds[0], 'hard', scoreDataHard);
}
static function migrateLegacyControls(result:Save, inputSaveData:RawSaveData_v1_0_0):Void
{
var p1Data = inputSaveData?.controls?.p1;
if (p1Data != null)
{
migrateLegacyPlayerControls(result, 1, p1Data);
}
var p2Data = inputSaveData?.controls?.p2;
if (p2Data != null)
{
migrateLegacyPlayerControls(result, 2, p2Data);
}
}
static function migrateLegacyPlayerControls(result:Save, playerId:Int, controlsData:SavePlayerControlsData_v1_0_0):Void
{
var outputKeyControls:SaveControlsData =
{
ACCEPT: controlsData?.keys?.ACCEPT ?? null,
BACK: controlsData?.keys?.BACK ?? null,
CUTSCENE_ADVANCE: controlsData?.keys?.CUTSCENE_ADVANCE ?? null,
CUTSCENE_SKIP: controlsData?.keys?.CUTSCENE_SKIP ?? null,
NOTE_DOWN: controlsData?.keys?.NOTE_DOWN ?? null,
NOTE_LEFT: controlsData?.keys?.NOTE_LEFT ?? null,
NOTE_RIGHT: controlsData?.keys?.NOTE_RIGHT ?? null,
NOTE_UP: controlsData?.keys?.NOTE_UP ?? null,
PAUSE: controlsData?.keys?.PAUSE ?? null,
RESET: controlsData?.keys?.RESET ?? null,
UI_DOWN: controlsData?.keys?.UI_DOWN ?? null,
UI_LEFT: controlsData?.keys?.UI_LEFT ?? null,
UI_RIGHT: controlsData?.keys?.UI_RIGHT ?? null,
UI_UP: controlsData?.keys?.UI_UP ?? null,
VOLUME_DOWN: controlsData?.keys?.VOLUME_DOWN ?? null,
VOLUME_MUTE: controlsData?.keys?.VOLUME_MUTE ?? null,
VOLUME_UP: controlsData?.keys?.VOLUME_UP ?? null,
};
var outputPadControls:SaveControlsData =
{
ACCEPT: controlsData?.pad?.ACCEPT ?? null,
BACK: controlsData?.pad?.BACK ?? null,
CUTSCENE_ADVANCE: controlsData?.pad?.CUTSCENE_ADVANCE ?? null,
CUTSCENE_SKIP: controlsData?.pad?.CUTSCENE_SKIP ?? null,
NOTE_DOWN: controlsData?.pad?.NOTE_DOWN ?? null,
NOTE_LEFT: controlsData?.pad?.NOTE_LEFT ?? null,
NOTE_RIGHT: controlsData?.pad?.NOTE_RIGHT ?? null,
NOTE_UP: controlsData?.pad?.NOTE_UP ?? null,
PAUSE: controlsData?.pad?.PAUSE ?? null,
RESET: controlsData?.pad?.RESET ?? null,
UI_DOWN: controlsData?.pad?.UI_DOWN ?? null,
UI_LEFT: controlsData?.pad?.UI_LEFT ?? null,
UI_RIGHT: controlsData?.pad?.UI_RIGHT ?? null,
UI_UP: controlsData?.pad?.UI_UP ?? null,
VOLUME_DOWN: controlsData?.pad?.VOLUME_DOWN ?? null,
VOLUME_MUTE: controlsData?.pad?.VOLUME_MUTE ?? null,
VOLUME_UP: controlsData?.pad?.VOLUME_UP ?? null,
};
result.setControls(playerId, Keys, outputKeyControls);
result.setControls(playerId, Gamepad(0), outputPadControls);
}
}

View file

@ -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<Int> = null;
var buttonUsedToEnterPrompt:Null<Int> = 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;
}
}
}

View file

@ -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<String, Dynamic> = new Map();
var items:TextMenuList;
var preferenceItems:FlxTypedSpriteGroup<FlxSprite>;
var checkboxes:Array<CheckboxThingie> = [];
var menuCamera:FlxCamera;
var camFollow:FlxObject;
@ -27,13 +26,9 @@ class PreferencesMenu extends Page
camera = menuCamera;
add(items = new TextMenuList());
add(preferenceItems = new FlxTypedSpriteGroup<FlxSprite>());
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;
}
}

View file

@ -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;

View file

@ -10,7 +10,7 @@ class TextMenuList extends MenuTypedList<TextMenuItem>
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<TextMenuItem>
class TextMenuItem extends TextTypedMenuItem<AtlasText>
{
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<AtlasText>
class TextTypedMenuItem<T:AtlasText> extends MenuTypedItem<T>
{
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);
}

View file

@ -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<Bytes> = 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<Bytes> = 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<Bytes> = state.audioInstTrackData.get(instId);
var instTrack:Null<FlxSound> = 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<Bytes> = state.audioVocalTrackData.get(trackId);
var vocalTrack:Null<FlxSound> = 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<FlxSoundAsset> = 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<Bytes>):Null<FlxSound>
{
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<haxe.zip.Entry>
{
var zipEntries = [];
for (key in state.audioInstTrackData.keys())
{
if (key == 'default')
{
var data:Null<Bytes> = state.audioInstTrackData.get('default');
if (data == null) continue;
zipEntries.push(FileUtil.makeZIPEntryFromBytes('Inst.ogg', data));
}
else
{
var data:Null<Bytes> = state.audioInstTrackData.get(key);
if (data == null) continue;
zipEntries.push(FileUtil.makeZIPEntryFromBytes('Inst-${key}.ogg', data));
}
}
return zipEntries;
}
static function makeZIPEntriesFromVocals(state:ChartEditorState):Array<haxe.zip.Entry>
{
var zipEntries = [];
for (key in state.audioVocalTrackData.keys())
{
var data:Null<Bytes> = state.audioVocalTrackData.get(key);
if (data == null) continue;
zipEntries.push(FileUtil.makeZIPEntryFromBytes('Vocals-${key}.ogg', data));
}
return zipEntries;
}
}

View file

@ -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

View file

@ -83,7 +83,7 @@ class ChartEditorDialogHandler
var dialog:Null<Dialog> = 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<Link> = 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<Link> = 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<Link> = 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<Dialog> = 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<Button> = dialog.findComponent('dialogCancel', Button);
if (buttonCancel == null) throw 'Could not locate dialogCancel button in Song Metadata dialog';
buttonCancel.onClick = function(_event) {
@ -574,7 +674,11 @@ class ChartEditorDialogHandler
var dialogContinue:Null<Button> = dialog.findComponent('dialogContinue', Button);
if (dialogContinue == null) throw 'Could not locate dialogContinue button in Song Metadata dialog';
dialogContinue.onClick = (_event) -> dialog.hideDialog(DialogButton.APPLY);
dialogContinue.onClick = (_event) -> {
state.songMetadata.set(targetVariation, newSongMetadata);
dialog.hideDialog(DialogButton.APPLY);
}
return dialog;
}
@ -587,6 +691,7 @@ class ChartEditorDialogHandler
*/
public static function openUploadVocalsDialog(state:ChartEditorState, closable:Bool = true):Dialog
{
var instId:String = state.currentInstrumentalId;
var charIdsForVocals:Array<String> = [];
var charData:SongCharacterData = state.currentSongMetadata.playData.characters;
@ -633,14 +738,14 @@ class ChartEditorDialogHandler
trace('Selected file: $pathStr');
var path:Path = new Path(pathStr);
if (ChartEditorAudioHandler.loadVocalsFromPath(state, path, charKey))
if (ChartEditorAudioHandler.loadVocalsFromPath(state, path, charKey, instId))
{
// Tell the user the load was successful.
#if !mac
NotificationManager.instance.addNotification(
{
title: 'Success',
body: 'Loaded vocal track for $charName (${path.file}.${path.ext})',
body: 'Loaded vocals for $charName (${path.file}.${path.ext}), variation ${state.selectedVariation}',
type: NotificationType.Success,
expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME
});
@ -656,21 +761,14 @@ class ChartEditorDialogHandler
}
else
{
var message:String = if (!ChartEditorState.SUPPORTED_MUSIC_FORMATS.contains(path.ext ?? ''))
{
'File format (${path.ext}) not supported for vocal track (${path.file}.${path.ext})';
}
else
{
'Failed to load vocal track (${path.file}.${path.ext})';
}
trace('Failed to load vocal track (${path.file}.${path.ext})');
// Vocals failed to load.
#if !mac
NotificationManager.instance.addNotification(
{
title: 'Failure',
body: message,
body: 'Failed to load vocal track (${path.file}.${path.ext}) for variation (${state.selectedVariation})',
type: NotificationType.Error,
expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME
});
@ -690,14 +788,46 @@ class ChartEditorDialogHandler
if (selectedFile != null && selectedFile.bytes != null)
{
trace('Selected file: ' + selectedFile.name);
#if FILE_DROP_SUPPORTED
vocalsEntryLabel.text = 'Vocals for $charName (drag and drop, or click to browse)\nSelected file: ${selectedFile.name}';
#else
vocalsEntryLabel.text = 'Vocals for $charName (click to browse)\n${selectedFile.name}';
#end
ChartEditorAudioHandler.loadVocalsFromBytes(state, selectedFile.bytes, charKey);
dialogNoVocals.hidden = true;
removeDropHandler(onDropFile);
if (ChartEditorAudioHandler.loadVocalsFromBytes(state, selectedFile.bytes, charKey, instId))
{
// Tell the user the load was successful.
#if !mac
NotificationManager.instance.addNotification(
{
title: 'Success',
body: 'Loaded vocals for $charName (${selectedFile.name}), variation ${state.selectedVariation}',
type: NotificationType.Success,
expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME
});
#end
#if FILE_DROP_SUPPORTED
vocalsEntryLabel.text = 'Vocals for $charName (drag and drop, or click to browse)\nSelected file: ${selectedFile.name}';
#else
vocalsEntryLabel.text = 'Vocals for $charName (click to browse)\n${selectedFile.name}';
#end
dialogNoVocals.hidden = true;
}
else
{
trace('Failed to load vocal track (${selectedFile.fullPath})');
#if !mac
NotificationManager.instance.addNotification(
{
title: 'Failure',
body: 'Failed to load vocal track (${selectedFile.name}) for variation (${state.selectedVariation})',
type: NotificationType.Error,
expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME
});
#end
#if FILE_DROP_SUPPORTED
vocalsEntryLabel.text = 'Drag and drop vocals for $charName here, or click to browse.';
#else
vocalsEntryLabel.text = 'Click to browse for vocals for $charName.';
#end
}
}
});
}

View file

@ -36,7 +36,7 @@ class ChartEditorImportExportHandler
for (metadata in rawSongMetadata)
{
if (metadata == null) continue;
var variation = (metadata.variation == null || metadata.variation == '') ? 'default' : metadata.variation;
var variation = (metadata.variation == null || metadata.variation == '') ? Constants.DEFAULT_VARIATION : metadata.variation;
// Clone to prevent modifying the original.
var metadataClone:SongMetadata = metadata.clone(variation);
@ -52,23 +52,44 @@ class ChartEditorImportExportHandler
state.clearVocals();
ChartEditorAudioHandler.loadInstrumentalFromAsset(state, Paths.inst(songId));
var diff:Null<SongDifficulty> = song.getDifficulty(state.selectedDifficulty);
var voiceList:Array<String> = diff != null ? diff.buildVoiceList() : [];
if (voiceList.length == 2)
var variations:Array<String> = state.availableVariations;
for (variation in variations)
{
ChartEditorAudioHandler.loadVocalsFromAsset(state, voiceList[0], BF);
ChartEditorAudioHandler.loadVocalsFromAsset(state, voiceList[1], DAD);
}
else
{
for (voicePath in voiceList)
if (variation == Constants.DEFAULT_VARIATION)
{
ChartEditorAudioHandler.loadVocalsFromAsset(state, voicePath);
ChartEditorAudioHandler.loadInstFromAsset(state, Paths.inst(songId));
}
else
{
ChartEditorAudioHandler.loadInstFromAsset(state, Paths.inst(songId, '-$variation'), variation);
}
}
for (difficultyId in song.listDifficulties())
{
var diff:Null<SongDifficulty> = song.getDifficulty(difficultyId);
if (diff == null) continue;
var instId:String = diff.variation == Constants.DEFAULT_VARIATION ? '' : diff.variation;
var voiceList:Array<String> = diff.buildVoiceList(); // SongDifficulty accounts for variation already.
if (voiceList.length == 2)
{
ChartEditorAudioHandler.loadVocalsFromAsset(state, voiceList[0], diff.characters.player, instId);
ChartEditorAudioHandler.loadVocalsFromAsset(state, voiceList[1], diff.characters.opponent, instId);
}
else if (voiceList.length == 1)
{
ChartEditorAudioHandler.loadVocalsFromAsset(state, voiceList[0], diff.characters.player, instId);
}
else
{
trace('[WARN] Strange quantity of voice paths for difficulty ${difficultyId}: ${voiceList.length}');
}
}
state.switchToCurrentInstrumental();
state.refreshMetadataToolbox();
#if !mac
@ -148,13 +169,8 @@ class ChartEditorImportExportHandler
}
}
if (state.audioInstTrackData != null) zipEntries.push(FileUtil.makeZIPEntryFromBytes('Inst.ogg', state.audioInstTrackData));
for (charId in state.audioVocalTrackData.keys())
{
var entryData = state.audioVocalTrackData.get(charId);
if (entryData == null) continue;
zipEntries.push(FileUtil.makeZIPEntryFromBytes('Vocals-$charId.ogg', entryData));
}
if (state.audioInstTrackData != null) zipEntries.concat(ChartEditorAudioHandler.makeZIPEntriesFromInstrumentals(state));
if (state.audioVocalTrackData != null) zipEntries.concat(ChartEditorAudioHandler.makeZIPEntriesFromVocals(state));
trace('Exporting ${zipEntries.length} files to ZIP...');

View file

@ -461,6 +461,8 @@ class ChartEditorState extends HaxeUIState
notePreviewDirty = true;
notePreviewViewportBoundsDirty = true;
this.scrollPositionInPixels = this.scrollPositionInPixels;
// Characters have probably changed too.
healthIconsDirty = true;
return isViewDownscroll;
}
@ -519,8 +521,14 @@ class ChartEditorState extends HaxeUIState
*/
var selectedVariation(default, set):String = Constants.DEFAULT_VARIATION;
/**
* Setter called when we are switching variations.
* We will likely need to switch instrumentals as well.
*/
function set_selectedVariation(value:String):String
{
// Don't update if we're already on the variation.
if (selectedVariation == value) return selectedVariation;
selectedVariation = value;
// Make sure view is updated when the variation changes.
@ -528,6 +536,8 @@ class ChartEditorState extends HaxeUIState
notePreviewDirty = true;
notePreviewViewportBoundsDirty = true;
switchToCurrentInstrumental();
return selectedVariation;
}
@ -548,6 +558,23 @@ class ChartEditorState extends HaxeUIState
return selectedDifficulty;
}
/**
* The instrumental ID which is currently selected.
*/
var currentInstrumentalId(get, set):String;
function get_currentInstrumentalId():String
{
var instId:Null<String> = currentSongMetadata.playData.characters.instrumental;
if (instId == null || instId == '') instId = (selectedVariation == Constants.DEFAULT_VARIATION) ? '' : selectedVariation;
return instId;
}
function set_currentInstrumentalId(value:String):String
{
return currentSongMetadata.playData.characters.instrumental = value;
}
/**
* The character ID for the character which is currently selected.
*/
@ -592,6 +619,11 @@ class ChartEditorState extends HaxeUIState
*/
var noteDisplayDirty:Bool = true;
/**
* Whether the selected charactesr have been modified and the health icons need to be updated.
*/
var healthIconsDirty:Bool = true;
/**
* Whether the note preview graphic needs to be FULLY rebuilt.
*/
@ -773,28 +805,29 @@ class ChartEditorState extends HaxeUIState
/**
* The audio track for the instrumental.
* Replaced when switching instrumentals.
* `null` until an instrumental track is loaded.
*/
var audioInstTrack:Null<FlxSound> = null;
/**
* The raw byte data for the instrumental audio track.
* The raw byte data for the instrumental audio tracks.
* Key is the instrumental name.
* `null` until an instrumental track is loaded.
*/
var audioInstTrackData:Null<Bytes> = null;
var audioInstTrackData:Map<String, Bytes> = [];
/**
* The audio track for the vocals.
* `null` until vocal track(s) are loaded.
* When switching characters, the elements of the VoicesGroup will be swapped to match the new character.
*/
var audioVocalTrackGroup:Null<VoicesGroup> = null;
/**
* A map of the audio tracks for each character's vocals.
* - Keys are the character IDs.
* - Values are the FlxSound objects to play that character's vocals.
*
* When switching characters, the elements of the VoicesGroup will be swapped to match the new character.
* - Keys are `characterId-variation` (with `characterId` being the default variation).
* - Values are the byte data for the audio track.
*/
var audioVocalTrackData:Map<String, Bytes> = [];
@ -1045,30 +1078,6 @@ class ChartEditorState extends HaxeUIState
return currentSongMetadata.artist = value;
}
var currentSongCharacterPlayer(get, set):String;
function get_currentSongCharacterPlayer():String
{
return currentSongMetadata.playData.characters.player;
}
function set_currentSongCharacterPlayer(value:String):String
{
return currentSongMetadata.playData.characters.player = value;
}
var currentSongCharacterOpponent(get, set):String;
function get_currentSongCharacterOpponent():String
{
return currentSongMetadata.playData.characters.opponent;
}
function set_currentSongCharacterOpponent(value:String):String
{
return currentSongMetadata.playData.characters.opponent = value;
}
/**
* SIGNALS
*/
@ -1379,7 +1388,7 @@ class ChartEditorState extends HaxeUIState
gridPlayhead.add(playheadBlock);
// Character icons.
healthIconDad = new HealthIcon(currentSongCharacterOpponent);
healthIconDad = new HealthIcon(currentSongMetadata.playData.characters.opponent);
healthIconDad.autoUpdate = false;
healthIconDad.size.set(0.5, 0.5);
healthIconDad.x = gridTiledSprite.x - 15 - (HealthIcon.HEALTH_ICON_SIZE * 0.5);
@ -1387,7 +1396,7 @@ class ChartEditorState extends HaxeUIState
add(healthIconDad);
healthIconDad.zIndex = 30;
healthIconBF = new HealthIcon(currentSongCharacterPlayer);
healthIconBF = new HealthIcon(currentSongMetadata.playData.characters.player);
healthIconBF.autoUpdate = false;
healthIconBF.size.set(0.5, 0.5);
healthIconBF.x = gridTiledSprite.x + gridTiledSprite.width + 15;
@ -1484,6 +1493,12 @@ class ChartEditorState extends HaxeUIState
return bounds;
}
public function switchToCurrentInstrumental():Void
{
ChartEditorAudioHandler.switchToInstrumental(this, currentInstrumentalId, currentSongMetadata.playData.characters.player,
currentSongMetadata.playData.characters.opponent);
}
function setNotePreviewViewportBounds(bounds:FlxRect = null):Void
{
if (notePreviewViewport == null)
@ -1532,11 +1547,11 @@ class ChartEditorState extends HaxeUIState
renderedEvents.setPosition(gridTiledSprite.x, gridTiledSprite.y);
add(renderedEvents);
renderedNotes.zIndex = 25;
renderedEvents.zIndex = 25;
renderedSelectionSquares.setPosition(gridTiledSprite.x, gridTiledSprite.y);
add(renderedSelectionSquares);
renderedNotes.zIndex = 26;
renderedSelectionSquares.zIndex = 26;
}
function buildAdditionalUI():Void
@ -1647,7 +1662,18 @@ class ChartEditorState extends HaxeUIState
addUIClickListener('menubarItemCut', _ -> performCommand(new CutItemsCommand(currentNoteSelection, currentEventSelection)));
addUIClickListener('menubarItemPaste', _ -> performCommand(new PasteItemsCommand(scrollPositionInMs + playheadPositionInMs)));
addUIClickListener('menubarItemPaste', _ -> {
var targetMs:Float = scrollPositionInMs + playheadPositionInMs;
var targetStep:Float = Conductor.getTimeInSteps(targetMs);
var targetSnappedStep:Float = Math.floor(targetStep / noteSnapRatio) * noteSnapRatio;
var targetSnappedMs:Float = Conductor.getStepTimeInMs(targetSnappedStep);
performCommand(new PasteItemsCommand(targetSnappedMs));
});
addUIClickListener('menubarItemPasteUnsnapped', _ -> {
var targetMs:Float = scrollPositionInMs + playheadPositionInMs;
performCommand(new PasteItemsCommand(targetMs));
});
addUIClickListener('menubarItemDelete', function(_) {
if (currentNoteSelection.length > 0 && currentEventSelection.length > 0)
@ -1691,6 +1717,7 @@ class ChartEditorState extends HaxeUIState
});
addUIClickListener('menubarItemAbout', _ -> ChartEditorDialogHandler.openAboutDialog(this));
addUIClickListener('menubarItemWelcomeDialog', _ -> ChartEditorDialogHandler.openWelcomeDialog(this, true));
addUIClickListener('menubarItemUserGuide', _ -> ChartEditorDialogHandler.openUserGuideDialog(this));
@ -1713,6 +1740,11 @@ class ChartEditorState extends HaxeUIState
});
setUICheckboxSelected('menuBarItemThemeDark', currentTheme == ChartEditorTheme.Dark);
addUIClickListener('menubarItemPlayPause', _ -> toggleAudioPlayback());
addUIClickListener('menubarItemLoadInstrumental', _ -> ChartEditorDialogHandler.openUploadInstDialog(this, true));
addUIClickListener('menubarItemLoadVocals', _ -> ChartEditorDialogHandler.openUploadVocalsDialog(this, true));
addUIChangeListener('menubarItemMetronomeEnabled', event -> isMetronomeEnabled = event.value);
setUICheckboxSelected('menubarItemMetronomeEnabled', isMetronomeEnabled);
@ -1726,7 +1758,7 @@ class ChartEditorState extends HaxeUIState
if (instVolumeLabel != null)
{
addUIChangeListener('menubarItemVolumeInstrumental', function(event:UIEvent) {
var volume:Float = event?.value ?? 0 / 100.0;
var volume:Float = (event?.value ?? 0) / 100.0;
if (audioInstTrack != null) audioInstTrack.volume = volume;
instVolumeLabel.text = 'Instrumental - ${Std.int(event.value)}%';
});
@ -1736,7 +1768,7 @@ class ChartEditorState extends HaxeUIState
if (vocalsVolumeLabel != null)
{
addUIChangeListener('menubarItemVolumeVocals', function(event:UIEvent) {
var volume:Float = event?.value ?? 0 / 100.0;
var volume:Float = (event?.value ?? 0) / 100.0;
if (audioVocalTrackGroup != null) audioVocalTrackGroup.volume = volume;
vocalsVolumeLabel.text = 'Vocals - ${Std.int(event.value)}%';
});
@ -1920,33 +1952,33 @@ class ChartEditorState extends HaxeUIState
// Mouse Wheel = Scroll
if (FlxG.mouse.wheel != 0 && !FlxG.keys.pressed.CONTROL)
{
scrollAmount = -10 * FlxG.mouse.wheel;
scrollAmount = -50 * FlxG.mouse.wheel;
shouldPause = true;
}
// Up Arrow = Scroll Up
if (upKeyHandler.activated && currentLiveInputStyle == None)
{
scrollAmount = -GRID_SIZE * 0.25 * 5.0;
scrollAmount = -GRID_SIZE * 0.25 * 25.0;
shouldPause = true;
}
// Down Arrow = Scroll Down
if (downKeyHandler.activated && currentLiveInputStyle == None)
{
scrollAmount = GRID_SIZE * 0.25 * 5.0;
scrollAmount = GRID_SIZE * 0.25 * 25.0;
shouldPause = true;
}
// W = Scroll Up (doesn't work with Ctrl+Scroll)
if (wKeyHandler.activated && currentLiveInputStyle == None && !FlxG.keys.pressed.CONTROL)
{
scrollAmount = -GRID_SIZE * 0.25 * 5.0;
scrollAmount = -GRID_SIZE * 0.25 * 25.0;
shouldPause = true;
}
// S = Scroll Down (doesn't work with Ctrl+Scroll)
if (sKeyHandler.activated && currentLiveInputStyle == None && !FlxG.keys.pressed.CONTROL)
{
scrollAmount = GRID_SIZE * 0.25 * 5.0;
scrollAmount = GRID_SIZE * 0.25 * 25.0;
shouldPause = true;
}
@ -2011,7 +2043,7 @@ class ChartEditorState extends HaxeUIState
// SHIFT + Scroll = Scroll Fast
if (FlxG.keys.pressed.SHIFT)
{
scrollAmount *= 5;
scrollAmount *= 2;
}
// CONTROL + Scroll = Scroll Precise
if (FlxG.keys.pressed.CONTROL)
@ -2314,7 +2346,6 @@ class ChartEditorState extends HaxeUIState
// Scroll up.
var diff:Float = MENU_BAR_HEIGHT - FlxG.mouse.screenY;
scrollPositionInPixels -= diff * 0.5; // Too fast!
trace('Scroll up: ' + diff);
moveSongToScrollPosition();
}
else if (FlxG.mouse.screenY > (playbarHeadLayout?.y ?? 0.0))
@ -2322,7 +2353,6 @@ class ChartEditorState extends HaxeUIState
// Scroll down.
var diff:Float = FlxG.mouse.screenY - (playbarHeadLayout?.y ?? 0.0);
scrollPositionInPixels += diff * 0.5; // Too fast!
trace('Scroll down: ' + diff);
moveSongToScrollPosition();
}
@ -2947,8 +2977,8 @@ class ChartEditorState extends HaxeUIState
// Set the position and size (because we might be recycling one with bad values).
selectionSquare.x = noteSprite.x;
selectionSquare.y = noteSprite.y;
selectionSquare.width = noteSprite.width;
selectionSquare.height = noteSprite.height;
selectionSquare.width = GRID_SIZE;
selectionSquare.height = GRID_SIZE;
}
}
@ -2979,6 +3009,8 @@ class ChartEditorState extends HaxeUIState
FlxG.watch.addQuick("tapNotesRendered", renderedNotes.members.length);
FlxG.watch.addQuick("holdNotesRendered", renderedHoldNotes.members.length);
FlxG.watch.addQuick("eventsRendered", renderedEvents.members.length);
FlxG.watch.addQuick("notesSelected", currentNoteSelection.length);
FlxG.watch.addQuick("eventsSelected", currentEventSelection.length);
}
/**
@ -2986,6 +3018,12 @@ class ChartEditorState extends HaxeUIState
*/
function handleHealthIcons():Void
{
if (healthIconsDirty)
{
if (healthIconBF != null) healthIconBF.characterId = currentSongMetadata.playData.characters.player;
if (healthIconDad != null) healthIconDad.characterId = currentSongMetadata.playData.characters.opponent;
}
// Right align the BF health icon.
if (healthIconBF != null)
{
@ -3002,6 +3040,8 @@ class ChartEditorState extends HaxeUIState
if (selectionSquareBitmap == null)
throw "ERROR: Tried to build selection square, but selectionSquareBitmap is null! Check ChartEditorThemeHandler.updateSelectionSquare()";
FlxG.bitmapLog.add(selectionSquareBitmap, "selectionSquareBitmap");
return new FlxSprite().loadGraphic(selectionSquareBitmap);
}
@ -3121,8 +3161,20 @@ class ChartEditorState extends HaxeUIState
// CTRL + V = Paste
if (FlxG.keys.pressed.CONTROL && FlxG.keys.justPressed.V)
{
// Paste notes from clipboard, at the playhead.
performCommand(new PasteItemsCommand(scrollPositionInMs + playheadPositionInMs));
// CTRL + SHIFT + V = Paste Unsnapped.
var targetMs:Float = if (FlxG.keys.pressed.SHIFT)
{
scrollPositionInMs + playheadPositionInMs;
}
else
{
var targetMs:Float = scrollPositionInMs + playheadPositionInMs;
var targetStep:Float = Conductor.getTimeInSteps(targetMs);
var targetSnappedStep:Float = Math.floor(targetStep / noteSnapRatio) * noteSnapRatio;
var targetSnappedMs:Float = Conductor.getStepTimeInMs(targetSnappedStep);
targetSnappedMs;
}
performCommand(new PasteItemsCommand(targetMs));
}
// DELETE = Delete
@ -3287,7 +3339,7 @@ class ChartEditorState extends HaxeUIState
*/
function handleTestKeybinds():Void
{
if (!isHaxeUIDialogOpen && FlxG.keys.justPressed.ENTER)
if (!isHaxeUIDialogOpen && !isCursorOverHaxeUI && FlxG.keys.justPressed.ENTER)
{
var minimal = FlxG.keys.pressed.SHIFT;
testSongInPlayState(minimal);
@ -3413,11 +3465,11 @@ class ChartEditorState extends HaxeUIState
{
playerPreviewDirty = false;
if (currentSongCharacterPlayer != charPlayer.charId)
if (currentSongMetadata.playData.characters.player != charPlayer.charId)
{
if (healthIconBF != null) healthIconBF.characterId = currentSongCharacterPlayer;
if (healthIconBF != null) healthIconBF.characterId = currentSongMetadata.playData.characters.player;
charPlayer.loadCharacter(currentSongCharacterPlayer);
charPlayer.loadCharacter(currentSongMetadata.playData.characters.player);
charPlayer.characterType = CharacterType.BF;
charPlayer.flip = true;
charPlayer.targetScale = 0.5;
@ -3449,11 +3501,11 @@ class ChartEditorState extends HaxeUIState
{
opponentPreviewDirty = false;
if (currentSongCharacterOpponent != charPlayer.charId)
if (currentSongMetadata.playData.characters.opponent != charPlayer.charId)
{
if (healthIconDad != null) healthIconDad.characterId = currentSongCharacterOpponent;
if (healthIconDad != null) healthIconDad.characterId = currentSongMetadata.playData.characters.opponent;
charPlayer.loadCharacter(currentSongCharacterOpponent);
charPlayer.loadCharacter(currentSongMetadata.playData.characters.opponent);
charPlayer.characterType = CharacterType.DAD;
charPlayer.flip = false;
charPlayer.targetScale = 0.5;
@ -3833,9 +3885,9 @@ class ChartEditorState extends HaxeUIState
switch (noteData.getStrumlineIndex())
{
case 0: // Player
if (hitsoundsEnabledPlayer) ChartEditorAudioHandler.playSound(Paths.sound('funnyNoise/funnyNoise-09'));
if (hitsoundsEnabledPlayer) ChartEditorAudioHandler.playSound(Paths.sound('ui/chart-editor/playerHitsound'));
case 1: // Opponent
if (hitsoundsEnabledOpponent) ChartEditorAudioHandler.playSound(Paths.sound('funnyNoise/funnyNoise-010'));
if (hitsoundsEnabledOpponent) ChartEditorAudioHandler.playSound(Paths.sound('ui/chart-editor/opponentHitsound'));
}
}
}
@ -4072,12 +4124,18 @@ class ChartEditorState extends HaxeUIState
buildSpectrogram(audioInstTrack);
}
else
{
trace('[WARN] Instrumental track was null!');
}
// Pretty much everything is going to need to be reset.
scrollPositionInPixels = 0;
playheadPositionInPixels = 0;
notePreviewDirty = true;
notePreviewViewportBoundsDirty = true;
noteDisplayDirty = true;
healthIconsDirty = true;
moveSongToScrollPosition();
}

View file

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

View file

@ -4,6 +4,9 @@ import flixel.util.FlxColor;
import lime.app.Application;
import funkin.data.song.SongData.SongTimeFormat;
/**
* A store of unchanging, globally relevant values.
*/
class Constants
{
/**
@ -39,7 +42,7 @@ class Constants
*/
public static final VERSION_SUFFIX:String = ' PROTOTYPE';
#if debug
#if (debug || FORCE_DEBUG_VERSION)
static function get_VERSION():String
{
return 'v${Application.current.meta.get('version')} (${GIT_BRANCH} : ${GIT_HASH})' + VERSION_SUFFIX;
@ -71,7 +74,7 @@ class Constants
*/
// ==============================
#if debug
#if (debug || FORCE_DEBUG_VERSION)
/**
* The current Git branch.
*/

View file

@ -0,0 +1,44 @@
package funkin.util;
import flixel.input.gamepad.FlxGamepad;
import flixel.input.gamepad.FlxGamepadInputID;
import lime.ui.Gamepad as LimeGamepad;
import lime.ui.GamepadAxis as LimeGamepadAxis;
import lime.ui.GamepadButton as LimeGamepadButton;
class FlxGamepadUtil
{
public static function getInputID(gamepad:FlxGamepad, button:LimeGamepadButton):FlxGamepadInputID
{
#if FLX_GAMEINPUT_API
// FLX_GAMEINPUT_API internally assigns 6 axes to IDs 0-5, which LimeGamepadButton doesn't account for, so we need to offset the ID by 6.
final OFFSET:Int = 6;
#else
final OFFSET:Int = 0;
#end
var result:FlxGamepadInputID = gamepad.mapping.getID(button + OFFSET);
if (result == NONE) return NONE;
return result;
}
public static function getLimeGamepad(input:FlxGamepad):Null<LimeGamepad>
{
#if FLX_GAMEINPUT_API @:privateAccess
return input._device.getLimeGamepad();
#else
return null;
#end
}
@:privateAccess
public static function getFlxGamepadByLimeGamepad(gamepad:LimeGamepad):FlxGamepad
{
// Why is this so elaborate?
@:privateAccess
var gameInputDevice:openfl.ui.GameInputDevice = openfl.ui.GameInput.__getDevice(gamepad);
@:privateAccess
var gamepadIndex:Int = FlxG.gamepads.findGamepadIndex(gameInputDevice);
return FlxG.gamepads.getByID(gamepadIndex);
}
}

View file

@ -1,6 +1,6 @@
package funkin.util.macro;
#if debug
#if (debug || FORCE_DEBUG_VERSION)
class GitCommit
{
/**