diff --git a/css/general/layout.css b/css/general/layout.css
index 877cf5d1015c1a6a5190e8868800b94b8ac93e54..a7e20161b5c234608c7c4a4aa7a973f93d88383c 100644
--- a/css/general/layout.css
+++ b/css/general/layout.css
@@ -43,6 +43,12 @@ div.grid-2columns-auto {
 	grid-column-gap: 1em;
 }
 
+div.grid-3columns-auto {
+	display: grid;
+	grid-template-columns: max-content auto auto;
+	grid-column-gap: 1em;
+}
+
 .margin-top {
 	margin-top: 1em;
 }
diff --git a/js/002-config/fc-js-init.js b/js/002-config/fc-js-init.js
index 436a152428ab08ce4f8c53bd46b98903305565f7..44af7f7416466653c6bd764d03bd231d0bf63866 100644
--- a/js/002-config/fc-js-init.js
+++ b/js/002-config/fc-js-init.js
@@ -75,6 +75,8 @@ App.SF = {};
 App.SecExp = {};
 App.SlaveAssignment = {};
 App.StartingGirls = {};
+App.Status = {};
+App.Status.storyReady = false;
 App.UI = {};
 App.UI.Cheat = {};
 App.UI.DOM = {};
diff --git a/js/dynamicJSLoading.js b/js/dynamicJSLoading.js
index aafbcf33d65380d6fd4c63cacc48ee5105f7725c..3eb225f9d8c362df620aed8a4b0fcb209e760fa5 100644
--- a/js/dynamicJSLoading.js
+++ b/js/dynamicJSLoading.js
@@ -106,6 +106,236 @@ App.Loader = (function() {
 		},
 		get lastScript() {
 			return lastScript;
+		},
+		get hasNextScript() {
+			return scriptQueue.length > 0;
 		}
 	};
 })();
