diff --git a/source/funkin/InitState.hx b/source/funkin/InitState.hx
index 399f52498..ec6a4e6f0 100644
--- a/source/funkin/InitState.hx
+++ b/source/funkin/InitState.hx
@@ -50,11 +50,13 @@ class InitState extends FlxState
    */
   public override function create():Void
   {
+    // Setup a bunch of important Flixel stuff.
     setupShit();
 
-    // loadSaveData(); // Moved to Main.hx
     // Load player options from save data.
+    // Flixel has already loaded the save data, so we can just use it.
     Preferences.init();
+
     // Load controls from save data.
     PlayerSettings.init();
 
@@ -198,6 +200,10 @@ class InitState extends FlxState
     //
     // FLIXEL PLUGINS
     //
+    // Plugins provide a useful interface for globally active Flixel objects,
+    // that receive update events regardless of the current state.
+    // TODO: Move Module behavior to a Flixel plugin.
+    funkin.util.plugins.ScreenshotPlugin.initialize();
     funkin.util.plugins.EvacuateDebugPlugin.initialize();
     funkin.util.plugins.ReloadAssetsDebugPlugin.initialize();
     funkin.util.plugins.WatchPlugin.initialize();
diff --git a/source/funkin/Preferences.hx b/source/funkin/Preferences.hx
index 6b0911ede..039a4c285 100644
--- a/source/funkin/Preferences.hx
+++ b/source/funkin/Preferences.hx
@@ -20,7 +20,10 @@ class Preferences
 
   static function set_naughtyness(value:Bool):Bool
   {
-    return Save.get().options.naughtyness = value;
+    var save = Save.get();
+    save.options.naughtyness = value;
+    save.flush();
+    return value;
   }
 
   /**
@@ -36,7 +39,10 @@ class Preferences
 
   static function set_downscroll(value:Bool):Bool
   {
-    return Save.get().options.downscroll = value;
+    var save = Save.get();
+    save.options.downscroll = value;
+    save.flush();
+    return value;
   }
 
   /**
@@ -52,7 +58,10 @@ class Preferences
 
   static function set_flashingLights(value:Bool):Bool
   {
-    return Save.get().options.flashingLights = value;
+    var save = Save.get();
+    save.options.flashingLights = value;
+    save.flush();
+    return value;
   }
 
   /**
@@ -68,7 +77,10 @@ class Preferences
 
   static function set_zoomCamera(value:Bool):Bool
   {
-    return Save.get().options.zoomCamera = value;
+    var save = Save.get();
+    save.options.zoomCamera = value;
+    save.flush();
+    return value;
   }
 
   /**
@@ -89,7 +101,10 @@ class Preferences
       toggleDebugDisplay(value);
     }
 
-    return Save.get().options.debugDisplay = value;
+    var save = Save.get();
+    save.options.debugDisplay = value;
+    save.flush();
+    return value;
   }
 
   /**
@@ -107,7 +122,10 @@ class Preferences
   {
     if (value != Save.get().options.autoPause) FlxG.autoPause = value;
 
-    return Save.get().options.autoPause = value;
+    var save = Save.get();
+    save.options.autoPause = value;
+    save.flush();
+    return value;
   }
 
   public static function init():Void
diff --git a/source/funkin/save/Save.hx b/source/funkin/save/Save.hx
index 6a4dd048c..5bbded231 100644
--- a/source/funkin/save/Save.hx
+++ b/source/funkin/save/Save.hx
@@ -113,6 +113,9 @@ abstract Save(RawSaveData)
       };
   }
 
+  /**
+   * NOTE: Modifications will not be saved without calling `Save.flush()`!
+   */
   public var options(get, never):SaveDataOptions;
 
   function get_options():SaveDataOptions
@@ -120,6 +123,9 @@ abstract Save(RawSaveData)
     return this.options;
   }
 
+  /**
+   * NOTE: Modifications will not be saved without calling `Save.flush()`!
+   */
   public var modOptions(get, never):Map<String, Dynamic>;
 
   function get_modOptions():Map<String, Dynamic>
