Files
Funkin/source/funkin/data/song/importer/OsuManiaImporter.hx
T
2026-04-06 04:18:09 -06:00

224 lines
7.4 KiB
Haxe

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.OsuManiaData;
import funkin.data.song.importer.OsuManiaData.TimingPoint;
import funkin.data.song.importer.OsuManiaData.ManiaHitObject;
class OsuManiaImporter
{
public static function parseOsuFile(osuContent:String):OsuManiaData
{
var lines:Array<String> = osuContent.split("\n");
var result:Dynamic = {};
var currentSection:String = null;
var nonCSVLikeSections = ["General", "Editor", "Metadata", "Difficulty"];
for (line in lines)
{
line = StringTools.trim(line);
if (line == "" || StringTools.startsWith(line, "//")) continue;
// Section header like [General]
var sectionRegex = ~/^\[(.+)\]$/;
if (sectionRegex.match(line))
{
currentSection = sectionRegex.matched(1);
if (nonCSVLikeSections.contains(currentSection))
{
Reflect.setField(result, currentSection, {
});
}
else
{
Reflect.setField(result, currentSection, []);
}
continue;
}
// Key-value pairs (INI style)
if (currentSection != null && nonCSVLikeSections.contains(currentSection))
{
var parts:Array<String> = line.split(":");
var key:String = StringTools.trim(parts.shift());
var value:Any = parseValue(StringTools.trim(parts.join(":")));
if (Reflect.field(result, currentSection) == null) Reflect.setField(result, currentSection, {
});
Reflect.setField(Reflect.field(result, currentSection), key, value);
}
// For CSV-like sections
else if (currentSection != null)
{
var theArray:Array<String> = cast Reflect.field(result, currentSection);
theArray.push(line);
Reflect.setField(result, currentSection, theArray);
}
}
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 SongMetadata
*/
public static function migrateMetadata(songData:OsuManiaData, difficulty:String = 'normal'):SongMetadata
{
trace('Migrating song metadata from Osu!Mania.');
var songMetadata:SongMetadata = new SongMetadata('Import', songData.Metadata.ArtistUnicode ?? songData.Metadata.Artist ?? Constants.DEFAULT_ARTIST,
songData.Metadata.Creator ?? Constants.DEFAULT_CHARTER, Constants.DEFAULT_VARIATION);
// Set generatedBy string for debugging.
songMetadata.generatedBy = 'Chart Editor Import (Osu!Mania)';
songMetadata.playData.stage = 'mainStage';
songMetadata.songName = songData.Metadata.TitleUnicode ?? songData.Metadata.Title ?? 'Import';
songMetadata.playData.difficulties = [difficulty];
songMetadata.playData.songVariations = [];
songMetadata.timeChanges = rebuildTimeChanges(songData);
songMetadata.playData.characters = new SongCharacterData('bf', 'gf', 'dad');
songMetadata.playData.ratings.set(difficulty, songData.Difficulty.OverallDifficulty ?? 0);
return songMetadata;
}
static function rebuildTimeChanges(songData:OsuManiaData):Array<SongTimeChange>
{
var timings:Array<TimingPoint> = parseTimingPoints(cast songData.TimingPoints);
var bpmPoints:Array<TimingPoint> = timings.filter((tp) -> tp.uninherited == 1);
var result:Array<SongTimeChange> = [];
if (bpmPoints.length >= 1)
{
result.push(new SongTimeChange(0, bpmPoints[0].bpm ?? Constants.DEFAULT_BPM));
for (i in 1...bpmPoints.length)
{
var bpmPoint:TimingPoint = bpmPoints[i];
result.push(new SongTimeChange(bpmPoint.time, bpmPoint.bpm ?? Constants.DEFAULT_BPM));
}
}
if (result.length == 0)
{
result.push(new SongTimeChange(0, Constants.DEFAULT_BPM));
trace("[WARN] No BPM points found, resulting to default BPM...");
}
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.');
// Osu!Mania doesn't have a scroll speed variable as its controlled by the player
var songChartData:SongChartData = new SongChartData([
difficulty => Constants.DEFAULT_SCROLLSPEED
], [], [difficulty => []]);
// songData.HitObjects is a Array<String> here so im casting it so haxe stops yelling at me
var osuNotes:Array<ManiaHitObject> = parseManiaHitObjects(cast songData.HitObjects, songData.Difficulty.CircleSize);
songChartData.notes.set(difficulty, convertNotes(osuNotes, songData.Difficulty.CircleSize));
songChartData.events = [];
return songChartData;
}
static final STRUMLINE_SIZE = 4;
static function convertNotes(hitObjects:Array<ManiaHitObject>, keyCount:Int):Array<SongNoteData>
{
var result:Array<SongNoteData> = [];
for (hitObject in hitObjects)
{
var wrappedColumn:Int = hitObject.column % (keyCount * 2); // wrap overflow for 9K+
if (keyCount <= 4) // if its 4K or less, flip to BF side
{
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, ''));
}
else
result.push(new SongNoteData(hitObject.time, wrappedColumn, hitObject.holdDuration ?? 0, ''));
}
return result;
}
static function parseTimingPoints(timingLines:Array<String>):Array<TimingPoint>
{
return timingLines.map(function(line:String):TimingPoint
{
var parts = line.split(",");
var time = Std.parseFloat(parts[0]);
var beatLength = Std.parseFloat(parts[1]);
var meter = Std.parseInt(parts[2]);
var sampleSet = Std.parseInt(parts[3]);
var sampleIndex = Std.parseInt(parts[4]);
var volume = Std.parseInt(parts[5]);
var uninherited = Std.parseInt(parts[6]);
var effects = Std.parseInt(parts[7]);
return new TimingPoint(time, beatLength, meter, sampleSet, sampleIndex, volume, uninherited, effects);
});
}
static function parseManiaHitObjects(hitObjectsLines:Array<String>, ?columns:Int = 4):Array<ManiaHitObject>
{
return hitObjectsLines.map(function(line:String):ManiaHitObject
{
var parts = line.split(",");
var x:Int = Std.parseInt(parts[0]);
var time:Int = Std.parseInt(parts[2]);
var type:Int = Std.parseInt(parts[3]);
var hasHold:Bool = (type & 128) == 128;
var noteD:Int = Std.int(x / (512 / columns));
var holdEndTime:Null<Int> = hasHold ? Std.parseInt(parts[5].split(":")[0]) : null;
var holdDuration:Int = (holdEndTime != null) ? (holdEndTime - time) : 0;
return new ManiaHitObject(time, noteD, holdDuration);
});
}
static function parseValue(v:String):Any
{
var result:Null<Any>;
if (v.contains('.'))
{
result = Std.parseFloat(v);
if (!Math.isNaN(result)) return result;
}
else
{
result = Std.parseInt(v);
if (result != null) return result;
}
return v;
}
}