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
//
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,
// 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();
ConversationRegistry.instance.loadEntries();
DialogueBoxRegistry.instance.loadEntries();
SpeakerRegistry.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.
ModuleHandler.buildModuleCallbacks();
ModuleHandler.loadModuleCache();
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>();
}
/**
* TODO: Create a `loadEntriesAsync()` function.
*/
public function loadEntries():Void
{
clearEntries();
@ -54,7 +57,7 @@ abstract class BaseRegistry<T:(IRegistryEntry<J> & Constructible<EntryConstructo
// SCRIPTED ENTRIES
//
var scriptedEntryClassNames:Array<String> = getScriptedClassNames();
log('Registering ${scriptedEntryClassNames.length} scripted entries...');
log('Parsing ${scriptedEntryClassNames.length} scripted entries...');
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 {
return !entries.exists(entryId);
});
log('Fetching data for ${unscriptedEntryIds.length} unscripted entries...');
log('Parsing ${unscriptedEntryIds.length} unscripted entries...');
for (entryId in unscriptedEntryIds)
{
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)
{
case JObject(fields):
var result:BackdropData = {};
var result:Dynamic = {};
var backdropType:String = '';
for (field in fields)
{
switch (field.name)
{
case 'backdropType':
case 'type':
backdropType = 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)
{
case JObject(fields):
var result:OutroData = {};
var result:Dynamic = {};
var outroType:String = '';
for (field in fields)
{
switch (field.name)
{
case 'outroType':
case 'type':
outroType = Tools.getValue(field.value);
}
Reflect.setField(result, field.name, Tools.getValue(field.value));
@ -179,6 +179,9 @@ class DataParse
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}.';
}
}

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
//
var scriptedEntryClassNames:Array<String> = 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<Song, SongMetadata>
var unscriptedEntryIds:Array<String> = 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

View file

@ -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,
}

View file

@ -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

View file

@ -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.data.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<ConversationData>
{
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<ConversationData>
{
return ConversationRegistry.instance.parseEntryDataWithMigration(id, ConversationRegistry.instance.fetchEntryVersion(id));
}
}

View file

@ -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<DialogueBoxData>
{
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<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.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(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.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<DialogueBoxData>
{
return DialogueBoxRegistry.instance.parseEntryDataWithMigration(id, DialogueBoxRegistry.instance.fetchEntryVersion(id));
}
}

View file

@ -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<SpeakerData>
{
/**
* 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<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 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>
{
var textAssets = openfl.utils.Assets.list();
var textAssets = openfl.utils.Assets.list(TEXT);
var queryPath = buildDataPath(path);
var results:Array<String> = [];