diff --git a/source/funkin/util/FileUtil.hx b/source/funkin/util/FileUtil.hx
index 612737680..5d189c0e9 100644
--- a/source/funkin/util/FileUtil.hx
+++ b/source/funkin/util/FileUtil.hx
@@ -20,6 +20,7 @@ class FileUtil
 {
   public static final FILE_FILTER_FNFC:FileFilter = new FileFilter("Friday Night Funkin' Chart (.fnfc)", "*.fnfc");
   public static final FILE_FILTER_ZIP:FileFilter = new FileFilter("ZIP Archive (.zip)", "*.zip");
+  public static final FILE_FILTER_PNG:FileFilter = new FileFilter("PNG Image (.png)", "*.png");
 
   public static final FILE_EXTENSION_INFO_FNFC:FileDialogExtensionInfo =
     {
@@ -31,6 +32,11 @@ class FileUtil
       extension: 'zip',
       label: 'ZIP Archive',
     };
+  public static final FILE_EXTENSION_INFO_PNG:FileDialogExtensionInfo =
+    {
+      extension: 'png',
+      label: 'PNG Image',
+    };
 
   /**
    * Browses for a single file, then calls `onSelect(fileInfo)` when a file is selected.
diff --git a/source/funkin/util/plugins/ScreenshotPlugin.hx b/source/funkin/util/plugins/ScreenshotPlugin.hx
new file mode 100644
index 000000000..a8b494fee
--- /dev/null
+++ b/source/funkin/util/plugins/ScreenshotPlugin.hx
@@ -0,0 +1,269 @@
+package funkin.util.plugins;
+
+import flixel.FlxBasic;
+import flixel.FlxCamera;
+import flixel.FlxG;
+import flixel.FlxState;
+import flixel.graphics.FlxGraphic;
+import flixel.input.keyboard.FlxKey;
+import flixel.tweens.FlxEase;
+import flixel.tweens.FlxTween;
+import flixel.util.FlxColor;
+import flixel.util.FlxSignal;
+import flixel.util.FlxTimer;
+import funkin.graphics.FunkinSprite;
+import funkin.input.Cursor;
+import openfl.display.Bitmap;
+import openfl.display.BitmapData;
+import openfl.display.PNGEncoderOptions;
+import openfl.geom.Matrix;
+import openfl.geom.Rectangle;
+import openfl.utils.ByteArray;
+
+typedef ScreenshotPluginParams =
+{
+  hotkeys:Array<FlxKey>,
+  ?region:Rectangle,
+  shouldHideMouse:Bool,
+  flashColor:Null<FlxColor>,
+  fancyPreview:Bool,
+};
+
+/**
+ * What if `flixel.addons.plugin.screengrab.FlxScreenGrab` but it's better?
+ * TODO: Contribute this upstream.
+ */
+class ScreenshotPlugin extends FlxBasic
+{
+  public static final SCREENSHOT_FOLDER = 'screenshots';
+
+  var _hotkeys:Array<FlxKey>;
+
+  var _region:Null<Rectangle>;
+
+  var _shouldHideMouse:Bool;
+
+  var _flashColor:Null<FlxColor>;
+
+  var _fancyPreview:Bool;
+
+  /**
+   * A signal fired before the screenshot is taken.
+   */
+  public var onPreScreenshot(default, null):FlxTypedSignal<Void->Void>;
+
+  /**
+   * A signal fired after the screenshot is taken.
+   * @param bitmap The bitmap that was captured.
+   */
+  public var onPostScreenshot(default, null):FlxTypedSignal<Bitmap->Void>;
+
+  public function new(params:ScreenshotPluginParams)
+  {
+    super();
+
+    _hotkeys = params.hotkeys;
+    _region = params.region ?? null;
+    _shouldHideMouse = params.shouldHideMouse;
+    _flashColor = params.flashColor;
+    _fancyPreview = params.fancyPreview;
+
+    onPreScreenshot = new FlxTypedSignal<Void->Void>();
+    onPostScreenshot = new FlxTypedSignal<Bitmap->Void>();
+  }
+
+  public override function update(elapsed:Float):Void
+  {
+    super.update(elapsed);
+
+    if (FlxG.keys.anyJustReleased(_hotkeys))
+    {
+      capture();
+    }
+  }
+
+  /**
+   * Initialize the screenshot plugin.
+   */
+  public static function initialize():Void
+  {
+    FlxG.plugins.addPlugin(new ScreenshotPlugin(
+      {
+        flashColor: Preferences.flashingLights ? FlxColor.WHITE : null, // Was originally a black flash.
+
+        // TODO: Add a way to configure screenshots from the options menu.
+        hotkeys: [FlxKey.PRINTSCREEN],
+        shouldHideMouse: false,
+        fancyPreview: true, // TODO: Fancy preview is broken on substates.
+      }));
+  }
+
+  public function updatePreferences():Void
+  {
+    _flashColor = Preferences.flashingLights ? FlxColor.WHITE : null;
+  }
+
+  /**
+   * Defines the region of the screen that should be captured.
+   * You don't need to call this method if you want to capture the entire screen, that's the default behavior.
+   */
+  public function defineCaptureRegion(x:Int, y:Int, width:Int, height:Int):Void
+  {
+    _region = new Rectangle(x, y, width, height);
+  }
+
+  /**
+   * Capture the game screen as a bitmap.
+   */
+  public function capture():Void
+  {
+    onPreScreenshot.dispatch();
+
+    var captureRegion = _region != null ? _region : new Rectangle(0, 0, FlxG.stage.stageWidth, FlxG.stage.stageHeight);
+
+    var wasMouseHidden = false;
+    if (_shouldHideMouse && FlxG.mouse.visible)
+    {
+      wasMouseHidden = true;
+      Cursor.hide();
+    }
+
+    // The actual work.
+    // var bitmap = new Bitmap(new BitmapData(Math.floor(captureRegion.width), Math.floor(captureRegion.height), true, 0x00000000)); // Create a transparent empty bitmap.
+    // var drawMatrix = new Matrix(1, 0, 0, 1, -captureRegion.x, -captureRegion.y); // Modifying this will scale or skew the bitmap.
+    // bitmap.bitmapData.draw(FlxG.stage, drawMatrix);
+    var bitmap = new Bitmap(BitmapData.fromImage(FlxG.stage.window.readPixels()));
+
+    if (wasMouseHidden)
+    {
+      Cursor.show();
+    }
+
+    // Save the bitmap to a file.
+    saveScreenshot(bitmap);
+
+    // Show some feedback.
+    showCaptureFeedback();
+    if (_fancyPreview)
+    {
+      showFancyPreview(bitmap);
+    }
+
+    onPostScreenshot.dispatch(bitmap);
+  }
+
+  final CAMERA_FLASH_DURATION = 0.25;
+
+  /**
+   * Visual (and audio?) feedback when a screenshot is taken.
+   */
+  function showCaptureFeedback():Void
+  {
+    if (_flashColor != null)
+    {
+      for (camera in FlxG.cameras.list)
+      {
+        camera.flash(_flashColor, CAMERA_FLASH_DURATION);
+      }
+    }
+  }
+
+  static final PREVIEW_INITIAL_DELAY = 0.25; // How long before the preview starts fading in.
+  static final PREVIEW_FADE_IN_DURATION = 0.3; // How long the preview takes to fade in.
+  static final PREVIEW_FADE_OUT_DELAY = 0.25; // How long the preview stays on screen.
+  static final PREVIEW_FADE_OUT_DURATION = 0.3; // How long the preview takes to fade out.
+
+  function showFancyPreview(bitmap:Bitmap):Void
+  {
+    // TODO: This function looks really nice but breaks substates.
+    var targetCamera = new FlxCamera();
+    targetCamera.bgColor.alpha = 0; // Show the scene behind the camera.
+    FlxG.cameras.add(targetCamera);
+
+    var flxGraphic = FlxGraphic.fromBitmapData(bitmap.bitmapData, false, "screenshot", false);
+
+    var preview = new FunkinSprite(0, 0);
+    preview.frames = flxGraphic.imageFrame;
+    preview.setGraphicSize(bitmap.width / 4, bitmap.height / 4);
+    preview.x = FlxG.width - preview.width - 10;
+    preview.y = FlxG.height - preview.height - 10;
+
+    preview.alpha = 0.0;
+    preview.cameras = [targetCamera];
+    getCurrentState().add(preview);
+
+    // Wait to fade in.
+    new FlxTimer().start(PREVIEW_INITIAL_DELAY, function(_) {
+      // Fade in.
+      FlxTween.tween(preview, {alpha: 1.0}, PREVIEW_FADE_IN_DURATION,
+        {
+          ease: FlxEase.elasticOut,
+          onComplete: function(_) {
+            // Wait to fade out.
+            new FlxTimer().start(PREVIEW_FADE_OUT_DELAY, function(_) {
+              // Fade out.
+              FlxTween.tween(preview, {alpha: 0.0}, PREVIEW_FADE_OUT_DURATION,
+                {
+                  ease: FlxEase.elasticIn,
+                  onComplete: function(_) {
+                    preview.kill();
+                  }
+                });
+            });
+          }
+        });
+    });
+  }
+
+  static function getCurrentState():FlxState
+  {
+    var state = FlxG.state;
+    while (state.subState != null)
+    {
+      state = state.subState;
+    }
+    return state;
+  }
+
+  static function getScreenshotPath():String
+  {
+    return '$SCREENSHOT_FOLDER/screenshot-${DateUtil.generateTimestamp()}.png';
+  }
+
+  static function makeScreenshotPath():Void
+  {
+    FileUtil.createDirIfNotExists(SCREENSHOT_FOLDER);
+  }
+
+  /**
+   * Convert a Bitmap to a PNG ByteArray to save to a file.
+   */
+  static function encodePNG(bitmap:Bitmap):ByteArray
+  {
+    return bitmap.bitmapData.encode(bitmap.bitmapData.rect, new PNGEncoderOptions());
+  }
+
+  /**
+   * Save the generated bitmap to a file.
+   * @param bitmap The bitmap to save.
+   */
+  static function saveScreenshot(bitmap:Bitmap)
+  {
+    makeScreenshotPath();
+    var targetPath:String = getScreenshotPath();
+
+    var pngData = encodePNG(bitmap);
+
+    if (pngData == null)
+    {
+      trace('[WARN] Failed to encode PNG data.');
+      return;
+    }
+    else
+    {
+      trace('Saving screenshot to: ' + targetPath);
+      // TODO: Make this work on browser.
+      FileUtil.writeBytesToPath(targetPath, pngData);
+    }
+  }
+}