2022-09-06 00:59:54 -04:00
package funkin . play . song ;
2022-09-13 01:09:30 -04:00
import flixel . util . typeLimit . OneOfTwo ;
import funkin . play . song . ScriptedSong ;
2022-09-07 19:07:08 -04:00
import funkin . util . assets . DataAssets ;
2022-09-13 01:09:30 -04:00
import haxe . DynamicAccess ;
import haxe . Json ;
2022-09-07 19:07:08 -04:00
import openfl . utils . Assets ;
import thx . semver . Version ;
using StringTools ;
2022-09-06 00:59:54 -04:00
/ * *
* Contains utilities for l o a d i n g a n d p a r s i n g s t a g e d a t a .
* /
class SongDataParser
{
/ * *
* A list containing all the songs available to the game .
* /
2022-09-07 19:07:08 -04:00
static final songCache : Map < String , Song > = new Map < String , Song > ( ) ;
2022-09-06 00:59:54 -04:00
static final DEFAULT_SONG_ID = ' U N K N O W N ' ;
2022-09-13 01:09:30 -04:00
static final SONG_DATA_PATH = ' s o n g s / ' ;
static final SONG_DATA_SUFFIX = ' / m e t a d a t a . j s o n ' ;
2022-09-06 00:59:54 -04:00
/ * *
* Parses and preloads the game ' s s o n g m e t a d a t a a n d s c r i p t s w h e n t h e g a m e s t a r t s .
*
* If you want to force song metadata to be reloaded , you can just call this function again .
* /
public static function loadSongCache ( ) : Void
{
clearSongCache ( ) ;
trace ( " [ S O N G D A T A ] L o a d i n g s o n g c a c h e . . . " ) ;
//
// SCRIPTED SONGS
//
var scriptedSongClassNames: Array < String > = ScriptedSong . listScriptClasses ( ) ;
trace ( ' I n s t a n t i a t i n g ${ scriptedSongClassNames . length } s c r i p t e d s o n g s . . . ' ) ;
for ( songCls in scriptedSongClassNames )
{
var song: Song = ScriptedSong . init ( songCls , DEFAULT_SONG_ID ) ;
if ( song != null )
{
trace ( ' L o a d e d s c r i p t e d s o n g : ${ song . songId } ' ) ;
songCache . set ( song . songId , song ) ;
}
e lse
{
trace ( ' F a i l e d t o i n s t a n t i a t e s c r i p t e d s o n g c l a s s : ${ songCls } ' ) ;
}
}
//
// UNSCRIPTED SONGS
//
2022-09-13 01:09:30 -04:00
var songIdList: Array < String > = DataAssets . listDataFilesInPath ( SONG_DATA_PATH , SONG_DATA_SUFFIX ) ;
2022-09-06 00:59:54 -04:00
var unscriptedSongIds: Array < String > = songIdList . filter ( function ( songId : String ) : Bool
{
return ! songCache . exists ( songId ) ;
} ) ;
trace ( ' I n s t a n t i a t i n g ${ unscriptedSongIds . length } n o n - s c r i p t e d s o n g s . . . ' ) ;
for ( songId in unscriptedSongIds )
{
try
{
2022-09-07 19:07:08 -04:00
var song = new Song ( songId ) ;
if ( song != null )
2022-09-06 00:59:54 -04:00
{
trace ( ' L o a d e d s o n g d a t a : ${ song . songId } ' ) ;
songCache . set ( song . songId , song ) ;
}
}
c atch ( e )
{
trace ( ' A n e r r o r o c c u r r e d w h i l e l o a d i n g s o n g d a t a : ${ songId } ' ) ;
2022-09-13 01:09:30 -04:00
trace ( e ) ;
2022-09-06 00:59:54 -04:00
// Assume error was already logged.
continue ;
}
}
trace ( ' S u c c e s s f u l l y l o a d e d ${ Lambda . count ( songCache ) } s t a g e s . ' ) ;
}
/ * *
* Retrieves a particular song from the cache .
* /
2022-09-07 19:07:08 -04:00
public static function fetchSong ( songId : String ) : Null < Song >
2022-09-06 00:59:54 -04:00
{
if ( songCache . exists ( songId ) )
{
var song: Song = songCache . get ( songId ) ;
trace ( ' [ S T A G E D A T A ] S u c c e s s f u l l y f e t c h s o n g : ${ songId } ' ) ;
return song ;
}
e lse
{
trace ( ' [ S T A G E D A T A ] F a i l e d t o f e t c h s o n g , n o t f o u n d i n c a c h e : ${ songId } ' ) ;
return null ;
}
}
static function clearSongCache ( ) : Void
{
if ( songCache != null )
{
songCache . clear ( ) ;
}
}
2022-09-13 01:09:30 -04:00
public static function parseSongMetadata ( songId : String ) : Array < SongMetadata >
2022-09-06 00:59:54 -04:00
{
2022-09-13 01:09:30 -04:00
var result: Array < SongMetadata > = [ ] ;
var rawJson: String = loadSongMetadataFile ( songId ) ;
var jsonData: Dynamic = null ;
try
{
jsonData = Json . parse ( rawJson ) ;
}
c atch ( e )
{
}
var songMetadata: SongMetadata = SongMigrator . migrateSongMetadata ( jsonData , songId ) ;
songMetadata = SongValidator . validateSongMetadata ( songMetadata , songId ) ;
if ( songMetadata == null )
{
return result ;
}
result . push ( songMetadata ) ;
var variations = songMetadata . playData . songVariations ;
for ( variation in variations )
{
var variationRawJson: String = loadSongMetadataFile ( songId , variation ) ;
var variationSongMetadata: SongMetadata = SongMigrator . migrateSongMetadata ( variationRawJson , ' ${ songId } _ ${ variation } ' ) ;
variationSongMetadata = SongValidator . validateSongMetadata ( variationSongMetadata , ' ${ songId } _ ${ variation } ' ) ;
if ( variationSongMetadata != null )
{
variationSongMetadata . variation = variation ;
result . push ( variationSongMetadata ) ;
}
}
return result ;
2022-09-06 00:59:54 -04:00
}
2022-09-13 01:09:30 -04:00
static function loadSongMetadataFile ( songPath : String , variation : String = ' ' ) : String
2022-09-06 00:59:54 -04:00
{
2022-09-13 01:09:30 -04:00
var songMetadataFilePath: String = ( variation != ' ' ) ? Paths . json ( ' $ SONG_DATA_PATH $ songPath / m e t a d a t a - $ variation ' ) : Paths . json ( ' $ SONG_DATA_PATH $ songPath / m e t a d a t a ' ) ;
2022-09-07 19:07:08 -04:00
var rawJson: String = Assets . getText ( songMetadataFilePath ) . trim ( ) ;
2022-09-06 00:59:54 -04:00
while ( ! rawJson . endsWith ( " } " ) )
{
rawJson = rawJson . substr ( 0 , rawJson . length - 1 ) ;
}
return rawJson ;
}
2022-09-13 01:09:30 -04:00
public static function parseSongChartData ( songId : String , variation : String = " " ) : SongChartData
{
var rawJson: String = loadSongChartDataFile ( songId , variation ) ;
var jsonData: Dynamic = null ;
try
{
jsonData = Json . parse ( rawJson ) ;
}
c atch ( e )
{
}
var songChartData: SongChartData = SongMigrator . migrateSongChartData ( jsonData , songId ) ;
songChartData = SongValidator . validateSongChartData ( songChartData , songId ) ;
if ( songChartData == null )
{
trace ( ' F a i l e d t o v a l i d a t e s o n g c h a r t d a t a : ${ songId } ' ) ;
return null ;
}
return songChartData ;
}
static function loadSongChartDataFile ( songPath : String , variation : String = ' ' ) : String
{
var songChartDataFilePath: String = ( variation != ' ' ) ? Paths . json ( ' $ SONG_DATA_PATH $ songPath / c h a r t - $ variation ' ) : Paths . json ( ' $ SONG_DATA_PATH $ songPath / c h a r t ' ) ;
var rawJson: String = Assets . getText ( songChartDataFilePath ) . trim ( ) ;
while ( ! rawJson . endsWith ( " } " ) )
{
rawJson = rawJson . substr ( 0 , rawJson . length - 1 ) ;
}
return rawJson ;
}
2022-09-06 00:59:54 -04:00
}
2022-09-07 19:07:08 -04:00
typedef SongMetadata =
{
2022-09-13 01:09:30 -04:00
/ * *
* A semantic versioning string for t h e s o n g d a t a f o r m a t .
*
* /
2022-09-07 19:07:08 -04:00
var version: Version ;
var songName: String ;
var artist: String ;
var timeFormat: SongTimeFormat ;
var divisions: Int ;
var timeChanges: Array < SongTimeChange > ;
2022-09-13 01:09:30 -04:00
var loop: Bool ;
var playData: SongPlayData ;
var generatedBy: String ;
/ * *
* Defaults to ' ' . Populated later .
* /
var variation: String ;
2022-09-07 19:07:08 -04:00
} ;
2022-09-13 01:09:30 -04:00
typedef SongPlayData =
{
var songVariations: Array < String > ;
var difficulties: Array < String > ;
/ * *
* Keys are the player characters and the values give info on what opponent / GF / inst to use .
* /
var playableChars: DynamicAccess < SongPlayableChar > ;
var stage: String ;
var noteSkin: String ;
}
typedef RawSongPlayableChar =
{
var g: String ;
var o: String ;
var i: String ;
}
2022-09-16 15:37:00 -04:00
typedef RawSongNoteData =
{
/ * *
* The timestamp of the note . The timestamp is in the format of the song ' s t i m e f o r m a t .
* /
var t: Float ;
/ * *
* Data for t h e n o t e . R e p r e s e n t s t h e i n d e x o n t h e s t r u m l i n e .
* 0 = left , 1 = down , 2 = up , 3 = right
* ` floor ( direction / strumlineSize ) ` specifies which strumline the note is on .
* 0 = player , 1 = opponent , etc .
* /
var d: Int ;
/ * *
* Length of the note , if a p p l i c a b l e .
* Defaults to 0 for s i n g l e n o t e s .
* /
var l: Float ;
/ * *
* The kind of the note .
* This can allow the note to include information used for c u s t o m b e h a v i o r .
* Defaults to blank or ` " n o r m a l " ` .
* /
var k: String ;
}
abstract SongNoteData ( RawSongNoteData )
{
public function n e w ( time : Float , data : Int , length : Float = 0 , kind : String = " " )
{
this = {
t : time ,
d : data ,
l : length ,
k : kind
} ;
}
public var time( get , set ) : Float ;
public function get_time ( ) : Float
{
return this . t ;
}
public function set_time ( value : Float ) : Float
{
return this . t = value ;
}
/ * *
* The raw data for t h e n o t e .
* /
public var data( get , set ) : Int ;
public function get_data ( ) : Int
{
return this . d ;
}
public function set_data ( value : Int ) : Int
{
return this . d = value ;
}
/ * *
* The direction of the note , if a p p l i c a b l e .
* Strips the strumline index from the data .
*
* 0 = left , 1 = down , 2 = up , 3 = right
* /
public inline function getDirection ( strumlineSize : Int = 4 ) : Int
{
return this . d % strumlineSize ;
}
/ * *
* The strumline index of the note , if a p p l i c a b l e .
* Strips the direction from the data .
*
* 0 = player , 1 = opponent , etc .
* /
public inline function getStrumlineIndex ( strumlineSize : Int = 4 ) : Int
{
return Math . floor ( this . d / strumlineSize ) ;
}
public var length( get , set ) : Float ;
public function get_length ( ) : Float
{
return this . l ;
}
public function set_length ( value : Float ) : Float
{
return this . l = value ;
}
public var kind( get , set ) : String ;
public function get_kind ( ) : String
{
if ( this . k == null || this . k == ' ' )
return ' n o r m a l ' ;
return this . k ;
}
public function set_kind ( value : String ) : String
{
if ( value == ' n o r m a l ' || value == ' ' )
value = null ;
return this . k = value ;
}
}
typedef RawSongEventData =
{
/ * *
* The timestamp of the event . The timestamp is in the format of the song ' s t i m e f o r m a t .
* /
var t: Float ;
/ * *
* The kind of the event .
* Examples include " F o c u s C a m e r a " and " P l a y A n i m a t i o n "
* Custom events can be added by scripts with the ` ScriptedSongEvent ` c lass .
* /
var e: String ;
/ * *
* The data for t h e e v e n t .
* This can allow the event to include information used for c u s t o m b e h a v i o r .
* Data type depends on the event kind . It can be anything that ' s J S O N s e r i a l i z a b l e .
* /
var v: Dynamic ;
}
abstract SongEventData ( RawSongEventData )
{
public function n e w ( time : Float , event : String , value : Dynamic = null )
{
this = {
t : time ,
e : event ,
v : value
} ;
}
public var time( get , set ) : Float ;
public function get_time ( ) : Float
{
return this . t ;
}
public function set_time ( value : Float ) : Float
{
return this . t = value ;
}
public var event( get , set ) : String ;
public function get_event ( ) : String
{
return this . e ;
}
public function set_event ( value : String ) : String
{
return this . e = value ;
}
public var value( get , set ) : Dynamic ;
public function get_value ( ) : Dynamic
{
return this . v ;
}
public function set_value ( value : Dynamic ) : Dynamic
{
return this . v = value ;
}
public inline function getBool ( ) : Bool
{
return cast this . v ;
}
public inline function getInt ( ) : Int
{
return cast this . v ;
}
public inline function getFloat ( ) : Float
{
return cast this . v ;
}
public inline function getString ( ) : String
{
return cast this . v ;
}
public inline function getArray ( ) : Array < Dynamic >
{
return cast this . v ;
}
public inline function getMap ( ) : DynamicAccess < Dynamic >
{
return cast this . v ;
}
public inline function getBoolArray ( ) : Array < Bool >
{
return cast this . v ;
}
}
2022-09-13 01:09:30 -04:00
abstract SongPlayableChar ( RawSongPlayableChar )
{
public function n e w ( girlfriend : String , opponent : String , inst : String = " " )
{
this = {
g : girlfriend ,
o : opponent ,
i : inst
} ;
}
public var girlfriend( get , set ) : String ;
public function get_girlfriend ( ) : String
{
return this . g ;
}
public function set_girlfriend ( value : String ) : String
{
return this . g = value ;
}
public var opponent( get , set ) : String ;
public function get_opponent ( ) : String
{
return this . o ;
}
public function set_opponent ( value : String ) : String
{
return this . o = value ;
}
public var inst( get , set ) : String ;
public function get_inst ( ) : String
{
return this . i ;
}
public function set_inst ( value : String ) : String
{
return this . i = value ;
}
}
2022-09-07 19:07:08 -04:00
typedef SongChartData =
{
2022-09-16 15:37:00 -04:00
var version: Version ;
var scrollSpeed: DynamicAccess < Float > ;
var events: Array < SongEventData > ;
var notes: DynamicAccess < Array < SongNoteData > > ;
var generatedBy: String ;
2022-09-07 19:07:08 -04:00
} ;
2022-09-13 01:09:30 -04:00
typedef RawSongTimeChange =
{
/ * *
* Timestamp in specified ` timeFormat ` .
* /
var t: Float ;
/ * *
* Time in beats ( int ) . The game will calculate further beat values based on this one ,
* so it can do it in a s i m p l e l i n e a r f a s h i o n .
* /
var b: Int ;
/ * *
* Quarter notes per minute ( float ) . Cannot be empty in the first element of the list ,
* but otherwise it ' s o p t i o n a l , a n d d e f a u l t s t o t h e v a l u e o f t h e p r e v i o u s e l e m e n t .
* /
var bpm: Float ;
/ * *
* Time signature numerator ( int ) . Optional , defaults to 4.
* /
var n: Int ;
/ * *
* Time signature denominator ( int ) . Optional , defaults to 4. Should only ever be a power of two .
* /
var d: Int ;
/ * *
* Beat tuplets ( Array < int > or i n t ) . This defines how many steps each beat is divided into .
* It can either be an array of length ` n ` ( see a b o v e ) or a single integer number .
* Optional , defaults to ` [ 4 ] ` .
* /
var bt: OneOfTwo < Int , Array < Int > > ;
}
/ * *
* Add aliases to the minimalized property names of the typedef ,
* to improve readability .
* /
abstract SongTimeChange ( RawSongTimeChange )
{
public function n e w ( timeStamp : Float , beatTime : Int , bpm : Float , timeSignatureNum : Int = 4 , timeSignatureDen : Int = 4 , beatTuplets : Array < Int > )
{
this = {
t : timeStamp ,
b : beatTime ,
bpm : bpm ,
n : timeSignatureNum ,
d : timeSignatureDen ,
bt : beatTuplets ,
}
}
public var timeStamp( get , set ) : Float ;
public function get_timeStamp ( ) : Float
{
return this . t ;
}
public function set_timeStamp ( value : Float ) : Float
{
return this . t = value ;
}
public var beatTime( get , set ) : Int ;
public function get_beatTime ( ) : Int
{
return this . b ;
}
public function set_beatTime ( value : Int ) : Int
{
return this . b = value ;
}
public var bpm( get , set ) : Float ;
public function get_bpm ( ) : Float
{
return this . bpm ;
}
public function set_bpm ( value : Float ) : Float
{
return this . bpm = value ;
}
public var timeSignatureNum( get , set ) : Int ;
public function get_timeSignatureNum ( ) : Int
{
return this . n ;
}
public function set_timeSignatureNum ( value : Int ) : Int
{
return this . n = value ;
}
public var timeSignatureDen( get , set ) : Int ;
public function get_timeSignatureDen ( ) : Int
{
return this . d ;
}
public function set_timeSignatureDen ( value : Int ) : Int
{
return this . d = value ;
}
public var beatTuplets( get , set ) : Array < Int > ;
public function get_beatTuplets ( ) : Array < Int >
{
if ( Std . isOfType ( this . bt , Int ) )
{
return [ this . bt ] ;
}
e lse
{
return this . bt ;
}
}
public function set_beatTuplets ( value : Array < Int > ) : Array < Int >
{
return this . bt = value ;
}
}
2022-09-07 19:07:08 -04:00
enum a b s t r a c t SongTimeFormat ( S t r i n g ) f r o m S t r i n g t o S t r i n g
{
var TICKS = " t i c k s " ;
var FLOAT = " f l o a t " ;
var MILLISECONDS = " m s " ;
}