From 19b2f3799a1c00206c032d8779aab7aa4534f288 Mon Sep 17 00:00:00 2001
From: Eric Myllyoja <ericmyllyoja@gmail.com>
Date: Thu, 8 Dec 2022 19:33:47 -0500
Subject: [PATCH] Chart wizard plus shader fixes

---
 hmm.json                                      |   4 +-
 source/funkin/Conductor.hx                    |   5 +-
 source/funkin/VoicesGroup.hx                  |  51 +++++-
 source/funkin/charting/ChartingState.hx       |   2 +-
 source/funkin/input/Cursor.hx                 |   2 +-
 source/funkin/play/PlayState.hx               |   6 +-
 source/funkin/play/song/Song.hx               |   7 +-
 source/funkin/play/song/SongData.hx           |   4 +-
 .../charting/ChartEditorDialogHandler.hx      |  30 +++-
 .../ui/debug/charting/ChartEditorState.hx     | 158 ++++++++++++++----
 10 files changed, 213 insertions(+), 56 deletions(-)

diff --git a/hmm.json b/hmm.json
index f4f8ddc7b..c8aa048ce 100644
--- a/hmm.json
+++ b/hmm.json
@@ -11,14 +11,14 @@
       "name": "flixel",
       "type": "git",
       "dir": null,
-      "ref": "6728df7",
+      "ref": "8ff2aa9",
       "url": "https://github.com/MasterEric/flixel"
     },
     {
       "name": "flixel-addons",
       "type": "git",
       "dir": null,
-      "ref": "a3877f0",
+      "ref": "157eaf3",
       "url": "https://github.com/MasterEric/flixel-addons"
     },
     {
diff --git a/source/funkin/Conductor.hx b/source/funkin/Conductor.hx
index 9a95a9863..378930f34 100644
--- a/source/funkin/Conductor.hx
+++ b/source/funkin/Conductor.hx
@@ -129,6 +129,7 @@ class Conductor
 	 */
 	public static function forceBPM(bpm:Float)
 	{
+		trace('[CONDUCTOR] Forcing BPM to ' + bpm);
 		Conductor.bpmOverride = bpm;
 	}
 
@@ -213,10 +214,8 @@ class Conductor
 		}
 	}
 
