diff --git a/.github/actions/setup-haxeshit/action.yml b/.github/actions/setup-haxeshit/action.yml
index 0cc544cf7..dcf5fd0a7 100644
--- a/.github/actions/setup-haxeshit/action.yml
+++ b/.github/actions/setup-haxeshit/action.yml
@@ -23,8 +23,6 @@ runs:
with:
path: .haxelib
key: ${{ runner.os }}-hmm-${{ hashFiles('**/hmm.json') }}
- restore-keys: |
- ${{ runner.os }}-hmm-
- if: ${{ steps.cache-hmm.outputs.cache-hit != 'true' }}
name: hmm install
run: |
diff --git a/.github/workflows/build-shit.yml b/.github/workflows/build-shit.yml
index 809a8b94b..ed10cbdc2 100644
--- a/.github/workflows/build-shit.yml
+++ b/.github/workflows/build-shit.yml
@@ -53,9 +53,8 @@ jobs:
token: ${{ secrets.GH_RO_PAT }}
- uses: ./.github/actions/setup-haxeshit
- name: Make HXCPP cache dir
- shell: bash
run: |
- mkdir -p ${{ runner.temp }}\\hxcpp_cache
+ mkdir -p ${{ runner.temp }}\hxcpp_cache
- name: Restore build cache
id: cache-build-win
uses: actions/cache@v3
@@ -63,10 +62,8 @@ jobs:
path: |
.haxelib
export
- ${{ runner.temp }}\\hxcpp_cache
- key: ${{ runner.os }}-build-win-${{ github.ref_name }}
- restore-keys: |
- ${{ runner.os }}-build-win-
+ ${{ runner.temp }}\hxcpp_cache
+ key: ${{ runner.os }}-build-win-${{ github.ref_name }}-${{ hashFiles('**/hmm.json') }}
- name: Build game
run: |
haxelib run lime build windows -release -DNO_REDIRECT_ASSETS_FOLDER
diff --git a/Project.xml b/Project.xml
index ccf6c83a3..69400d8b1 100644
--- a/Project.xml
+++ b/Project.xml
@@ -156,7 +156,6 @@
-
@@ -196,6 +195,22 @@
+
+
+
+
+
-->
-->
diff --git a/assets b/assets
index 486ea1cdc..15a238b4c 160000
--- a/assets
+++ b/assets
@@ -1 +1 @@
-Subproject commit 486ea1cdc37a1f1907ba9231b0a1946ff4051f27
+Subproject commit 15a238b4c59914849df282f9ce2eec5b80030207
diff --git a/hmm.json b/hmm.json
index 3f420ac48..070d96cd0 100644
--- a/hmm.json
+++ b/hmm.json
@@ -104,7 +104,7 @@
"name": "lime",
"type": "git",
"dir": null,
- "ref": "f195121ebec688b417e38ab115185c8d93c349d3",
+ "ref": "737b86f121cdc90358d59e2e527934f267c94a2c",
"url": "https://github.com/EliteMasterEric/lime"
},
{
@@ -139,7 +139,7 @@
"name": "openfl",
"type": "git",
"dir": null,
- "ref": "de9395d2f367a80f93f082e1b639b9cde2258bf1",
+ "ref": "f229d76361c7e31025a048fe7909847f75bb5d5e",
"url": "https://github.com/EliteMasterEric/openfl"
},
{
diff --git a/source/Main.hx b/source/Main.hx
index 8419d3fb4..dffe666b7 100644
--- a/source/Main.hx
+++ b/source/Main.hx
@@ -85,6 +85,13 @@ 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();
@@ -93,15 +100,6 @@ class Main extends Sprite
#if hxcpp_debug_server
trace('hxcpp_debug_server is enabled! You can now connect to the game with a debugger.');
#end
-
- #if debug
- fpsCounter = new FPS(10, 3, 0xFFFFFF);
- addChild(fpsCounter);
- #if !html5
- memoryCounter = new MemoryCounter(10, 13, 0xFFFFFF);
- addChild(memoryCounter);
- #end
- #end
}
function initHaxeUI():Void
diff --git a/source/funkin/Controls.hx b/source/funkin/Controls.hx
index 81055fb34..9372c4dc6 100644
--- a/source/funkin/Controls.hx
+++ b/source/funkin/Controls.hx
@@ -1,5 +1,7 @@
+
package funkin;
+import flixel.input.gamepad.FlxGamepad;
import flixel.util.FlxDirectionFlags;
import flixel.FlxObject;
import flixel.input.FlxInput;
@@ -832,6 +834,14 @@ class Controls extends FlxActionSet
fromSaveData(padData, Gamepad(id));
}
+ public function getGamepadIds():Array {
+ return gamepadsAdded;
+ }
+
+ public function getGamepads():Array {
+ return [for (id in gamepadsAdded) FlxG.gamepads.getByID(id)];
+ }
+
inline function addGamepadLiteral(id:Int, ?buttonMap:Map>):Void
{
gamepadsAdded.push(id);
diff --git a/source/funkin/FreeplayState.hx b/source/funkin/FreeplayState.hx
index 3d27c4336..ae73524a8 100644
--- a/source/funkin/FreeplayState.hx
+++ b/source/funkin/FreeplayState.hx
@@ -557,7 +557,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;
@@ -616,6 +616,8 @@ 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);
@@ -667,6 +669,13 @@ class FreeplayState extends MusicBeatSubState
txtCompletion.text = Math.floor(lerpCompletion * 100) + "%";
+ handleInputs(elapsed);
+ }
+
+ function handleInputs(elapsed:Float):Void
+ {
+ if (busy) return;
+
var upP = controls.UI_UP_P;
var downP = controls.UI_DOWN_P;
var accepted = controls.ACCEPT;
@@ -928,7 +937,7 @@ class FreeplayState extends MusicBeatSubState
{
for (song in songs)
{
- if (song == null) return;
+ if (song == null) continue;
if (song.songName != actualSongTho)
{
trace('trying to remove: ' + song.songName);
@@ -937,8 +946,17 @@ class FreeplayState extends MusicBeatSubState
}
}
+ function capsuleOnConfirmRandom(cap:SongMenuItem):Void
+ {
+ trace("RANDOM SELECTED");
+
+ busy = true;
+ }
+
function capsuleOnConfirmDefault(cap:SongMenuItem):Void
{
+ busy = true;
+
PlayStatePlaylist.isStoryMode = false;
var songId:String = cap.songTitle.toLowerCase();
@@ -963,6 +981,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,
diff --git a/source/funkin/InitState.hx b/source/funkin/InitState.hx
index 0a7d413c1..ecfa32eb3 100644
--- a/source/funkin/InitState.hx
+++ b/source/funkin/InitState.hx
@@ -48,7 +48,7 @@ class InitState extends FlxState
// loadSaveData(); // Moved to Main.hx
// Load player options from save data.
- PreferencesMenu.initPrefs();
+ Preferences.init();
// Load controls from save data.
PlayerSettings.init();
diff --git a/source/funkin/MainMenuState.hx b/source/funkin/MainMenuState.hx
index 7c54357bb..7267a6da8 100644
--- a/source/funkin/MainMenuState.hx
+++ b/source/funkin/MainMenuState.hx
@@ -13,23 +13,13 @@ import flixel.input.touch.FlxTouch;
import flixel.text.FlxText;
import flixel.tweens.FlxEase;
import flixel.tweens.FlxTween;
-import flixel.util.FlxColor;
import flixel.util.FlxTimer;
-import funkin.NGio;
-import funkin.modding.events.ScriptEvent.UpdateScriptEvent;
-import funkin.modding.module.ModuleHandler;
-import funkin.shaderslmfao.ScreenWipeShader;
import funkin.ui.AtlasMenuList;
-import funkin.ui.MenuList.MenuItem;
import funkin.ui.MenuList;
import funkin.ui.title.TitleState;
import funkin.ui.story.StoryMenuState;
-import funkin.ui.OptionsState;
-import funkin.ui.PreferencesMenu;
import funkin.ui.Prompt;
import funkin.util.WindowUtil;
-import lime.app.Application;
-import openfl.filters.ShaderFilter;
#if discord_rpc
import Discord.DiscordClient;
#end
@@ -82,8 +72,10 @@ class MainMenuState extends MusicBeatState
magenta.y = bg.y;
magenta.visible = false;
magenta.color = 0xFFfd719b;
- if (PreferencesMenu.preferences.get('flashing-menu')) add(magenta);
- // magenta.scrollFactor.set();
+
+ // TODO: Why doesn't this line compile I'm going fucking feral
+
+ if (Preferences.flashingLights) add(magenta);
menuItems = new MenuTypedList();
add(menuItems);
@@ -116,7 +108,7 @@ class MainMenuState extends MusicBeatState
#end
createMenuItem('options', 'mainmenu/options', function() {
- startExitState(new OptionsState());
+ startExitState(new funkin.ui.OptionsState());
});
// Reset position of menu items.
diff --git a/source/funkin/PauseSubState.hx b/source/funkin/PauseSubState.hx
index f93e5a450..a074410ea 100644
--- a/source/funkin/PauseSubState.hx
+++ b/source/funkin/PauseSubState.hx
@@ -16,17 +16,18 @@ class PauseSubState extends MusicBeatSubState
{
var grpMenuShit:FlxTypedGroup;
- var pauseOptionsBase:Array = [
+ final pauseOptionsBase:Array = [
'Resume',
'Restart Song',
'Change Difficulty',
'Toggle Practice Mode',
'Exit to Menu'
];
+ final pauseOptionsCharting:Array = ['Resume', 'Restart Song', 'Exit to Chart Editor'];
- var pauseOptionsDifficulty:Array = ['EASY', 'NORMAL', 'HARD', 'ERECT', 'BACK'];
+ final pauseOptionsDifficultyBase:Array = ['BACK'];
- var pauseOptionsCharting:Array = ['Resume', 'Restart Song', 'Exit to Chart Editor'];
+ var pauseOptionsDifficulty:Array = []; // AUTO-POPULATED
var menuItems:Array = [];
var curSelected:Int = 0;
@@ -48,6 +49,12 @@ class PauseSubState extends MusicBeatSubState
this.isChartingMode = isChartingMode;
menuItems = this.isChartingMode ? pauseOptionsCharting : pauseOptionsBase;
+ var difficultiesInVariation = PlayState.instance.currentSong.listDifficulties(PlayState.instance.currentChart.variation);
+ trace('DIFFICULTIES: ${difficultiesInVariation}');
+
+ pauseOptionsDifficulty = difficultiesInVariation.map(function(item:String):String {
+ return item.toUpperCase();
+ }).concat(pauseOptionsDifficultyBase);
if (PlayStatePlaylist.campaignId == 'week6')
{
@@ -150,6 +157,11 @@ class PauseSubState extends MusicBeatSubState
super.update(elapsed);
+ handleInputs();
+ }
+
+ function handleInputs():Void
+ {
var upP = controls.UI_UP_P;
var downP = controls.UI_DOWN_P;
var accepted = controls.ACCEPT;
@@ -196,18 +208,6 @@ class PauseSubState extends MusicBeatSubState
menuItems = pauseOptionsDifficulty;
regenMenu();
- case 'EASY' | 'NORMAL' | 'HARD' | 'ERECT':
- PlayState.instance.currentSong = SongRegistry.instance.fetchEntry(PlayState.instance.currentSong.id.toLowerCase());
-
- PlayState.instance.currentDifficulty = daSelected.toLowerCase();
-
- PlayState.instance.needsReset = true;
-
- close();
- case 'BACK':
- menuItems = this.isChartingMode ? pauseOptionsCharting : pauseOptionsBase;
- regenMenu();
-
case 'Toggle Practice Mode':
PlayState.instance.isPracticeMode = true;
practiceText.visible = PlayState.instance.isPracticeMode;
@@ -229,14 +229,43 @@ class PauseSubState extends MusicBeatSubState
FlxTransitionableState.skipNextTransIn = true;
FlxTransitionableState.skipNextTransOut = true;
- if (PlayStatePlaylist.isStoryMode) openSubState(new funkin.ui.StickerSubState(null, STORY));
+ if (PlayStatePlaylist.isStoryMode)
+ {
+ openSubState(new funkin.ui.StickerSubState(null, STORY));
+ }
else
+ {
openSubState(new funkin.ui.StickerSubState(null, FREEPLAY));
+ }
case 'Exit to Chart Editor':
this.close();
if (FlxG.sound.music != null) FlxG.sound.music.stop();
PlayState.instance.close(); // This only works because PlayState is a substate!
+
+ case 'BACK':
+ menuItems = this.isChartingMode ? pauseOptionsCharting : pauseOptionsBase;
+ regenMenu();
+
+ default:
+ if (pauseOptionsDifficulty.contains(daSelected))
+ {
+ PlayState.instance.currentSong = SongRegistry.instance.fetchEntry(PlayState.instance.currentSong.id.toLowerCase());
+
+ // Reset campaign score when changing difficulty
+ // So if you switch difficulty on the last song of a week you get a really low overall score.
+ PlayStatePlaylist.campaignScore = 0;
+ PlayStatePlaylist.campaignDifficulty = daSelected.toLowerCase();
+ PlayState.instance.currentDifficulty = PlayStatePlaylist.campaignDifficulty;
+
+ PlayState.instance.needsReset = true;
+
+ close();
+ }
+ else
+ {
+ trace('[WARN] Unhandled pause menu option: ${daSelected}');
+ }
}
}
diff --git a/source/funkin/PlayerSettings.hx b/source/funkin/PlayerSettings.hx
index a4d8a3b5c..e97cfe384 100644
--- a/source/funkin/PlayerSettings.hx
+++ b/source/funkin/PlayerSettings.hx
@@ -77,6 +77,11 @@ class PlayerSettings
this.id = id;
this.controls = new Controls('player$id', None);
+ addKeyboard();
+ }
+
+ function addKeyboard():Void
+ {
var useDefault = true;
if (Save.get().hasControls(id, Keys))
{
@@ -96,7 +101,6 @@ class PlayerSettings
controls.setKeyboardScheme(Solo);
}
- // Apply loaded settings.
PreciseInputManager.instance.initializeKeys(controls);
}
@@ -124,6 +128,7 @@ class PlayerSettings
trace("Loading gamepad control scheme");
controls.addDefaultGamepad(gamepad.id);
}
+ PreciseInputManager.instance.initializeButtons(controls, gamepad);
}
/**
diff --git a/source/funkin/Preferences.hx b/source/funkin/Preferences.hx
new file mode 100644
index 000000000..7e3c3c6d7
--- /dev/null
+++ b/source/funkin/Preferences.hx
@@ -0,0 +1,138 @@
+package funkin;
+
+import funkin.save.Save;
+
+/**
+ * A store of user-configurable, globally relevant values.
+ */
+class Preferences
+{
+ /**
+ * Whether some particularly fowl language is displayed.
+ * @default `true`
+ */
+ public static var naughtyness(get, set):Bool;
+
+ static function get_naughtyness():Bool
+ {
+ return Save.get().options.naughtyness;
+ }
+
+ static function set_naughtyness(value:Bool):Bool
+ {
+ return Save.get().options.naughtyness = value;
+ }
+
+ /**
+ * If enabled, the strumline is at the bottom of the screen rather than the top.
+ * @default `false`
+ */
+ public static var downscroll(get, set):Bool;
+
+ static function get_downscroll():Bool
+ {
+ return Save.get().options.downscroll;
+ }
+
+ static function set_downscroll(value:Bool):Bool
+ {
+ return Save.get().options.downscroll = value;
+ }
+
+ /**
+ * If disabled, flashing lights in the main menu and other areas will be less intense.
+ * @default `true`
+ */
+ public static var flashingLights(get, set):Bool;
+
+ static function get_flashingLights():Bool
+ {
+ return Save.get().options.flashingLights;
+ }
+
+ static function set_flashingLights(value:Bool):Bool
+ {
+ return Save.get().options.flashingLights = value;
+ }
+
+ /**
+ * If disabled, the camera bump synchronized to the beat.
+ * @default `false`
+ */
+ public static var zoomCamera(get, set):Bool;
+
+ static function get_zoomCamera():Bool
+ {
+ return Save.get().options.zoomCamera;
+ }
+
+ static function set_zoomCamera(value:Bool):Bool
+ {
+ return Save.get().options.zoomCamera = value;
+ }
+
+ /**
+ * If enabled, an FPS and memory counter will be displayed even if this is not a debug build.
+ * @default `false`
+ */
+ public static var debugDisplay(get, set):Bool;
+
+ static function get_debugDisplay():Bool
+ {
+ return Save.get().options.debugDisplay;
+ }
+
+ static function set_debugDisplay(value:Bool):Bool
+ {
+ if (value != Save.get().options.debugDisplay)
+ {
+ toggleDebugDisplay(value);
+ }
+
+ return Save.get().options.debugDisplay = value;
+ }
+
+ /**
+ * If enabled, the game will automatically pause when tabbing out.
+ * @default `true`
+ */
+ public static var autoPause(get, set):Bool;
+
+ static function get_autoPause():Bool
+ {
+ return Save.get().options.autoPause;
+ }
+
+ static function set_autoPause(value:Bool):Bool
+ {
+ if (value != Save.get().options.autoPause) FlxG.autoPause = value;
+
+ return Save.get().options.autoPause = value;
+ }
+
+ public static function init():Void
+ {
+ FlxG.autoPause = Preferences.autoPause;
+ toggleDebugDisplay(Preferences.debugDisplay);
+ }
+
+ static function toggleDebugDisplay(show:Bool):Void
+ {
+ if (show)
+ {
+ // Enable the debug display.
+ FlxG.stage.addChild(Main.fpsCounter);
+ #if !html5
+ FlxG.stage.addChild(Main.memoryCounter);
+ #end
+ }
+ else
+ {
+ // Disable the debug display.
+ FlxG.stage.removeChild(Main.fpsCounter);
+ #if !html5
+ FlxG.stage.removeChild(Main.memoryCounter);
+ #end
+ }
+ }
+}
diff --git a/source/funkin/audiovis/ABotVis.hx b/source/funkin/audiovis/ABotVis.hx
index 2018a99b3..060bddcf7 100644
--- a/source/funkin/audiovis/ABotVis.hx
+++ b/source/funkin/audiovis/ABotVis.hx
@@ -7,7 +7,6 @@ import flixel.graphics.frames.FlxAtlasFrames;
import flixel.group.FlxSpriteGroup.FlxTypedSpriteGroup;
import flixel.math.FlxMath;
import flixel.sound.FlxSound;
-import funkin.ui.PreferencesMenu.CheckboxThingie;
using Lambda;
diff --git a/source/funkin/data/song/SongData.hx b/source/funkin/data/song/SongData.hx
index d557bd39c..9340e46c9 100644
--- a/source/funkin/data/song/SongData.hx
+++ b/source/funkin/data/song/SongData.hx
@@ -4,6 +4,7 @@ import flixel.util.typeLimit.OneOfTwo;
import funkin.data.song.SongRegistry;
import thx.semver.Version;
+@:nullSafety
class SongMetadata
{
/**
@@ -42,7 +43,7 @@ class SongMetadata
public var timeChanges:Array;
/**
- * Defaults to `default` or `''`. Populated later.
+ * Defaults to `Constants.DEFAULT_VARIATION`. Populated later.
*/
@:jignored
public var variation:String;
@@ -228,10 +229,10 @@ class SongMusicData
public var timeChanges:Array;
/**
- * Defaults to `default` or `''`. Populated later.
+ * Defaults to `Constants.DEFAULT_VARIATION`. Populated later.
*/
@:jignored
- public var variation:String = Constants.DEFAULT_VARIATION;
+ public var variation:String;
public function new(songName:String, artist:String, variation:String = 'default')
{
@@ -375,6 +376,9 @@ class SongChartData
@:default(funkin.data.song.SongRegistry.DEFAULT_GENERATEDBY)
public var generatedBy:String;
+ /**
+ * Defaults to `Constants.DEFAULT_VARIATION`. Populated later.
+ */
@:jignored
public var variation:String;
diff --git a/source/funkin/data/song/SongDataUtils.hx b/source/funkin/data/song/SongDataUtils.hx
index 4b9318df2..ee3dfe98c 100644
--- a/source/funkin/data/song/SongDataUtils.hx
+++ b/source/funkin/data/song/SongDataUtils.hx
@@ -21,11 +21,21 @@ class SongDataUtils
* @param notes The notes to modify.
* @param offset The time difference to apply in milliseconds.
*/
- public static function offsetSongNoteData(notes:Array, offset:Int):Array
+ public static function offsetSongNoteData(notes:Array, offset:Float):Array
{
- return notes.map(function(note:SongNoteData):SongNoteData {
- return new SongNoteData(note.time + offset, note.data, note.length, note.kind);
- });
+ var offsetNote = function(note:SongNoteData):SongNoteData {
+ var time:Float = note.time + offset;
+ var data:Int = note.data;
+ var length:Float = note.length;
+ var kind:String = note.kind;
+ return new SongNoteData(time, data, length, kind);
+ };
+
+ trace(notes);
+ trace(notes[0]);
+ var result = [for (i in 0...notes.length) offsetNote(notes[i])];
+ trace(result);
+ return result;
}
/**
@@ -36,7 +46,7 @@ class SongDataUtils
* @param events The events to modify.
* @param offset The time difference to apply in milliseconds.
*/
- public static function offsetSongEventData(events:Array, offset:Int):Array
+ public static function offsetSongEventData(events:Array, offset:Float):Array
{
return events.map(function(event:SongEventData):SongEventData {
return new SongEventData(event.time + offset, event.event, event.value);
@@ -152,7 +162,8 @@ class SongDataUtils
*/
public static function writeItemsToClipboard(data:SongClipboardItems):Void
{
- var dataString = SerializerUtil.toJSON(data);
+ var writer = new json2object.JsonWriter();
+ var dataString:String = writer.write(data, ' ');
ClipboardUtil.setClipboard(dataString);
@@ -170,19 +181,24 @@ class SongDataUtils
trace('Read ${notesString.length} characters from clipboard.');
- var data:SongClipboardItems = notesString.parseJSON();
-
- if (data == null)
+ var parser = new json2object.JsonParser();
+ parser.fromJson(notesString, 'clipboard');
+ if (parser.errors.length > 0)
{
- trace('Failed to parse notes from clipboard.');
+ trace('[SongDataUtils] Error parsing note JSON data from clipboard.');
+ for (error in parser.errors)
+ DataError.printError(error);
return {
+ valid: false,
notes: [],
events: []
};
}
else
{
+ var data:SongClipboardItems = parser.value;
trace('Parsed ' + data.notes.length + ' notes and ' + data.events.length + ' from clipboard.');
+ data.valid = true;
return data;
}
}
@@ -230,6 +246,7 @@ class SongDataUtils
typedef SongClipboardItems =
{
+ ?valid:Bool,
notes:Array,
events:Array
}
diff --git a/source/funkin/data/song/SongRegistry.hx b/source/funkin/data/song/SongRegistry.hx
index cf2da14f7..889fca707 100644
--- a/source/funkin/data/song/SongRegistry.hx
+++ b/source/funkin/data/song/SongRegistry.hx
@@ -156,7 +156,7 @@ class SongRegistry extends BaseRegistry
return cleanMetadata(parser.value, variation);
}
- public function parseEntryMetadataWithMigration(id:String, ?variation:String, version:thx.semver.Version):Null
+ public function parseEntryMetadataWithMigration(id:String, variation:String, version:thx.semver.Version):Null
{
variation = variation == null ? Constants.DEFAULT_VARIATION : variation;
@@ -192,7 +192,7 @@ class SongRegistry extends BaseRegistry
}
}
- function parseEntryMetadata_v2_0_0(id:String, variation:String = ""):Null
+ function parseEntryMetadata_v2_0_0(id:String, ?variation:String):Null
{
variation = variation == null ? Constants.DEFAULT_VARIATION : variation;
diff --git a/source/funkin/import.hx b/source/funkin/import.hx
index 06fe2bfa8..1c3a0fdb4 100644
--- a/source/funkin/import.hx
+++ b/source/funkin/import.hx
@@ -4,6 +4,7 @@ package;
// Only import these when we aren't in a macro.
import funkin.util.Constants;
import funkin.Paths;
+import funkin.Preferences;
import flixel.FlxG; // This one in particular causes a compile error if you're using macros.
// These are great.
diff --git a/source/funkin/input/PreciseInputManager.hx b/source/funkin/input/PreciseInputManager.hx
index 4cce0964d..59e6610a5 100644
--- a/source/funkin/input/PreciseInputManager.hx
+++ b/source/funkin/input/PreciseInputManager.hx
@@ -1,18 +1,25 @@
package funkin.input;
-import openfl.ui.Keyboard;
-import funkin.play.notes.NoteDirection;
-import flixel.input.keyboard.FlxKeyboard.FlxKeyInput;
-import openfl.events.KeyboardEvent;
import flixel.FlxG;
+import flixel.input.FlxInput;
import flixel.input.FlxInput.FlxInputState;
import flixel.input.FlxKeyManager;
+import flixel.input.gamepad.FlxGamepad;
+import flixel.input.gamepad.FlxGamepadInputID;
import flixel.input.keyboard.FlxKey;
+import flixel.input.keyboard.FlxKeyboard.FlxKeyInput;
import flixel.input.keyboard.FlxKeyList;
import flixel.util.FlxSignal.FlxTypedSignal;
+import funkin.play.notes.NoteDirection;
+import funkin.util.FlxGamepadUtil;
import haxe.Int64;
+import lime.ui.Gamepad as LimeGamepad;
+import lime.ui.GamepadAxis as LimeGamepadAxis;
+import lime.ui.GamepadButton as LimeGamepadButton;
import lime.ui.KeyCode;
import lime.ui.KeyModifier;
+import openfl.events.KeyboardEvent;
+import openfl.ui.Keyboard;
/**
* A precise input manager that:
@@ -43,6 +50,20 @@ class PreciseInputManager extends FlxKeyManager
*/
var _keyListDir:Map;
+ /**
+ * A FlxGamepadID->Array, with FlxGamepadInputID being the counterpart to FlxKey.
+ */
+ var _buttonList:Map>;
+
+ var _buttonListArray:Array>;
+
+ var _buttonListMap:Map>>;
+
+ /**
+ * A FlxGamepadID->FlxGamepadInputID->NoteDirection, with FlxGamepadInputID being the counterpart to FlxKey.
+ */
+ var _buttonListDir:Map>;
+
/**
* The timestamp at which a given note direction was last pressed.
*/
@@ -53,15 +74,32 @@ class PreciseInputManager extends FlxKeyManager
*/
var _dirReleaseTimestamps:Map;
+ var _deviceBinds:MapInt64->Void,
+ onButtonUp:LimeGamepadButton->Int64->Void
+ }>;
+
public function new()
{
super(PreciseInputList.new);
+ _deviceBinds = [];
+
_keyList = [];
- _dirPressTimestamps = new Map();
- _dirReleaseTimestamps = new Map();
+ // _keyListMap
+ // _keyListArray
_keyListDir = new Map();
+ _buttonList = [];
+ _buttonListMap = [];
+ _buttonListArray = [];
+ _buttonListDir = new Map>();
+
+ _dirPressTimestamps = new Map();
+ _dirReleaseTimestamps = new Map();
+
+ // Keyboard
FlxG.stage.removeEventListener(KeyboardEvent.KEY_DOWN, onKeyDown);
FlxG.stage.removeEventListener(KeyboardEvent.KEY_UP, onKeyUp);
FlxG.stage.application.window.onKeyDownPrecise.add(handleKeyDown);
@@ -84,6 +122,17 @@ class PreciseInputManager extends FlxKeyManager
};
}
+ public static function getButtonsForDirection(controls:Controls, noteDirection:NoteDirection)
+ {
+ return switch (noteDirection)
+ {
+ case NoteDirection.LEFT: controls.getButtonsForAction(NOTE_LEFT);
+ case NoteDirection.DOWN: controls.getButtonsForAction(NOTE_DOWN);
+ case NoteDirection.UP: controls.getButtonsForAction(NOTE_UP);
+ case NoteDirection.RIGHT: controls.getButtonsForAction(NOTE_RIGHT);
+ };
+ }
+
/**
* Convert from int to Int64.
*/
@@ -138,6 +187,43 @@ class PreciseInputManager extends FlxKeyManager
}
}
+ public function initializeButtons(controls:Controls, gamepad:FlxGamepad):Void
+ {
+ clearButtons();
+
+ var limeGamepad = FlxGamepadUtil.getLimeGamepad(gamepad);
+ var callbacks =
+ {
+ onButtonDown: handleButtonDown.bind(gamepad),
+ onButtonUp: handleButtonUp.bind(gamepad)
+ };
+ limeGamepad.onButtonDownPrecise.add(callbacks.onButtonDown);
+ limeGamepad.onButtonUpPrecise.add(callbacks.onButtonUp);
+
+ for (noteDirection in DIRECTIONS)
+ {
+ var buttons = getButtonsForDirection(controls, noteDirection);
+ for (button in buttons)
+ {
+ var input = new FlxInput(button);
+
+ var buttonListEntry = _buttonList.get(gamepad.id);
+ if (buttonListEntry == null) _buttonList.set(gamepad.id, buttonListEntry = []);
+ buttonListEntry.push(button);
+
+ _buttonListArray.push(input);
+
+ var buttonListMapEntry = _buttonListMap.get(gamepad.id);
+ if (buttonListMapEntry == null) _buttonListMap.set(gamepad.id, buttonListMapEntry = new Map>());
+ buttonListMapEntry.set(button, input);
+
+ var buttonListDirEntry = _buttonListDir.get(gamepad.id);
+ if (buttonListDirEntry == null) _buttonListDir.set(gamepad.id, buttonListDirEntry = new Map());
+ buttonListDirEntry.set(button, noteDirection);
+ }
+ }
+ }
+
/**
* Get the time, in nanoseconds, since the given note direction was last pressed.
* @param noteDirection The note direction to check.
@@ -165,11 +251,41 @@ class PreciseInputManager extends FlxKeyManager
return _keyListMap.get(key);
}
+ public function getInputByButton(gamepad:FlxGamepad, button:FlxGamepadInputID):FlxInput
+ {
+ return _buttonListMap.get(gamepad.id).get(button);
+ }
+
public function getDirectionForKey(key:FlxKey):NoteDirection
{
return _keyListDir.get(key);
}
+ public function getDirectionForButton(gamepad:FlxGamepad, button:FlxGamepadInputID):NoteDirection
+ {
+ return _buttonListDir.get(gamepad.id).get(button);
+ }
+
+ function getButton(gamepad:FlxGamepad, button:FlxGamepadInputID):FlxInput
+ {
+ return _buttonListMap.get(gamepad.id).get(button);
+ }
+
+ function updateButtonStates(gamepad:FlxGamepad, button:FlxGamepadInputID, down:Bool):Void
+ {
+ var input = getButton(gamepad, button);
+ if (input == null) return;
+
+ if (down)
+ {
+ input.press();
+ }
+ else
+ {
+ input.release();
+ }
+ }
+
function handleKeyDown(keyCode:KeyCode, _:KeyModifier, timestamp:Int64):Void
{
var key:FlxKey = convertKeyCode(keyCode);
@@ -198,7 +314,7 @@ class PreciseInputManager extends FlxKeyManager
if (_keyList.indexOf(key) == -1) return;
// TODO: Remove this line with SDL3 when timestamps change meaning.
- // This is because SDL3's timestamps are measured in nanoseconds, not milliseconds.
+ // This is because SDL3's timestamps ar e measured in nanoseconds, not milliseconds.
timestamp *= Constants.NS_PER_MS;
updateKeyStates(key, false);
@@ -214,6 +330,54 @@ class PreciseInputManager extends FlxKeyManager
}
}
+ function handleButtonDown(gamepad:FlxGamepad, button:LimeGamepadButton, timestamp:Int64):Void
+ {
+ var buttonId:FlxGamepadInputID = FlxGamepadUtil.getInputID(gamepad, button);
+
+ var buttonListEntry = _buttonList.get(gamepad.id);
+ if (buttonListEntry == null || buttonListEntry.indexOf(buttonId) == -1) return;
+
+ // TODO: Remove this line with SDL3 when timestamps change meaning.
+ // This is because SDL3's timestamps ar e measured in nanoseconds, not milliseconds.
+ timestamp *= Constants.NS_PER_MS;
+
+ updateButtonStates(gamepad, buttonId, true);
+
+ if (getInputByButton(gamepad, buttonId)?.justPressed ?? false)
+ {
+ onInputPressed.dispatch(
+ {
+ noteDirection: getDirectionForButton(gamepad, buttonId),
+ timestamp: timestamp
+ });
+ _dirPressTimestamps.set(getDirectionForButton(gamepad, buttonId), timestamp);
+ }
+ }
+
+ function handleButtonUp(gamepad:FlxGamepad, button:LimeGamepadButton, timestamp:Int64):Void
+ {
+ var buttonId:FlxGamepadInputID = FlxGamepadUtil.getInputID(gamepad, button);
+
+ var buttonListEntry = _buttonList.get(gamepad.id);
+ if (buttonListEntry == null || buttonListEntry.indexOf(buttonId) == -1) return;
+
+ // TODO: Remove this line with SDL3 when timestamps change meaning.
+ // This is because SDL3's timestamps ar e measured in nanoseconds, not milliseconds.
+ timestamp *= Constants.NS_PER_MS;
+
+ updateButtonStates(gamepad, buttonId, false);
+
+ if (getInputByButton(gamepad, buttonId)?.justReleased ?? false)
+ {
+ onInputReleased.dispatch(
+ {
+ noteDirection: getDirectionForButton(gamepad, buttonId),
+ timestamp: timestamp
+ });
+ _dirReleaseTimestamps.set(getDirectionForButton(gamepad, buttonId), timestamp);
+ }
+ }
+
static function convertKeyCode(input:KeyCode):FlxKey
{
@:privateAccess
@@ -228,6 +392,31 @@ class PreciseInputManager extends FlxKeyManager
_keyListMap.clear();
_keyListDir.clear();
}
+
+ function clearButtons():Void
+ {
+ _buttonListArray = [];
+ _buttonListDir.clear();
+
+ for (gamepad in _deviceBinds.keys())
+ {
+ var callbacks = _deviceBinds.get(gamepad);
+ var limeGamepad = FlxGamepadUtil.getLimeGamepad(gamepad);
+ limeGamepad.onButtonDownPrecise.remove(callbacks.onButtonDown);
+ limeGamepad.onButtonUpPrecise.remove(callbacks.onButtonUp);
+ }
+ _deviceBinds.clear();
+ }
+
+ public override function destroy():Void
+ {
+ // Keyboard
+ FlxG.stage.application.window.onKeyDownPrecise.remove(handleKeyDown);
+ FlxG.stage.application.window.onKeyUpPrecise.remove(handleKeyUp);
+
+ clearKeys();
+ clearButtons();
+ }
}
class PreciseInputList extends FlxKeyList
diff --git a/source/funkin/play/GameOverSubState.hx b/source/funkin/play/GameOverSubState.hx
index 15ed0421e..a3aeb4139 100644
--- a/source/funkin/play/GameOverSubState.hx
+++ b/source/funkin/play/GameOverSubState.hx
@@ -11,7 +11,6 @@ import funkin.modding.events.ScriptEvent;
import funkin.modding.events.ScriptEventDispatcher;
import funkin.play.PlayState;
import funkin.play.character.BaseCharacter;
-import funkin.ui.PreferencesMenu;
/**
* A substate which renders over the PlayState when the player dies.
@@ -292,7 +291,7 @@ class GameOverSubState extends MusicBeatSubState
{
var randomCensor:Array = [];
- if (PreferencesMenu.getPref('censor-naughty')) randomCensor = [1, 3, 8, 13, 17, 21];
+ if (!Preferences.naughtyness) randomCensor = [1, 3, 8, 13, 17, 21];
FlxG.sound.play(Paths.sound('jeffGameover/jeffGameover-' + FlxG.random.int(1, 25, randomCensor)), 1, false, null, true, function() {
// Once the quote ends, fade in the game over music.
diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx
index d56d2e1a4..8b47e6ebd 100644
--- a/source/funkin/play/PlayState.hx
+++ b/source/funkin/play/PlayState.hx
@@ -920,7 +920,6 @@ class PlayState extends MusicBeatSubState
}
// Handle keybinds.
- // if (!isInCutscene && !disableKeys) keyShit(true);
processInputQueue();
if (!isInCutscene && !disableKeys) debugKeyShit();
if (isInCutscene && !disableKeys) handleCutsceneKeys(elapsed);
@@ -1268,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);
@@ -1477,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];
@@ -2464,9 +2463,9 @@ class PlayState extends MusicBeatSubState
accuracy: Highscore.tallies.totalNotesHit / Highscore.tallies.totalNotes,
};
- if (Save.get().isLevelHighScore(PlayStatePlaylist.campaignId, currentDifficulty, data))
+ if (Save.get().isLevelHighScore(PlayStatePlaylist.campaignId, PlayStatePlaylist.campaignDifficulty, data))
{
- Save.get().setLevelScore(PlayStatePlaylist.campaignId, currentDifficulty, data);
+ Save.get().setLevelScore(PlayStatePlaylist.campaignId, PlayStatePlaylist.campaignDifficulty, data);
#if newgrounds
NGio.postScore(score, 'Level ${PlayStatePlaylist.campaignId}');
#end
@@ -2514,7 +2513,7 @@ class PlayState extends MusicBeatSubState
var nextPlayState:PlayState = new PlayState(
{
targetSong: targetSong,
- targetDifficulty: currentDifficulty,
+ targetDifficulty: PlayStatePlaylist.campaignDifficulty,
targetCharacter: currentPlayerId,
});
nextPlayState.previousCameraFollowPoint = new FlxSprite(cameraFollowPoint.x, cameraFollowPoint.y);
@@ -2530,7 +2529,7 @@ class PlayState extends MusicBeatSubState
var nextPlayState:PlayState = new PlayState(
{
targetSong: targetSong,
- targetDifficulty: currentDifficulty,
+ targetDifficulty: PlayStatePlaylist.campaignDifficulty,
targetCharacter: currentPlayerId,
});
nextPlayState.previousCameraFollowPoint = new FlxSprite(cameraFollowPoint.x, cameraFollowPoint.y);
@@ -2656,7 +2655,12 @@ class PlayState extends MusicBeatSubState
persistentUpdate = false;
vocals.stop();
camHUD.alpha = 1;
- var res:ResultState = new ResultState();
+ var res:ResultState = new ResultState(
+ {
+ storyMode: PlayStatePlaylist.isStoryMode,
+ title: PlayStatePlaylist.isStoryMode ? ('${PlayStatePlaylist.campaignTitle}') : ('${currentChart.songName} by ${currentChart.songArtist}'),
+ tallies: Highscore.tallies,
+ });
res.camera = camHUD;
openSubState(res);
}
diff --git a/source/funkin/play/PlayStatePlaylist.hx b/source/funkin/play/PlayStatePlaylist.hx
index 6b754878c..3b0fb01f6 100644
--- a/source/funkin/play/PlayStatePlaylist.hx
+++ b/source/funkin/play/PlayStatePlaylist.hx
@@ -34,10 +34,7 @@ class PlayStatePlaylist
*/
public static var campaignId:String = 'unknown';
- /**
- * The current difficulty selected for this level (as a named ID).
- */
- public static var currentDifficulty(default, default):String = Constants.DEFAULT_DIFFICULTY;
+ public static var campaignDifficulty:String = Constants.DEFAULT_DIFFICULTY;
/**
* Resets the playlist to its default state.
@@ -49,6 +46,6 @@ class PlayStatePlaylist
campaignScore = 0;
campaignTitle = 'UNKNOWN';
campaignId = 'unknown';
- currentDifficulty = Constants.DEFAULT_DIFFICULTY;
+ campaignDifficulty = Constants.DEFAULT_DIFFICULTY;
}
}
diff --git a/source/funkin/play/ResultState.hx b/source/funkin/play/ResultState.hx
index 0c2984719..3f7231c2a 100644
--- a/source/funkin/play/ResultState.hx
+++ b/source/funkin/play/ResultState.hx
@@ -22,6 +22,8 @@ import flxanimate.FlxAnimate.Settings;
class ResultState extends MusicBeatSubState
{
+ final params:ResultsStateParams;
+
var resultsVariation:ResultVariations;
var songName:FlxBitmapText;
var difficulty:FlxSprite;
@@ -29,13 +31,18 @@ class ResultState extends MusicBeatSubState
var maskShaderSongName = new LeftMaskShader();
var maskShaderDifficulty = new LeftMaskShader();
+ public function new(params:ResultsStateParams)
+ {
+ super();
+
+ this.params = params;
+ }
+
override function create():Void
{
- if (Highscore.tallies.sick == Highscore.tallies.totalNotesHit
- && Highscore.tallies.maxCombo == Highscore.tallies.totalNotesHit) resultsVariation = PERFECT;
- else if (Highscore.tallies.missed
- + Highscore.tallies.bad
- + Highscore.tallies.shit >= Highscore.tallies.totalNotes * 0.50)
+ if (params.tallies.sick == params.tallies.totalNotesHit
+ && params.tallies.maxCombo == params.tallies.totalNotesHit) resultsVariation = PERFECT;
+ else if (params.tallies.missed + params.tallies.bad + params.tallies.shit >= params.tallies.totalNotes * 0.50)
resultsVariation = SHIT; // if more than half of your song was missed, bad, or shit notes, you get shit ending!
else
resultsVariation = NORMAL;
@@ -135,17 +142,7 @@ class ResultState extends MusicBeatSubState
var fontLetters:String = "AaBbCcDdEeFfGgHhiIJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz:1234567890";
songName = new FlxBitmapText(FlxBitmapFont.fromMonospace(Paths.image("resultScreen/tardlingSpritesheet"), fontLetters, FlxPoint.get(49, 62)));
-
- // stole this from PauseSubState, I think eric wrote it!!
- if (PlayState.instance.currentChart != null)
- {
- songName.text += '${PlayState.instance.currentChart.songName}:${PlayState.instance.currentChart.songArtist}';
- }
- else
- {
- songName.text += PlayState.instance.currentSong.id;
- }
-
+ songName.text = params.title;
songName.letterSpacing = -15;
songName.angle = -4.1;
add(songName);
@@ -194,27 +191,27 @@ class ResultState extends MusicBeatSubState
var ratingGrp:FlxTypedGroup = new FlxTypedGroup();
add(ratingGrp);
- var totalHit:TallyCounter = new TallyCounter(375, hStuf * 3, Highscore.tallies.totalNotesHit);
+ var totalHit:TallyCounter = new TallyCounter(375, hStuf * 3, params.tallies.totalNotesHit);
ratingGrp.add(totalHit);
- var maxCombo:TallyCounter = new TallyCounter(375, hStuf * 4, Highscore.tallies.maxCombo);
+ var maxCombo:TallyCounter = new TallyCounter(375, hStuf * 4, params.tallies.maxCombo);
ratingGrp.add(maxCombo);
hStuf += 2;
var extraYOffset:Float = 5;
- var tallySick:TallyCounter = new TallyCounter(230, (hStuf * 5) + extraYOffset, Highscore.tallies.sick, 0xFF89E59E);
+ var tallySick:TallyCounter = new TallyCounter(230, (hStuf * 5) + extraYOffset, params.tallies.sick, 0xFF89E59E);
ratingGrp.add(tallySick);
- var tallyGood:TallyCounter = new TallyCounter(210, (hStuf * 6) + extraYOffset, Highscore.tallies.good, 0xFF89C9E5);
+ var tallyGood:TallyCounter = new TallyCounter(210, (hStuf * 6) + extraYOffset, params.tallies.good, 0xFF89C9E5);
ratingGrp.add(tallyGood);
- var tallyBad:TallyCounter = new TallyCounter(190, (hStuf * 7) + extraYOffset, Highscore.tallies.bad, 0xffE6CF8A);
+ var tallyBad:TallyCounter = new TallyCounter(190, (hStuf * 7) + extraYOffset, params.tallies.bad, 0xffE6CF8A);
ratingGrp.add(tallyBad);
- var tallyShit:TallyCounter = new TallyCounter(220, (hStuf * 8) + extraYOffset, Highscore.tallies.shit, 0xFFE68C8A);
+ var tallyShit:TallyCounter = new TallyCounter(220, (hStuf * 8) + extraYOffset, params.tallies.shit, 0xFFE68C8A);
ratingGrp.add(tallyShit);
- var tallyMissed:TallyCounter = new TallyCounter(260, (hStuf * 9) + extraYOffset, Highscore.tallies.missed, 0xFFC68AE6);
+ var tallyMissed:TallyCounter = new TallyCounter(260, (hStuf * 9) + extraYOffset, params.tallies.missed, 0xFFC68AE6);
ratingGrp.add(tallyMissed);
for (ind => rating in ratingGrp.members)
@@ -275,7 +272,7 @@ class ResultState extends MusicBeatSubState
}
});
- if (Highscore.tallies.isNewHighscore) trace("ITS A NEW HIGHSCORE!!!");
+ if (params.tallies.isNewHighscore) trace("ITS A NEW HIGHSCORE!!!");
super.create();
}
@@ -351,7 +348,7 @@ class ResultState extends MusicBeatSubState
if (controls.PAUSE)
{
- if (PlayStatePlaylist.isStoryMode)
+ if (params.storyMode)
{
FlxG.switchState(new StoryMenuState());
}
@@ -372,3 +369,21 @@ enum abstract ResultVariations(String)
var NORMAL;
var SHIT;
}
+
+typedef ResultsStateParams =
+{
+ /**
+ * True if results are for a level, false if results are for a single song.
+ */
+ var storyMode:Bool;
+
+ /**
+ * Either "Song Name by Artist Name" or "Week Name"
+ */
+ var title:String;
+
+ /**
+ * The score, accuracy, and judgements.
+ */
+ var tallies:Highscore.Tallies;
+};
diff --git a/source/funkin/play/notes/Strumline.hx b/source/funkin/play/notes/Strumline.hx
index 7bd6e7ae7..60b995c06 100644
--- a/source/funkin/play/notes/Strumline.hx
+++ b/source/funkin/play/notes/Strumline.hx
@@ -231,7 +231,7 @@ class Strumline extends FlxSpriteGroup
notesVwoosh.add(note);
var targetY:Float = FlxG.height + note.y;
- if (PreferencesMenu.getPref('downscroll')) targetY = 0 - note.height;
+ if (Preferences.downscroll) targetY = 0 - note.height;
FlxTween.tween(note, {y: targetY}, 0.5,
{
ease: FlxEase.expoIn,
@@ -252,7 +252,7 @@ class Strumline extends FlxSpriteGroup
holdNotesVwoosh.add(holdNote);
var targetY:Float = FlxG.height + holdNote.y;
- if (PreferencesMenu.getPref('downscroll')) targetY = 0 - holdNote.height;
+ if (Preferences.downscroll) targetY = 0 - holdNote.height;
FlxTween.tween(holdNote, {y: targetY}, 0.5,
{
ease: FlxEase.expoIn,
@@ -277,7 +277,7 @@ class Strumline extends FlxSpriteGroup
var vwoosh:Float = (strumTime < Conductor.songPosition) && vwoosh ? 2.0 : 1.0;
var scrollSpeed:Float = PlayState.instance?.currentChart?.scrollSpeed ?? 1.0;
- return Constants.PIXELS_PER_MS * (Conductor.songPosition - strumTime) * scrollSpeed * vwoosh * (PreferencesMenu.getPref('downscroll') ? 1 : -1);
+ return Constants.PIXELS_PER_MS * (Conductor.songPosition - strumTime) * scrollSpeed * vwoosh * (Preferences.downscroll ? 1 : -1);
}
function updateNotes():Void
@@ -321,7 +321,7 @@ class Strumline extends FlxSpriteGroup
note.y = this.y - INITIAL_OFFSET + calculateNoteYPos(note.strumTime, vwoosh);
// If the note is miss
- var isOffscreen = PreferencesMenu.getPref('downscroll') ? note.y > FlxG.height : note.y < -note.height;
+ var isOffscreen = Preferences.downscroll ? note.y > FlxG.height : note.y < -note.height;
if (note.handledMiss && isOffscreen)
{
killNote(note);
@@ -388,7 +388,7 @@ class Strumline extends FlxSpriteGroup
var vwoosh:Bool = false;
- if (PreferencesMenu.getPref('downscroll'))
+ if (Preferences.downscroll)
{
holdNote.y = this.y + calculateNoteYPos(holdNote.strumTime, vwoosh) - holdNote.height + STRUMLINE_SIZE / 2;
}
@@ -410,7 +410,7 @@ class Strumline extends FlxSpriteGroup
holdNote.visible = false;
}
- if (PreferencesMenu.getPref('downscroll'))
+ if (Preferences.downscroll)
{
holdNote.y = this.y - holdNote.height + STRUMLINE_SIZE / 2;
}
@@ -425,7 +425,7 @@ class Strumline extends FlxSpriteGroup
holdNote.visible = true;
var vwoosh:Bool = false;
- if (PreferencesMenu.getPref('downscroll'))
+ if (Preferences.downscroll)
{
holdNote.y = this.y + calculateNoteYPos(holdNote.strumTime, vwoosh) - holdNote.height + STRUMLINE_SIZE / 2;
}
diff --git a/source/funkin/play/notes/SustainTrail.hx b/source/funkin/play/notes/SustainTrail.hx
index 37bc674a5..f55799828 100644
--- a/source/funkin/play/notes/SustainTrail.hx
+++ b/source/funkin/play/notes/SustainTrail.hx
@@ -114,7 +114,7 @@ class SustainTrail extends FlxSprite
height = sustainHeight(sustainLength, getScrollSpeed());
// instead of scrollSpeed, PlayState.SONG.speed
- flipY = PreferencesMenu.getPref('downscroll');
+ flipY = Preferences.downscroll;
// alpha = 0.6;
alpha = 1.0;
diff --git a/source/funkin/play/song/Song.hx b/source/funkin/play/song/Song.hx
index 000572d6a..33363e1ff 100644
--- a/source/funkin/play/song/Song.hx
+++ b/source/funkin/play/song/Song.hx
@@ -56,8 +56,6 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry;
-
public var songName(get, never):String;
function get_songName():String
@@ -85,7 +83,6 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry();
_data = _fetchData(id);
@@ -127,8 +124,7 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry chartData in charts)
@@ -162,8 +158,6 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry
{
- if (diffId == null) diffId = difficulties.keys().array()[0];
+ if (diffId == null) diffId = listDifficulties()[0];
return difficulties.get(diffId);
}
- public function listDifficulties():Array
+ /**
+ * List all the difficulties in this song.
+ * @param variationId Optionally filter by variation.
+ * @return The list of difficulties.
+ */
+ public function listDifficulties(?variationId:String):Array
{
- return difficultyIds;
+ if (variationId == '') variationId = null;
+
+ var diffFiltered:Array = difficulties.keys().array().filter(function(diffId:String):Bool {
+ if (variationId == null) return true;
+ var difficulty:Null = difficulties.get(diffId);
+ if (difficulty == null) return false;
+ return difficulty.variation == variationId;
+ });
+
+ // sort the difficulties, since they may be out of order in the chart JSON
+ // maybe be careful of lowercase/uppercase?
+ // also used in Level.listDifficulties()!!
+ var diffMap:Map = new Map();
+ for (difficulty in diffFiltered)
+ {
+ var num:Int = 0;
+ switch (difficulty)
+ {
+ case 'easy':
+ num = 0;
+ case 'normal':
+ num = 1;
+ case 'hard':
+ num = 2;
+ case 'erect':
+ num = 3;
+ case 'nightmare':
+ num = 4;
+ }
+ diffMap.set(difficulty, num);
+ }
+
+ diffFiltered.sort(function(a:String, b:String) {
+ return (diffMap.get(a) ?? 0) - (diffMap.get(b) ?? 0);
+ });
+
+ return diffFiltered;
}
- public function hasDifficulty(diffId:String):Bool
+ public function hasDifficulty(diffId:String, ?variationId:String):Bool
{
- return difficulties.exists(diffId);
+ if (variationId == '') variationId = null;
+ var difficulty:Null = difficulties.get(diffId);
+ return variationId == null ? (difficulty != null) : (difficulty != null && difficulty.variation == variationId);
}
/**
diff --git a/source/funkin/save/Save.hx b/source/funkin/save/Save.hx
index d1f9800ea..2666d2bff 100644
--- a/source/funkin/save/Save.hx
+++ b/source/funkin/save/Save.hx
@@ -63,10 +63,10 @@ abstract Save(RawSaveData)
// Reasonable defaults.
naughtyness: true,
downscroll: false,
- flashingMenu: true,
+ flashingLights: true,
zoomCamera: true,
debugDisplay: false,
- pauseOnTabOut: true,
+ autoPause: true,
controls:
{
@@ -88,7 +88,7 @@ abstract Save(RawSaveData)
{
// No mods enabled.
enabledMods: [],
- modSettings: [],
+ modOptions: [],
},
optionsChartEditor:
@@ -98,6 +98,20 @@ abstract Save(RawSaveData)
};
}
+ public var options(get, never):SaveDataOptions;
+
+ function get_options():SaveDataOptions
+ {
+ return this.options;
+ }
+
+ public var modOptions(get, never):Map;
+
+ function get_modOptions():Map
+ {
+ return this.mods.modOptions;
+ }
+
/**
* The current session ID for the logged-in Newgrounds user, or null if the user is cringe.
*/
@@ -458,7 +472,7 @@ typedef SaveHighScoresData =
typedef SaveDataMods =
{
var enabledMods:Array;
- var modSettings:Map;
+ var modOptions:Map;
}
/**
@@ -530,10 +544,10 @@ typedef SaveDataOptions =
var downscroll:Bool;
/**
- * If disabled, the main menu won't flash when entering a submenu.
+ * If disabled, flashing lights in the main menu and other areas will be less intense.
* @default `true`
*/
- var flashingMenu:Bool;
+ var flashingLights:Bool;
/**
* If disabled, the camera bump synchronized to the beat.
@@ -551,7 +565,7 @@ typedef SaveDataOptions =
* If enabled, the game will automatically pause when tabbing out.
* @default `true`
*/
- var pauseOnTabOut:Bool;
+ var autoPause:Bool;
var controls:
{
diff --git a/source/funkin/ui/ControlsMenu.hx b/source/funkin/ui/ControlsMenu.hx
index 0d9db5b34..8197424ee 100644
--- a/source/funkin/ui/ControlsMenu.hx
+++ b/source/funkin/ui/ControlsMenu.hx
@@ -163,7 +163,17 @@ class ControlsMenu extends funkin.ui.OptionsState.Page
function onSelect():Void
{
- keyUsedToEnterPrompt = FlxG.keys.firstJustPressed();
+ switch (currentDevice)
+ {
+ case Keys:
+ {
+ keyUsedToEnterPrompt = FlxG.keys.firstJustPressed();
+ }
+ case Gamepad(id):
+ {
+ buttonUsedToEnterPrompt = FlxG.gamepads.getByID(id).firstJustPressedID();
+ }
+ }
controlGrid.enabled = false;
canExit = false;
@@ -204,6 +214,7 @@ class ControlsMenu extends funkin.ui.OptionsState.Page
}
var keyUsedToEnterPrompt:Null = null;
+ var buttonUsedToEnterPrompt:Null = null;
override function update(elapsed:Float):Void
{
@@ -246,19 +257,49 @@ class ControlsMenu extends funkin.ui.OptionsState.Page
case Gamepad(id):
{
var button = FlxG.gamepads.getByID(id).firstJustReleasedID();
- if (button != NONE && button != keyUsedToEnterPrompt)
+ if (button != NONE && button != buttonUsedToEnterPrompt)
{
if (button != BACK) onInputSelect(button);
closePrompt();
}
+
+ var key = FlxG.keys.firstJustReleased();
+ if (key != NONE && key != keyUsedToEnterPrompt)
+ {
+ if (key == ESCAPE)
+ {
+ closePrompt();
+ }
+ else if (key == BACKSPACE)
+ {
+ onInputSelect(NONE);
+ closePrompt();
+ }
+ }
}
}
}
- var keyJustReleased:Int = FlxG.keys.firstJustReleased();
- if (keyJustReleased != NONE && keyJustReleased == keyUsedToEnterPrompt)
+ switch (currentDevice)
{
- keyUsedToEnterPrompt = null;
+ case Keys:
+ {
+ var keyJustReleased:Int = FlxG.keys.firstJustReleased();
+ if (keyJustReleased != NONE && keyJustReleased == keyUsedToEnterPrompt)
+ {
+ keyUsedToEnterPrompt = null;
+ }
+ buttonUsedToEnterPrompt = null;
+ }
+ case Gamepad(id):
+ {
+ var buttonJustReleased:Int = FlxG.gamepads.getByID(id).firstJustReleasedID();
+ if (buttonJustReleased != NONE && buttonJustReleased == buttonUsedToEnterPrompt)
+ {
+ buttonUsedToEnterPrompt = null;
+ }
+ keyUsedToEnterPrompt = null;
+ }
}
}
diff --git a/source/funkin/ui/PreferencesMenu.hx b/source/funkin/ui/PreferencesMenu.hx
index 4fa8f7f5b..812d0ab49 100644
--- a/source/funkin/ui/PreferencesMenu.hx
+++ b/source/funkin/ui/PreferencesMenu.hx
@@ -3,17 +3,16 @@ package funkin.ui;
import flixel.FlxCamera;
import flixel.FlxObject;
import flixel.FlxSprite;
+import flixel.group.FlxSpriteGroup.FlxTypedSpriteGroup;
import funkin.ui.AtlasText.AtlasFont;
import funkin.ui.OptionsState.Page;
import funkin.ui.TextMenuList.TextMenuItem;
class PreferencesMenu extends Page
{
- public static var preferences:Map = new Map();
-
var items:TextMenuList;
+ var preferenceItems:FlxTypedSpriteGroup;
- var checkboxes:Array = [];
var menuCamera:FlxCamera;
var camFollow:FlxObject;
@@ -27,13 +26,9 @@ class PreferencesMenu extends Page
camera = menuCamera;
add(items = new TextMenuList());
+ add(preferenceItems = new FlxTypedSpriteGroup());
- createPrefItem('naughtyness', 'censor-naughty', true);
- createPrefItem('downscroll', 'downscroll', false);
- createPrefItem('flashing menu', 'flashing-menu', true);
- createPrefItem('Camera Zooming on Beat', 'camera-zoom', true);
- createPrefItem('FPS Counter', 'fps-counter', true);
- createPrefItem('Auto Pause', 'auto-pause', false);
+ createPrefItems();
camFollow = new FlxObject(FlxG.width / 2, 0, 140, 70);
if (items != null) camFollow.y = items.selectedItem.y;
@@ -48,128 +43,63 @@ class PreferencesMenu extends Page
});
}
- public static function getPref(pref:String):Dynamic
+ /**
+ * Create the menu items for each of the preferences.
+ */
+ function createPrefItems():Void
{
- return preferences.get(pref);
+ createPrefItemCheckbox('Naughtyness', 'Toggle displaying raunchy content', function(value:Bool):Void {
+ Preferences.naughtyness = value;
+ }, Preferences.naughtyness);
+ createPrefItemCheckbox('Downscroll', 'Enable to make notes move downwards', function(value:Bool):Void {
+ Preferences.downscroll = value;
+ }, Preferences.downscroll);
+ createPrefItemCheckbox('Flashing Lights', 'Disable to dampen flashing effects', function(value:Bool):Void {
+ Preferences.flashingLights = value;
+ }, Preferences.flashingLights);
+ createPrefItemCheckbox('Camera Zooming on Beat', 'Disable to stop the camera bouncing to the song', function(value:Bool):Void {
+ Preferences.zoomCamera = value;
+ }, Preferences.zoomCamera);
+ createPrefItemCheckbox('Debug Display', 'Enable to show FPS and other debug stats', function(value:Bool):Void {
+ Preferences.debugDisplay = value;
+ }, Preferences.debugDisplay);
+ createPrefItemCheckbox('Auto Pause', 'Automatically pause the game when it loses focus', function(value:Bool):Void {
+ Preferences.autoPause = value;
+ }, Preferences.autoPause);
}
- // easy shorthand?
- public static function setPref(pref:String, value:Dynamic):Void
+ function createPrefItemCheckbox(prefName:String, prefDesc:String, onChange:Bool->Void, defaultValue:Bool):Void
{
- preferences.set(pref, value);
- }
+ var checkbox:CheckboxPreferenceItem = new CheckboxPreferenceItem(0, 120 * (items.length - 1 + 1), defaultValue);
- public static function initPrefs():Void
- {
- preferenceCheck('censor-naughty', true);
- preferenceCheck('downscroll', false);
- preferenceCheck('flashing-menu', true);
- preferenceCheck('camera-zoom', true);
- preferenceCheck('fps-counter', true);
- preferenceCheck('auto-pause', false);
- preferenceCheck('master-volume', 1);
-
- #if muted
- setPref('master-volume', 0);
- FlxG.sound.muted = true;
- #end
-
- if (!getPref('fps-counter')) FlxG.stage.removeChild(Main.fpsCounter);
-
- FlxG.autoPause = getPref('auto-pause');
- }
-
- function createPrefItem(prefName:String, prefString:String, prefValue:Dynamic):Void
- {
items.createItem(120, (120 * items.length) + 30, prefName, AtlasFont.BOLD, function() {
- preferenceCheck(prefString, prefValue);
-
- switch (Type.typeof(prefValue).getName())
- {
- case 'TBool':
- prefToggle(prefString);
-
- default:
- trace('swag');
- }
+ var value = !checkbox.currentValue;
+ onChange(value);
+ checkbox.currentValue = value;
});
- switch (Type.typeof(prefValue).getName())
- {
- case 'TBool':
- createCheckbox(prefString);
-
- default:
- trace('swag');
- }
-
- trace(Type.typeof(prefValue).getName());
- }
-
- function createCheckbox(prefString:String)
- {
- var checkbox:CheckboxThingie = new CheckboxThingie(0, 120 * (items.length - 1), preferences.get(prefString));
- checkboxes.push(checkbox);
- add(checkbox);
- }
-
- /**
- * Assumes that the preference has already been checked/set?
- */
- function prefToggle(prefName:String)
- {
- var daSwap:Bool = preferences.get(prefName);
- daSwap = !daSwap;
- preferences.set(prefName, daSwap);
- checkboxes[items.selectedIndex].daValue = daSwap;
- trace('toggled? ' + preferences.get(prefName));
-
- switch (prefName)
- {
- case 'fps-counter':
- if (getPref('fps-counter')) FlxG.stage.addChild(Main.fpsCounter);
- else
- FlxG.stage.removeChild(Main.fpsCounter);
- case 'auto-pause':
- FlxG.autoPause = getPref('auto-pause');
- }
-
- if (prefName == 'fps-counter') {}
+ preferenceItems.add(checkbox);
}
override function update(elapsed:Float)
{
super.update(elapsed);
- // menuCamera.followLerp = CoolUtil.camLerpShit(0.05);
-
+ // Indent the selected item.
+ // TODO: Only do this on menu change?
items.forEach(function(daItem:TextMenuItem) {
if (items.selectedItem == daItem) daItem.x = 150;
else
daItem.x = 120;
});
}
-
- static function preferenceCheck(prefString:String, defaultValue:Dynamic):Void
- {
- if (preferences.get(prefString) == null)
- {
- // Set the value to default.
- preferences.set(prefString, defaultValue);
- trace('Set preference to default: ${prefString} = ${defaultValue}');
- }
- else
- {
- trace('Found preference: ${prefString} = ${preferences.get(prefString)}');
- }
- }
}
-class CheckboxThingie extends FlxSprite
+class CheckboxPreferenceItem extends FlxSprite
{
- public var daValue(default, set):Bool;
+ public var currentValue(default, set):Bool;
- public function new(x:Float, y:Float, daValue:Bool = false)
+ public function new(x:Float, y:Float, defaultValue:Bool = false)
{
super(x, y);
@@ -180,7 +110,7 @@ class CheckboxThingie extends FlxSprite
setGraphicSize(Std.int(width * 0.7));
updateHitbox();
- this.daValue = daValue;
+ this.currentValue = defaultValue;
}
override function update(elapsed:Float)
@@ -196,12 +126,17 @@ class CheckboxThingie extends FlxSprite
}
}
- function set_daValue(value:Bool):Bool
+ function set_currentValue(value:Bool):Bool
{
- if (value) animation.play('checked', true);
+ if (value)
+ {
+ animation.play('checked', true);
+ }
else
+ {
animation.play('static');
+ }
- return value;
+ return currentValue = value;
}
}
diff --git a/source/funkin/ui/StickerSubState.hx b/source/funkin/ui/StickerSubState.hx
index 067f50c31..a4e3a6acb 100644
--- a/source/funkin/ui/StickerSubState.hx
+++ b/source/funkin/ui/StickerSubState.hx
@@ -17,6 +17,9 @@ import openfl.geom.Matrix;
import openfl.display.Sprite;
import openfl.display.Bitmap;
+using Lambda;
+using StringTools;
+
class StickerSubState extends MusicBeatSubState
{
public var grpStickers:FlxTypedGroup;
@@ -26,10 +29,60 @@ class StickerSubState extends MusicBeatSubState
var nextState:NEXTSTATE = FREEPLAY;
+ // what "folders" to potentially load from (as of writing only "keys" exist)
+ var soundSelections:Array = [];
+ // what "folder" was randomly selected
+ var soundSelection:String = "";
+ var sounds:Array = [];
+
public function new(?oldStickers:Array, ?nextState:NEXTSTATE = FREEPLAY):Void
{
super();
+ // todo still
+ // make sure that ONLY plays mp3/ogg files
+ // if there's no mp3/ogg file, then it regenerates/reloads the random folder
+
+ var assetsInList = openfl.utils.Assets.list();
+
+ var soundFilterFunc = function(a:String) {
+ return a.startsWith('assets/shared/sounds/stickersounds/');
+ };
+
+ soundSelections = assetsInList.filter(soundFilterFunc);
+ soundSelections = soundSelections.map(function(a:String) {
+ return a.replace('assets/shared/sounds/stickersounds/', '').split('/')[0];
+ });
+
+ // cracked cleanup... yuchh...
+ for (i in soundSelections)
+ {
+ while (soundSelections.contains(i))
+ {
+ soundSelections.remove(i);
+ }
+ soundSelections.push(i);
+ }
+
+ trace(soundSelections);
+
+ soundSelection = FlxG.random.getObject(soundSelections);
+
+ var filterFunc = function(a:String) {
+ return a.startsWith('assets/shared/sounds/stickersounds/' + soundSelection + '/');
+ };
+ var assetsInList3 = openfl.utils.Assets.list();
+ sounds = assetsInList3.filter(filterFunc);
+ for (i in 0...sounds.length)
+ {
+ sounds[i] = sounds[i].replace('assets/shared/sounds/', '');
+ sounds[i] = sounds[i].substring(0, sounds[i].lastIndexOf('.'));
+ }
+
+ trace(sounds);
+
+ // trace(assetsInList);
+
this.nextState = nextState;
grpStickers = new FlxTypedGroup();
@@ -66,8 +119,10 @@ class StickerSubState extends MusicBeatSubState
{
new FlxTimer().start(sticker.timing, _ -> {
sticker.visible = false;
+ 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();
@@ -151,7 +206,11 @@ 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));
var frameTimer:Int = FlxG.random.int(0, 2);
@@ -212,10 +271,10 @@ class StickerSubState extends MusicBeatSubState
{
super.update(elapsed);
- if (FlxG.keys.justPressed.ANY)
- {
- regenStickers();
- }
+ // if (FlxG.keys.justPressed.ANY)
+ // {
+ // regenStickers();
+ // }
}
var switchingState:Bool = false;
diff --git a/source/funkin/ui/TextMenuList.hx b/source/funkin/ui/TextMenuList.hx
index 0c9f9eb8b..521f46faf 100644
--- a/source/funkin/ui/TextMenuList.hx
+++ b/source/funkin/ui/TextMenuList.hx
@@ -10,7 +10,7 @@ class TextMenuList extends MenuTypedList
super(navControls, wrapMode);
}
- public function createItem(x = 0.0, y = 0.0, name:String, font:AtlasFont = BOLD, callback, fireInstantly = false)
+ public function createItem(x = 0.0, y = 0.0, name:String, font:AtlasFont = BOLD, ?callback:Void->Void, fireInstantly = false)
{
var item = new TextMenuItem(x, y, name, font, callback);
item.fireInstantly = fireInstantly;
@@ -20,7 +20,7 @@ class TextMenuList extends MenuTypedList
class TextMenuItem extends TextTypedMenuItem
{
- public function new(x = 0.0, y = 0.0, name:String, font:AtlasFont = BOLD, callback)
+ public function new(x = 0.0, y = 0.0, name:String, font:AtlasFont = BOLD, ?callback:Void->Void)
{
super(x, y, new AtlasText(0, 0, name, font), name, callback);
setEmptyBackground();
@@ -29,7 +29,7 @@ class TextMenuItem extends TextTypedMenuItem
class TextTypedMenuItem extends MenuTypedItem
{
- public function new(x = 0.0, y = 0.0, label:T, name:String, callback)
+ public function new(x = 0.0, y = 0.0, label:T, name:String, ?callback:Void->Void)
{
super(x, y, label, name, callback);
}
diff --git a/source/funkin/ui/debug/charting/ChartEditorAudioHandler.hx b/source/funkin/ui/debug/charting/ChartEditorAudioHandler.hx
index e852dff0a..b5a6f36be 100644
--- a/source/funkin/ui/debug/charting/ChartEditorAudioHandler.hx
+++ b/source/funkin/ui/debug/charting/ChartEditorAudioHandler.hx
@@ -1,11 +1,14 @@
package funkin.ui.debug.charting;
-import openfl.utils.Assets;
import flixel.system.FlxAssets.FlxSoundAsset;
import flixel.system.FlxSound;
-import funkin.play.character.BaseCharacter.CharacterType;
import flixel.system.FlxSound;
+import funkin.audio.VoicesGroup;
+import funkin.play.character.BaseCharacter.CharacterType;
+import funkin.util.FileUtil;
+import haxe.io.Bytes;
import haxe.io.Path;
+import openfl.utils.Assets;
/**
* Functions for loading audio for the chart editor.
@@ -17,16 +20,18 @@ import haxe.io.Path;
class ChartEditorAudioHandler
{
/**
- * Loads a vocal track from an absolute file path.
+ * Loads and stores byte data for a vocal track from an absolute file path
+ *
* @param path The absolute path to the audio file.
- * @param charKey The character to load the vocal track for.
+ * @param charId The character this vocal track will be for.
+ * @param instId The instrumental this vocal track will be for.
* @return Success or failure.
*/
- static function loadVocalsFromPath(state:ChartEditorState, path:Path, charKey:String = 'default'):Bool
+ static function loadVocalsFromPath(state:ChartEditorState, path:Path, charId:String, instId:String = ''):Bool
{
#if sys
- var fileBytes:haxe.io.Bytes = sys.io.File.getBytes(path.toString());
- return loadVocalsFromBytes(state, fileBytes, charKey);
+ var fileBytes:Bytes = sys.io.File.getBytes(path.toString());
+ return loadVocalsFromBytes(state, fileBytes, charId, instId);
#else
trace("[WARN] This platform can't load audio from a file path, you'll need to fetch the bytes some other way.");
return false;
@@ -34,137 +39,235 @@ class ChartEditorAudioHandler
}
/**
- * Load a vocal track for a given song and character and add it to the voices group.
+ * Loads and stores byte data for a vocal track from an asset
*
- * @param path ID of the asset.
- * @param charKey Character to load the vocal track for.
+ * @param path The path to the asset. Use `Paths` to build this.
+ * @param charId The character this vocal track will be for.
+ * @param instId The instrumental this vocal track will be for.
* @return Success or failure.
*/
- static function loadVocalsFromAsset(state:ChartEditorState, path:String, charType:CharacterType = OTHER):Bool
+ static function loadVocalsFromAsset(state:ChartEditorState, path:String, charId:String, instId:String = ''):Bool
{
- var vocalTrack:FlxSound = FlxG.sound.load(path, 1.0, false);
+ var trackData:Null = Assets.getBytes(path);
+ if (trackData != null)
+ {
+ return loadVocalsFromBytes(state, trackData, charId, instId);
+ }
+ return false;
+ }
+
+ /**
+ * Loads and stores byte data for a vocal track
+ *
+ * @param bytes The audio byte data.
+ * @param charId The character this vocal track will be for.
+ * @param instId The instrumental this vocal track will be for.
+ */
+ static function loadVocalsFromBytes(state:ChartEditorState, bytes:Bytes, charId:String, instId:String = ''):Bool
+ {
+ var trackId:String = '${charId}${instId == '' ? '' : '-${instId}'}';
+ state.audioVocalTrackData.set(trackId, bytes);
+ return true;
+ }
+
+ /**
+ * Loads and stores byte data for an instrumental track from an absolute file path
+ *
+ * @param path The absolute path to the audio file.
+ * @param instId The instrumental this vocal track will be for.
+ * @return Success or failure.
+ */
+ static function loadInstFromPath(state:ChartEditorState, path:Path, instId:String = ''):Bool
+ {
+ #if sys
+ var fileBytes:Bytes = sys.io.File.getBytes(path.toString());
+ return loadInstFromBytes(state, fileBytes, instId);
+ #else
+ trace("[WARN] This platform can't load audio from a file path, you'll need to fetch the bytes some other way.");
+ return false;
+ #end
+ }
+
+ /**
+ * Loads and stores byte data for an instrumental track from an asset
+ *
+ * @param path The path to the asset. Use `Paths` to build this.
+ * @param instId The instrumental this vocal track will be for.
+ * @return Success or failure.
+ */
+ static function loadInstFromAsset(state:ChartEditorState, path:String, instId:String = ''):Bool
+ {
+ var trackData:Null = Assets.getBytes(path);
+ if (trackData != null)
+ {
+ return loadInstFromBytes(state, trackData, instId);
+ }
+ return false;
+ }
+
+ /**
+ * Loads and stores byte data for a vocal track
+ *
+ * @param bytes The audio byte data.
+ * @param charId The character this vocal track will be for.
+ * @param instId The instrumental this vocal track will be for.
+ */
+ static function loadInstFromBytes(state:ChartEditorState, bytes:Bytes, instId:String = ''):Bool
+ {
+ if (instId == '') instId = 'default';
+ state.audioInstTrackData.set(instId, bytes);
+ return true;
+ }
+
+ public static function switchToInstrumental(state:ChartEditorState, instId:String = '', playerId:String, opponentId:String):Bool
+ {
+ var result:Bool = playInstrumental(state, instId);
+ if (!result) return false;
+
+ stopExistingVocals(state);
+ result = playVocals(state, BF, playerId, instId);
+ if (!result) return false;
+ result = playVocals(state, DAD, opponentId, instId);
+ if (!result) return false;
+
+ return true;
+ }
+
+ /**
+ * Tell the Chart Editor to select a specific instrumental track, that is already loaded.
+ */
+ static function playInstrumental(state:ChartEditorState, instId:String = ''):Bool
+ {
+ if (instId == '') instId = 'default';
+ var instTrackData:Null = state.audioInstTrackData.get(instId);
+ var instTrack:Null = buildFlxSoundFromBytes(instTrackData);
+ if (instTrack == null) return false;
+
+ stopExistingInstrumental(state);
+ state.audioInstTrack = instTrack;
+ state.postLoadInstrumental();
+ return true;
+ }
+
+ static function stopExistingInstrumental(state:ChartEditorState):Void
+ {
+ if (state.audioInstTrack != null)
+ {
+ state.audioInstTrack.stop();
+ state.audioInstTrack.destroy();
+ state.audioInstTrack = null;
+ }
+ }
+
+ /**
+ * Tell the Chart Editor to select a specific vocal track, that is already loaded.
+ */
+ static function playVocals(state:ChartEditorState, charType:CharacterType, charId:String, instId:String = ''):Bool
+ {
+ var trackId:String = '${charId}${instId == '' ? '' : '-${instId}'}';
+ var vocalTrackData:Null = state.audioVocalTrackData.get(trackId);
+ var vocalTrack:Null = buildFlxSoundFromBytes(vocalTrackData);
+
+ if (state.audioVocalTrackGroup == null) state.audioVocalTrackGroup = new VoicesGroup();
+
if (vocalTrack != null)
{
switch (charType)
{
- case CharacterType.BF:
- if (state.audioVocalTrackGroup != null) state.audioVocalTrackGroup.addPlayerVoice(vocalTrack);
- state.audioVocalTrackData.set(state.currentSongCharacterPlayer, Assets.getBytes(path));
- case CharacterType.DAD:
- if (state.audioVocalTrackGroup != null) state.audioVocalTrackGroup.addOpponentVoice(vocalTrack);
- state.audioVocalTrackData.set(state.currentSongCharacterOpponent, Assets.getBytes(path));
+ case BF:
+ state.audioVocalTrackGroup.addPlayerVoice(vocalTrack);
+ return true;
+ case DAD:
+ state.audioVocalTrackGroup.addOpponentVoice(vocalTrack);
+ return true;
+ case OTHER:
+ state.audioVocalTrackGroup.add(vocalTrack);
+ return true;
default:
- if (state.audioVocalTrackGroup != null) state.audioVocalTrackGroup.add(vocalTrack);
- state.audioVocalTrackData.set('default', Assets.getBytes(path));
+ // Do nothing.
}
-
- return true;
}
return false;
}
- /**
- * Loads a vocal track from audio byte data.
- */
- static function loadVocalsFromBytes(state:ChartEditorState, bytes:haxe.io.Bytes, charKey:String = ''):Bool
+ static function stopExistingVocals(state:ChartEditorState):Void
{
- var openflSound:openfl.media.Sound = new openfl.media.Sound();
- openflSound.loadCompressedDataFromByteArray(openfl.utils.ByteArray.fromBytes(bytes), bytes.length);
- var vocalTrack:FlxSound = FlxG.sound.load(openflSound, 1.0, false);
- if (state.audioVocalTrackGroup != null) state.audioVocalTrackGroup.add(vocalTrack);
- state.audioVocalTrackData.set(charKey, bytes);
- return true;
- }
-
- /**
- * Loads an instrumental from an absolute file path, replacing the current instrumental.
- *
- * @param path The absolute path to the audio file.
- *
- * @return Success or failure.
- */
- static function loadInstrumentalFromPath(state:ChartEditorState, path:Path):Bool
- {
- #if sys
- // Validate file extension.
- if (path.ext != null && !ChartEditorState.SUPPORTED_MUSIC_FORMATS.contains(path.ext))
+ if (state.audioVocalTrackGroup != null)
{
- return false;
+ state.audioVocalTrackGroup.clear();
}
-
- var fileBytes:haxe.io.Bytes = sys.io.File.getBytes(path.toString());
- return loadInstrumentalFromBytes(state, fileBytes, '${path.file}.${path.ext}');
- #else
- trace("[WARN] This platform can't load audio from a file path, you'll need to fetch the bytes some other way.");
- return false;
- #end
- }
-
- /**
- * Loads an instrumental from audio byte data, replacing the current instrumental.
- * @param bytes The audio byte data.
- * @param fileName The name of the file, if available. Used for notifications.
- * @return Success or failure.
- */
- static function loadInstrumentalFromBytes(state:ChartEditorState, bytes:haxe.io.Bytes, fileName:String = null):Bool
- {
- if (bytes == null)
- {
- return false;
- }
-
- var openflSound:openfl.media.Sound = new openfl.media.Sound();
- openflSound.loadCompressedDataFromByteArray(openfl.utils.ByteArray.fromBytes(bytes), bytes.length);
- state.audioInstTrack = FlxG.sound.load(openflSound, 1.0, false);
- state.audioInstTrack.autoDestroy = false;
- state.audioInstTrack.pause();
-
- state.audioInstTrackData = bytes;
-
- state.postLoadInstrumental();
-
- return true;
- }
-
- /**
- * Loads an instrumental from an OpenFL asset, replacing the current instrumental.
- * @param path The path to the asset. Use `Paths` to build this.
- * @return Success or failure.
- */
- static function loadInstrumentalFromAsset(state:ChartEditorState, path:String):Bool
- {
- var instTrack:FlxSound = FlxG.sound.load(path, 1.0, false);
- if (instTrack != null)
- {
- state.audioInstTrack = instTrack;
-
- state.audioInstTrackData = Assets.getBytes(path);
-
- state.postLoadInstrumental();
- return true;
- }
-
- return false;
}
/**
* Play a sound effect.
* Automatically cleans up after itself and recycles previous FlxSound instances if available, for performance.
+ * @param path The path to the sound effect. Use `Paths` to build this.
*/
public static function playSound(path:String):Void
{
var snd:FlxSound = FlxG.sound.list.recycle(FlxSound) ?? new FlxSound();
-
var asset:Null = FlxG.sound.cache(path);
if (asset == null)
{
trace('WARN: Failed to play sound $path, asset not found.');
return;
}
-
snd.loadEmbedded(asset);
snd.autoDestroy = true;
FlxG.sound.list.add(snd);
snd.play();
}
+
+ /**
+ * Convert byte data into a playable sound.
+ *
+ * @param input The byte data.
+ * @return The playable sound, or `null` if loading failed.
+ */
+ public static function buildFlxSoundFromBytes(input:Null):Null
+ {
+ if (input == null) return null;
+
+ var openflSound:openfl.media.Sound = new openfl.media.Sound();
+ openflSound.loadCompressedDataFromByteArray(openfl.utils.ByteArray.fromBytes(input), input.length);
+ var output:FlxSound = FlxG.sound.load(openflSound, 1.0, false);
+ return output;
+ }
+
+ static function makeZIPEntriesFromInstrumentals(state:ChartEditorState):Array
+ {
+ var zipEntries = [];
+
+ for (key in state.audioInstTrackData.keys())
+ {
+ if (key == 'default')
+ {
+ var data:Null = state.audioInstTrackData.get('default');
+ if (data == null) continue;
+ zipEntries.push(FileUtil.makeZIPEntryFromBytes('Inst.ogg', data));
+ }
+ else
+ {
+ var data:Null = state.audioInstTrackData.get(key);
+ if (data == null) continue;
+ zipEntries.push(FileUtil.makeZIPEntryFromBytes('Inst-${key}.ogg', data));
+ }
+ }
+
+ return zipEntries;
+ }
+
+ static function makeZIPEntriesFromVocals(state:ChartEditorState):Array
+ {
+ var zipEntries = [];
+
+ for (key in state.audioVocalTrackData.keys())
+ {
+ var data:Null = state.audioVocalTrackData.get(key);
+ if (data == null) continue;
+ zipEntries.push(FileUtil.makeZIPEntryFromBytes('Vocals-${key}.ogg', data));
+ }
+
+ return zipEntries;
+ }
}
diff --git a/source/funkin/ui/debug/charting/ChartEditorCommand.hx b/source/funkin/ui/debug/charting/ChartEditorCommand.hx
index c358c1d3d..3328336e6 100644
--- a/source/funkin/ui/debug/charting/ChartEditorCommand.hx
+++ b/source/funkin/ui/debug/charting/ChartEditorCommand.hx
@@ -1,5 +1,7 @@
package funkin.ui.debug.charting;
+import haxe.ui.notifications.NotificationType;
+import haxe.ui.notifications.NotificationManager;
import funkin.data.song.SongData.SongEventData;
import funkin.data.song.SongData.SongNoteData;
import funkin.data.song.SongDataUtils;
@@ -760,6 +762,22 @@ class PasteItemsCommand implements ChartEditorCommand
{
var currentClipboard:SongClipboardItems = SongDataUtils.readItemsFromClipboard();
+ if (currentClipboard.valid != true)
+ {
+ #if !mac
+ NotificationManager.instance.addNotification(
+ {
+ title: 'Failed to Paste',
+ body: 'Could not parse clipboard contents.',
+ type: NotificationType.Error,
+ expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME
+ });
+ #end
+ return;
+ }
+
+ trace(currentClipboard.notes);
+
addedNotes = SongDataUtils.offsetSongNoteData(currentClipboard.notes, Std.int(targetTimestamp));
addedEvents = SongDataUtils.offsetSongEventData(currentClipboard.events, Std.int(targetTimestamp));
@@ -773,6 +791,16 @@ class PasteItemsCommand implements ChartEditorCommand
state.notePreviewDirty = true;
state.sortChartData();
+
+ #if !mac
+ NotificationManager.instance.addNotification(
+ {
+ title: 'Paste Successful',
+ body: 'Successfully pasted clipboard contents.',
+ type: NotificationType.Success,
+ expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME
+ });
+ #end
}
public function undo(state:ChartEditorState):Void
diff --git a/source/funkin/ui/debug/charting/ChartEditorDialogHandler.hx b/source/funkin/ui/debug/charting/ChartEditorDialogHandler.hx
index 736851d16..30f0381c6 100644
--- a/source/funkin/ui/debug/charting/ChartEditorDialogHandler.hx
+++ b/source/funkin/ui/debug/charting/ChartEditorDialogHandler.hx
@@ -83,7 +83,7 @@ class ChartEditorDialogHandler
var dialog:Null