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<String, Speaker> = new Map<String, Speaker>(); static final speakerScriptedClass:Map<String, String> = new Map<String, String>(); 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<String> = 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<String> = DataAssets.listDataFilesInPath('dialogue/speakers/'); // Filter out speakers that are scripted. var unscriptedSpeakerIds:Array<String> = 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<Speaker> { 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<String> { 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<SpeakerData> { 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; } }