From 3939439086cc8a9aa7ba985ff4f77c60d71b087c Mon Sep 17 00:00:00 2001 From: Ralty <78720179+Raltyro@users.noreply.github.com> Date: Fri, 8 Aug 2025 20:50:12 +0700 Subject: [PATCH] Chart Variations Backend Implementation --- source/funkin/backend/chart/Chart.hx | 179 ++++++++++++----------- source/funkin/backend/chart/ChartData.hx | 13 +- 2 files changed, 102 insertions(+), 90 deletions(-) diff --git a/source/funkin/backend/chart/Chart.hx b/source/funkin/backend/chart/Chart.hx index 98b1126f..263b16d3 100644 --- a/source/funkin/backend/chart/Chart.hx +++ b/source/funkin/backend/chart/Chart.hx @@ -83,77 +83,72 @@ class Chart { return data; } - public static function loadChartMeta(songName:String, difficulty:String = '', fromMods:Bool = true, includeMetaDifficulties:Bool = true):ChartMetaData { - var metaPath = Paths.file('songs/${songName}/meta.json'); - var metaDiffPath = Paths.file('songs/${songName}/meta-${difficulty}.json'); - - var data:ChartMetaData = null, fromDifficulty = false; - var fromMods:Bool = fromMods; - for (path in [metaDiffPath, metaPath]) if (Assets.exists(path)) { + public static function loadChartMeta(songName:String, ?variant:String, fromMods:Bool = true, includeMetaVariations = true):ChartMetaData { + var defaultPath = Paths.file('songs/$songName/meta.json'), isVariant = false; + var data:ChartMetaData = null, paths = (variant == null || variant == '') ? [defaultPath] : [Paths.file('songs/$songName/meta-$variant.json'), defaultPath]; + for (path in paths) if (Assets.exists(path)) { fromMods = Paths.assetsTree.existsSpecific(path, "TEXT", MODS); try { var tempData = Json.parse(Assets.getText(path)); tempData.color = CoolUtil.getColorFromDynamic(tempData.color).getDefault(Flags.DEFAULT_COLOR); data = tempData; - } catch(e) Logs.trace('Failed to load song metadata for ${songName} ($path): ${Std.string(e)}', ERROR); + } catch(e) Logs.trace('Failed to load song metadata for $songName ($path): ${Std.string(e)}', ERROR); + if (data != null) { - fromDifficulty = path == metaDiffPath; + isVariant = path != defaultPath; break; } } - if (data == null) { - data = { - name: songName, - bpm: Flags.DEFAULT_BPM - }; - } - else { - data.name = songName; - data.setFieldDefault("bpm", Flags.DEFAULT_BPM); - } + if (data != null) data.name = songName; + else data = { + name: songName, + color: Flags.DEFAULT_COLOR + }; + + if (isVariant) data.variant = variant; + else data.variant = null; data.setFieldDefault("displayName", data.name); + + data.setFieldDefault("bpm", Flags.DEFAULT_BPM); data.setFieldDefault("beatsPerMeasure", Flags.DEFAULT_BEATS_PER_MEASURE); data.setFieldDefault("stepsPerBeat", Flags.DEFAULT_STEPS_PER_BEAT); data.setFieldDefault("icon", Flags.DEFAULT_HEALTH_ICON); - data.setFieldDefault("difficulties", []); data.setFieldDefault("coopAllowed", Flags.DEFAULT_COOP_ALLOWED); data.setFieldDefault("opponentModeAllowed", Flags.DEFAULT_OPPONENT_MODE_ALLOWED); data.setFieldDefault("instSuffix", ""); data.setFieldDefault("vocalsSuffix", ""); data.setFieldDefault("needsVoices", true); + data.setFieldDefault("difficulties", []); + data.setFieldDefault("variants", []); if (data.difficulties.length <= 0) { - data.difficulties = [for(f in Paths.getFolderContent('songs/${songName}/charts/', false, fromMods ? MODS : SOURCE)) if (Path.extension(f.toUpperCase()) == "JSON") Path.withoutExtension(f)]; - var tempDiffs = []; + var path = 'songs/$songName/charts/'; + if (isVariant) path += '$variant/'; + + data.difficulties = [for(f in Paths.getFolderContent(path, false, fromMods ? MODS : SOURCE)) if (Path.extension(f.toUpperCase()) == "JSON") Path.withoutExtension(f)]; if (data.difficulties.length == 3) { - for(d in data.difficulties) { - switch(d.toLowerCase()) { - case "easy": tempDiffs.insert(0, d); - case "normal": tempDiffs.insert(1, d); - case "hard": tempDiffs.insert(2, d); - } + var tempDiffs = []; + for (d in data.difficulties) switch(d.toLowerCase()) { + case "easy": tempDiffs.insert(0, d); + case "normal": tempDiffs.insert(1, d); + case "hard": tempDiffs.insert(2, d); } - data.difficulties = tempDiffs; + if (tempDiffs.length == 3) data.difficulties = tempDiffs; } } - if (includeMetaDifficulties && !fromDifficulty) { - data.metas = []; - for (difficulty in data.difficulties) { - if (!data.metas.exists(difficulty) && Assets.exists(Paths.file('songs/${songName}/meta-${difficulty}.json'))) - data.metas.set(difficulty, loadChartMeta(songName, difficulty, fromMods, false)); - } + if (includeMetaVariations & data.variants.length > 0) for (variant in data.variants) { + if (!data.metas.exists(variant) && Assets.exists(Paths.file('songs/$songName/meta-$variant.json'))) + data.metas.set(variant, loadChartMeta(songName, variant, fromMods)); } return data; } - public static function parse(songName:String, ?difficulty:String):ChartData { - if (difficulty == null) difficulty = Flags.DEFAULT_DIFFICULTY; - - var chartPath = Paths.chart(songName, difficulty); + public static function parse(songName:String, ?variant:String):ChartData { + var chartPath = Paths.chart(songName, variant); var base:ChartData = { strumLines: [], noteTypes: [], @@ -167,15 +162,15 @@ class Chart { fromMods: Paths.assetsTree.existsSpecific(chartPath, "TEXT", MODS) }; - var valid:Bool = true; + var valid:Bool = true, namePrint = (variant == null || variant == '') ? '$songName' : '$songName ($variant)'; if (!Assets.exists(chartPath)) { - Logs.error('Chart for song ${songName} ($difficulty) at "$chartPath" was not found.'); + Logs.error('Chart for song $namePrint at "$chartPath" was not found.'); valid = false; } var data:Dynamic = null; if (valid) { try data = Json.parse(Assets.getText(chartPath)) - catch(e) Logs.trace('Could not parse chart for song ${songName} ($difficulty): ${Std.string(e)}', ERROR, RED); + catch(e) Logs.trace('Could not parse chart for song $namePrint: ${Std.string(e)}', ERROR, RED); } /** @@ -207,13 +202,12 @@ class Chart { } #end - var loadedMeta = loadChartMeta(songName, difficulty, base.fromMods); + var loadedMeta = loadChartMeta(songName, variant, base.fromMods, false); if (base.meta == null) base.meta = loadedMeta; else { for (field in Reflect.fields(base.meta)) { var f = Reflect.field(base.meta, field); - if (f != null) - Reflect.setField(loadedMeta, field, f); + if (f != null) Reflect.setField(loadedMeta, field, f); } base.meta = loadedMeta; } @@ -252,61 +246,49 @@ class Chart { /** * Saves the chart to the specific song folder path. - * @param songFolderPath Path to the song folder (ex: `mods/your mod/songs/song/`) - * @param chart Chart to save - * @param difficulty Name of the difficulty - * @param saveSettings + * @param chart Chart to save. + * @param difficulty Name of the difficulty (Optional). + * @param variant Name of the Variant (Optional). + * @param saveSettings (Optional). * @return Filtered chart used for saving. */ - public static function save(songFolderPath:String, chart:ChartData, ?difficulty:String, ?saveSettings:ChartSaveSettings):ChartData { + public static function save(chart:ChartData, ?difficulty:String, ?variant:String, ?saveSettings:ChartSaveSettings):ChartData { if (difficulty == null) difficulty = Flags.DEFAULT_DIFFICULTY; if (saveSettings == null) saveSettings = {}; - if (saveSettings.saveMetaInChart == null) saveSettings.saveMetaInChart = true; - if (saveSettings.saveLocalEvents == null) saveSettings.saveLocalEvents = true; - if (saveSettings.saveGlobalEvents == null) saveSettings.saveGlobalEvents = false; - var filteredChart = filterChartForSaving(chart, saveSettings.saveMetaInChart, saveSettings.saveLocalEvents, saveSettings.saveGlobalEvents); - var meta = filteredChart.meta; #if sys - var saveFolder:String = saveSettings.folder == null ? "charts" : saveSettings.folder; + var songPath = saveSettings.songFolder == null ? 'assets/songs/${chart.meta.name}' : saveSettings.songFolder; + var metaPath = '$songPath/meta.json', temp:String; + if ((temp = Paths.assetsTree.getPath(metaPath)) != null) metaPath = temp; - if (!FileSystem.exists('${songFolderPath}/$saveFolder/')) - FileSystem.createDirectory('${songFolderPath}/$saveFolder/'); + var chartFolder = saveSettings.folder == null ? ((variant == null || variant == '') ? 'charts' : 'charts/$variant') : saveSettings.folder; + var chartPath = '$songPath/$chartFolder/${difficulty.trim()}.json'; + if ((temp = Paths.assetsTree.getPath(chartPath)) != null) chartPath = temp; - var chartPath = '${songFolderPath}/$saveFolder/${difficulty.trim()}.json'; - var metaPath = '${songFolderPath}/meta.json'; + if (saveSettings.saveChart == null || saveSettings.saveChart == true) + CoolUtil.safeSaveFile(chartPath, Json.stringify(filteredChart, null, saveSettings.prettyPrint == true ? Flags.JSON_PRETTY_PRINT : null)); - CoolUtil.safeSaveFile(chartPath, Json.stringify(filteredChart, null, saveSettings.prettyPrint == true ? Flags.JSON_PRETTY_PRINT : null)); - - if (meta != null && (saveSettings.overrideExistingMeta || !FileSystem.exists(metaPath))) - CoolUtil.safeSaveFile(metaPath, makeMetaSaveable(meta)); + if (filteredChart.meta != null && (saveSettings.overrideExistingMeta || !FileSystem.exists(metaPath))) + CoolUtil.safeSaveFile(metaPath, makeMetaSaveable(filteredChart.meta)); #end + return filteredChart; } - public static function filterChartForSaving(chart:ChartData, ?saveMetaInChart:Bool, ?saveLocalEvents:Bool, ?saveGlobalEvents:Bool):ChartData { + public static function filterChartForSaving(chart:ChartData, saveMetaInChart = true, ?saveLocalEvents:Bool, ?saveGlobalEvents:Bool):ChartData { + var meta = chart.meta, events = chart.events; + chart.meta = null; + chart.events = null; + var data = Reflect.copy(chart); // make a copy of the chart to leave the OG intact - if (saveMetaInChart != true) { - data.meta = null; - } else { - data.meta = Reflect.copy(chart.meta); // also make a copy of the metadata to leave the OG intact. - if (data.meta != null && Reflect.hasField(data.meta, "parsedColor")) Reflect.deleteField(data.meta, "parsedColor"); - } - // in this part abt the events, i gotta account that these booleans can be null - Nex - if (saveLocalEvents != true && saveGlobalEvents != true) data.events = null; - else { - data.events = []; - for (event in chart.events) if ((saveLocalEvents == true && event.global != true) || (saveGlobalEvents == true && event.global == true)) { - var copy = Reflect.copy(event); - if (saveLocalEvents == true ? event.global != true : event.global == true) Reflect.deleteField(copy, "global"); // should NOT delete the field when saving with the local events and the event should have been global - Nex - data.events.push(copy); - } - if (data.events.length == 0) data.events = null; - } + chart.meta = meta; + chart.events = events; + data.meta = saveMetaInChart ? filterMetaForSaving(meta) : null; + data.events = filterEventsForSaving(events, saveLocalEvents, saveGlobalEvents); data.fromMods = null; var sortedData:Dynamic = {}; @@ -318,19 +300,42 @@ class Chart { return sortedData; } - public static inline function makeMetaSaveable(meta:ChartMetaData, prettyPrint:Bool = true):String { + public static function filterEventsForSaving(events:Array, saveLocalEvents = true, saveGlobalEvents = false):Array { + var data = []; + if (!saveLocalEvents && !saveGlobalEvents) return data; + + for (event in chart.events) if ((saveLocalEvents && event.global != true) || (saveGlobalEvents && event.global == true)) { + var copy = Reflect.copy(event); + if (saveLocalEvents ? event.global != true : event.global == true) Reflect.deleteField(copy, "global"); // should NOT delete the field when saving with the local events and the event should have been global - Nex + data.events.push(copy); + } + + return data; + } + + public static inline function makeMetaSaveable(meta:ChartMetaData, prettyPrint:Bool = true):String + return Json.stringify(filterMetaForSaving(meta), null, prettyPrint ? Flags.JSON_PRETTY_PRINT : null); + + public static inline function filterMetaForSaving(meta:ChartMetaData):ChartMetaData { var data:Dynamic = Reflect.copy(meta); - if (data.color != null) data.color = FlxColor.fromInt(data.color).toWebString(); // dont even ask me - Nex + if (data.color != null) data.color = FlxColor.fromInt(data.color).toWebString(); // dont even ask me - Nex + Reflect.deleteField(data, 'parsedColor'); Reflect.deleteField(data, 'metas'); - return Json.stringify(data, null, prettyPrint ? Flags.JSON_PRETTY_PRINT : null); + return data; } } typedef ChartSaveSettings = { - var ?overrideExistingMeta:Bool; + var ?prettyPrint:Bool; + var ?saveMetaInChart:Bool; var ?saveLocalEvents:Bool; var ?saveGlobalEvents:Bool; - var ?prettyPrint:Bool; + + var ?saveChart:Bool; + var ?overrideExistingMeta:Bool; + var ?seperateGlobalEvents:Bool; + var ?folder:String; + var ?songFolder:String; } diff --git a/source/funkin/backend/chart/ChartData.hx b/source/funkin/backend/chart/ChartData.hx index e5ac048a..dfb514bd 100644 --- a/source/funkin/backend/chart/ChartData.hx +++ b/source/funkin/backend/chart/ChartData.hx @@ -18,16 +18,23 @@ typedef ChartData = { typedef ChartMetaData = { public var name:String; - public var ?bpm:Float; + public var ?variant:String; public var ?displayName:String; + + public var ?bpm:Float; public var ?beatsPerMeasure:Float; public var ?stepsPerBeat:Int; + + public var ?difficulties:Array; + public var ?variants:Array; + public var ?customValues:Dynamic; + public var ?icon:String; public var ?color:FlxColor; - public var ?difficulties:Array; + public var ?coopAllowed:Bool; public var ?opponentModeAllowed:Bool; - public var ?customValues:Dynamic; + public var ?metas:Map; public var ?instSuffix:String; public var ?vocalsSuffix:String;