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 = openDialog(state, CHART_EDITOR_DIALOG_WELCOME_LAYOUT, true, closable); if (dialog == null) throw 'Could not locate Welcome dialog'; - // Add handlers to the "Create From Song" section. + // Create New Song "Easy/Normal/Hard" var linkCreateBasic:Null = dialog.findComponent('splashCreateFromSongBasic', Link); if (linkCreateBasic == null) throw 'Could not locate splashCreateFromSongBasic link in Welcome dialog'; linkCreateBasic.onClick = function(_event) { @@ -94,7 +94,20 @@ class ChartEditorDialogHandler // // Create Song Wizard // - openCreateSongWizard(state, false); + openCreateSongWizardBasic(state, false); + } + + // Create New Song "Erect/Nightmare" + var linkCreateErect:Null = dialog.findComponent('splashCreateFromSongErect', Link); + if (linkCreateErect == null) throw 'Could not locate splashCreateFromSongErect link in Welcome dialog'; + linkCreateErect.onClick = function(_event) { + // Hide the welcome dialog + dialog.hideDialog(DialogButton.CANCEL); + + // + // Create Song Wizard + // + openCreateSongWizardErect(state, false); } var linkImportChartLegacy:Null = dialog.findComponent('splashImportChartLegacy', Link); @@ -237,34 +250,112 @@ class ChartEditorDialogHandler }; } - public static function openCreateSongWizard(state:ChartEditorState, closable:Bool):Void + public static function openCreateSongWizardBasic(state:ChartEditorState, closable:Bool):Void { - // Step 1. Upload Instrumental - var uploadInstDialog:Dialog = openUploadInstDialog(state, closable); - uploadInstDialog.onDialogClosed = function(_event) { + // Step 1. Song Metadata + var songMetadataDialog:Dialog = openSongMetadataDialog(state); + songMetadataDialog.onDialogClosed = function(_event) { state.isHaxeUIDialogOpen = false; if (_event.button == DialogButton.APPLY) { - // Step 2. Song Metadata - var songMetadataDialog:Dialog = openSongMetadataDialog(state); - songMetadataDialog.onDialogClosed = function(_event) { + // Step 2. Upload Instrumental + var uploadInstDialog:Dialog = openUploadInstDialog(state, closable); + uploadInstDialog.onDialogClosed = function(_event) { state.isHaxeUIDialogOpen = false; if (_event.button == DialogButton.APPLY) { // Step 3. Upload Vocals // NOTE: Uploading vocals is optional, so we don't need to check if the user cancelled the wizard. - openUploadVocalsDialog(state, false); // var uploadVocalsDialog:Dialog + var uploadVocalsDialog:Dialog = openUploadVocalsDialog(state, closable); // var uploadVocalsDialog:Dialog + uploadVocalsDialog.onDialogClosed = function(_event) { + state.isHaxeUIDialogOpen = false; + state.switchToCurrentInstrumental(); + state.postLoadInstrumental(); + } } else { - // User cancelled the wizard! Back to the welcome dialog. + // User cancelled the wizard at Step 2! Back to the welcome dialog. openWelcomeDialog(state); } }; } else { - // User cancelled the wizard! Back to the welcome dialog. + // User cancelled the wizard at Step 1! Back to the welcome dialog. + openWelcomeDialog(state); + } + }; + } + + public static function openCreateSongWizardErect(state:ChartEditorState, closable:Bool):Void + { + // Step 1. Song Metadata + var songMetadataDialog:Dialog = openSongMetadataDialog(state); + songMetadataDialog.onDialogClosed = function(_event) { + state.isHaxeUIDialogOpen = false; + if (_event.button == DialogButton.APPLY) + { + // Step 2. Upload Instrumental + var uploadInstDialog:Dialog = openUploadInstDialog(state, closable); + uploadInstDialog.onDialogClosed = function(_event) { + state.isHaxeUIDialogOpen = false; + if (_event.button == DialogButton.APPLY) + { + // Step 3. Upload Vocals + // NOTE: Uploading vocals is optional, so we don't need to check if the user cancelled the wizard. + var uploadVocalsDialog:Dialog = openUploadVocalsDialog(state, closable); // var uploadVocalsDialog:Dialog + uploadVocalsDialog.onDialogClosed = function(_event) { + state.switchToCurrentInstrumental(); + // Step 4. Song Metadata (Erect) + var songMetadataDialogErect:Dialog = openSongMetadataDialog(state, 'erect'); + songMetadataDialogErect.onDialogClosed = function(_event) { + state.isHaxeUIDialogOpen = false; + if (_event.button == DialogButton.APPLY) + { + // Switch to the Erect variation so uploading the instrumental applies properly. + state.selectedVariation = 'erect'; + + // Step 5. Upload Instrumental (Erect) + var uploadInstDialogErect:Dialog = openUploadInstDialog(state, closable); + uploadInstDialogErect.onDialogClosed = function(_event) { + state.isHaxeUIDialogOpen = false; + if (_event.button == DialogButton.APPLY) + { + // Step 6. Upload Vocals (Erect) + // NOTE: Uploading vocals is optional, so we don't need to check if the user cancelled the wizard. + var uploadVocalsDialogErect:Dialog = openUploadVocalsDialog(state, closable); // var uploadVocalsDialog:Dialog + uploadVocalsDialogErect.onDialogClosed = function(_event) { + state.isHaxeUIDialogOpen = false; + state.switchToCurrentInstrumental(); + state.postLoadInstrumental(); + } + } + else + { + // User cancelled the wizard at Step 5! Back to the welcome dialog. + openWelcomeDialog(state); + } + }; + } + else + { + // User cancelled the wizard at Step 4! Back to the welcome dialog. + openWelcomeDialog(state); + } + } + } + } + else + { + // User cancelled the wizard at Step 2! Back to the welcome dialog. + openWelcomeDialog(state); + } + }; + } + else + { + // User cancelled the wizard at Step 1! Back to the welcome dialog. openWelcomeDialog(state); } }; @@ -302,6 +393,8 @@ class ChartEditorDialogHandler Cursor.cursorMode = Default; } + var instId:String = state.currentInstrumentalId; + var onDropFile:String->Void; instrumentalBox.onClick = function(_event) { @@ -309,14 +402,14 @@ class ChartEditorDialogHandler {label: 'Audio File (.ogg)', extension: 'ogg'}], function(selectedFile:SelectedFileInfo) { if (selectedFile != null && selectedFile.bytes != null) { - if (ChartEditorAudioHandler.loadInstrumentalFromBytes(state, selectedFile.bytes)) + if (ChartEditorAudioHandler.loadInstFromBytes(state, selectedFile.bytes, instId)) { trace('Selected file: ' + selectedFile.fullPath); #if !mac NotificationManager.instance.addNotification( { title: 'Success', - body: 'Loaded instrumental track (${selectedFile.name})', + body: 'Loaded instrumental track (${selectedFile.name}) for variation (${state.selectedVariation})', type: NotificationType.Success, expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME }); @@ -333,7 +426,7 @@ class ChartEditorDialogHandler NotificationManager.instance.addNotification( { title: 'Failure', - body: 'Failed to load instrumental track (${selectedFile.name})', + body: 'Failed to load instrumental track (${selectedFile.name}) for variation (${state.selectedVariation})', type: NotificationType.Error, expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME }); @@ -346,14 +439,14 @@ class ChartEditorDialogHandler onDropFile = function(pathStr:String) { var path:Path = new Path(pathStr); trace('Dropped file (${path})'); - if (ChartEditorAudioHandler.loadInstrumentalFromPath(state, path)) + if (ChartEditorAudioHandler.loadInstFromPath(state, path, instId)) { // Tell the user the load was successful. #if !mac NotificationManager.instance.addNotification( { title: 'Success', - body: 'Loaded instrumental track (${path.file}.${path.ext})', + body: 'Loaded instrumental track (${path.file}.${path.ext}) for variation (${state.selectedVariation})', type: NotificationType.Success, expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME }); @@ -370,7 +463,7 @@ class ChartEditorDialogHandler } else { - 'Failed to load instrumental track (${path.file}.${path.ext})'; + 'Failed to load instrumental track (${path.file}.${path.ext}) for variation (${state.selectedVariation})'; } // Tell the user the load was successful. @@ -457,11 +550,18 @@ class ChartEditorDialogHandler * @return The dialog to open. */ @:haxe.warning("-WVarInit") - public static function openSongMetadataDialog(state:ChartEditorState):Dialog + public static function openSongMetadataDialog(state:ChartEditorState, ?targetVariation:String):Dialog { + if (targetVariation == null) targetVariation = Constants.DEFAULT_VARIATION; + var dialog:Null = openDialog(state, CHART_EDITOR_DIALOG_SONG_METADATA_LAYOUT, true, false); if (dialog == null) throw 'Could not locate Song Metadata dialog'; + if (targetVariation != Constants.DEFAULT_VARIATION) + { + dialog.title = 'New Chart - Provide Song Metadata (${targetVariation.toTitleCase()})'; + } + var buttonCancel:Null