Skip to content

Commit 0ff829e

Browse files
committed
Replace meta difficulties with variations.
2 parents 769a33f + 91abb37 commit 0ff829e

File tree

20 files changed

+467
-273
lines changed

20 files changed

+467
-273
lines changed

assets/languages/en/Editors.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,12 +45,15 @@
4545
<group name="CharterSelection" prefix="charterSelection.">
4646
<str id="desc">Select a song to modify the charts from.</str>
4747
<str id="acceptSong">Press ACCEPT to choose a difficulty to edit.</str>
48+
<str id="acceptVariation">Press ACCEPT to choose a song variation to edit.</str>
4849
<str id="acceptDifficulty">Press ACCEPT to edit the chart for the selected difficulty.</str>
4950
<str id="selectDifficulty">Select a difficulty to continue.</str>
5051
<str id="newDifficulty">New Difficulty</str>
5152
<str id="newDifficultyDesc">Press ACCEPT to create a new chart difficulty.</str>
5253
<str id="newSong">New Song</str>
5354
<str id="newSongDesc">Press ACCEPT to create a new song.</str>
55+
<str id="newVariation">New Variation</str>
56+
<str id="newVariationDesc">Press ACCEPT to create a new song variation.</str>
5457
</group>
5558

5659
<group name="ChartCreationScreen" prefix="chartCreation.">
@@ -104,6 +107,7 @@
104107

105108
<str id="songData">Song Data</str>
106109
<str id="songName">Song Name</str>
110+
<str id="variation">Song Variation</str>
107111
<str id="bpm">BPM</str>
108112
<str id="timeSignature">Time Signature</str>
109113
<str id="menusData">Menus Data (Freeplay/Story)</str>

source/funkin/backend/assets/Paths.hx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -128,11 +128,11 @@ class Paths
128128
return scriptPath;
129129
}
130130

131-
static public function chart(song:String, ?difficulty:String):String
131+
static public function chart(song:String, ?difficulty:String, ?variant:String):String
132132
{
133133
difficulty = (difficulty != null ? difficulty : Flags.DEFAULT_DIFFICULTY);
134134

135-
return getPath('songs/$song/charts/$difficulty.json', null);
135+
return getPath('songs/$song/charts/${variant != null ? variant + "/" : ""}$difficulty.json', null);
136136
}
137137

