stepmania importing
fix stops
This commit is contained in:
+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);
|
||||
|
||||
Reference in New Issue
Block a user