+
+
+/**
+ * Dedicated Modding API to abstract the loading mechanisms
+ */
+App.Modding = (function() {
+	let loadingDone = false;
+	/**
+	 * @type {Mod}
+	 */
+	let currentMod = null;
+
+	/**
+	 * @type {string[]}
+	 */
+	let modsToLoad = [];
+
+	/**
+	 * @type {Mod[]}
+	 */
+	const loadedMods = [];
+
+	class Mod {
+		/**
+		 * @param {string} directory where this mod is located relative to 'mods/'
+		 */
+		constructor(directory) {
+			this._path = "./mods/" + directory;
+			this.name = directory;
+			this.version = "0";
+			this.description = "";
+
+			currentMod = this;
+			loadedMods.push(this);
+
+			App.Loader.loadGroup(this._path);
+			App.Loader.nextScript();
+		}
+
+		/**
+		 * @returns {string}
+		 */
+		get path() {
+			return this._path;
+		}
+
+		/**
+		 * @param {string} path
+		 */
+		addSubscript(path) {
+			App.Loader.getGroup(this._path).queueSubscript(path);
+		}
+	}
+
+	/**
+	 * Loads the next subscript or mod
+	 */
+	function nextScript() {
+		if (App.Loader.hasNextScript) {
+			App.Loader.nextScript();
+		} else if (modsToLoad.length > 0) {
+			new Mod(modsToLoad.shift());
+		} else {
+			loadingDone = true;
+		}
+	}
+
+	/**
+	 * Load all mods
+	 */
+	function loadMods() {
+		modsToLoad = loadModList();
+		nextScript();
+	}
+
+	/**
+	 * @returns {string[]} modList
+	 */
+	function loadModList() {
+		return SugarCube.storage.get("modList") || [];
+	}
+
+	return {
+		internal: {
+			load: loadMods,
+			/** @returns {string[]} modList */
+			get modList() {
+				return loadModList();
+			},
+			/** @param {string[]} modList */
+			set modList(modList) {
+				SugarCube.storage.set("modList", modList);
+			},
+			get loadedMods() {
+				return loadedMods;
+			},
+			get done() {
+				return loadingDone;
+			}
+		},
+		scriptDone: nextScript,
+		get currentMod() {
+			return currentMod;
+		},
+	};
+})();
+
+App.UI.playerMods = function() {
+	let modList = loadModList();
+	const container = document.createElement("div");
+	container.append(makeModSettings());
+	return container;
+
+	function makeModSettings() {
+		const f = new DocumentFragment();
+		App.UI.DOM.appendNewElement("h2", f, "Player mods");
+		App.UI.DOM.appendNewElement("div", f, "Player mods are mods loaded from external files located at 'mods/'. If a mod does not exist loading will fail and never finish. Mods are save independent.");
+		if (!App.Modding.internal.done) {
+			f.append("Mods have not finished loading. ", App.UI.DOM.link("Refresh", () => {
+				modList = loadModList();
+				refresh();
+			}));
+		}
+		App.UI.DOM.appendNewElement("h3", f, "Currently loaded mods");
+		f.append(loadedList());
+		App.UI.DOM.appendNewElement("h3", f, "Edit mod list");
+		f.append(makeEditor());
+		return f;
+	}
+
+	/**
+	 * @returns {string[]}
+	 */
+	function loadModList() {
+		if (App.Status.storyReady) {
+			return App.Modding.internal.modList;
+		} else {
+			return [];
+		}
+	}
+
+	function loadedList() {
+		const loadedMods = App.Modding.internal.loadedMods;
+		const div = document.createElement("div");
+		div.classList.add("grid-3columns-auto");
+		for (const mod of loadedMods) {
+			App.UI.DOM.appendNewElement("div", div, mod.name);
+			App.UI.DOM.appendNewElement("div", div, mod.version);
+			App.UI.DOM.appendNewElement("div", div, mod.description);
+		}
+		return div;
+	}
+
+	function refresh() {
+		$(container).empty().append(makeModSettings());
+	}
+
+	function makeEditor() {
+		const div = document.createElement("div");
+		div.append("Add new mod. Input the exact name of the directory for the mod at 'mods/': ",
+			App.UI.DOM.makeTextBox("", name => {
+				modList.push(name);
+				refresh();
+			}));
+
+		App.UI.DOM.appendNewElement("div", div, "Mods are loaded from top to bottom.");
+
+		const listDiv = document.createElement("div");
+		for (let i = 0; i < modList.length; i++) {
+			const row = document.createElement("div");
+			row.append(up(i), " ", down(i), " ", remove(i), " ", modList[i]);
+			listDiv.append(row);
+		}
+		div.append(listDiv);
+
+		div.append(App.UI.DOM.link("Finalize (Will reload the game!)", () => {
+			App.Modding.internal.modList = modList;
+			window.location.reload();
+		}));
+		return div;
+	}
+
+	/**
+	 * @param {number} index
+	 * @returns {HTMLElement}
+	 */
+	function up(index) {
+		if (index === 0) {
+			return App.UI.DOM.makeElement("span", "Up", ["gray"]);
+		}
+		const button = App.UI.DOM.makeElement("a", "Up");
+		button.onclick = () => {
+			arraySwap(modList, index, index - 1);
+			refresh();
+		};
+		return button;
+	}
+
+	/**
+	 * @param {number} index
+	 * @returns {HTMLElement}
+	 */
+	function down(index) {
+		if (index === modList.length - 1) {
+			return App.UI.DOM.makeElement("span", "Down", ["gray"]);
+		}
+		const button = App.UI.DOM.makeElement("a", "Down");
+		button.onclick = () => {
+			arraySwap(modList, index, index + 1);
+			refresh();
+		};
+		return button;
+	}
+
+	/**
+	 * @param {number} index
+	 * @returns {HTMLElement}
+	 */
+	function remove(index) {
+		const button = App.UI.DOM.makeElement("a", "Remove");
+		button.onclick = () => {
+			modList.splice(index, 1);
+			refresh();
+		};
+		return button;
+	}
+};
diff --git a/mods/example/index.js b/mods/example/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..67ff48ea01f56c20b7166fec46a0af68a0e403af
--- /dev/null
+++ b/mods/example/index.js
@@ -0,0 +1,24 @@
+// Always scope your code to not pollute the global namespace.
+{
+	// Get the mod so we can modify it:
+	const mod = App.Modding.currentMod;
+
+	// Set the name and version:
+	mod.name = "Example Mod";
+	mod.version = "1.0";
+	mod.description = "This is an example mod showing off the modding API and gives examples for moddable systems.";
+
+	// For bigger mods it can be useful to split the mod in multiple parts.
+	// These parts can then be loaded as subscripts.
+	// Subscripts will be loaded in series and only once this script is done.
+	mod.addSubscript("subscript");
+
+	// Usage examples for moddable systems
+	mod.addSubscript("rulesAssistantGetters");
+
+	// Write to console, so we can easily see that the mod has loaded:
+	console.log("Index Loading: Success!");
+
+	// Tell the modding system this file is done. Do this at the end of EVERY script file.
+	App.Modding.scriptDone();
+}
diff --git a/mods/example/rulesAssistantGetters.js b/mods/example/rulesAssistantGetters.js
new file mode 100644
index 0000000000000000000000000000000000000000..e72951818c0e7f4e9ce221d324d7629905062467
--- /dev/null
+++ b/mods/example/rulesAssistantGetters.js
@@ -0,0 +1,18 @@
+// Add a new boolean getter for use in the RA condition editor.
+// The first argument is a unique name for the getter. To ensure uniqueness with other mods it is recommended to
+// prefix the name with the mod name.
+// The second argument is an object describing the getter.
+// 'name' is the string shown in the RA condition editor when selecting.
+// 'description' should be a short description of the value the getter returns, with possible values if applicable.
+//      It will be displayed in the encyclopedia.
+// 'val' is the actual getter reading out the value and returning it. It operates on a 'context' object with the current
+//      slave as an attribute.
+// It is important that the getter always returns the data type that it is being added as, in this case 'string'.
+// 'addNumber()' and 'addBoolean()' also exist. The usage is the same.
+App.RA.Activation.getterManager.addString("example_slavename",
+	{name: "Slave Name", description: "The slave's name.", val: context => context.slave.slaveName}
+);
+
+console.log("Rule Assistant Getters loaded.");
+
+App.Modding.scriptDone();
diff --git a/mods/example/subscript.js b/mods/example/subscript.js
new file mode 100644
index 0000000000000000000000000000000000000000..98b9946a62ee900a8c75fbf2f40c967ef4593f96
--- /dev/null
+++ b/mods/example/subscript.js
@@ -0,0 +1,5 @@
+// Show the subscript loaded successfully:
+console.log("Subscript loaded!");
+
+// Tell the modding system this file is done. Do this at the end of EVERY script file.
+App.Modding.scriptDone();
diff --git a/mods/mods.md b/mods/mods.md
new file mode 100644
index 0000000000000000000000000000000000000000..e9eeb5db4b805293c088b72a73f9a1cfa205946d
--- /dev/null
+++ b/mods/mods.md
@@ -0,0 +1,16 @@
+# Player Mods
+
+Player mods are scripts that are loaded after loading the game itself.
+
+There is currently very limited support for these mods with few systems exposing an API for modding.
+
+To use the mods in this directory copy the `mods` directory directly next to the game HTML file like this:
+
+* `FC_pregmod.html`
+* `mods/`
+    * `example/`
+        * `index.js`
+        * ...
+    * ...
+
+The example mod explains the modding API itself and has examples for all moddable systems.
diff --git a/src/gui/options/options.js b/src/gui/options/options.js
index cdb48250c30b3fae8e60aef33bbf3ed55d13a747..ebda1302ef01f94bb3b9419ec40f57642bf046f9 100644
--- a/src/gui/options/options.js
+++ b/src/gui/options/options.js
@@ -778,6 +778,8 @@ App.UI.optionsPassage = function() {
 			.addComment("This will enable a new way to interact with slaves. Currently working but missing flavor text.");
 
 		el.append(options.render());
+
+		el.append(App.UI.playerMods());
 		return el;
 	}
 };
diff --git a/src/js/eventHandlers.js b/src/js/eventHandlers.js
index c72f2e3efddaaa583adc4962106404e95bf8b628..521bb80790a99932e8c9cc5a1df85733d39dc7fd 100644
--- a/src/js/eventHandlers.js
+++ b/src/js/eventHandlers.js
@@ -33,9 +33,12 @@ App.EventHandlers = function() {
 	}
 
 	function storyReady() {
+		App.Status.storyReady = true;
 		App.UI.Theme.init();
 		App.UI.Hotkeys.init();
 		App.UI.GlobalTooltips.update();
+		// Load player mods. Do this last
+		App.Modding.internal.load();
 	}
 
 	function optionsChanged() {