138138
public static function character(character:String):String {

source/funkin/backend/chart/Chart.hx

Lines changed: 109 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -83,77 +83,75 @@ class Chart {
8383
return data;
8484
}
8585

86-
public static function loadChartMeta(songName:String, difficulty:String = '', fromMods:Bool = true, includeMetaDifficulties:Bool = true):ChartMetaData {
87-
var metaPath = Paths.file('songs/${songName}/meta.json');
88-
var metaDiffPath = Paths.file('songs/${songName}/meta-${difficulty}.json');
89-
90-
var data:ChartMetaData = null, fromDifficulty = false;
91-
var fromMods:Bool = fromMods;
92-
for (path in [metaDiffPath, metaPath]) if (Assets.exists(path)) {
86+
public static function loadChartMeta(songName:String, ?variant:String, fromMods:Bool = true, includeMetaVariations = true):ChartMetaData {
87+
var defaultPath = Paths.file('songs/$songName/meta.json'), isVariant = false;
88+
var data:ChartMetaData = null, paths = (variant == null || variant == '') ? [defaultPath] : [Paths.file('songs/$songName/meta-$variant.json'), defaultPath];
89+
for (path in paths) if (Assets.exists(path)) {
9390
fromMods = Paths.assetsTree.existsSpecific(path, "TEXT", MODS);
9491
try {
9592
var tempData = Json.parse(Assets.getText(path));
9693
tempData.color = CoolUtil.getColorFromDynamic(tempData.color).getDefault(Flags.DEFAULT_COLOR);
9794
data = tempData;
98-
} catch(e) Logs.trace('Failed to load song metadata for ${songName} ($path): ${Std.string(e)}', ERROR);
95+
} catch(e) Logs.trace('Failed to load song metadata for $songName ($path): ${Std.string(e)}', ERROR);
96+
9997
if (data != null) {
100-
fromDifficulty = path == metaDiffPath;
98+
isVariant = path != defaultPath;
10199
break;
102100
}
103101
}
104102

105-
if (data == null) {
106-
data = {
107-
name: songName,
108-
bpm: Flags.DEFAULT_BPM
109-
};
110-
}
111-
else {
112-
data.name = songName;
113-
data.setFieldDefault("bpm", Flags.DEFAULT_BPM);
114-
}
103+
if (data != null) data.name = songName;
104+
else data = {
105+
name: songName,
106+
color: Flags.DEFAULT_COLOR
107+
};
108+
109+
if (isVariant) data.variant = variant;
110+
else data.variant = null;
115111

116112
data.setFieldDefault("displayName", data.name);
113+
114+
data.setFieldDefault("bpm", Flags.DEFAULT_BPM);
117115
data.setFieldDefault("beatsPerMeasure", Flags.DEFAULT_BEATS_PER_MEASURE);
118116
data.setFieldDefault("stepsPerBeat", Flags.DEFAULT_STEPS_PER_BEAT);
119117
data.setFieldDefault("icon", Flags.DEFAULT_HEALTH_ICON);
120-
data.setFieldDefault("difficulties", []);
121118
data.setFieldDefault("coopAllowed", Flags.DEFAULT_COOP_ALLOWED);
122119
data.setFieldDefault("opponentModeAllowed", Flags.DEFAULT_OPPONENT_MODE_ALLOWED);
123120
data.setFieldDefault("instSuffix", "");
124121
data.setFieldDefault("vocalsSuffix", "");
125122
data.setFieldDefault("needsVoices", true);
123+
data.setFieldDefault("difficulties", []);
124+
data.setFieldDefault("variants", []);
126125

127126
if (data.difficulties.length <= 0) {
128-
data.difficulties = [for(f in Paths.getFolderContent('songs/${songName}/charts/', false, fromMods ? MODS : SOURCE)) if (Path.extension(f.toUpperCase()) == "JSON") Path.withoutExtension(f)];
129-
var tempDiffs = [];
127+
var path = 'songs/$songName/charts/';
128+
if (isVariant) path += '$variant/';
129+
130+
data.difficulties = [for(f in Paths.getFolderContent(path, false, fromMods ? MODS : SOURCE)) if (Path.extension(f.toUpperCase()) == "JSON") Path.withoutExtension(f)];
130131
if (data.difficulties.length == 3) {
131-
for(d in data.difficulties) {
132-
switch(d.toLowerCase()) {
133-
case "easy": tempDiffs.insert(0, d);
134-
case "normal": tempDiffs.insert(1, d);
135-
case "hard": tempDiffs.insert(2, d);
136-
}
132+
var tempDiffs = [];
133+
for (d in data.difficulties) switch(d.toLowerCase()) {
134+
case "easy": tempDiffs.insert(0, d);
135+
case "normal": tempDiffs.insert(1, d);
136+
case "hard": tempDiffs.insert(2, d);
137137
}
138-
data.difficulties = tempDiffs;
138+
if (tempDiffs.length == 3) data.difficulties = tempDiffs;
139139
}
140140
}
141141

142-
if (includeMetaDifficulties && !fromDifficulty) {
143-
data.metas = [];
144-
for (difficulty in data.difficulties) {
145-
if (!data.metas.exists(difficulty) && Assets.exists(Paths.file('songs/${songName}/meta-${difficulty}.json')))
146-
data.metas.set(difficulty, loadChartMeta(songName, difficulty, fromMods, false));
147-
}
142+
data.metas = [];
143+
if (includeMetaVariations && data.variants.length > 0) for (variant in data.variants) {
144+
if (!data.metas.exists(variant) && Assets.exists(Paths.file('songs/$songName/meta-$variant.json')))
145+
data.metas.set(variant, loadChartMeta(songName, variant, fromMods));
148146
}
149147

150148
return data;
151149
}
152150

153-
public static function parse(songName:String, ?difficulty:String):ChartData {
151+
public static function parse(songName:String, ?difficulty:String, ?variant:String):ChartData {
154152
if (difficulty == null) difficulty = Flags.DEFAULT_DIFFICULTY;
155153

156-
var chartPath = Paths.chart(songName, difficulty);
154+
var chartPath = Paths.chart(songName, difficulty, variant);
157155
var base:ChartData = {
158156
strumLines: [],
159157
noteTypes: [],
@@ -167,15 +165,15 @@ class Chart {
167165
fromMods: Paths.assetsTree.existsSpecific(chartPath, "TEXT", MODS)
168166
};
169167

170-
var valid:Bool = true;
168+
var valid:Bool = true, namePrint = '$songName $difficulty' + ((variant != null && variant != '') ? ' ($variant)' : '');
171169
if (!Assets.exists(chartPath)) {
172-
Logs.error('Chart for song ${songName} ($difficulty) at "$chartPath" was not found.');
170+
Logs.error('Chart for song $namePrint at "$chartPath" was not found.');
173171
valid = false;
174172
}
175173
var data:Dynamic = null;
176174
if (valid) {
177175
try data = Json.parse(Assets.getText(chartPath))
178-
catch(e) Logs.trace('Could not parse chart for song ${songName} ($difficulty): ${Std.string(e)}', ERROR, RED);
176+
catch(e) Logs.trace('Could not parse chart for song $namePrint: ${Std.string(e)}', ERROR, RED);
179177
}
180178

181179
/**
@@ -207,13 +205,12 @@ class Chart {
207205
}
208206
#end
209207

210-
var loadedMeta = loadChartMeta(songName, difficulty, base.fromMods);
208+
var loadedMeta = loadChartMeta(songName, variant, base.fromMods, false);
211209
if (base.meta == null) base.meta = loadedMeta;
212210
else {
213211
for (field in Reflect.fields(base.meta)) {
214212
var f = Reflect.field(base.meta, field);
215-
if (f != null)
216-
Reflect.setField(loadedMeta, field, f);
213+
if (f != null) Reflect.setField(loadedMeta, field, f);
217214
}
218215
base.meta = loadedMeta;
219216
}
@@ -223,8 +220,7 @@ class Chart {
223220
*/
224221
#if REGION
225222
var extraEvents:Array<ChartEvent> = loadEventsJson(songName);
226-
if (extraEvents != null)
227-
base.events = base.events.concat(extraEvents);
223+
if (extraEvents != null) base.events = base.events.concat(extraEvents);
228224
#end
229225

230226
/**
@@ -252,61 +248,60 @@ class Chart {
252248

253249
/**
254250
* Saves the chart to the specific song folder path.
255-
* @param songFolderPath Path to the song folder (ex: `mods/your mod/songs/song/`)
256-
* @param chart Chart to save
257-
* @param difficulty Name of the difficulty
258-
* @param saveSettings
251+
* @param chart Chart to save.
252+
* @param difficulty Name of the difficulty (Optional).
253+
* @param variant Name of the Variant (Optional).
254+
* @param saveSettings (Optional).
259255
* @return Filtered chart used for saving.
260256
*/
261-
public static function save(songFolderPath:String, chart:ChartData, ?difficulty:String, ?saveSettings:ChartSaveSettings):ChartData {
257+
public static function save(chart:ChartData, ?difficulty:String, ?variant:String, ?saveSettings:ChartSaveSettings):ChartData {
262258
if (difficulty == null) difficulty = Flags.DEFAULT_DIFFICULTY;
263259
if (saveSettings == null) saveSettings = {};
264260

265-
if (saveSettings.saveMetaInChart == null) saveSettings.saveMetaInChart = true;
266-
if (saveSettings.saveLocalEvents == null) saveSettings.saveLocalEvents = true;
267-
if (saveSettings.saveGlobalEvents == null) saveSettings.saveGlobalEvents = false;
268-
269-
var filteredChart = filterChartForSaving(chart, saveSettings.saveMetaInChart, saveSettings.saveLocalEvents, saveSettings.saveGlobalEvents);
270-
var meta = filteredChart.meta;
261+
var filteredChart = filterChartForSaving(chart, saveSettings.saveMetaInChart, saveSettings.saveLocalEvents, saveSettings.saveGlobalEvents && saveSettings.seperateGlobalEvents != true);
271262

272263
#if sys
273-
var saveFolder:String = saveSettings.folder == null ? "charts" : saveSettings.folder;
264+
var songPath = saveSettings.songFolder == null ? 'assets/songs/${chart.meta.name}' : saveSettings.songFolder;
265+
var metaPath = 'meta.json', prettyPrint = saveSettings.prettyPrint == true ? Flags.JSON_PRETTY_PRINT : null, temp:String;
266+
if ((temp = Paths.assetsTree.getPath('$songPath/$metaPath')) != null) {
267+
songPath = temp.substr(0, temp.length - metaPath.length - 1);
268+
metaPath = temp;
269+
}
270+
else if (saveSettings.songFolder == null)
271+
metaPath = (songPath = '${Paths.getAssetsRoot()}/$songPath') + '/$metaPath';
272+
273+
var chartFolder = saveSettings.folder == null ? ((variant == null || variant == '') ? 'charts' : 'charts/$variant') : saveSettings.folder;
274+
var chartPath = '$songPath/$chartFolder/${difficulty.trim()}.json';
274275

275-
if (!FileSystem.exists('${songFolderPath}/$saveFolder/'))
276-
FileSystem.createDirectory('${songFolderPath}/$saveFolder/');
276+
if (saveSettings.saveChart == null || saveSettings.saveChart == true)
277+
CoolUtil.safeSaveFile(chartPath, Json.stringify(filteredChart, null, prettyPrint));
277278

278-
var chartPath = '${songFolderPath}/$saveFolder/${difficulty.trim()}.json';
279-
var metaPath = '${songFolderPath}/meta.json';
279+
if (saveSettings.overrideExistingMeta || !FileSystem.exists(metaPath))
280+
CoolUtil.safeSaveFile(metaPath, Json.stringify(filterMetaForSaving(chart.meta), null, prettyPrint));
280281

281-
CoolUtil.safeSaveFile(chartPath, Json.stringify(filteredChart, null, saveSettings.prettyPrint == true ? Flags.JSON_PRETTY_PRINT : null));
282+
if (saveSettings.seperateGlobalEvents == true) {
283+
var eventsPath = '$songPath/events.json', events = filterEventsForSaving(chart.events, false, true);
282284

283-
if (meta != null && (saveSettings.overrideExistingMeta || !FileSystem.exists(metaPath)))
284-
CoolUtil.safeSaveFile(metaPath, makeMetaSaveable(meta));
285+
if (events.length != 0) CoolUtil.safeSaveFile(eventsPath, Json.stringify({events: events}, null, prettyPrint));
286+
else if (FileSystem.exists(eventsPath)) FileSystem.deleteFile(eventsPath);
287+
}
285288
#end
289+
286290
return filteredChart;
287291
}
288292

289-
public static function filterChartForSaving(chart:ChartData, ?saveMetaInChart:Bool, ?saveLocalEvents:Bool, ?saveGlobalEvents:Bool):ChartData {
293+
public static function filterChartForSaving(chart:ChartData, saveMetaInChart = true, ?saveLocalEvents:Bool, ?saveGlobalEvents:Bool):ChartData {
294+
var meta = chart.meta, events = chart.events;
295+
chart.meta = null;
296+
chart.events = null;
297+
290298
var data = Reflect.copy(chart); // make a copy of the chart to leave the OG intact
291-
if (saveMetaInChart != true) {
292-
data.meta = null;
293-
} else {
294-
data.meta = Reflect.copy(chart.meta); // also make a copy of the metadata to leave the OG intact.
295-
if (data.meta != null && Reflect.hasField(data.meta, "parsedColor")) Reflect.deleteField(data.meta, "parsedColor");
296-
}
297299

298-
// in this part abt the events, i gotta account that these booleans can be null - Nex
299-
if (saveLocalEvents != true && saveGlobalEvents != true) data.events = null;
300-
else {
301-
data.events = [];
302-
for (event in chart.events) if ((saveLocalEvents == true && event.global != true) || (saveGlobalEvents == true && event.global == true)) {
303-
var copy = Reflect.copy(event);
304-
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
305-
data.events.push(copy);
306-
}
307-
if (data.events.length == 0) data.events = null;
308-
}
300+
chart.meta = meta;
301+
chart.events = events;
309302

303+
data.meta = saveMetaInChart ? filterMetaForSaving(meta) : null;
304+
data.events = filterEventsForSaving(events, saveLocalEvents, saveGlobalEvents);
310305
data.fromMods = null;
311306

312307
var sortedData:Dynamic = {};
@@ -318,19 +313,46 @@ class Chart {
318313
return sortedData;
319314
}
320315

321-
public static inline function makeMetaSaveable(meta:ChartMetaData, prettyPrint:Bool = true):String {
316+
public static function filterEventsForSaving(events:Array<ChartEvent>, saveLocalEvents = true, saveGlobalEvents = false):Array<ChartEvent> {
317+
var data = [];
318+
if (!saveLocalEvents && !saveGlobalEvents) return data;
319+
320+
for (event in events) if ((saveLocalEvents && event.global != true) || (saveGlobalEvents && event.global == true)) {
321+
var copy = Reflect.copy(event);
322+
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
323+
data.push(copy);
324+
}
325+
326+
return data;
327+
}
328+
329+
public static inline function makeMetaSaveable(meta:ChartMetaData, prettyPrint:Bool = true):String
330+
return Json.stringify(filterMetaForSaving(meta), null, prettyPrint ? Flags.JSON_PRETTY_PRINT : null);
331+
332+
public static inline function filterMetaForSaving(meta:ChartMetaData):ChartMetaData {
322333
var data:Dynamic = Reflect.copy(meta);
323-
if (data.color != null) data.color = FlxColor.fromInt(data.color).toWebString(); // dont even ask me - Nex
334+
if (data.color != null) data.color = FlxColor.fromInt(data.color).toWebString(); // dont even ask me - Nex
335+
Reflect.deleteField(data, 'parsedColor');
324336
Reflect.deleteField(data, 'metas');
325-
return Json.stringify(data, null, prettyPrint ? Flags.JSON_PRETTY_PRINT : null);
337+
Reflect.deleteField(data, "variant");
338+
if (data.instSuffix != null && data.instSuffix == "") Reflect.deleteField(data, "instSuffix");
339+
if (data.vocalsSuffix != null && data.vocalsSuffix == "") Reflect.deleteField(data, "vocalsSuffix");
340+
if (data.variants != null && data.variants.length == 0) Reflect.deleteField(data, "variants");
341+
return data;
326342
}
327343
}
328344

329345
typedef ChartSaveSettings = {
330-
var ?overrideExistingMeta:Bool;
346+
var ?prettyPrint:Bool;
347+
331348
var ?saveMetaInChart:Bool;
332349
var ?saveLocalEvents:Bool;
333350
var ?saveGlobalEvents:Bool;
334-
var ?prettyPrint:Bool;
351+
352+
var ?saveChart:Bool;
353+
var ?overrideExistingMeta:Bool;
354+
var ?seperateGlobalEvents:Bool;
355+
335356
var ?folder:String;
357+
var ?songFolder:String;
336358
}

source/funkin/backend/chart/ChartData.hx

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,23 @@ typedef ChartData = {
1818

1919
typedef ChartMetaData = {
2020
public var name:String;
21-
public var ?bpm:Float;
21+
public var ?variant:String;
2222
public var ?displayName:String;
23+
24+
public var ?bpm:Float;
2325
public var ?beatsPerMeasure:Float;
2426
public var ?stepsPerBeat:Int;
27+
28+
public var ?difficulties:Array<String>;
29+
public var ?variants:Array<String>;
30+
public var ?customValues:Dynamic;
31+
2532
public var ?icon:String;
2633
public var ?color:FlxColor;
27-
public var ?difficulties:Array<String>;
34+
2835
public var ?coopAllowed:Bool;
2936
public var ?opponentModeAllowed:Bool;
30-
public var ?customValues:Dynamic;
37+
3138
public var ?metas:Map<String, ChartMetaData>;
3239
public var ?instSuffix:String;
3340
public var ?vocalsSuffix:String;

source/funkin/backend/scripting/events/menu/freeplay/FreeplaySongSelectEvent.hx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ final class FreeplaySongSelectEvent extends CancellableEvent {
99
* Difficulty name
1010
*/
1111
public var difficulty:String;
12+
/**
13+
* Variation Name
14+
*/
15+
public var variant:String;
1216
/**
1317
* Whenever opponent mode is enabled or not.
1418
*/

0 commit comments

Comments
 (0)