From 6d5c5f5acb874d04534104ebcb8a479ce027ac8c Mon Sep 17 00:00:00 2001
From: EliteMasterEric <ericmyllyoja@gmail.com>
Date: Mon, 26 Jun 2023 20:39:47 -0400
Subject: [PATCH] Refactor InitState plus fix a couple crash bugs

---
 source/Main.hx                           |  11 -
 source/funkin/InitState.hx               | 375 +++++++++++++----------
 source/funkin/PlayerSettings.hx          |  10 +-
 source/funkin/TitleState.hx              |  33 +-
 source/funkin/ui/story/StoryMenuState.hx |   2 +-
 5 files changed, 224 insertions(+), 207 deletions(-)

diff --git a/source/Main.hx b/source/Main.hx
index 006b54e18..9b70549ab 100644
--- a/source/Main.hx
+++ b/source/Main.hx
@@ -77,17 +77,6 @@ class Main extends Sprite
      * -Eric
      */
 
-    #if !debug
-    /**
-     * Someone was like "hey let's make a state that only runs code on debug builds"
-     * then put essential initialization code in it.
-     * The easiest fix is to make it run in all builds.
-     * -Eric
-     */
-    // TODO: Fix this properly.
-    // initialState = funkin.TitleState;
-    #end
-
     initHaxeUI();
 
     addChild(new FlxGame(gameWidth, gameHeight, initialState, framerate, framerate, skipSplash, startFullscreen));
diff --git a/source/funkin/InitState.hx b/source/funkin/InitState.hx
index 52bdb1015..4d74e1a05 100644
--- a/source/funkin/InitState.hx
+++ b/source/funkin/InitState.hx
@@ -6,52 +6,82 @@ import flixel.addons.transition.TransitionData;
 import flixel.graphics.FlxGraphic;
 import flixel.math.FlxPoint;
 import flixel.math.FlxRect;
+import flixel.FlxSprite;
 import flixel.system.debug.log.LogStyle;
 import flixel.util.FlxColor;
-import funkin.modding.module.ModuleHandler;
-import funkin.play.character.CharacterData.CharacterDataParser;
-import funkin.play.cutscene.dialogue.ConversationDataParser;
-import funkin.play.cutscene.dialogue.DialogueBoxDataParser;
-import funkin.play.cutscene.dialogue.SpeakerDataParser;
-import funkin.play.event.SongEventData.SongEventParser;
-import funkin.play.PlayState;
-import funkin.play.song.SongData.SongDataParser;
-import funkin.play.stage.StageData.StageDataParser;
 import funkin.ui.PreferencesMenu;
 import funkin.util.macro.MacroUtil;
 import funkin.util.WindowUtil;
