-
-
Notifications
You must be signed in to change notification settings - Fork 320
Expand file tree
/
Copy pathChart.hx
More file actions
379 lines (315 loc) · 13.5 KB
/
Chart.hx
File metadata and controls
379 lines (315 loc) · 13.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
package funkin.backend.chart;
import funkin.backend.chart.ChartData;
import flixel.util.FlxColor;
import funkin.backend.chart.ChartData;
import haxe.Json;
import haxe.io.Path;
#if sys
import sys.FileSystem;
#end
using StringTools;
enum abstract ChartFormat(Int) {
var CODENAME = 0;
var LEGACY = 1; // also used by many other engines (old Psych, Kade and more) - Nex
var VSLICE = 2;
var PSYCH_NEW = 3;
@:to public function toString():String {
return switch(cast (this, ChartFormat)) {
case CODENAME: "CODENAME";
case LEGACY: "LEGACY";
case VSLICE: "VSLICE";
case PSYCH_NEW: "PSYCH_NEW";
}
}
public static function fromString(str:String, def:ChartFormat = ChartFormat.LEGACY) {
str = str.toLowerCase();
str = StringTools.replace(str, " ", "");
str = StringTools.replace(str, "_", "");
str = StringTools.replace(str, ".", "");
if(StringTools.startsWith(str, "psychv1") || StringTools.startsWith(str, "psych1"))
return PSYCH_NEW;
return switch(str) {
case "codename" | "codenameengine": CODENAME;
case "newpsych" | "psychnew": PSYCH_NEW;
default: def;
}
}
}
class Chart {
public static final version:String = "1.6.0";
public static function cleanSongData(data:Dynamic):Dynamic {
if (Reflect.hasField(data, "song")) {
var field:Dynamic = Reflect.field(data, "song");
if (field != null && Type.typeof(field) == TObject) // Cant use Reflect.isObject, because it detects strings for some reason
return field;
}
return data;
}
public static function detectChartFormat(data:Dynamic):ChartFormat {
var __temp:Dynamic; // imma reuse this var so the program doesn't have to get values multiple times - Nex
if ((__temp = data.codenameChart) == true || __temp == "true")
return CODENAME;
if (Reflect.hasField(data, "version") && Reflect.hasField(data, "scrollSpeed"))
return VSLICE;
if ((__temp = cleanSongData(data).format) != null && __temp is String && StringTools.startsWith(__temp, "psych_v1"))
return PSYCH_NEW;
return LEGACY;
}
public static function loadEventsJson(songName:String, ?variant:String) {
var variantSuffix = variant != null && variant != "" ? '-$variant' : "";
var path = Paths.file('songs/${songName}/events$variantSuffix.json');
var data:Array<ChartEvent> = null;
if (Assets.exists(path)) {
try {
data = Json.parse(Assets.getText(path)).events;
for (event in data) event.global = true;
} catch(e) Logs.trace('Failed to load song event data for ${songName} ($path): ${Std.string(e)}', ERROR);
}
return data;
}
inline public static function defaultChartMetaFields(data:ChartMetaData):ChartMetaData {
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("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", []);
data.setFieldDefault("metas", []);
return data;
}
public static function loadChartMeta(songName:String, ?variant:String, ?difficulty:String, fromMods:Bool = true, includeMetaVariations = true):ChartMetaData {
var folder = 'songs/$songName', isVariant = false, data:ChartMetaData = null;
var defaultPaths = [Paths.file('$folder/meta-$difficulty.json'), Paths.file('$folder/meta.json')], variantPaths = [];
if (difficulty != null) defaultPaths.unshift(Paths.file('$folder/meta-$difficulty.json'));
if (variant != null && variant != '') {
variantPaths.push(Paths.file('$folder/meta-$variant.json'));
if (difficulty != null) variantPaths.unshift(Paths.file('$folder/meta-$variant-$difficulty.json'));
}
for (path in variantPaths.concat(defaultPaths)) 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);
if (data != null) {
isVariant = variantPaths.contains(path);
break;
}
}
if (data != null) data.name = songName;
else data = {
name: songName,
color: Flags.DEFAULT_COLOR
};
if (isVariant) data.variant = variant;
else data.variant = null;
defaultChartMetaFields(data);
if (data.difficulties.length <= 0) {
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) {
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);
}
if (tempDiffs.length == 3) data.difficulties = tempDiffs;
}
}
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'))) {
var meta = loadChartMeta(songName, variant, fromMods);
if (meta.variant != null) data.metas.set(variant, meta);
}
}
return data;
}
public static function parse(songName:String, ?difficulty:String, ?variant:String):ChartData {
if (difficulty == null) difficulty = Flags.DEFAULT_DIFFICULTY;
var chartPath = Paths.chart(songName, difficulty, variant);
var base:ChartData = {
strumLines: [],
noteTypes: [],
events: [],
meta: {
name: null
},
scrollSpeed: Flags.DEFAULT_SCROLL_SPEED,
stage: Flags.DEFAULT_STAGE,
codenameChart: true,
fromMods: Paths.assetsTree.existsSpecific(chartPath, "TEXT", MODS)
};
var valid:Bool = true, namePrint = '$songName $difficulty' + ((variant != null && variant != '') ? ' ($variant)' : '');
if (!Assets.exists(chartPath)) {
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 $namePrint: ${Std.string(e)}', ERROR, RED);
}
/**
* CHART CONVERSION
*/
#if REGION
if (data != null) switch (detectChartFormat(data)) {
case CODENAME:
// backward compat on events since it caused problems
var eventTypesToString:Map<Int, String> = [
-1 => "HScript Call",
0 => "Unknown",
1 => "Camera Movement",
2 => "BPM Change",
3 => "Alt Animation Toggle",
];
if (data.events == null) data.events = [];
for (event in cast(data.events, Array<Dynamic>)) if (Reflect.hasField(event, "type")) {
if (event.type != null)
event.name = eventTypesToString[event.type];
Reflect.deleteField(event, "type");
}
base = data;
case PSYCH_NEW: PsychParser.parse(data, base);
case VSLICE: Logs.trace("Couldn't parse V-Slice chart because it's not supported on runtime, it MUST be imported and converted in the new song editor window.", ERROR, RED);
case LEGACY: FNFLegacyParser.parse(data, base);
}
#end
var loadedMeta = loadChartMeta(songName, variant, difficulty, 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);
}
base.meta = loadedMeta;
}
/**
* events.json LOADING
*/
#if REGION
var extraEvents:Array<ChartEvent> = loadEventsJson(songName, variant);
if (extraEvents != null) base.events = base.events.concat(extraEvents);
#end
/**
* Set defaults on strum lines
*/
for(strumLine in base.strumLines) {
if(strumLine.keyCount == null)
strumLine.keyCount = 4;
}
return base;
}
public static function addNoteType(chart:ChartData, noteTypeName:String):Int {
switch(noteTypeName.trim()) {
case "Default Note" | null | "":
return 0;
default:
var index = chart.noteTypes.indexOf(noteTypeName);
if (index > -1) return index + 1;
chart.noteTypes.push(noteTypeName);
return chart.noteTypes.length;
}
}
/**
* Saves the chart to the specific song folder path.
* @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(chart:ChartData, ?difficulty:String, ?variant:String, ?saveSettings:ChartSaveSettings):ChartData {
if (difficulty == null) difficulty = Flags.DEFAULT_DIFFICULTY;
if (saveSettings == null) saveSettings = {};
var filteredChart = filterChartForSaving(chart, saveSettings.saveMetaInChart, saveSettings.saveLocalEvents, saveSettings.saveGlobalEvents && saveSettings.seperateGlobalEvents != true);
#if sys
var songPath = saveSettings.songFolder == null ? 'songs/${chart.meta.name}' : saveSettings.songFolder, variantSuffix = variant != null && variant != "" ? '-$variant' : "", difficultySuffix = difficulty != null && difficulty != "" ? '-$difficulty' : "";
var metaPath = 'meta$variantSuffix.json', altMetaPath = 'meta${variantSuffix}${difficultySuffix}.json', prettyPrint = saveSettings.prettyPrint == true ? Flags.JSON_PRETTY_PRINT : null, temp:String;
if ((temp = Paths.assetsTree.getPath('assets/$songPath/$altMetaPath')) != null) { //check for difficulty specific
songPath = temp.substr(0, temp.length - altMetaPath.length - 1);
metaPath = temp;
}
else if ((temp = Paths.assetsTree.getPath('assets/$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';
var chartFolder = saveSettings.folder == null ? ((variant == null || variant == '') ? 'charts' : 'charts/$variant') : saveSettings.folder;
var chartPath = '$songPath/$chartFolder/${difficulty.trim()}.json';
if (saveSettings.saveChart == null || saveSettings.saveChart == true)
CoolUtil.safeSaveFile(chartPath, Json.stringify(filteredChart, null, prettyPrint));
if (saveSettings.overrideExistingMeta || !FileSystem.exists(metaPath))
CoolUtil.safeSaveFile(metaPath, Json.stringify(filterMetaForSaving(chart.meta), null, prettyPrint));
if (saveSettings.seperateGlobalEvents == true) {
var eventsPath = '$songPath/events$variantSuffix.json', events = filterEventsForSaving(chart.events, false, true);
if (events.length != 0) CoolUtil.safeSaveFile(eventsPath, Json.stringify({events: events}, null, prettyPrint));
}
#end
return filteredChart;
}
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
chart.meta = meta;
chart.events = events;
data.meta = saveMetaInChart ? filterMetaForSaving(meta) : null;
data.events = filterEventsForSaving(events, saveLocalEvents, saveGlobalEvents);
data.fromMods = null;
var sortedData:Dynamic = {};
for (f in Reflect.fields(data)) {
var v = Reflect.field(data, f);
if (v != null)
Reflect.setField(sortedData, f, v);
}
return sortedData;
}
public static function filterEventsForSaving(events:Array<ChartEvent>, saveLocalEvents = true, saveGlobalEvents = false):Array<ChartEvent> {
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
Reflect.deleteField(data, "name");
Reflect.deleteField(data, 'parsedColor');
Reflect.deleteField(data, 'metas');
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 ?prettyPrint:Bool;
var ?saveMetaInChart:Bool;
var ?saveLocalEvents:Bool;
var ?saveGlobalEvents:Bool;
var ?saveChart:Bool;
var ?overrideExistingMeta:Bool;
var ?seperateGlobalEvents:Bool;
var ?folder:String;
var ?songFolder:String;
}