2019-11-10 15:09:14 +10:30
#extends Object
extends Node
2020-05-03 00:11:01 +09:30
const ERROR_CODES : = [
' OK ' , ' FAILED ' , ' ERR_UNAVAILABLE ' , ' ERR_UNCONFIGURED ' , ' ERR_UNAUTHORIZED ' , ' ERR_PARAMETER_RANGE_ERROR ' ,
' ERR_OUT_OF_MEMORY ' , ' ERR_FILE_NOT_FOUND ' , ' ERR_FILE_BAD_DRIVE ' , ' ERR_FILE_BAD_PATH ' , ' ERR_FILE_NO_PERMISSION ' ,
' ERR_FILE_ALREADY_IN_USE ' , ' ERR_FILE_CANT_OPEN ' , ' ERR_FILE_CANT_WRITE ' , ' ERR_FILE_CANT_READ ' , ' ERR_FILE_UNRECOGNIZED ' ,
' ERR_FILE_CORRUPT ' , ' ERR_FILE_MISSING_DEPENDENCIES ' , ' ERR_FILE_EOF ' , ' ERR_CANT_OPEN ' , ' ERR_CANT_CREATE ' , ' ERR_QUERY_FAILED ' ,
' ERR_ALREADY_IN_USE ' , ' ERR_LOCKED ' , ' ERR_TIMEOUT ' , ' ERR_CANT_CONNECT ' , ' ERR_CANT_RESOLVE ' , ' ERR_CONNECTION_ERROR ' ,
' ERR_CANT_ACQUIRE_RESOURCE ' , ' ERR_CANT_FORK ' , ' ERR_INVALID_DATA ' , ' ERR_INVALID_PARAMETER ' , ' ERR_ALREADY_EXISTS ' ,
' ERR_DOES_NOT_EXIST ' , ' ERR_DATABASE_CANT_READ ' , ' ERR_DATABASE_CANT_WRITE ' , ' ERR_COMPILATION_FAILED ' , ' ERR_METHOD_NOT_FOUND ' ,
' ERR_LINK_FAILED ' , ' ERR_SCRIPT_FAILED ' , ' ERR_CYCLIC_LINK ' , ' ERR_INVALID_DECLARATION ' , ' ERR_DUPLICATE_SYMBOL ' ,
' ERR_PARSE_ERROR ' , ' ERR_BUSY ' , ' ERR_SKIP ' , ' ERR_HELP ' , ' ERR_BUG '
]
var userroot : = OS . get_user_data_dir ( ) . rstrip ( ' / ' ) + ' / ' if OS . get_name ( ) != ' Android ' else ' /storage/emulated/0/RhythmGame/ '
2020-12-21 22:52:15 +10:30
var PATHS : = PoolStringArray ( [ userroot , ' /media/fridge-q/Games/Other/maimai Finale/decoded/RhythmGameCharts/slow_userdir/ ' ] ) # Temporary hardcoded testing
2020-05-01 15:45:28 +09:30
# The following would probably work. One huge caveat is that permission needs to be manually granted by the user in app settings as we can't use OS.request_permission('WRITE_EXTERNAL_STORAGE')
# '/storage/emulated/0/Android/data/au.ufeff.rhythmgame/'
# '/sdcard/Android/data/au.ufeff.rhythmgame/'
2020-05-03 00:11:01 +09:30
func _ready ( ) - > void :
print ( ' Library paths: ' , PATHS )
2020-03-29 17:43:28 +10:30
2020-12-21 22:52:15 +10:30
func find_file ( name : String ) - > String :
# Searches through all of the paths to find the file
var file : = File . new ( )
for root in PATHS :
var filename : String = root + name
if file . file_exists ( filename ) :
return filename
return ' '
2020-03-30 23:21:20 +10:30
func directory_list ( directory : String , hidden : bool , sort : = true ) - > Dictionary :
2020-03-29 17:43:28 +10:30
# Sadly there's no filelist sugar so we make our own
var output = { folders = [ ] , files = [ ] , err = OK }
var dir = Directory . new ( )
output . err = dir . open ( directory )
if output . err != OK :
print_debug ( ' Failed to open directory: ' + directory + ' (Error code ' + output . err + ' ) ' )
return output
output . err = dir . list_dir_begin ( true , ! hidden )
if output . err != OK :
print_debug ( ' Failed to begin listing directory: ' + directory + ' (Error code ' + output . err + ' ) ' )
return output
var item = dir . get_next ( )
while ( item != ' ' ) :
if dir . current_is_dir ( ) :
output [ ' folders ' ] . append ( item )
else :
output [ ' files ' ] . append ( item )
item = dir . get_next ( )
dir . list_dir_end ( )
2020-03-30 23:21:20 +10:30
if sort :
output . folders . sort ( )
output . files . sort ( )
# Maybe convert the Arrays to PoolStringArrays?
return output
2020-05-01 15:45:28 +09:30
func find_by_extensions ( array , extensions = null ) - > Dictionary :
2020-03-30 23:21:20 +10:30
# Both args can be Array or PoolStringArray
2020-05-01 15:45:28 +09:30
# If extensions omitted, do all extensions
2020-03-30 23:21:20 +10:30
var output = { }
2020-05-01 15:45:28 +09:30
if extensions :
2020-03-30 23:21:20 +10:30
for ext in extensions :
2020-05-01 15:45:28 +09:30
output [ ext ] = [ ]
for filename in array :
for ext in extensions :
if filename . ends_with ( ext ) :
output [ ext ] . append ( filename )
else :
for filename in array :
var ext = filename . rsplit ( ' . ' , false , 1 ) [ 1 ]
if ext in output :
output [ ext ] . append ( filename )
else :
output [ ext ] = [ filename ]
2020-03-29 17:43:28 +10:30
return output
2020-05-02 15:45:27 +09:30
const default_difficulty_keys = [ ' Z ' , ' B ' , ' A ' , ' E ' , ' M ' , ' R ' ]
2020-03-29 17:43:28 +10:30
func scan_library ( ) :
2020-05-01 15:45:28 +09:30
print ( ' Scanning library ' )
var rootdir = userroot + ' songs '
2020-03-29 17:43:28 +10:30
var dir = Directory . new ( )
var err = dir . make_dir_recursive ( rootdir )
if err != OK :
2020-05-01 15:45:28 +09:30
print_debug ( ' An error occurred while trying to create the songs directory: ' , err )
2020-03-29 17:43:28 +10:30
return err
var songslist = directory_list ( rootdir , false )
if songslist . err != OK :
2020-05-01 15:45:28 +09:30
print ( ' An error occurred when trying to access the songs directory: ' , songslist . err )
2020-03-29 17:43:28 +10:30
return songslist . err
var song_defs = { }
2020-05-01 15:45:28 +09:30
var collections = { }
2020-03-29 17:43:28 +10:30
var genres = { }
dir . open ( rootdir )
2020-08-22 14:33:40 +09:30
for folder in songslist . folders :
var full_folder : = ' %s / %s ' % [ rootdir , folder ]
if dir . file_exists ( folder + ' /song.json ' ) :
2020-03-30 23:21:20 +10:30
# Our format
2020-08-22 14:33:40 +09:30
song_defs [ folder ] = FileLoader . load_folder ( full_folder )
print ( ' Loaded song directory: %s ' % folder )
if song_defs [ folder ] [ ' genre ' ] in genres :
genres [ song_defs [ folder ] [ ' genre ' ] ] . append ( folder )
2020-03-29 17:43:28 +10:30
else :
2020-08-22 14:33:40 +09:30
genres [ song_defs [ folder ] [ ' genre ' ] ] = [ folder ]
if typeof ( song_defs [ folder ] [ ' chart_difficulties ' ] ) == TYPE_ARRAY :
var diffs = song_defs [ folder ] [ ' chart_difficulties ' ]
2020-05-02 15:45:27 +09:30
var chart_difficulties = { }
for i in min ( len ( diffs ) , len ( default_difficulty_keys ) ) :
chart_difficulties [ default_difficulty_keys [ i ] ] = diffs [ i ]
2020-08-22 14:33:40 +09:30
song_defs [ folder ] [ ' chart_difficulties ' ] = chart_difficulties
2020-05-03 00:11:01 +09:30
2020-08-22 14:33:40 +09:30
elif dir . file_exists ( folder + ' /collection.json ' ) :
var collection = FileLoader . load_folder ( full_folder , ' collection ' )
collections [ folder ] = collection
var base_dict = { ' filepath ' : folder + ' / ' } # Top level of the collection dict contains defaults for every song in it
2020-05-01 15:45:28 +09:30
for key in collection . keys ( ) :
if key != ' songs ' :
base_dict [ key ] = collection [ key ]
2020-05-02 15:45:27 +09:30
for song_key in collection [ ' songs ' ] . keys ( ) :
var song_dict = collection [ ' songs ' ] [ song_key ]
2020-05-01 15:45:28 +09:30
var song_def = base_dict . duplicate ( )
2020-05-02 15:45:27 +09:30
for key in song_dict . keys ( ) :
song_def [ key ] = song_dict [ key ]
2020-05-03 00:11:01 +09:30
Library . add_song ( song_key , song_def )
# Legacy compat stuff
song_defs [ song_key ] = song_def
2020-05-02 15:45:27 +09:30
if song_defs [ song_key ] [ ' genre ' ] in genres :
genres [ song_defs [ song_key ] [ ' genre ' ] ] . append ( song_key )
else :
genres [ song_defs [ song_key ] [ ' genre ' ] ] = [ song_key ]
2020-05-03 00:11:01 +09:30
2020-03-29 17:43:28 +10:30
else :
2020-08-22 14:33:40 +09:30
var files_by_ext = find_by_extensions ( directory_list ( full_folder , false ) . files )
2020-05-01 15:45:28 +09:30
if ' sm ' in files_by_ext :
var sm_filename = files_by_ext [ ' sm ' ] [ 0 ]
2020-03-30 23:21:20 +10:30
print ( sm_filename )
2020-08-22 14:33:40 +09:30
var thing = SM . load_file ( full_folder + ' / ' + sm_filename )
2020-03-30 23:21:20 +10:30
print ( thing )
pass
else :
2020-08-22 14:33:40 +09:30
print ( ' Found non-song directory: ' + folder )
2020-03-29 17:43:28 +10:30
for file in songslist . files :
2020-05-01 15:45:28 +09:30
print ( ' Found file: ' + file )
2020-03-29 17:43:28 +10:30
2020-08-22 14:33:40 +09:30
return { song_defs = song_defs , genres = genres }
2020-03-29 17:43:28 +10:30
2019-11-10 15:09:14 +10:30
class SRT :
const TAP_DURATION : = 0.062500
const ID_BREAK : = 4
const ID_HOLD : = 2
const ID_SLIDE_END : = 128
2019-11-13 00:48:06 +10:30
const ID3_SLIDE_CHORD : = 0 # Straight line
const ID3_SLIDE_ARC_CW : = 1
const ID3_SLIDE_ARC_ACW : = 2
2019-11-10 15:09:14 +10:30
static func load_file ( filename ) :
var file = File . new ( )
var err = file . open ( filename , File . READ )
if err != OK :
print ( err )
return err
2021-02-07 00:17:27 +10:30
var metadata : = { }
var num_taps : = 0
var num_holds : = 0
var num_slides : = 0
var notes : = [ ]
2019-11-10 15:09:14 +10:30
var beats_per_measure : = 4
var length = file . get_len ( )
2020-05-01 01:26:46 +09:30
var slide_ids = { }
2019-11-10 15:09:14 +10:30
while ( file . get_position ( ) < ( length - 2 ) ) :
var noteline = file . get_csv_line ( )
2021-02-07 00:17:27 +10:30
var time_hit : = ( float ( noteline [ 0 ] ) + ( float ( noteline [ 1 ] ) ) ) * beats_per_measure
2019-11-10 15:09:14 +10:30
var duration : = float ( noteline [ 2 ] ) * beats_per_measure
var column : = int ( noteline [ 3 ] )
var id : = int ( noteline [ 4 ] )
var id2 : = int ( noteline [ 5 ] )
var id3 : = int ( noteline [ 6 ] )
match id :
ID_HOLD :
2020-04-30 17:15:44 +09:30
notes . push_back ( Note . NoteHold . new ( time_hit , column , duration ) )
2021-02-07 00:17:27 +10:30
num_holds += 1
2019-11-10 15:09:14 +10:30
ID_BREAK :
2020-04-30 17:15:44 +09:30
notes . push_back ( Note . NoteTap . new ( time_hit , column , true ) )
2021-02-07 00:17:27 +10:30
num_taps += 1
2019-11-10 15:09:14 +10:30
ID_SLIDE_END :
2019-11-13 00:48:06 +10:30
# id2 is slide ID
2020-05-01 01:26:46 +09:30
if id2 in slide_ids :
slide_ids [ id2 ] . column_release = column
slide_ids [ id2 ] . update_slide_variables ( )
2019-11-10 15:09:14 +10:30
_ :
if id2 == 0 :
2020-04-30 17:15:44 +09:30
notes . push_back ( Note . NoteTap . new ( time_hit , column ) )
2021-02-07 00:17:27 +10:30
num_taps += 1
2019-11-10 15:09:14 +10:30
else :
# id2 is slide ID, id3 is slide pattern
# In order to properly declare the slide, we need the paired endcap which may not be the next note
2019-11-14 22:27:30 +10:30
var slide_type = Note . SlideType . CHORD
2019-11-13 00:48:06 +10:30
match id3 :
ID3_SLIDE_CHORD :
2019-11-14 22:27:30 +10:30
slide_type = Note . SlideType . CHORD
2019-11-13 00:48:06 +10:30
ID3_SLIDE_ARC_CW :
2019-11-14 22:27:30 +10:30
slide_type = Note . SlideType . ARC_CW
2019-11-13 00:48:06 +10:30
ID3_SLIDE_ARC_ACW :
2019-11-14 22:27:30 +10:30
slide_type = Note . SlideType . ARC_ACW
2019-11-13 00:48:06 +10:30
_ :
2020-05-01 15:45:28 +09:30
print ( ' Unknown slide type: ' , id3 )
2020-05-01 01:26:46 +09:30
var note = Note . NoteStar . new ( time_hit , column )
2021-02-07 00:17:27 +10:30
num_slides += 1
2020-05-01 01:26:46 +09:30
note . duration = duration
notes . push_back ( note )
var slide = Note . NoteSlide . new ( time_hit , column , duration , - 1 , slide_type )
notes . push_back ( slide )
slide_ids [ id2 ] = slide
2021-02-07 00:17:27 +10:30
metadata [ ' num_taps ' ] = num_taps
metadata [ ' num_holds ' ] = num_holds
metadata [ ' num_slides ' ] = num_slides
return [ metadata , notes ]
2019-11-10 15:09:14 +10:30
2019-11-13 00:48:06 +10:30
2020-04-30 17:15:44 +09:30
class RGT :
# RhythmGameText formats
# .rgts - simplified format cutting out redundant data, should be easy to write charts in
# .rgtx - a lossless representation of MM in-memory format
# .rgtm - a collection of rgts charts, with a [title] at the start of each one
enum Format { RGTS , RGTX , RGTM }
const EXTENSIONS = {
' rgts ' : Format . RGTS ,
' rgtx ' : Format . RGTX ,
' rgtm ' : Format . RGTM ,
}
const NOTE_TYPES = {
' t ' : Note . NOTE_TAP ,
' h ' : Note . NOTE_HOLD ,
2020-05-01 15:45:28 +09:30
' s ' : Note . NOTE_STAR ,
2020-04-30 17:15:44 +09:30
' e ' : Note . NOTE_SLIDE ,
2021-01-16 02:56:32 +10:30
' b ' : Note . NOTE_TAP , # Break
' x ' : Note . NOTE_STAR # Break star
2020-04-30 17:15:44 +09:30
}
const SLIDE_TYPES = {
' 0 ' : null , # Seems to be used for stars without slides attached
' 1 ' : Note . SlideType . CHORD ,
2021-01-16 02:56:32 +10:30
' 2 ' : Note . SlideType . ARC_ACW ,
' 3 ' : Note . SlideType . ARC_CW ,
' 4 ' : Note . SlideType . COMPLEX , # From nekomatsuri master - Loop ACW around center. Size of loop is roughly inscribed in chords of 0-3, 1-4, 2-5... NB: doesn't loop if directly opposite col
' 5 ' : Note . SlideType . COMPLEX , # CW of above
' 6 ' : Note . SlideType . COMPLEX , # S zigzag through center
' 7 ' : Note . SlideType . COMPLEX , # Z zigzag through center
' 8 ' : Note . SlideType . COMPLEX , # V into center
' 9 ' : Note . SlideType . COMPLEX , # From nekomatsuri master - Seems to loop around to center ACW to make a + to the end
' a ' : Note . SlideType . COMPLEX , # CW of above
' b ' : Note . SlideType . COMPLEX , # V into column 2 places ACW
' c ' : Note . SlideType . COMPLEX , # V into column 2 places CW
2020-05-01 15:45:28 +09:30
' d ' : Note . SlideType . CHORD_TRIPLE , # Triple cone. Spreads out to the adjacent receptors of the target.
2021-01-16 02:56:32 +10:30
' e ' : Note . SlideType . CHORD , # Not used in any of our charts
' f ' : Note . SlideType . CHORD , # Not used in any of our charts
2020-04-30 17:15:44 +09:30
}
2021-01-16 02:56:32 +10:30
const SLIDE_IN_R : = sin ( PI / 8 ) # Circle radius circumscribed by chords 0-3, 1-4, 2-5 etc.
2020-05-01 15:45:28 +09:30
2020-04-30 17:15:44 +09:30
static func load_file ( filename : String ) :
var extension = filename . rsplit ( ' . ' , false , 1 ) [ 1 ]
if not EXTENSIONS . has ( extension ) :
return - 1
var format = EXTENSIONS [ extension ]
var file : = File . new ( )
var err : = file . open ( filename , File . READ )
if err != OK :
print ( err )
return err
var length = file . get_len ( )
2020-05-01 15:45:28 +09:30
var chart_ids = [ ]
2020-04-30 17:15:44 +09:30
var lines = [ [ ] ]
2020-05-01 15:45:28 +09:30
# This loop will segment the lines as if the file were RGTM
2020-04-30 17:15:44 +09:30
while ( file . get_position ( ) < ( length - 1 ) ) : # Could probably replace this with file.eof_reached()
var line : String = file . get_line ( )
if line . begins_with ( ' [ ' ) : # Split to a new list for each chart definition
2020-05-01 15:45:28 +09:30
chart_ids . append ( line . lstrip ( ' [ ' ) . rstrip ( ' ] ' ) )
2020-04-30 17:15:44 +09:30
lines . append ( [ ] )
2020-05-01 15:45:28 +09:30
elif ! line . empty ( ) :
2020-12-25 23:11:09 +10:30
lines [ - 1 ] . push_back ( line )
2020-04-30 17:15:44 +09:30
file . close ( )
2020-12-25 23:11:09 +10:30
print ( ' Parsing chart: ' , filename )
2020-04-30 17:15:44 +09:30
match format :
Format . RGTS :
2021-02-07 00:17:27 +10:30
var metadata_and_notes = parse_rgts ( lines [ 0 ] )
return metadata_and_notes
2020-04-30 17:15:44 +09:30
Format . RGTX :
2021-02-07 00:17:27 +10:30
var metadata_and_notes = parse_rgtx ( lines [ 0 ] )
return metadata_and_notes
2020-04-30 17:15:44 +09:30
Format . RGTM :
2020-05-01 15:45:28 +09:30
lines . pop_front ( ) # Anything before the first [header] is meaningless
2020-05-02 15:45:27 +09:30
var charts = { }
for i in len ( lines ) :
charts [ chart_ids [ i ] ] = parse_rgts ( lines [ i ] )
return charts
2020-04-30 17:15:44 +09:30
return format
2020-12-25 23:11:09 +10:30
static func parse_rgtx ( lines : PoolStringArray ) :
2020-05-01 15:45:28 +09:30
return [ ] # To be implemented later
2020-05-03 01:47:55 +09:30
const beats_per_measure = 4.0 # TODO: Bit of an ugly hack, need to revisit this later
2020-12-25 23:11:09 +10:30
static func parse_rgts ( lines : PoolStringArray ) :
var metadata : = { }
var num_taps : = 0
var num_holds : = 0
var num_slides : = 0
var notes : = [ ]
var slide_ids : = { }
var slide_stars : = { } # Multiple stars might link to one star. We only care about linking for the spin speed.
var last_star : = [ ]
2020-05-01 01:26:46 +09:30
for i in Rules . COLS :
last_star . append ( null )
2020-04-30 17:15:44 +09:30
for line in lines :
if len ( line ) < 4 : # shortest legal line would be like '1:1t'
continue
var s = line . split ( ' : ' )
2020-12-25 23:11:09 +10:30
var time : = float ( s [ 0 ] ) * beats_per_measure
var note_hits : = [ ]
var note_nonhits : = [ ]
2020-04-30 17:15:44 +09:30
for i in range ( 1 , len ( s ) ) :
var n = s [ i ]
2020-12-25 23:11:09 +10:30
var column : = int ( n [ 0 ] )
2020-04-30 17:15:44 +09:30
var ntype = n [ 1 ]
n = n . substr ( 2 )
match ntype :
2021-01-21 18:31:02 +10:30
' t ' , ' b ' : # tap
note_hits . append ( Note . NoteTap . new ( time , column , ntype == ' b ' ) )
2020-12-25 23:11:09 +10:30
num_taps += 1
2020-04-30 17:15:44 +09:30
' h ' : # hold
2020-05-03 01:47:55 +09:30
var duration = float ( n ) * beats_per_measure
2020-04-30 17:15:44 +09:30
note_hits . append ( Note . NoteHold . new ( time , column , duration ) )
2020-12-25 23:11:09 +10:30
num_holds += 1
2021-01-21 18:31:02 +10:30
' s ' , ' x ' : # slide star
var star = Note . NoteStar . new ( time , column , ntype == ' z ' )
2020-05-01 01:26:46 +09:30
note_hits . append ( star )
2020-12-25 23:11:09 +10:30
num_slides += 1
2021-01-21 18:31:02 +10:30
last_star [ column ] = star
if len ( n ) > 1 : # Not all stars have proper slide info
var slide_type = n [ 0 ] # hex digit
var slide_id = int ( n . substr ( 1 ) )
if slide_id > 0 :
slide_stars [ slide_id ] = star
var slide = Note . NoteSlide . new ( time , column )
slide_ids [ slide_id ] = slide
note_nonhits . append ( slide )
2020-04-30 17:15:44 +09:30
' e ' : # slide end
var slide_type = n [ 0 ] # numeric digit, left as str just in case
var slide_id = int ( n . substr ( 1 ) )
2020-05-01 01:26:46 +09:30
if slide_id in slide_ids : # Classic slide end
slide_ids [ slide_id ] . time_release = time
if slide_id in slide_stars :
slide_stars [ slide_id ] . duration = slide_ids [ slide_id ] . duration # Should probably recalc in case start time is different but w/e
slide_ids [ slide_id ] . column_release = column
slide_ids [ slide_id ] . slide_type = SLIDE_TYPES [ slide_type ]
slide_ids [ slide_id ] . update_slide_variables ( )
2021-01-16 02:56:32 +10:30
if SLIDE_TYPES [ slide_type ] == Note . SlideType . COMPLEX :
var col_hit = slide_ids [ slide_id ] . column
var RUV = GameTheme . RADIAL_UNIT_VECTORS
slide_ids [ slide_id ] . values . curve2d . add_point ( RUV [ col_hit ] ) # Start col
match slide_type :
' 4 ' : # TODO: Loop ACW around center. Size of loop is roughly inscribed in chords of 0-3, 1-4, 2-5... NB: doesn't loop if directly opposite col
slide_ids [ slide_id ] . values . curve2d . add_point ( ( RUV [ posmod ( col_hit - 3 , Rules . COLS ) ] + RUV [ col_hit ] ) * 0.5 )
' 5 ' : # TODO: CW of above
slide_ids [ slide_id ] . values . curve2d . add_point ( ( RUV [ posmod ( col_hit + 3 , Rules . COLS ) ] + RUV [ col_hit ] ) * 0.5 )
' 6 ' : # S zigzag through center
slide_ids [ slide_id ] . values . curve2d . add_point ( RUV [ posmod ( col_hit - 2 , Rules . COLS ) ] * SLIDE_IN_R )
slide_ids [ slide_id ] . values . curve2d . add_point ( RUV [ posmod ( col_hit + 2 , Rules . COLS ) ] * SLIDE_IN_R )
' 7 ' : # Z zigzag through center
slide_ids [ slide_id ] . values . curve2d . add_point ( RUV [ posmod ( col_hit + 2 , Rules . COLS ) ] * SLIDE_IN_R )
slide_ids [ slide_id ] . values . curve2d . add_point ( RUV [ posmod ( col_hit - 2 , Rules . COLS ) ] * SLIDE_IN_R )
' 8 ' : # V into center
slide_ids [ slide_id ] . values . curve2d . add_point ( Vector2 . ZERO )
' 9 ' : # TODO: From nekomatsuri master - Seems to loop around to center ACW to make a + to the end
slide_ids [ slide_id ] . values . curve2d . add_point ( Vector2 . ZERO )
' a ' : # TODO: CW of above
slide_ids [ slide_id ] . values . curve2d . add_point ( Vector2 . ZERO )
' b ' : # V into column 2 places ACW
slide_ids [ slide_id ] . values . curve2d . add_point ( GameTheme . RADIAL_UNIT_VECTORS [ posmod ( col_hit - 2 , Rules . COLS ) ] )
' c ' : # V into column 2 places CW
slide_ids [ slide_id ] . values . curve2d . add_point ( GameTheme . RADIAL_UNIT_VECTORS [ posmod ( col_hit + 2 , Rules . COLS ) ] )
slide_ids [ slide_id ] . values . curve2d . add_point ( GameTheme . RADIAL_UNIT_VECTORS [ column ] ) # End col
2020-05-01 01:26:46 +09:30
else : # Naked slide start
if last_star [ column ] != null :
slide_stars [ slide_id ] = last_star [ column ]
else :
print_debug ( ' Naked slide with no prior star in column! ' )
var note = Note . NoteSlide . new ( time , column )
slide_ids [ slide_id ] = note
note_nonhits . append ( note )
2021-01-21 18:31:02 +10:30
' _ ' :
print_debug ( ' Unknown note type: ' , ntype )
2020-04-30 17:15:44 +09:30
if len ( note_hits ) > 1 :
2020-05-01 01:26:46 +09:30
for note in note_hits : # Set multihit on each one
note . double_hit = true
notes += note_hits + note_nonhits
2020-12-25 23:11:09 +10:30
metadata [ ' num_taps ' ] = num_taps
metadata [ ' num_holds ' ] = num_holds
metadata [ ' num_slides ' ] = num_slides
return [ metadata , notes ]
2019-11-13 00:48:06 +10:30
2020-03-29 17:43:28 +10:30
class SM :
# Stepmania simfile
const NOTE_VALUES = {
' 0 ' : ' None ' ,
' 1 ' : ' Tap ' ,
' 2 ' : ' HoldStart ' ,
' 3 ' : ' HoldRollEnd ' ,
' 4 ' : ' RollStart ' ,
' M ' : ' Mine ' ,
# These three are less likely to show up anywhere, no need to implement
' K ' : ' Keysound ' ,
' L ' : ' Lift ' ,
' F ' : ' Fake ' ,
}
const CHART_DIFFICULTIES = {
' Beginner ' : 0 ,
' Easy ' : 1 ,
' Medium ' : 2 ,
' Hard ' : 3 ,
' Challenge ' : 4 ,
' Edit ' : 5 ,
# Some will just write whatever for special difficulties, but we should at least color-code these standard ones
}
const TAG_TRANSLATIONS = {
' #TITLE ' : ' title ' ,
' #SUBTITLE ' : ' subtitle ' ,
' #ARTIST ' : ' artist ' ,
' #TITLETRANSLIT ' : ' title_transliteration ' ,
' #SUBTITLETRANSLIT ' : ' subtitle_transliteration ' ,
' #ARTISTTRANSLIT ' : ' artist_transliteration ' ,
' #GENRE ' : ' genre ' ,
' #CREDIT ' : ' chart_author ' ,
' #BANNER ' : ' image_banner ' ,
' #BACKGROUND ' : ' image_background ' ,
2020-03-30 23:21:20 +10:30
# '#LYRICSPATH': '',
2020-03-29 17:43:28 +10:30
' #CDTITLE ' : ' image_cd_title ' ,
' #MUSIC ' : ' audio_filelist ' ,
' #OFFSET ' : ' audio_offsets ' ,
' #SAMPLESTART ' : ' audio_preview_times ' ,
' #SAMPLELENGTH ' : ' audio_preview_times ' ,
2020-03-30 23:21:20 +10:30
# '#SELECTABLE': '',
2020-03-29 17:43:28 +10:30
' #BPMS ' : ' bpm_values ' ,
2020-03-30 23:21:20 +10:30
# '#STOPS': '',
# '#BGCHANGES': '',
# '#KEYSOUNDS': '',
2020-03-29 17:43:28 +10:30
}
static func load_chart ( lines ) :
var metadata = { }
var notes = [ ]
assert ( lines [ 0 ] . begins_with ( ' #NOTES: ' ) )
metadata [ ' chart_type ' ] = lines [ 1 ] . strip_edges ( ) . rstrip ( ' : ' )
metadata [ ' description ' ] = lines [ 2 ] . strip_edges ( ) . rstrip ( ' : ' )
metadata [ ' difficulty_str ' ] = lines [ 3 ] . strip_edges ( ) . rstrip ( ' : ' )
metadata [ ' numerical_meter ' ] = lines [ 4 ] . strip_edges ( ) . rstrip ( ' : ' )
metadata [ ' groove_radar ' ] = lines [ 5 ] . strip_edges ( ) . rstrip ( ' : ' )
# Measures are separated by lines that start with a comma
# Each line has a state for each of the pads, e.g. '0000' for none pressed
# The lines become even subdivisions of the measure, so if there's 4 lines everything represents a 1/4 beat, if there's 8 lines everything represents a 1/8 beat etc.
# For this reason it's probably best to just have a float for beat-within-measure rather than integer beats.
var measures = [ [ ] ]
for i in range ( 6 , len ( lines ) ) :
var line = lines [ i ] . strip_edges ( )
if line . begins_with ( ' , ' ) :
measures . append ( [ ] )
elif line . begins_with ( ' ; ' ) :
break
elif len ( line ) > 0 :
measures [ - 1 ] . append ( line )
var ongoing_holds = { }
var num_notes : = 0
var num_jumps : = 0
var num_hands : = 0
var num_holds : = 0
var num_rolls : = 0
var num_mines : = 0
for measure in range ( len ( measures ) ) :
var m_lines = measures [ measure ]
var m_length = len ( m_lines ) # Divide out all lines by this
for beat in m_length :
var line : String = m_lines [ beat ]
# Jump check at a line-level (check for multiple 1/2/4s)
var hits : int = line . count ( ' 1 ' ) + line . count ( ' 2 ' ) + line . count ( ' 4 ' )
# Hand/quad check more complex as need to check hold/roll state as well
# TODO: are they exclusive? Does quad override hand override jump? SM5 doesn't have quads and has hands+jumps inclusive
var total_pressed : int = hits + len ( ongoing_holds )
var jump : bool = hits > = 2
var hand : bool = total_pressed > = 3
# var quad : bool = total_pressed >= 4
num_notes += hits
num_jumps += int ( jump )
num_hands += int ( hand )
var time = measure + beat / float ( m_length )
for col in len ( line ) :
match line [ col ] :
' 1 ' :
2020-04-30 17:15:44 +09:30
notes . append ( Note . NoteTap . new ( time , col ) )
2020-03-29 17:43:28 +10:30
' 2 ' : # Hold
ongoing_holds [ col ] = len ( notes )
2020-04-30 17:15:44 +09:30
notes . append ( Note . NoteHold . new ( time , col , 0.0 ) )
2020-03-30 23:21:20 +10:30
num_holds += 1
2020-03-29 17:43:28 +10:30
' 4 ' : # Roll
ongoing_holds [ col ] = len ( notes )
2020-04-30 17:15:44 +09:30
notes . append ( Note . NoteRoll . new ( time , col , 0.0 ) )
2020-03-30 23:21:20 +10:30
num_rolls += 1
2020-03-29 17:43:28 +10:30
' 3 ' : # End Hold/Roll
assert ( ongoing_holds . has ( col ) )
notes [ ongoing_holds [ col ] ] . set_time_release ( time )
2020-03-30 23:21:20 +10:30
ongoing_holds . erase ( col )
2020-03-29 17:43:28 +10:30
' M ' : # Mine
num_mines += 1
pass
metadata [ ' num_notes ' ] = num_notes
2020-03-30 23:21:20 +10:30
metadata [ ' num_taps ' ] = num_notes - num_jumps
2020-03-29 17:43:28 +10:30
metadata [ ' num_jumps ' ] = num_jumps
metadata [ ' num_hands ' ] = num_hands
metadata [ ' num_holds ' ] = num_holds
metadata [ ' num_rolls ' ] = num_rolls
metadata [ ' num_mines ' ] = num_mines
return [ metadata , notes ]
2020-12-25 23:11:09 +10:30
static func load_file ( filename : String ) - > Array :
# Output is [metadata, [[meta0, chart0], ..., [metaN, chartN]]]
2020-03-29 17:43:28 +10:30
# Technically, declarations end with a semicolon instead of a linebreak.
# This is a PITA to do correctly in GDScript and the files in our collection are well-behaved with linebreaks anyway, so we won't bother.
var file : = File . new ( )
2020-12-25 23:11:09 +10:30
match file . open ( filename , File . READ ) :
OK :
pass
var err :
print_debug ( ' Error loading file: ' , err )
return [ ]
2020-03-29 17:43:28 +10:30
var length = file . get_len ( )
var lines = [ [ ] ] # First list will be header, then every subsequent one is a chart
while ( file . get_position ( ) < ( length - 1 ) ) : # Could probably replace this with file.eof_reached()
2020-03-30 23:21:20 +10:30
var line : String = file . get_line ( )
2020-03-29 17:43:28 +10:30
if line . begins_with ( ' #NOTES ' ) : # Split to a new list for each chart definition
lines . append ( [ ] )
lines [ - 1 ] . append ( line )
file . close ( )
var metadata = { }
for line in lines [ 0 ] :
var tokens = line . rstrip ( ' ; ' ) . split ( ' : ' )
if TAG_TRANSLATIONS . has ( tokens [ 0 ] ) :
metadata [ TAG_TRANSLATIONS [ tokens [ 0 ] ] ] = tokens [ 1 ]
2020-03-30 23:21:20 +10:30
elif len ( tokens ) > = 2 :
metadata [ tokens [ 0 ] ] = tokens [ 1 ]
2020-03-29 17:43:28 +10:30
var charts = [ ]
for i in range ( 1 , len ( lines ) ) :
charts . append ( load_chart ( lines [ i ] ) )
2020-03-30 23:21:20 +10:30
return [ metadata , charts ]
2020-03-29 17:43:28 +10:30
2019-11-13 00:48:06 +10:30
class Test :
static func stress_pattern ( ) :
var notes = [ ]
for bar in range ( 8 ) :
2020-04-30 17:15:44 +09:30
notes . push_back ( Note . NoteHold . new ( bar * 4 , bar % 8 , 1 ) )
2019-11-13 00:48:06 +10:30
for i in range ( 1 , 8 ) :
2020-04-30 17:15:44 +09:30
notes . push_back ( Note . NoteTap . new ( bar * 4 + ( i / 2.0 ) , ( bar + i ) % 8 ) )
notes . push_back ( Note . NoteTap . new ( bar * 4 + ( 7 / 2.0 ) , ( bar + 3 ) % 8 ) )
2019-11-13 00:48:06 +10:30
for bar in range ( 8 , 16 ) :
2020-04-30 17:15:44 +09:30
notes . push_back ( Note . NoteHold . new ( bar * 4 , bar % 8 , 2 ) )
2019-11-13 00:48:06 +10:30
for i in range ( 1 , 8 ) :
2020-04-30 17:15:44 +09:30
notes . push_back ( Note . NoteTap . new ( bar * 4 + ( i / 2.0 ) , ( bar + i ) % 8 ) )
notes . push_back ( Note . NoteTap . new ( bar * 4 + ( ( i + 0.5 ) / 2.0 ) , ( bar + i ) % 8 ) )
2019-11-13 00:48:06 +10:30
notes . push_back ( Note . make_slide ( bar * 4 + ( ( i + 1 ) / 2.0 ) , 1 , ( bar + i ) % 8 , 0 ) )
for bar in range ( 16 , 24 ) :
2020-04-30 17:15:44 +09:30
notes . push_back ( Note . NoteHold . new ( bar * 4 , bar % 8 , 2 ) )
notes . push_back ( Note . NoteHold . new ( bar * 4 , ( bar + 1 ) % 8 , 1 ) )
2019-11-13 00:48:06 +10:30
for i in range ( 2 , 8 ) :
2020-04-30 17:15:44 +09:30
notes . push_back ( Note . NoteTap . new ( bar * 4 + ( i / 2.0 ) , ( bar + i ) % 8 ) )
notes . push_back ( Note . NoteHold . new ( bar * 4 + ( ( i + 1 ) / 2.0 ) , ( bar + i ) % 8 , 0.5 ) )
2019-11-13 00:48:06 +10:30
for bar in range ( 24 , 32 ) :
2020-04-30 17:15:44 +09:30
notes . push_back ( Note . NoteHold . new ( bar * 4 , bar % 8 , 1 ) )
2019-11-13 00:48:06 +10:30
for i in range ( 1 , 32 ) :
2020-04-30 17:15:44 +09:30
notes . push_back ( Note . NoteTap . new ( bar * 4 + ( i / 8.0 ) , ( bar + i ) % 8 ) )
2019-11-13 00:48:06 +10:30
if ( i % 2 ) > 0 :
2020-04-30 17:15:44 +09:30
notes . push_back ( Note . NoteTap . new ( bar * 4 + ( i / 8.0 ) , ( bar + i + 4 ) % 8 ) )
2019-11-13 00:48:06 +10:30
for bar in range ( 32 , 48 ) :
2020-04-30 17:15:44 +09:30
notes . push_back ( Note . NoteHold . new ( bar * 4 , bar % 8 , 1 ) )
2019-11-13 00:48:06 +10:30
for i in range ( 1 , 32 ) :
2020-04-30 17:15:44 +09:30
notes . push_back ( Note . NoteTap . new ( bar * 4 + ( i / 8.0 ) , ( bar + i ) % 8 ) )
notes . push_back ( Note . NoteTap . new ( bar * 4 + ( i / 8.0 ) , ( bar + i + 3 ) % 8 ) )
2019-11-22 23:59:38 +10:30
return notes
2020-05-01 15:45:28 +09:30
func load_folder ( folder , filename = ' song ' ) :
2019-11-22 23:59:38 +10:30
var file = File . new ( )
2020-05-01 15:45:28 +09:30
var err = file . open ( ' %s / %s .json ' % [ folder , filename ] , File . READ )
2019-11-22 23:59:38 +10:30
if err != OK :
print ( err )
return err
var result_json = JSON . parse ( file . get_as_text ( ) )
file . close ( )
if result_json . error != OK :
2020-05-01 15:45:28 +09:30
print ( ' Error: ' , result_json . error )
print ( ' Error Line: ' , result_json . error_line )
print ( ' Error String: ' , result_json . error_string )
2019-11-22 23:59:38 +10:30
return result_json . error
var result = result_json . result
2019-11-25 22:35:31 +10:30
result . directory = folder
2019-12-11 23:55:25 +10:30
return result
2020-05-03 00:11:01 +09:30
func load_filelist ( filelist : Array , directory = ' ' ) :
2020-05-02 15:45:27 +09:30
var charts = { }
2020-12-27 17:51:40 +10:30
var key : = 0
2020-12-21 22:52:15 +10:30
for name in filelist :
var extension : String = name . rsplit ( ' . ' , true , 1 ) [ - 1 ]
name = directory . rstrip ( ' / ' ) + ' / ' + name
var filename = find_file ( name )
if filename != ' ' :
match extension :
' rgtm ' : # multiple charts
var res = RGT . load_file ( filename )
for k in res :
2020-12-27 17:51:40 +10:30
charts [ Library . difficulty_translations . get ( k , k ) ] = res [ k ]
' rgts ' , ' rgtx ' : # single chart - The keys for this should be translated afterwards
2020-12-21 22:52:15 +10:30
charts [ key ] = RGT . load_file ( filename )
key += 1
' srt ' : # maimai, single chart
2021-02-07 00:17:27 +10:30
var metadata_and_notes = SRT . load_file ( filename )
Note . process_note_list ( metadata_and_notes [ 1 ] ) # SRT doesn't handle doubles
charts [ key ] = metadata_and_notes
2020-12-21 22:52:15 +10:30
key += 1
' sm ' : # Stepmania, multiple charts
var res = SM . load_file ( filename )
2020-12-25 23:11:09 +10:30
for chart in res [ 1 ] :
var diff = chart [ 0 ] . difficulty_str
charts [ diff ] = chart [ 1 ]
2020-12-21 22:52:15 +10:30
_ :
pass
2020-05-02 15:45:27 +09:30
return charts
2020-12-21 22:52:15 +10:30
func direct_load_ogg ( filename : String ) - > AudioStreamOGGVorbis :
# Loads the ogg file with that exact filename
2019-12-11 23:55:25 +10:30
var audiostream = AudioStreamOGGVorbis . new ( )
var oggfile = File . new ( )
oggfile . open ( filename , File . READ )
audiostream . set_data ( oggfile . get_buffer ( oggfile . get_len ( ) ) )
oggfile . close ( )
return audiostream
2020-05-03 00:11:01 +09:30
var fallback_audiostream = AudioStreamOGGVorbis . new ( )
2020-12-21 22:52:15 +10:30
func load_ogg ( name : String ) - > AudioStreamOGGVorbis :
# Searches through all of the paths to find the file
match find_file ( name ) :
' ' : return fallback_audiostream
var filename : return direct_load_ogg ( filename )
2020-05-03 00:11:01 +09:30
2020-05-03 01:47:55 +09:30
var fallback_videostream = VideoStreamWebm . new ( )
2020-12-21 22:52:15 +10:30
func load_video ( name : String ) :
match find_file ( name ) :
' ' : return fallback_videostream
var filename :
return load ( filename )
# var videostream = VideoStreamGDNative.new()
# videostream.set_file(filename1)
# return videostream
func direct_load_image ( filename : String ) - > ImageTexture :
2020-05-03 00:11:01 +09:30
var tex : = ImageTexture . new ( )
var img : = Image . new ( )
2019-12-11 23:55:25 +10:30
img . load ( filename )
tex . create_from_image ( img )
2020-03-12 23:05:12 +10:30
return tex
2020-05-03 00:11:01 +09:30
var fallback_texture : = ImageTexture . new ( )
2020-12-21 22:52:15 +10:30
func load_image ( name : String ) - > ImageTexture :
var filename = find_file ( name )
if filename != ' ' :
return direct_load_image ( filename )
print ( ' File not found: ' , name )
2020-05-03 00:11:01 +09:30
return fallback_texture
func init_directory ( directory : String ) :
var dir = Directory . new ( )
var err = dir . make_dir_recursive ( directory )
if err != OK :
print ( ' An error occurred while trying to create the scores directory: ' , err , ERROR_CODES [ err ] )
return err
func save_json ( filename : String , data : Dictionary ) :
filename = userroot + filename
var dir = filename . rsplit ( ' / ' , true , 1 ) [ 0 ]
2020-12-25 23:11:09 +10:30
match FileLoader . init_directory ( dir ) :
OK :
pass
var err :
print_debug ( ' Error making directory for JSON file: ' , err , ERROR_CODES [ err ] )
return err
2020-05-03 00:11:01 +09:30
var json = JSON . print ( data )
var file = File . new ( )
2020-12-25 23:11:09 +10:30
match file . open ( filename , File . WRITE ) :
OK :
file . store_string ( json )
file . close ( )
return OK
var err :
print_debug ( ' Error saving JSON file: ' , err , ERROR_CODES [ err ] )
return err
2020-05-03 00:11:01 +09:30
func load_json ( filename : String ) :
var file = File . new ( )
var err
for root in PATHS :
var filename1 = root + filename
if file . file_exists ( filename1 ) :
err = file . open ( filename1 , File . READ )
if err != OK :
print ( ' An error occurred while trying to open file: ' , filename1 , err , ERROR_CODES [ err ] )
continue # return err
var result_json = JSON . parse ( file . get_as_text ( ) )
file . close ( )
if result_json . error != OK :
print ( ' Error: ' , result_json . error )
print ( ' Error Line: ' , result_json . error_line )
print ( ' Error String: ' , result_json . error_string )
return result_json . error
return result_json . result
print ( ' File not found in any libraries: ' , filename )
return ERR_FILE_NOT_FOUND