+import funkin.play.PlayStatePlaylist;
 import openfl.display.BitmapData;
 #if discord_rpc
 import Discord.DiscordClient;
 #end
 
 /**
- * Initializes the game state using custom defines.
- * Only used in Debug builds.
+ * The initialization state has several functions:
+ * - Calls code to set up the game, including loading saves and parsing game data.
+ * - Chooses whether to start via debug or via launching normally.
  */
 class InitState extends FlxTransitionableState
 {
-  override public function create():Void
+  /**
+   * Perform a bunch of game setup, then immediately transition to the title screen.
+   */
+  public override function create():Void
+  {
+    setupShit();
+
+    loadSaveData();
+
+    startGame();
+  }
+
+  /**
+   * Setup a bunch of important Flixel stuff.
+   */
+  function setupShit()
   {
     //
-    // FLIXEL SETUP
+    // GAME SETUP
     //
+
+    // Setup window events (like callbacks for onWindowClose)
+    WindowUtil.initWindowEvents();
+    // Disable the thing on Windows where it tries to send a bug report to Microsoft because why do they care?
+    WindowUtil.disableCrashHandler();
+
     // This ain't a pixel art game! (most of the time)
     FlxSprite.defaultAntialiasing = true;
 
-    Application.current.onExit.add(function(exitCode) {
-      DiscordClient.shutdown();
-    });
-    #end
+    // Disable default keybinds for volume (we manually control volume in MusicBeatState with custom binds)
+    FlxG.sound.volumeUpKeys = [];
+    FlxG.sound.volumeDownKeys = [];
+    FlxG.sound.muteKeys = [];
 
-    // ==== flixel shit ==== //
+    // TODO: Make sure volume still saves/loads properly.
+    // if (FlxG.save.data.volume != null) FlxG.sound.volume = FlxG.save.data.volume;
+    // if (FlxG.save.data.mute != null) FlxG.sound.muted = FlxG.save.data.mute;
 
+    // Set the game to a lower frame rate while it is in the background.
+    FlxG.game.focusLostFramerate = 30;
+
+    //
+    // FLIXEL DEBUG SETUP
+    //
+    #if debug
+    // Disable using ~ to open the console (we use that for the Editor menu)
+    FlxG.debugger.toggleKeys = [F2];
+
+    // Adds an additional Close Debugger button.
     // This big obnoxious white button is for MOBILE, so that you can press it
     // easily with your finger when debug bullshit pops up during testing lol!
     FlxG.debugger.addButton(LEFT, new BitmapData(200, 200), function() {
       FlxG.debugger.visible = false;
     });
 
+    // Adds a red button to the debugger.
+    // This pauses the game AND the music! This ensures the Conductor stops.
     FlxG.debugger.addButton(CENTER, new BitmapData(20, 20, true, 0xFFCC2233), function() {
       if (FlxG.vcr.paused)
       {
@@ -77,7 +107,8 @@ class InitState extends FlxTransitionableState
       }
     });
 
-    #if FLX_DEBUG
+    // Adds a blue button to the debugger.
+    // This skips forward in the song.
     FlxG.debugger.addButton(CENTER, new BitmapData(20, 20, true, 0xFF2222CC), function() {
       FlxG.game.debugger.vcr.onStep();
 
@@ -90,175 +121,197 @@ class InitState extends FlxTransitionableState
       FlxG.sound.music.pause();
       FlxG.sound.music.time += FlxG.elapsed * 1000;
     });
+
+    // Make errors and warnings less annoying.
+    // TODO: Disable this so we know to fix warnings.
+    if (false)
+    {
+      LogStyle.ERROR.openConsole = false;
+      LogStyle.ERROR.errorSound = null;
+      LogStyle.WARNING.openConsole = false;
+      LogStyle.WARNING.errorSound = null;
+    }
     #end
 
-    FlxG.sound.muteKeys = [ZERO];
-    FlxG.game.focusLostFramerate = 60;
-
-    // FlxG.stage.window.borderless = true;
-    // FlxG.stage.window.mouseLock = true;
+    //
+    // FLIXEL TRANSITIONS
+    //
 
+    // Diamond Transition
     var diamond:FlxGraphic = FlxGraphic.fromClass(GraphicTransTileDiamond);
     diamond.persist = true;
     diamond.destroyOnNoUse = false;
 
-    FlxTransitionableState.defaultTransIn = new TransitionData(FADE, FlxColor.BLACK, 1, new FlxPoint(0, -1), {asset: diamond, width: 32, height: 32},
-      new FlxRect(-200, -200, FlxG.width * 1.4, FlxG.height * 1.4));
-    FlxTransitionableState.defaultTransOut = new TransitionData(FADE, FlxColor.BLACK, 0.7, new FlxPoint(0, 1), {asset: diamond, width: 32, height: 32},
-      new FlxRect(-200, -200, FlxG.width * 1.4, FlxG.height * 1.4));
-
-    // ===== save shit ===== //
-
-    FlxG.save.bind('funkin', 'ninjamuffin99');
-
-    // https://github.com/HaxeFlixel/flixel/pull/2396
-    // IF/WHEN MY PR GOES THRU AND IT GETS INTO MAIN FLIXEL, DELETE THIS CHUNKOF CODE, AND THEN UNCOMMENT THE LINE BELOW
-    // FlxG.sound.loadSavedPrefs();
-
-    if (FlxG.save.data.volume != null) FlxG.sound.volume = FlxG.save.data.volume;
-    if (FlxG.save.data.mute != null) FlxG.sound.muted = FlxG.save.data.mute;
-
-    // Make errors and warnings less annoying.
-    LogStyle.ERROR.openConsole = false;
-    LogStyle.ERROR.errorSound = null;
-    LogStyle.WARNING.openConsole = false;
-    LogStyle.WARNING.errorSound = null;
-
-    // FlxG.save.close();
-    // FlxG.sound.loadSavedPrefs();
-    WindowUtil.initWindowEvents();
-    WindowUtil.disableCrashHandler();
-
-    PreferencesMenu.initPrefs();
-    PlayerSettings.init();
-    Highscore.load();
-
-    if (FlxG.save.data.weekUnlocked != null)
-    {
-      // FIX LATER!!!
-      // WEEK UNLOCK PROGRESSION!!
-      // StoryMenuState.weekUnlocked = FlxG.save.data.weekUnlocked;
-
-      // if (StoryMenuState.weekUnlocked.length < 4) StoryMenuState.weekUnlocked.insert(0, true);
-
-      // QUICK PATCH OOPS!
-      // if (!StoryMenuState.weekUnlocked[0]) StoryMenuState.weekUnlocked[0] = true;
-    }
-
-    if (FlxG.save.data.seenVideo != null) VideoState.seenVideo = FlxG.save.data.seenVideo;
-
-    // ===== fuck outta here ===== //
-
-    // FlxTransitionableState.skipNextTransOut = true;
+    // FlxTransitionableState.defaultTransIn = new TransitionData(FADE, FlxColor.BLACK, 1, new FlxPoint(0, -1), {asset: diamond, width: 32, height: 32},
+    //   new FlxRect(-200, -200, FlxG.width * 1.4, FlxG.height * 1.4));
+    // FlxTransitionableState.defaultTransOut = new TransitionData(FADE, FlxColor.BLACK, 0.7, new FlxPoint(0, 1), {asset: diamond, width: 32, height: 32},
+    //   new FlxRect(-200, -200, FlxG.width * 1.4, FlxG.height * 1.4));
+    // Don't play transition in when entering the title state.
     FlxTransitionableState.skipNextTransIn = true;
 
-    // TODO: Register custom event callbacks here
+    //
+    // NEWGROUNDS API SETUP
+    //
+    #if newgrounds
+    NGio.init();
+    #end
 
+    //
+    // DISCORD API SETUP
+    //
+    #if discord_rpc
+    DiscordClient.initialize();
+
+    Application.current.onExit.add(function(exitCode) {
+      DiscordClient.shutdown();
+    });
+    #end
+
+    //
+    // ANDROID SETUP
+    //
+    #if android
+    FlxG.android.preventDefaultKeys = [flixel.input.android.FlxAndroidKey.BACK];
+    #end
+
+    //
+    // GAME DATA PARSING
+    //
     funkin.data.level.LevelRegistry.instance.loadEntries();
-    SongEventParser.loadEventCache();
-    ConversationDataParser.loadConversationCache();
-    DialogueBoxDataParser.loadDialogueBoxCache();
-    SpeakerDataParser.loadSpeakerCache();
-    SongDataParser.loadSongCache();
-    StageDataParser.loadStageCache();
-    CharacterDataParser.loadCharacterCache();
-    ModuleHandler.buildModuleCallbacks();
-    ModuleHandler.loadModuleCache();
+    funkin.play.event.SongEventData.SongEventParser.loadEventCache();
+    funkin.play.cutscene.dialogue.ConversationDataParser.loadConversationCache();
+    funkin.play.cutscene.dialogue.DialogueBoxDataParser.loadDialogueBoxCache();
+    funkin.play.cutscene.dialogue.SpeakerDataParser.loadSpeakerCache();
+    funkin.play.song.SongData.SongDataParser.loadSongCache();
+    funkin.play.stage.StageData.StageDataParser.loadStageCache();
+    funkin.play.character.CharacterData.CharacterDataParser.loadCharacterCache();
+    funkin.modding.module.ModuleHandler.buildModuleCallbacks();
+    funkin.modding.module.ModuleHandler.loadModuleCache();
 
-    FlxG.debugger.toggleKeys = [F2];
+    funkin.modding.module.ModuleHandler.callOnCreate();
+  }
 
-    ModuleHandler.callOnCreate();
+  /**
+   * Retrive and parse data from the user's save.
+   */
+  function loadSaveData()
+  {
+    // Bind save data.
+    // TODO: Migrate save data to a better format.
+    FlxG.save.bind('funkin', 'ninjamuffin99');
 
-    #if song
-    var song:String = getSong();
+    // Load player options from save data.
+    PreferencesMenu.initPrefs();
+    // Load controls from save data.
+    PlayerSettings.init();
+    // Load highscores from save data.
+    Highscore.load();
+    // TODO: Load level/character/cosmetic unlocks from save data.
+  }
 
-    var weeks:Array<Array<String>> = [
-      ['bopeebo', 'fresh', 'dadbattle'],
-      ['spookeez', 'south', 'monster'],
-      ['spooky', 'spooky', 'monster'],
-      ['pico', 'philly', 'blammed'],
-      ['satin-panties', 'high', 'milf'],
-      ['cocoa', 'eggnog', 'winter-horrorland'],
-      ['senpai', 'roses', 'thorns'],
-      ['ugh', 'guns', 'stress']
-    ];
-
-    var week:Int = 0;
-    for (i in 0...weeks.length)
-    {
-      if (weeks[i].contains(song))
-      {
-        week = i + 1;
-        break;
-      }
-    }
-
-    if (week == 0) throw 'Invalid -D song=$song';
-
-    startSong(week, song, false);
-    #elseif week
-    var week:Int = getWeek();
-
-    var songs:Array<String> = [
-      'bopeebo',
-      'spookeez',
-      'spooky',
-      'pico',
-      'satin-panties',
-      'cocoa',
-      'senpai',
-      'ugh'
-    ];
-
-    if (week <= 0 || week >= songs.length) throw 'invalid -D week=' + week;
-
-    startSong(week, songs[week - 1], true);
-    #elseif FREEPLAY
+  /**
+   * Start the game.
+   *
+   * By default, moves to the `TitleState`.
+   * But based on compile defines, the game can start immediately on a specific song,
+   * or immediately in a specific debug menu.
+   */
+  function startGame():Void
+  {
+    #if SONG // -DSONG=bopeebo
+    startSong(defineSong(), defineDifficulty());
+    #elseif LEVEL // -DLEVEL=week1 -DDIFFICULTY=hard
+    startLevel(defineLevel(), defineDifficulty());
+    #elseif FREEPLAY // -DFREEPLAY
     FlxG.switchState(new FreeplayState());
-    #elseif ANIMATE
+    #elseif ANIMATE // -DANIMATE
     FlxG.switchState(new funkin.ui.animDebugShit.FlxAnimateTest());
-    #elseif CHARTING
+    #elseif CHARTING // -DCHARTING
     FlxG.switchState(new funkin.ui.debug.charting.ChartEditorState());
-    #elseif STAGEBUILD
-    FlxG.switchState(new StageBuilderState());
-    #elseif FIGHT
-    FlxG.switchState(new PicoFight());
-    #elseif ANIMDEBUG
+    #elseif STAGEBUILD // -DSTAGEBUILD
+    FlxG.switchState(new funkin.ui.stageBullshit.StageBuilderState());
+    #elseif ANIMDEBUG // -DANIMDEBUG
     FlxG.switchState(new funkin.ui.animDebugShit.DebugBoundingState());
-    #elseif LATENCY
-    FlxG.switchState(new LatencyState());
-    #elseif NETTEST
-    FlxG.switchState(new netTest.NetTest());
+    #elseif LATENCY // -DLATENCY
+    FlxG.switchState(new funkin.LatencyState());
     #else
-    FlxG.sound.cache(Paths.music('freakyMenu'));
-    FlxG.switchState(new TitleState());
+    startGameNormally();
     #end
   }
 
-  function startSong(week, song, isStoryMode):Void
+  /**
+   * Start the game by moving to the title state and play the game as normal.
+   */
+  function startGameNormally():Void
   {
-    var dif:Int = getDif();
+    FlxG.sound.cache(Paths.music('freakyMenu'));
+    FlxG.switchState(new TitleState());
+  }
 
-    var targetDifficulty = switch (dif)
+  /**
+   * Start the game by directly loading into a specific song.
+   * @param songId
+   * @param difficultyId
+   */
+  function startSong(songId:String, difficultyId:String = 'normal'):Void
+  {
+    var songData:funkin.play.song.Song = funkin.play.song.SongData.SongDataParser.fetchSong(songId);
+
+    if (songData == null)
     {
-      case 0: 'easy';
-      case 1: 'normal';
-      case 2: 'hard';
-      default: 'normal';
-    };
-    LoadingState.loadAndSwitchState(new PlayState(
+      startGameNormally();
+      return;
+    }
+
+    LoadingState.loadAndSwitchState(new funkin.play.PlayState(
       {
-        targetSong: SongDataParser.fetchSong(song),
-        targetDifficulty: targetDifficulty,
+        targetSong: songData,
+        targetDifficulty: difficultyId,
       }));
   }
+
+  /**
+   * Start the game by directly loading into a specific story mode level.
+   * @param levelId
+   * @param difficultyId
+   */
+  function startLevel(levelId:String, difficultyId:String = 'normal'):Void
+  {
+    var currentLevel:funkin.ui.story.Level = funkin.data.level.LevelRegistry.instance.fetchEntry(levelId);
+
+    if (currentLevel == null)
+    {
+      startGameNormally();
+      return;
+    }
+
+    PlayStatePlaylist.playlistSongIds = currentLevel.getSongs();
+    PlayStatePlaylist.isStoryMode = true;
+    PlayStatePlaylist.campaignScore = 0;
+
+    var targetSongId:String = PlayStatePlaylist.playlistSongIds.shift();
+
+    var targetSong:funkin.play.song.Song = funkin.play.song.SongData.SongDataParser.fetchSong(targetSongId);
+
+    LoadingState.loadAndSwitchState(new funkin.play.PlayState(
+      {
+        targetSong: targetSong,
+        targetDifficulty: difficultyId,
+      }));
+  }
+
+  function defineSong():String
+  {
+    return MacroUtil.getDefine('SONG');
+  }
+
+  function defineLevel():String
+  {
+    return MacroUtil.getDefine('LEVEL');
+  }
+
+  function defineDifficulty():String
+  {
+    return MacroUtil.getDefine('DIFFICULTY');
+  }
 }
-
-function getWeek():Int
-  return Std.parseInt(MacroUtil.getDefine('week'));
-
-function getSong():String
-  return MacroUtil.getDefine('song');
-
-function getDif():Int
-  return Std.parseInt(MacroUtil.getDefine('dif', '1'));
diff --git a/source/funkin/PlayerSettings.hx b/source/funkin/PlayerSettings.hx
index 1b64d26c2..54fd559fb 100644
--- a/source/funkin/PlayerSettings.hx
+++ b/source/funkin/PlayerSettings.hx
@@ -26,8 +26,10 @@ class PlayerSettings
   // public var avatar:Player;
   // public var camera(get, never):PlayCamera;
 
-  function new(id)
+  function new(id:Int)
   {
+    trace('loading player settings for id: $id');
+
     this.id = id;
     this.controls = new Controls('player$id', None);
 
@@ -52,7 +54,11 @@ class PlayerSettings
       }
     }
 
