Rework Conversation data parsing

This commit is contained in:
EliteMasterEric 2024-02-07 18:45:13 -05:00
parent fa556dc1f2
commit 31cd5b3414
17 changed files with 849 additions and 113 deletions

View file

@ -208,30 +208,29 @@ class InitState extends FlxState
// GAME DATA PARSING // GAME DATA PARSING
// //
trace('Parsing game data...'); // NOTE: Registries must be imported and not referenced with fully qualified names,
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. // 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(); SongRegistry.instance.loadEntries();
LevelRegistry.instance.loadEntries(); LevelRegistry.instance.loadEntries();
NoteStyleRegistry.instance.loadEntries(); NoteStyleRegistry.instance.loadEntries();
SongEventRegistry.loadEventCache();
ConversationRegistry.instance.loadEntries(); ConversationRegistry.instance.loadEntries();
DialogueBoxRegistry.instance.loadEntries(); DialogueBoxRegistry.instance.loadEntries();
SpeakerRegistry.instance.loadEntries(); SpeakerRegistry.instance.loadEntries();
StageRegistry.instance.loadEntries(); StageRegistry.instance.loadEntries();
// TODO: CharacterDataParser doesn't use json2object, so it's way slower than the other parsers.
CharacterDataParser.loadCharacterCache(); // TODO: Migrate characters to BaseRegistry. CharacterDataParser.loadCharacterCache(); // TODO: Migrate characters to BaseRegistry.
ModuleHandler.buildModuleCallbacks(); ModuleHandler.buildModuleCallbacks();
ModuleHandler.loadModuleCache(); ModuleHandler.loadModuleCache();
ModuleHandler.callOnCreate(); ModuleHandler.callOnCreate();
var perf_gameDataParse_end = haxe.Timer.stamp(); var perfEnd = haxe.Timer.stamp();
trace('Done parsing game data. Duration: ${perf_gameDataParse_end - perf_gameDataParse_start} seconds'); trace('Parsing game data took ${Math.floor((perfEnd - perfStart) * 1000)}ms.');
} }
/** /**

View file

@ -46,6 +46,9 @@ abstract class BaseRegistry<T:(IRegistryEntry<J> & Constructible<EntryConstructo
this.entries = new Map<String, T>(); this.entries = new Map<String, T>();
} }
/**
* TODO: Create a `loadEntriesAsync()` function.
*/
public function loadEntries():Void public function loadEntries():Void
{ {
clearEntries(); clearEntries();
@ -54,7 +57,7 @@ abstract class BaseRegistry<T:(IRegistryEntry<J> & Constructible<EntryConstructo
// SCRIPTED ENTRIES // SCRIPTED ENTRIES
// //
var scriptedEntryClassNames:Array<String> = getScriptedClassNames(); var scriptedEntryClassNames:Array<String> = getScriptedClassNames();
log('Registering ${scriptedEntryClassNames.length} scripted entries...'); log('Parsing ${scriptedEntryClassNames.length} scripted entries...');
for (entryCls in scriptedEntryClassNames) for (entryCls in scriptedEntryClassNames)
{ {
@ -78,7 +81,7 @@ abstract class BaseRegistry<T:(IRegistryEntry<J> & Constructible<EntryConstructo
var unscriptedEntryIds:Array<String> = entryIdList.filter(function(entryId:String):Bool { var unscriptedEntryIds:Array<String> = entryIdList.filter(function(entryId:String):Bool {
return !entries.exists(entryId); return !entries.exists(entryId);
}); });
log('Fetching data for ${unscriptedEntryIds.length} unscripted entries...'); log('Parsing ${unscriptedEntryIds.length} unscripted entries...');
for (entryId in unscriptedEntryIds) for (entryId in unscriptedEntryIds)
{ {
try try

View file

@ -120,19 +120,19 @@ class DataParse
} }
} }
public static function backdropData(json:Json, name:String):BackdropData public static function backdropData(json:Json, name:String):funkin.data.dialogue.ConversationData.BackdropData
{ {
switch (json.value) switch (json.value)
{ {
case JObject(fields): case JObject(fields):
var result:BackdropData = {}; var result:Dynamic = {};
var backdropType:String = ''; var backdropType:String = '';
for (field in fields) for (field in fields)
{ {
switch (field.name) switch (field.name)
{ {
case 'backdropType': case 'type':
backdropType = Tools.getValue(field.value); backdropType = Tools.getValue(field.value);
} }
Reflect.setField(result, field.name, Tools.getValue(field.value)); Reflect.setField(result, field.name, Tools.getValue(field.value));
@ -152,19 +152,19 @@ class DataParse
} }
} }
public static function outroData(json:Json, name:String):OutroData public static function outroData(json:Json, name:String):Null<funkin.data.dialogue.ConversationData.OutroData>
{ {
switch (json.value) switch (json.value)
{ {
case JObject(fields): case JObject(fields):
var result:OutroData = {}; var result:Dynamic = {};
var outroType:String = ''; var outroType:String = '';
for (field in fields) for (field in fields)
{ {
switch (field.name) switch (field.name)
{ {
case 'outroType': case 'type':
outroType = Tools.getValue(field.value); outroType = Tools.getValue(field.value);
} }
Reflect.setField(result, field.name, Tools.getValue(field.value)); Reflect.setField(result, field.name, Tools.getValue(field.value));
@ -179,6 +179,9 @@ class DataParse
default: default:
throw 'Expected Outro property $name to be specify a valid "type", but it was "${outroType}".'; 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}.';
} }
} }

View file

@ -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<OutroData>;
/**
* Data on the music for the conversation.
*/
@:optional
public var music:Null<MusicData>;
/**
* Data for each line of dialogue in the conversation.
*/
public var dialogue:Array<DialogueEntryData>;
}
/**
* 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<String>;
/**
* 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;
};

View file

@ -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<Conversation, ConversationData>
{
/**
* 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<ConversationData>
{
// JsonParser does not take type parameters,
// otherwise this function would be in BaseRegistry.
var parser = new json2object.JsonParser<ConversationData>();
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<ConversationData>
{
var parser = new json2object.JsonParser<ConversationData>();
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<String>
{
return ScriptedConversation.listScriptClasses();
}
}

View file

@ -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<Float>;
/**
* 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<AnimationData>;
}
typedef DialogueBoxTextData =
{
/**
* The position of the text in teh box.
*/
@:optional
@:default([0, 0])
var offsets:Array<Float>;
/**
* 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;
};

View file

@ -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<DialogueBox, DialogueBoxData>
{
/**
* 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<DialogueBoxData>
{
// JsonParser does not take type parameters,
// otherwise this function would be in BaseRegistry.
var parser = new json2object.JsonParser<DialogueBoxData>();
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<DialogueBoxData>
{
var parser = new json2object.JsonParser<DialogueBoxData>();
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<String>
{
return ScriptedDialogueBox.listScriptClasses();
}
}

View file

@ -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<Float>;
/**
* 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<AnimationData>;
}

View file

@ -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<Speaker, SpeakerData>
{
/**
* 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<SpeakerData>
{
// JsonParser does not take type parameters,
// otherwise this function would be in BaseRegistry.
var parser = new json2object.JsonParser<SpeakerData>();
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<SpeakerData>
{
var parser = new json2object.JsonParser<SpeakerData>();
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<String>
{
return ScriptedSpeaker.listScriptClasses();
}
}

View file

@ -58,7 +58,7 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
// SCRIPTED ENTRIES // SCRIPTED ENTRIES
// //
var scriptedEntryClassNames:Array<String> = getScriptedClassNames(); var scriptedEntryClassNames:Array<String> = getScriptedClassNames();
log('Registering ${scriptedEntryClassNames.length} scripted entries...'); log('Parsing ${scriptedEntryClassNames.length} scripted entries...');
for (entryCls in scriptedEntryClassNames) for (entryCls in scriptedEntryClassNames)
{ {
@ -84,7 +84,7 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
var unscriptedEntryIds:Array<String> = entryIdList.filter(function(entryId:String):Bool { var unscriptedEntryIds:Array<String> = entryIdList.filter(function(entryId:String):Bool {
return !entries.exists(entryId); return !entries.exists(entryId);
}); });
log('Fetching data for ${unscriptedEntryIds.length} unscripted entries...'); log('Parsing ${unscriptedEntryIds.length} unscripted entries...');
for (entryId in unscriptedEntryIds) for (entryId in unscriptedEntryIds)
{ {
try try

View file

@ -2,7 +2,6 @@ package funkin.modding;
import funkin.util.macro.ClassMacro; import funkin.util.macro.ClassMacro;
import funkin.modding.module.ModuleHandler; import funkin.modding.module.ModuleHandler;
import funkin.play.character.CharacterData.CharacterDataParser;
import funkin.data.song.SongData; import funkin.data.song.SongData;
import funkin.data.stage.StageData; import funkin.data.stage.StageData;
import polymod.Polymod; import polymod.Polymod;
@ -13,10 +12,11 @@ import funkin.data.stage.StageRegistry;
import funkin.util.FileUtil; import funkin.util.FileUtil;
import funkin.data.level.LevelRegistry; import funkin.data.level.LevelRegistry;
import funkin.data.notestyle.NoteStyleRegistry; import funkin.data.notestyle.NoteStyleRegistry;
import funkin.play.cutscene.dialogue.ConversationDataParser; import funkin.data.dialogue.ConversationRegistry;
import funkin.play.cutscene.dialogue.DialogueBoxDataParser; import funkin.data.dialogue.DialogueBoxRegistry;
import funkin.data.dialogue.SpeakerRegistry;
import funkin.play.character.CharacterData.CharacterDataParser;
import funkin.save.Save; import funkin.save.Save;
import funkin.play.cutscene.dialogue.SpeakerDataParser;
import funkin.data.song.SongRegistry; import funkin.data.song.SongRegistry;
class PolymodHandler class PolymodHandler
@ -208,8 +208,8 @@ class PolymodHandler
{ {
return { return {
assetLibraryPaths: [ assetLibraryPaths: [
"default" => "preload", "shared" => "", "songs" => "songs", "tutorial" => "tutorial", "week1" => "week1", "week2" => "week2", "week3" => "week3", "default" => "preload", "shared" => "shared", "songs" => "songs", "tutorial" => "tutorial", "week1" => "week1", "week2" => "week2",
"week4" => "week4", "week5" => "week5", "week6" => "week6", "week7" => "week7", "weekend1" => "weekend1", "week3" => "week3", "week4" => "week4", "week5" => "week5", "week6" => "week6", "week7" => "week7", "weekend1" => "weekend1",
], ],
coreAssetRedirect: CORE_FOLDER, coreAssetRedirect: CORE_FOLDER,
} }

View file

@ -43,7 +43,7 @@ class CharacterDataParser
{ {
// Clear any stages that are cached if there were any. // Clear any stages that are cached if there were any.
clearCharacterCache(); clearCharacterCache();
trace('Loading character cache...'); trace('[CHARACTER] Parsing all entries...');
// //
// UNSCRIPTED CHARACTERS // UNSCRIPTED CHARACTERS

View file

@ -1,8 +1,10 @@
package funkin.play.cutscene.dialogue; package funkin.play.cutscene.dialogue;
import funkin.data.IRegistryEntry;
import flixel.FlxSprite; import flixel.FlxSprite;
import flixel.group.FlxSpriteGroup; import flixel.group.FlxSpriteGroup;
import flixel.util.FlxColor; import flixel.util.FlxColor;
import funkin.graphics.FunkinSprite;
import flixel.tweens.FlxTween; import flixel.tweens.FlxTween;
import flixel.tweens.FlxEase; import flixel.tweens.FlxEase;
import flixel.sound.FlxSound; import flixel.sound.FlxSound;
@ -13,27 +15,30 @@ import funkin.modding.IScriptedClass.IEventHandler;
import funkin.play.cutscene.dialogue.DialogueBox; import funkin.play.cutscene.dialogue.DialogueBox;
import funkin.modding.IScriptedClass.IDialogueScriptedClass; import funkin.modding.IScriptedClass.IDialogueScriptedClass;
import funkin.modding.events.ScriptEventDispatcher; import funkin.modding.events.ScriptEventDispatcher;
import funkin.data.dialogue.ConversationData.DialogueEntryData;
import flixel.addons.display.FlxPieDial; 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. * 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 * 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<ConversationData>
{ {
static final CONVERSATION_SKIP_TIMER:Float = 1.5; static final CONVERSATION_SKIP_TIMER:Float = 1.5;
var skipHeldTimer:Float = 0.0; var skipHeldTimer:Float = 0.0;
/** /**
* DATA * The ID of the conversation.
*/ */
/** public final id:String;
* The ID of the associated dialogue.
*/
public final conversationId:String;
/** /**
* The current state of the conversation. * The current state of the conversation.
@ -41,9 +46,9 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass
var state:ConversationState = ConversationState.Start; 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. * The current entry in the dialogue.
@ -54,7 +59,7 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass
function get_currentDialogueEntryCount():Int 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 function get_currentDialogueEntryData():DialogueEntryData
{ {
if (conversationData == null || conversationData.dialogue == null) return null; if (_data == null || _data.dialogue == null) return null;
if (currentDialogueEntry < 0 || currentDialogueEntry >= conversationData.dialogue.length) return null; if (currentDialogueEntry < 0 || currentDialogueEntry >= _data.dialogue.length) return null;
return conversationData.dialogue[currentDialogueEntry]; return _data.dialogue[currentDialogueEntry];
} }
var currentDialogueLineString(get, never):String; var currentDialogueLineString(get, never):String;
@ -94,7 +99,7 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass
/** /**
* GRAPHICS * GRAPHICS
*/ */
var backdrop:FlxSprite; var backdrop:FunkinSprite;
var currentSpeaker:Speaker; var currentSpeaker:Speaker;
@ -102,14 +107,17 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass
var skipTimer:FlxPieDial; var skipTimer:FlxPieDial;
public function new(conversationId:String) public function new(id:String)
{ {
super(); super();
this.conversationId = conversationId; this.id = id;
this.conversationData = ConversationDataParser.parseConversationData(this.conversationId); 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 public function onCreate(event:ScriptEvent):Void
@ -125,14 +133,14 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass
function setupMusic():Void 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; 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 else
{ {
@ -145,19 +153,20 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass
function setupBackdrop():Void 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 // Play intro
switch (conversationData?.backdrop.type) switch (_data.backdrop)
{ {
case SOLID: case SOLID(backdropData):
backdrop.makeGraphic(Std.int(FlxG.width), Std.int(FlxG.height), FlxColor.fromString(conversationData.backdrop.data.color)); var targetColor:FlxColor = FlxColor.fromString(backdropData.color);
if (conversationData.backdrop.data.fadeTime > 0.0) backdrop.makeSolidColor(Std.int(FlxG.width), Std.int(FlxG.height), targetColor);
if (backdropData.fadeTime > 0.0)
{ {
backdrop.alpha = 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 else
{ {
@ -190,9 +199,9 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass
var nextSpeakerId:String = currentDialogueEntryData.speaker; var nextSpeakerId:String = currentDialogueEntryData.speaker;
// Skip the next steps if the current speaker is already displayed. // 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) if (currentSpeaker != null)
{ {
@ -241,7 +250,7 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass
var nextDialogueBoxId:String = currentDialogueEntryData?.box; var nextDialogueBoxId:String = currentDialogueEntryData?.box;
// Skip the next steps if the current speaker is already displayed. // 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) if (currentDialogueBox != null)
{ {
@ -250,7 +259,7 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass
currentDialogueBox = null; currentDialogueBox = null;
} }
var nextDialogueBox:DialogueBox = DialogueBoxDataParser.fetchDialogueBox(nextDialogueBoxId); var nextDialogueBox:DialogueBox = DialogueBoxRegistry.instance.fetchEntry(nextDialogueBoxId);
if (nextDialogueBox == null) if (nextDialogueBox == null)
{ {
@ -378,20 +387,18 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass
public function startOutro():Void public function startOutro():Void
{ {
switch (conversationData?.outro?.type) switch (_data?.outro)
{ {
case FADE: case FADE(outroData):
var fadeTime:Float = conversationData?.outro.data.fadeTime ?? 1.0; outroTween = FlxTween.tween(this, {alpha: 0.0}, outroData.fadeTime,
outroTween = FlxTween.tween(this, {alpha: 0.0}, fadeTime,
{ {
type: ONESHOT, // holy shit like the game no way type: ONESHOT, // holy shit like the game no way
startDelay: 0, startDelay: 0,
onComplete: (_) -> endOutro(), onComplete: (_) -> endOutro(),
}); });
FlxTween.tween(this.music, {volume: 0.0}, fadeTime); FlxTween.tween(this.music, {volume: 0.0}, outroData.fadeTime);
case NONE: case NONE(_):
// Immediately clean up. // Immediately clean up.
endOutro(); endOutro();
default: default:
@ -400,7 +407,7 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass
} }
} }
public var completeCallback:Void->Void; public var completeCallback:() -> Void;
public function endOutro():Void public function endOutro():Void
{ {
@ -596,7 +603,12 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass
public override function toString():String public override function toString():String
{ {
return 'Conversation($conversationId)'; return 'Conversation($id)';
}
static function _fetchData(id:String):Null<ConversationData>
{
return ConversationRegistry.instance.parseEntryDataWithMigration(id, ConversationRegistry.instance.fetchEntryVersion(id));
} }
} }

View file

@ -1,6 +1,7 @@
package funkin.play.cutscene.dialogue; package funkin.play.cutscene.dialogue;
import flixel.FlxSprite; import flixel.FlxSprite;
import funkin.data.IRegistryEntry;
import flixel.group.FlxSpriteGroup; import flixel.group.FlxSpriteGroup;
import flixel.graphics.frames.FlxFramesCollection; import flixel.graphics.frames.FlxFramesCollection;
import flixel.text.FlxText; import flixel.text.FlxText;
@ -9,18 +10,21 @@ import funkin.util.assets.FlxAnimationUtil;
import funkin.modding.events.ScriptEvent; import funkin.modding.events.ScriptEvent;
import funkin.modding.IScriptedClass.IDialogueScriptedClass; import funkin.modding.IScriptedClass.IDialogueScriptedClass;
import flixel.util.FlxColor; 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<DialogueBoxData>
{ {
public final dialogueBoxId:String; public final id:String;
public var dialogueBoxName(get, never):String; public var dialogueBoxName(get, never):String;
function get_dialogueBoxName():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. * 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; return this.speed;
} }
public function new(dialogueBoxId:String) public function new(id:String)
{ {
super(); super();
this.dialogueBoxId = dialogueBoxId; this.id = id;
this.boxData = DialogueBoxDataParser.parseDialogueBoxData(this.dialogueBoxId); 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 public function onCreate(event:ScriptEvent):Void
@ -115,18 +122,18 @@ class DialogueBox extends FlxSpriteGroup implements IDialogueScriptedClass
function loadSpritesheet():Void 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) if (tex == null)
{ {
trace('Could not load Sparrow sprite: ${boxData.assetPath}'); trace('Could not load Sparrow sprite: ${_data.assetPath}');
return; return;
} }
this.boxSprite.frames = tex; this.boxSprite.frames = tex;
if (boxData.isPixel) if (_data.isPixel)
{ {
this.boxSprite.antialiasing = false; this.boxSprite.antialiasing = false;
} }
@ -135,9 +142,10 @@ class DialogueBox extends FlxSpriteGroup implements IDialogueScriptedClass
this.boxSprite.antialiasing = true; this.boxSprite.antialiasing = true;
} }
this.flipX = boxData.flipX; this.flipX = _data.flipX;
this.globalOffsets = boxData.offsets; this.flipY = _data.flipY;
this.setScale(boxData.scale); this.globalOffsets = _data.offsets;
this.setScale(_data.scale);
} }
public function setText(newText:String):Void public function setText(newText:String):Void
@ -184,11 +192,11 @@ class DialogueBox extends FlxSpriteGroup implements IDialogueScriptedClass
function loadAnimations():Void 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) if (anim.offsets == null)
{ {
@ -201,7 +209,7 @@ class DialogueBox extends FlxSpriteGroup implements IDialogueScriptedClass
} }
var animNames:Array<String> = this.boxSprite?.animation?.getNameList() ?? []; var animNames:Array<String> = 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.callback = this.onAnimationFrame;
boxSprite.animation.finishCallback = this.onAnimationFinished; boxSprite.animation.finishCallback = this.onAnimationFinished;
@ -234,16 +242,16 @@ class DialogueBox extends FlxSpriteGroup implements IDialogueScriptedClass
function loadText():Void function loadText():Void
{ {
textDisplay = new FlxTypeText(0, 0, 300, '', 32); textDisplay = new FlxTypeText(0, 0, 300, '', 32);
textDisplay.fieldWidth = boxData.text.width; textDisplay.fieldWidth = _data.text.width;
textDisplay.setFormat(boxData.text.fontFamily, boxData.text.size, FlxColor.fromString(boxData.text.color), LEFT, SHADOW, textDisplay.setFormat(_data.text.fontFamily, _data.text.size, FlxColor.fromString(_data.text.color), LEFT, SHADOW,
FlxColor.fromString(boxData.text.shadowColor ?? '#00000000'), false); FlxColor.fromString(_data.text.shadowColor ?? '#00000000'), false);
textDisplay.borderSize = boxData.text.shadowWidth ?? 2; textDisplay.borderSize = _data.text.shadowWidth ?? 2;
textDisplay.sounds = [FlxG.sound.load(Paths.sound('pixelText'), 0.6)]; textDisplay.sounds = [FlxG.sound.load(Paths.sound('pixelText'), 0.6)];
textDisplay.completeCallback = onTypingComplete; textDisplay.completeCallback = onTypingComplete;
textDisplay.x += boxData.text.offsets[0]; textDisplay.x += _data.text.offsets[0];
textDisplay.y += boxData.text.offsets[1]; textDisplay.y += _data.text.offsets[1];
add(textDisplay); add(textDisplay);
} }
@ -374,4 +382,14 @@ class DialogueBox extends FlxSpriteGroup implements IDialogueScriptedClass
} }
public function onScriptEvent(event:ScriptEvent):Void {} public function onScriptEvent(event:ScriptEvent):Void {}
public override function toString():String
{
return 'DialogueBox($id)';
}
static function _fetchData(id:String):Null<DialogueBoxData>
{
return DialogueBoxRegistry.instance.parseEntryDataWithMigration(id, DialogueBoxRegistry.instance.fetchEntryVersion(id));
}
} }

View file

@ -1,27 +1,30 @@
package funkin.play.cutscene.dialogue; package funkin.play.cutscene.dialogue;
import flixel.FlxSprite; import flixel.FlxSprite;
import funkin.data.IRegistryEntry;
import funkin.modding.events.ScriptEvent; import funkin.modding.events.ScriptEvent;
import flixel.graphics.frames.FlxFramesCollection; import flixel.graphics.frames.FlxFramesCollection;
import funkin.util.assets.FlxAnimationUtil; import funkin.util.assets.FlxAnimationUtil;
import funkin.modding.IScriptedClass.IDialogueScriptedClass; import funkin.modding.IScriptedClass.IDialogueScriptedClass;
import funkin.data.dialogue.SpeakerData;
import funkin.data.dialogue.SpeakerRegistry;
/** /**
* The character sprite which displays during dialogue. * The character sprite which displays during dialogue.
* *
* Most conversations have two speakers, with one being flipped. * Most conversations have two speakers, with one being flipped.
*/ */
class Speaker extends FlxSprite implements IDialogueScriptedClass class Speaker extends FlxSprite implements IDialogueScriptedClass implements IRegistryEntry<SpeakerData>
{ {
/** /**
* The internal ID for this speaker. * The internal ID for this speaker.
*/ */
public final speakerId:String; public final id:String;
/** /**
* The full data for a speaker. * The full data for a speaker.
*/ */
var speakerData:SpeakerData; public final _data:SpeakerData;
/** /**
* A readable name for this speaker. * A readable name for this speaker.
@ -30,7 +33,7 @@ class Speaker extends FlxSprite implements IDialogueScriptedClass
function get_speakerName():String function get_speakerName():String
{ {
return speakerData.name; return _data.name;
} }
/** /**
@ -75,14 +78,17 @@ class Speaker extends FlxSprite implements IDialogueScriptedClass
return globalOffsets = value; return globalOffsets = value;
} }
public function new(speakerId:String) public function new(id:String)
{ {
super(); super();
this.speakerId = speakerId; this.id = id;
this.speakerData = SpeakerDataParser.parseSpeakerData(this.speakerId); 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 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) if (tex == null)
{ {
trace('Could not load Sparrow sprite: ${speakerData.assetPath}'); trace('Could not load Sparrow sprite: ${_data.assetPath}');
return; return;
} }
this.frames = tex; this.frames = tex;
if (speakerData.isPixel) if (_data.isPixel)
{ {
this.antialiasing = false; this.antialiasing = false;
} }
@ -122,9 +128,10 @@ class Speaker extends FlxSprite implements IDialogueScriptedClass
this.antialiasing = true; this.antialiasing = true;
} }
this.flipX = speakerData.flipX; this.flipX = _data.flipX;
this.globalOffsets = speakerData.offsets; this.flipY = _data.flipY;
this.setScale(speakerData.scale); this.globalOffsets = _data.offsets;
this.setScale(_data.scale);
} }
/** /**
@ -141,11 +148,11 @@ class Speaker extends FlxSprite implements IDialogueScriptedClass
function loadAnimations():Void 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) if (anim.offsets == null)
{ {
@ -158,7 +165,7 @@ class Speaker extends FlxSprite implements IDialogueScriptedClass
} }
var animNames:Array<String> = this.animation.getNameList(); var animNames:Array<String> = 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 function onScriptEvent(event:ScriptEvent):Void {}
public override function toString():String
{
return 'Speaker($id)';
}
static function _fetchData(id:String):Null<SpeakerData>
{
return SpeakerRegistry.instance.parseEntryDataWithMigration(id, SpeakerRegistry.instance.fetchEntryVersion(id));
}
} }

View file

@ -0,0 +1,76 @@
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.
* 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 = ConversationRegistry.instance.fetchEntry(conversationId);
conversation.completeCallback = onConversationComplete;
add(conversation);
ScriptEventDispatcher.callEvent(conversation, new ScriptEvent(CREATE, false));
}
function onConversationComplete():Void
{
remove(conversation);
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);
if (conversation != null)
{
if (controls.CUTSCENE_ADVANCE) conversation.advanceConversation();
if (controls.CUTSCENE_SKIP)
{
conversation.trySkipConversation(elapsed);
}
else
{
conversation.trySkipConversation(-1);
}
}
}
}

View file

@ -9,7 +9,8 @@ class DataAssets
public static function listDataFilesInPath(path:String, ?suffix:String = '.json'):Array<String> public static function listDataFilesInPath(path:String, ?suffix:String = '.json'):Array<String>
{ {
var textAssets = openfl.utils.Assets.list(); var textAssets = openfl.utils.Assets.list(TEXT);
var queryPath = buildDataPath(path); var queryPath = buildDataPath(path);
var results:Array<String> = []; var results:Array<String> = [];