diff --git a/source/funkin/Conductor.hx b/source/funkin/Conductor.hx
index a0493869b..f83de069b 100644
--- a/source/funkin/Conductor.hx
+++ b/source/funkin/Conductor.hx
@@ -50,6 +50,16 @@ class Conductor
   // OLD, replaced with timeChanges.
   public static var bpmChangeMap:Array<BPMChangeEvent> = [];
 
+  /**
+   * Duration of a measure in milliseconds. Calculated based on bpm.
+   */
+  public static var measureLengthMs(get, null):Float;
+
+  static function get_measureLengthMs():Float
+  {
+    return crochet * timeSignatureNumerator;
+  }
+
   /**
    * Duration of a beat in millisecond. Calculated based on bpm.
    */
diff --git a/source/funkin/CoolUtil.hx b/source/funkin/CoolUtil.hx
index c7d3f7dab..82bf5201a 100644
--- a/source/funkin/CoolUtil.hx
+++ b/source/funkin/CoolUtil.hx
@@ -20,13 +20,6 @@ import openfl.filters.ShaderFilter;
 
 class CoolUtil
 {
-  public static var difficultyArray:Array<String> = ['EASY', "NORMAL", "HARD"];
-
-  public static function difficultyString():String
-  {
-    return difficultyArray[PlayState.storyDifficulty];
-  }
-
   public static function coolBaseLog(base:Float, fin:Float):Float
   {
     return Math.log(fin) / Math.log(base);
@@ -119,8 +112,7 @@ class CoolUtil
     FlxTween.tween(screenWipeShit, {daAlphaShit: 1}, time,
       {
         ease: FlxEase.quadInOut,
-        onComplete: function(twn)
-        {
+        onComplete: function(twn) {
           screenShit.destroy();
           FlxG.switchState(new MainMenuState());
         }
diff --git a/source/funkin/DialogueBox.hx b/source/funkin/DialogueBox.hx
index e9c3587ad..4258f71ce 100644
--- a/source/funkin/DialogueBox.hx
+++ b/source/funkin/DialogueBox.hx
@@ -38,7 +38,7 @@ class DialogueBox extends FlxSpriteGroup
   {
     super();
 
-    switch (PlayState.currentSong.song.toLowerCase())
+    switch (PlayState.instance.currentSong.songId.toLowerCase())
     {
       case 'senpai':
         FlxG.sound.playMusic(Paths.music('Lunchbox'), 0);
@@ -53,8 +53,7 @@ class DialogueBox extends FlxSpriteGroup
     bgFade.alpha = 0;
     add(bgFade);
 
-    new FlxTimer().start(0.83, function(tmr:FlxTimer)
-    {
+    new FlxTimer().start(0.83, function(tmr:FlxTimer) {
       bgFade.alpha += (1 / 5) * 0.7;
       if (bgFade.alpha > 0.7) bgFade.alpha = 0.7;
     }, 5);
@@ -80,7 +79,7 @@ class DialogueBox extends FlxSpriteGroup
     box = new FlxSprite(-20, 45);
 
     var hasDialog:Bool = false;
-    switch (PlayState.currentSong.song.toLowerCase())
+    switch (PlayState.instance.currentSong.songId.toLowerCase())
     {
       case 'senpai':
         hasDialog = true;
@@ -152,8 +151,8 @@ class DialogueBox extends FlxSpriteGroup
   override function update(elapsed:Float):Void
   {
     // HARD CODING CUZ IM STUPDI
-    if (PlayState.currentSong.song.toLowerCase() == 'roses') portraitLeft.visible = false;
-    if (PlayState.currentSong.song.toLowerCase() == 'thorns')
+    if (PlayState.instance.currentSong.songId.toLowerCase() == 'roses') portraitLeft.visible = false;
+    if (PlayState.instance.currentSong.songId.toLowerCase() == 'thorns')
     {
       portraitLeft.color = FlxColor.BLACK;
       swagDialogue.color = FlxColor.WHITE;
@@ -189,11 +188,10 @@ class DialogueBox extends FlxSpriteGroup
         {
           isEnding = true;
 
-          if (PlayState.currentSong.song.toLowerCase() == 'senpai'
-            || PlayState.currentSong.song.toLowerCase() == 'thorns') FlxG.sound.music.fadeOut(2.2, 0);
+          if (PlayState.instance.currentSong.songId.toLowerCase() == 'senpai'
+            || PlayState.instance.currentSong.songId.toLowerCase() == 'thorns') FlxG.sound.music.fadeOut(2.2, 0);
 
-          new FlxTimer().start(0.2, function(tmr:FlxTimer)
-          {
+          new FlxTimer().start(0.2, function(tmr:FlxTimer) {
             box.alpha -= 1 / 5;
             bgFade.alpha -= 1 / 5 * 0.7;
             portraitLeft.visible = false;
@@ -203,8 +201,7 @@ class DialogueBox extends FlxSpriteGroup
             dropText.alpha = swagDialogue.alpha;
           }, 5);
 
-          new FlxTimer().start(1.2, function(tmr:FlxTimer)
-          {
+          new FlxTimer().start(1.2, function(tmr:FlxTimer) {
             finishThing();
             kill();
           });
@@ -233,8 +230,7 @@ class DialogueBox extends FlxSpriteGroup
     // swagDialogue.text = ;
     swagDialogue.resetText(dialogueList[0]);
     swagDialogue.start(0.04);
-    swagDialogue.completeCallback = function()
-    {
+    swagDialogue.completeCallback = function() {
       trace('dialogue finish');
       handSelect.visible = true;
       dialogueEnded = true;
diff --git a/source/funkin/FreeplayState.hx b/source/funkin/FreeplayState.hx
index 2b869a21e..563c13c34 100644
--- a/source/funkin/FreeplayState.hx
+++ b/source/funkin/FreeplayState.hx
@@ -130,20 +130,26 @@ class FreeplayState extends MusicBeatSubState
       if (!FlxG.sound.music.playing) FlxG.sound.playMusic(Paths.music('freakyMenu'));
     }
 
-    if (StoryMenuState.weekUnlocked[2] || isDebug) addWeek(['Bopeebo', 'Fresh', 'Dadbattle'], 1, ['dad']);
+    // if (StoryMenuState.weekUnlocked[2] || isDebug)
+    addWeek(['Bopeebo', 'Fresh', 'Dadbattle'], 1, ['dad']);
 
-    if (StoryMenuState.weekUnlocked[2] || isDebug) addWeek(['Spookeez', 'South', 'Monster'], 2, ['spooky', 'spooky', 'monster']);
+    // if (StoryMenuState.weekUnlocked[2] || isDebug)
+    addWeek(['Spookeez', 'South', 'Monster'], 2, ['spooky', 'spooky', 'monster']);
 
-    if (StoryMenuState.weekUnlocked[3] || isDebug) addWeek(['Pico', 'Philly-Nice', 'Blammed'], 3, ['pico']);
+    // if (StoryMenuState.weekUnlocked[3] || isDebug)
+    addWeek(['Pico', 'Philly-Nice', 'Blammed'], 3, ['pico']);
 
-    if (StoryMenuState.weekUnlocked[4] || isDebug) addWeek(['Satin-Panties', 'High', 'MILF'], 4, ['mom']);
+    // if (StoryMenuState.weekUnlocked[4] || isDebug)
+    addWeek(['Satin-Panties', 'High', 'MILF'], 4, ['mom']);
 
-    if (StoryMenuState.weekUnlocked[5] || isDebug) addWeek(['Cocoa', 'Eggnog', 'Winter-Horrorland'], 5,
-      ['parents-christmas', 'parents-christmas', 'monster-christmas']);
+    // if (StoryMenuState.weekUnlocked[5] || isDebug)
+    addWeek(['Cocoa', 'Eggnog', 'Winter-Horrorland'], 5, ['parents-christmas', 'parents-christmas', 'monster-christmas']);
 
-    if (StoryMenuState.weekUnlocked[6] || isDebug) addWeek(['Senpai', 'Roses', 'Thorns'], 6, ['senpai', 'senpai', 'spirit']);
+    // if (StoryMenuState.weekUnlocked[6] || isDebug)
+    addWeek(['Senpai', 'Roses', 'Thorns'], 6, ['senpai', 'senpai', 'spirit']);
 
-    if (StoryMenuState.weekUnlocked[7] || isDebug) addWeek(['Ugh', 'Guns', 'Stress'], 7, ['tankman']);
+    // if (StoryMenuState.weekUnlocked[7] || isDebug)
+    addWeek(['Ugh', 'Guns', 'Stress'], 7, ['tankman']);
 
     addWeek(["Darnell", "lit-up", "2hot", "blazin"], 8, ['darnell']);
 
diff --git a/source/funkin/GitarooPause.hx b/source/funkin/GitarooPause.hx
index fda809548..5747de5e5 100644
--- a/source/funkin/GitarooPause.hx
+++ b/source/funkin/GitarooPause.hx
@@ -11,12 +11,16 @@ class GitarooPause extends MusicBeatState
 
   var replaySelect:Bool = false;
 
-  public function new():Void
+  var previousParams:PlayStateParams;
+
+  public function new(previousParams:PlayStateParams):Void
   {
     super();
+
+    this.previousParams = previousParams;
   }
 
-  override function create()
+  override function create():Void
   {
     if (FlxG.sound.music != null) FlxG.sound.music.stop();
 
@@ -49,7 +53,7 @@ class GitarooPause extends MusicBeatState
     super.create();
   }
 
-  override function update(elapsed:Float)
+  override function update(elapsed:Float):Void
   {
     if (controls.UI_LEFT_P || controls.UI_RIGHT_P) changeThing();
 
@@ -57,7 +61,7 @@ class GitarooPause extends MusicBeatState
     {
       if (replaySelect)
       {
-        FlxG.switchState(new PlayState());
+        FlxG.switchState(new PlayState(previousParams));
       }
       else
       {
diff --git a/source/funkin/Highscore.hx b/source/funkin/Highscore.hx
index 08ad7dcba..904d2cb45 100644
--- a/source/funkin/Highscore.hx
+++ b/source/funkin/Highscore.hx
@@ -39,7 +39,17 @@ class Highscore
     return false;
   }
 
-  public static function saveCompletion(song:String, completion:Float, ?diff:Int = 0):Bool
+  public static function saveScoreForDifficulty(song:String, score:Int = 0, diff:String = 'normal'):Bool
+  {
+    var diffInt:Int = 1;
+
+    if (diff == 'easy') diffInt = 0;
+    else if (diff == 'hard') diffInt = 2;
+
+    return saveScore(song, score, diffInt);
+  }
+
+  public static function saveCompletion(song:String, completion:Float, diff:Int = 0):Bool
   {
     var formattedSong:String = formatSong(song, diff);
 
@@ -57,20 +67,42 @@ class Highscore
     return false;
   }
 
-  public static function saveWeekScore(week:Int = 1, score:Int = 0, ?diff:Int = 0):Void
+  public static function saveCompletionForDifficulty(song:String, completion:Float, diff:String = 'normal'):Bool
+  {
+    var diffInt:Int = 1;
+
+    if (diff == 'easy') diffInt = 0;
+    else if (diff == 'hard') diffInt = 2;
+
+    return saveCompletion(song, completion, diffInt);
+  }
+
+  public static function saveWeekScore(week:String, score:Int = 0, diff:Int = 0):Void
   {
     #if newgrounds
-    NGio.postScore(score, "Week " + week);
+    NGio.postScore(score, 'Campaign ID $week');
     #end
 
-    var formattedSong:String = formatSong('week' + week, diff);
+    var formattedSong:String = formatSong(week, diff);
 
     if (songScores.exists(formattedSong))
     {
       if (songScores.get(formattedSong) < score) setScore(formattedSong, score);
     }
     else
+    {
       setScore(formattedSong, score);
+    }
+  }
+
+  public static function saveWeekScoreForDifficulty(week:String, score:Int = 0, diff:String = 'normal'):Void
+  {
+    var diffInt:Int = 1;
+
+    if (diff == 'easy') diffInt = 0;
+    else if (diff == 'hard') diffInt = 2;
+
+    saveWeekScore(week, score, diffInt);
   }
 
   static function setCompletion(formattedSong:String, completion:Float):Void
@@ -122,7 +154,7 @@ class Highscore
     return songCompletion.get(formatSong(song, diff));
   }
 
-  public static function getAllScores()
+  public static function getAllScores():Void
   {
     trace(songScores.toString());
   }
diff --git a/source/funkin/InitState.hx b/source/funkin/InitState.hx
index 76e85befd..8d7d2d550 100644
--- a/source/funkin/InitState.hx
+++ b/source/funkin/InitState.hx
@@ -237,20 +237,18 @@ class InitState extends FlxTransitionableState
   {
     var dif:Int = getDif();
 
-    PlayState.currentSong = SongLoad.loadFromJson(song, song);
-    PlayState.currentSong_NEW = SongDataParser.fetchSong(song);
-    PlayState.isStoryMode = isStoryMode;
-    PlayState.storyDifficulty = dif;
-    PlayState.storyDifficulty_NEW = switch (dif)
+    var targetDifficulty = switch (dif)
     {
       case 0: 'easy';
       case 1: 'normal';
       case 2: 'hard';
       default: 'normal';
     };
-    SongLoad.curDiff = PlayState.storyDifficulty_NEW;
-    PlayState.storyWeek = week;
-    LoadingState.loadAndSwitchState(new PlayState());
+    LoadingState.loadAndSwitchState(new PlayState(
+      {
+        targetSong: SongDataParser.fetchSong(song),
+        targetDifficulty: targetDifficulty,
+      }));
   }
 }
 
diff --git a/source/funkin/LoadingState.hx b/source/funkin/LoadingState.hx
index dd5ff6040..604e78f79 100644
--- a/source/funkin/LoadingState.hx
+++ b/source/funkin/LoadingState.hx
@@ -1,5 +1,6 @@
 package funkin;
 
+import funkin.play.PlayStatePlaylist;
 import flixel.FlxSprite;
 import flixel.FlxState;
 import flixel.math.FlxMath;
@@ -32,7 +33,7 @@ class LoadingState extends MusicBeatState
     this.stopMusic = stopMusic;
   }
 
-  override function create()
+  override function create():Void
   {
     var bg:FlxSprite = new FlxSprite().makeGraphic(FlxG.width, FlxG.height, 0xFFcaff4d);
     add(bg);
@@ -52,57 +53,56 @@ class LoadingState extends MusicBeatState
 
     initSongsManifest().onComplete(function(lib) {
       callbacks = new MultiCallback(onLoad);
-      var introComplete = callbacks.add("introComplete");
-      checkLoadSong(getSongPath());
-      if (PlayState.currentSong.needsVoices)
-      {
-        var files = PlayState.currentSong.voiceList;
+      var introComplete = callbacks.add('introComplete');
+      // checkLoadSong(getSongPath());
+      // if (PlayState.currentSong.needsVoices)
+      // {
+      //  var files = PlayState.currentSong.voiceList;
+      //
+      //  if (files == null) files = ['']; // loads with no file name assumption, to load 'Voices.ogg' or whatev normally
+      //
+      //  for (sndFile in files)
+      //  {
+      //    checkLoadSong(getVocalPath(sndFile));
+      //  }
+      // }
 
-        if (files == null) files = [""]; // loads with no file name assumption, to load "Voices.ogg" or whatev normally
+      checkLibrary('shared');
+      checkLibrary(PlayStatePlaylist.campaignId);
+      checkLibrary('tutorial');
 
-        for (sndFile in files)
-        {
-          checkLoadSong(getVocalPath(sndFile));
-        }
-      }
-
-      checkLibrary("shared");
-      if (PlayState.storyWeek > 0) checkLibrary("week" + PlayState.storyWeek);
-      else
-        checkLibrary("tutorial");
-
-      var fadeTime = 0.5;
+      var fadeTime:Float = 0.5;
       FlxG.camera.fade(FlxG.camera.bgColor, fadeTime, true);
       new FlxTimer().start(fadeTime + MIN_TIME, function(_) introComplete());
     });
   }
 
-  function checkLoadSong(path:String)
+  function checkLoadSong(path:String):Void
   {
     if (!Assets.cache.hasSound(path))
     {
-      var library = Assets.getLibrary("songs");
-      var symbolPath = path.split(":").pop();
+      var library = Assets.getLibrary('songs');
+      var symbolPath = path.split(':').pop();
       // @:privateAccess
       // library.types.set(symbolPath, SOUND);
       // @:privateAccess
       // library.pathGroups.set(symbolPath, [library.__cacheBreak(symbolPath)]);
-      var callback = callbacks.add("song:" + path);
+      var callback = callbacks.add('song:' + path);
       Assets.loadSound(path).onComplete(function(_) {
         callback();
       });
     }
   }
 
-  function checkLibrary(library:String)
+  function checkLibrary(library:String):Void
   {
     trace(Assets.hasLibrary(library));
     if (Assets.getLibrary(library) == null)
     {
       @:privateAccess
-      if (!LimeAssets.libraryPaths.exists(library)) throw "Missing library: " + library;
+      if (!LimeAssets.libraryPaths.exists(library)) throw 'Missing library: ' + library;
 
-      var callback = callbacks.add("library:" + library);
+      var callback = callbacks.add('library:' + library);
       Assets.loadLibrary(library).onComplete(function(_) {
         callback();
       });
@@ -121,7 +121,7 @@ class LoadingState extends MusicBeatState
 
   var targetShit:Float = 0;
 
-  override function update(elapsed:Float)
+  override function update(elapsed:Float):Void
   {
     super.update(elapsed);
 
@@ -147,44 +147,41 @@ class LoadingState extends MusicBeatState
     }
 
     #if debug
-    if (FlxG.keys.justPressed.SPACE) trace('fired: ' + callbacks.getFired() + " unfired:" + callbacks.getUnfired());
+    if (FlxG.keys.justPressed.SPACE) trace('fired: ' + callbacks.getFired() + ' unfired:' + callbacks.getUnfired());
     #end
   }
 
-  function onLoad()
+  function onLoad():Void
   {
     if (stopMusic && FlxG.sound.music != null) FlxG.sound.music.stop();
 
     FlxG.switchState(target);
   }
 
-  static function getSongPath()
+  static function getSongPath():String
   {
-    return Paths.inst(PlayState.currentSong.song);
+    return Paths.inst(PlayState.instance.currentSong.songId);
   }
 
-  static function getVocalPath(?suffix:String)
+  inline static public function loadAndSwitchState(nextState:FlxState, shouldStopMusic = false):Void
   {
-    return Paths.voices(PlayState.currentSong.song, suffix);
+    FlxG.switchState(getNextState(nextState, shouldStopMusic));
   }
 
-  inline static public function loadAndSwitchState(target:FlxState, stopMusic = false)
+  static function getNextState(nextState:FlxState, shouldStopMusic = false):FlxState
   {
-    FlxG.switchState(getNextState(target, stopMusic));
-  }
+    Paths.setCurrentLevel(PlayStatePlaylist.campaignId);
 
-  static function getNextState(target:FlxState, stopMusic = false):FlxState
-  {
     #if NO_PRELOAD_ALL
-    var loaded = isSoundLoaded(getSongPath())
-      && (!PlayState.currentSong.needsVoices || isSoundLoaded(getVocalPath()))
-      && isLibraryLoaded("shared");
-
-    if (!loaded) return new LoadingState(target, stopMusic);
+    // var loaded = isSoundLoaded(getSongPath())
+    //  && (!PlayState.currentSong.needsVoices || isSoundLoaded(getVocalPath()))
+    //  && isLibraryLoaded('shared');
+    //
+    if (true) return new LoadingState(nextState, shouldStopMusic);
     #end
-    if (stopMusic && FlxG.sound.music != null) FlxG.sound.music.stop();
+    if (shouldStopMusic && FlxG.sound.music != null) FlxG.sound.music.stop();
 
-    return target;
+    return nextState;
   }
 
   #if NO_PRELOAD_ALL
@@ -199,16 +196,16 @@ class LoadingState extends MusicBeatState
   }
   #end
 
-  override function destroy()
+  override function destroy():Void
   {
     super.destroy();
 
     callbacks = null;
   }
 
-  static function initSongsManifest()
+  static function initSongsManifest():Future<AssetLibrary>
   {
-    var id = "songs";
+    var id = 'songs';
     var promise = new Promise<AssetLibrary>();
 
     var library = LimeAssets.getLibrary(id);
@@ -230,10 +227,10 @@ class LoadingState extends MusicBeatState
     }
     else
     {
-      if (path.endsWith(".bundle"))
+      if (path.endsWith('.bundle'))
       {
         rootPath = path;
-        path += "/library.json";
+        path += '/library.json';
       }
       else
       {
@@ -246,7 +243,7 @@ class LoadingState extends MusicBeatState
     AssetManifest.loadFromFile(path, rootPath).onComplete(function(manifest) {
       if (manifest == null)
       {
-        promise.error("Cannot parse asset manifest for library \"" + id + "\"");
+        promise.error('Cannot parse asset manifest for library \'' + id + '\'');
         return;
       }
 
@@ -254,7 +251,7 @@ class LoadingState extends MusicBeatState
 
       if (library == null)
       {
-        promise.error("Cannot open library \"" + id + "\"");
+        promise.error('Cannot open library \'' + id + '\'');
       }
       else
       {
@@ -264,7 +261,7 @@ class LoadingState extends MusicBeatState
         promise.completeWith(Future.withValue(library));
       }
     }).onError(function(_) {
-      promise.error("There is no asset library with an ID of \"" + id + "\"");
+      promise.error('There is no asset library with an ID of \'' + id + '\'');
     });
 
     return promise.future;
@@ -287,7 +284,7 @@ class MultiCallback
     this.logId = logId;
   }
 
-  public function add(id = "untitled")
+  public function add(id = 'untitled'):Void->Void
   {
     id = '$length:$id';
     length++;
@@ -320,9 +317,9 @@ class MultiCallback
     if (logId != null) trace('$logId: $msg');
   }
 
-  public function getFired()
+  public function getFired():Array<String>
     return fired.copy();
 
-  public function getUnfired()
+  public function getUnfired():Array<Void->Void>
     return unfired.array();
 }
diff --git a/source/funkin/Note.hx b/source/funkin/Note.hx
index 7fc49717e..4479774c5 100644
--- a/source/funkin/Note.hx
+++ b/source/funkin/Note.hx
@@ -1,5 +1,6 @@
 package funkin;
 
+import funkin.play.Strumline.StrumlineArrow;
 import flixel.FlxSprite;
 import flixel.math.FlxMath;
 import funkin.noteStuff.NoteBasic.NoteData;
@@ -215,6 +216,24 @@ class Note extends FlxSprite
     }
   }
 
+  public function alignToSturmlineArrow(arrow:StrumlineArrow):Void
+  {
+    x = arrow.x;
+
+    if (isSustainNote && prevNote != null)
+    {
+      if (prevNote.isSustainNote)
+      {
+        x = prevNote.x;
+      }
+      else
+      {
+        x += prevNote.width / 2;
+        x -= width / 2;
+      }
+    }
+  }
+
   override function destroy()
   {
     prevNote = null;
diff --git a/source/funkin/PauseSubState.hx b/source/funkin/PauseSubState.hx
index 890b51cb7..7349052aa 100644
--- a/source/funkin/PauseSubState.hx
+++ b/source/funkin/PauseSubState.hx
@@ -1,9 +1,10 @@
 package funkin;
 
+import funkin.play.PlayStatePlaylist;
 import flixel.FlxSprite;
 import flixel.addons.transition.FlxTransitionableState;
 import flixel.group.FlxGroup.FlxTypedGroup;
-import flixel.sound.FlxSound;
+import flixel.system.FlxSound;
 import flixel.text.FlxText;
 import flixel.tweens.FlxEase;
 import flixel.tweens.FlxTween;
@@ -20,9 +21,9 @@ class PauseSubState extends MusicBeatSubState
     'Restart Song',
     'Change Difficulty',
     'Toggle Practice Mode',
-    'Exit to menu'
+    'Exit to Menu'
   ];
-  var difficultyChoices:Array<String> = ['EASY', 'NORMAL', 'HARD', 'BACK'];
+  var difficultyChoices:Array<String> = ['EASY', 'NORMAL', 'HARD', 'ERECT', 'BACK'];
 
   var menuItems:Array<String> = [];
   var curSelected:Int = 0;
@@ -41,10 +42,14 @@ class PauseSubState extends MusicBeatSubState
 
     menuItems = pauseOG;
 
-    if (PlayState.storyWeek == 6) // consistent with logic that decides asset lib!!
+    if (PlayStatePlaylist.campaignId == 'week6')
+    {
       pauseMusic = new FlxSound().loadEmbedded(Paths.music('breakfast-pixel'), true, true);
+    }
     else
+    {
       pauseMusic = new FlxSound().loadEmbedded(Paths.music('breakfast'), true, true);
+    }
     pauseMusic.volume = 0;
     pauseMusic.play(false, FlxG.random.int(0, Std.int(pauseMusic.length / 2)));
 
@@ -58,43 +63,38 @@ class PauseSubState extends MusicBeatSubState
     metaDataGrp = new FlxTypedGroup<FlxSprite>();
     add(metaDataGrp);
 
-    var levelInfo:FlxText = new FlxText(20, 15, 0, "", 32);
+    var levelInfo:FlxText = new FlxText(20, 15, 0, '', 32);
     if (PlayState.instance.currentChart != null)
     {
       levelInfo.text += '${PlayState.instance.currentChart.songName} - ${PlayState.instance.currentChart.songArtist}';
     }
-    else
-    {
-      levelInfo.text += PlayState.currentSong.song;
-    }
     levelInfo.scrollFactor.set();
-    levelInfo.setFormat(Paths.font("vcr.ttf"), 32);
+    levelInfo.setFormat(Paths.font('vcr.ttf'), 32);
     levelInfo.updateHitbox();
     metaDataGrp.add(levelInfo);
 
-    var levelDifficulty:FlxText = new FlxText(20, 15 + 32, 0, "", 32);
-    levelDifficulty.text += CoolUtil.difficultyString();
+    var levelDifficulty:FlxText = new FlxText(20, 15 + 32, 0, '', 32);
+    levelDifficulty.text += PlayState.instance.currentDifficulty.toTitleCase();
     levelDifficulty.scrollFactor.set();
     levelDifficulty.setFormat(Paths.font('vcr.ttf'), 32);
     levelDifficulty.updateHitbox();
     metaDataGrp.add(levelDifficulty);
 
-    var deathCounter:FlxText = new FlxText(20, 15 + 64, 0, "", 32);
-    deathCounter.text = "Blue balled: " + PlayState.deathCounter;
-    deathCounter.text += "\n" + Highscore.tallies.totalNotesHit;
-    deathCounter.text += "\n" + Highscore.tallies.totalNotes;
-    deathCounter.text += "\n" + Std.string(Highscore.tallies.totalNotesHit / Highscore.tallies.totalNotes);
+    var deathCounter:FlxText = new FlxText(20, 15 + 64, 0, '', 32);
+    deathCounter.text = 'Blue balled: ${PlayState.instance.deathCounter}';
+    FlxG.watch.addQuick('totalNotesHit', Highscore.tallies.totalNotesHit);
+    FlxG.watch.addQuick('totalNotes', Highscore.tallies.totalNotes);
     deathCounter.scrollFactor.set();
     deathCounter.setFormat(Paths.font('vcr.ttf'), 32);
     deathCounter.updateHitbox();
     metaDataGrp.add(deathCounter);
 
-    practiceText = new FlxText(20, 15 + 64 + 32, 0, "PRACTICE MODE", 32);
+    practiceText = new FlxText(20, 15 + 64 + 32, 0, 'PRACTICE MODE', 32);
     practiceText.scrollFactor.set();
     practiceText.setFormat(Paths.font('vcr.ttf'), 32);
     practiceText.updateHitbox();
     practiceText.x = FlxG.width - (practiceText.width + 20);
-    practiceText.visible = PlayState.isPracticeMode;
+    practiceText.visible = PlayState.instance.isPracticeMode;
     metaDataGrp.add(practiceText);
 
     levelDifficulty.alpha = 0;
@@ -137,7 +137,7 @@ class PauseSubState extends MusicBeatSubState
     changeSelection();
   }
 
-  override function update(elapsed:Float)
+  override function update(elapsed:Float):Void
   {
     if (pauseMusic.volume < 0.5) pauseMusic.volume += 0.01 * elapsed;
 
@@ -180,41 +180,39 @@ class PauseSubState extends MusicBeatSubState
       {
         var daSelected:String = menuItems[curSelected];
 
+        // TODO: Why is this based on the menu item's name? Make this an enum or something.
         switch (daSelected)
         {
-          case "Resume":
+          case 'Resume':
             close();
-          case "EASY" | 'NORMAL' | "HARD":
-            PlayState.currentSong = SongLoad.loadFromJson(PlayState.currentSong.song.toLowerCase(), PlayState.currentSong.song.toLowerCase());
-            PlayState.currentSong_NEW = SongDataParser.fetchSong(PlayState.currentSong.song.toLowerCase());
-            SongLoad.curDiff = daSelected.toLowerCase();
-
-            PlayState.storyDifficulty = curSelected;
-            PlayState.storyDifficulty_NEW = daSelected.toLowerCase();
-
-            PlayState.needsReset = true;
-
-            close();
-
-          case 'Toggle Practice Mode':
-            PlayState.isPracticeMode = !PlayState.isPracticeMode;
-            practiceText.visible = PlayState.isPracticeMode;
 
           case 'Change Difficulty':
             menuItems = difficultyChoices;
             regenMenu();
+
+          case 'EASY' | 'NORMAL' | 'HARD' | 'ERECT':
+            PlayState.instance.currentSong = SongDataParser.fetchSong(PlayState.instance.currentSong.songId.toLowerCase());
+
+            PlayState.instance.currentDifficulty = daSelected.toLowerCase();
+
+            PlayState.instance.needsReset = true;
+
+            close();
           case 'BACK':
             menuItems = pauseOG;
             regenMenu();
-          case "Restart Song":
-            PlayState.needsReset = true;
 
+          case 'Toggle Practice Mode':
+            PlayState.instance.isPracticeMode = true;
+            practiceText.visible = PlayState.instance.isPracticeMode;
+
+          case 'Restart Song':
+            PlayState.instance.needsReset = true;
             close();
-          // FlxG.resetState();
-          case "Exit to menu":
+
+          case 'Exit to Menu':
             exitingToMenu = true;
-            PlayState.seenCutscene = false;
-            PlayState.deathCounter = 0;
+            PlayState.instance.deathCounter = 0;
 
             for (item in grpMenuShit.members)
             {
@@ -225,7 +223,7 @@ class PauseSubState extends MusicBeatSubState
             FlxTransitionableState.skipNextTransIn = true;
             FlxTransitionableState.skipNextTransOut = true;
 
-            if (PlayState.isStoryMode) openSubState(new funkin.ui.StickerSubState(null, STORY));
+            if (PlayStatePlaylist.isStoryMode) openSubState(new funkin.ui.StickerSubState(null, STORY));
             else
               openSubState(new funkin.ui.StickerSubState());
         }
@@ -239,7 +237,7 @@ class PauseSubState extends MusicBeatSubState
     }
   }
 
-  override function destroy()
+  override function destroy():Void
   {
     pauseMusic.destroy();
 
@@ -260,12 +258,10 @@ class PauseSubState extends MusicBeatSubState
       item.targetY = index - curSelected;
 
       item.alpha = 0.6;
-      // item.setGraphicSize(Std.int(item.width * 0.8));
 
       if (item.targetY == 0)
       {
         item.alpha = 1;
-        // item.setGraphicSize(Std.int(item.width));
       }
     }
   }
diff --git a/source/funkin/TitleState.hx b/source/funkin/TitleState.hx
index e8e96e54d..b0ed157dd 100644
--- a/source/funkin/TitleState.hx
+++ b/source/funkin/TitleState.hx
@@ -84,8 +84,7 @@ class TitleState extends MusicBeatState
      */
 
     // netConnection.addEventListener(MouseEvent.MOUSE_DOWN, overlay_onMouseDown);
-    new FlxTimer().start(1, function(tmr:FlxTimer)
-    {
+    new FlxTimer().start(1, function(tmr:FlxTimer) {
       startIntro();
     });
   }
@@ -284,44 +283,6 @@ class TitleState extends MusicBeatState
       FlxTween.tween(FlxG.stage.window, {y: FlxG.stage.window.y + 100}, 0.7, {ease: FlxEase.quadInOut, type: PINGPONG});
     }
 
-    /* 
-          FlxG.watch.addQuick('cur display', FlxG.stage.window.display.id);
-          if (FlxG.keys.justPressed.Y)
-          {
-      // trace(FlxG.stage.window.display.name);
-
-      if (FlxG.gamepads.firstActive != null)
-      {
-        trace(FlxG.gamepads.firstActive.model);
-        FlxG.gamepads.firstActive.id
-      }
-      else
-        trace('gamepad null');
-
-      // FlxG.stage.window.title = Std.string(FlxG.random.int(0, 20000));
-      // FlxG.stage.window.setIcon(Image.fromFile('assets/images/icon16.png'));
-      // FlxG.stage.window.readPixels;
-
-      if (FlxG.stage.window.width == Std.int(FlxG.stage.window.display.bounds.width))
-      {
-        FlxG.stage.window.width = 1280;
-        FlxG.stage.window.height = 720;
-        FlxG.stage.window.y = 30;
-      }
-      else
-      {
-        FlxG.stage.window.width = Std.int(FlxG.stage.window.display.bounds.width);
-        FlxG.stage.window.height = Std.int(FlxG.stage.window.display.bounds.height);
-        FlxG.stage.window.x = Std.int(FlxG.stage.window.display.bounds.x);
-        FlxG.stage.window.y = Std.int(FlxG.stage.window.display.bounds.y);
-      }
-          }
-     */
-
-    #if debug
-    if (FlxG.keys.justPressed.EIGHT) FlxG.switchState(new CutsceneAnimTestState());
-    #end
-
     if (FlxG.sound.music != null) Conductor.songPosition = FlxG.sound.music.time;
     if (FlxG.keys.justPressed.F) FlxG.fullscreen = !FlxG.fullscreen;
 
@@ -373,8 +334,7 @@ class TitleState extends MusicBeatState
       #if newgrounds
       if (!OutdatedSubState.leftState)
       {
-        NGio.checkVersion(function(version)
-        {
+        NGio.checkVersion(function(version) {
           // Check if version is outdated
           var localVersion:String = "v" + Application.current.meta.get('version');
           var onlineVersion = version.split(" ")[0].trim();
@@ -391,8 +351,7 @@ class TitleState extends MusicBeatState
         });
       }
       #end
-      new FlxTimer().start(2, function(tmr:FlxTimer)
-      {
+      new FlxTimer().start(2, function(tmr:FlxTimer) {
         // These assets are very unlikely to be used for the rest of gameplay, so it unloads them from cache/memory
         // Saves about 50mb of RAM or so???
         Assets.cache.clear(Paths.image('gfDanceTitle'));
diff --git a/source/funkin/play/Countdown.hx b/source/funkin/play/Countdown.hx
index 0af098dbd..f7e072070 100644
--- a/source/funkin/play/Countdown.hx
+++ b/source/funkin/play/Countdown.hx
@@ -37,7 +37,7 @@ class Countdown
     // Stop any existing countdown.
     stopCountdown();
 
-    PlayState.isInCountdown = true;
+    PlayState.instance.isInCountdown = true;
     Conductor.songPosition = Conductor.crochet * -5;
     // Handle onBeatHit events manually
     @:privateAccess
@@ -46,8 +46,7 @@ class Countdown
     // The timer function gets called based on the beat of the song.
     countdownTimer = new FlxTimer();
 
-    countdownTimer.start(Conductor.crochet / 1000, function(tmr:FlxTimer)
-    {
+    countdownTimer.start(Conductor.crochet / 1000, function(tmr:FlxTimer) {
       countdownStep = decrement(countdownStep);
 
       // Handle onBeatHit events manually
@@ -216,8 +215,7 @@ class Countdown
     FlxTween.tween(countdownSprite, {y: countdownSprite.y += 100, alpha: 0}, Conductor.crochet / 1000,
       {
         ease: FlxEase.cubeInOut,
-        onComplete: function(twn:FlxTween)
-        {
+        onComplete: function(twn:FlxTween) {
           countdownSprite.destroy();
         }
       });
diff --git a/source/funkin/play/GameOverSubstate.hx b/source/funkin/play/GameOverSubstate.hx
index aa121ac36..f0694c818 100644
--- a/source/funkin/play/GameOverSubstate.hx
+++ b/source/funkin/play/GameOverSubstate.hx
@@ -153,11 +153,9 @@ class GameOverSubState extends MusicBeatSubState
     // KEYBOARD ONLY: Return to the menu when pressing the assigned key.
     if (controls.BACK)
     {
-      PlayState.deathCounter = 0;
-      PlayState.seenCutscene = false;
       gameOverMusic.stop();
 
-      if (PlayState.isStoryMode) FlxG.switchState(new StoryMenuState());
+      if (PlayStatePlaylist.isStoryMode) FlxG.switchState(new StoryMenuState());
       else
         FlxG.switchState(new FreeplayState());
     }
@@ -171,11 +169,11 @@ class GameOverSubState extends MusicBeatSubState
     else
     {
       // Music hasn't started yet.
-      switch (PlayState.storyWeek)
+      switch (PlayStatePlaylist.campaignId)
       {
         // TODO: Make the behavior for playing Jeff's voicelines generic or un-hardcoded.
         // This will simplify the class and make it easier for mods to add death quotes.
-        case 7:
+        case "week7":
           if (boyfriend.getCurrentAnimation().startsWith('firstDeath') && boyfriend.isAnimationFinished() && !playingJeffQuote)
           {
             playingJeffQuote = true;
@@ -214,7 +212,7 @@ class GameOverSubState extends MusicBeatSubState
         FlxG.camera.fade(FlxColor.BLACK, 2, false, function() {
           // ...close the GameOverSubState.
           FlxG.camera.fade(FlxColor.BLACK, 1, true, null, true);
-          PlayState.needsReset = true;
+          PlayState.instance.needsReset = true;
 
           // Readd Boyfriend to the stage.
           boyfriend.isDead = false;
diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx
index adcc509c5..bac61d854 100644
--- a/source/funkin/play/PlayState.hx
+++ b/source/funkin/play/PlayState.hx
@@ -1,5 +1,6 @@
 package funkin.play;
 
+import funkin.ui.story.StoryMenuState;
 import flixel.addons.display.FlxPieDial;
 import flixel.addons.transition.FlxTransitionableState;
 import flixel.FlxCamera;
@@ -969,10 +970,10 @@ class PlayState extends MusicBeatState
       oldNote = newNote;
 
       // Generate X sustain notes.
-      var sustainSections = Math.round(songNote.length / Conductor.stepLengthMs);
+      var sustainSections = Math.round(songNote.length / Conductor.stepCrochet);
       for (noteIndex in 0...sustainSections)
       {
-        var noteTimeOffset:Float = Conductor.stepLengthMs + (Conductor.stepLengthMs * noteIndex);
+        var noteTimeOffset:Float = Conductor.stepCrochet + (Conductor.stepCrochet * noteIndex);
         var sustainNote:Note = new Note(songNote.time + noteTimeOffset, songNote.data, oldNote, true, strumlineStyle);
         sustainNote.mustPress = mustHitNote;
         sustainNote.data.noteKind = songNote.kind;
@@ -1573,7 +1574,7 @@ class PlayState extends MusicBeatState
           Highscore.saveWeekScoreForDifficulty(PlayStatePlaylist.campaignId, PlayStatePlaylist.campaignScore, currentDifficulty);
         }
 
-        FlxG.save.data.weekUnlocked = StoryMenuState.weekUnlocked;
+        // FlxG.save.data.weekUnlocked = StoryMenuState.weekUnlocked;
         FlxG.save.flush();
 
         moveToResultsScreen();
@@ -2213,7 +2214,7 @@ class PlayState extends MusicBeatState
 
       var frameShit:Float = (1 / 24) * 2; // equals 2 frames in the animation
 
-      new FlxTimer().start(((Conductor.beatLengthMs / 1000) * 1.25) - frameShit, function(tmr) {
+      new FlxTimer().start(((Conductor.crochet / 1000) * 1.25) - frameShit, function(tmr) {
         animShit.forceFinish();
       });
     }
diff --git a/source/funkin/play/PlayStatePlaylist.hx b/source/funkin/play/PlayStatePlaylist.hx
new file mode 100644
index 000000000..acfd26752
--- /dev/null
+++ b/source/funkin/play/PlayStatePlaylist.hx
@@ -0,0 +1,56 @@
+package funkin.play;
+
+import funkin.util.Constants;
+
+/**
+ * Manages playback of multiple songs in a row.
+ * 
+ * TODO: Add getters/setters for all these properties to validate them.
+ */
+class PlayStatePlaylist
+{
+  /**
+   * Whether the game is currently in Story Mode. If false, we are in Free Play Mode.
+   */
+  public static var isStoryMode(default, default):Bool = false;
+
+  /**
+   * The loist of upcoming songs to be played.
+   * When the user completes a song in Story Mode, the first entry in this list is played.
+   * When this list is empty, move to the Results screen instead.
+   */
+  public static var playlistSongIds:Array<String> = [];
+
+  /**
+   * The cumulative score for all the songs in the playlist.
+   */
+  public static var campaignScore:Int = 0;
+
+  /**
+   * The title of this playlist, for example `Week 4` or `Weekend 1`
+   */
+  public static var campaignTitle:String = 'UNKNOWN';
+
+  /**
+   * The internal ID of the current playlist, for example `week4` or `weekend-1`.
+   */
+  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;
+
+  /**
+   * Resets the playlist to its default state.
+   */
+  public static function reset():Void
+  {
+    isStoryMode = false;
+    playlistSongIds = [];
+    campaignScore = 0;
+    campaignTitle = 'UNKNOWN';
+    campaignId = 'unknown';
+    currentDifficulty = Constants.DEFAULT_DIFFICULTY;
+  }
+}
diff --git a/source/funkin/play/ResultState.hx b/source/funkin/play/ResultState.hx
index 6b9167846..12be46fc8 100644
--- a/source/funkin/play/ResultState.hx
+++ b/source/funkin/play/ResultState.hx
@@ -118,16 +118,16 @@ class ResultState extends MusicBeatSubState
 
     difficulty = new FlxSprite(555);
 
-    var diffSpr:String = switch (CoolUtil.difficultyString())
+    var diffSpr:String = switch (PlayState.instance.currentDifficulty)
     {
-      case "EASY":
-        "difEasy";
-      case "NORMAL":
-        "difNormal";
-      case "HARD":
-        "difHard";
+      case 'EASY':
+        'difEasy';
+      case 'NORMAL':
+        'difNormal';
+      case 'HARD':
+        'difHard';
       case _:
-        "difNormal";
+        'difNormal';
     }
 
     difficulty.loadGraphic(Paths.image("resultScreen/" + diffSpr));
@@ -144,7 +144,7 @@ class ResultState extends MusicBeatSubState
     }
     else
     {
-      songName.text += PlayState.currentSong.song;
+      songName.text += PlayState.instance.currentSong.songId;
     }
 
     songName.antialiasing = true;
diff --git a/source/funkin/play/event/ZoomCameraSongEvent.hx b/source/funkin/play/event/ZoomCameraSongEvent.hx
index bceeb251a..e6e9c843d 100644
--- a/source/funkin/play/event/ZoomCameraSongEvent.hx
+++ b/source/funkin/play/event/ZoomCameraSongEvent.hx
@@ -76,8 +76,7 @@ class ZoomCameraSongEvent extends SongEvent
           return;
         }
 
-        FlxTween.tween(PlayState.instance, {defaultCameraZoom: zoom * FlxCamera.defaultZoom}, (Conductor.stepLengthMs * duration / 1000),
-          {ease: easeFunction});
+        FlxTween.tween(PlayState.instance, {defaultCameraZoom: zoom * FlxCamera.defaultZoom}, (Conductor.stepCrochet * duration / 1000), {ease: easeFunction});
     }
   }
 
diff --git a/source/funkin/ui/stageBuildShit/StageOffsetSubstate.hx b/source/funkin/ui/stageBuildShit/StageOffsetSubstate.hx
index f6b9cae3d..c757ce72e 100644
--- a/source/funkin/ui/stageBuildShit/StageOffsetSubstate.hx
+++ b/source/funkin/ui/stageBuildShit/StageOffsetSubstate.hx
@@ -260,7 +260,7 @@ class StageOffsetSubState extends HaxeUISubState
       // if (uiStuff != null) remove(uiStuff);
 
       // uiStuff = null;
-      PlayState.disableKeys = false;
+      PlayState.instance.disableKeys = false;
       PlayState.instance.resetCamera();
       FlxG.mouse.visible = false;
       close();
diff --git a/source/funkin/ui/story/StoryMenuState.hx b/source/funkin/ui/story/StoryMenuState.hx
index fadc7bbee..2ff0c0235 100644
--- a/source/funkin/ui/story/StoryMenuState.hx
+++ b/source/funkin/ui/story/StoryMenuState.hx
@@ -13,6 +13,8 @@ import funkin.data.level.LevelRegistry;
 import funkin.modding.events.ScriptEvent;
 import funkin.modding.events.ScriptEventDispatcher;
 import funkin.play.PlayState;
+import funkin.play.PlayStatePlaylist;
+import funkin.play.song.Song;
 import funkin.play.song.SongData.SongDataParser;
 import funkin.util.Constants;
 
@@ -474,26 +476,25 @@ class StoryMenuState extends MusicBeatState
       prop.playConfirm();
     }
 
-    PlayState.storyPlaylist = currentLevel.getSongs();
-    PlayState.isStoryMode = true;
-
-    PlayState.currentSong = SongLoad.loadFromJson(PlayState.storyPlaylist[0].toLowerCase(), PlayState.storyPlaylist[0].toLowerCase());
-    PlayState.currentSong_NEW = SongDataParser.fetchSong(PlayState.storyPlaylist[0].toLowerCase());
-
     Paths.setCurrentLevel(currentLevel.id);
 
-    // TODO: Fix this.
-    PlayState.storyWeek = 0;
-    PlayState.campaignScore = 0;
+    PlayStatePlaylist.playlistSongIds = currentLevel.getSongs();
+    PlayStatePlaylist.isStoryMode = true;
+    PlayStatePlaylist.campaignScore = 0;
 
-    // TODO: Fix this.
-    PlayState.storyDifficulty = 0;
-    PlayState.storyDifficulty_NEW = currentDifficultyId;
+    var targetSongId:String = PlayStatePlaylist.playlistSongIds.shift();
 
-    SongLoad.curDiff = PlayState.storyDifficulty_NEW;
+    var targetSong:Song = SongDataParser.fetchSong(targetSongId);
+
+    PlayStatePlaylist.campaignId = currentLevel.id;
+    PlayStatePlaylist.campaignTitle = currentLevel.getTitle();
 
     new FlxTimer().start(1, function(tmr:FlxTimer) {
-      LoadingState.loadAndSwitchState(new PlayState(), true);
+      LoadingState.loadAndSwitchState(new PlayState(
+        {
+          targetSong: targetSong,
+          targetDifficulty: currentDifficultyId,
+        }), true);
     });
   }