From 00cfeeff72710a8dacb885f5c0c821e238baacd6 Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Thu, 13 Jul 2023 20:25:44 -0400 Subject: [PATCH] Created the Note Style class and data registry --- source/funkin/data/BaseRegistry.hx | 14 + source/funkin/data/notestyle/NoteStyleData.hx | 171 ++++++++++ .../data/notestyle/NoteStyleRegistry.hx | 65 ++++ .../funkin/play/notes/notestyle/NoteStyle.hx | 304 ++++++++++++++++++ .../play/notes/notestyle/ScriptedNoteStyle.hx | 9 + 5 files changed, 563 insertions(+) create mode 100644 source/funkin/data/notestyle/NoteStyleData.hx create mode 100644 source/funkin/data/notestyle/NoteStyleRegistry.hx create mode 100644 source/funkin/play/notes/notestyle/NoteStyle.hx create mode 100644 source/funkin/play/notes/notestyle/ScriptedNoteStyle.hx diff --git a/source/funkin/data/BaseRegistry.hx b/source/funkin/data/BaseRegistry.hx index b30c311a3..36b1d26e3 100644 --- a/source/funkin/data/BaseRegistry.hx +++ b/source/funkin/data/BaseRegistry.hx @@ -84,6 +84,7 @@ abstract class BaseRegistry & Constructible & Constructible { return entries.keys().array(); } + /** + * Count the number of entries in this registry. + * @return The number of entries. + */ public function countEntries():Int { return entries.size(); } + /** + * Fetch an entry by its ID. + * @param id The ID of the entry to fetch. + * @return The entry, or `null` if it does not exist. + */ public function fetchEntry(id:String):Null { return entries.get(id); diff --git a/source/funkin/data/notestyle/NoteStyleData.hx b/source/funkin/data/notestyle/NoteStyleData.hx new file mode 100644 index 000000000..04fda67ca --- /dev/null +++ b/source/funkin/data/notestyle/NoteStyleData.hx @@ -0,0 +1,171 @@ +package funkin.data.notestyle; + +import haxe.DynamicAccess; +import funkin.data.animation.AnimationData; + +/** + * A type definition for the data in a note style JSON file. + * @see https://lib.haxe.org/p/json2object/ + */ +typedef NoteStyleData = +{ + /** + * The version number of the note style data schema. + * When making changes to the note style data format, this should be incremented, + * and a migration function should be added to NoteStyleDataParser to handle old versions. + */ + @:default(funkin.data.notestyle.NoteStyleRegistry.NOTE_STYLE_DATA_VERSION) + var version:String; + + /** + * The readable title of the note style. + */ + var name:String; + + /** + * The author of the note style. + */ + var author:String; + + /** + * The note style to use as a fallback/parent. + * @default null + */ + @:optional + var fallback:Null; + + /** + * Data for each of the assets in the note style. + */ + var assets:NoteStyleAssetsData; +} + +typedef NoteStyleAssetsData = +{ + /** + * The sprites for the notes. + * @default The sprites from the fallback note style. + */ + @:optional + var note:NoteStyleAssetData; + + /** + * The sprites for the hold notes. + * @default The sprites from the fallback note style. + */ + @:optional + var holdNote:NoteStyleAssetData; + + /** + * The sprites for the strumline. + * @default The sprites from the fallback note style. + */ + @:optional + var noteStrumline:NoteStyleAssetData; + + /** + * The sprites for the note splashes. + */ + @:optional + var noteSplash:NoteStyleAssetData; + + /** + * The sprites for the hold note covers. + */ + @:optional + var holdNoteCover:NoteStyleAssetData; +} + +/** + * Data shared by all note style assets. + */ +typedef NoteStyleAssetData = +{ + /** + * The image to use for the asset. May be a Sparrow sprite sheet. + */ + var assetPath:String; + + /** + * The scale to render the prop at. + * @default 1.0 + */ + @:default(1.0) + @:optional + var scale:Float; + + /** + * Offset the sprite's position by this amount. + * @default [0, 0] + */ + @:default([0, 0]) + @:optional + var offsets:Null>; + + /** + * If true, the prop is a pixel sprite, and will be rendered without anti-aliasing. + */ + @:default(false) + @:optional + var isPixel:Bool; + + /** + * The structure of this data depends on the asset. + */ + var data:T; +} + +typedef NoteStyleData_Note = +{ + var left:UnnamedAnimationData; + var down:UnnamedAnimationData; + var up:UnnamedAnimationData; + var right:UnnamedAnimationData; +} + +typedef NoteStyleData_HoldNote = {} + +/** + * Data on animations for each direction of the strumline. + */ +typedef NoteStyleData_NoteStrumline = +{ + var leftStatic:UnnamedAnimationData; + var leftPress:UnnamedAnimationData; + var leftConfirm:UnnamedAnimationData; + var leftConfirmHold:UnnamedAnimationData; + var downStatic:UnnamedAnimationData; + var downPress:UnnamedAnimationData; + var downConfirm:UnnamedAnimationData; + var downConfirmHold:UnnamedAnimationData; + var upStatic:UnnamedAnimationData; + var upPress:UnnamedAnimationData; + var upConfirm:UnnamedAnimationData; + var upConfirmHold:UnnamedAnimationData; + var rightStatic:UnnamedAnimationData; + var rightPress:UnnamedAnimationData; + var rightConfirm:UnnamedAnimationData; + var rightConfirmHold:UnnamedAnimationData; +} + +typedef NoteStyleData_NoteSplash = +{ + /** + * If false, note splashes are entirely hidden on this note style. + * @default Note splashes are enabled. + */ + @:optional + @:default(true) + var enabled:Bool; +}; + +typedef NoteStyleData_HoldNoteCover = +{ + /** + * If false, hold note covers are entirely hidden on this note style. + * @default Hold note covers are enabled. + */ + @:optional + @:default(true) + var enabled:Bool; +}; diff --git a/source/funkin/data/notestyle/NoteStyleRegistry.hx b/source/funkin/data/notestyle/NoteStyleRegistry.hx new file mode 100644 index 000000000..d666a037c --- /dev/null +++ b/source/funkin/data/notestyle/NoteStyleRegistry.hx @@ -0,0 +1,65 @@ +package funkin.data.notestyle; + +import funkin.play.notes.notestyle.NoteStyle; +import funkin.play.notes.notestyle.ScriptedNoteStyle; +import funkin.data.notestyle.NoteStyleData; + +class NoteStyleRegistry extends BaseRegistry +{ + /** + * The current version string for the note style data format. + * Handle breaking changes by incrementing this value + * and adding migration to the `migrateNoteStyleData()` function. + */ + public static final NOTE_STYLE_DATA_VERSION:String = "1.0.0"; + + public static final DEFAULT_NOTE_STYLE_ID:String = "funkin"; + + public static final instance:NoteStyleRegistry = new NoteStyleRegistry(); + + public function new() + { + super('NOTESTYLE', 'notestyles'); + } + + public function fetchDefault():NoteStyle + { + return fetchEntry(DEFAULT_NOTE_STYLE_ID); + } + + /** + * Read, parse, and validate the JSON data and produce the corresponding data object. + */ + public function parseEntryData(id:String):Null + { + if (id == null) id = DEFAULT_NOTE_STYLE_ID; + + // JsonParser does not take type parameters, + // otherwise this function would be in BaseRegistry. + var parser = new json2object.JsonParser(); + var jsonStr:String = loadEntryFile(id); + + parser.fromJson(jsonStr); + + if (parser.errors.length > 0) + { + trace('Failed to parse entry data: ${id}'); + for (error in parser.errors) + { + trace(error); + } + return null; + } + return parser.value; + } + + function createScriptedEntry(clsName:String):NoteStyle + { + return ScriptedNoteStyle.init(clsName, "unknown"); + } + + function getScriptedClassNames():Array + { + return ScriptedNoteStyle.listScriptClasses(); + } +} diff --git a/source/funkin/play/notes/notestyle/NoteStyle.hx b/source/funkin/play/notes/notestyle/NoteStyle.hx new file mode 100644 index 000000000..6cb9e7cdc --- /dev/null +++ b/source/funkin/play/notes/notestyle/NoteStyle.hx @@ -0,0 +1,304 @@ +package funkin.play.notes.notestyle; + +import flixel.graphics.frames.FlxAtlasFrames; +import flixel.graphics.frames.FlxFramesCollection; +import funkin.data.animation.AnimationData; +import funkin.data.IRegistryEntry; +import funkin.data.notestyle.NoteStyleData; +import funkin.data.notestyle.NoteStyleRegistry; +import funkin.data.notestyle.NoteStyleRegistry; +import funkin.util.assets.FlxAnimationUtil; + +using funkin.data.animation.AnimationData.AnimationDataUtil; + +/** + * Holds the data for what assets to use for a note style, + * and provides convenience methods for building sprites based on them. + */ +class NoteStyle implements IRegistryEntry +{ + /** + * The ID of the note style. + */ + public final id:String; + + /** + * Note style data as parsed from the JSON file. + */ + public final _data:NoteStyleData; + + /** + * The note style to use if this one doesn't have a certain asset. + * This can be recursive, ehe. + */ + final fallback:Null; + + /** + * @param id The ID of the JSON file to parse. + */ + public function new(id:String) + { + this.id = id; + _data = _fetchData(id); + + if (_data == null) + { + throw 'Could not parse note style data for id: $id'; + } + + this.fallback = NoteStyleRegistry.instance.fetchEntry(getFallbackID()); + } + + /** + * Get the readable name of the note style. + * @return String + */ + public function getName():String + { + return _data.name; + } + + /** + * Get the author of the note style. + * @return String + */ + public function getAuthor():String + { + return _data.author; + } + + /** + * Get the note style ID of the parent note style. + * @return The string ID, or `null` if there is no parent. + */ + function getFallbackID():Null + { + return _data.fallback; + } + + public function buildNoteSprite(target:NoteSprite):Void + { + // Apply the note sprite frames. + var atlas:FlxAtlasFrames = buildNoteFrames(false); + + if (atlas == null) + { + throw 'Could not load spritesheet for note style: $id'; + } + + target.frames = atlas; + + target.scale.x = _data.assets.note.scale; + target.scale.y = _data.assets.note.scale; + target.antialiasing = !_data.assets.note.isPixel; + + // Apply the animations. + buildNoteAnimations(target); + } + + var noteFrames:FlxAtlasFrames = null; + + function buildNoteFrames(force:Bool = false):FlxAtlasFrames + { + if (noteFrames != null && !force) return noteFrames; + + noteFrames = Paths.getSparrowAtlas(getNoteAssetPath(), getNoteAssetLibrary()); + + noteFrames.parent.persist = true; + + return noteFrames; + } + + function getNoteAssetPath(?raw:Bool = false):String + { + if (raw) + { + var rawPath:Null = _data?.assets?.note?.assetPath; + if (rawPath == null) return fallback.getNoteAssetPath(true); + return rawPath; + } + + // library:path + var parts = getNoteAssetPath(true).split(Constants.LIBRARY_SEPARATOR); + if (parts.length == 1) return getNoteAssetPath(true); + return parts[1]; + } + + function getNoteAssetLibrary():Null + { + // library:path + var parts = getNoteAssetPath(true).split(Constants.LIBRARY_SEPARATOR); + if (parts.length == 1) return null; + return parts[0]; + } + + function buildNoteAnimations(target:NoteSprite):Void + { + var leftData:AnimationData = fetchNoteAnimationData(LEFT); + target.animation.addByPrefix('purpleScroll', leftData.prefix); + var downData:AnimationData = fetchNoteAnimationData(DOWN); + target.animation.addByPrefix('blueScroll', downData.prefix); + var upData:AnimationData = fetchNoteAnimationData(UP); + target.animation.addByPrefix('greenScroll', upData.prefix); + var rightData:AnimationData = fetchNoteAnimationData(RIGHT); + target.animation.addByPrefix('redScroll', rightData.prefix); + } + + function fetchNoteAnimationData(dir:NoteDirection):AnimationData + { + var result:Null = switch (dir) + { + case LEFT: _data.assets.note.data.left.toNamed(); + case DOWN: _data.assets.note.data.down.toNamed(); + case UP: _data.assets.note.data.up.toNamed(); + case RIGHT: _data.assets.note.data.right.toNamed(); + }; + + return (result == null) ? fallback.fetchNoteAnimationData(dir) : result; + } + + public function getHoldNoteAssetPath(?raw:Bool = false):String + { + if (raw) + { + var rawPath:Null = _data?.assets?.holdNote?.assetPath; + return (rawPath == null) ? fallback.getHoldNoteAssetPath(true) : rawPath; + } + + // library:path + var parts = getHoldNoteAssetPath(true).split(Constants.LIBRARY_SEPARATOR); + if (parts.length == 1) return Paths.image(parts[0]); + return Paths.image(parts[1], parts[0]); + } + + public function isHoldNotePixel():Bool + { + var data = _data?.assets?.holdNote; + if (data == null) return fallback.isHoldNotePixel(); + return data.isPixel; + } + + public function fetchHoldNoteScale():Float + { + var data = _data?.assets?.holdNote; + if (data == null) return fallback.fetchHoldNoteScale(); + return data.scale; + } + + public function applyStrumlineFrames(target:StrumlineNote):Void + { + // TODO: Add support for multi-Sparrow. + // Will be less annoying after this is merged: https://github.com/HaxeFlixel/flixel/pull/2772 + + var atlas:FlxAtlasFrames = Paths.getSparrowAtlas(getStrumlineAssetPath(), getStrumlineAssetLibrary()); + + if (atlas == null) + { + throw 'Could not load spritesheet for note style: $id'; + } + + target.frames = atlas; + + target.scale.x = _data.assets.noteStrumline.scale; + target.scale.y = _data.assets.noteStrumline.scale; + target.antialiasing = !_data.assets.noteStrumline.isPixel; + } + + function getStrumlineAssetPath(?raw:Bool = false):String + { + if (raw) + { + var rawPath:Null = _data?.assets?.noteStrumline?.assetPath; + if (rawPath == null) return fallback.getStrumlineAssetPath(true); + return rawPath; + } + + // library:path + var parts = getStrumlineAssetPath(true).split(Constants.LIBRARY_SEPARATOR); + if (parts.length == 1) return getStrumlineAssetPath(true); + return parts[1]; + } + + function getStrumlineAssetLibrary():Null + { + // library:path + var parts = getStrumlineAssetPath(true).split(Constants.LIBRARY_SEPARATOR); + if (parts.length == 1) return null; + return parts[0]; + } + + public function applyStrumlineAnimations(target:StrumlineNote, dir:NoteDirection):Void + { + FlxAnimationUtil.addAtlasAnimations(target, getStrumlineAnimationData(dir)); + } + + function getStrumlineAnimationData(dir:NoteDirection):Array + { + var result:Array = switch (dir) + { + case NoteDirection.LEFT: [ + _data.assets.noteStrumline.data.leftStatic.toNamed('static'), + _data.assets.noteStrumline.data.leftPress.toNamed('press'), + _data.assets.noteStrumline.data.leftConfirm.toNamed('confirm'), + _data.assets.noteStrumline.data.leftConfirmHold.toNamed('confirm-hold'), + ]; + case NoteDirection.DOWN: [ + _data.assets.noteStrumline.data.downStatic.toNamed('static'), + _data.assets.noteStrumline.data.downPress.toNamed('press'), + _data.assets.noteStrumline.data.downConfirm.toNamed('confirm'), + _data.assets.noteStrumline.data.downConfirmHold.toNamed('confirm-hold'), + ]; + case NoteDirection.UP: [ + _data.assets.noteStrumline.data.upStatic.toNamed('static'), + _data.assets.noteStrumline.data.upPress.toNamed('press'), + _data.assets.noteStrumline.data.upConfirm.toNamed('confirm'), + _data.assets.noteStrumline.data.upConfirmHold.toNamed('confirm-hold'), + ]; + case NoteDirection.RIGHT: [ + _data.assets.noteStrumline.data.rightStatic.toNamed('static'), + _data.assets.noteStrumline.data.rightPress.toNamed('press'), + _data.assets.noteStrumline.data.rightConfirm.toNamed('confirm'), + _data.assets.noteStrumline.data.rightConfirmHold.toNamed('confirm-hold'), + ]; + }; + + return result; + } + + public function applyStrumlineOffsets(target:StrumlineNote) + { + target.x += _data.assets.noteStrumline.offsets[0]; + target.y += _data.assets.noteStrumline.offsets[1]; + } + + public function getStrumlineScale():Float + { + return _data.assets.noteStrumline.scale; + } + + public function isNoteSplashEnabled():Bool + { + var data = _data?.assets?.noteSplash?.data; + if (data == null) return fallback.isNoteSplashEnabled(); + return data.enabled; + } + + public function isHoldNoteCoverEnabled():Bool + { + var data = _data?.assets?.holdNoteCover?.data; + if (data == null) return fallback.isHoldNoteCoverEnabled(); + return data.enabled; + } + + public function destroy():Void {} + + public function toString():String + { + return 'NoteStyle($id)'; + } + + public function _fetchData(id:String):Null + { + return NoteStyleRegistry.instance.parseEntryData(id); + } +} diff --git a/source/funkin/play/notes/notestyle/ScriptedNoteStyle.hx b/source/funkin/play/notes/notestyle/ScriptedNoteStyle.hx new file mode 100644 index 000000000..cae0e60ec --- /dev/null +++ b/source/funkin/play/notes/notestyle/ScriptedNoteStyle.hx @@ -0,0 +1,9 @@ +package funkin.play.notes.notestyle; + +/** + * A script that can be tied to a NoteStyle. + * Create a scripted class that extends NoteStyle to use this. + * This allows you to customize how a specific note style appears. + */ +@:hscriptClass +class ScriptedNoteStyle extends funkin.play.notes.notestyle.NoteStyle implements polymod.hscript.HScriptedClass {}