diff --git a/assets b/assets index f2822bdc5..6385196f1 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit f2822bdc528de8b28a3e4f022a3138cc35441903 +Subproject commit 6385196f16365e7bc36c9b5c79601afcb5dcb642 diff --git a/source/funkin/data/song/importer/OsuManiaImporter.hx b/source/funkin/data/song/importer/OsuManiaImporter.hx index acebec05a..b25c53eb4 100644 --- a/source/funkin/data/song/importer/OsuManiaImporter.hx +++ b/source/funkin/data/song/importer/OsuManiaImporter.hx @@ -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; diff --git a/source/funkin/data/song/importer/StepManiaData.hx b/source/funkin/data/song/importer/StepManiaData.hx new file mode 100644 index 000000000..4ce0e115d --- /dev/null +++ b/source/funkin/data/song/importer/StepManiaData.hx @@ -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; + var Stops:Array; + var Difficulties:Array; +} + +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; + + 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; + } +} diff --git a/source/funkin/data/song/importer/StepManiaImporter.hx b/source/funkin/data/song/importer/StepManiaImporter.hx new file mode 100644 index 000000000..a8f894540 --- /dev/null +++ b/source/funkin/data/song/importer/StepManiaImporter.hx @@ -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 = new Map(); + + static function parseMetadataLine(line:String, result:StepManiaData):StepManiaData + { + var parts:Array = 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 + { + // Remove #BPMS: prefix if present + if (StringTools.startsWith(line, "#BPMS:")) line = line.substr(6); + + var parts:Array = line.split(","); + + var stepTimingPoints:Array = []; + 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 + { + // Remove #STOPS: prefix if present + if (StringTools.startsWith(line, "#STOPS:")) line = line.substr(7); + + // Same as timing points + var parts:Array = line.split(","); + var stepStopPoints:Array = []; + 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, measureIndex:Int):Array + { + var lengthInRows:Float = 192.0 / lines.length; + var rowIndex:Int = 0; + var beat:Float = 0; + + var stepNotes:Array = []; + + 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, stepTimingPoints:Array):Array + { + // 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(); + var lines:Array = stepContent.split("\n"); + + var currentMeasure:Int = 0; + var measure:Array = []; + 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 + { + 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, stepStops:Array):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, + stepTimingPoints:Array, stepStops:Array):Array + { + var result:Array = []; + var holdArray:Array = []; + 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 = []; + 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 = new Map(); + var stepNoteMap:Map> = new Map>(); + + 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; + } +} diff --git a/source/funkin/ui/debug/charting/dialogs/ChartEditorWelcomeDialog.hx b/source/funkin/ui/debug/charting/dialogs/ChartEditorWelcomeDialog.hx index b3a4b9aa4..e63e21476 100644 --- a/source/funkin/ui/debug/charting/dialogs/ChartEditorWelcomeDialog.hx +++ b/source/funkin/ui/debug/charting/dialogs/ChartEditorWelcomeDialog.hx @@ -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 diff --git a/source/funkin/ui/debug/charting/handlers/ChartEditorDialogHandler.hx b/source/funkin/ui/debug/charting/handlers/ChartEditorDialogHandler.hx index 690aff944..bb91de2f0 100644 --- a/source/funkin/ui/debug/charting/handlers/ChartEditorDialogHandler.hx +++ b/source/funkin/ui/debug/charting/handlers/ChartEditorDialogHandler.hx @@ -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 = 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 = OsuManiaImporter.parseOsuFile(content);