diff --git a/Project.xml b/Project.xml
index 8c133047d..8208dc649 100644
--- a/Project.xml
+++ b/Project.xml
@@ -29,9 +29,6 @@
-
-
-
@@ -40,12 +37,9 @@
-
-
-
@@ -102,9 +96,8 @@
-
+
-
@@ -223,6 +216,28 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/README.md b/README.md
index 4d209a9b5..d868c6574 100644
--- a/README.md
+++ b/README.md
@@ -2,19 +2,26 @@
This is the repository for Friday Night Funkin, a game originally made for Ludum Dare 47 "Stuck In a Loop".
-Play the Ludum Dare prototype here: https://ninja-muffin24.itch.io/friday-night-funkin
-Play the Newgrounds one here: https://www.newgrounds.com/portal/view/770371
-Support the project on the itch.io page: https://ninja-muffin24.itch.io/funkin
+Play free in your browser on Newgrounds: https://www.newgrounds.com/portal/view/770371
+Download the game for desktop on Itch.io: https://ninja-muffin24.itch.io/funkin
+
+# Credits
+
+- [ninjamuffin99](https://twitter.com/ninja_muffin99) - Programmer
+- [PhantomArcade3K](https://twitter.com/phantomarcade3k) and [Evilsk8r](https://twitter.com/evilsk8r) - Art
+- [Kawaisprite](https://twitter.com/kawaisprite) - Musician
+
+This game was made with love to Newgrounds and it's community. Extra love to Tom Fulp.
+
+
IF YOU MAKE A MOD AND DISTRIBUTE A MODIFIED / RECOMIPLED VERSION, YOU MUST OPEN SOURCE YOUR MOD AS WELL
## Credits / shoutouts
-- [ninjamuffin99 (me!)](https://twitter.com/ninja_muffin99) - Programmer
-- [PhantomArcade3K](https://twitter.com/phantomarcade3k) and [Evilsk8r](https://twitter.com/evilsk8r) - Art
-- [Kawaisprite](https://twitter.com/kawaisprite) - Musician
-This game was made with love to Newgrounds and it's community. Extra love to Tom Fulp.
+
+
## Build instructions
diff --git a/example_mods/.gitignore b/example_mods/.gitignore
new file mode 100644
index 000000000..cd193dac6
--- /dev/null
+++ b/example_mods/.gitignore
@@ -0,0 +1,2 @@
+./tricky
+./enaSkin
\ No newline at end of file
diff --git a/example_mods/modList.txt b/example_mods/modList.txt
deleted file mode 100644
index 3e2492595..000000000
--- a/example_mods/modList.txt
+++ /dev/null
@@ -1 +0,0 @@
-introMod
\ No newline at end of file
diff --git a/example_mods/readme.txt b/example_mods/readme.txt
deleted file mode 100644
index f8d8f6e7e..000000000
--- a/example_mods/readme.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-THIS MOD FOLDER DOES NOT ENTIRELY WORK JUST YET!!!
-DONT EXPECT MUCH OUT OF IT RIGHT NOW!!!
\ No newline at end of file
diff --git a/example_mods/testing123/_polymod_icon.png b/example_mods/testing123/_polymod_icon.png
new file mode 100644
index 000000000..2828bd554
Binary files /dev/null and b/example_mods/testing123/_polymod_icon.png differ
diff --git a/example_mods/testing123/_polymod_meta.json b/example_mods/testing123/_polymod_meta.json
new file mode 100644
index 000000000..e74efc4f3
--- /dev/null
+++ b/example_mods/testing123/_polymod_meta.json
@@ -0,0 +1,8 @@
+{
+ "title": "Testing123",
+ "description": "Newgrounds? More like OLDGROUNDS lol.",
+ "author": "MasterEric",
+ "api_version": "0.1.0",
+ "mod_version": "1.0.0",
+ "license": "Apache-2.0"
+}
diff --git a/source/Main.hx b/source/Main.hx
index 7b4429484..0c219844c 100644
--- a/source/Main.hx
+++ b/source/Main.hx
@@ -1,5 +1,6 @@
package;
+import modding.PolymodHandler;
import flixel.FlxGame;
import flixel.FlxState;
import flixel.util.FlxColor;
@@ -41,30 +42,14 @@ class Main extends Sprite
{
super();
- #if polymod
- polymod.Polymod.init({
- modRoot: "mods",
- dirs: ['testing123'],
- frameworkParams: {
- assetLibraryPaths: [
- "songs" => "songs", "shared" => "shared", "tutorial" => "tutorial", "week1" => "week1", "week2" => "week2", "week3" => "week3",
- "week4" => "week4", "week5" => "week5", "week6" => "week6", "week7" => "week7", "week8" => "week8"
- ]
- },
- framework: OPENFL,
- errorCallback: function(error:polymod.Polymod.PolymodError)
- {
- trace("POLYMOD ERROR! code = "
- + error.code
- + " severity = "
- + error.severity
- + " origin = "
- + error.origin
- + " message = "
- + error.message);
- }
- });
- #end
+ // TODO: Ideally this should change to utilize a user interface.
+ // 1. Call PolymodHandler.getAllMods(). This gives you an array of ModMetadata items,
+ // each of which contains information about the mod including an icon.
+ // 2. Provide an interface to enable, disable, and reorder enabled mods.
+ // A design similar to that of Minecraft resource packs would be intuitive.
+ // 3. The interface should save (to the save file) and output an ordered array of mod IDs.
+ // 4. Replace the call to PolymodHandler.loadAllMods() with a call to PolymodHandler.loadModsById(ids:Array).
+ PolymodHandler.loadAllMods();
if (stage != null)
{
diff --git a/source/TitleState.hx b/source/TitleState.hx
index 084f7e3c5..e5271ed6a 100644
--- a/source/TitleState.hx
+++ b/source/TitleState.hx
@@ -278,7 +278,7 @@ class TitleState extends MusicBeatState
override function update(elapsed:Float)
{
- trace(FlxG.renderBlit);
+ // trace(FlxG.renderBlit);
#if HAS_PITCH
if (FlxG.keys.pressed.UP)
diff --git a/source/modding/IHook.hx b/source/modding/IHook.hx
new file mode 100644
index 000000000..a1a75d08d
--- /dev/null
+++ b/source/modding/IHook.hx
@@ -0,0 +1,13 @@
+package modding;
+
+import polymod.hscript.HScriptable;
+
+/**
+ * Add this interface to a class to make it a scriptable object.
+ * Functions annotated with @:hscript will call the relevant script.
+ */
+@:hscript({
+ // ALL of these values are added to ALL scripts in the child classes.
+ context: [FlxG, FlxSprite, Math, Paths, Std]
+})
+interface IHook extends HScriptable {}
diff --git a/source/modding/PolymodHandler.hx b/source/modding/PolymodHandler.hx
new file mode 100644
index 000000000..3d3c9bace
--- /dev/null
+++ b/source/modding/PolymodHandler.hx
@@ -0,0 +1,201 @@
+package modding;
+
+#if cpp
+import polymod.Polymod;
+import polymod.backends.OpenFLBackend;
+import polymod.backends.PolymodAssets.PolymodAssetType;
+import polymod.format.ParseRules.LinesParseFormat;
+import polymod.format.ParseRules.TextFileFormat;
+#end
+
+class PolymodHandler
+{
+ /**
+ * The API version that mods should comply with.
+ * Format this with Semantic Versioning; ...
+ * Bug fixes increment the patch version, new features increment the minor version.
+ * Changes that break old mods increment the major version.
+ */
+ static final API_VERSION = "0.1.0";
+
+ /**
+ * Where relative to the executable that mods are located.
+ */
+ static final MOD_FOLDER = "mods";
+
+ /**
+ * Loads the game with ALL mods enabled with Polymod.
+ */
+ public static function loadAllMods()
+ {
+ #if cpp
+ trace("Initializing Polymod (using all mods)...");
+ loadModsById(getAllModIds());
+ #else
+ trace("Polymod not initialized; not supported on this platform.");
+ #end
+ }
+
+ /**
+ * Loads the game without any mods enabled with Polymod.
+ */
+ public static function loadNoMods()
+ {
+ // We still need to configure the debug print calls etc.
+ #if cpp
+ trace("Initializing Polymod (using no mods)...");
+ loadModsById([]);
+ #else
+ trace("Polymod not initialized; not supported on this platform.");
+ #end
+ }
+
+ public static function loadModsById(ids:Array)
+ {
+ #if cpp
+ if (ids.length == 0)
+ {
+ trace('You attempted to load zero mods.');
+ }
+ else
+ {
+ trace('Attempting to load ${ids.length} mods...');
+ }
+ var loadedModList = polymod.Polymod.init({
+ // Root directory for all mods.
+ modRoot: MOD_FOLDER,
+ // The directories for one or more mods to load.
+ dirs: ids,
+ // Framework being used to load assets.
+ framework: OPENFL,
+ // The current version of our API.
+ apiVersion: API_VERSION,
+ // Call this function any time an error occurs.
+ errorCallback: onPolymodError,
+ // Enforce semantic version patterns for each mod.
+ // modVersions: null,
+ // A map telling Polymod what the asset type is for unfamiliar file extensions.
+ // extensionMap: [],
+
+ frameworkParams: buildFrameworkParams(),
+
+ // List of filenames to ignore in mods. Use the default list to ignore the metadata file, etc.
+ ignoredFiles: Polymod.getDefaultIgnoreList(),
+
+ // Parsing rules for various data formats.
+ parseRules: buildParseRules(),
+ });
+
+ if (loadedModList == null)
+ {
+ trace('[POLYMOD] An error occurred! Failed when loading mods!');
+ }
+ else
+ {
+ if (loadedModList.length == 0)
+ {
+ trace('[POLYMOD] Mod loading complete. We loaded no mods / ${ids.length} mods.');
+ }
+ else
+ {
+ trace('[POLYMOD] Mod loading complete. We loaded ${loadedModList.length} / ${ids.length} mods.');
+ }
+ }
+
+ for (mod in loadedModList)
+ trace(' * ${mod.title} v${mod.modVersion} [${mod.id}]');
+
+ #if debug
+ var fileList = Polymod.listModFiles("IMAGE");
+ trace('[POLYMOD] Installed mods have replaced ${fileList.length} images.');
+ for (item in fileList)
+ trace(' * $item');
+
+ fileList = Polymod.listModFiles("TEXT");
+ trace('[POLYMOD] Installed mods have replaced ${fileList.length} text files.');
+ for (item in fileList)
+ trace(' * $item');
+
+ fileList = Polymod.listModFiles("MUSIC");
+ trace('[POLYMOD] Installed mods have replaced ${fileList.length} music files.');
+ for (item in fileList)
+ trace(' * $item');
+
+ fileList = Polymod.listModFiles("SOUND");
+ trace('[POLYMOD] Installed mods have replaced ${fileList.length} sound files.');
+ for (item in fileList)
+ trace(' * $item');
+ #end
+ #else
+ trace("[POLYMOD] Mods are not supported on this platform.");
+ #end
+ }
+
+ #if cpp
+ static function buildParseRules():polymod.format.ParseRules
+ {
+ var output = polymod.format.ParseRules.getDefault();
+ // Ensure TXT files have merge support.
+ output.addType("txt", TextFileFormat.LINES);
+ // Ensure script files have merge support.
+ output.addType("hscript", TextFileFormat.PLAINTEXT);
+
+ // You can specify the format of a specific file, with file extension.
+ // output.addFile("data/introText.txt", TextFileFormat.LINES)
+ return output;
+ }
+
+ static inline function buildFrameworkParams():polymod.Polymod.FrameworkParams
+ {
+ return {
+ assetLibraryPaths: [
+ "songs" => "./songs", "shared" => "./", "tutorial" => "./tutorial", "scripts" => "./scripts", "week1" => "./week1", "week2" => "./week2",
+ "week3" => "./week3", "week4" => "./week4", "week5" => "./week5", "week6" => "./week6", "week7" => "./week7", "week8" => "./week8",
+ ]
+ }
+ }
+
+ static function onPolymodError(error:PolymodError):Void
+ {
+ // Perform an action based on the error code.
+ switch (error.code)
+ {
+ case MOD_LOAD_PREPARE:
+ trace('[POLYMOD] ${error.message}');
+ case MOD_LOAD_DONE:
+ trace('[POLYMOD] ${error.message}');
+ case MISSING_ICON:
+ trace('[POLYMOD] A mod is missing an icon. Please add one.');
+ default:
+ // Log the message based on its severity.
+ switch (error.severity)
+ {
+ case NOTICE:
+ trace('[POLYMOD] ${error.message}');
+ case WARNING:
+ trace('[POLYMOD] ${error.message}');
+ case ERROR:
+ trace('[POLYMOD] ${error.message}');
+ }
+ }
+ }
+ #end
+
+ public static function getAllMods():Array
+ {
+ #if cpp
+ trace('Scanning the mods folder...');
+ var modMetadata = Polymod.scan(MOD_FOLDER);
+ trace('Found ${modMetadata.length} mods when scanning.');
+ return modMetadata;
+ #else
+ return [];
+ #end
+ }
+
+ public static function getAllModIds():Array
+ {
+ var modIds = [for (i in getAllMods()) i.id];
+ return modIds;
+ }
+}