-    if (useDefault) controls.setKeyboardScheme(Solo);
+    if (useDefault)
+    {
+      trace("falling back to default control scheme");
+      controls.setKeyboardScheme(Solo);
+    }
 
     // Apply loaded settings.
     PreciseInputManager.instance.initializeKeys(controls);
diff --git a/source/funkin/TitleState.hx b/source/funkin/TitleState.hx
index bd4e7084c..59845ed40 100644
--- a/source/funkin/TitleState.hx
+++ b/source/funkin/TitleState.hx
@@ -44,6 +44,7 @@ class TitleState extends MusicBeatState
 
   override public function create():Void
   {
+    super.create();
     swagShader = new ColorSwap();
 
     curWacky = FlxG.random.getObject(getIntroTextShit());
@@ -51,38 +52,6 @@ class TitleState extends MusicBeatState
 
     // DEBUG BULLSHIT
 
-    super.create();
-
-    /*
-          #elseif web
-
-
-          if (!initialized)
-          {
-
-      video = new Video();
-      FlxG.stage.addChild(video);
-
-      var netConnection = new NetConnection();
-      netConnection.connect(null);
-
-      netStream = new NetStream(netConnection);
-      netStream.client = {onMetaData: client_onMetaData};
-      netStream.addEventListener(AsyncErrorEvent.ASYNC_ERROR, netStream_onAsyncError);
-      netConnection.addEventListener(NetStatusEvent.NET_STATUS, netConnection_onNetStatus);
-      // netStream.addEventListener(NetStatusEvent.NET_STATUS) // netStream.play(Paths.file('music/kickstarterTrailer.mp4'));
-
-      overlay = new Sprite();
-      overlay.graphics.beginFill(0, 0.5);
-      overlay.graphics.drawRect(0, 0, 1280, 720);
-      overlay.addEventListener(MouseEvent.MOUSE_DOWN, overlay_onMouseDown);
-
-      overlay.buttonMode = true;
-      // FlxG.stage.addChild(overlay);
-
-          }
-     */
-
     // netConnection.addEventListener(MouseEvent.MOUSE_DOWN, overlay_onMouseDown);
     new FlxTimer().start(1, function(tmr:FlxTimer) {
       startIntro();
diff --git a/source/funkin/ui/story/StoryMenuState.hx b/source/funkin/ui/story/StoryMenuState.hx
index d7f6db00d..f62e064e1 100644
--- a/source/funkin/ui/story/StoryMenuState.hx
+++ b/source/funkin/ui/story/StoryMenuState.hx
@@ -110,7 +110,7 @@ class StoryMenuState extends MusicBeatState
     transIn = FlxTransitionableState.defaultTransIn;
     transOut = FlxTransitionableState.defaultTransOut;
 
-    if (!FlxG.sound.music.playing)
+    if (FlxG.sound.music == null || !FlxG.sound.music.playing)
     {
       FlxG.sound.playMusic(Paths.music('freakyMenu'));
       FlxG.sound.music.fadeIn(4, 0, 0.7);