diff --git a/.github/workflows/build-shit.yml b/.github/workflows/build-shit.yml
index 76126d106..8ea3b16f3 100644
--- a/.github/workflows/build-shit.yml
+++ b/.github/workflows/build-shit.yml
@@ -31,7 +31,7 @@ jobs:
sudo apt-get install -y libx11-dev xorg-dev libgl-dev libxi-dev libxext-dev libasound2-dev libxinerama-dev libxrandr-dev libgl1-mesa-dev
- name: build game
run: |
- haxelib run lime build html5 -release --times
+ haxelib run lime build html5 -release --times -DGITHUB_BUILD
ls
- uses: ./.github/actions/upload-itch
with:
@@ -68,7 +68,7 @@ jobs:
key: ${{ runner.os }}-build-win-${{ github.ref_name }}-${{ hashFiles('**/hmm.json') }}
- name: build game
run: |
- haxelib run lime build windows -release -DNO_REDIRECT_ASSETS_FOLDER
+ haxelib run lime build windows -release -DNO_REDIRECT_ASSETS_FOLDER -DGITHUB_BUILD
dir
env:
HXCPP_COMPILE_CACHE: "${{ runner.temp }}\\hxcpp_cache"
@@ -110,7 +110,7 @@ jobs:
key: ${{ runner.os }}-build-mac-${{ github.ref_name }}-${{ hashFiles('**/hmm.json') }}
- name: Build game
run: |
- haxelib run lime build macos -release --times
+ haxelib run lime build macos -release --times -DGITHUB_BUILD
ls
env:
HXCPP_COMPILE_CACHE: "${{ runner.temp }}/hxcpp_cache"
@@ -119,7 +119,7 @@ jobs:
butler-key: ${{ secrets.BUTLER_API_KEY}}
build-dir: export/release/macos/bin
target: macos
-
+
# test-unit-win:
# needs: create-nightly-win
# runs-on: windows-latest
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 8979e4de6..3d1f488f7 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -110,6 +110,11 @@
"target": "windows",
"args": ["-debug", "-DSONG=bopeebo -DDIFFICULTY=normal"]
},
+ {
+ "label": "Windows / Debug (Conversation Test)",
+ "target": "windows",
+ "args": ["-debug", "-DDIALOGUE"]
+ },
{
"label": "Windows / Debug (Straight to Chart Editor)",
"target": "windows",
diff --git a/Project.xml b/Project.xml
index d09ee2dbb..b39618908 100644
--- a/Project.xml
+++ b/Project.xml
@@ -52,6 +52,7 @@
+
@@ -82,14 +83,16 @@
If we can exclude the `mods` folder from the manifest, we can re-enable this line.
-->
-
-
+
+
+
+
@@ -116,7 +119,7 @@
-
+
@@ -209,13 +212,6 @@
-
-
-->
-->
diff --git a/assets b/assets
index 9308c50bd..160acbd8a 160000
--- a/assets
+++ b/assets
@@ -1 +1 @@
-Subproject commit 9308c50bd7fd904787e2fbac511bb83e1197347f
+Subproject commit 160acbd8a854a9f677ef7587396340e79a5ea6ca
diff --git a/hmm.json b/hmm.json
index aa38abb99..7321e731f 100644
--- a/hmm.json
+++ b/hmm.json
@@ -11,7 +11,7 @@
"name": "flixel",
"type": "git",
"dir": null,
- "ref": "07c6018008801972d12275690fc144fcc22e3de6",
+ "ref": "25c84b29665329f7c6366342542a3978f29300ee",
"url": "https://github.com/FunkinCrew/flixel"
},
{
@@ -151,7 +151,7 @@
"name": "polymod",
"type": "git",
"dir": null,
- "ref": "cb11a95d0159271eb3587428cf4b9602e46dc469",
+ "ref": "0b53e478bc375ec51b760b650201ac7a965d2ef4",
"url": "https://github.com/larsiusprime/polymod"
},
{
diff --git a/source/Main.hx b/source/Main.hx
index 86e520e69..a40fda29d 100644
--- a/source/Main.hx
+++ b/source/Main.hx
@@ -33,6 +33,10 @@ class Main extends Sprite
public static function main():Void
{
+ // We need to make the crash handler LITERALLY FIRST so nothing EVER gets past it.
+ CrashHandler.initialize();
+ CrashHandler.queryStatus();
+
Lib.current.addChild(new Main());
}
@@ -40,7 +44,12 @@ class Main extends Sprite
{
super();
- // TODO: Replace this with loadEnabledMods().
+ // Initialize custom logging.
+ haxe.Log.trace = funkin.util.logging.AnsiTrace.trace;
+ funkin.util.logging.AnsiTrace.traceBF();
+
+ // Load mods to override assets.
+ // TODO: Replace with loadEnabledMods() once the user can configure the mod list.
funkin.modding.PolymodHandler.loadAllMods();
if (stage != null)
@@ -80,10 +89,6 @@ class Main extends Sprite
* -Eric
*/
- CrashHandler.initialize();
-
- CrashHandler.queryStatus();
-
initHaxeUI();
fpsCounter = new FPS(10, 3, 0xFFFFFF);
diff --git a/source/funkin/InitState.hx b/source/funkin/InitState.hx
index f8c303bf3..5e69f58b9 100644
--- a/source/funkin/InitState.hx
+++ b/source/funkin/InitState.hx
@@ -21,9 +21,9 @@ import funkin.data.level.LevelRegistry;
import funkin.data.notestyle.NoteStyleRegistry;
import funkin.data.event.SongEventRegistry;
import funkin.data.stage.StageRegistry;
-import funkin.play.cutscene.dialogue.ConversationDataParser;
-import funkin.play.cutscene.dialogue.DialogueBoxDataParser;
-import funkin.play.cutscene.dialogue.SpeakerDataParser;
+import funkin.data.dialogue.ConversationRegistry;
+import funkin.data.dialogue.DialogueBoxRegistry;
+import funkin.data.dialogue.SpeakerRegistry;
import funkin.data.song.SongRegistry;
import funkin.play.character.CharacterData.CharacterDataParser;
import funkin.modding.module.ModuleHandler;
@@ -89,7 +89,7 @@ class InitState extends FlxState
//
// FLIXEL DEBUG SETUP
//
- #if debug
+ #if (debug || FORCE_DEBUG_VERSION)
// Disable using ~ to open the console (we use that for the Editor menu)
FlxG.debugger.toggleKeys = [F2];
@@ -141,16 +141,14 @@ class InitState extends FlxState
FlxG.sound.music.pause();
FlxG.sound.music.time += FlxG.elapsed * 1000;
});
+ #end
// 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;
- }
+ #if FORCE_DEBUG_VERSION
+ LogStyle.ERROR.openConsole = false;
+ LogStyle.ERROR.errorSound = null;
+ LogStyle.WARNING.openConsole = false;
+ LogStyle.WARNING.errorSound = null;
#end
//
@@ -208,22 +206,29 @@ class InitState extends FlxState
// GAME DATA PARSING
//
- // NOTE: Registries and data parsers must be imported and not referenced with fully qualified names,
+ // NOTE: Registries must be imported and not referenced with fully qualified names,
// to ensure build macros work properly.
+ trace('Parsing game data...');
+ var perfStart = haxe.Timer.stamp();
+ SongEventRegistry.loadEventCache(); // SongEventRegistry is structured differently so it's not a BaseRegistry.
SongRegistry.instance.loadEntries();
LevelRegistry.instance.loadEntries();
NoteStyleRegistry.instance.loadEntries();
- SongEventRegistry.loadEventCache();
- ConversationDataParser.loadConversationCache();
- DialogueBoxDataParser.loadDialogueBoxCache();
- SpeakerDataParser.loadSpeakerCache();
+ ConversationRegistry.instance.loadEntries();
+ DialogueBoxRegistry.instance.loadEntries();
+ SpeakerRegistry.instance.loadEntries();
StageRegistry.instance.loadEntries();
- CharacterDataParser.loadCharacterCache();
+
+ // TODO: CharacterDataParser doesn't use json2object, so it's way slower than the other parsers.
+ CharacterDataParser.loadCharacterCache(); // TODO: Migrate characters to BaseRegistry.
ModuleHandler.buildModuleCallbacks();
ModuleHandler.loadModuleCache();
-
ModuleHandler.callOnCreate();
+
+ var perfEnd = haxe.Timer.stamp();
+
+ trace('Parsing game data took ${Math.floor((perfEnd - perfStart) * 1000)}ms.');
}
/**
@@ -240,7 +245,9 @@ class InitState extends FlxState
#elseif LEVEL // -DLEVEL=week1 -DDIFFICULTY=hard
startLevel(defineLevel(), defineDifficulty());
#elseif FREEPLAY // -DFREEPLAY
- FlxG.switchState(() -> new funkin.ui.freeplay.FreeplayState());
+ FlxG.switchState(new FreeplayState());
+ #elseif DIALOGUE // -DDIALOGUE
+ FlxG.switchState(new funkin.ui.debug.dialogue.ConversationDebugState());
#elseif ANIMATE // -DANIMATE
FlxG.switchState(() -> new funkin.ui.debug.anim.FlxAnimateTest());
#elseif WAVEFORM // -DWAVEFORM
diff --git a/source/funkin/data/BaseRegistry.hx b/source/funkin/data/BaseRegistry.hx
index 70615069b..0ccbe2f18 100644
--- a/source/funkin/data/BaseRegistry.hx
+++ b/source/funkin/data/BaseRegistry.hx
@@ -46,6 +46,9 @@ abstract class BaseRegistry & Constructible();
}
+ /**
+ * TODO: Create a `loadEntriesAsync()` function.
+ */
public function loadEntries():Void
{
clearEntries();
@@ -54,7 +57,7 @@ abstract class BaseRegistry & Constructible = getScriptedClassNames();
- log('Registering ${scriptedEntryClassNames.length} scripted entries...');
+ log('Parsing ${scriptedEntryClassNames.length} scripted entries...');
for (entryCls in scriptedEntryClassNames)
{
@@ -78,7 +81,7 @@ abstract class BaseRegistry & Constructible = entryIdList.filter(function(entryId:String):Bool {
return !entries.exists(entryId);
});
- log('Fetching data for ${unscriptedEntryIds.length} unscripted entries...');
+ log('Parsing ${unscriptedEntryIds.length} unscripted entries...');
for (entryId in unscriptedEntryIds)
{
try
diff --git a/source/funkin/data/DataParse.hx b/source/funkin/data/DataParse.hx
index 49dde0198..244d41132 100644
--- a/source/funkin/data/DataParse.hx
+++ b/source/funkin/data/DataParse.hx
@@ -120,6 +120,71 @@ class DataParse
}
}
+ public static function backdropData(json:Json, name:String):funkin.data.dialogue.ConversationData.BackdropData
+ {
+ switch (json.value)
+ {
+ case JObject(fields):
+ var result:Dynamic = {};
+ var backdropType:String = '';
+
+ for (field in fields)
+ {
+ switch (field.name)
+ {
+ case 'type':
+ backdropType = Tools.getValue(field.value);
+ }
+ Reflect.setField(result, field.name, Tools.getValue(field.value));
+ }
+
+ switch (backdropType)
+ {
+ case 'solid':
+ return SOLID(result);
+ default:
+ throw 'Expected Backdrop property $name to be specify a valid "type", but it was "${backdropType}".';
+ }
+
+ return null;
+ default:
+ throw 'Expected property $name to be an object, but it was ${json.value}.';
+ }
+ }
+
+ public static function outroData(json:Json, name:String):Null
+ {
+ switch (json.value)
+ {
+ case JObject(fields):
+ var result:Dynamic = {};
+ var outroType:String = '';
+
+ for (field in fields)
+ {
+ switch (field.name)
+ {
+ case 'type':
+ outroType = Tools.getValue(field.value);
+ }
+ Reflect.setField(result, field.name, Tools.getValue(field.value));
+ }
+
+ switch (outroType)
+ {
+ case 'none':
+ return NONE(result);
+ case 'fade':
+ return FADE(result);
+ default:
+ throw 'Expected Outro property $name to be specify a valid "type", but it was "${outroType}".';
+ }
+ return null;
+ default:
+ throw 'Expected property $name to be an object, but it was ${json.value}.';
+ }
+ }
+
/**
* Parser which outputs a `Either`.
* Used by the FNF legacy JSON importer.
diff --git a/source/funkin/data/dialogue/ConversationData.hx b/source/funkin/data/dialogue/ConversationData.hx
new file mode 100644
index 000000000..795ddae9a
--- /dev/null
+++ b/source/funkin/data/dialogue/ConversationData.hx
@@ -0,0 +1,168 @@
+package funkin.data.dialogue;
+
+import funkin.data.animation.AnimationData;
+
+/**
+ * A type definition for the data for a specific conversation.
+ * It includes things like what dialogue boxes to use, what text to display, and what animations to play.
+ * @see https://lib.haxe.org/p/json2object/
+ */
+typedef ConversationData =
+{
+ /**
+ * Semantic version for conversation data.
+ */
+ public var version:String;
+
+ /**
+ * Data on the backdrop for the conversation.
+ */
+ @:jcustomparse(funkin.data.DataParse.backdropData)
+ public var backdrop:BackdropData;
+
+ /**
+ * Data on the outro for the conversation.
+ */
+ @:jcustomparse(funkin.data.DataParse.outroData)
+ @:optional
+ public var outro:Null;
+
+ /**
+ * Data on the music for the conversation.
+ */
+ @:optional
+ public var music:Null;
+
+ /**
+ * Data for each line of dialogue in the conversation.
+ */
+ public var dialogue:Array;
+}
+
+/**
+ * Data on the backdrop for the conversation, behind the dialogue box.
+ * A custom parser distinguishes between backdrop types based on the `type` field.
+ */
+enum BackdropData
+{
+ SOLID(data:BackdropData_Solid); // 'solid'
+}
+
+/**
+ * Data for a Solid color backdrop.
+ */
+typedef BackdropData_Solid =
+{
+ /**
+ * Used to distinguish between backdrop types. Should always be `solid` for this type.
+ */
+ var type:String;
+
+ /**
+ * The color of the backdrop.
+ */
+ var color:String;
+
+ /**
+ * Fade-in time for the backdrop.
+ * @default No fade-in
+ */
+ @:optional
+ @:default(0.0)
+ var fadeTime:Float;
+};
+
+enum OutroData
+{
+ NONE(data:OutroData_None); // 'none'
+ FADE(data:OutroData_Fade); // 'fade'
+}
+
+typedef OutroData_None =
+{
+ /**
+ * Used to distinguish between outro types. Should always be `none` for this type.
+ */
+ var type:String;
+}
+
+typedef OutroData_Fade =
+{
+ /**
+ * Used to distinguish between outro types. Should always be `fade` for this type.
+ */
+ var type:String;
+
+ /**
+ * The time to fade out the conversation.
+ * @default 1 second
+ */
+ @:optional
+ @:default(1.0)
+ var fadeTime:Float;
+}
+
+typedef MusicData =
+{
+ /**
+ * The asset to play for the music.
+ */
+ var asset:String;
+
+ /**
+ * The time to fade in the music.
+ */
+ @:optional
+ @:default(0.0)
+ var fadeTime:Float;
+
+ @:optional
+ @:default(false)
+ var looped:Bool;
+};
+
+/**
+ * Data on a single line of dialogue in a conversation.
+ */
+typedef DialogueEntryData =
+{
+ /**
+ * Which speaker is speaking.
+ * @see `SpeakerData.hx`
+ */
+ public var speaker:String;
+
+ /**
+ * The animation the speaker should play for this line of dialogue.
+ */
+ public var speakerAnimation:String;
+
+ /**
+ * Which dialogue box to use for this line of dialogue.
+ * @see `DialogueBoxData.hx`
+ */
+ public var box:String;
+
+ /**
+ * Which animation to play for the dialogue box.
+ */
+ public var boxAnimation:String;
+
+ /**
+ * The text that will display for this line of dialogue.
+ * Text will automatically wrap.
+ * When the user advances the dialogue, the next entry in the array will concatenate on.
+ * Advancing when the last entry is displayed will move to the next `DialogueEntryData`,
+ * or end the conversation if there are no more.
+ */
+ public var text:Array;
+
+ /**
+ * The relative speed at which text gets "typed out".
+ * Setting `speed` to `1.5` would make it look like the character is speaking quickly,
+ * and setting `speed` to `0.5` would make it look like the character is emphasizing each word.
+ */
+ @:optional
+ @:default(1.0)
+ public var speed:Float;
+};
diff --git a/source/funkin/data/dialogue/ConversationRegistry.hx b/source/funkin/data/dialogue/ConversationRegistry.hx
new file mode 100644
index 000000000..9186ef786
--- /dev/null
+++ b/source/funkin/data/dialogue/ConversationRegistry.hx
@@ -0,0 +1,81 @@
+package funkin.data.dialogue;
+
+import funkin.play.cutscene.dialogue.Conversation;
+import funkin.data.dialogue.ConversationData;
+import funkin.play.cutscene.dialogue.ScriptedConversation;
+
+class ConversationRegistry extends BaseRegistry
+{
+ /**
+ * The current version string for the dialogue box data format.
+ * Handle breaking changes by incrementing this value
+ * and adding migration to the `migrateConversationData()` function.
+ */
+ public static final CONVERSATION_DATA_VERSION:thx.semver.Version = "1.0.0";
+
+ public static final CONVERSATION_DATA_VERSION_RULE:thx.semver.VersionRule = "1.0.x";
+
+ public static final instance:ConversationRegistry = new ConversationRegistry();
+
+ public function new()
+ {
+ super('CONVERSATION', 'dialogue/conversations', CONVERSATION_DATA_VERSION_RULE);
+ }
+
+ /**
+ * Read, parse, and validate the JSON data and produce the corresponding data object.
+ */
+ public function parseEntryData(id:String):Null
+ {
+ // JsonParser does not take type parameters,
+ // otherwise this function would be in BaseRegistry.
+ var parser = new json2object.JsonParser();
+ parser.ignoreUnknownVariables = false;
+
+ switch (loadEntryFile(id))
+ {
+ case {fileName: fileName, contents: contents}:
+ parser.fromJson(contents, fileName);
+ default:
+ return null;
+ }
+
+ if (parser.errors.length > 0)
+ {
+ printErrors(parser.errors, id);
+ return null;
+ }
+ return parser.value;
+ }
+
+ /**
+ * Parse and validate the JSON data and produce the corresponding data object.
+ *
+ * NOTE: Must be implemented on the implementation class.
+ * @param contents The JSON as a string.
+ * @param fileName An optional file name for error reporting.
+ */
+ public function parseEntryDataRaw(contents:String, ?fileName:String):Null
+ {
+ var parser = new json2object.JsonParser();
+ parser.ignoreUnknownVariables = false;
+ parser.fromJson(contents, fileName);
+
+ if (parser.errors.length > 0)
+ {
+ printErrors(parser.errors, fileName);
+ return null;
+ }
+ return parser.value;
+ }
+
+ function createScriptedEntry(clsName:String):Conversation
+ {
+ return ScriptedConversation.init(clsName, "unknown");
+ }
+
+ function getScriptedClassNames():Array
+ {
+ return ScriptedConversation.listScriptClasses();
+ }
+}
diff --git a/source/funkin/data/dialogue/DialogueBoxData.hx b/source/funkin/data/dialogue/DialogueBoxData.hx
new file mode 100644
index 000000000..a75a5595a
--- /dev/null
+++ b/source/funkin/data/dialogue/DialogueBoxData.hx
@@ -0,0 +1,128 @@
+package funkin.data.dialogue;
+
+import funkin.data.animation.AnimationData;
+
+/**
+ * A type definition for the data for a conversation text box.
+ * It includes things like the sprite to use, and the font and color for the text.
+ * The actual text is included in the ConversationData.
+ * @see https://lib.haxe.org/p/json2object/
+ */
+typedef DialogueBoxData =
+{
+ /**
+ * Semantic version for dialogue box data.
+ */
+ public var version:String;
+
+ /**
+ * A human readable name for the dialogue box type.
+ */
+ public var name:String;
+
+ /**
+ * The asset path for the sprite to use for the dialogue box.
+ * Takes a static sprite or a sprite sheet.
+ */
+ public var assetPath:String;
+
+ /**
+ * Whether to horizontally flip the dialogue box sprite.
+ */
+ @:optional
+ @:default(false)
+ public var flipX:Bool;
+
+ /**
+ * Whether to vertically flip the dialogue box sprite.
+ */
+ @:optional
+ @:default(false)
+ public var flipY:Bool;
+
+ /**
+ * Whether to disable anti-aliasing for the dialogue box sprite.
+ */
+ @:optional
+ @:default(false)
+ public var isPixel:Bool;
+
+ /**
+ * The relative horizontal and vertical offsets for the dialogue box sprite.
+ */
+ @:optional
+ @:default([0, 0])
+ public var offsets:Array;
+
+ /**
+ * Info about how to display text in the dialogue box.
+ */
+ public var text:DialogueBoxTextData;
+
+ /**
+ * Multiply the size of the dialogue box sprite.
+ */
+ @:optional
+ @:default(1)
+ public var scale:Float;
+
+ /**
+ * If using a spritesheet for the dialogue box, the animations to use.
+ */
+ @:optional
+ @:default([])
+ public var animations:Array;
+}
+
+typedef DialogueBoxTextData =
+{
+ /**
+ * The position of the text in teh box.
+ */
+ @:optional
+ @:default([0, 0])
+ var offsets:Array;
+
+ /**
+ * The width of the
+ */
+ @:optional
+ @:default(300)
+ var width:Int;
+
+ /**
+ * The font size to use for the text.
+ */
+ @:optional
+ @:default(32)
+ var size:Int;
+
+ /**
+ * The color to use for the text.
+ * Use a string that can be translated to a color, like `#FF0000` for red.
+ */
+ @:optional
+ @:default("#000000")
+ var color:String;
+
+ /**
+ * The font to use for the text.
+ * @since v1.1.0
+ * @default `Arial`, make sure to switch this!
+ */
+ @:optional
+ @:default("Arial")
+ var fontFamily:String;
+
+ /**
+ * The color to use for the shadow of the text. Use transparent to disable.
+ */
+ var shadowColor:String;
+
+ /**
+ * The width of the shadow of the text.
+ */
+ @:optional
+ @:default(0)
+ var shadowWidth:Int;
+};
diff --git a/source/funkin/data/dialogue/DialogueBoxRegistry.hx b/source/funkin/data/dialogue/DialogueBoxRegistry.hx
new file mode 100644
index 000000000..87205d96c
--- /dev/null
+++ b/source/funkin/data/dialogue/DialogueBoxRegistry.hx
@@ -0,0 +1,81 @@
+package funkin.data.dialogue;
+
+import funkin.play.cutscene.dialogue.DialogueBox;
+import funkin.data.dialogue.DialogueBoxData;
+import funkin.play.cutscene.dialogue.ScriptedDialogueBox;
+
+class DialogueBoxRegistry extends BaseRegistry
+{
+ /**
+ * The current version string for the dialogue box data format.
+ * Handle breaking changes by incrementing this value
+ * and adding migration to the `migrateDialogueBoxData()` function.
+ */
+ public static final DIALOGUEBOX_DATA_VERSION:thx.semver.Version = "1.1.0";
+
+ public static final DIALOGUEBOX_DATA_VERSION_RULE:thx.semver.VersionRule = "1.1.x";
+
+ public static final instance:DialogueBoxRegistry = new DialogueBoxRegistry();
+
+ public function new()
+ {
+ super('DIALOGUEBOX', 'dialogue/boxes', DIALOGUEBOX_DATA_VERSION_RULE);
+ }
+
+ /**
+ * Read, parse, and validate the JSON data and produce the corresponding data object.
+ */
+ public function parseEntryData(id:String):Null
+ {
+ // JsonParser does not take type parameters,
+ // otherwise this function would be in BaseRegistry.
+ var parser = new json2object.JsonParser();
+ parser.ignoreUnknownVariables = false;
+
+ switch (loadEntryFile(id))
+ {
+ case {fileName: fileName, contents: contents}:
+ parser.fromJson(contents, fileName);
+ default:
+ return null;
+ }
+
+ if (parser.errors.length > 0)
+ {
+ printErrors(parser.errors, id);
+ return null;
+ }
+ return parser.value;
+ }
+
+ /**
+ * Parse and validate the JSON data and produce the corresponding data object.
+ *
+ * NOTE: Must be implemented on the implementation class.
+ * @param contents The JSON as a string.
+ * @param fileName An optional file name for error reporting.
+ */
+ public function parseEntryDataRaw(contents:String, ?fileName:String):Null
+ {
+ var parser = new json2object.JsonParser();
+ parser.ignoreUnknownVariables = false;
+ parser.fromJson(contents, fileName);
+
+ if (parser.errors.length > 0)
+ {
+ printErrors(parser.errors, fileName);
+ return null;
+ }
+ return parser.value;
+ }
+
+ function createScriptedEntry(clsName:String):DialogueBox
+ {
+ return ScriptedDialogueBox.init(clsName, "unknown");
+ }
+
+ function getScriptedClassNames():Array
+ {
+ return ScriptedDialogueBox.listScriptClasses();
+ }
+}
diff --git a/source/funkin/data/dialogue/SpeakerData.hx b/source/funkin/data/dialogue/SpeakerData.hx
new file mode 100644
index 000000000..e8a2eacf0
--- /dev/null
+++ b/source/funkin/data/dialogue/SpeakerData.hx
@@ -0,0 +1,68 @@
+package funkin.data.dialogue;
+
+import funkin.data.animation.AnimationData;
+
+/**
+ * A type definition for a specific speaker in a conversation.
+ * It includes things like what sprite to use and its available animations.
+ * @see https://lib.haxe.org/p/json2object/
+ */
+typedef SpeakerData =
+{
+ /**
+ * Semantic version of the speaker data.
+ */
+ public var version:String;
+
+ /**
+ * A human-readable name for the speaker.
+ */
+ public var name:String;
+
+ /**
+ * The path to the asset to use for the speaker's sprite.
+ */
+ public var assetPath:String;
+
+ /**
+ * Whether the sprite should be flipped horizontally.
+ */
+ @:optional
+ @:default(false)
+ public var flipX:Bool;
+
+ /**
+ * Whether the sprite should be flipped vertically.
+ */
+ @:optional
+ @:default(false)
+ public var flipY:Bool;
+
+ /**
+ * Whether to disable anti-aliasing for the dialogue box sprite.
+ */
+ @:optional
+ @:default(false)
+ public var isPixel:Bool;
+
+ /**
+ * The offsets to apply to the sprite's position.
+ */
+ @:optional
+ @:default([0, 0])
+ public var offsets:Array;
+
+ /**
+ * The scale to apply to the sprite.
+ */
+ @:optional
+ @:default(1.0)
+ public var scale:Float;
+
+ /**
+ * The available animations for the speaker.
+ */
+ @:optional
+ @:default([])
+ public var animations:Array;
+}
diff --git a/source/funkin/data/dialogue/SpeakerRegistry.hx b/source/funkin/data/dialogue/SpeakerRegistry.hx
new file mode 100644
index 000000000..6bd301dd7
--- /dev/null
+++ b/source/funkin/data/dialogue/SpeakerRegistry.hx
@@ -0,0 +1,81 @@
+package funkin.data.dialogue;
+
+import funkin.play.cutscene.dialogue.Speaker;
+import funkin.data.dialogue.SpeakerData;
+import funkin.play.cutscene.dialogue.ScriptedSpeaker;
+
+class SpeakerRegistry extends BaseRegistry
+{
+ /**
+ * The current version string for the speaker data format.
+ * Handle breaking changes by incrementing this value
+ * and adding migration to the `migrateSpeakerData()` function.
+ */
+ public static final SPEAKER_DATA_VERSION:thx.semver.Version = "1.0.0";
+
+ public static final SPEAKER_DATA_VERSION_RULE:thx.semver.VersionRule = "1.0.x";
+
+ public static final instance:SpeakerRegistry = new SpeakerRegistry();
+
+ public function new()
+ {
+ super('SPEAKER', 'dialogue/speakers', SPEAKER_DATA_VERSION_RULE);
+ }
+
+ /**
+ * Read, parse, and validate the JSON data and produce the corresponding data object.
+ */
+ public function parseEntryData(id:String):Null
+ {
+ // JsonParser does not take type parameters,
+ // otherwise this function would be in BaseRegistry.
+ var parser = new json2object.JsonParser();
+ parser.ignoreUnknownVariables = false;
+
+ switch (loadEntryFile(id))
+ {
+ case {fileName: fileName, contents: contents}:
+ parser.fromJson(contents, fileName);
+ default:
+ return null;
+ }
+
+ if (parser.errors.length > 0)
+ {
+ printErrors(parser.errors, id);
+ return null;
+ }
+ return parser.value;
+ }
+
+ /**
+ * Parse and validate the JSON data and produce the corresponding data object.
+ *
+ * NOTE: Must be implemented on the implementation class.
+ * @param contents The JSON as a string.
+ * @param fileName An optional file name for error reporting.
+ */
+ public function parseEntryDataRaw(contents:String, ?fileName:String):Null
+ {
+ var parser = new json2object.JsonParser();
+ parser.ignoreUnknownVariables = false;
+ parser.fromJson(contents, fileName);
+
+ if (parser.errors.length > 0)
+ {
+ printErrors(parser.errors, fileName);
+ return null;
+ }
+ return parser.value;
+ }
+
+ function createScriptedEntry(clsName:String):Speaker
+ {
+ return ScriptedSpeaker.init(clsName, "unknown");
+ }
+
+ function getScriptedClassNames():Array
+ {
+ return ScriptedSpeaker.listScriptClasses();
+ }
+}
diff --git a/source/funkin/data/song/SongData.hx b/source/funkin/data/song/SongData.hx
index 0fc986a9f..73ecbce14 100644
--- a/source/funkin/data/song/SongData.hx
+++ b/source/funkin/data/song/SongData.hx
@@ -418,10 +418,10 @@ class SongPlayData implements ICloneable
/**
* The difficulty ratings for this song as displayed in Freeplay.
- * Key is a difficulty ID or `default`.
+ * Key is a difficulty ID.
*/
@:optional
- @:default(['default' => 1])
+ @:default(['normal' => 0])
public var ratings:Map;
/**
diff --git a/source/funkin/data/song/SongRegistry.hx b/source/funkin/data/song/SongRegistry.hx
index d2a548c62..dad287e82 100644
--- a/source/funkin/data/song/SongRegistry.hx
+++ b/source/funkin/data/song/SongRegistry.hx
@@ -58,7 +58,7 @@ class SongRegistry extends BaseRegistry
// SCRIPTED ENTRIES
//
var scriptedEntryClassNames:Array = getScriptedClassNames();
- log('Registering ${scriptedEntryClassNames.length} scripted entries...');
+ log('Parsing ${scriptedEntryClassNames.length} scripted entries...');
for (entryCls in scriptedEntryClassNames)
{
@@ -84,7 +84,7 @@ class SongRegistry extends BaseRegistry
var unscriptedEntryIds:Array = entryIdList.filter(function(entryId:String):Bool {
return !entries.exists(entryId);
});
- log('Fetching data for ${unscriptedEntryIds.length} unscripted entries...');
+ log('Parsing ${unscriptedEntryIds.length} unscripted entries...');
for (entryId in unscriptedEntryIds)
{
try
diff --git a/source/funkin/modding/PolymodHandler.hx b/source/funkin/modding/PolymodHandler.hx
index 151e658b4..f1e82aee9 100644
--- a/source/funkin/modding/PolymodHandler.hx
+++ b/source/funkin/modding/PolymodHandler.hx
@@ -2,7 +2,6 @@ package funkin.modding;
import funkin.util.macro.ClassMacro;
import funkin.modding.module.ModuleHandler;
-import funkin.play.character.CharacterData.CharacterDataParser;
import funkin.data.song.SongData;
import funkin.data.stage.StageData;
import polymod.Polymod;
@@ -13,10 +12,11 @@ import funkin.data.stage.StageRegistry;
import funkin.util.FileUtil;
import funkin.data.level.LevelRegistry;
import funkin.data.notestyle.NoteStyleRegistry;
-import funkin.play.cutscene.dialogue.ConversationDataParser;
-import funkin.play.cutscene.dialogue.DialogueBoxDataParser;
+import funkin.data.dialogue.ConversationRegistry;
+import funkin.data.dialogue.DialogueBoxRegistry;
+import funkin.data.dialogue.SpeakerRegistry;
+import funkin.play.character.CharacterData.CharacterDataParser;
import funkin.save.Save;
-import funkin.play.cutscene.dialogue.SpeakerDataParser;
import funkin.data.song.SongRegistry;
class PolymodHandler
@@ -208,8 +208,8 @@ class PolymodHandler
{
return {
assetLibraryPaths: [
- "default" => "preload", "shared" => "", "songs" => "songs", "tutorial" => "tutorial", "week1" => "week1", "week2" => "week2", "week3" => "week3",
- "week4" => "week4", "week5" => "week5", "week6" => "week6", "week7" => "week7", "weekend1" => "weekend1",
+ "default" => "preload", "shared" => "shared", "songs" => "songs", "tutorial" => "tutorial", "week1" => "week1", "week2" => "week2",
+ "week3" => "week3", "week4" => "week4", "week5" => "week5", "week6" => "week6", "week7" => "week7", "weekend1" => "weekend1",
],
coreAssetRedirect: CORE_FOLDER,
}
@@ -273,11 +273,11 @@ class PolymodHandler
LevelRegistry.instance.loadEntries();
NoteStyleRegistry.instance.loadEntries();
SongEventRegistry.loadEventCache();
- ConversationDataParser.loadConversationCache();
- DialogueBoxDataParser.loadDialogueBoxCache();
- SpeakerDataParser.loadSpeakerCache();
+ ConversationRegistry.instance.loadEntries();
+ DialogueBoxRegistry.instance.loadEntries();
+ SpeakerRegistry.instance.loadEntries();
StageRegistry.instance.loadEntries();
- CharacterDataParser.loadCharacterCache();
+ CharacterDataParser.loadCharacterCache(); // TODO: Migrate characters to BaseRegistry.
ModuleHandler.loadModuleCache();
}
}
diff --git a/source/funkin/play/GameOverSubState.hx b/source/funkin/play/GameOverSubState.hx
index 6027ea1ca..74b39417e 100644
--- a/source/funkin/play/GameOverSubState.hx
+++ b/source/funkin/play/GameOverSubState.hx
@@ -90,6 +90,7 @@ class GameOverSubState extends MusicBeatSubState
{
animationSuffix = "";
musicSuffix = "";
+ blueBallSuffix = "";
}
override public function create()
diff --git a/source/funkin/play/PauseSubState.hx b/source/funkin/play/PauseSubState.hx
index d38e3ac87..10e59f078 100644
--- a/source/funkin/play/PauseSubState.hx
+++ b/source/funkin/play/PauseSubState.hx
@@ -168,7 +168,7 @@ class PauseSubState extends MusicBeatSubState
var downP = controls.UI_DOWN_P;
var accepted = controls.ACCEPT;
- #if debug
+ #if (debug || FORCE_DEBUG_VERSION)
// to pause the game and get screenshots easy, press H on pause menu!
if (FlxG.keys.justPressed.H)
{
diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx
index ad2ea5a45..74347765e 100644
--- a/source/funkin/play/PlayState.hx
+++ b/source/funkin/play/PlayState.hx
@@ -39,7 +39,7 @@ import funkin.modding.events.ScriptEventDispatcher;
import funkin.play.character.BaseCharacter;
import funkin.play.character.CharacterData.CharacterDataParser;
import funkin.play.cutscene.dialogue.Conversation;
-import funkin.play.cutscene.dialogue.ConversationDataParser;
+import funkin.data.dialogue.ConversationRegistry;
import funkin.play.cutscene.VanillaCutscenes;
import funkin.play.cutscene.VideoCutscene;
import funkin.data.event.SongEventRegistry;
@@ -644,7 +644,7 @@ class PlayState extends MusicBeatSubState
rightWatermarkText.cameras = [camHUD];
// Initialize some debug stuff.
- #if debug
+ #if (debug || FORCE_DEBUG_VERSION)
// Display the version number (and git commit hash) in the bottom right corner.
this.rightWatermarkText.text = Constants.VERSION;
@@ -913,7 +913,7 @@ class PlayState extends MusicBeatSubState
// Disable updates, preventing animations in the background from playing.
persistentUpdate = false;
- #if debug
+ #if (debug || FORCE_DEBUG_VERSION)
if (FlxG.keys.pressed.THREE)
{
// TODO: Change the key or delete this?
@@ -924,7 +924,7 @@ class PlayState extends MusicBeatSubState
{
#end
persistentDraw = false;
- #if debug
+ #if (debug || FORCE_DEBUG_VERSION)
}
#end
@@ -1374,7 +1374,7 @@ class PlayState extends MusicBeatSubState
// Add the stage to the scene.
this.add(currentStage);
- #if debug
+ #if (debug || FORCE_DEBUG_VERSION)
FlxG.console.registerObject('stage', currentStage);
#end
}
@@ -1464,7 +1464,7 @@ class PlayState extends MusicBeatSubState
{
currentStage.addCharacter(girlfriend, GF);
- #if debug
+ #if (debug || FORCE_DEBUG_VERSION)
FlxG.console.registerObject('gf', girlfriend);
#end
}
@@ -1473,7 +1473,7 @@ class PlayState extends MusicBeatSubState
{
currentStage.addCharacter(boyfriend, BF);
- #if debug
+ #if (debug || FORCE_DEBUG_VERSION)
FlxG.console.registerObject('bf', boyfriend);
#end
}
@@ -1484,7 +1484,7 @@ class PlayState extends MusicBeatSubState
// Camera starts at dad.
cameraFollowPoint.setPosition(dad.cameraFocusPoint.x, dad.cameraFocusPoint.y);
- #if debug
+ #if (debug || FORCE_DEBUG_VERSION)
FlxG.console.registerObject('dad', dad);
#end
}
@@ -1668,7 +1668,7 @@ class PlayState extends MusicBeatSubState
{
isInCutscene = true;
- currentConversation = ConversationDataParser.fetchConversation(conversationId);
+ currentConversation = ConversationRegistry.instance.fetchEntry(conversationId);
if (currentConversation == null) return;
currentConversation.completeCallback = onConversationComplete;
@@ -2259,7 +2259,7 @@ class PlayState extends MusicBeatSubState
}));
}
- #if debug
+ #if (debug || FORCE_DEBUG_VERSION)
// 1: End the song immediately.
if (FlxG.keys.justPressed.ONE) endSong();
@@ -2273,7 +2273,7 @@ class PlayState extends MusicBeatSubState
// 9: Toggle the old icon.
if (FlxG.keys.justPressed.NINE) iconP1.toggleOldIcon();
- #if debug
+ #if (debug || FORCE_DEBUG_VERSION)
// PAGEUP: Skip forward two sections.
// SHIFT+PAGEUP: Skip forward twenty sections.
if (FlxG.keys.justPressed.PAGEUP) changeSection(FlxG.keys.pressed.SHIFT ? 20 : 2);
@@ -2781,7 +2781,7 @@ class PlayState extends MusicBeatSubState
FlxG.camera.focusOn(cameraFollowPoint.getPosition());
}
- #if debug
+ #if (debug || FORCE_DEBUG_VERSION)
/**
* Jumps forward or backward a number of sections in the song.
* Accounts for BPM changes, does not prevent death from skipped notes.
diff --git a/source/funkin/play/character/CharacterData.hx b/source/funkin/play/character/CharacterData.hx
index 16cc8b299..69e3ca48e 100644
--- a/source/funkin/play/character/CharacterData.hx
+++ b/source/funkin/play/character/CharacterData.hx
@@ -43,7 +43,7 @@ class CharacterDataParser
{
// Clear any stages that are cached if there were any.
clearCharacterCache();
- trace('Loading character cache...');
+ trace('[CHARACTER] Parsing all entries...');
//
// UNSCRIPTED CHARACTERS
diff --git a/source/funkin/play/cutscene/dialogue/Conversation.hx b/source/funkin/play/cutscene/dialogue/Conversation.hx
index b2361c795..dc3fd8c8a 100644
--- a/source/funkin/play/cutscene/dialogue/Conversation.hx
+++ b/source/funkin/play/cutscene/dialogue/Conversation.hx
@@ -1,8 +1,10 @@
package funkin.play.cutscene.dialogue;
+import funkin.data.IRegistryEntry;
import flixel.FlxSprite;
import flixel.group.FlxSpriteGroup;
import flixel.util.FlxColor;
+import funkin.graphics.FunkinSprite;
import flixel.tweens.FlxTween;
import flixel.tweens.FlxEase;
import flixel.sound.FlxSound;
@@ -13,27 +15,30 @@ import funkin.modding.IScriptedClass.IEventHandler;
import funkin.play.cutscene.dialogue.DialogueBox;
import funkin.modding.IScriptedClass.IDialogueScriptedClass;
import funkin.modding.events.ScriptEventDispatcher;
-import funkin.play.cutscene.dialogue.ConversationData.DialogueEntryData;
import flixel.addons.display.FlxPieDial;
+import funkin.data.dialogue.ConversationData;
+import funkin.data.dialogue.ConversationData.DialogueEntryData;
+import funkin.data.dialogue.ConversationRegistry;
+import funkin.data.dialogue.SpeakerData;
+import funkin.data.dialogue.SpeakerRegistry;
+import funkin.data.dialogue.DialogueBoxData;
+import funkin.data.dialogue.DialogueBoxRegistry;
/**
* A high-level handler for dialogue.
*
* This shit is great for modders but it's pretty elaborate for how much it'll actually be used, lolol. -Eric
*/
-class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass
+class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass implements IRegistryEntry
{
static final CONVERSATION_SKIP_TIMER:Float = 1.5;
var skipHeldTimer:Float = 0.0;
/**
- * DATA
+ * The ID of the conversation.
*/
- /**
- * The ID of the associated dialogue.
- */
- public final conversationId:String;
+ public final id:String;
/**
* The current state of the conversation.
@@ -41,9 +46,9 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass
var state:ConversationState = ConversationState.Start;
/**
- * The data for the associated dialogue.
+ * Conversation data as parsed from the JSON file.
*/
- var conversationData:ConversationData;
+ public final _data:ConversationData;
/**
* The current entry in the dialogue.
@@ -54,7 +59,7 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass
function get_currentDialogueEntryCount():Int
{
- return conversationData.dialogue.length;
+ return _data.dialogue.length;
}
/**
@@ -73,10 +78,10 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass
function get_currentDialogueEntryData():DialogueEntryData
{
- if (conversationData == null || conversationData.dialogue == null) return null;
- if (currentDialogueEntry < 0 || currentDialogueEntry >= conversationData.dialogue.length) return null;
+ if (_data == null || _data.dialogue == null) return null;
+ if (currentDialogueEntry < 0 || currentDialogueEntry >= _data.dialogue.length) return null;
- return conversationData.dialogue[currentDialogueEntry];
+ return _data.dialogue[currentDialogueEntry];
}
var currentDialogueLineString(get, never):String;
@@ -94,7 +99,7 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass
/**
* GRAPHICS
*/
- var backdrop:FlxSprite;
+ var backdrop:FunkinSprite;
var currentSpeaker:Speaker;
@@ -102,14 +107,17 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass
var skipTimer:FlxPieDial;
- public function new(conversationId:String)
+ public function new(id:String)
{
super();
- this.conversationId = conversationId;
- this.conversationData = ConversationDataParser.parseConversationData(this.conversationId);
+ this.id = id;
+ this._data = _fetchData(id);
- if (conversationData == null) throw 'Could not load conversation data for conversation ID "$conversationId"';
+ if (_data == null)
+ {
+ throw 'Could not parse conversation data for id: $id';
+ }
}
public function onCreate(event:ScriptEvent):Void
@@ -125,14 +133,14 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass
function setupMusic():Void
{
- if (conversationData.music == null) return;
+ if (_data.music == null) return;
- music = new FlxSound().loadEmbedded(Paths.music(conversationData.music.asset), true, true);
+ music = new FlxSound().loadEmbedded(Paths.music(_data.music.asset), true, true);
music.volume = 0;
- if (conversationData.music.fadeTime > 0.0)
+ if (_data.music.fadeTime > 0.0)
{
- FlxTween.tween(music, {volume: 1.0}, conversationData.music.fadeTime, {ease: FlxEase.linear});
+ FlxTween.tween(music, {volume: 1.0}, _data.music.fadeTime, {ease: FlxEase.linear});
}
else
{
@@ -145,19 +153,20 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass
function setupBackdrop():Void
{
- backdrop = new FlxSprite(0, 0);
+ backdrop = new FunkinSprite(0, 0);
- if (conversationData.backdrop == null) return;
+ if (_data.backdrop == null) return;
// Play intro
- switch (conversationData?.backdrop.type)
+ switch (_data.backdrop)
{
- case SOLID:
- backdrop.makeGraphic(Std.int(FlxG.width), Std.int(FlxG.height), FlxColor.fromString(conversationData.backdrop.data.color));
- if (conversationData.backdrop.data.fadeTime > 0.0)
+ case SOLID(backdropData):
+ var targetColor:FlxColor = FlxColor.fromString(backdropData.color);
+ backdrop.makeSolidColor(Std.int(FlxG.width), Std.int(FlxG.height), targetColor);
+ if (backdropData.fadeTime > 0.0)
{
backdrop.alpha = 0.0;
- FlxTween.tween(backdrop, {alpha: 1.0}, conversationData.backdrop.data.fadeTime, {ease: FlxEase.linear});
+ FlxTween.tween(backdrop, {alpha: 1.0}, backdropData.fadeTime, {ease: FlxEase.linear});
}
else
{
@@ -190,9 +199,9 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass
var nextSpeakerId:String = currentDialogueEntryData.speaker;
// Skip the next steps if the current speaker is already displayed.
- if (currentSpeaker != null && nextSpeakerId == currentSpeaker.speakerId) return;
+ if (currentSpeaker != null && nextSpeakerId == currentSpeaker.id) return;
- var nextSpeaker:Speaker = SpeakerDataParser.fetchSpeaker(nextSpeakerId);
+ var nextSpeaker:Speaker = SpeakerRegistry.instance.fetchEntry(nextSpeakerId);
if (currentSpeaker != null)
{
@@ -241,7 +250,7 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass
var nextDialogueBoxId:String = currentDialogueEntryData?.box;
// Skip the next steps if the current speaker is already displayed.
- if (currentDialogueBox != null && nextDialogueBoxId == currentDialogueBox.dialogueBoxId) return;
+ if (currentDialogueBox != null && nextDialogueBoxId == currentDialogueBox.id) return;
if (currentDialogueBox != null)
{
@@ -250,7 +259,7 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass
currentDialogueBox = null;
}
- var nextDialogueBox:DialogueBox = DialogueBoxDataParser.fetchDialogueBox(nextDialogueBoxId);
+ var nextDialogueBox:DialogueBox = DialogueBoxRegistry.instance.fetchEntry(nextDialogueBoxId);
if (nextDialogueBox == null)
{
@@ -378,20 +387,18 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass
public function startOutro():Void
{
- switch (conversationData?.outro?.type)
+ switch (_data?.outro)
{
- case FADE:
- var fadeTime:Float = conversationData?.outro.data.fadeTime ?? 1.0;
-
- outroTween = FlxTween.tween(this, {alpha: 0.0}, fadeTime,
+ case FADE(outroData):
+ outroTween = FlxTween.tween(this, {alpha: 0.0}, outroData.fadeTime,
{
type: ONESHOT, // holy shit like the game no way
startDelay: 0,
onComplete: (_) -> endOutro(),
});
- FlxTween.tween(this.music, {volume: 0.0}, fadeTime);
- case NONE:
+ FlxTween.tween(this.music, {volume: 0.0}, outroData.fadeTime);
+ case NONE(_):
// Immediately clean up.
endOutro();
default:
@@ -400,7 +407,7 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass
}
}
- public var completeCallback:Void->Void;
+ public var completeCallback:() -> Void;
public function endOutro():Void
{
@@ -596,7 +603,12 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass
public override function toString():String
{
- return 'Conversation($conversationId)';
+ return 'Conversation($id)';
+ }
+
+ static function _fetchData(id:String):Null
+ {
+ return ConversationRegistry.instance.parseEntryDataWithMigration(id, ConversationRegistry.instance.fetchEntryVersion(id));
}
}
diff --git a/source/funkin/play/cutscene/dialogue/ConversationData.hx b/source/funkin/play/cutscene/dialogue/ConversationData.hx
deleted file mode 100644
index d36b75452..000000000
--- a/source/funkin/play/cutscene/dialogue/ConversationData.hx
+++ /dev/null
@@ -1,241 +0,0 @@
-package funkin.play.cutscene.dialogue;
-
-import funkin.util.SerializerUtil;
-
-/**
- * Data about a conversation.
- * Includes what speakers are in the conversation, and what phrases they say.
- */
-@:jsonParse(j -> funkin.play.cutscene.dialogue.ConversationData.fromJson(j))
-@:jsonStringify(v -> v.toJson())
-class ConversationData
-{
- public var version:String;
- public var backdrop:BackdropData;
- public var outro:OutroData;
- public var music:MusicData;
- public var dialogue:Array;
-
- public function new(version:String, backdrop:BackdropData, outro:OutroData, music:MusicData, dialogue:Array)
- {
- this.version = version;
- this.backdrop = backdrop;
- this.outro = outro;
- this.music = music;
- this.dialogue = dialogue;
- }
-
- public static function fromString(i:String):ConversationData
- {
- // TODO: Replace this with json2object.
- if (i == null || i == '') return null;
- var data:
- {
- version:String,
- backdrop:Dynamic, // TODO: tink.Json doesn't like when these are typed
- ?outro:Dynamic, // TODO: tink.Json doesn't like when these are typed
- ?music:Dynamic, // TODO: tink.Json doesn't like when these are typed
- dialogue:Array // TODO: tink.Json doesn't like when these are typed
- } = SerializerUtil.fromJSON(i);
- return fromJson(data);
- }
-
- public static function fromJson(j:Dynamic):ConversationData
- {
- // TODO: Check version and perform migrations if necessary.
- if (j == null) return null;
- return new ConversationData(j.version, BackdropData.fromJson(j.backdrop), OutroData.fromJson(j.outro), MusicData.fromJson(j.music),
- j.dialogue.map(d -> DialogueEntryData.fromJson(d)));
- }
-
- public function toJson():Dynamic
- {
- return {
- version: this.version,
- backdrop: this.backdrop.toJson(),
- dialogue: this.dialogue.map(d -> d.toJson())
- };
- }
-}
-
-/**
- * Data about a single dialogue entry.
- */
-@:jsonParse(j -> funkin.play.cutscene.dialogue.ConversationData.DialogueEntryData.fromJson(j))
-@:jsonStringify(v -> v.toJson())
-class DialogueEntryData
-{
- /**
- * The speaker who says this phrase.
- */
- public var speaker:String;
-
- /**
- * The animation the speaker will play.
- */
- public var speakerAnimation:String;
-
- /**
- * The text box that will appear.
- */
- public var box:String;
-
- /**
- * The animation the dialogue box will play.
- */
- public var boxAnimation:String;
-
- /**
- * The lines of text that will appear in the text box.
- */
- public var text:Array;
-
- /**
- * The relative speed at which the text will scroll.
- * @default 1.0
- */
- public var speed:Float = 1.0;
-
- public function new(speaker:String, speakerAnimation:String, box:String, boxAnimation:String, text:Array, speed:Float = null)
- {
- this.speaker = speaker;
- this.speakerAnimation = speakerAnimation;
- this.box = box;
- this.boxAnimation = boxAnimation;
- this.text = text;
- if (speed != null) this.speed = speed;
- }
-
- public static function fromJson(j:Dynamic):DialogueEntryData
- {
- if (j == null) return null;
- return new DialogueEntryData(j.speaker, j.speakerAnimation, j.box, j.boxAnimation, j.text, j.speed);
- }
-
- public function toJson():Dynamic
- {
- var result:Dynamic =
- {
- speaker: this.speaker,
- speakerAnimation: this.speakerAnimation,
- box: this.box,
- boxAnimation: this.boxAnimation,
- text: this.text,
- };
-
- if (this.speed != 1.0) result.speed = this.speed;
-
- return result;
- }
-}
-
-/**
- * Data about a backdrop.
- */
-@:jsonParse(j -> funkin.play.cutscene.dialogue.ConversationData.BackdropData.fromJson(j))
-@:jsonStringify(v -> v.toJson())
-class BackdropData
-{
- public var type:BackdropType;
- public var data:Dynamic;
-
- public function new(typeStr:String, data:Dynamic)
- {
- this.type = typeStr;
- this.data = data;
- }
-
- public static function fromJson(j:Dynamic):BackdropData
- {
- if (j == null) return null;
- return new BackdropData(j.type, j.data);
- }
-
- public function toJson():Dynamic
- {
- return {
- type: this.type,
- data: this.data
- };
- }
-}
-
-enum abstract BackdropType(String) from String to String
-{
- public var SOLID:BackdropType = 'solid';
-}
-
-/**
- * Data about a music track.
- */
-@:jsonParse(j -> funkin.play.cutscene.dialogue.ConversationData.MusicData.fromJson(j))
-@:jsonStringify(v -> v.toJson())
-class MusicData
-{
- public var asset:String;
-
- public var fadeTime:Float;
-
- @:optional
- @:default(false)
- public var looped:Bool;
-
- public function new(asset:String, looped:Bool, fadeTime:Float = 0.0)
- {
- this.asset = asset;
- this.looped = looped;
- this.fadeTime = fadeTime;
- }
-
- public static function fromJson(j:Dynamic):MusicData
- {
- if (j == null) return null;
- return new MusicData(j.asset, j.looped, j.fadeTime);
- }
-
- public function toJson():Dynamic
- {
- return {
- asset: this.asset,
- looped: this.looped,
- fadeTime: this.fadeTime
- };
- }
-}
-
-/**
- * Data about an outro.
- */
-@:jsonParse(j -> funkin.play.cutscene.dialogue.ConversationData.OutroData.fromJson(j))
-@:jsonStringify(v -> v.toJson())
-class OutroData
-{
- public var type:OutroType;
- public var data:Dynamic;
-
- public function new(?typeStr:String, data:Dynamic)
- {
- this.type = typeStr ?? OutroType.NONE;
- this.data = data;
- }
-
- public static function fromJson(j:Dynamic):OutroData
- {
- if (j == null) return null;
- return new OutroData(j.type, j.data);
- }
-
- public function toJson():Dynamic
- {
- return {
- type: this.type,
- data: this.data
- };
- }
-}
-
-enum abstract OutroType(String) from String to String
-{
- public var NONE:OutroType = 'none';
- public var FADE:OutroType = 'fade';
-}
diff --git a/source/funkin/play/cutscene/dialogue/ConversationDataParser.hx b/source/funkin/play/cutscene/dialogue/ConversationDataParser.hx
deleted file mode 100644
index 9f80f8f9b..000000000
--- a/source/funkin/play/cutscene/dialogue/ConversationDataParser.hx
+++ /dev/null
@@ -1,163 +0,0 @@
-package funkin.play.cutscene.dialogue;
-
-import openfl.Assets;
-import funkin.util.assets.DataAssets;
-import funkin.play.cutscene.dialogue.ScriptedConversation;
-
-/**
- * Contains utilities for loading and parsing conversation data.
- * TODO: Refactor to use the json2object + BaseRegistry system that actually validates things for you.
- */
-class ConversationDataParser
-{
- public static final CONVERSATION_DATA_VERSION:String = '1.0.0';
- public static final CONVERSATION_DATA_VERSION_RULE:String = '1.0.x';
-
- static final conversationCache:Map = new Map();
- static final conversationScriptedClass:Map = new Map();
-
- static final DEFAULT_CONVERSATION_ID:String = 'UNKNOWN';
-
- /**
- * Parses and preloads the game's conversation data and scripts when the game starts.
- *
- * If you want to force conversations to be reloaded, you can just call this function again.
- */
- public static function loadConversationCache():Void
- {
- clearConversationCache();
- trace('Loading dialogue conversation cache...');
-
- //
- // SCRIPTED CONVERSATIONS
- //
- var scriptedConversationClassNames:Array = ScriptedConversation.listScriptClasses();
- trace(' Instantiating ${scriptedConversationClassNames.length} scripted conversations...');
- for (conversationCls in scriptedConversationClassNames)
- {
- var conversation:Conversation = ScriptedConversation.init(conversationCls, DEFAULT_CONVERSATION_ID);
- if (conversation != null)
- {
- trace(' Loaded scripted conversation: ${conversationCls}');
- // Disable the rendering logic for conversation until it's loaded.
- // Note that kill() =/= destroy()
- conversation.kill();
-
- // Then store it.
- conversationCache.set(conversation.conversationId, conversation);
- }
- else
- {
- trace(' Failed to instantiate scripted conversation class: ${conversationCls}');
- }
- }
-
- //
- // UNSCRIPTED CONVERSATIONS
- //
- // Scripts refers to code here, not the actual dialogue.
- var conversationIdList:Array = DataAssets.listDataFilesInPath('dialogue/conversations/');
- // Filter out conversations that are scripted.
- var unscriptedConversationIds:Array = conversationIdList.filter(function(conversationId:String):Bool {
- return !conversationCache.exists(conversationId);
- });
- trace(' Fetching data for ${unscriptedConversationIds.length} conversations...');
- for (conversationId in unscriptedConversationIds)
- {
- try
- {
- var conversation:Conversation = new Conversation(conversationId);
- // Say something offensive to kill the conversation.
- // We will revive it later.
- conversation.kill();
- if (conversation != null)
- {
- trace(' Loaded conversation data: ${conversation.conversationId}');
- conversationCache.set(conversation.conversationId, conversation);
- }
- }
- catch (e)
- {
- trace(e);
- continue;
- }
- }
- }
-
- /**
- * Fetches data for a conversation and returns a Conversation instance,
- * ready to be displayed.
- * @param conversationId The ID of the conversation to fetch.
- * @return The conversation instance, or null if the conversation was not found.
- */
- public static function fetchConversation(conversationId:String):Null
- {
- if (conversationId != null && conversationId != '' && conversationCache.exists(conversationId))
- {
- trace('Successfully fetched conversation: ${conversationId}');
- var conversation:Conversation = conversationCache.get(conversationId);
- // ...ANYway...
- conversation.revive();
- return conversation;
- }
- else
- {
- trace('Failed to fetch conversation, not found in cache: ${conversationId}');
- return null;
- }
- }
-
- static function clearConversationCache():Void
- {
- if (conversationCache != null)
- {
- for (conversation in conversationCache)
- {
- conversation.destroy();
- }
- conversationCache.clear();
- }
- }
-
- public static function listConversationIds():Array
- {
- return conversationCache.keys().array();
- }
-
- /**
- * Load a conversation's JSON file, parse its data, and return it.
- *
- * @param conversationId The conversation to load.
- * @return The conversation data, or null if validation failed.
- */
- public static function parseConversationData(conversationId:String):Null
- {
- trace('Parsing conversation data: ${conversationId}');
- var rawJson:String = loadConversationFile(conversationId);
-
- try
- {
- var conversationData:ConversationData = ConversationData.fromString(rawJson);
- return conversationData;
- }
- catch (e)
- {
- trace('Failed to parse conversation ($conversationId).');
- trace(e);
- return null;
- }
- }
-
- static function loadConversationFile(conversationPath:String):String
- {
- var conversationFilePath:String = Paths.json('dialogue/conversations/${conversationPath}');
- var rawJson:String = Assets.getText(conversationFilePath).trim();
-
- while (!rawJson.endsWith('}') && rawJson.length > 0)
- {
- rawJson = rawJson.substr(0, rawJson.length - 1);
- }
-
- return rawJson;
- }
-}
diff --git a/source/funkin/play/cutscene/dialogue/DialogueBox.hx b/source/funkin/play/cutscene/dialogue/DialogueBox.hx
index cdac3c233..6f8a0086a 100644
--- a/source/funkin/play/cutscene/dialogue/DialogueBox.hx
+++ b/source/funkin/play/cutscene/dialogue/DialogueBox.hx
@@ -1,6 +1,7 @@
package funkin.play.cutscene.dialogue;
import flixel.FlxSprite;
+import funkin.data.IRegistryEntry;
import flixel.group.FlxSpriteGroup;
import flixel.graphics.frames.FlxFramesCollection;
import flixel.text.FlxText;
@@ -9,18 +10,21 @@ import funkin.util.assets.FlxAnimationUtil;
import funkin.modding.events.ScriptEvent;
import funkin.modding.IScriptedClass.IDialogueScriptedClass;
import flixel.util.FlxColor;
+import funkin.data.dialogue.DialogueBoxData;
+import funkin.data.dialogue.DialogueBoxRegistry;
-class DialogueBox extends FlxSpriteGroup implements IDialogueScriptedClass
+class DialogueBox extends FlxSpriteGroup implements IDialogueScriptedClass implements IRegistryEntry
{
- public final dialogueBoxId:String;
+ public final id:String;
+
public var dialogueBoxName(get, never):String;
function get_dialogueBoxName():String
{
- return boxData?.name ?? 'UNKNOWN';
+ return _data.name ?? 'UNKNOWN';
}
- var boxData:DialogueBoxData;
+ public final _data:DialogueBoxData;
/**
* Offset the speaker's sprite by this much when playing each animation.
@@ -88,13 +92,16 @@ class DialogueBox extends FlxSpriteGroup implements IDialogueScriptedClass
return this.speed;
}
- public function new(dialogueBoxId:String)
+ public function new(id:String)
{
super();
- this.dialogueBoxId = dialogueBoxId;
- this.boxData = DialogueBoxDataParser.parseDialogueBoxData(this.dialogueBoxId);
+ this.id = id;
+ this._data = _fetchData(id);
- if (boxData == null) throw 'Could not load dialogue box data for box ID "$dialogueBoxId"';
+ if (_data == null)
+ {
+ throw 'Could not parse dialogue box data for id: $id';
+ }
}
public function onCreate(event:ScriptEvent):Void
@@ -115,18 +122,18 @@ class DialogueBox extends FlxSpriteGroup implements IDialogueScriptedClass
function loadSpritesheet():Void
{
- trace('[DIALOGUE BOX] Loading spritesheet ${boxData.assetPath} for ${dialogueBoxId}');
+ trace('[DIALOGUE BOX] Loading spritesheet ${_data.assetPath} for ${id}');
- var tex:FlxFramesCollection = Paths.getSparrowAtlas(boxData.assetPath);
+ var tex:FlxFramesCollection = Paths.getSparrowAtlas(_data.assetPath);
if (tex == null)
{
- trace('Could not load Sparrow sprite: ${boxData.assetPath}');
+ trace('Could not load Sparrow sprite: ${_data.assetPath}');
return;
}
this.boxSprite.frames = tex;
- if (boxData.isPixel)
+ if (_data.isPixel)
{
this.boxSprite.antialiasing = false;
}
@@ -135,9 +142,10 @@ class DialogueBox extends FlxSpriteGroup implements IDialogueScriptedClass
this.boxSprite.antialiasing = true;
}
- this.flipX = boxData.flipX;
- this.globalOffsets = boxData.offsets;
- this.setScale(boxData.scale);
+ this.flipX = _data.flipX;
+ this.flipY = _data.flipY;
+ this.globalOffsets = _data.offsets;
+ this.setScale(_data.scale);
}
public function setText(newText:String):Void
@@ -184,11 +192,11 @@ class DialogueBox extends FlxSpriteGroup implements IDialogueScriptedClass
function loadAnimations():Void
{
- trace('[DIALOGUE BOX] Loading ${boxData.animations.length} animations for ${dialogueBoxId}');
+ trace('[DIALOGUE BOX] Loading ${_data.animations.length} animations for ${id}');
- FlxAnimationUtil.addAtlasAnimations(this.boxSprite, boxData.animations);
+ FlxAnimationUtil.addAtlasAnimations(this.boxSprite, _data.animations);
- for (anim in boxData.animations)
+ for (anim in _data.animations)
{
if (anim.offsets == null)
{
@@ -201,7 +209,7 @@ class DialogueBox extends FlxSpriteGroup implements IDialogueScriptedClass
}
var animNames:Array = this.boxSprite?.animation?.getNameList() ?? [];
- trace('[DIALOGUE BOX] Successfully loaded ${animNames.length} animations for ${dialogueBoxId}');
+ trace('[DIALOGUE BOX] Successfully loaded ${animNames.length} animations for ${id}');
boxSprite.animation.callback = this.onAnimationFrame;
boxSprite.animation.finishCallback = this.onAnimationFinished;
@@ -234,16 +242,16 @@ class DialogueBox extends FlxSpriteGroup implements IDialogueScriptedClass
function loadText():Void
{
textDisplay = new FlxTypeText(0, 0, 300, '', 32);
- textDisplay.fieldWidth = boxData.text.width;
- textDisplay.setFormat('Pixel Arial 11 Bold', boxData.text.size, FlxColor.fromString(boxData.text.color), LEFT, SHADOW,
- FlxColor.fromString(boxData.text.shadowColor ?? '#00000000'), false);
- textDisplay.borderSize = boxData.text.shadowWidth ?? 2;
+ textDisplay.fieldWidth = _data.text.width;
+ textDisplay.setFormat(_data.text.fontFamily, _data.text.size, FlxColor.fromString(_data.text.color), LEFT, SHADOW,
+ FlxColor.fromString(_data.text.shadowColor ?? '#00000000'), false);
+ textDisplay.borderSize = _data.text.shadowWidth ?? 2;
textDisplay.sounds = [FlxG.sound.load(Paths.sound('pixelText'), 0.6)];
textDisplay.completeCallback = onTypingComplete;
- textDisplay.x += boxData.text.offsets[0];
- textDisplay.y += boxData.text.offsets[1];
+ textDisplay.x += _data.text.offsets[0];
+ textDisplay.y += _data.text.offsets[1];
add(textDisplay);
}
@@ -374,4 +382,14 @@ class DialogueBox extends FlxSpriteGroup implements IDialogueScriptedClass
}
public function onScriptEvent(event:ScriptEvent):Void {}
+
+ public override function toString():String
+ {
+ return 'DialogueBox($id)';
+ }
+
+ static function _fetchData(id:String):Null
+ {
+ return DialogueBoxRegistry.instance.parseEntryDataWithMigration(id, DialogueBoxRegistry.instance.fetchEntryVersion(id));
+ }
}
diff --git a/source/funkin/play/cutscene/dialogue/DialogueBoxData.hx b/source/funkin/play/cutscene/dialogue/DialogueBoxData.hx
deleted file mode 100644
index 322a637e7..000000000
--- a/source/funkin/play/cutscene/dialogue/DialogueBoxData.hx
+++ /dev/null
@@ -1,125 +0,0 @@
-package funkin.play.cutscene.dialogue;
-
-import funkin.data.animation.AnimationData;
-import funkin.util.SerializerUtil;
-
-/**
- * Data about a text box.
- */
-@:jsonParse(j -> funkin.play.cutscene.dialogue.DialogueBoxData.fromJson(j))
-@:jsonStringify(v -> v.toJson())
-class DialogueBoxData
-{
- public var version:String;
- public var name:String;
- public var assetPath:String;
- public var flipX:Bool;
- public var flipY:Bool;
- public var isPixel:Bool;
- public var offsets:Array;
- public var text:DialogueBoxTextData;
- public var scale:Float;
- public var animations:Array;
-
- public function new(version:String, name:String, assetPath:String, flipX:Bool = false, flipY:Bool = false, isPixel:Bool = false, offsets:Null>,
- text:DialogueBoxTextData, scale:Float = 1.0, animations:Array)
- {
- this.version = version;
- this.name = name;
- this.assetPath = assetPath;
- this.flipX = flipX;
- this.flipY = flipY;
- this.isPixel = isPixel;
- this.offsets = offsets ?? [0, 0];
- this.text = text;
- this.scale = scale;
- this.animations = animations;
- }
-
- public static function fromString(i:String):DialogueBoxData
- {
- // TODO: Replace this with json2object.
- if (i == null || i == '') return null;
- var data:
- {
- version:String,
- name:String,
- assetPath:String,
- flipX:Bool,
- flipY:Bool,
- isPixel:Bool,
- ?offsets:Array,
- text:Dynamic,
- scale:Float,
- animations:Array
- } = SerializerUtil.fromJSON(i);
- return fromJson(data);
- }
-
- public static function fromJson(j:Dynamic):DialogueBoxData
- {
- // TODO: Check version and perform migrations if necessary.
- if (j == null) return null;
- return new DialogueBoxData(j.version, j.name, j.assetPath, j.flipX, j.flipY, j.isPixel, j.offsets, DialogueBoxTextData.fromJson(j.text), j.scale,
- j.animations);
- }
-
- public function toJson():Dynamic
- {
- return {
- version: this.version,
- name: this.name,
- assetPath: this.assetPath,
- flipX: this.flipX,
- flipY: this.flipY,
- isPixel: this.isPixel,
- offsets: this.offsets,
- scale: this.scale,
- animations: this.animations
- };
- }
-}
-
-/**
- * Data about text in a text box.
- */
-@:jsonParse(j -> funkin.play.cutscene.dialogue.DialogueBoxTextData.fromJson(j))
-@:jsonStringify(v -> v.toJson())
-class DialogueBoxTextData
-{
- public var offsets:Array;
- public var width:Int;
- public var size:Int;
- public var color:String;
- public var shadowColor:Null;
- public var shadowWidth:Null;
-
- public function new(offsets:Null>, ?width:Int, ?size:Int, color:String, ?shadowColor:String, shadowWidth:Null)
- {
- this.offsets = offsets ?? [0, 0];
- this.width = width ?? 300;
- this.size = size ?? 32;
- this.color = color;
- this.shadowColor = shadowColor;
- this.shadowWidth = shadowWidth;
- }
-
- public static function fromJson(j:Dynamic):DialogueBoxTextData
- {
- // TODO: Check version and perform migrations if necessary.
- if (j == null) return null;
- return new DialogueBoxTextData(j.offsets, j.width, j.size, j.color, j.shadowColor, j.shadowWidth);
- }
-
- public function toJson():Dynamic
- {
- return {
- offsets: this.offsets,
- width: this.width,
- size: this.size,
- color: this.color,
- shadowColor: this.shadowColor,
- shadowWidth: this.shadowWidth,
- };
- }
-}
diff --git a/source/funkin/play/cutscene/dialogue/DialogueBoxDataParser.hx b/source/funkin/play/cutscene/dialogue/DialogueBoxDataParser.hx
deleted file mode 100644
index cb00dd80d..000000000
--- a/source/funkin/play/cutscene/dialogue/DialogueBoxDataParser.hx
+++ /dev/null
@@ -1,159 +0,0 @@
-package funkin.play.cutscene.dialogue;
-
-import openfl.Assets;
-import funkin.util.assets.DataAssets;
-import funkin.play.cutscene.dialogue.DialogueBox;
-import funkin.play.cutscene.dialogue.ScriptedDialogueBox;
-
-/**
- * Contains utilities for loading and parsing dialogueBox data.
- */
-class DialogueBoxDataParser
-{
- public static final DIALOGUE_BOX_DATA_VERSION:String = '1.0.0';
- public static final DIALOGUE_BOX_DATA_VERSION_RULE:String = '1.0.x';
-
- static final dialogueBoxCache:Map = new Map();
-
- static final dialogueBoxScriptedClass:Map = new Map();
-
- static final DEFAULT_DIALOGUE_BOX_ID:String = 'UNKNOWN';
-
- /**
- * Parses and preloads the game's dialogueBox data and scripts when the game starts.
- *
- * If you want to force dialogue boxes to be reloaded, you can just call this function again.
- */
- public static function loadDialogueBoxCache():Void
- {
- clearDialogueBoxCache();
- trace('Loading dialogue box cache...');
-
- //
- // SCRIPTED CONVERSATIONS
- //
- var scriptedDialogueBoxClassNames:Array = ScriptedDialogueBox.listScriptClasses();
- trace(' Instantiating ${scriptedDialogueBoxClassNames.length} scripted dialogue boxes...');
- for (dialogueBoxCls in scriptedDialogueBoxClassNames)
- {
- var dialogueBox:DialogueBox = ScriptedDialogueBox.init(dialogueBoxCls, DEFAULT_DIALOGUE_BOX_ID);
- if (dialogueBox != null)
- {
- trace(' Loaded scripted dialogue box: ${dialogueBox.dialogueBoxName}');
- // Disable the rendering logic for dialogueBox until it's loaded.
- // Note that kill() =/= destroy()
- dialogueBox.kill();
-
- // Then store it.
- dialogueBoxCache.set(dialogueBox.dialogueBoxId, dialogueBox);
- }
- else
- {
- trace(' Failed to instantiate scripted dialogueBox class: ${dialogueBoxCls}');
- }
- }
-
- //
- // UNSCRIPTED CONVERSATIONS
- //
- // Scripts refers to code here, not the actual dialogue.
- var dialogueBoxIdList:Array = DataAssets.listDataFilesInPath('dialogue/boxes/');
- // Filter out dialogue boxes that are scripted.
- var unscriptedDialogueBoxIds:Array = dialogueBoxIdList.filter(function(dialogueBoxId:String):Bool {
- return !dialogueBoxCache.exists(dialogueBoxId);
- });
- trace(' Fetching data for ${unscriptedDialogueBoxIds.length} dialogue boxes...');
- for (dialogueBoxId in unscriptedDialogueBoxIds)
- {
- try
- {
- var dialogueBox:DialogueBox = new DialogueBox(dialogueBoxId);
- if (dialogueBox != null)
- {
- trace(' Loaded dialogueBox data: ${dialogueBox.dialogueBoxName}');
- dialogueBoxCache.set(dialogueBox.dialogueBoxId, dialogueBox);
- }
- }
- catch (e)
- {
- trace(e);
- continue;
- }
- }
- }
-
- /**
- * Fetches data for a dialogueBox and returns a DialogueBox instance,
- * ready to be displayed.
- * @param dialogueBoxId The ID of the dialogueBox to fetch.
- * @return The dialogueBox instance, or null if the dialogueBox was not found.
- */
- public static function fetchDialogueBox(dialogueBoxId:String):Null
- {
- if (dialogueBoxId != null && dialogueBoxId != '' && dialogueBoxCache.exists(dialogueBoxId))
- {
- trace('Successfully fetched dialogueBox: ${dialogueBoxId}');
- var dialogueBox:DialogueBox = dialogueBoxCache.get(dialogueBoxId);
- dialogueBox.revive();
- return dialogueBox;
- }
- else
- {
- trace('Failed to fetch dialogueBox, not found in cache: ${dialogueBoxId}');
- return null;
- }
- }
-
- static function clearDialogueBoxCache():Void
- {
- if (dialogueBoxCache != null)
- {
- for (dialogueBox in dialogueBoxCache)
- {
- dialogueBox.destroy();
- }
- dialogueBoxCache.clear();
- }
- }
-
- public static function listDialogueBoxIds():Array
- {
- return dialogueBoxCache.keys().array();
- }
-
- /**
- * Load a dialogueBox's JSON file, parse its data, and return it.
- *
- * @param dialogueBoxId The dialogueBox to load.
- * @return The dialogueBox data, or null if validation failed.
- */
- public static function parseDialogueBoxData(dialogueBoxId:String):Null
- {
- var rawJson:String = loadDialogueBoxFile(dialogueBoxId);
-
- try
- {
- var dialogueBoxData:DialogueBoxData = DialogueBoxData.fromString(rawJson);
- return dialogueBoxData;
- }
- catch (e)
- {
- trace('Failed to parse dialogueBox ($dialogueBoxId).');
- trace(e);
- return null;
- }
- }
-
- static function loadDialogueBoxFile(dialogueBoxPath:String):String
- {
- var dialogueBoxFilePath:String = Paths.json('dialogue/boxes/${dialogueBoxPath}');
- var rawJson:String = Assets.getText(dialogueBoxFilePath).trim();
-
- while (!rawJson.endsWith('}') && rawJson.length > 0)
- {
- rawJson = rawJson.substr(0, rawJson.length - 1);
- }
-
- return rawJson;
- }
-}
diff --git a/source/funkin/play/cutscene/dialogue/ScriptedConversation.hx b/source/funkin/play/cutscene/dialogue/ScriptedConversation.hx
index 4fe383a5e..cb7344273 100644
--- a/source/funkin/play/cutscene/dialogue/ScriptedConversation.hx
+++ b/source/funkin/play/cutscene/dialogue/ScriptedConversation.hx
@@ -1,4 +1,10 @@
package funkin.play.cutscene.dialogue;
+/**
+ * A script that can be tied to a Conversation.
+ * Create a scripted class that extends Conversation to use this.
+ * This allows you to customize how a specific conversation appears and behaves.
+ * Someone clever could use this to add branching dialogue I think.
+ */
@:hscriptClass
class ScriptedConversation extends Conversation implements polymod.hscript.HScriptedClass {}
diff --git a/source/funkin/play/cutscene/dialogue/ScriptedDialogueBox.hx b/source/funkin/play/cutscene/dialogue/ScriptedDialogueBox.hx
index a1b36c7c2..7689fc0d9 100644
--- a/source/funkin/play/cutscene/dialogue/ScriptedDialogueBox.hx
+++ b/source/funkin/play/cutscene/dialogue/ScriptedDialogueBox.hx
@@ -1,4 +1,9 @@
package funkin.play.cutscene.dialogue;
+/**
+ * A script that can be tied to a DialogueBox.
+ * Create a scripted class that extends DialogueBox to use this.
+ * This allows you to customize how a specific dialogue box appears.
+ */
@:hscriptClass
class ScriptedDialogueBox extends DialogueBox implements polymod.hscript.HScriptedClass {}
diff --git a/source/funkin/play/cutscene/dialogue/Speaker.hx b/source/funkin/play/cutscene/dialogue/Speaker.hx
index d7ed004f1..d5bffd7b0 100644
--- a/source/funkin/play/cutscene/dialogue/Speaker.hx
+++ b/source/funkin/play/cutscene/dialogue/Speaker.hx
@@ -1,27 +1,30 @@
package funkin.play.cutscene.dialogue;
import flixel.FlxSprite;
+import funkin.data.IRegistryEntry;
import funkin.modding.events.ScriptEvent;
import flixel.graphics.frames.FlxFramesCollection;
import funkin.util.assets.FlxAnimationUtil;
import funkin.modding.IScriptedClass.IDialogueScriptedClass;
+import funkin.data.dialogue.SpeakerData;
+import funkin.data.dialogue.SpeakerRegistry;
/**
* The character sprite which displays during dialogue.
*
* Most conversations have two speakers, with one being flipped.
*/
-class Speaker extends FlxSprite implements IDialogueScriptedClass
+class Speaker extends FlxSprite implements IDialogueScriptedClass implements IRegistryEntry
{
/**
* The internal ID for this speaker.
*/
- public final speakerId:String;
+ public final id:String;
/**
* The full data for a speaker.
*/
- var speakerData:SpeakerData;
+ public final _data:SpeakerData;
/**
* A readable name for this speaker.
@@ -30,7 +33,7 @@ class Speaker extends FlxSprite implements IDialogueScriptedClass
function get_speakerName():String
{
- return speakerData.name;
+ return _data.name;
}
/**
@@ -75,14 +78,17 @@ class Speaker extends FlxSprite implements IDialogueScriptedClass
return globalOffsets = value;
}
- public function new(speakerId:String)
+ public function new(id:String)
{
super();
- this.speakerId = speakerId;
- this.speakerData = SpeakerDataParser.parseSpeakerData(this.speakerId);
+ this.id = id;
+ this._data = _fetchData(id);
- if (speakerData == null) throw 'Could not load speaker data for speaker ID "$speakerId"';
+ if (_data == null)
+ {
+ throw 'Could not parse speaker data for id: $id';
+ }
}
/**
@@ -102,18 +108,18 @@ class Speaker extends FlxSprite implements IDialogueScriptedClass
function loadSpritesheet():Void
{
- trace('[SPEAKER] Loading spritesheet ${speakerData.assetPath} for ${speakerId}');
+ trace('[SPEAKER] Loading spritesheet ${_data.assetPath} for ${id}');
- var tex:FlxFramesCollection = Paths.getSparrowAtlas(speakerData.assetPath);
+ var tex:FlxFramesCollection = Paths.getSparrowAtlas(_data.assetPath);
if (tex == null)
{
- trace('Could not load Sparrow sprite: ${speakerData.assetPath}');
+ trace('Could not load Sparrow sprite: ${_data.assetPath}');
return;
}
this.frames = tex;
- if (speakerData.isPixel)
+ if (_data.isPixel)
{
this.antialiasing = false;
}
@@ -122,9 +128,10 @@ class Speaker extends FlxSprite implements IDialogueScriptedClass
this.antialiasing = true;
}
- this.flipX = speakerData.flipX;
- this.globalOffsets = speakerData.offsets;
- this.setScale(speakerData.scale);
+ this.flipX = _data.flipX;
+ this.flipY = _data.flipY;
+ this.globalOffsets = _data.offsets;
+ this.setScale(_data.scale);
}
/**
@@ -141,11 +148,11 @@ class Speaker extends FlxSprite implements IDialogueScriptedClass
function loadAnimations():Void
{
- trace('[SPEAKER] Loading ${speakerData.animations.length} animations for ${speakerId}');
+ trace('[SPEAKER] Loading ${_data.animations.length} animations for ${id}');
- FlxAnimationUtil.addAtlasAnimations(this, speakerData.animations);
+ FlxAnimationUtil.addAtlasAnimations(this, _data.animations);
- for (anim in speakerData.animations)
+ for (anim in _data.animations)
{
if (anim.offsets == null)
{
@@ -158,7 +165,7 @@ class Speaker extends FlxSprite implements IDialogueScriptedClass
}
var animNames:Array = this.animation.getNameList();
- trace('[SPEAKER] Successfully loaded ${animNames.length} animations for ${speakerId}');
+ trace('[SPEAKER] Successfully loaded ${animNames.length} animations for ${id}');
}
/**
@@ -271,4 +278,14 @@ class Speaker extends FlxSprite implements IDialogueScriptedClass
}
public function onScriptEvent(event:ScriptEvent):Void {}
+
+ public override function toString():String
+ {
+ return 'Speaker($id)';
+ }
+
+ static function _fetchData(id:String):Null
+ {
+ return SpeakerRegistry.instance.parseEntryDataWithMigration(id, SpeakerRegistry.instance.fetchEntryVersion(id));
+ }
}
diff --git a/source/funkin/play/cutscene/dialogue/SpeakerData.hx b/source/funkin/play/cutscene/dialogue/SpeakerData.hx
deleted file mode 100644
index 7fe6a3b72..000000000
--- a/source/funkin/play/cutscene/dialogue/SpeakerData.hx
+++ /dev/null
@@ -1,80 +0,0 @@
-package funkin.play.cutscene.dialogue;
-
-import funkin.data.animation.AnimationData;
-import funkin.util.SerializerUtil;
-
-/**
- * Data about a conversation.
- * Includes what speakers are in the conversation, and what phrases they say.
- */
-@:jsonParse(j -> funkin.play.cutscene.dialogue.SpeakerData.fromJson(j))
-@:jsonStringify(v -> v.toJson())
-class SpeakerData
-{
- public var version:String;
- public var name:String;
- public var assetPath:String;
- public var flipX:Bool;
- public var isPixel:Bool;
- public var offsets:Array;
- public var scale:Float;
- public var animations:Array;
-
- public function new(version:String, name:String, assetPath:String, animations:Array, ?offsets:Array, flipX:Bool = false,
- isPixel:Bool = false, ?scale:Float = 1.0)
- {
- this.version = version;
- this.name = name;
- this.assetPath = assetPath;
- this.animations = animations;
-
- this.offsets = offsets;
- if (this.offsets == null || this.offsets == []) this.offsets = [0, 0];
-
- this.flipX = flipX;
- this.isPixel = isPixel;
- this.scale = scale;
- }
-
- public static function fromString(i:String):SpeakerData
- {
- // TODO: Replace this with json2object.
- if (i == null || i == '') return null;
- var data:
- {
- version:String,
- name:String,
- assetPath:String,
- animations:Array,
- ?offsets:Array,
- ?flipX:Bool,
- ?isPixel:Bool,
- ?scale:Float
- } = SerializerUtil.fromJSON(i);
- return fromJson(data);
- }
-
- public static function fromJson(j:Dynamic):SpeakerData
- {
- // TODO: Check version and perform migrations if necessary.
- if (j == null) return null;
- return new SpeakerData(j.version, j.name, j.assetPath, j.animations, j.offsets, j.flipX, j.isPixel, j.scale);
- }
-
- public function toJson():Dynamic
- {
- var result:Dynamic =
- {
- version: this.version,
- name: this.name,
- assetPath: this.assetPath,
- animations: this.animations,
- flipX: this.flipX,
- isPixel: this.isPixel
- };
-
- if (this.scale != 1.0) result.scale = this.scale;
-
- return result;
- }
-}
diff --git a/source/funkin/play/cutscene/dialogue/SpeakerDataParser.hx b/source/funkin/play/cutscene/dialogue/SpeakerDataParser.hx
deleted file mode 100644
index f7ddb099f..000000000
--- a/source/funkin/play/cutscene/dialogue/SpeakerDataParser.hx
+++ /dev/null
@@ -1,159 +0,0 @@
-package funkin.play.cutscene.dialogue;
-
-import openfl.Assets;
-import funkin.util.assets.DataAssets;
-import funkin.play.cutscene.dialogue.Speaker;
-import funkin.play.cutscene.dialogue.ScriptedSpeaker;
-
-/**
- * Contains utilities for loading and parsing speaker data.
- */
-class SpeakerDataParser
-{
- public static final SPEAKER_DATA_VERSION:String = '1.0.0';
- public static final SPEAKER_DATA_VERSION_RULE:String = '1.0.x';
-
- static final speakerCache:Map = new Map();
-
- static final speakerScriptedClass:Map = new Map();
-
- static final DEFAULT_SPEAKER_ID:String = 'UNKNOWN';
-
- /**
- * Parses and preloads the game's speaker data and scripts when the game starts.
- *
- * If you want to force speakers to be reloaded, you can just call this function again.
- */
- public static function loadSpeakerCache():Void
- {
- clearSpeakerCache();
- trace('Loading dialogue speaker cache...');
-
- //
- // SCRIPTED CONVERSATIONS
- //
- var scriptedSpeakerClassNames:Array = ScriptedSpeaker.listScriptClasses();
- trace(' Instantiating ${scriptedSpeakerClassNames.length} scripted speakers...');
- for (speakerCls in scriptedSpeakerClassNames)
- {
- var speaker:Speaker = ScriptedSpeaker.init(speakerCls, DEFAULT_SPEAKER_ID);
- if (speaker != null)
- {
- trace(' Loaded scripted speaker: ${speaker.speakerName}');
- // Disable the rendering logic for speaker until it's loaded.
- // Note that kill() =/= destroy()
- speaker.kill();
-
- // Then store it.
- speakerCache.set(speaker.speakerId, speaker);
- }
- else
- {
- trace(' Failed to instantiate scripted speaker class: ${speakerCls}');
- }
- }
-
- //
- // UNSCRIPTED CONVERSATIONS
- //
- // Scripts refers to code here, not the actual dialogue.
- var speakerIdList:Array = DataAssets.listDataFilesInPath('dialogue/speakers/');
- // Filter out speakers that are scripted.
- var unscriptedSpeakerIds:Array = speakerIdList.filter(function(speakerId:String):Bool {
- return !speakerCache.exists(speakerId);
- });
- trace(' Fetching data for ${unscriptedSpeakerIds.length} speakers...');
- for (speakerId in unscriptedSpeakerIds)
- {
- try
- {
- var speaker:Speaker = new Speaker(speakerId);
- if (speaker != null)
- {
- trace(' Loaded speaker data: ${speaker.speakerName}');
- speakerCache.set(speaker.speakerId, speaker);
- }
- }
- catch (e)
- {
- trace(e);
- continue;
- }
- }
- }
-
- /**
- * Fetches data for a speaker and returns a Speaker instance,
- * ready to be displayed.
- * @param speakerId The ID of the speaker to fetch.
- * @return The speaker instance, or null if the speaker was not found.
- */
- public static function fetchSpeaker(speakerId:String):Null
- {
- if (speakerId != null && speakerId != '' && speakerCache.exists(speakerId))
- {
- trace('Successfully fetched speaker: ${speakerId}');
- var speaker:Speaker = speakerCache.get(speakerId);
- speaker.revive();
- return speaker;
- }
- else
- {
- trace('Failed to fetch speaker, not found in cache: ${speakerId}');
- return null;
- }
- }
-
- static function clearSpeakerCache():Void
- {
- if (speakerCache != null)
- {
- for (speaker in speakerCache)
- {
- speaker.destroy();
- }
- speakerCache.clear();
- }
- }
-
- public static function listSpeakerIds():Array
- {
- return speakerCache.keys().array();
- }
-
- /**
- * Load a speaker's JSON file, parse its data, and return it.
- *
- * @param speakerId The speaker to load.
- * @return The speaker data, or null if validation failed.
- */
- public static function parseSpeakerData(speakerId:String):Null
- {
- var rawJson:String = loadSpeakerFile(speakerId);
-
- try
- {
- var speakerData:SpeakerData = SpeakerData.fromString(rawJson);
- return speakerData;
- }
- catch (e)
- {
- trace('Failed to parse speaker ($speakerId).');
- trace(e);
- return null;
- }
- }
-
- static function loadSpeakerFile(speakerPath:String):String
- {
- var speakerFilePath:String = Paths.json('dialogue/speakers/${speakerPath}');
- var rawJson:String = Assets.getText(speakerFilePath).trim();
-
- while (!rawJson.endsWith('}') && rawJson.length > 0)
- {
- rawJson = rawJson.substr(0, rawJson.length - 1);
- }
-
- return rawJson;
- }
-}
diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx
index 7730fdc95..96f31899b 100644
--- a/source/funkin/ui/debug/charting/ChartEditorState.hx
+++ b/source/funkin/ui/debug/charting/ChartEditorState.hx
@@ -1296,6 +1296,29 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
return currentSongChartData.events = value;
}
+ /**
+ * Convenience property to get the rating for this difficulty in the Freeplay menu.
+ */
+ var currentSongChartDifficultyRating(get, set):Int;
+
+ function get_currentSongChartDifficultyRating():Int
+ {
+ var result:Null = currentSongMetadata.playData.ratings.get(selectedDifficulty);
+ if (result == null)
+ {
+ // Initialize to the default value if not set.
+ currentSongMetadata.playData.ratings.set(selectedDifficulty, 0);
+ return 0;
+ }
+ return result;
+ }
+
+ function set_currentSongChartDifficultyRating(value:Int):Int
+ {
+ currentSongMetadata.playData.ratings.set(selectedDifficulty, value);
+ return value;
+ }
+
var currentSongNoteStyle(get, set):String;
function get_currentSongNoteStyle():String
@@ -3160,7 +3183,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
handleTestKeybinds();
handleHelpKeybinds();
- #if debug
+ #if (debug || FORCE_DEBUG_VERSION)
handleQuickWatch();
#end
diff --git a/source/funkin/ui/debug/charting/toolboxes/ChartEditorMetadataToolbox.hx b/source/funkin/ui/debug/charting/toolboxes/ChartEditorMetadataToolbox.hx
index 5d8c25bae..f85307c64 100644
--- a/source/funkin/ui/debug/charting/toolboxes/ChartEditorMetadataToolbox.hx
+++ b/source/funkin/ui/debug/charting/toolboxes/ChartEditorMetadataToolbox.hx
@@ -151,6 +151,10 @@ class ChartEditorMetadataToolbox extends ChartEditorBaseToolbox
labelScrollSpeed.text = 'Scroll Speed: ${chartEditorState.currentSongChartScrollSpeed}x';
};
+ inputDifficultyRating.onChange = function(event:UIEvent) {
+ chartEditorState.currentSongChartDifficultyRating = event.target.value;
+ };
+
buttonCharacterOpponent.onClick = function(_) {
chartEditorState.openCharacterDropdown(CharacterType.DAD, false);
};
@@ -175,6 +179,7 @@ class ChartEditorMetadataToolbox extends ChartEditorBaseToolbox
inputStage.value = chartEditorState.currentSongMetadata.playData.stage;
inputNoteStyle.value = chartEditorState.currentSongMetadata.playData.noteStyle;
inputBPM.value = chartEditorState.currentSongMetadata.timeChanges[0].bpm;
+ inputDifficultyRating.value = chartEditorState.currentSongChartDifficultyRating;
inputScrollSpeed.value = chartEditorState.currentSongChartScrollSpeed;
labelScrollSpeed.text = 'Scroll Speed: ${chartEditorState.currentSongChartScrollSpeed}x';
frameVariation.text = 'Variation: ${chartEditorState.selectedVariation.toTitleCase()}';
diff --git a/source/funkin/play/cutscene/dialogue/ConversationDebugState.hx b/source/funkin/ui/debug/dialogue/ConversationDebugState.hx
similarity index 64%
rename from source/funkin/play/cutscene/dialogue/ConversationDebugState.hx
rename to source/funkin/ui/debug/dialogue/ConversationDebugState.hx
index 13697b9f4..33a6f365a 100644
--- a/source/funkin/play/cutscene/dialogue/ConversationDebugState.hx
+++ b/source/funkin/ui/debug/dialogue/ConversationDebugState.hx
@@ -1,10 +1,19 @@
-package funkin.play.cutscene.dialogue;
+package funkin.ui.debug.dialogue;
import flixel.FlxState;
import funkin.modding.events.ScriptEventDispatcher;
import funkin.modding.events.ScriptEvent;
import flixel.util.FlxColor;
import funkin.ui.MusicBeatState;
+import funkin.data.dialogue.ConversationData;
+import funkin.data.dialogue.ConversationRegistry;
+import funkin.data.dialogue.DialogueBoxData;
+import funkin.data.dialogue.DialogueBoxRegistry;
+import funkin.data.dialogue.SpeakerData;
+import funkin.data.dialogue.SpeakerRegistry;
+import funkin.play.cutscene.dialogue.Conversation;
+import funkin.play.cutscene.dialogue.DialogueBox;
+import funkin.play.cutscene.dialogue.Speaker;
/**
* A state with displays a conversation with no background.
@@ -27,7 +36,7 @@ class ConversationDebugState extends MusicBeatState
public override function create():Void
{
- conversation = ConversationDataParser.fetchConversation(conversationId);
+ conversation = ConversationRegistry.instance.fetchEntry(conversationId);
conversation.completeCallback = onConversationComplete;
add(conversation);
@@ -40,6 +49,12 @@ class ConversationDebugState extends MusicBeatState
conversation = null;
}
+ public override function dispatchEvent(event:ScriptEvent):Void
+ {
+ // Dispatch event to conversation script.
+ ScriptEventDispatcher.callEvent(conversation, event);
+ }
+
public override function update(elapsed:Float):Void
{
super.update(elapsed);
diff --git a/source/funkin/ui/freeplay/FreeplayState.hx b/source/funkin/ui/freeplay/FreeplayState.hx
index b5b43c5ca..9cbab2cb5 100644
--- a/source/funkin/ui/freeplay/FreeplayState.hx
+++ b/source/funkin/ui/freeplay/FreeplayState.hx
@@ -185,9 +185,9 @@ class FreeplayState extends MusicBeatSubState
isDebug = true;
#end
- if (FlxG.sound.music != null)
+ if (FlxG.sound.music == null || (FlxG.sound.music != null && !FlxG.sound.music.playing))
{
- if (!FlxG.sound.music.playing) FlxG.sound.playMusic(Paths.music('freakyMenu/freakyMenu'));
+ FlxG.sound.playMusic(Paths.music('freakyMenu/freakyMenu'));
}
// Add a null entry that represents the RANDOM option
diff --git a/source/funkin/util/assets/DataAssets.hx b/source/funkin/util/assets/DataAssets.hx
index ee75dd207..b38c993fe 100644
--- a/source/funkin/util/assets/DataAssets.hx
+++ b/source/funkin/util/assets/DataAssets.hx
@@ -9,7 +9,8 @@ class DataAssets
public static function listDataFilesInPath(path:String, ?suffix:String = '.json'):Array
{
- var textAssets = openfl.utils.Assets.list();
+ var textAssets = openfl.utils.Assets.list(TEXT);
+
var queryPath = buildDataPath(path);
var results:Array = [];
diff --git a/source/funkin/util/logging/AnsiTrace.hx b/source/funkin/util/logging/AnsiTrace.hx
new file mode 100644
index 000000000..c8d27b86f
--- /dev/null
+++ b/source/funkin/util/logging/AnsiTrace.hx
@@ -0,0 +1,120 @@
+package funkin.util.logging;
+
+class AnsiTrace
+{
+ // mostly a copy of haxe.Log.trace()
+ // but adds nice cute ANSI things
+ public static function trace(v:Dynamic, ?info:haxe.PosInfos)
+ {
+ var str = formatOutput(v, info);
+ #if js
+ if (js.Syntax.typeof(untyped console) != "undefined" && (untyped console).log != null) (untyped console).log(str);
+ #elseif lua
+ untyped __define_feature__("use._hx_print", _hx_print(str));
+ #elseif sys
+ Sys.println(str);
+ #else
+ throw new haxe.exceptions.NotImplementedException()
+ #end
+ }
+
+ public static var colorSupported:Bool = #if sys (Sys.getEnv("TERM") == "xterm" || Sys.getEnv("ANSICON") != null) #else false #end;
+
+ // ansi stuff
+ public static inline var RED = "\x1b[31m";
+ public static inline var YELLOW = "\x1b[33m";
+ public static inline var WHITE = "\x1b[37m";
+ public static inline var NORMAL = "\x1b[0m";
+ public static inline var BOLD = "\x1b[1m";
+ public static inline var ITALIC = "\x1b[3m";
+
+ // where the real mf magic happens with ansi stuff!
+ public static function formatOutput(v:Dynamic, infos:haxe.PosInfos):String
+ {
+ var str = Std.string(v);
+ if (infos == null) return str;
+
+ if (colorSupported)
+ {
+ var dirs:Array = infos.fileName.split("/");
+ dirs[dirs.length - 1] = ansiWrap(dirs[dirs.length - 1], BOLD);
+
+ // rejoin the dirs
+ infos.fileName = dirs.join("/");
+ }
+
+ var pstr = infos.fileName + ":" + ansiWrap(infos.lineNumber, BOLD);
+ if (infos.customParams != null) for (v in infos.customParams)
+ str += ", " + Std.string(v);
+ return pstr + ": " + str;
+ }
+
+ public static function traceBF()
+ {
+ #if sys
+ if (colorSupported) Sys.println(ansiBF.join("\n"));
+ #end
+ }
+
+ public static function ansiWrap(str:Dynamic, ansiCol:String)
+ {
+ return ansify(ansiCol) + str + ansify(NORMAL);
+ }
+
+ public static function ansify(ansiCol:String)
+ {
+ return (colorSupported ? ansiCol : "");
+ }
+
+ // generated using https://dom111.github.io/image-to-ansi/
+ public static var ansiBF:Array = [
+ "\x1b[39m\x1b[49m \x1b[48;2;154;23;70m \x1b[49m \x1b[m",
+ "\x1b[39m\x1b[49m \x1b[48;2;154;23;70m \x1b[48;2;184;46;83m \x1b[48;2;246;87;102m \x1b[48;2;239;83;100m \x1b[48;2;154;23;70m \x1b[48;2;154;23;69m \x1b[49m \x1b[m",
+ "\x1b[39m\x1b[49m \x1b[48;2;154;23;70m \x1b[48;2;191;52;87m \x1b[48;2;246;87;102m \x1b[48;2;241;84;100m \x1b[48;2;191;52;87m \x1b[48;2;153;23;69m \x1b[49m \x1b[m",
+ "\x1b[39m\x1b[49m \x1b[48;2;154;23;70m \x1b[48;2;246;87;102m \x1b[48;2;154;23;70m \x1b[49m \x1b[m",
+ "\x1b[39m\x1b[49m \x1b[48;2;154;23;70m \x1b[48;2;246;87;102m \x1b[48;2;234;94;114m \x1b[48;2;160;97;151m \x1b[48;2;246;87;102m \x1b[48;2;154;23;70m \x1b[48;2;205;63;93m \x1b[48;2;36;35;46m \x1b[49m \x1b[m",
+ "\x1b[39m\x1b[49m \x1b[48;2;47;49;144m \x1b[48;2;246;87;102m \x1b[48;2;80;121;206m \x1b[48;2;193;167;177m \x1b[48;2;246;87;102m \x1b[48;2;184;46;83m \x1b[48;2;205;63;93m \x1b[48;2;20;19;31m \x1b[49m \x1b[m",
+ "\x1b[39m\x1b[49m \x1b[48;2;47;49;144m \x1b[48;2;110;187;236m \x1b[48;2;109;66;125m \x1b[48;2;246;87;102m \x1b[48;2;74;107;200m \x1b[48;2;141;248;252m \x1b[48;2;107;177;226m \x1b[48;2;234;94;114m \x1b[48;2;246;87;102m \x1b[48;2;237;81;99m \x1b[48;2;205;63;93m \x1b[48;2;20;19;31m \x1b[49m \x1b[m",
+ "\x1b[39m\x1b[49m \x1b[48;2;74;106;196m \x1b[48;2;87;133;210m \x1b[48;2;64;105;174m \x1b[48;2;141;248;252m \x1b[48;2;126;219;244m \x1b[48;2;57;65;148m \x1b[48;2;47;49;144m \x1b[48;2;141;248;252m \x1b[48;2;129;225;245m \x1b[48;2;157;94;147m \x1b[48;2;246;87;102m \x1b[48;2;159;27;72m \x1b[48;2;205;63;93m \x1b[48;2;55;27;43m \x1b[48;2;21;21;31m \x1b[49m \x1b[m",
+ "\x1b[39m\x1b[49m \x1b[48;2;74;107;200m \x1b[48;2;125;216;244m \x1b[48;2;141;248;252m \x1b[48;2;126;219;244m \x1b[48;2;60;97;187m \x1b[48;2;141;248;252m \x1b[48;2;126;219;244m \x1b[48;2;104;173;229m \x1b[48;2;146;68;123m \x1b[48;2;246;87;102m \x1b[48;2;180;44;82m \x1b[48;2;205;63;93m \x1b[48;2;20;19;31m \x1b[49m \x1b[m",
+ "\x1b[39m\x1b[49m \x1b[48;2;74;107;200m \x1b[48;2;141;248;252m \x1b[48;2;110;187;236m \x1b[48;2;141;248;252m \x1b[48;2;110;187;236m \x1b[48;2;104;173;229m \x1b[48;2;146;68;123m \x1b[48;2;246;87;102m \x1b[48;2;154;23;70m \x1b[48;2;205;63;93m \x1b[48;2;20;19;31m \x1b[49m \x1b[m",
+ "\x1b[39m\x1b[49m \x1b[48;2;73;106;199m \x1b[48;2;132;230;247m \x1b[48;2;141;248;252m \x1b[48;2;110;187;236m \x1b[48;2;141;248;252m \x1b[48;2;110;187;236m \x1b[48;2;78;118;190m \x1b[48;2;239;83;100m \x1b[48;2;246;87;102m \x1b[48;2;205;63;93m \x1b[48;2;20;19;31m \x1b[49m \x1b[m",
+ "\x1b[39m\x1b[49m \x1b[48;2;73;106;199m \x1b[48;2;132;230;247m \x1b[48;2;141;248;252m \x1b[48;2;110;187;236m \x1b[48;2;20;19;31m \x1b[48;2;110;187;236m \x1b[48;2;141;248;252m \x1b[48;2;110;187;236m \x1b[48;2;78;118;190m \x1b[48;2;239;83;100m \x1b[48;2;246;87;102m \x1b[48;2;154;23;70m \x1b[48;2;205;63;93m \x1b[48;2;20;19;31m \x1b[49m \x1b[m",
+ "\x1b[39m\x1b[49m \x1b[48;2;73;106;199m \x1b[48;2;132;230;247m \x1b[48;2;110;187;236m \x1b[48;2;20;19;31m \x1b[48;2;110;187;236m \x1b[48;2;51;72;160m \x1b[48;2;246;87;102m \x1b[48;2;154;23;70m \x1b[48;2;205;63;93m \x1b[48;2;20;19;31m \x1b[49m \x1b[m",
+ "\x1b[39m\x1b[49m \x1b[48;2;74;107;200m \x1b[48;2;141;248;252m \x1b[48;2;110;187;236m \x1b[48;2;117;138;166m \x1b[48;2;20;19;31m \x1b[48;2;110;187;236m \x1b[48;2;55;134;228m \x1b[48;2;110;187;236m \x1b[48;2;139;244;251m \x1b[48;2;205;63;93m \x1b[48;2;123;4;53m \x1b[48;2;125;6;54m \x1b[48;2;146;23;68m \x1b[48;2;205;63;93m \x1b[48;2;123;4;53m \x1b[48;2;20;19;31m \x1b[49m \x1b[m",
+ "\x1b[39m\x1b[49m \x1b[48;2;74;107;200m \x1b[48;2;141;248;252m \x1b[48;2;110;187;236m \x1b[48;2;20;19;31m \x1b[48;2;103;130;185m \x1b[48;2;240;174;162m \x1b[48;2;74;107;200m \x1b[48;2;110;187;236m \x1b[48;2;141;248;252m \x1b[48;2;107;177;226m \x1b[48;2;74;107;200m \x1b[48;2;20;19;31m \x1b[48;2;205;63;93m \x1b[48;2;20;19;31m \x1b[48;2;153;78;112m \x1b[49m \x1b[m",
+ "\x1b[39m\x1b[49m \x1b[48;2;74;107;200m \x1b[48;2;110;187;236m \x1b[48;2;141;248;252m \x1b[48;2;110;187;236m \x1b[48;2;58;123;219m \x1b[48;2;74;107;200m \x1b[48;2;20;19;31m \x1b[48;2;240;174;162m \x1b[48;2;141;248;252m \x1b[48;2;135;237;249m \x1b[48;2;157;140;181m \x1b[48;2;44;30;46m \x1b[48;2;20;19;31m \x1b[48;2;205;63;93m \x1b[48;2;36;35;46m \x1b[48;2;153;78;112m \x1b[48;2;249;225;202m \x1b[48;2;240;174;162m \x1b[48;2;153;78;112m \x1b[49m \x1b[m",
+ "\x1b[39m\x1b[49m \x1b[48;2;74;107;200m \x1b[48;2;141;248;252m \x1b[48;2;110;187;236m \x1b[48;2;74;107;200m \x1b[48;2;20;19;31m \x1b[48;2;240;174;162m \x1b[48;2;93;37;66m \x1b[48;2;240;174;162m \x1b[48;2;74;107;200m \x1b[48;2;240;174;162m \x1b[48;2;130;96;96m \x1b[48;2;20;19;31m \x1b[48;2;240;174;162m \x1b[48;2;74;107;200m \x1b[48;2;141;248;252m \x1b[48;2;110;187;236m \x1b[48;2;20;19;31m \x1b[48;2;205;63;93m \x1b[48;2;170;35;77m \x1b[48;2;196;126;137m \x1b[48;2;249;225;202m \x1b[48;2;132;70;100m \x1b[48;2;20;19;31m \x1b[49m \x1b[m",
+ "\x1b[39m\x1b[49m \x1b[48;2;73;106;199m \x1b[48;2;132;230;247m \x1b[48;2;138;242;250m \x1b[48;2;74;107;200m \x1b[49m \x1b[48;2;20;19;31m \x1b[48;2;240;174;162m \x1b[48;2;20;19;31m \x1b[48;2;175;111;124m \x1b[48;2;235;169;159m \x1b[48;2;240;174;162m \x1b[48;2;227;160;155m \x1b[48;2;20;19;31m \x1b[48;2;232;165;158m \x1b[48;2;240;174;162m \x1b[48;2;85;109;196m \x1b[48;2;138;242;250m \x1b[48;2;112;191;237m \x1b[48;2;104;181;235m \x1b[48;2;110;187;236m \x1b[48;2;23;22;43m \x1b[48;2;26;23;37m \x1b[48;2;249;225;202m \x1b[48;2;248;220;198m \x1b[48;2;249;225;202m \x1b[48;2;137;90;124m \x1b[48;2;51;112;205m \x1b[48;2;53;128;224m \x1b[48;2;23;25;44m \x1b[48;2;18;18;28m \x1b[49m \x1b[m",
+ "\x1b[39m\x1b[49m \x1b[48;2;73;106;199m \x1b[48;2;109;182;229m \x1b[48;2;74;107;200m \x1b[49m \x1b[48;2;151;78;111m \x1b[48;2;194;126;136m \x1b[48;2;110;100;98m \x1b[48;2;244;194;178m \x1b[48;2;72;42;63m \x1b[48;2;103;76;81m \x1b[48;2;191;136;147m \x1b[48;2;240;174;162m \x1b[48;2;206;136;142m \x1b[48;2;20;19;31m \x1b[48;2;232;165;158m \x1b[48;2;240;174;162m \x1b[48;2;65;128;218m \x1b[48;2;141;248;252m \x1b[48;2;74;107;200m \x1b[48;2;85;133;200m \x1b[48;2;88;139;214m \x1b[48;2;84;69;150m \x1b[48;2;211;167;166m \x1b[48;2;187;116;132m \x1b[48;2;213;145;147m \x1b[48;2;245;205;186m \x1b[48;2;135;83;115m \x1b[48;2;43;66;124m \x1b[48;2;47;87;174m \x1b[48;2;51;130;227m \x1b[48;2;28;40;76m \x1b[49m \x1b[m",
+ "\x1b[39m\x1b[49m \x1b[48;2;71;103;199m \x1b[48;2;73;105;197m \x1b[49m \x1b[48;2;153;78;112m \x1b[48;2;196;126;137m \x1b[48;2;246;209;189m \x1b[48;2;249;225;202m \x1b[48;2;226;159;154m \x1b[48;2;244;215;196m \x1b[48;2;249;225;202m \x1b[48;2;240;174;162m \x1b[48;2;226;159;154m \x1b[48;2;142;54;93m \x1b[48;2;245;213;193m \x1b[48;2;249;225;202m \x1b[48;2;213;185;192m \x1b[48;2;85;132;211m \x1b[48;2;222;158;164m \x1b[48;2;183;111;129m \x1b[48;2;110;187;236m \x1b[48;2;171;158;211m \x1b[48;2;153;78;112m \x1b[48;2;196;126;137m \x1b[48;2;240;174;162m \x1b[48;2;166;93;120m \x1b[48;2;130;70;98m \x1b[48;2;19;19;31m \x1b[48;2;29;34;56m \x1b[48;2;55;93;183m \x1b[48;2;68;101;193m \x1b[49m \x1b[m",
+ "\x1b[39m\x1b[49m \x1b[48;2;153;78;112m \x1b[48;2;249;225;202m \x1b[48;2;196;126;137m \x1b[48;2;218;150;149m \x1b[48;2;249;225;202m \x1b[48;2;240;174;162m \x1b[48;2;196;126;137m \x1b[48;2;47;49;144m \x1b[48;2;196;126;137m \x1b[48;2;153;78;112m \x1b[49m \x1b[48;2;20;19;31m \x1b[48;2;41;43;121m \x1b[49m \x1b[m",
+ "\x1b[39m\x1b[49m \x1b[48;2;153;78;112m \x1b[48;2;249;225;202m \x1b[48;2;244;215;196m \x1b[48;2;249;225;202m \x1b[48;2;145;49;90m \x1b[48;2;249;225;202m \x1b[48;2;240;174;162m \x1b[48;2;153;78;112m \x1b[49m \x1b[m",
+ "\x1b[39m\x1b[49m \x1b[48;2;153;78;112m \x1b[48;2;196;126;137m \x1b[48;2;249;225;202m \x1b[48;2;200;131;139m \x1b[48;2;249;225;202m \x1b[48;2;154;63;98m \x1b[48;2;145;49;90m \x1b[48;2;240;174;162m \x1b[48;2;249;225;202m \x1b[48;2;240;174;162m \x1b[48;2;153;78;112m \x1b[48;2;255;224;255m \x1b[48;2;153;78;112m \x1b[49m \x1b[m",
+ "\x1b[39m\x1b[49m \x1b[48;2;153;78;112m \x1b[48;2;210;141;145m \x1b[48;2;241;181;168m \x1b[48;2;249;225;202m \x1b[48;2;196;126;137m \x1b[48;2;195;125;136m \x1b[48;2;170;94;119m \x1b[48;2;237;73;115m \x1b[48;2;244;75;120m \x1b[48;2;145;49;90m \x1b[48;2;249;225;202m \x1b[48;2;241;181;167m \x1b[48;2;181;121;161m \x1b[48;2;255;224;255m \x1b[48;2;178;117;159m \x1b[49m \x1b[m",
+ "\x1b[39m\x1b[49m \x1b[48;2;136;72;102m \x1b[48;2;190;119;133m \x1b[48;2;171;99;123m \x1b[48;2;152;74;109m \x1b[48;2;244;75;120m \x1b[48;2;145;49;90m \x1b[48;2;190;119;133m \x1b[48;2;185;128;172m \x1b[48;2;180;121;164m \x1b[48;2;255;224;255m \x1b[48;2;153;78;112m \x1b[49m \x1b[m",
+ "\x1b[39m\x1b[49m \x1b[48;2;147;80;107m \x1b[48;2;153;78;112m \x1b[49m \x1b[48;2;36;35;46m \x1b[48;2;98;121;155m \x1b[48;2;50;68;111m \x1b[48;2;55;73;115m \x1b[48;2;36;35;46m \x1b[48;2;251;117;129m \x1b[48;2;205;63;93m \x1b[48;2;230;143;174m \x1b[48;2;255;224;255m \x1b[48;2;145;49;90m \x1b[48;2;153;78;112m \x1b[48;2;255;224;255m \x1b[48;2;251;219;252m \x1b[48;2;105;60;85m \x1b[48;2;36;35;46m \x1b[48;2;212;170;223m \x1b[48;2;255;224;255m \x1b[48;2;153;78;112m \x1b[49m \x1b[m",
+ "\x1b[39m\x1b[49m \x1b[48;2;153;78;112m \x1b[48;2;156;82;114m \x1b[48;2;240;174;162m \x1b[48;2;153;78;112m \x1b[49m \x1b[48;2;65;84;125m \x1b[48;2;98;121;155m \x1b[48;2;124;146;175m \x1b[48;2;194;215;238m \x1b[48;2;50;68;111m \x1b[48;2;124;146;175m \x1b[48;2;98;121;155m \x1b[48;2;36;35;46m \x1b[48;2;254;224;245m \x1b[48;2;231;143;176m \x1b[48;2;255;224;255m \x1b[48;2;145;49;90m \x1b[48;2;255;224;255m \x1b[48;2;72;85;110m \x1b[48;2;240;174;162m \x1b[48;2;196;126;137m \x1b[48;2;153;78;112m \x1b[49m \x1b[m",
+ "\x1b[39m\x1b[49m \x1b[48;2;153;78;112m \x1b[48;2;196;126;137m \x1b[48;2;250;227;206m \x1b[48;2;50;68;111m \x1b[48;2;65;84;125m \x1b[48;2;50;68;111m \x1b[48;2;124;146;175m \x1b[48;2;78;99;137m \x1b[48;2;194;215;238m \x1b[48;2;124;146;175m \x1b[48;2;36;35;46m \x1b[48;2;254;224;245m \x1b[48;2;253;170;192m \x1b[48;2;255;224;255m \x1b[48;2;251;117;129m \x1b[48;2;255;224;255m \x1b[48;2;170;105;144m \x1b[48;2;240;174;162m \x1b[48;2;196;126;137m \x1b[49m \x1b[m",
+ "\x1b[39m\x1b[49m \x1b[48;2;153;76;111m \x1b[48;2;165;91;118m \x1b[48;2;180;108;128m \x1b[48;2;250;227;206m \x1b[48;2;116;138;169m \x1b[48;2;124;146;175m \x1b[48;2;36;35;46m \x1b[48;2;116;138;169m \x1b[48;2;172;193;218m \x1b[48;2;168;206;237m \x1b[48;2;49;62;121m \x1b[48;2;73;92;131m \x1b[48;2;115;137;168m \x1b[48;2;116;138;169m \x1b[48;2;124;146;175m \x1b[48;2;50;40;54m \x1b[48;2;57;43;58m \x1b[48;2;251;170;183m \x1b[48;2;255;206;227m \x1b[48;2;251;117;129m \x1b[48;2;252;132;140m \x1b[48;2;255;224;255m \x1b[48;2;153;78;112m \x1b[48;2;243;190;174m \x1b[48;2;249;226;203m \x1b[48;2;196;126;137m \x1b[48;2;44;70;156m \x1b[48;2;47;86;175m \x1b[49m \x1b[m",
+ "\x1b[39m\x1b[49m \x1b[48;2;153;78;112m \x1b[48;2;239;198;185m \x1b[48;2;250;227;206m \x1b[48;2;244;195;179m \x1b[48;2;250;227;206m \x1b[48;2;50;68;111m \x1b[48;2;166;188;213m \x1b[48;2;36;35;46m \x1b[48;2;46;58;91m \x1b[48;2;79;100;138m \x1b[48;2;71;79;97m \x1b[48;2;60;69;89m \x1b[48;2;136;158;188m \x1b[48;2;117;140;170m \x1b[48;2;36;35;46m \x1b[48;2;71;79;97m \x1b[48;2;79;100;138m \x1b[48;2;62;51;68m \x1b[48;2;246;192;224m \x1b[48;2;253;182;205m \x1b[48;2;255;224;255m \x1b[48;2;144;140;167m \x1b[48;2;82;52;72m \x1b[48;2;120;110;108m \x1b[48;2;250;227;206m \x1b[48;2;229;187;179m \x1b[48;2;169;166;186m \x1b[48;2;47;64;156m \x1b[48;2;46;87;175m \x1b[49m \x1b[m",
+ "\x1b[39m\x1b[49m \x1b[48;2;153;78;112m \x1b[48;2;250;227;206m \x1b[48;2;240;174;162m \x1b[48;2;220;168;164m \x1b[48;2;250;227;206m \x1b[48;2;50;68;111m \x1b[48;2;194;215;238m \x1b[48;2;36;35;46m \x1b[48;2;50;68;111m \x1b[48;2;98;121;155m \x1b[48;2;36;35;46m \x1b[48;2;50;68;111m \x1b[48;2;98;121;155m \x1b[48;2;95;115;147m \x1b[48;2;93;115;150m \x1b[48;2;50;68;111m \x1b[48;2;37;37;48m \x1b[48;2;51;44;59m \x1b[48;2;66;116;206m \x1b[48;2;47;83;172m \x1b[48;2;68;134;227m \x1b[48;2;250;227;206m \x1b[48;2;61;91;174m \x1b[48;2;47;85;173m \x1b[48;2;47;87;175m \x1b[48;2;47;86;175m \x1b[49m \x1b[m",
+ "\x1b[48;2;153;78;112m \x1b[48;2;250;227;206m \x1b[48;2;240;174;162m \x1b[48;2;217;165;163m \x1b[48;2;250;227;206m \x1b[48;2;240;174;162m \x1b[48;2;82;52;72m \x1b[48;2;194;215;238m \x1b[48;2;36;35;46m \x1b[48;2;50;68;111m \x1b[48;2;98;121;155m \x1b[48;2;36;35;46m \x1b[48;2;98;121;155m \x1b[48;2;50;68;111m \x1b[48;2;38;41;58m \x1b[48;2;47;87;175m \x1b[48;2;51;130;227m \x1b[48;2;47;87;175m \x1b[48;2;51;130;227m \x1b[48;2;47;49;144m \x1b[48;2;47;87;175m \x1b[48;2;45;85;174m \x1b[49m \x1b[m",
+ "\x1b[48;2;153;78;112m \x1b[48;2;250;227;206m \x1b[48;2;242;185;171m \x1b[48;2;250;227;206m \x1b[48;2;240;174;162m \x1b[48;2;217;165;163m \x1b[48;2;250;227;206m \x1b[48;2;240;174;162m \x1b[48;2;36;35;46m \x1b[48;2;124;146;175m \x1b[48;2;36;35;46m \x1b[48;2;50;68;111m \x1b[48;2;98;121;155m \x1b[48;2;50;68;111m \x1b[48;2;36;35;46m \x1b[48;2;98;121;155m \x1b[48;2;36;35;46m \x1b[48;2;38;41;58m \x1b[48;2;47;68;159m \x1b[48;2;47;87;175m \x1b[48;2;51;130;227m \x1b[48;2;47;87;175m \x1b[48;2;51;130;227m \x1b[48;2;47;87;175m \x1b[48;2;45;85;174m \x1b[49m \x1b[m",
+ "\x1b[48;2;153;78;112m \x1b[48;2;250;227;206m \x1b[48;2;242;185;171m \x1b[48;2;196;126;137m \x1b[48;2;250;227;206m \x1b[48;2;240;174;162m \x1b[48;2;206;136;142m \x1b[48;2;227;160;155m \x1b[48;2;240;174;162m \x1b[48;2;82;52;72m \x1b[48;2;50;68;111m \x1b[48;2;36;35;46m \x1b[48;2;50;68;111m \x1b[48;2;36;35;46m \x1b[48;2;47;87;175m \x1b[48;2;51;130;227m \x1b[48;2;153;55;95m \x1b[48;2;47;87;175m \x1b[48;2;51;130;227m \x1b[48;2;47;87;175m \x1b[48;2;45;85;174m \x1b[49m \x1b[m",
+ "\x1b[39m\x1b[49m \x1b[48;2;152;77;112m \x1b[48;2;228;161;155m \x1b[48;2;237;171;160m \x1b[48;2;196;126;137m \x1b[48;2;193;123;135m \x1b[48;2;177;105;126m \x1b[48;2;193;123;135m \x1b[48;2;37;37;50m \x1b[48;2;155;69;105m \x1b[48;2;36;35;46m \x1b[48;2;49;66;107m \x1b[48;2;36;35;46m \x1b[48;2;159;83;115m \x1b[48;2;155;67;103m \x1b[48;2;47;69;142m \x1b[48;2;47;87;175m \x1b[48;2;46;73;157m \x1b[48;2;47;85;173m \x1b[48;2;63;77;159m \x1b[48;2;247;97;126m \x1b[48;2;254;165;165m \x1b[48;2;253;160;161m \x1b[48;2;159;79;111m \x1b[48;2;72;106;198m \x1b[48;2;50;67;149m \x1b[48;2;53;69;151m \x1b[48;2;157;77;111m \x1b[48;2;151;22;68m \x1b[49m \x1b[m",
+ "\x1b[39m\x1b[49m \x1b[48;2;153;78;112m \x1b[48;2;147;80;107m \x1b[48;2;176;104;125m \x1b[48;2;240;174;162m \x1b[48;2;66;46;63m \x1b[48;2;36;35;46m \x1b[48;2;170;35;77m \x1b[48;2;212;170;223m \x1b[48;2;255;224;255m \x1b[48;2;201;153;202m \x1b[48;2;246;87;102m \x1b[48;2;245;171;163m \x1b[48;2;253;156;159m \x1b[48;2;205;63;93m \x1b[48;2;154;23;70m \x1b[48;2;47;87;175m \x1b[48;2;154;23;70m \x1b[48;2;246;170;163m \x1b[48;2;254;165;165m \x1b[48;2;246;87;102m \x1b[48;2;245;79;114m \x1b[48;2;118;0;50m \x1b[48;2;246;87;102m \x1b[48;2;219;68;93m \x1b[48;2;118;0;50m \x1b[49m \x1b[m",
+ "\x1b[39m\x1b[49m \x1b[48;2;147;80;107m \x1b[48;2;187;116;132m \x1b[48;2;240;174;162m \x1b[48;2;153;78;112m \x1b[48;2;212;170;223m \x1b[48;2;255;224;255m \x1b[48;2;212;170;223m \x1b[48;2;182;124;167m \x1b[48;2;255;224;255m \x1b[48;2;251;127;141m \x1b[48;2;246;87;102m \x1b[48;2;154;23;70m \x1b[48;2;205;63;93m \x1b[48;2;246;87;102m \x1b[48;2;225;75;97m \x1b[48;2;154;23;70m \x1b[48;2;159;27;72m \x1b[48;2;246;87;102m \x1b[48;2;154;23;70m \x1b[48;2;246;87;102m \x1b[48;2;118;0;50m \x1b[49m \x1b[m",
+ "\x1b[39m\x1b[49m \x1b[48;2;151;78;111m \x1b[48;2;153;78;112m \x1b[48;2;255;224;255m \x1b[48;2;212;170;223m \x1b[48;2;255;224;255m \x1b[48;2;212;170;223m \x1b[48;2;200;151;200m \x1b[48;2;216;175;226m \x1b[48;2;154;23;70m \x1b[48;2;205;63;93m \x1b[48;2;246;85;105m \x1b[48;2;205;63;93m \x1b[48;2;118;0;50m \x1b[48;2;246;87;102m \x1b[48;2;159;27;72m \x1b[48;2;205;63;93m \x1b[48;2;246;87;102m \x1b[48;2;212;170;223m \x1b[48;2;255;224;255m \x1b[48;2;212;170;223m \x1b[48;2;144;16;64m \x1b[48;2;118;0;50m \x1b[49m \x1b[m",
+ "\x1b[39m\x1b[49m \x1b[48;2;154;23;70m \x1b[48;2;246;87;102m \x1b[48;2;182;124;167m \x1b[48;2;255;224;255m \x1b[48;2;153;78;112m \x1b[48;2;182;124;167m \x1b[48;2;255;224;255m \x1b[48;2;205;63;93m \x1b[48;2;159;27;72m \x1b[48;2;154;23;70m \x1b[48;2;205;63;93m \x1b[48;2;118;0;50m \x1b[48;2;246;87;102m \x1b[48;2;205;63;93m \x1b[48;2;212;170;223m \x1b[48;2;255;224;255m \x1b[48;2;212;170;223m \x1b[48;2;182;124;167m \x1b[48;2;168;74;106m \x1b[48;2;174;39;79m \x1b[48;2;153;78;112m \x1b[49m \x1b[m",
+ "\x1b[39m\x1b[49m \x1b[48;2;154;23;70m \x1b[48;2;246;87;102m \x1b[48;2;211;70;106m \x1b[48;2;233;196;238m \x1b[48;2;255;224;255m \x1b[48;2;178;42;81m \x1b[48;2;205;157;200m \x1b[48;2;154;23;70m \x1b[48;2;205;63;93m \x1b[48;2;178;42;81m \x1b[48;2;154;23;70m \x1b[48;2;153;52;92m \x1b[48;2;136;40;82m \x1b[48;2;144;17;63m \x1b[48;2;225;75;97m \x1b[48;2;234;198;240m \x1b[48;2;255;224;255m \x1b[48;2;234;198;240m \x1b[48;2;233;196;238m \x1b[48;2;239;204;243m \x1b[48;2;212;165;204m \x1b[48;2;245;87;103m \x1b[48;2;180;44;82m \x1b[48;2;180;70;102m \x1b[48;2;152;77;112m \x1b[48;2;151;76;110m \x1b[49m \x1b[m",
+ "\x1b[39m\x1b[49m \x1b[48;2;153;68;104m \x1b[48;2;213;67;95m \x1b[48;2;246;87;102m \x1b[48;2;213;67;95m \x1b[48;2;195;66;97m \x1b[48;2;173;61;105m \x1b[48;2;154;23;70m \x1b[48;2;195;55;89m \x1b[48;2;205;63;93m \x1b[48;2;125;4;54m \x1b[48;2;170;101;145m \x1b[48;2;171;102;146m \x1b[48;2;201;153;202m \x1b[48;2;182;124;167m \x1b[48;2;158;87;122m \x1b[48;2;138;36;79m \x1b[48;2;205;63;93m \x1b[48;2;196;143;183m \x1b[48;2;255;224;255m \x1b[48;2;234;94;114m \x1b[48;2;229;85;104m \x1b[48;2;236;96;117m \x1b[48;2;248;113;131m \x1b[48;2;246;88;103m \x1b[48;2;246;87;102m \x1b[48;2;195;66;97m \x1b[48;2;172;109;148m \x1b[49m \x1b[m",
+ "\x1b[39m\x1b[49m \x1b[48;2;153;78;112m \x1b[48;2;205;63;93m \x1b[48;2;246;87;102m \x1b[48;2;205;63;93m \x1b[48;2;154;23;70m \x1b[48;2;182;124;167m \x1b[48;2;197;147;195m \x1b[48;2;212;170;223m \x1b[48;2;182;124;167m \x1b[48;2;212;170;223m \x1b[48;2;153;78;112m \x1b[48;2;142;44;86m \x1b[48;2;154;23;70m \x1b[48;2;205;63;93m \x1b[48;2;255;224;255m \x1b[48;2;225;187;233m \x1b[48;2;246;87;102m \x1b[48;2;205;63;93m \x1b[48;2;246;87;102m \x1b[48;2;205;63;93m \x1b[48;2;154;23;70m \x1b[48;2;200;151;200m \x1b[49m \x1b[m",
+ "\x1b[39m\x1b[49m \x1b[48;2;153;78;112m \x1b[48;2;255;224;255m \x1b[48;2;212;170;223m \x1b[48;2;205;63;93m \x1b[48;2;154;23;70m \x1b[48;2;182;124;167m \x1b[48;2;147;64;101m \x1b[48;2;36;35;46m \x1b[48;2;153;78;112m \x1b[49m \x1b[48;2;66;78;122m \x1b[48;2;200;100;119m \x1b[48;2;205;63;93m \x1b[48;2;246;87;102m \x1b[48;2;205;63;93m \x1b[48;2;154;23;70m \x1b[48;2;200;151;200m \x1b[49m \x1b[m",
+ "\x1b[39m\x1b[49m \x1b[48;2;153;78;112m \x1b[48;2;255;224;255m \x1b[48;2;212;170;223m \x1b[48;2;182;124;167m \x1b[48;2;36;35;46m \x1b[49m \x1b[48;2;66;78;122m \x1b[48;2;153;55;95m \x1b[48;2;205;63;93m \x1b[48;2;246;87;102m \x1b[48;2;205;63;93m \x1b[48;2;154;23;70m \x1b[48;2;255;224;255m \x1b[48;2;176;115;156m \x1b[49m \x1b[m",
+ "\x1b[39m\x1b[49m \x1b[48;2;153;78;112m \x1b[48;2;255;224;255m \x1b[48;2;212;170;223m \x1b[48;2;255;224;255m \x1b[48;2;185;128;172m \x1b[48;2;153;78;112m \x1b[48;2;36;35;46m \x1b[49m \x1b[48;2;66;78;122m \x1b[48;2;153;55;95m \x1b[48;2;154;23;70m \x1b[48;2;246;87;102m \x1b[48;2;205;63;93m \x1b[48;2;154;23;70m \x1b[48;2;255;224;255m \x1b[48;2;212;170;223m \x1b[48;2;176;115;156m \x1b[49m \x1b[m",
+ "\x1b[39m\x1b[49m \x1b[48;2;153;78;112m \x1b[49m \x1b[48;2;87;54;75m \x1b[48;2;207;163;214m \x1b[48;2;190;136;182m \x1b[48;2;202;151;191m \x1b[48;2;244;187;187m \x1b[48;2;241;206;245m \x1b[48;2;243;209;246m \x1b[48;2;212;170;223m \x1b[48;2;153;78;112m \x1b[49m \x1b[m",
+ "\x1b[39m\x1b[49m \x1b[48;2;56;42;57m \x1b[48;2;212;170;223m \x1b[48;2;255;224;255m \x1b[48;2;212;170;223m \x1b[48;2;255;224;255m \x1b[48;2;243;208;246m \x1b[48;2;238;203;242m \x1b[48;2;255;224;255m \x1b[48;2;212;170;223m \x1b[48;2;153;78;112m \x1b[48;2;153;77;111m \x1b[49m \x1b[m",
+ "\x1b[39m\x1b[49m \x1b[48;2;153;78;112m \x1b[49m \x1b[m"
+ ];
+}
diff --git a/tests/unit/project.xml b/tests/unit/project.xml
index 2e505e015..dfbf06502 100644
--- a/tests/unit/project.xml
+++ b/tests/unit/project.xml
@@ -27,7 +27,6 @@
-