From fa556dc1f24a955be93c60bec985433e06fc8200 Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Wed, 7 Feb 2024 09:21:44 -0500 Subject: [PATCH] Rewrite conversation JSON parsing code. --- .vscode/settings.json | 5 + Project.xml | 1 - hmm.json | 5 - source/funkin/InitState.hx | 24 +- source/funkin/data/DataParse.hx | 62 +++++ source/funkin/modding/PolymodHandler.hx | 8 +- source/funkin/play/PlayState.hx | 4 +- .../play/cutscene/dialogue/Conversation.hx | 2 +- .../cutscene/dialogue/ConversationData.hx | 240 ------------------ .../dialogue/ConversationDataParser.hx | 163 ------------ .../dialogue/ConversationDebugState.hx | 61 ----- .../play/cutscene/dialogue/DialogueBox.hx | 2 +- .../play/cutscene/dialogue/DialogueBoxData.hx | 124 --------- .../dialogue/DialogueBoxDataParser.hx | 159 ------------ .../cutscene/dialogue/ScriptedConversation.hx | 6 + .../cutscene/dialogue/ScriptedDialogueBox.hx | 5 + .../play/cutscene/dialogue/SpeakerData.hx | 78 ------ .../cutscene/dialogue/SpeakerDataParser.hx | 159 ------------ tests/unit/project.xml | 1 - 19 files changed, 103 insertions(+), 1006 deletions(-) delete mode 100644 source/funkin/play/cutscene/dialogue/ConversationData.hx delete mode 100644 source/funkin/play/cutscene/dialogue/ConversationDataParser.hx delete mode 100644 source/funkin/play/cutscene/dialogue/ConversationDebugState.hx delete mode 100644 source/funkin/play/cutscene/dialogue/DialogueBoxData.hx delete mode 100644 source/funkin/play/cutscene/dialogue/DialogueBoxDataParser.hx delete mode 100644 source/funkin/play/cutscene/dialogue/SpeakerData.hx delete mode 100644 source/funkin/play/cutscene/dialogue/SpeakerDataParser.hx 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 f5d506688..9755a5e37 100644 --- a/Project.xml +++ b/Project.xml @@ -108,7 +108,6 @@ - diff --git a/hmm.json b/hmm.json index c8b1d911e..fa9a67057 100644 --- a/hmm.json +++ b/hmm.json @@ -156,11 +156,6 @@ "name": "thx.semver", "type": "haxelib", "version": "0.2.2" - }, - { - "name": "tink_json", - "type": "haxelib", - "version": "0.11.0" } ] } diff --git a/source/funkin/InitState.hx b/source/funkin/InitState.hx index 23bc255f1..12a937b2a 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; @@ -208,22 +208,30 @@ class InitState extends FlxState // GAME DATA PARSING // + trace('Parsing game data...'); + + var perf_gameDataParse_start = haxe.Timer.stamp(); + // NOTE: Registries and data parsers must be imported and not referenced with fully qualified names, // to ensure build macros work properly. 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(); + CharacterDataParser.loadCharacterCache(); // TODO: Migrate characters to BaseRegistry. ModuleHandler.buildModuleCallbacks(); ModuleHandler.loadModuleCache(); ModuleHandler.callOnCreate(); + + var perf_gameDataParse_end = haxe.Timer.stamp(); + + trace('Done parsing game data. Duration: ${perf_gameDataParse_end - perf_gameDataParse_start} seconds'); } /** @@ -241,6 +249,8 @@ class InitState extends FlxState startLevel(defineLevel(), defineDifficulty()); #elseif FREEPLAY // -DFREEPLAY 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/DataParse.hx b/source/funkin/data/DataParse.hx index 49dde0198..dbded3fba 100644 --- a/source/funkin/data/DataParse.hx +++ b/source/funkin/data/DataParse.hx @@ -120,6 +120,68 @@ class DataParse } } + public static function backdropData(json:Json, name:String):BackdropData + { + switch (json.value) + { + case JObject(fields): + var result:BackdropData = {}; + var backdropType:String = ''; + + for (field in fields) + { + switch (field.name) + { + case 'backdropType': + 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):OutroData + { + switch (json.value) + { + case JObject(fields): + var result:OutroData = {}; + var outroType:String = ''; + + for (field in fields) + { + switch (field.name) + { + case 'outroType': + 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}".'; + } + } + } + /** * Parser which outputs a `Either`. * Used by the FNF legacy JSON importer. diff --git a/source/funkin/modding/PolymodHandler.hx b/source/funkin/modding/PolymodHandler.hx index 151e658b4..5488cbbbe 100644 --- a/source/funkin/modding/PolymodHandler.hx +++ b/source/funkin/modding/PolymodHandler.hx @@ -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/PlayState.hx b/source/funkin/play/PlayState.hx index cc9debf13..aee9f2210 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; @@ -1662,7 +1662,7 @@ class PlayState extends MusicBeatSubState { isInCutscene = true; - currentConversation = ConversationDataParser.fetchConversation(conversationId); + currentConversation = ConversationRegistry.instance.fetchEntry(conversationId); if (currentConversation == null) return; currentConversation.completeCallback = onConversationComplete; diff --git a/source/funkin/play/cutscene/dialogue/Conversation.hx b/source/funkin/play/cutscene/dialogue/Conversation.hx index b2361c795..817c8caf3 100644 --- a/source/funkin/play/cutscene/dialogue/Conversation.hx +++ b/source/funkin/play/cutscene/dialogue/Conversation.hx @@ -13,7 +13,7 @@ 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 funkin.data.dialogue.ConversationData.DialogueEntryData; import flixel.addons.display.FlxPieDial; /** diff --git a/source/funkin/play/cutscene/dialogue/ConversationData.hx b/source/funkin/play/cutscene/dialogue/ConversationData.hx deleted file mode 100644 index 8c4aa9684..000000000 --- a/source/funkin/play/cutscene/dialogue/ConversationData.hx +++ /dev/null @@ -1,240 +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 - { - 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 - } = tink.Json.parse(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/ConversationDebugState.hx b/source/funkin/play/cutscene/dialogue/ConversationDebugState.hx deleted file mode 100644 index 13697b9f4..000000000 --- a/source/funkin/play/cutscene/dialogue/ConversationDebugState.hx +++ /dev/null @@ -1,61 +0,0 @@ -package funkin.play.cutscene.dialogue; - -import flixel.FlxState; -import funkin.modding.events.ScriptEventDispatcher; -import funkin.modding.events.ScriptEvent; -import flixel.util.FlxColor; -import funkin.ui.MusicBeatState; - -/** - * A state with displays a conversation with no background. - * Used for testing. - * @param conversationId The conversation to display. - */ -class ConversationDebugState extends MusicBeatState -{ - final conversationId:String = 'senpai'; - - var conversation:Conversation; - - public function new() - { - super(); - - // TODO: Fix this BS - Paths.setCurrentLevel('week6'); - } - - public override function create():Void - { - conversation = ConversationDataParser.fetchConversation(conversationId); - conversation.completeCallback = onConversationComplete; - add(conversation); - - ScriptEventDispatcher.callEvent(conversation, new ScriptEvent(CREATE, false)); - } - - function onConversationComplete():Void - { - remove(conversation); - conversation = null; - } - - public override function update(elapsed:Float):Void - { - super.update(elapsed); - - if (conversation != null) - { - if (controls.CUTSCENE_ADVANCE) conversation.advanceConversation(); - - if (controls.CUTSCENE_SKIP) - { - conversation.trySkipConversation(elapsed); - } - else - { - conversation.trySkipConversation(-1); - } - } - } -} diff --git a/source/funkin/play/cutscene/dialogue/DialogueBox.hx b/source/funkin/play/cutscene/dialogue/DialogueBox.hx index cdac3c233..4df34badc 100644 --- a/source/funkin/play/cutscene/dialogue/DialogueBox.hx +++ b/source/funkin/play/cutscene/dialogue/DialogueBox.hx @@ -235,7 +235,7 @@ class DialogueBox extends FlxSpriteGroup implements IDialogueScriptedClass { 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, + textDisplay.setFormat(boxData.text.fontFamily, boxData.text.size, FlxColor.fromString(boxData.text.color), LEFT, SHADOW, FlxColor.fromString(boxData.text.shadowColor ?? '#00000000'), false); textDisplay.borderSize = boxData.text.shadowWidth ?? 2; textDisplay.sounds = [FlxG.sound.load(Paths.sound('pixelText'), 0.6)]; diff --git a/source/funkin/play/cutscene/dialogue/DialogueBoxData.hx b/source/funkin/play/cutscene/dialogue/DialogueBoxData.hx deleted file mode 100644 index 801a01dd7..000000000 --- a/source/funkin/play/cutscene/dialogue/DialogueBoxData.hx +++ /dev/null @@ -1,124 +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 - { - 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 - } = tink.Json.parse(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/SpeakerData.hx b/source/funkin/play/cutscene/dialogue/SpeakerData.hx deleted file mode 100644 index 88883ead8..000000000 --- a/source/funkin/play/cutscene/dialogue/SpeakerData.hx +++ /dev/null @@ -1,78 +0,0 @@ -package funkin.play.cutscene.dialogue; - -import funkin.data.animation.AnimationData; - -/** - * 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 - { - if (i == null || i == '') return null; - var data: - { - version:String, - name:String, - assetPath:String, - animations:Array, - ?offsets:Array, - ?flipX:Bool, - ?isPixel:Bool, - ?scale:Float - } = tink.Json.parse(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/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 @@ -