diff --git a/assets/languages/en/Editors.xml b/assets/languages/en/Editors.xml index e5517f75..be6f9cdb 100644 --- a/assets/languages/en/Editors.xml +++ b/assets/languages/en/Editors.xml @@ -45,12 +45,15 @@ Select a song to modify the charts from. Press ACCEPT to choose a difficulty to edit. + Press ACCEPT to choose a song variation to edit. Press ACCEPT to edit the chart for the selected difficulty. Select a difficulty to continue. New Difficulty Press ACCEPT to create a new chart difficulty. New Song Press ACCEPT to create a new song. + New Variation + Press ACCEPT to create a new song variation. @@ -104,6 +107,7 @@ Song Data Song Name + Song Variation BPM Time Signature Menus Data (Freeplay/Story) diff --git a/source/funkin/backend/assets/Paths.hx b/source/funkin/backend/assets/Paths.hx index 48cc8f4f..5b8d8395 100644 --- a/source/funkin/backend/assets/Paths.hx +++ b/source/funkin/backend/assets/Paths.hx @@ -128,11 +128,11 @@ class Paths return scriptPath; } - static public function chart(song:String, ?difficulty:String):String + static public function chart(song:String, ?difficulty:String, ?variant:String):String { difficulty = (difficulty != null ? difficulty : Flags.DEFAULT_DIFFICULTY); - return getPath('songs/$song/charts/$difficulty.json', null); + return getPath('songs/$song/charts/${variant != null ? variant + "/" : ""}$difficulty.json', null); } public static function character(character:String):String { diff --git a/source/funkin/backend/chart/Chart.hx b/source/funkin/backend/chart/Chart.hx index 98b1126f..bf2fb50f 100644 --- a/source/funkin/backend/chart/Chart.hx +++ b/source/funkin/backend/chart/Chart.hx @@ -83,77 +83,75 @@ 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)); - } + data.metas = []; + 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 { + public static function parse(songName:String, ?difficulty:String, ?variant:String):ChartData { if (difficulty == null) difficulty = Flags.DEFAULT_DIFFICULTY; - var chartPath = Paths.chart(songName, difficulty); + var chartPath = Paths.chart(songName, difficulty, variant); var base:ChartData = { strumLines: [], noteTypes: [], @@ -167,15 +165,15 @@ class Chart { fromMods: Paths.assetsTree.existsSpecific(chartPath, "TEXT", MODS) }; - var valid:Bool = true; + var valid:Bool = true, namePrint = '$songName $difficulty' + ((variant != null && variant != '') ? ' ($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 +205,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; } @@ -223,8 +220,7 @@ class Chart { */ #if REGION var extraEvents:Array = loadEventsJson(songName); - if (extraEvents != null) - base.events = base.events.concat(extraEvents); + if (extraEvents != null) base.events = base.events.concat(extraEvents); #end /** @@ -252,61 +248,60 @@ 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; + var filteredChart = filterChartForSaving(chart, saveSettings.saveMetaInChart, saveSettings.saveLocalEvents, saveSettings.saveGlobalEvents && saveSettings.seperateGlobalEvents != true); #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 = 'meta.json', prettyPrint = saveSettings.prettyPrint == true ? Flags.JSON_PRETTY_PRINT : null, temp:String; + if ((temp = Paths.assetsTree.getPath('$songPath/$metaPath')) != null) { + songPath = temp.substr(0, temp.length - metaPath.length - 1); + metaPath = temp; + } + else if (saveSettings.songFolder == null) + metaPath = (songPath = '${Paths.getAssetsRoot()}/$songPath') + '/$metaPath'; - 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'; - 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, prettyPrint)); - CoolUtil.safeSaveFile(chartPath, Json.stringify(filteredChart, null, saveSettings.prettyPrint == true ? Flags.JSON_PRETTY_PRINT : null)); + if (saveSettings.overrideExistingMeta || !FileSystem.exists(metaPath)) + CoolUtil.safeSaveFile(metaPath, Json.stringify(filterMetaForSaving(chart.meta), null, prettyPrint)); - if (meta != null && (saveSettings.overrideExistingMeta || !FileSystem.exists(metaPath))) - CoolUtil.safeSaveFile(metaPath, makeMetaSaveable(meta)); + if (saveSettings.seperateGlobalEvents == true) { + var eventsPath = '$songPath/events.json', events = filterEventsForSaving(chart.events, false, true); + + if (events.length != 0) CoolUtil.safeSaveFile(eventsPath, Json.stringify({events: events}, null, prettyPrint)); + else if (FileSystem.exists(eventsPath)) FileSystem.deleteFile(eventsPath); + } #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 +313,46 @@ 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 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.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); + Reflect.deleteField(data, "variant"); + if (data.instSuffix != null && data.instSuffix == "") Reflect.deleteField(data, "instSuffix"); + if (data.vocalsSuffix != null && data.vocalsSuffix == "") Reflect.deleteField(data, "vocalsSuffix"); + if (data.variants != null && data.variants.length == 0) Reflect.deleteField(data, "variants"); + 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; diff --git a/source/funkin/backend/scripting/events/menu/freeplay/FreeplaySongSelectEvent.hx b/source/funkin/backend/scripting/events/menu/freeplay/FreeplaySongSelectEvent.hx index e2ee1fc7..1445c6a9 100644 --- a/source/funkin/backend/scripting/events/menu/freeplay/FreeplaySongSelectEvent.hx +++ b/source/funkin/backend/scripting/events/menu/freeplay/FreeplaySongSelectEvent.hx @@ -9,6 +9,10 @@ final class FreeplaySongSelectEvent extends CancellableEvent { * Difficulty name */ public var difficulty:String; + /** + * Variation Name + */ + public var variant:String; /** * Whenever opponent mode is enabled or not. */ diff --git a/source/funkin/backend/system/macros/FunkinSaveMacro.hx b/source/funkin/backend/system/macros/FunkinSaveMacro.hx index 5ba9a051..8fc0e431 100644 --- a/source/funkin/backend/system/macros/FunkinSaveMacro.hx +++ b/source/funkin/backend/system/macros/FunkinSaveMacro.hx @@ -43,7 +43,6 @@ class FunkinSaveMacro { * SAVE FUNCTION */ var saveFuncBlocks:Array = [for(f in fieldNames) macro $i{saveFieldName}.data.$f = $i{f}]; - saveFuncBlocks.push(macro $i{saveFieldName}.flush()); fields.push({ @@ -59,6 +58,8 @@ class FunkinSaveMacro { access: [APublic, AStatic] }); + var loadFuncBlocks:Array = [for(f in fieldNames) macro if ($i{saveFieldName}.data.$f != null) $i{f} = $i{saveFieldName}.data.$f]; + /** * LOAD FUNCTION */ @@ -69,10 +70,7 @@ class FunkinSaveMacro { args: [], expr: { pos: Context.currentPos(), - expr: EBlock([for(f in fieldNames) - macro if ($i{saveFieldName}.data.$f != null) - $i{f} = $i{saveFieldName}.data.$f - ]) + expr: EBlock(loadFuncBlocks) } }), access: [APublic, AStatic] diff --git a/source/funkin/backend/week/WeekData.hx b/source/funkin/backend/week/WeekData.hx index 7be40175..1a75687a 100644 --- a/source/funkin/backend/week/WeekData.hx +++ b/source/funkin/backend/week/WeekData.hx @@ -26,6 +26,7 @@ typedef WeekCharacter = { typedef WeekSong = { var name:String; + var ?variation:String; var hide:Bool; var ?displayName:String; } \ No newline at end of file diff --git a/source/funkin/editors/charter/Charter.hx b/source/funkin/editors/charter/Charter.hx index 48163b78..128e505b 100644 --- a/source/funkin/editors/charter/Charter.hx +++ b/source/funkin/editors/charter/Charter.hx @@ -28,6 +28,7 @@ import flixel.util.FlxColor; class Charter extends UIState { public static var __song:String; static var __diff:String; + static var __variant:String; static var __reload:Bool; var chart(get, never):ChartData; @@ -114,11 +115,12 @@ class Charter extends UIState { private var SONGPOSINFO_BPM = TU.getRaw("songPosInfo.bpm"); private var SONGPOSINFO_TIMESIGNATURE = TU.getRaw("songPosInfo.timeSignature"); - public function new(song:String, diff:String, reload:Bool = true) { + public function new(song:String, diff:String, variant:String, reload:Bool = true) { super(); if (song != null) { __song = song; __diff = diff; + __variant = variant; __reload = reload; } } @@ -578,7 +580,7 @@ class Charter extends UIState { dataDisplay.screenCenter(Y); dataDisplay.cameras = [charterCamera]; dataDisplay.x = -dataDisplay.width; add(dataDisplay);*/ - DiscordUtil.call("onEditorLoaded", ["Chart Editor", __song + " (" + __diff + ")"]); + DiscordUtil.call("onEditorLoaded", ["Chart Editor", __song + " (" + __diff + ")" + (__variant != null && __variant != "" ? " (" + __variant + ")" : "")]); } override function destroy() { @@ -595,9 +597,10 @@ class Charter extends UIState { public function loadSong() { if (__reload) { EventsData.reloadEvents(); - PlayState.loadSong(__song, __diff, false, false); + PlayState.loadSong(__song, __diff, __variant, false, false); __resetStatics(); } + Conductor.setupSong(PlayState.SONG); noteTypes = PlayState.SONG.noteTypes; @@ -612,14 +615,14 @@ class Charter extends UIState { gridBackdrops.createGrids(PlayState.SONG.strumLines.length); - for(strL in PlayState.SONG.strumLines) + var noteCount:Int = 0; + for (strL in PlayState.SONG.strumLines) { createStrumline(strumLines.members.length, strL, false, false); + noteCount += strL.notes.length; + } // create notes notesGroup.autoSort = false; - var noteCount:Int = 0; - for (strL in PlayState.SONG.strumLines) - noteCount += strL.notes.length; notesGroup.preallocate(noteCount); var notesCreated:Int = 0; @@ -683,10 +686,10 @@ class Charter extends UIState { var wavesToGenerate:Array<{name:String, sound:FlxSound}> = []; if (FlxG.sound.music.loaded) - wavesToGenerate.push({name: "Inst.ogg", sound: FlxG.sound.music}); + wavesToGenerate.push({name: 'Inst${PlayState.SONG.meta.instSuffix}.ogg', sound: FlxG.sound.music}); if (vocals.loaded) - wavesToGenerate.push({name: "Voices.ogg", sound: vocals}); + wavesToGenerate.push({name: 'Voices${PlayState.SONG.meta.vocalsSuffix}.ogg', sound: vocals}); for (strumLine in strumLines) if (strumLine.vocals != null && strumLine.strumLine.vocalsSuffix != null && strumLine.strumLine.vocalsSuffix != "" && strumLine.vocals.loaded) @@ -788,12 +791,13 @@ class Charter extends UIState { autoSaveTimer = Options.charterAutoSaveTime; if (!autoSaveNotif.cancelled) { buildChart(); - var songPath:String = '${Paths.getAssetsRoot()}/songs/${__song.toLowerCase()}'; if (Options.charterAutoSavesSeparateFolder) - Chart.save(songPath, PlayState.SONG, __autoSaveLocation, {saveMetaInChart: true, saveLocalEvents: true, saveGlobalEvents: true, folder: "autosaves", prettyPrint: Options.editorCharterPrettyPrint}); - else // These two chart saves are particular, to avoid any kind of loss, stuff like meta, global and local events will be save all together - Nex - Chart.save(songPath, PlayState.SONG, __diff.toLowerCase(), {saveMetaInChart: true, saveLocalEvents: true, saveGlobalEvents: true, prettyPrint: Options.editorCharterPrettyPrint}); + Chart.save(PlayState.SONG, __diff.toLowerCase(), __autoSaveLocation, {saveMetaInChart: true, saveLocalEvents: true, seperateGlobalEvents: true, folder: 'autosaves', prettyPrint: Options.editorCharterPrettyPrint}); + else + Chart.save(PlayState.SONG, __diff.toLowerCase(), __variant, {saveMetaInChart: true, saveLocalEvents: true, seperateGlobalEvents: true, prettyPrint: Options.editorCharterPrettyPrint}); + + FlxG.sound.play(Paths.sound('editors/save')); undos.save(); } autoSaveNotif.cancelled = false; @@ -1476,8 +1480,8 @@ class Charter extends UIState { public static function saveChart(shouldBuild:Bool = true, withEvents:Bool = true) { #if sys - FlxG.sound.play(Paths.sound('editors/save')); - saveTo('${Paths.getAssetsRoot()}/songs/${__song.toLowerCase()}', !withEvents, shouldBuild); + if (shouldBuild && instance != null) instance.buildChart(); + Chart.save(PlayState.SONG, __diff.toLowerCase(), __variant, {saveMetaInChart: false, saveLocalEvents: withEvents, prettyPrint: Options.editorCharterPrettyPrint}); if (undos != null) undos.save(); #else saveChartAs(shouldBuild, withEvents); @@ -1493,13 +1497,9 @@ class Charter extends UIState { public static function saveEvents(shouldBuild:Bool = true) { #if sys - FlxG.sound.play(Paths.sound('editors/save')); if (shouldBuild && instance != null) instance.buildChart(); - var data = {events: Chart.filterChartForSaving(PlayState.SONG, false, false, true).events}; - - var path = '${Paths.getAssetsRoot()}/songs/${__song.toLowerCase()}/events.json'; - if (data.events != null && data.events.length > 0) CoolUtil.safeSaveFile(path, Json.stringify(data, null, Options.editorCharterPrettyPrint ? Flags.JSON_PRETTY_PRINT : null)); - else if (FileSystem.exists(path)) FileSystem.deleteFile(path); // Instead of replacing with a useless empty file, deletes the file directly - Nex + Chart.save(PlayState.SONG, __diff.toLowerCase(), __variant, {saveChart: false, seperateGlobalEvents: true, prettyPrint: Options.editorCharterPrettyPrint}); + if (undos != null) undos.save(); #else saveEventsAs(shouldBuild); #end @@ -1507,7 +1507,7 @@ class Charter extends UIState { public static function saveEventsAs(shouldBuild:Bool = true) { if (shouldBuild && instance != null) instance.buildChart(); - var data = {events: Chart.filterChartForSaving(PlayState.SONG, false, false, true).events}; + var data = {events: Chart.filterEventsForSaving(PlayState.SONG.events, false, true)}; saveAs(data, null, Options.editorCharterPrettyPrint ? Flags.JSON_PRETTY_PRINT : null, { defaultSaveFile: 'events.json' @@ -1516,12 +1516,8 @@ class Charter extends UIState { public static function saveMeta(shouldBuild:Bool = true) { #if sys - FlxG.sound.play(Paths.sound('editors/save')); if (shouldBuild && instance != null) instance.buildChart(); - CoolUtil.safeSaveFile( - '${Paths.getAssetsRoot()}/songs/${__song.toLowerCase()}/meta.json', - Json.stringify(PlayState.SONG.meta == null ? {} : Chart.filterChartForSaving(PlayState.SONG, true, false, false).meta, null, Flags.JSON_PRETTY_PRINT) - ); + Chart.save(PlayState.SONG, __diff.toLowerCase(), __variant, {saveChart: false, overrideExistingMeta: true, prettyPrint: true}); #else saveMetaAs(shouldBuild); #end @@ -1545,9 +1541,7 @@ class Charter extends UIState { }, null, shouldBuild); } - public static function saveAs(data:Dynamic, ?replacer:(key:Dynamic, value:Dynamic) -> Dynamic, ?space:String, ?options:SaveSubstate.SaveSubstateData, ?saveOptions:Map, shouldBuild:Bool = true) { - FlxG.sound.play(Paths.sound('editors/save')); - if (shouldBuild && instance != null) instance.buildChart(); + public static function saveAs(data:Dynamic, ?replacer:(key:Dynamic, value:Dynamic) -> Dynamic, ?space:String, ?options:SaveSubstate.SaveSubstateData, ?saveOptions:Map, shouldBuild:Bool = true) { if (shouldBuild && instance != null) instance.buildChart(); var cur = FlxG.state; while(true) { if (instance != null || cur.subState == null) return cur.openSubState(new SaveSubstate(Json.stringify(data, replacer, space), options, saveOptions)); @@ -1558,7 +1552,7 @@ class Charter extends UIState { #if sys public static function saveTo(path:String, separateEvents:Bool = false, shouldBuild:Bool = true) { if (shouldBuild && instance != null) instance.buildChart(); - Chart.save(path, PlayState.SONG, __diff.toLowerCase(), {saveMetaInChart: false, saveLocalEvents: !separateEvents, prettyPrint: Options.editorCharterPrettyPrint}); + Chart.save(PlayState.SONG, __diff.toLowerCase(), __variant, {saveMetaInChart: false, saveLocalEvents: !separateEvents, songFolder: path, prettyPrint: Options.editorCharterPrettyPrint}); } #end #end @@ -1570,17 +1564,17 @@ class Charter extends UIState { else {undos = null; FlxG.switchState(new CharterSelection()); Charter.instance.__clearStatics();} } - function _file_save_all(_) saveEverything(); - function _file_save(_) saveChart(); - function _file_saveas(_) saveChartAs(); - function _file_events_save(_) saveEvents(); - function _file_events_saveas(_) saveEventsAs(); - function _file_save_no_events(_) saveChart(true, false); - function _file_saveas_no_events(_) saveChartAs(true, false); - function _file_meta_save(_) saveMeta(); - function _file_meta_saveas(_) saveMetaAs(); - function _file_saveas_fnflegacy(_) saveLegacyChartAs(); - function _file_saveas_psych(_) savePsychChartAs(); + function _file_save_all(_) {saveEverything(); FlxG.sound.play(Paths.sound('editors/save'));} + function _file_save(_) {saveChart(); FlxG.sound.play(Paths.sound('editors/save'));} + function _file_saveas(_) {saveChartAs(); FlxG.sound.play(Paths.sound('editors/save'));} + function _file_events_save(_) {saveEvents(); FlxG.sound.play(Paths.sound('editors/save'));} + function _file_events_saveas(_) {saveEventsAs(); FlxG.sound.play(Paths.sound('editors/save'));} + function _file_save_no_events(_) {saveChart(true, false); FlxG.sound.play(Paths.sound('editors/save'));} + function _file_saveas_no_events(_) {saveChartAs(true, false); FlxG.sound.play(Paths.sound('editors/save'));} + function _file_meta_save(_) {saveMeta(); FlxG.sound.play(Paths.sound('editors/save'));} + function _file_meta_saveas(_) {saveMetaAs(); FlxG.sound.play(Paths.sound('editors/save'));} + function _file_saveas_fnflegacy(_) {saveLegacyChartAs(); FlxG.sound.play(Paths.sound('editors/save'));} + function _file_saveas_psych(_) {savePsychChartAs(); FlxG.sound.play(Paths.sound('editors/save'));} function _edit_copy(_, playSFX=true) { if (playSFX) FlxG.sound.play(Paths.sound(Flags.DEFAULT_EDITOR_COPY_SOUND)); diff --git a/source/funkin/editors/charter/CharterMetaDataScreen.hx b/source/funkin/editors/charter/CharterMetaDataScreen.hx index a86ae09b..cf709ab1 100644 --- a/source/funkin/editors/charter/CharterMetaDataScreen.hx +++ b/source/funkin/editors/charter/CharterMetaDataScreen.hx @@ -150,20 +150,21 @@ class CharterMetaDataScreen extends UISubstateWindow { Reflect.setProperty(customVals, vals.propertyText.label.text, vals.valueText.label.text); } - PlayState.SONG.meta = { - name: songNameTextBox.label.text, - bpm: bpmStepper.value, - needsVoices: needsVoicesCheckbox.checked, - beatsPerMeasure: Std.int(beatsPerMeasureStepper.value), - stepsPerBeat: Std.int(16 / denominatorStepper.value), - displayName: displayNameTextBox.label.text, - icon: iconTextBox.label.text, - color: colorWheel.curColor, - opponentModeAllowed: opponentModeCheckbox.checked, - coopAllowed: coopAllowedCheckbox.checked, - difficulties: [for (diff in difficultiesTextBox.label.text.split(",")) diff.trim()], - customValues: customVals, - }; + var meta = PlayState.SONG.meta; + if (meta == null) meta = {name: songNameTextBox.label.text}; + else meta.name = songNameTextBox.label.text; + + meta.bpm = bpmStepper.value; + meta.needsVoices = needsVoicesCheckbox.checked; + meta.beatsPerMeasure = Std.int(beatsPerMeasureStepper.value); + meta.stepsPerBeat = Std.int(16 / denominatorStepper.value); + meta.displayName = displayNameTextBox.label.text; + meta.icon = iconTextBox.label.text; + meta.color = colorWheel.curColor; + meta.opponentModeAllowed = opponentModeCheckbox.checked; + meta.coopAllowed = coopAllowedCheckbox.checked; + meta.difficulties = [for (diff in difficultiesTextBox.label.text.split(",")) diff.trim()]; + meta.customValues = customVals; Charter.instance.updateBPMEvents(); Charter.instance.vocals.muted = !needsVoicesCheckbox.checked; diff --git a/source/funkin/editors/charter/CharterSelection.hx b/source/funkin/editors/charter/CharterSelection.hx index 85776f44..2b259d94 100644 --- a/source/funkin/editors/charter/CharterSelection.hx +++ b/source/funkin/editors/charter/CharterSelection.hx @@ -25,48 +25,65 @@ class CharterSelectionScreen extends EditorTreeMenuScreen { public var songList:Array = []; public var curSong:ChartMetaData; - inline public function makeChartOption(d:String, name:String):TextOption { - return new TextOption(d, getID('acceptDifficulty'), () -> FlxG.switchState(new Charter(name, d))); + inline public function makeChartOption(d:String, v:String, name:String):TextOption { + return new TextOption(d, getID('acceptDifficulty'), () -> FlxG.switchState(new Charter(name, d, v))); + } + + inline public function makeVariationOption(s:ChartMetaData):TextOption { + return new TextOption(s.variant, getID('acceptVariation'), " >", () -> openSongOption(s, false)); + } + + public function openSongOption(s:ChartMetaData, first = true) { + curSong = s; + + var isVariant = s.variant != null && s.variant != ''; + var screen = new EditorTreeMenuScreen((first || !isVariant) ? (s.name + (isVariant ? ' (${s.variant})' : '')) : s.variant, getID('selectDifficulty')); + + for (d in s.difficulties) if (d != '') screen.add(makeChartOption(d, isVariant ? s.variant : null, s.name)); + screen.add(new Separator()); + for (v in s.variants) if (s.metas.get(v) != null) screen.add(makeVariationOption(s.metas.get(v))); + + #if sys + screen.insert(0, new NewOption(getID('newDifficulty'), getID('newDifficultyDesc'), () -> { + parent.openSubState(new ChartCreationScreen(saveChart)); + })); + + if (!first) screen.curSelected = 1; + else { + cast(screen.members[0], NewOption).itemHeight = 120; + screen.insert(1, new NewOption(getID('newVariation'), getID('newVariationDesc'), () -> { + parent.openSubState(new VariationCreationScreen(s, saveSong)); + })); + screen.curSelected = 2; + } + #end + + parent.addMenu(screen); } public function makeSongOption(s:ChartMetaData):IconOption { songList.push(s.name.toLowerCase()); - var opt = new IconOption(s.name, getID('acceptSong'), s.icon, () -> { - curSong = s; - - var screen = new EditorTreeMenuScreen(s.name, getID('selectDifficulty'), [ - for (d in s.difficulties) if (d != '') makeChartOption(d, s.name) - ]); - - #if sys - screen.insert(0, new NewOption(getID('newDifficulty'), getID('newDifficultyDesc'), () -> { - parent.openSubState(new ChartCreationScreen(saveChart)); - })); - screen.curSelected = 1; - #end - - parent.addMenu(screen); - }); - opt.suffix = ' >'; + var opt = new IconOption(s.name, getID('acceptSong'), s.icon, () -> openSongOption(s, true)); + opt.suffix = " >"; opt.editorFlashColor = s.color.getDefault(FlxColor.WHITE); return opt; } public function new() { - super('editor.chart.name', 'charterSelection.desc', 'charterSelection.', 'newSong', 'newSongDesc', () -> { + super('editor.chart.name', 'charterSelection.desc', 'charterSelection.', 'newSong', 'newSongDesc', #if sys () -> { parent.openSubState(new SongCreationScreen(saveSong)); - }); + } #end); freeplayList = FreeplaySonglist.get(false); for (i => s in freeplayList.songs) add(makeSongOption(s)); } #if sys - public function saveSong(creation:SongCreationData, ?callback:String -> SongCreationData -> Void) { - var songAlreadyExists:Bool = songList.contains(creation.meta.name.toLowerCase()); - if (songAlreadyExists) { + public function saveSong(creation:SongCreationData, ?callback:String -> Void) { + var variant = creation.meta.variant != null && creation.meta.variant != "" ? creation.meta.variant : null; + if (songList.contains(creation.meta.name.toLowerCase()) || (variant != null && curSong != null && curSong.variants.contains(variant))) { parent.openSubState(new UIWarningSubstate(TU.translate("chartCreation.warnings.song-exists-title"), TU.translate("chartCreation.warnings.song-exists-body"), [ {label: TU.translate("editor.ok"), color: 0xFFFF0000, onClick: (t) -> {}}, ])); @@ -74,6 +91,7 @@ class CharterSelectionScreen extends EditorTreeMenuScreen { } var songFolder:String = '${Paths.getAssetsRoot()}/songs/${creation.meta.name}'; + creation.meta = Chart.filterMetaForSaving(creation.meta); #if sys // Make Directories @@ -82,24 +100,32 @@ class CharterSelectionScreen extends EditorTreeMenuScreen { sys.FileSystem.createDirectory('$songFolder/charts'); // Save Files - CoolUtil.safeSaveFile('$songFolder/meta.json', Chart.makeMetaSaveable(creation.meta)); - if (creation.instBytes != null) sys.io.File.saveBytes('$songFolder/song/Inst.${Flags.SOUND_EXT}', creation.instBytes); - if (creation.voicesBytes != null) sys.io.File.saveBytes('$songFolder/song/Voices.${Flags.SOUND_EXT}', creation.voicesBytes); + var instSuffix = creation.meta.instSuffix != null ? creation.meta.instSuffix : '', vocalsSuffix = creation.meta.vocalsSuffix != null ? creation.meta.vocalsSuffix : ''; + CoolUtil.safeSaveFile('$songFolder/meta${variant != null ? "-" + variant : ""}.json', Json.stringify(creation.meta, null, Flags.JSON_PRETTY_PRINT)); + if (creation.instBytes != null) sys.io.File.saveBytes('$songFolder/song/Inst$instSuffix.${Flags.SOUND_EXT}', creation.instBytes); + if (creation.voicesBytes != null) sys.io.File.saveBytes('$songFolder/song/Voices$vocalsSuffix.${Flags.SOUND_EXT}', creation.voicesBytes); - if (creation.playerVocals != null) sys.io.File.saveBytes('$songFolder/song/Voices-Player.${Flags.SOUND_EXT}', creation.playerVocals); - if (creation.oppVocals != null) sys.io.File.saveBytes('$songFolder/song/Voices-Opponent.${Flags.SOUND_EXT}', creation.oppVocals); + if (creation.playerVocals != null) sys.io.File.saveBytes('$songFolder/song/Voices-Player$vocalsSuffix.${Flags.SOUND_EXT}', creation.playerVocals); + if (creation.oppVocals != null) sys.io.File.saveBytes('$songFolder/song/Voices-Opponent$vocalsSuffix.${Flags.SOUND_EXT}', creation.oppVocals); #end - if (callback != null) callback(songFolder, creation); + if (callback != null) callback(songFolder); // Add to List - freeplayList.songs.insert(0, creation.meta); - insert(1, makeSongOption(creation.meta)); + if (variant != null && curSong != null) { + curSong.variants.push(variant); + curSong.metas.set(variant, creation.meta); + + parent.tree.last().add(makeVariationOption(creation.meta)); + } + else { + freeplayList.songs.insert(0, creation.meta); + insert(1, makeSongOption(creation.meta)); + } } public function saveChart(name:String, data:ChartData) { - var difficultyAlreadyExists:Bool = curSong.difficulties.contains(name); - if (difficultyAlreadyExists) { + if (curSong.difficulties.contains(name)) { parent.openSubState(new UIWarningSubstate(TU.translate("chartCreation.warnings.chart-exists-title"), TU.translate("chartCreation.warnings.chart-exists-body"), [ {label: TU.translate("editor.ok"), color: 0xFFFF0000, onClick: (t) -> {}}, ])); @@ -113,11 +139,16 @@ class CharterSelectionScreen extends EditorTreeMenuScreen { // Add to List curSong.difficulties.push(name); - parent.tree.last().insert(parent.tree.last().length - 1, makeChartOption(name, curSong.name)); + + var screen = parent.tree.last(); + var idx = 0; + while (!(screen.members[idx] is Separator)) idx++; + screen.insert(idx, makeChartOption(name, curSong.variant != null && curSong.variant != "" ? curSong.variant : null, curSong.name)); // Add to Meta var meta = Json.parse(sys.io.File.getContent('$songFolder/meta.json')); - if (meta.difficulties != null && !meta.difficulties.contains(name)) { + if (meta.difficulties == null) meta.difficulties = []; + if (!meta.difficulties.contains(name)) { meta.difficulties.push(name); CoolUtil.safeSaveFile('$songFolder/meta.json', Chart.makeMetaSaveable(meta)); } diff --git a/source/funkin/editors/charter/SongCreationScreen.hx b/source/funkin/editors/charter/SongCreationScreen.hx index 5528640d..c6e9a8f3 100644 --- a/source/funkin/editors/charter/SongCreationScreen.hx +++ b/source/funkin/editors/charter/SongCreationScreen.hx @@ -23,7 +23,7 @@ typedef SongCreationData = { } class SongCreationScreen extends UISubstateWindow { - private var onSave:Null -> Null SongCreationData -> Void> -> Void = null; + private var onSave:Null -> Null Void> -> Void = null; public var songNameTextBox:UITextBox; public var bpmStepper:UINumericStepper; @@ -72,7 +72,7 @@ class SongCreationScreen extends UISubstateWindow { public var curPage:Int = 0; - public function new(?onSave:SongCreationData -> Null SongCreationData -> Void> -> Void) { + public function new(?onSave:SongCreationData -> Null Void> -> Void) { super(); if (onSave != null) this.onSave = onSave; } @@ -356,6 +356,12 @@ class SongCreationScreen extends UISubstateWindow { iconSprite.scrollFactor.set(1, 1); } + // for variations + function formatMeta(meta:ChartMetaData):ChartMetaData return meta; + + function getChartSavePath(meta:ChartMetaData, diff:String):String + return 'charts/${meta.variant != null && meta.variant != "" ? meta.variant + "/" : ""}$diff.json'; + function saveSongInfo() { if (isImporting) { @@ -390,20 +396,21 @@ class SongCreationScreen extends UISubstateWindow { codenameChart: true }; PsychParser.parse(oldChart, base); + var meta = formatMeta({ + name: songId, + difficulties: ['normal'], + bpm: oldChart.bpm, + beatsPerMeasure: oldChart.beatsPerMeasure, + stepsPerBeat: oldChart.stepsPerBeat, + displayName: oldChart.song + }); if (onSave != null) onSave({ - meta: { - name: songId, - difficulties: [songId], - bpm: oldChart.bpm, - beatsPerMeasure: oldChart.beatsPerMeasure, - stepsPerBeat: oldChart.stepsPerBeat, - displayName: oldChart.song - }, + meta: meta, instBytes: importInstExplorer.file, voicesBytes: importVoicesExplorer.file, - }, (songFolder:String, creation:SongCreationData) -> { + }, (songFolder:String) -> { #if sys - CoolUtil.safeSaveFile('$songFolder/charts/${songId}.json', Json.stringify(base, Flags.JSON_PRETTY_PRINT)); + CoolUtil.safeSaveFile('$songFolder/${getChartSavePath(meta, "normal")}', Json.stringify(base, Flags.JSON_PRETTY_PRINT)); #end }); } catch (e:haxe.Exception) { @@ -415,7 +422,7 @@ class SongCreationScreen extends UISubstateWindow { for (stepper in [bpmStepper, beatsPerMeasureStepper, denominatorStepper]) @:privateAccess stepper.__onChange(stepper.label.text); - var meta:ChartMetaData = { + var meta:ChartMetaData = formatMeta({ name: songNameTextBox.label.text, bpm: bpmStepper.value, beatsPerMeasure: Std.int(beatsPerMeasureStepper.value), @@ -426,7 +433,7 @@ class SongCreationScreen extends UISubstateWindow { opponentModeAllowed: opponentModeCheckbox.checked, coopAllowed: coopAllowedCheckbox.checked, difficulties: [for (diff in difficultiesTextBox.label.text.split(",")) diff.trim()], - }; + }); if (onSave != null) onSave({ meta: meta, @@ -445,6 +452,7 @@ class SongCreationScreen extends UISubstateWindow { var meta:ChartMetaData = {name: songId}; var diffCharts:Array = []; VSliceParser.parse(vslicemeta, vslicechart, meta, diffCharts, songId); + formatMeta(meta); if (onSave != null) onSave({ meta: meta, @@ -452,9 +460,9 @@ class SongCreationScreen extends UISubstateWindow { voicesBytes: files.get('Voices.${Flags.SOUND_EXT}'), //it may exist playerVocals: files.get('Voices-${playData.characters.playerVocals != null ? playData.characters.playerVocals[0] : playData.characters.player}.${Flags.SOUND_EXT}'), oppVocals: files.get('Voices-${playData.characters.opponentVocals != null ? playData.characters.opponentVocals[0] : playData.characters.opponent}.${Flags.SOUND_EXT}'), - }, (songFolder:String, creation:SongCreationData) -> { + }, (songFolder:String) -> { #if sys - for (diff in diffCharts) CoolUtil.safeSaveFile('$songFolder/charts/${diff.diffName}.json', Json.stringify(diff.chart, Flags.JSON_PRETTY_PRINT)); + for (diff in diffCharts) CoolUtil.safeSaveFile('$songFolder/${getChartSavePath(meta, diff.diffName)}', Json.stringify(diff.chart, Flags.JSON_PRETTY_PRINT)); #end }); } diff --git a/source/funkin/editors/charter/VariationCreationScreen.hx b/source/funkin/editors/charter/VariationCreationScreen.hx new file mode 100644 index 00000000..132bda75 --- /dev/null +++ b/source/funkin/editors/charter/VariationCreationScreen.hx @@ -0,0 +1,44 @@ +package funkin.editors.charter; + +import flixel.text.FlxText.FlxTextFormat; +import flixel.text.FlxText.FlxTextFormatMarkerPair; +import funkin.backend.chart.ChartData; +import funkin.editors.charter.SongCreationScreen; + +class VariationCreationScreen extends SongCreationScreen { + public var parentMeta:ChartMetaData; + + public function new(parent:ChartMetaData, ?onSave:SongCreationData -> Null Void> -> Void) { + super(onSave); + this.parentMeta = parent; + } + + override function create() { + super.create(); + + songNameTextBox.label.text = translateMeta("variation"); + cast(songNameTextBox.members[songNameTextBox.members.length - 1], UIText).text = translateMeta("variation"); + + cast(importIdTextBox.members[importIdTextBox.members.length - 1], UIText).applyMarkup( + translate("variation"), + [new FlxTextFormatMarkerPair(new FlxTextFormat(0xFFAD1212), "$")] + ); + + bpmStepper.value = parentMeta.bpm; + beatsPerMeasureStepper.value = parentMeta.beatsPerMeasure; + denominatorStepper.value = Std.int(16 / parentMeta.stepsPerBeat); + displayNameTextBox.label.text = parentMeta.displayName; + iconTextBox.label.text = parentMeta.icon; + opponentModeCheckbox.checked = parentMeta.opponentModeAllowed; + coopAllowedCheckbox.checked = parentMeta.coopAllowed; + colorWheel.setColor(parentMeta.color); + difficultiesTextBox.label.text = parentMeta.difficulties.join(', '); + } + + override function formatMeta(meta:ChartMetaData):ChartMetaData { + meta.variant = meta.name; + meta.vocalsSuffix = meta.instSuffix = '-${meta.variant}'; + meta.name = parentMeta.name; + return meta; + } +} \ No newline at end of file diff --git a/source/funkin/editors/ui/UIColorwheel.hx b/source/funkin/editors/ui/UIColorwheel.hx index c1ac432f..cc348a0e 100644 --- a/source/funkin/editors/ui/UIColorwheel.hx +++ b/source/funkin/editors/ui/UIColorwheel.hx @@ -31,7 +31,6 @@ class UIColorwheel extends UISliceSprite { hue = curColor.hue; saturation = curColor.saturation; brightness = curColor.brightness; colorPickerShader = new CustomShader("engine/colorPicker"); - colorPickerShader.hset("hue", hue); colorPicker = new FlxSprite(x + 12.5, (y + 125/2) - (100/2)).makeGraphic(100, 100, FlxColor.TRANSPARENT); colorPicker.drawRoundRect(0, 0, 100, 100, 10, 10, FlxColor.WHITE); @@ -91,6 +90,11 @@ class UIColorwheel extends UISliceSprite { updateWheel(false); } + public function setColor(color:FlxColor) { + hue = curColor.hue; saturation = curColor.saturation; brightness = curColor.brightness; + updateWheel(false); + } + inline function updateColorPickerSelector() colorPickerSelector.selector.setPosition(colorPicker.x + (colorPicker.width *saturation) - 8, colorPicker.y + (colorPicker.height + (colorPicker.height * -brightness))- 8); diff --git a/source/funkin/game/PlayState.hx b/source/funkin/game/PlayState.hx index 1457d4ce..9e36b860 100644 --- a/source/funkin/game/PlayState.hx +++ b/source/funkin/game/PlayState.hx @@ -60,11 +60,15 @@ class PlayState extends MusicBeatState /** * The remaining songs in the Story Mode playlist. */ - public static var storyPlaylist:Array = []; + public static var storyPlaylist:Array = []; /** * The selected difficulty name. */ public static var difficulty:String = Flags.DEFAULT_DIFFICULTY; + /** + * The selected variation name. (It can be null) + */ + public static var variation:Null; /** * Whenever the week is coming from the mods folder or not. */ @@ -657,7 +661,7 @@ class PlayState extends MusicBeatState persistentDraw = true; if (SONG == null) - SONG = Chart.parse('tutorial', 'normal'); + SONG = Chart.parse('tutorial', difficulty = 'normal', variation = null); scrollSpeed = SONG.scrollSpeed; @@ -1352,7 +1356,7 @@ class PlayState extends MusicBeatState updateRatingStuff(); if (canAccessDebugMenus && chartingMode && controls.DEV_ACCESS) - FlxG.switchState(new funkin.editors.charter.Charter(SONG.meta.name, difficulty, false)); + FlxG.switchState(new funkin.editors.charter.Charter(SONG.meta.name, difficulty, variation, false)); if (Options.camZoomOnBeat && camZooming) { var beat = Conductor.getBeats(camZoomingEvery, camZoomingInterval, camZoomingOffset); @@ -1697,7 +1701,7 @@ class PlayState extends MusicBeatState if (validScore) { #if !switch - FunkinSave.setSongHighscore(SONG.meta.name, difficulty, { + FunkinSave.setSongHighscore(SONG.meta.name, difficulty, variation, { score: songScore, misses: misses, accuracy: accuracy, @@ -1748,16 +1752,16 @@ class PlayState extends MusicBeatState FlxG.save.flush(); } else { - Logs.infos('Loading next song (${storyPlaylist[0].toLowerCase()}/$difficulty)', "PlayState"); + Logs.infos('Loading next song (${storyPlaylist[0].name.toLowerCase()}/$difficulty/${storyPlaylist[0].variation})', "PlayState"); registerSmoothTransition(); - __loadSong(storyPlaylist[0], difficulty); + __loadSong(storyPlaylist[0].name, difficulty, variation); FlxG.switchState(new PlayState()); } } else if (chartingMode) - FlxG.switchState(new funkin.editors.charter.Charter(SONG.meta.name, difficulty, false)); + FlxG.switchState(new funkin.editors.charter.Charter(SONG.meta.name, difficulty, variation, false)); else FlxG.switchState(new FreeplayState()); } @@ -2131,44 +2135,51 @@ class PlayState extends MusicBeatState public static function loadWeek(weekData:WeekData, ?difficulty:String) { if (difficulty == null) difficulty = Flags.DEFAULT_DIFFICULTY; storyWeek = weekData; - storyPlaylist = [for (e in weekData.songs) e.name]; + storyPlaylist = weekData.songs.copy(); isStoryMode = true; campaignScore = 0; campaignMisses = 0; campaignAccuracyTotal = 0; campaignAccuracyCount = 0; - chartingMode = false; - opponentMode = coopMode = false; - __loadSong(storyPlaylist[0], difficulty); + chartingMode = coopMode = opponentMode = false; + __loadSong(storyPlaylist[0].name, difficulty, storyPlaylist[0].variation); } /** * Loads a song into PlayState * @param name Song name * @param difficulty Chart difficulty (if invalid, will load an empty chart) + * @param variation Song Variation * @param opponentMode Whenever opponent mode is on * @param coopMode Whenever co-op mode is on. */ - public static function loadSong(_name:String, ?_difficulty:String, _opponentMode:Bool = false, _coopMode:Bool = false) { + public static function loadSong(_name:String, ?_difficulty:String, ?_variation:Dynamic, _opponentMode:Bool = false, _coopMode:Bool = false) { if (_difficulty == null) difficulty = Flags.DEFAULT_DIFFICULTY; - isStoryMode = false; + if (_variation is Bool) { // vackward cumpatibshit + _coopMode = _opponentMode; + _opponentMode = cast _variation; + _variation = null; + } + else if (!(_variation is String)) _variation = null; opponentMode = _opponentMode; - chartingMode = false; coopMode = _coopMode; - __loadSong(_name, _difficulty); + isStoryMode = chartingMode = false; + __loadSong(_name, _difficulty, cast _variation); } /** * (INTERNAL) Loads a song without resetting story mode/opponent mode/coop mode values. * @param name Song name - * @param difficulty Song difficulty + * @param difficulty Chart difficulty + * @param variation Song Variation */ - public static function __loadSong(_name:String, _difficulty:String) { + public static function __loadSong(_name:String, _difficulty:String, _variation:String) { difficulty = _difficulty; + variation = _variation; seenCutscene = false; deathCounter = 0; - SONG = Chart.parse(_name, _difficulty); + SONG = Chart.parse(_name, _difficulty, _variation); fromMods = SONG.fromMods; } } diff --git a/source/funkin/menus/FreeplayState.hx b/source/funkin/menus/FreeplayState.hx index e02c34aa..da855156 100644 --- a/source/funkin/menus/FreeplayState.hx +++ b/source/funkin/menus/FreeplayState.hx @@ -25,9 +25,14 @@ class FreeplayState extends MusicBeatState public var curSong:Null; /** - * Current song metadata difficulty + * Current song difficulties */ - public var curSongDifficulty:Null; + public var curDifficulties:Array; + + /** + * songs[curSelected].metas.get(curDiffMetaIndices[curDifficulty]) + */ + public var curDiffMetaKeys:Array = []; /** * Currently selected song @@ -117,10 +122,12 @@ class FreeplayState extends MusicBeatState curSelected = k; } } - updateCurSongMeta(); + + updateCurDifficulties(); + updateCurSong(); if (curSong != null) { - for(k=>diff in songs[curSelected].difficulties) { + for(k=>diff in curDifficulties) { if (diff == Options.freeplayLastDifficulty) { curDifficulty = k; } @@ -257,7 +264,7 @@ class FreeplayState extends MusicBeatState var dontPlaySongThisFrame = false; autoplayElapsed += elapsed; if (!disableAutoPlay && !songInstPlaying && (autoplayElapsed > timeUntilAutoplay)) { - if (curPlayingInst != (curPlayingInst = Paths.inst(curSong.name, curSongDifficulty, curSong.instSuffix))) { + if (curPlayingInst != (curPlayingInst = Paths.inst(curSong.name, curDifficulties[curDifficulty], curSong.instSuffix))) { var streamed = false; if (Options.streamedMusic) { var sound = Assets.getMusic(curPlayingInst, true, false); @@ -330,9 +337,9 @@ class FreeplayState extends MusicBeatState public function select() { updateCoopModes(); - if (songs[curSelected].difficulties.length <= 0) return; + if (curDifficulties.length == 0) return; - var event = event("onSelect", EventManager.get(FreeplaySongSelectEvent).recycle(curSong.name, curSongDifficulty, __opponentMode, __coopMode)); + var event = event("onSelect", EventManager.get(FreeplaySongSelectEvent).recycle(curSong.name, curDifficulties[curDifficulty], curSong.variant, __opponentMode, __coopMode)); if (event.cancelled) return; @@ -341,16 +348,17 @@ class FreeplayState extends MusicBeatState #end Options.freeplayLastSong = curSong.name; - Options.freeplayLastDifficulty = curSongDifficulty; + Options.freeplayLastDifficulty = curDifficulties[curDifficulty]; + Options.freeplayLastVariation = curSong.variant; - PlayState.loadSong(event.song, event.difficulty, event.opponentMode, event.coopMode); + PlayState.loadSong(event.song, event.difficulty, event.variant, event.opponentMode, event.coopMode); FlxG.switchState(new PlayState()); } public function convertChart() { - trace('Converting ${curSong.name} (${curSongDifficulty}) to Codename format...'); - var chart = Chart.parse(curSong.name, curSongDifficulty); - Chart.save('${Main.pathBack}assets/songs/${curSong.name}', chart, curSongDifficulty); + trace('Converting ${curSong.name} ${curDifficulties[curDifficulty]} ${curSong.variant} to Codename format...'); + var chart = Chart.parse(curSong.name, curDifficulties[curDifficulty], curSong.variant); + Chart.save(chart, curDifficulties[curDifficulty], curSong.variant); } /** @@ -358,39 +366,35 @@ class FreeplayState extends MusicBeatState * @param change How much to change. * @param force Force the change if `change` is equal to 0 */ - public function changeDiff(change:Int = 0, force:Bool = false) - { + public function changeDiff(change:Int = 0, force:Bool = false) { if (change == 0 && !force) return; - var song = songs[curSelected]; - var validDifficulties = song.difficulties.length > 0; - var event = event("onChangeDiff", EventManager.get(MenuChangeEvent).recycle(curDifficulty, validDifficulties ? FlxMath.wrap(curDifficulty + change, 0, song.difficulties.length-1) : 0, change)); + var validDifficulties = curDifficulties.length > 0; + var event = event("onChangeDiff", EventManager.get(MenuChangeEvent).recycle(curDifficulty, validDifficulties ? FlxMath.wrap(curDifficulty + change, 0, curDifficulties.length-1) : 0, change)); if (event.cancelled) { - if (force) updateCurSongMeta(); + if (force) updateCurSong(); return; } - var prevInstSuffix = curSong.instSuffix; + var prevSong = curSong; curDifficulty = event.value; - updateCurSongMeta(); + updateCurSong(); updateScore(); #if PRELOAD_ALL - if (curSong.instSuffix != prevInstSuffix) { + if (curSong != prevSong) { autoplayElapsed = 0; songInstPlaying = false; } #end - if (song.difficulties.length > 1) - diffText.text = '< ${song.difficulties[curDifficulty].toUpperCase()} >'; - else - diffText.text = validDifficulties ? song.difficulties[curDifficulty].toUpperCase() : "-"; + var text = validDifficulties ? curDifficulties[curDifficulty].toUpperCase() + (curSong != songs[curSelected] ? ' (${curSong.variant.toUpperCase()})' : '') : '-'; + diffText.text = curDifficulties.length > 1 ? '< $text >' : text; } function updateScore() { - if (songs[curSelected].difficulties.length <= 0) { + if (curDifficulties.length == 0) { intendedScore = 0; return; } @@ -398,7 +402,7 @@ class FreeplayState extends MusicBeatState var changes:Array = []; if (__coopMode) changes.push(CCoopMode); if (__opponentMode) changes.push(COpponentMode); - var saveData = FunkinSave.getSongHighscore(curSong.name, curSongDifficulty, changes); + var saveData = FunkinSave.getSongHighscore(curSong.name, curDifficulties[curDifficulty], curSong.variant, changes); intendedScore = saveData.score; } @@ -448,8 +452,7 @@ class FreeplayState extends MusicBeatState * @param change How much to change * @param force Force the change, even if `change` is equal to 0. */ - public function changeSelection(change:Int = 0, force:Bool = false) - { + public function changeSelection(change:Int = 0, force:Bool = false) { if (change == 0 && !force) return; var event = event("onChangeSelection", EventManager.get(MenuChangeEvent).recycle(curSelected, FlxMath.wrap(curSelected + change, 0, songs.length-1), change)); @@ -458,6 +461,14 @@ class FreeplayState extends MusicBeatState curSelected = event.value; if (event.playMenuSFX) CoolUtil.playMenuSFX(SCROLL, 0.7); + var prevDiff = curDifficulties[curDifficulty], prevVariant = curDiffMetaKeys[curDifficulty]; + updateCurDifficulties(); + + for (i => diff in curDifficulties) if (diff == prevDiff && curDiffMetaKeys[i] == prevVariant) { + curDifficulty = i; + break; + } + changeDiff(0, true); #if PRELOAD_ALL @@ -491,13 +502,22 @@ class FreeplayState extends MusicBeatState } } - function updateCurSongMeta() { - var song = songs[curSelected]; - if (song == null) { - curSong = null; - curSongDifficulty = null; + function updateCurDifficulties() { + curDiffMetaKeys.resize(0); + curDifficulties = songs[curSelected].difficulties.copy(); + for (i in 0...curDifficulties.length) curDiffMetaKeys.push(null); + + var meta:ChartMetaData; + for (variant in songs[curSelected].variants) if ((meta = songs[curSelected].metas.get(variant)) != null) { + curDifficulties = curDifficulties.concat(meta.difficulties); + for (i in 0...meta.difficulties.length) curDiffMetaKeys.push(variant); } - else if ((curSong = song.metas.get(curSongDifficulty = song.difficulties[curDifficulty])) == null) + } + + function updateCurSong() { + var song = songs[curSelected]; + if (song == null) curSong = null; + else if ((curSong = song.metas.get(curDiffMetaKeys[curDifficulty])) == null) curSong = song; } } diff --git a/source/funkin/menus/PauseSubState.hx b/source/funkin/menus/PauseSubState.hx index f790c1eb..d06cf013 100644 --- a/source/funkin/menus/PauseSubState.hx +++ b/source/funkin/menus/PauseSubState.hx @@ -167,7 +167,7 @@ class PauseSubState extends MusicBeatSubstate case "Change Options": FlxG.switchState(new OptionsMenu((_) -> FlxG.switchState(new PlayState()))); case "Exit to charter": - FlxG.switchState(new Charter(PlayState.SONG.meta.name, PlayState.difficulty, false)); + FlxG.switchState(new Charter(PlayState.SONG.meta.name, PlayState.difficulty, PlayState.variation, false)); case "Exit to menu": if (PlayState.chartingMode && Charter.undos.unsaved) game.saveWarn(false); diff --git a/source/funkin/options/Options.hx b/source/funkin/options/Options.hx index c6c61a41..ac5c87ac 100644 --- a/source/funkin/options/Options.hx +++ b/source/funkin/options/Options.hx @@ -73,6 +73,7 @@ class Options */ public static var freeplayLastSong:String = null; public static var freeplayLastDifficulty:String = "normal"; + public static var freeplayLastVariation:String = null; public static var contributors:Array = []; public static var mainDevs:Array = []; // IDs public static var lastUpdated:Null; diff --git a/source/funkin/options/TreeMenuScreen.hx b/source/funkin/options/TreeMenuScreen.hx index 33883fc0..c5b26678 100644 --- a/source/funkin/options/TreeMenuScreen.hx +++ b/source/funkin/options/TreeMenuScreen.hx @@ -128,11 +128,12 @@ class TreeMenuScreen extends FlxSpriteGroup { var i = curSelected, y = initY, object:FlxSprite = null, itemHeight:Float = 0; inline function updateItem() { - object.y = CoolUtil.fpsLerp(object.y, y - (itemHeight = object.height) * 0.5, r); + object.y = CoolUtil.fpsLerp(object.y, y - itemHeight * 0.5, r); object.x = x + 100 - Math.pow(Math.abs((object.y - (FlxG.height - itemHeight) * 0.5) / itemHeight / FlxG.height * FlxG.initialHeight), 1.6) * 15; } while (i < length) if ((object = members[i++]) != null) { + itemHeight = object.height; updateItem(); y += itemHeight; } @@ -140,7 +141,7 @@ class TreeMenuScreen extends FlxSpriteGroup { y = initY; i = curSelected; while (i-- > 0) if ((object = members[i]) != null) { - y -= itemHeight; + y -= (itemHeight = object.height); updateItem(); } } diff --git a/source/funkin/options/type/NewOption.hx b/source/funkin/options/type/NewOption.hx index 85181412..e9d6a00b 100644 --- a/source/funkin/options/type/NewOption.hx +++ b/source/funkin/options/type/NewOption.hx @@ -10,7 +10,7 @@ class NewOption extends TextOption { public function new(name:String, desc:String, callback:Void->Void) { super(name, desc, callback); - itemHeight = 160; + itemHeight = 150; __text.color = FlxColor.LIME; __text.x = 100; diff --git a/source/funkin/savedata/FunkinSave.hx b/source/funkin/savedata/FunkinSave.hx index 5e5ed2eb..5d43a40d 100644 --- a/source/funkin/savedata/FunkinSave.hx +++ b/source/funkin/savedata/FunkinSave.hx @@ -3,25 +3,24 @@ package funkin.savedata; import flixel.util.FlxSave; import lime.app.Application; import openfl.Lib; +import haxe.Serializer; +import haxe.Unserializer; /** * Class used for saves WITHOUT going through the struggle of type checks * Just add your save variables the way you would do in the Options.hx file. * The macro will automatically generate the `flush` and `load` functions. */ -@:build(funkin.backend.system.macros.FunkinSaveMacro.build("save", "flush", "load")) +@:build(funkin.backend.system.macros.FunkinSaveMacro.build("save", "__flush", "__load")) class FunkinSave { - public static var highscores:Map = []; - + @:doNotSave public static var highscores:Map = []; /** * ONLY OPEN IF YOU WANT TO EDIT FUNCTIONS RELATED TO SAVING, LOADING OR HIGHSCORES. */ #if REGION - @:dox(hide) @:doNotSave - private static var __eventAdded = false; - @:doNotSave - public static var save:FlxSave; + @:dox(hide) @:doNotSave private static var __eventAdded = false; + @:doNotSave public static var save:FlxSave; public static function init() { var path = Flags.SAVE_PATH, name = Flags.SAVE_NAME; @@ -46,20 +45,64 @@ class FunkinSave { } } + public static function load() { + __load(); + if (save.data.highscores != null) { + var temp; + for (entryData in Reflect.fields(save.data.highscores)) + if ((temp = __getHighscoreEntry(entryData)) != null && Reflect.field(save.data.highscores, entryData) != null) + highscores.set(temp, Reflect.field(save.data.highscores, entryData)); + } + } + + public static function flush() { + __flush(); + if (save.data.highscores == null) save.data.highscores = {}; + for (entry => score in highscores) Reflect.setField(save.data.highscores, __formatHighscoreEntry(entry), score); + } + + static function __getHighscoreEntry(data:String):HighscoreEntry { + try { + var d = Unserializer.run(data); + if (d.song is String) + return HSongEntry(d.song, d.diff, d.variation, d.changes); + else if (d.week is String) + return HWeekEntry(d.week, d.diff); + } + catch (e) {} + return null; + } + + static function __formatHighscoreEntry(entry:HighscoreEntry):String { + switch (entry) { + case HWeekEntry(weekName, difficulty): + return Serializer.run({week: weekName, diff: difficulty}); + case HSongEntry(songName, difficulty, variation, changes): + var d:Dynamic = { + song: songName, + diff: difficulty, + changes: changes + }; + if (variation != null && variation != '') d.variation = variation; + return Serializer.run(d); + } + return ''; + } + /** * Returns the high-score for a song. * @param name Song name * @param diff Song difficulty * @param changes Changes made to that song in freeplay. */ - public static inline function getSongHighscore(name:String, diff:String, ?changes:Array) { + public static inline function getSongHighscore(name:String, diff:String, ?variation:String, ?changes:Array) { if (changes == null) changes = []; - return safeGetHighscore(HSongEntry(name.toLowerCase(), diff.toLowerCase(), changes)); + return safeGetHighscore(HSongEntry(name.toLowerCase(), diff.toLowerCase(), variation, changes)); } - public static inline function setSongHighscore(name:String, diff:String, highscore:SongScore, ?changes:Array) { + public static inline function setSongHighscore(name:String, diff:String, ?variation:String, highscore:SongScore, ?changes:Array) { if (changes == null) changes = []; - if (safeRegisterHighscore(HSongEntry(name.toLowerCase(), diff.toLowerCase(), changes), highscore)) { + if (safeRegisterHighscore(HSongEntry(name.toLowerCase(), diff.toLowerCase(), variation, changes), highscore)) { flush(); return true; } @@ -103,7 +146,7 @@ class FunkinSave { enum HighscoreEntry { HWeekEntry(weekName:String, difficulty:String); - HSongEntry(songName:String, difficulty:String, changes:Array); + HSongEntry(songName:String, difficulty:String, variation:Null, changes:Array); } enum HighscoreChange {