-	public static function mapTimeChanges(currentChart:SongDifficulty)
+	public static function mapTimeChanges(songTimeChanges:Array<SongTimeChange>)
 	{
-		var songTimeChanges:Array<SongTimeChange> = currentChart.timeChanges;
-
 		timeChanges = [];
 
 		for (currentTimeChange in songTimeChanges)
diff --git a/source/funkin/VoicesGroup.hx b/source/funkin/VoicesGroup.hx
index 306d82900..db31cb052 100644
--- a/source/funkin/VoicesGroup.hx
+++ b/source/funkin/VoicesGroup.hx
@@ -7,30 +7,38 @@ import flixel.system.FlxSound;
 // when needed
 class VoicesGroup extends FlxTypedGroup<FlxSound>
 {
-	public var time(default, set):Float = 0;
+	public var time(get, set):Float;
 
-	public var volume(default, set):Float = 1;
+	public var volume(get, set):Float;
 
-	public var pitch(default, set):Float = 1;
+	public var pitch(get, set):Float;
 
 	// make it a group that you add to?
-	public function new(song:String, ?files:Array<String> = null)
+	public function new()
 	{
 		super();
+	}
+
+	// TODO: Remove this.
+	public static function build(song:String, ?files:Array<String> = null):VoicesGroup
+	{
+		var result = new VoicesGroup();
 
 		if (files == null)
 		{
 			// Add an empty voice.
-			add(new FlxSound());
-			return;
+			result.add(new FlxSound());
+			return result;
 		}
 
 		for (sndFile in files)
 		{
 			var snd:FlxSound = new FlxSound().loadEmbedded(Paths.voices(song, '$sndFile'));
 			FlxG.sound.list.add(snd); // adds it to sound group for proper volumes
-			add(snd); // adds it to main group for other shit
+			result.add(snd); // adds it to main group for other shit
 		}
+
+		return result;
 	}
 
 	/**
@@ -83,6 +91,14 @@ class VoicesGroup extends FlxTypedGroup<FlxSound>
 		});
 	}
 
+	function get_time():Float
+	{
+		if (getFirstAlive() != null)
+			return getFirstAlive().time;
+		else
+			return 0;
+	}
+
 	function set_time(time:Float):Float
 	{
 		forEachAlive(function(snd)
@@ -94,6 +110,14 @@ class VoicesGroup extends FlxTypedGroup<FlxSound>
 		return time;
 	}
 
+	function get_volume():Float
+	{
+		if (getFirstAlive() != null)
+			return getFirstAlive().volume;
+		else
+			return 1;
+	}
+
 	// in PlayState, adjust the code so that it only mutes the player1 vocal tracks?
 	function set_volume(volume:Float):Float
 	{
@@ -105,9 +129,20 @@ class VoicesGroup extends FlxTypedGroup<FlxSound>
 		return volume;
 	}
 
+	function get_pitch():Float
+	{
+		#if FLX_PITCH
+		if (getFirstAlive() != null)
+			return getFirstAlive().pitch;
+		else
+		#end
+		return 1;
+	}
+
 	function set_pitch(val:Float):Float
 	{
-		#if HAS_PITCH
+		#if FLX_PITCH
+		trace('Setting audio pitch to ' + val);
 		forEachAlive(function(snd)
 		{
 			snd.pitch = val;
diff --git a/source/funkin/charting/ChartingState.hx b/source/funkin/charting/ChartingState.hx
index d51df7689..1b78c1d6a 100644
--- a/source/funkin/charting/ChartingState.hx
+++ b/source/funkin/charting/ChartingState.hx
@@ -445,7 +445,7 @@ class ChartingState extends MusicBeatState
 		add(playheadTest);
 
 		// WONT WORK FOR TUTORIAL OR TEST SONG!!! REDO LATER
-		vocals = new VoicesGroup(daSong, _song.voiceList);
+		vocals = VoicesGroup.build(daSong, _song.voiceList);
 		// vocals = new FlxSound().loadEmbedded(Paths.voices(daSong));
 		// FlxG.sound.list.add(vocals);
 
diff --git a/source/funkin/input/Cursor.hx b/source/funkin/input/Cursor.hx
index 6be47c919..50cdad079 100644
--- a/source/funkin/input/Cursor.hx
+++ b/source/funkin/input/Cursor.hx
@@ -35,7 +35,7 @@ class Cursor
 	static final CURSOR_GRABBING_PARAMS:CursorParams = {
 		graphic: "assets/images/cursor/cursor-grabbing.png",
 		scale: 1.0,
-		offsetX: 8,
+		offsetX: 32,
 		offsetY: 0,
 	};
 	static var assetCursorGrabbing:BitmapData = null;
diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx
index eb7f99fa8..ee8a9d3c3 100644
--- a/source/funkin/play/PlayState.hx
+++ b/source/funkin/play/PlayState.hx
@@ -354,7 +354,7 @@ class PlayState extends MusicBeatState
 
 		if (currentSong_NEW != null)
 		{
-			Conductor.mapTimeChanges(currentChart);
+			Conductor.mapTimeChanges(currentChart.timeChanges);
 			// Conductor.bpm = currentChart.getStartingBPM();
 
 			// TODO: Support for dialog.
@@ -1029,9 +1029,9 @@ class PlayState extends MusicBeatState
 		currentSong.song = currentSong.song;
 
 		if (currentSong.needsVoices)
-			vocals = new VoicesGroup(currentSong.song, currentSong.voiceList);
+			vocals = VoicesGroup.build(currentSong.song, currentSong.voiceList);
 		else
-			vocals = new VoicesGroup(currentSong.song, null);
+			vocals = VoicesGroup.build(currentSong.song, null);
 
 		vocals.members[0].onComplete = function()
 		{
diff --git a/source/funkin/play/song/Song.hx b/source/funkin/play/song/Song.hx
index b8ef2d7d6..1b24261f4 100644
--- a/source/funkin/play/song/Song.hx
+++ b/source/funkin/play/song/Song.hx
@@ -45,6 +45,11 @@ class Song // implements IPlayStateScriptedClass
 		populateFromMetadata();
 	}
 
+	public function getRawMetadata():Array<SongMetadata>
+	{
+		return _metadata;
+	}
+
 	/**
 	 * Populate the song data from the provided metadata,
 	 * including data from individual difficulties. Does not load chart data.
@@ -246,7 +251,7 @@ class SongDifficulty
 
 	public function buildVocals(charId:String = "bf"):VoicesGroup
 	{
-		var result:VoicesGroup = new VoicesGroup(this.song.songId, this.buildVoiceList());
+		var result:VoicesGroup = VoicesGroup.build(this.song.songId, this.buildVoiceList());
 		return result;
 	}
 }
diff --git a/source/funkin/play/song/SongData.hx b/source/funkin/play/song/SongData.hx
index e5679cab9..05c389b9e 100644
--- a/source/funkin/play/song/SongData.hx
+++ b/source/funkin/play/song/SongData.hx
@@ -96,12 +96,12 @@ class SongDataParser
 		if (songCache.exists(songId))
 		{
 			var song:Song = songCache.get(songId);
-			trace('[STAGEDATA] Successfully fetch song: ${songId}');
+			trace('[SONGDATA] Successfully fetch song: ${songId}');
 			return song;
 		}
 		else
 		{
-			trace('[STAGEDATA] Failed to fetch song, not found in cache: ${songId}');
+			trace('[SONGDATA] Failed to fetch song, not found in cache: ${songId}');
 			return null;
 		}
 	}
diff --git a/source/funkin/ui/debug/charting/ChartEditorDialogHandler.hx b/source/funkin/ui/debug/charting/ChartEditorDialogHandler.hx
index 3ff247e2b..7440b47aa 100644
--- a/source/funkin/ui/debug/charting/ChartEditorDialogHandler.hx
+++ b/source/funkin/ui/debug/charting/ChartEditorDialogHandler.hx
@@ -110,6 +110,25 @@ class ChartEditorDialogHandler
 		}
 
 		// TODO: Get the list of songs and insert them as links into the "Create From Song" section.
+		var linkTemplateDadBattle:Link = dialog.findComponent('splashTemplateDadBattle', Link);
+		linkTemplateDadBattle.onClick = (_event) ->
+		{
+			dialog.hideDialog(DialogButton.CANCEL);
+
+			// Load song from template
+			state.loadSongAsTemplate('dadbattle');
+		}
+		var linkTemplateBopeebo:Link = dialog.findComponent('splashTemplateBopeebo', Link);
+		linkTemplateBopeebo.onClick = (_event) ->
+		{
+			dialog.hideDialog(DialogButton.CANCEL);
+
+			// Load song from template
+			state.loadSongAsTemplate('bopeebo');
+		}
+
+		var splashTemplateContainer:VBox = dialog.findComponent('splashTemplateContainer', VBox);
+
 		return dialog;
 	}
 
@@ -237,7 +256,7 @@ class ChartEditorDialogHandler
 		var dialogBPM:NumberStepper = dialog.findComponent('dialogBPM', NumberStepper);
 		dialogBPM.onChange = (event:UIEvent) ->
 		{
-			if (event.value == null)
+			if (event.value == null || event.value <= 0)
 				return;
 
 			var timeChanges = state.currentSongMetadata.timeChanges;
@@ -249,6 +268,9 @@ class ChartEditorDialogHandler
 			{
 				timeChanges[0].bpm = event.value;
 			}
+
+			Conductor.forceBPM(event.value);
+
 			state.currentSongMetadata.timeChanges = timeChanges;
 		};
 
@@ -388,9 +410,9 @@ class ChartEditorDialogHandler
 				{
 					if (selectedFile != null)
 					{
-						trace('Selected file: ' + selectedFile);
-						vocalsEntryLabel.text = 'Vocals for $charName (click to browse)\n$selectedFile';
-						// state.loadVocalsFromBytes(selectedFile.bytes);
+						trace('Selected file: ' + selectedFile.name + "~" + selectedFile.fullPath);
+						vocalsEntryLabel.text = 'Vocals for $charName (click to browse)\n${selectedFile.name}';
+						state.loadVocalsFromBytes(selectedFile.bytes);
 						removeDropHandler(onDropFile);
 					}
 				});
diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx
index dd6f2d8fd..d349d9cdb 100644
--- a/source/funkin/ui/debug/charting/ChartEditorState.hx
+++ b/source/funkin/ui/debug/charting/ChartEditorState.hx
@@ -1,5 +1,7 @@
 package funkin.ui.debug.charting;
 
+import funkin.play.song.SongData.SongDataParser;
+import funkin.play.song.Song;
 import lime.media.AudioBuffer;
 import funkin.input.Cursor;
 import flixel.FlxSprite;
@@ -433,7 +435,7 @@ class ChartEditorState extends HaxeUIState
 	 * The audio track for the vocals.
 	 * TODO: Replace with a VocalSoundGroup.
 	 */
-	var audioVocalTrack:FlxSound;
+	var audioVocalTrackGroup:VoicesGroup;
 
 	/**
 	 * CHART DATA
@@ -758,7 +760,9 @@ class ChartEditorState extends HaxeUIState
 		// TODO: We should be loading the music later when the user requests it.
 		// loadDefaultMusic();
 
-		ChartEditorDialogHandler.openWelcomeDialog(this, false);
+		// TODO: Change to false.
+		var canCloseInitialDialog = true;
+		ChartEditorDialogHandler.openWelcomeDialog(this, canCloseInitialDialog);
 	}
 
 	function buildDefaultSongData()
@@ -771,6 +775,8 @@ class ChartEditorState extends HaxeUIState
 
 		// Initialize the song chart data.
 		songChartData = new Map<String, SongChartData>();
+
+		audioVocalTrackGroup = new VoicesGroup();
 	}
 
 	/**
@@ -933,7 +939,7 @@ class ChartEditorState extends HaxeUIState
 			playbarHeadDragging = true;
 
 			// If we were dragging the playhead while the song was playing, resume playing.
-			if (audioVocalTrack.playing)
+			if (audioInstTrack != null && audioInstTrack.playing)
 			{
 				playbarHeadDraggingWasPlaying = true;
 				stopAudioPlayback();
@@ -1057,16 +1063,35 @@ class ChartEditorState extends HaxeUIState
 		});
 		setUISelected('menubarItemMetronomeEnabled', shouldPlayMetronome);
 
+		var instVolumeLabel:Label = findComponent('menubarLabelVolumeInstrumental', Label);
 		addUIChangeListener('menubarItemVolumeInstrumental', (event:UIEvent) ->
 		{
 			var volume:Float = event.value / 100.0;
-			audioInstTrack.volume = volume;
+			if (audioInstTrack != null)
+				audioInstTrack.volume = volume;
+			instVolumeLabel.text = 'Instrumental - ${event.value}%';
 		});
 
+		var vocalsVolumeLabel:Label = findComponent('menubarLabelVolumeVocals', Label);
 		addUIChangeListener('menubarItemVolumeVocals', (event:UIEvent) ->
 		{
 			var volume:Float = event.value / 100.0;
-			audioVocalTrack.volume = volume;
+			if (audioVocalTrackGroup != null)
+				audioVocalTrackGroup.volume = volume;
+			vocalsVolumeLabel.text = 'Vocals - ${event.value}%';
+		});
+
+		var playbackSpeedLabel:Label = findComponent('menubarLabelPlaybackSpeed', Label);
+		addUIChangeListener('menubarItemPlaybackSpeed', (event:UIEvent) ->
+		{
+			var pitch = event.value * 2.0 / 100.0;
+			#if FLX_PITCH
+			if (audioInstTrack != null)
+				audioInstTrack.pitch = pitch;
+			if (audioVocalTrackGroup != null)
+				audioVocalTrackGroup.pitch = pitch;
+			#end
+			playbackSpeedLabel.text = 'Playback Speed - ${pitch}x';
 		});
 
 		addUIChangeListener('menubarItemToggleToolboxTools', (event:UIEvent) ->
@@ -2242,8 +2267,8 @@ class ChartEditorState extends HaxeUIState
 				var oldStepTime = Conductor.currentStepTime;
 				Conductor.update(audioInstTrack.time);
 				// Resync vocals.
-				if (Math.abs(audioInstTrack.time - audioVocalTrack.time) > 100)
-					audioVocalTrack.time = audioInstTrack.time;
+				if (Math.abs(audioInstTrack.time - audioVocalTrackGroup.time) > 100)
+					audioVocalTrackGroup.time = audioInstTrack.time;
 				var diffStepTime = Conductor.currentStepTime - oldStepTime;
 
 				// Move the playhead.
@@ -2257,8 +2282,8 @@ class ChartEditorState extends HaxeUIState
 
 				Conductor.update(audioInstTrack.time);
 				// Resync vocals.
-				if (audioVocalTrack != null && Math.abs(audioInstTrack.time - audioVocalTrack.time) > 100)
-					audioVocalTrack.time = audioInstTrack.time;
+				if (audioVocalTrackGroup != null && Math.abs(audioInstTrack.time - audioVocalTrackGroup.time) > 100)
+					audioVocalTrackGroup.time = audioInstTrack.time;
 
 				// We need time in fractional steps here to allow the song to actually play.
 				// Also account for a potentially offset playhead.
@@ -2281,16 +2306,20 @@ class ChartEditorState extends HaxeUIState
 	{
 		if (audioInstTrack != null)
 			audioInstTrack.play();
-		if (audioVocalTrack != null)
-			audioVocalTrack.play();
+		if (audioVocalTrackGroup != null)
+			audioVocalTrackGroup.play();
+		if (audioVocalTrackGroup != null)
+			audioVocalTrackGroup.play();
 	}
 
 	function stopAudioPlayback()
 	{
 		if (audioInstTrack != null)
 			audioInstTrack.pause();
-		if (audioVocalTrack != null)
-			audioVocalTrack.pause();
+		if (audioVocalTrackGroup != null)
+			audioVocalTrackGroup.pause();
+		if (audioVocalTrackGroup != null)
+			audioVocalTrackGroup.pause();
 	}
 
 	function toggleAudioPlayback()
@@ -2418,8 +2447,12 @@ class ChartEditorState extends HaxeUIState
 	 */
 	public function loadInstrumentalFromPath(path:String):Void
 	{
+		#if sys
 		var fileBytes:haxe.io.Bytes = sys.io.File.getBytes(path);
 		loadInstrumentalFromBytes(fileBytes);
+		#else
+		trace("[WARN] This platform can't load audio from a file path, you'll need to fetch the bytes some other way.");
+		#end
 	}
 
 	/**
@@ -2440,6 +2473,14 @@ class ChartEditorState extends HaxeUIState
 		postLoadInstrumental();
 	}
 
+	public function loadInstrumentalFromAsset(path:String):Void
+	{
+		var vocalTrack = FlxG.sound.load(path, 1.0, false);
+		audioInstTrack = vocalTrack;
+
+		postLoadInstrumental();
+	}
+
 	function postLoadInstrumental()
 	{
 		// Prevent the time from skipping back to 0 when the song ends.
@@ -2447,14 +2488,10 @@ class ChartEditorState extends HaxeUIState
 		{
 			if (audioInstTrack != null)
 				audioInstTrack.pause();
-			if (audioVocalTrack != null)
-				audioVocalTrack.pause();
+			if (audioVocalTrackGroup != null)
+				audioVocalTrackGroup.pause();
 		};
 
-		var DAD_BATTLE_BPM = 180;
-		var BOPEEBO_BPM = 100;
-		Conductor.forceBPM(DAD_BATTLE_BPM);
-
 		songLength = Std.int(Conductor.getTimeInSteps(audioInstTrack.length) * GRID_SIZE);
 
 		gridTiledSprite.height = songLength;
@@ -2470,20 +2507,79 @@ class ChartEditorState extends HaxeUIState
 	}
 
 	/**
-	 * Load a music track for playback.
+	 * Loads a vocal track from an absolute file path.
 	 */
-	function loadDefaultMusic()
+	public function loadVocalsFromPath(path:String):Void
 	{
-		// TODO: How to load music by selecting with a file dialog?
-		audioInstTrack = FlxG.sound.play(Paths.inst('dadbattle'), 1.0, false);
-		audioInstTrack.autoDestroy = false;
-		audioInstTrack.pause();
+		#if sys
+		var fileBytes:haxe.io.Bytes = sys.io.File.getBytes(path);
+		loadVocalsFromBytes(fileBytes);
+		#else
+		trace("[WARN] This platform can't load audio from a file path, you'll need to fetch the bytes some other way.");
+		#end
+	}
 
-		audioVocalTrack = FlxG.sound.play(Paths.voices('dadbattle'), 1.0, false);
-		audioVocalTrack.autoDestroy = false;
-		audioVocalTrack.pause();
+	public function loadVocalsFromAsset(path:String):Void
+	{
+		var vocalTrack = FlxG.sound.load(path, 1.0, false);
+		audioVocalTrackGroup.add(vocalTrack);
+	}
 
-		postLoadInstrumental();
+	/**
+	 * Loads a vocal track from audio byte data.
+	 */
+	public function loadVocalsFromBytes(bytes:haxe.io.Bytes):Void
+	{
+		var openflSound = new openfl.media.Sound();
+		openflSound.loadCompressedDataFromByteArray(openfl.utils.ByteArray.fromBytes(bytes), bytes.length);
+		var vocalTrack = FlxG.sound.load(openflSound, 1.0, false);
+		audioVocalTrackGroup.add(vocalTrack);
+
+		// Tell the user the load was successful.
+		// TODO: Un-bork this.
+		// showNotification('Loaded instrumental track successfully.');
+	}
+
+	/**
+	 * Fetch's a song's existing chart and audio and loads it, replacing the current song.
+	 */
+	public function loadSongAsTemplate(songId:String)
+	{
+		var song:Song = SongDataParser.fetchSong(songId);
+
+		if (song == null)
+		{
+			// showNotification('Failed to load song template.');
+			return;
+		}
+
+		// Load the song metadata.
+		var rawSongMetadata:Array<SongMetadata> = song.getRawMetadata();
+
+		this.songMetadata = new Map<String, SongMetadata>();
+
+		for (metadata in rawSongMetadata)
+		{
+			var variation = (metadata.variation == null || metadata.variation == '') ? 'default' : metadata.variation;
+			this.songMetadata.set(variation, metadata);
+		}
+
+		this.songChartData = new Map<String, SongChartData>();
+
+		for (metadata in rawSongMetadata)
+		{
+			var variation = (metadata.variation == null || metadata.variation == '') ? 'default' : metadata.variation;
+			this.songChartData.set(variation, SongDataParser.parseSongChartData(songId, metadata.variation));
+		}
+
+		Conductor.mapTimeChanges(currentSongMetadata.timeChanges);
+
+		loadInstrumentalFromAsset(Paths.inst(songId));
+		loadVocalsFromAsset(Paths.voices(songId));
+
+		// Apply BPM
+
+		// showNotification('Loaded song ${songId}.');
 	}
 
 	/**
@@ -2498,8 +2594,8 @@ class ChartEditorState extends HaxeUIState
 		// Update the songPosition in the audio tracks.
 		if (audioInstTrack != null)
 			audioInstTrack.time = scrollPositionInMs + playheadPositionInMs;
-		if (audioVocalTrack != null)
-			audioVocalTrack.time = scrollPositionInMs + playheadPositionInMs;
+		if (audioVocalTrackGroup != null)
+			audioVocalTrackGroup.time = scrollPositionInMs + playheadPositionInMs;
 
 		// We need to update the note sprites because we changed the scroll position.
 		noteDisplayDirty = true;