From f790cd8fd60ed87c67dcc36715de46ce8bacada1 Mon Sep 17 00:00:00 2001
From: Eric Myllyoja <ericmyllyoja@gmail.com>
Date: Tue, 6 Sep 2022 00:59:54 -0400
Subject: [PATCH] WIP

---
 source/funkin/play/song/ScriptedSong.hx |   8 ++
 source/funkin/play/song/Song.hx         |  38 +++++++
 source/funkin/play/song/SongData.hx     | 129 ++++++++++++++++++++++++
 source/funkin/play/song/SongMitrator.hx |  12 +++
 4 files changed, 187 insertions(+)
 create mode 100644 source/funkin/play/song/ScriptedSong.hx
 create mode 100644 source/funkin/play/song/Song.hx
 create mode 100644 source/funkin/play/song/SongData.hx
 create mode 100644 source/funkin/play/song/SongMitrator.hx

diff --git a/source/funkin/play/song/ScriptedSong.hx b/source/funkin/play/song/ScriptedSong.hx
new file mode 100644
index 000000000..e89f68596
--- /dev/null
+++ b/source/funkin/play/song/ScriptedSong.hx
@@ -0,0 +1,8 @@
+package funkin.play.song;
+
+import polymod.hscript.HScriptedClass;
+
+@:hscriptClass
+class ScriptedSong extends Song implements HScriptedClass
+{
+}
diff --git a/source/funkin/play/song/Song.hx b/source/funkin/play/song/Song.hx
new file mode 100644
index 000000000..fb86f4e20
--- /dev/null
+++ b/source/funkin/play/song/Song.hx
@@ -0,0 +1,38 @@
+package funkin.play.song;
+
+/**
+ * This is a data structure managing information about the current song.
+ * This structure is created when the game starts, and includes all the data
+ * from the `metadata.json` file.
+ * It also includes the chart data, but only when this is the currently loaded song.
+ *
+ * It also receives script events; scripted classes which extend this class
+ * can be used to perform custom gameplay behaviors only on specific songs.
+ */
+class Song implements IPlayStateScriptedClass
+{
+	public var songId(default, null):String;
+
+	public var songName(get, null):String;
+
+	final _metadata:SongMetadata;
+	final _chartData:SongChartData;
+
+	public function new(id:String)
+	{
+		this.songId = songId;
+
+		_metadata = SongDataParser.parseSongMetadata(this.songId);
+		if (_metadata == null)
+		{
+			throw 'Could not find song data for songId: $songId';
+		}
+	}
+
+	function get_songName():String
+	{
+		if (_metadata == null)
+			return null;
+		return _metadata.name;
+	}
+}
diff --git a/source/funkin/play/song/SongData.hx b/source/funkin/play/song/SongData.hx
new file mode 100644
index 000000000..b4992992e
--- /dev/null
+++ b/source/funkin/play/song/SongData.hx
@@ -0,0 +1,129 @@
+package funkin.play.song;
+
+/**
+ * Contains utilities for loading and parsing stage data.
+ */
+class SongDataParser
+{
+	/**
+	 * The current version string for the stage data format.
+	 * Handle breaking changes by incrementing this value
+	 * and adding migration to the SongMigrator class.
+	 */
+	public static final CHART_VERSION:String = "2.0.0";
+
+	/**
+	 * A list containing all the songs available to the game.
+	 */
+	static final songCache:Map<String, Stage> = new Map<String, Stage>();
+
+	static final DEFAULT_SONG_ID = 'UNKNOWN';
+
+	/**
+	 * Parses and preloads the game's song metadata and scripts when the game starts.
+	 * 
+	 * If you want to force song metadata to be reloaded, you can just call this function again.
+	 */
+	public static function loadSongCache():Void
+	{
+		clearSongCache();
+		trace("[SONGDATA] Loading song cache...");
+
+		//
+		// SCRIPTED SONGS
+		//
+		var scriptedSongClassNames:Array<String> = ScriptedSong.listScriptClasses();
+		trace('  Instantiating ${scriptedSongClassNames.length} scripted songs...');
+		for (songCls in scriptedSongClassNames)
+		{
+			var song:Song = ScriptedSong.init(songCls, DEFAULT_SONG_ID);
+			if (song != null)
+			{
+				trace('    Loaded scripted song: ${song.songId}');
+				songCache.set(song.songId, song);
+			}
+			else
+			{
+				trace('    Failed to instantiate scripted song class: ${songCls}');
+			}
+		}
+
+		//
+		// UNSCRIPTED SONGS
+		//
+		var songIdList:Array<String> = DataAssets.listDataFilesInPath('songs/');
+		var unscriptedSongIds:Array<String> = songIdList.filter(function(songId:String):Bool
+		{
+			return !songCache.exists(songId);
+		});
+		trace('  Instantiating ${unscriptedSongIds.length} non-scripted songs...');
+		for (songId in unscriptedSongIds)
+		{
+			var song:Song;
+			try
+			{
+				stage = new Song(songId);
+				if (stage != null)
+				{
+					trace('    Loaded song data: ${song.songId}');
+					songCache.set(song.songId, song);
+				}
+			}
+			catch (e)
+			{
+				trace('    An error occurred while loading song data: ${songId}');
+				// Assume error was already logged.
+				continue;
+			}
+		}
+
+		trace('  Successfully loaded ${Lambda.count(songCache)} stages.');
+	}
+
+	/**
+	 * Retrieves a particular song from the cache.
+	 */
+	public static function fetchStage(songId:String):Null<Song>
+	{
+		if (songCache.exists(songId))
+		{
+			var song:Song = songCache.get(songId);
+			trace('[STAGEDATA] Successfully fetch song: ${songId}');
+			return song;
+		}
+		else
+		{
+			trace('[STAGEDATA] Failed to fetch song, not found in cache: ${songId}');
+			return null;
+		}
+	}
+
+	static function clearSongCache():Void
+	{
+		if (songCache != null)
+		{
+			for (song in songCache)
+			{
+				song.destroy();
+			}
+			songCache.clear();
+		}
+	}
+
+	public static function parseSongMetadata(songId:String):Null<SongMetadata>
+	{
+	}
+
+	static function loadSongMetadataFile(songPath:String, variant:String = ''):String
+	{
+		var songMetadataFilePath:String = Paths.json('stages/${stagePath}');
+		var rawJson = Assets.getText(stageFilePath).trim();
+
+		while (!rawJson.endsWith("}"))
+		{
+			rawJson = rawJson.substr(0, rawJson.length - 1);
+		}
+
+		return rawJson;
+	}
+}
diff --git a/source/funkin/play/song/SongMitrator.hx b/source/funkin/play/song/SongMitrator.hx
new file mode 100644
index 000000000..cad03b8ff
--- /dev/null
+++ b/source/funkin/play/song/SongMitrator.hx
@@ -0,0 +1,12 @@
+package funkin.play.song;
+
+class SongMigrator
+{
+	public static function migrateSongMetadata(song:Song, jsonData:Dynamic)
+	{
+	}
+
+	public static function migrateSongChart(song:Song, jsonData:Dynamic)
+	{
+	}
+}