package funkin.data.song.importer; // import is a reserved word dumbass import funkin.data.song.SongData.SongMetadata; import funkin.data.song.SongData.SongChartData; import funkin.data.song.SongData.SongCharacterData; import funkin.data.song.SongData.SongEventData; import funkin.data.song.SongData.SongNoteData; import funkin.data.song.SongData.SongTimeChange; import funkin.data.song.importer.FNFLegacyData; import funkin.data.song.importer.FNFLegacyData.LegacyNoteSection; class FNFLegacyImporter { public static function parseLegacyDataRaw(input:String, fileName:String = 'raw'):FNFLegacyData { var parser = new json2object.JsonParser(); parser.ignoreUnknownVariables = true; // Set to true to ignore extra variables that might be included in the JSON. parser.fromJson(input, fileName); if (parser.errors.length > 0) { trace('[FNFLegacyImporter] Error parsing JSON data from ' + fileName + ':'); for (error in parser.errors) DataError.printError(error); return null; } return parser.value; } /** * @param data The raw parsed JSON data to migrate, as a Dynamic. * @param difficulty * @return SongMetadata */ public static function migrateMetadata(songData:FNFLegacyData, difficulty:String = 'normal'):SongMetadata { trace('Migrating song metadata from FNF Legacy.'); var songMetadata:SongMetadata = new SongMetadata('Import', Constants.DEFAULT_ARTIST, 'default'); var hadError:Bool = false; // Set generatedBy string for debugging. songMetadata.generatedBy = 'Chart Editor Import (FNF Legacy)'; songMetadata.playData.stage = songData?.song?.stageDefault ?? 'mainStage'; songMetadata.songName = songData?.song?.song ?? 'Import'; songMetadata.playData.difficulties = []; if (songData?.song?.notes != null) { switch (songData.song.notes) { case Left(notes): // One difficulty of notes. songMetadata.playData.difficulties.push(difficulty); case Right(difficulties): if (difficulties.easy != null) songMetadata.playData.difficulties.push('easy'); if (difficulties.normal != null) songMetadata.playData.difficulties.push('normal'); if (difficulties.hard != null) songMetadata.playData.difficulties.push('hard'); } } songMetadata.playData.songVariations = []; songMetadata.timeChanges = rebuildTimeChanges(songData); songMetadata.playData.characters = new SongCharacterData(songData?.song?.player1 ?? 'bf', 'gf', songData?.song?.player2 ?? 'dad'); return songMetadata; } public static function migrateChartData(songData:FNFLegacyData, difficulty:String = 'normal'):SongChartData { trace('Migrating song chart data from FNF Legacy.'); var songChartData:SongChartData = new SongChartData([difficulty => 1.0], [], [difficulty => []]); if (songData?.song?.notes != null) { switch (songData.song.notes) { case Left(notes): // One difficulty of notes. songChartData.notes.set(difficulty, migrateNoteSections(notes)); case Right(difficulties): var baseDifficulty = null; if (difficulties.easy != null) songChartData.notes.set('easy', migrateNoteSections(difficulties.easy)); if (difficulties.normal != null) songChartData.notes.set('normal', migrateNoteSections(difficulties.normal)); if (difficulties.hard != null) songChartData.notes.set('hard', migrateNoteSections(difficulties.hard)); } } // Import event data. songChartData.events = rebuildEventData(songData); switch (songData.song.speed) { case Left(speed): // All difficulties will use the one scroll speed. songChartData.scrollSpeed.set('default', speed); case Right(speeds): if (speeds.easy != null) songChartData.scrollSpeed.set('easy', speeds.easy); if (speeds.normal != null) songChartData.scrollSpeed.set('normal', speeds.normal); if (speeds.hard != null) songChartData.scrollSpeed.set('hard', speeds.hard); } return songChartData; } /** * FNF Legacy doesn't have song events, but without them the song won't look right, * so we insert camera events when the character changes. */ static function rebuildEventData(songData:FNFLegacyData):Array { var result:Array = []; var noteSections = []; switch (songData.song.notes) { case Left(notes): // All difficulties will use the one scroll speed. noteSections = notes; case Right(difficulties): if (difficulties.normal != null) noteSections = difficulties.normal; if (difficulties.hard != null) noteSections = difficulties.normal; if (difficulties.easy != null) noteSections = difficulties.normal; } if (noteSections == null || noteSections.length == 0) return result; // Add camera events. var lastSectionWasMustHit:Null = null; for (section in noteSections) { // Skip empty sections. if (section.sectionNotes.length == 0) continue; if (section.mustHitSection != lastSectionWasMustHit) { lastSectionWasMustHit = section.mustHitSection; var firstNote:LegacyNote = section.sectionNotes[0]; result.push(new SongEventData(firstNote.time, 'FocusCamera', {char: section.mustHitSection ? 0 : 1})); } } return result; } /** * Port over time changes from FNF Legacy. * If a section contains a BPM change, it will be applied at the timestamp of the first note in that section. */ static function rebuildTimeChanges(songData:FNFLegacyData):Array { var result:Array = []; result.push(new SongTimeChange(0, songData?.song?.bpm ?? Constants.DEFAULT_BPM)); var noteSections = []; switch (songData.song.notes) { case Left(notes): // All difficulties will use the one scroll speed. noteSections = notes; case Right(difficulties): if (difficulties.normal != null) noteSections = difficulties.normal; if (difficulties.hard != null) noteSections = difficulties.normal; if (difficulties.easy != null) noteSections = difficulties.normal; } if (noteSections == null || noteSections.length == 0) return result; for (noteSection in noteSections) { if (noteSection.changeBPM ?? false) { var firstNote:LegacyNote = noteSection.sectionNotes[0]; if (firstNote != null) result.push(new SongTimeChange(firstNote.time, noteSection.bpm)); } } return result; } static final STRUMLINE_SIZE = 4; static function migrateNoteSections(input:Array):Array { var result:Array = []; for (section in input) { var mustHitSection = section.mustHitSection ?? false; for (note in section.sectionNotes) { // Handle the dumb logic for mustHitSection. var noteData = note.data; if (noteData < 0) continue; // Exclude Psych event notes. if (noteData > (STRUMLINE_SIZE * 2)) noteData = noteData % (2 * STRUMLINE_SIZE); // Handle other engine event notes. // Flip notes if mustHitSection is FALSE (not true lol). if (!mustHitSection) { if (noteData >= STRUMLINE_SIZE) { noteData -= STRUMLINE_SIZE; } else { noteData += STRUMLINE_SIZE; } } result.push(new SongNoteData(note.time, noteData, note.length, note.getKind())); } } return result; } }