stepmania importing

fix stops
This commit is contained in:
Kade-gtihub
2025-11-07 21:59:07 -08:00
committed by Kade
parent 0fe226132d
commit e6aaca19f6
6 changed files with 785 additions and 7 deletions
+1 -1
Submodule assets updated: f2822bdc52...6385196f16
@@ -62,8 +62,8 @@ class OsuManiaImporter
}
/**
* @param data The raw parsed JSON data to migrate, as a Dynamic.
* @param difficulty
* @param songData The raw parsed JSON data to migrate, as a Dynamic.
* @param difficulty The difficulty name to assign to the migrated chart.
* @return SongMetadata
*/
public static function migrateMetadata(songData:OsuManiaData, difficulty:String = 'normal'):SongMetadata
@@ -117,6 +117,11 @@ class OsuManiaImporter
return result;
}
/**
* @param songData The raw parsed JSON data to migrate, as a Dynamic.
* @param difficulty The difficulty name to assign to the migrated chart.
* @return SongChartData
*/
public static function migrateChartData(songData:OsuManiaData, difficulty:String = 'normal'):SongChartData
{
trace('Migrating song chart data from Osu!Mania.');
@@ -142,13 +147,15 @@ class OsuManiaImporter
for (hitObject in hitObjects)
{
var wrappedColumn:Int = hitObject.column % (keyCount * 2); // wrap overflow for 9K+
if (keyCount <= 4) // if its 5K+ dont add the copies beatmap into the opponent
if (keyCount <= 4) // if its 4K or less, flip to BF side
{
var noteOffset:Int = Std.int(Math.abs(keyCount - STRUMLINE_SIZE)); // to make it on the opponent strumline when on 3K it has a one note offset
wrappedColumn -= 4;
var noteOffset:Int = Std.int(Math.abs(keyCount - STRUMLINE_SIZE));
var flippedNoteData:Int = wrappedColumn + keyCount + noteOffset;
result.push(new SongNoteData(hitObject.time, flippedNoteData, hitObject.holdDuration ?? 0, ''));
}
result.push(new SongNoteData(hitObject.time, wrappedColumn, hitObject.holdDuration ?? 0, ''));
else
result.push(new SongNoteData(hitObject.time, wrappedColumn, hitObject.holdDuration ?? 0, ''));
}
return result;
@@ -0,0 +1,123 @@
package funkin.data.song.importer;
typedef StepManiaData =
{
var Metadata:
{
var Title:String;
var Artist:String;
var Genre:String;
var Credit:String;
var Banner:String;
var Background:String;
var Offset:Float;
var SampleStart:Float;
};
var TimingPoints:Array<StepTimingPoint>;
var Stops:Array<StepStop>;
var Difficulties:Array<StepDifficulty>;
}
enum StepManiaChartType {
DanceSingle;
DanceDouble;
Unknown;
}
enum StepManiaNoteType {
Tap;
Head;
Tail;
Roll;
Mine;
Fake;
}
class StepNote
{
public var beat:Float;
public var column:Int;
public var type:StepManiaNoteType;
public function new(t:String, beat:Float, column:Int)
{
this.beat = beat;
this.column = column;
switch (t) {
case "2":
this.type = StepManiaNoteType.Head;
case "3":
this.type = StepManiaNoteType.Tail;
case "4":
this.type = StepManiaNoteType.Roll;
case "M":
this.type = StepManiaNoteType.Mine;
case "F":
this.type = StepManiaNoteType.Fake;
default:
this.type = StepManiaNoteType.Tap;
}
}
}
class StepDifficulty
{
public var name:String;
public var charter:String;
public var difficultyRating:Int;
public var type:StepManiaChartType;
public var notes:Array<StepNote>;
public function parseChartType(chartTypeStr:String):StepManiaChartType
{
switch (chartTypeStr) {
case "dance-single":
return StepManiaChartType.DanceSingle;
case "dance-double":
return StepManiaChartType.DanceDouble;
default:
return StepManiaChartType.Unknown;
}
}
public function new(name:String, charter:String, difficultyRating:Int, type:String)
{
this.name = name;
this.charter = charter;
this.difficultyRating = difficultyRating;
this.type = parseChartType(type);
}
}
class StepTimingPoint
{
public var bpm:Float;
public var startBeat:Float = Math.NEGATIVE_INFINITY;
public var endBeat:Float = Math.POSITIVE_INFINITY;
public var startTimestamp:Float = 0;
public var endTimestamp:Float = 0;
public function new(bpm:Float, startBeat:Float)
{
this.bpm = bpm;
this.startBeat = startBeat;
}
}
// Not implemented, but if any chart uses them then the chart will break.
// IE this messes with the timing of the notes.
class StepStop
{
public var startBeat:Float = Math.NEGATIVE_INFINITY;
public var duration:Float = 0;
public var startTimestamp:Float = Math.NEGATIVE_INFINITY;
public function new(startBeat:Float, duration:Float)
{
this.startBeat = startBeat;
this.duration = duration;
}
}
@@ -0,0 +1,613 @@
package funkin.data.song.importer;
import funkin.data.song.SongData.SongMetadata;
import funkin.data.song.SongData.SongChartData;
import funkin.data.song.SongData.SongCharacterData;
import funkin.data.song.SongData.SongNoteData;
import funkin.data.song.SongData.SongTimeChange;
import funkin.data.song.importer.StepManiaData.StepTimingPoint;
import funkin.data.song.importer.StepManiaData.StepDifficulty;
import funkin.data.song.importer.StepManiaData.StepManiaChartType;
import funkin.data.song.importer.StepManiaData.StepNote;
import funkin.data.song.importer.StepManiaData.StepStop;
import funkin.data.song.importer.StepManiaData.StepManiaNoteType;
enum StepStateEnum
{
Metadata;
TimingPoints;
Stops;
Notes;
}
class StepManiaImporter
{
private static var readDiffMetadata:Map<String, String> = new Map<String, String>();
static function parseMetadataLine(line:String, result:StepManiaData):StepManiaData
{
var parts:Array<String> = line.split(":");
if (parts.length != 2) return result;
var key:String = StringTools.trim(parts[0]);
var value:String = StringTools.trim(parts[1]);
// remove trailing ; on value
if (StringTools.endsWith(value, ";")) value = value.substr(0, value.length - 1);
switch (key)
{
case "TITLE":
result.Metadata.Title = value;
case "ARTIST":
result.Metadata.Artist = value;
case "GENRE":
result.Metadata.Genre = value;
case "CREDIT":
if (result.Metadata.Credit == "") result.Metadata.Credit = value;
else // .ssc
readDiffMetadata.set("CREDIT", value);
case "BANNER":
result.Metadata.Banner = value;
case "BACKGROUND":
result.Metadata.Background = value;
case "OFFSET":
result.Metadata.Offset = Std.parseFloat(value);
case "SAMPLESTART":
result.Metadata.SampleStart = Std.parseFloat(value);
// .ssc
case "STEPSTYPE":
readDiffMetadata.set("STEPSTYPE", value);
case "DESCRIPTION":
readDiffMetadata.set("DESCRIPTION", value);
case "DIFFICULTY":
readDiffMetadata.set("DIFFICULTY", value);
}
return result;
}
static function parseTimingPointLine(line:String):Array<StepTimingPoint>
{
// Remove #BPMS: prefix if present
if (StringTools.startsWith(line, "#BPMS:")) line = line.substr(6);
var parts:Array<String> = line.split(",");
var stepTimingPoints:Array<StepTimingPoint> = [];
for (i in 0...parts.length)
{
// Split the =
var split = parts[i].split("=");
if (split.length != 2) continue;
var beat:Float = Std.parseFloat(StringTools.trim(split[0]));
// Check if split[i + 1] has ; and remove it
var bpmSplit = split[1];
if (StringTools.endsWith(bpmSplit, ";")) bpmSplit = bpmSplit.substr(0, bpmSplit.length - 1);
var tp:StepTimingPoint = new StepTimingPoint(Std.parseFloat(bpmSplit), beat);
stepTimingPoints.push(tp);
}
return stepTimingPoints;
}
static function parseStopsLine(line:String):Array<StepStop>
{
// Remove #STOPS: prefix if present
if (StringTools.startsWith(line, "#STOPS:")) line = line.substr(7);
// Same as timing points
var parts:Array<String> = line.split(",");
var stepStopPoints:Array<StepStop> = [];
for (i in 0...parts.length)
{
// Split the =
var split = parts[i].split("=");
if (split.length != 2) continue;
var beat:Float = Std.parseFloat(StringTools.trim(split[0]));
// Check if split[i + 1] has ; and remove it
var durSplit = split[1];
if (StringTools.endsWith(durSplit, ";")) durSplit = durSplit.substr(0, durSplit.length - 1);
var tp:StepStop = new StepStop(beat, Std.parseFloat(durSplit));
stepStopPoints.push(tp);
}
return stepStopPoints;
}
static function parseMeasure(lines:Array<String>, measureIndex:Int):Array<StepNote>
{
var lengthInRows:Float = 192.0 / lines.length;
var rowIndex:Int = 0;
var beat:Float = 0;
var stepNotes:Array<StepNote> = [];
for (i in 0...lines.length)
{
var stepNoteRow:Float = measureIndex * 192 + (lengthInRows * rowIndex);
beat = stepNoteRow / 48.0; // 48 rows per beat
for (j in 0...lines[i].length)
{
var char:String = lines[i].charAt(j);
if (char != "0")
{
var stepNote:StepNote = new StepNote(char, beat, j);
stepNotes.push(stepNote);
}
}
rowIndex++;
}
return stepNotes;
}
static function synchronizeStepTimingPoints(stepStopPoints:Array<StepStop>, stepTimingPoints:Array<StepTimingPoint>):Array<StepTimingPoint>
{
// Initialize startTimestamp/endTimestamp and endBeat for timing points
for (tpIndex in 0...stepTimingPoints.length)
{
var tp = stepTimingPoints[tpIndex];
// ensure fields exist
if (tp.startTimestamp == Math.NEGATIVE_INFINITY) tp.startTimestamp = 0.0;
}
var tpIndex:Int = 1;
var spIndex:Int = 0;
// if we have more than one timing point or any StepStops, synchronize
if (stepTimingPoints.length > 1 || stepStopPoints.length > 0)
{
if (stepStopPoints.length > 1)
{
while (tpIndex < stepTimingPoints.length || spIndex < stepStopPoints.length)
{
var prevTp:StepTimingPoint = stepTimingPoints[tpIndex - 1];
if (tpIndex == stepTimingPoints.length)
{
var cts:Float = 0;
while (spIndex < stepStopPoints.length)
{
var sp = stepStopPoints[spIndex];
sp.startTimestamp = (prevTp.startTimestamp + (sp.startBeat - prevTp.startBeat) / (prevTp.bpm / 60)) + cts;
cts += sp.duration;
spIndex++;
}
continue;
}
if (spIndex == stepStopPoints.length)
{
while (tpIndex < stepTimingPoints.length)
{
var tp = stepTimingPoints[tpIndex];
prevTp = stepTimingPoints[tpIndex - 1];
prevTp.endBeat = tp.startBeat;
prevTp.endTimestamp += (prevTp.endBeat - prevTp.startBeat) / (prevTp.bpm / 60);
tp.startTimestamp = prevTp.endTimestamp;
tp.endTimestamp = tp.startTimestamp;
tpIndex++;
if (tpIndex == stepTimingPoints.length)
{
tp.endTimestamp = Math.POSITIVE_INFINITY;
tp.endBeat = Math.POSITIVE_INFINITY;
}
}
continue;
}
var tp:StepTimingPoint = stepTimingPoints[tpIndex];
var sp:StepStop = stepStopPoints[spIndex];
prevTp.endBeat = tp.startBeat;
if (sp.startBeat < prevTp.endBeat)
{
sp.startTimestamp = prevTp.endTimestamp + (sp.startBeat - prevTp.startBeat) / (prevTp.bpm / 60);
prevTp.endTimestamp += sp.duration;
spIndex++;
}
else
{
prevTp.endTimestamp += (prevTp.endBeat - prevTp.startBeat) / (prevTp.bpm / 60);
tp.startTimestamp = prevTp.endTimestamp;
tp.endTimestamp = tp.startTimestamp;
tpIndex++;
}
}
}
else
{
while (tpIndex < stepTimingPoints.length)
{
var prevTp:StepTimingPoint = stepTimingPoints[tpIndex - 1];
var tp:StepTimingPoint = stepTimingPoints[tpIndex];
prevTp.endBeat = tp.startBeat;
prevTp.endTimestamp = prevTp.startTimestamp + (prevTp.endBeat - prevTp.startBeat) / (prevTp.bpm / 60);
tp.startTimestamp = prevTp.endTimestamp;
tpIndex++;
if (tpIndex == stepTimingPoints.length)
{
tp.endTimestamp = Math.POSITIVE_INFINITY;
tp.endBeat = Math.POSITIVE_INFINITY;
}
}
}
}
return stepTimingPoints;
}
static function pushWorking(workingDiff:StepDifficulty, result:StepManiaData):StepManiaData
{
if (workingDiff != null && workingDiff.notes.length > 0)
{
// Apply .ssc metadata if any
for (metaKey in readDiffMetadata.keys())
{
var metaValue = readDiffMetadata.get(metaKey);
switch (metaKey)
{
case "STEPSTYPE":
workingDiff.type = workingDiff.parseChartType(metaValue);
case "DIFFICULTY":
workingDiff.name = metaValue;
case "CREDIT":
workingDiff.charter = metaValue;
// DESCRIPTION is ignored for now
}
}
result.Difficulties.push(workingDiff);
}
readDiffMetadata.clear();
return result;
}
static function parseBPMS(line:String, result:StepManiaData):StepManiaData
{
var tps = parseTimingPointLine(line);
if (tps != null)
{
for (tp in tps)
result.TimingPoints.push(tp);
}
return result;
}
static function parseStops(line:String, result:StepManiaData):StepManiaData
{
var sps = parseStopsLine(line);
if (sps != null)
{
for (sp in sps)
result.Stops.push(sp);
}
return result;
}
/**
* Parses a StepMania file content into StepManiaData structure.
* @param stepContent The content of the StepMania file as a string.
* @return StepManiaData The parsed StepMania data.
*/
public static function parseStepManiaFile(stepContent:String):StepManiaData
{
readDiffMetadata = new Map<String, String>();
var lines:Array<String> = stepContent.split("\n");
var currentMeasure:Int = 0;
var measure:Array<String> = [];
var workingDiff:StepDifficulty = null;
var state:StepStateEnum = StepStateEnum.Metadata;
var headerLines:Int = 0;
var result:StepManiaData = {
Metadata: {
Title: "",
Artist: "",
Genre: "",
Credit: "",
Banner: "",
Background: "",
Offset: 0,
SampleStart: 0
},
TimingPoints: [],
Stops: [],
Difficulties: []
};
// Parsing metadata
for (line in lines)
{
line = StringTools.trim(line);
if (line == "") continue;
if (StringTools.startsWith(line, "//")) continue; // Comment line
switch (state)
{
case StepStateEnum.Metadata:
if (StringTools.startsWith(line, "#BPMS:"))
{
state = StepStateEnum.TimingPoints;
result = parseBPMS(line, result);
if (StringTools.endsWith(line, ";")) state = StepStateEnum.Metadata;
}
else if (StringTools.startsWith(line, "#STOPS:"))
{
state = StepStateEnum.Stops;
result = parseStops(line, result);
if (StringTools.endsWith(line, ";")) state = StepStateEnum.Metadata;
}
else if (StringTools.startsWith(line, "#NOTES:"))
{
if (workingDiff != null)
{
// Save previous difficulty
result = pushWorking(workingDiff, result);
}
// Start new difficulty
workingDiff = new StepDifficulty("", "", 0, "");
workingDiff.notes = [];
headerLines = 0;
currentMeasure = 0;
measure = [];
state = StepStateEnum.Notes;
}
else if (StringTools.startsWith(line, "#")) result = parseMetadataLine(line.substr(1), result);
case StepStateEnum.TimingPoints:
if (line == ";") state = StepStateEnum.Metadata;
else
{
result = parseBPMS(line, result);
if (StringTools.endsWith(line, ";")) state = StepStateEnum.Metadata;
}
case StepStateEnum.Stops:
if (line == ";") state = StepStateEnum.Metadata;
else
{
result = parseStops(line, result);
if (StringTools.endsWith(line, ";")) state = StepStateEnum.Metadata;
}
case StepStateEnum.Notes:
if (line == "#NOTES:") continue;
// remove comments in line (if any) ie, "0000 // some comment"
var commentIndex = line.indexOf("//");
if (commentIndex != -1) line = StringTools.trim(line.substr(0, commentIndex));
if (StringTools.contains(line, ":")) // header
{
switch (headerLines)
{
case 0:
// First line is chart type
var chartTypeStr = StringTools.trim(line).replace(":", "");
workingDiff.type = workingDiff.parseChartType(chartTypeStr);
case 1:
// Second line is charter
workingDiff.charter = StringTools.trim(line).replace(":", "");
case 2:
// Third line is difficulty name
workingDiff.name = StringTools.trim(line).replace(":", "");
case 3:
// Fourth line is difficulty rating
workingDiff.difficultyRating = Std.parseInt(StringTools.trim(line).replace(":", ""));
// we dont care about the rest lol
}
headerLines++;
continue;
}
// we're reading StepNote data now
// start gathering measures until we hit a ,
if (line == "," || line == ";")
{
// end of measure
var stepNotesInMeasure = parseMeasure(measure, currentMeasure);
for (stepNote in stepNotesInMeasure)
workingDiff.notes.push(stepNote);
measure = [];
currentMeasure++;
// end of StepNotes section
if (line == ";") state = StepStateEnum.Metadata;
}
else
{
measure.push(line);
}
}
}
if (workingDiff != null && workingDiff.notes.length > 0)
{
// Save last difficulty
result = pushWorking(workingDiff, result);
}
// Syncronize timing points with StepStops
result.TimingPoints = synchronizeStepTimingPoints(result.Stops, result.TimingPoints);
return result;
}
static function getStepStopAtBeat(beat:Float, stepStops:Array<StepStop>):StepStop
{
var stepStop:StepStop = null;
for (s in stepStops)
{
if (s.startBeat < beat) stepStop = s;
}
return stepStop;
}
static function beatToTime(beat:Float, offset:Float, stepTimingPoints:Array<StepTimingPoint>, stepStops:Array<StepStop>):Float
{
var time:Float = 0;
for (tp in stepTimingPoints)
{
if (tp.startBeat <= beat && tp.endBeat > beat)
{
var stepStop:StepStop = getStepStopAtBeat(beat, stepStops);
var b:Float = tp.startBeat;
var startTime:Float = tp.startTimestamp;
if (stepStop != null)
{
if (stepStop.startBeat > tp.startBeat)
{
b = stepStop.startBeat;
startTime = stepStop.startTimestamp + stepStop.duration;
}
}
var nb:Float = (beat - b) / (tp.bpm / 60);
time = startTime + nb;
}
}
return (time * 1000) - (offset * 1000); // convert to ms
}
static function convertStepNotes(offset:Float, type:StepManiaChartType, stepNotes:Array<StepNote>,
stepTimingPoints:Array<StepTimingPoint>, stepStops:Array<StepStop>):Array<SongNoteData>
{
var result:Array<SongNoteData> = [];
var holdArray:Array<Float> = [];
if (type == StepManiaChartType.DanceSingle)
{
holdArray = [-1, -1, -1, -1];
}
else if (type == StepManiaChartType.DanceDouble)
{
holdArray = [-1, -1, -1, -1, -1, -1, -1, -1];
}
else
{
trace("[WARN] Unknown StepMania chart type when converting notes.");
return result;
}
for (stepNote in stepNotes)
{
if (stepNote.type == StepManiaNoteType.Mine || stepNote.type == StepManiaNoteType.Fake) continue;
var time = beatToTime(stepNote.beat, offset, stepTimingPoints, stepStops);
if (stepNote.type == StepManiaNoteType.Head || stepNote.type == StepManiaNoteType.Roll)
{
holdArray[stepNote.column] = time;
continue;
}
var snd:SongNoteData = new SongNoteData(time, stepNote.column, 0);
if (stepNote.type == StepManiaNoteType.Tail)
{
var length:Float = 0;
if (holdArray[stepNote.column] != -1) length = time - holdArray[stepNote.column];
snd.time = holdArray[stepNote.column];
snd.length = length;
holdArray[stepNote.column] = -1;
}
if (type != StepManiaChartType.DanceSingle)
{
if (stepNote.column < 4) snd.data = stepNote.column + 4;
else
snd.data = stepNote.column - 4;
}
else
{
if (snd.data >= 4) snd.data -= 4;
}
result.push(snd);
}
return result;
}
/**
* Migrates StepManiaData to SongMetadata.
* @param songData The StepManiaData to migrate.
* @return SongMetadata The migrated SongMetadata.
*/
public static function migrateChartMetadata(songData:StepManiaData):SongMetadata
{
var metadata:SongMetadata = new SongMetadata(songData.Metadata.Title, songData.Metadata.Artist);
metadata.playData.stage = 'mainStage';
metadata.playData.characters = new SongCharacterData('bf', 'gf', 'dad');
metadata.generatedBy = 'Chart Editor Import (StepMania)';
metadata.playData.songVariations = [];
// Difficulties
var difficulties:Array<String> = [];
for (diff in songData.Difficulties)
{
if (diff.type == StepManiaChartType.Unknown)
{
trace("[WARN] Skipping unknown StepMania chart type. Name: " + diff.name);
continue; // skip unknown chart types
}
difficulties.push(diff.name);
metadata.playData.ratings.set(diff.name, diff.difficultyRating);
}
metadata.playData.difficulties = difficulties;
metadata.charter = songData.Metadata.Credit != "" ? songData.Metadata.Credit : null;
// TimeChanges
metadata.timeChanges = [];
for (tp in songData.TimingPoints)
{
var timeChange = new SongTimeChange(tp.startTimestamp * 1000, tp.bpm);
timeChange.beatTime = tp.startBeat;
metadata.timeChanges.push(timeChange);
}
return metadata;
}
/**
* Migrates StepManiaData to SongChartData.
* @param songData The StepManiaData to migrate.
* @return SongChartData The migrated SongChartData.
*/
public static function migrateChartData(songData:StepManiaData):SongChartData {
var scrollsMap:Map<String, Float> = new Map<String, Float>();
var stepNoteMap:Map<String, Array<SongNoteData>> = new Map<String, Array<SongNoteData>>();
for (diff in songData.Difficulties)
{
if (diff.type == StepManiaChartType.Unknown)
{
trace("[WARN] Skipping unknown StepMania chart type. Name: " + diff.name);
continue; // skip unknown chart types
}
scrollsMap.set(diff.name, Constants.DEFAULT_SCROLLSPEED);
stepNoteMap.set(diff.name, convertStepNotes(songData.Metadata.Offset, diff.type, diff.notes, songData.TimingPoints, songData.Stops));
}
var songChartData:SongChartData = new SongChartData(scrollsMap, [], stepNoteMap);
return songChartData;
}
}
@@ -35,6 +35,7 @@ class ChartEditorWelcomeDialog extends ChartEditorBaseDialog
this.splashCreateFromSongBasicErect.onClick = _ -> onClickLinkCreateBasicErect();
this.splashImportChartLegacy.onClick = _ -> onClickLinkImportChartLegacy();
this.splashImportChartOsuMania.onClick = _ -> onClickLinkImportOsuMania();
this.splashImportChartStepMania.onClick = _ -> onClickLinkImportStepMania();
// Add items to the Recent Charts list
#if sys
@@ -256,5 +257,17 @@ class ChartEditorWelcomeDialog extends ChartEditorBaseDialog
// Open the "Import Chart" dialog
chartEditorState.openImportChartWizard('osumania', false);
}
/**
* Called when the user clicks the "Import Chart: StepMania" link in the dialog.
* Reassign this function to change the behavior.
*/
public function onClickLinkImportStepMania():Void
{
// Hide the welcome dialog
this.hideDialog(DialogButton.CANCEL);
// Open the "Import Chart" dialog
chartEditorState.openImportChartWizard('stepmania', false);
}
}
#end
@@ -6,6 +6,8 @@ import funkin.data.song.importer.FNFLegacyData;
import funkin.data.song.importer.FNFLegacyImporter;
import funkin.data.song.importer.OsuManiaData;
import funkin.data.song.importer.OsuManiaImporter;
import funkin.data.song.importer.StepManiaData;
import funkin.data.song.importer.StepManiaImporter;
import funkin.data.song.SongData.SongCharacterData;
import funkin.data.song.SongData.SongChartData;
import funkin.data.song.SongData.SongMetadata;
@@ -1059,6 +1061,7 @@ class ChartEditorDialogHandler
var prettyFormat:String = switch (format)
{
case 'legacy': 'FNF Legacy';
case 'stepmania': 'StepMania';
case 'osumania': 'Osu!Mania';
default: 'Unknown';
}
@@ -1068,6 +1071,9 @@ class ChartEditorDialogHandler
case 'legacy':
[
{label: 'JSON Data File (.json)', extension: 'json'}];
case 'stepmania':
[
{label: 'StepMania File (.sm)', extension: 'sm'}];
case 'osumania':
[
{label: 'OSU! Beatmap File (.osu)', extension: 'osu'}];
@@ -1078,7 +1084,8 @@ class ChartEditorDialogHandler
{
case 'osumania':
"osu";
case 'stepmania':
"sm";
default: "json";
}
@@ -1147,6 +1154,21 @@ class ChartEditorDialogHandler
songChartData = FNFLegacyImporter.migrateChartData(fnfLegacyData);
loadedText = 'Loaded chart file';
case 'stepmania':
var stepmaniaData:Null<StepManiaData> = StepManiaImporter.parseStepManiaFile(content);
if (stepmaniaData == null)
{
state.error('Failure', 'Failed to parse StepMania step file (${path.file}.${path.ext})');
return;
}
trace('Parsed StepMania data for ' + stepmaniaData.Metadata.Title);
songMetadata = StepManiaImporter.migrateChartMetadata(stepmaniaData);
songChartData = StepManiaImporter.migrateChartData(stepmaniaData);
loadedText = 'Loaded step file';
case 'osumania':
var osuManiaData:Null<OsuManiaData> = OsuManiaImporter.parseOsuFile(content);