2019-11-10 15:09:14 +10:30
#extends Object
extends Node
2020-03-29 17:43:28 +10:30
var userroot : = " user:// " if OS . get_name ( ) != " Android " else " /storage/emulated/0/RhythmGame/ "
# 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-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
func find_by_extensions ( array , extensions ) - > Dictionary :
# Both args can be Array or PoolStringArray
var output = { }
for ext in extensions :
output [ ext ] = [ ]
for string in array :
for ext in extensions :
if string . ends_with ( ext ) :
output [ ext ] . append ( string )
2020-03-29 17:43:28 +10:30
return output
func scan_library ( ) :
print ( " Scanning library " )
var rootdir = userroot + " songs "
var dir = Directory . new ( )
var err = dir . make_dir_recursive ( rootdir )
if err != OK :
print_debug ( " An error occurred while trying to create the songs directory: " , err )
return err
var songslist = directory_list ( rootdir , false )
if songslist . err != OK :
print ( " An error occurred when trying to access the songs directory: " , songslist . err )
return songslist . err
var song_defs = { }
var song_images = { }
var genres = { }
dir . open ( rootdir )
for key in songslist . folders :
if dir . file_exists ( key + " /song.json " ) :
2020-03-30 23:21:20 +10:30
# Our format
2020-03-29 17:43:28 +10:30
song_defs [ key ] = FileLoader . load_folder ( " %s / %s " % [ rootdir , key ] )
print ( " Loaded song directory: %s " % key )
song_images [ key ] = FileLoader . load_image ( " %s / %s / %s " % [ rootdir , key , song_defs [ key ] [ " tile_filename " ] ] )
if song_defs [ key ] [ " genre " ] in genres :
genres [ song_defs [ key ] [ " genre " ] ] . append ( key )
else :
genres [ song_defs [ key ] [ " genre " ] ] = [ key ]
else :
2020-03-30 23:21:20 +10:30
var step_files = find_by_extensions ( directory_list ( rootdir + ' / ' + key , false ) . files , [ ' .sm ' ] )
if len ( step_files [ ' .sm ' ] ) > 0 :
var sm_filename = step_files [ ' .sm ' ] [ 0 ]
print ( sm_filename )
var thing = SM . load_file ( rootdir + ' / ' + key + ' / ' + sm_filename )
print ( thing )
pass
else :
print ( " Found non-song directory: " + key )
2020-03-29 17:43:28 +10:30
for file in songslist . files :
print ( " Found file: " + file )
return { song_defs = song_defs , song_images = song_images , genres = genres }
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
var notes = [ ]
var beats_per_measure : = 4
var length = file . get_len ( )
2019-11-13 00:48:06 +10:30
var slide_idxs = { }
2019-11-10 15:09:14 +10:30
while ( file . get_position ( ) < ( length - 2 ) ) :
var noteline = file . get_csv_line ( )
2019-11-22 13:33:47 +10:30
var time_hit : = ( float ( noteline [ 0 ] ) + ( float ( noteline [ 1 ] ) ) - 1.0 ) * 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 :
notes . push_back ( Note . make_hold ( time_hit , duration , column ) )
ID_BREAK :
notes . push_back ( Note . make_break ( time_hit , column ) )
ID_SLIDE_END :
2019-11-13 00:48:06 +10:30
# id2 is slide ID
if id2 in slide_idxs :
notes [ slide_idxs [ id2 ] ] . column_release = column
2019-11-14 22:27:30 +10:30
notes [ slide_idxs [ id2 ] ] . update_slide_variables ( )
2019-11-10 15:09:14 +10:30
_ :
if id2 == 0 :
notes . push_back ( Note . make_tap ( time_hit , column ) )
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-13 00:48:06 +10:30
slide_idxs [ id2 ] = len ( notes )
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
_ :
print ( " Unknown slide type: " , id3 )
2019-11-14 22:27:30 +10:30
notes . push_back ( Note . NoteSlide . new ( time_hit , duration , column , - 1 , slide_type ) )
2019-11-10 15:09:14 +10:30
return notes
2019-11-13 00:48:06 +10:30
2019-11-10 15:09:14 +10:30
class SRB :
2019-11-13 00:48:06 +10:30
static func load_file ( filename ) :
2019-11-10 15:09:14 +10:30
pass
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 ' :
notes . append ( Note . make_tap ( time , col ) )
' 2 ' : # Hold
ongoing_holds [ col ] = len ( notes )
2020-03-30 23:21:20 +10:30
notes . append ( Note . make_hold ( time , 0.0 , col ) )
num_holds += 1
2020-03-29 17:43:28 +10:30
' 4 ' : # Roll
ongoing_holds [ col ] = len ( notes )
2020-03-30 23:21:20 +10:30
notes . append ( Note . make_roll ( time , 0.0 , col ) )
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 ]
static func load_file ( filename ) :
# 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 ( )
var err : = file . open ( filename , File . READ )
if err != OK :
print ( err )
return err
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 ) :
notes . push_back ( Note . make_hold ( bar * 4 , 1 , bar % 8 ) )
for i in range ( 1 , 8 ) :
notes . push_back ( Note . make_tap ( bar * 4 + ( i / 2.0 ) , ( bar + i ) % 8 ) )
notes . push_back ( Note . make_tap ( bar * 4 + ( 7 / 2.0 ) , ( bar + 3 ) % 8 ) )
for bar in range ( 8 , 16 ) :
notes . push_back ( Note . make_hold ( bar * 4 , 2 , bar % 8 ) )
for i in range ( 1 , 8 ) :
notes . push_back ( Note . make_tap ( bar * 4 + ( i / 2.0 ) , ( bar + i ) % 8 ) )
notes . push_back ( Note . make_tap ( bar * 4 + ( ( i + 0.5 ) / 2.0 ) , ( bar + i ) % 8 ) )
notes . push_back ( Note . make_slide ( bar * 4 + ( ( i + 1 ) / 2.0 ) , 1 , ( bar + i ) % 8 , 0 ) )
for bar in range ( 16 , 24 ) :
notes . push_back ( Note . make_hold ( bar * 4 , 2 , bar % 8 ) )
notes . push_back ( Note . make_hold ( bar * 4 , 1 , ( bar + 1 ) % 8 ) )
for i in range ( 2 , 8 ) :
notes . push_back ( Note . make_tap ( bar * 4 + ( i / 2.0 ) , ( bar + i ) % 8 ) )
notes . push_back ( Note . make_hold ( bar * 4 + ( ( i + 1 ) / 2.0 ) , 0.5 , ( bar + i ) % 8 ) )
for bar in range ( 24 , 32 ) :
notes . push_back ( Note . make_hold ( bar * 4 , 1 , bar % 8 ) )
for i in range ( 1 , 32 ) :
notes . push_back ( Note . make_tap ( bar * 4 + ( i / 8.0 ) , ( bar + i ) % 8 ) )
if ( i % 2 ) > 0 :
notes . push_back ( Note . make_tap ( bar * 4 + ( i / 8.0 ) , ( bar + i + 4 ) % 8 ) )
for bar in range ( 32 , 48 ) :
notes . push_back ( Note . make_hold ( bar * 4 , 1 , bar % 8 ) )
for i in range ( 1 , 32 ) :
notes . push_back ( Note . make_tap ( bar * 4 + ( i / 8.0 ) , ( bar + i ) % 8 ) )
notes . push_back ( Note . make_tap ( bar * 4 + ( i / 8.0 ) , ( bar + i + 3 ) % 8 ) )
2019-11-22 23:59:38 +10:30
return notes
func load_folder ( folder ) :
var file = File . new ( )
var err = file . open ( " %s /song.json " % folder , File . READ )
if err != OK :
print ( err )
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
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
func load_ogg ( filename ) - > AudioStreamOGGVorbis :
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
func load_image ( filename ) - > ImageTexture :
var tex = ImageTexture . new ( )
var img = Image . new ( )
img . load ( filename )
tex . create_from_image ( img )
2020-03-12 23:05:12 +10:30
return tex