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 :
2020-04-30 17:15:44 +09:30
notes . push_back ( Note . NoteHold . new ( time_hit , column , duration ) )
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 ) )
2019-11-10 15:09:14 +10:30
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 :
2020-04-30 17:15:44 +09:30
notes . push_back ( Note . NoteTap . new ( time_hit , column ) )
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-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 )
2020-04-30 17:15:44 +09:30
notes . push_back ( Note . NoteSlide . new ( time_hit , column , duration , - 1 , slide_type ) )
2019-11-10 15:09:14 +10:30
return notes
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 ,
' s ' : Note . NOTE_SLIDE ,
' e ' : Note . NOTE_SLIDE ,
' b ' : Note . NOTE_TAP # Break
}
const SLIDE_TYPES = {
' 0 ' : null , # Seems to be used for stars without slides attached
' 1 ' : Note . SlideType . CHORD ,
' 2 ' : Note . SlideType . ARC_ACW , # From Cirno master
' 3 ' : Note . SlideType . ARC_CW , # From Cirno master
' 4 ' : Note . SlideType . CHORD , # Probably some weird loop etc.
' 5 ' : Note . SlideType . CHORD , # Probably some weird loop etc.
}
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 ( )
var lines = [ [ ] ]
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
lines . append ( [ ] )
lines [ - 1 ] . append ( line )
file . close ( )
match format :
Format . RGTS :
pass
Format . RGTX :
pass
Format . RGTM :
pass
return format
static func parse_rgts ( lines ) :
var notes = [ ]
var slide_ids = { }
for line in lines :
if len ( line ) < 4 : # shortest legal line would be like '1:1t'
continue
var s = line . split ( ' : ' )
var time = float ( s [ 0 ] )
var note_hits = [ ]
var note_nonhits = [ ]
for i in range ( 1 , len ( s ) ) :
var n = s [ i ]
var column = n [ 0 ]
var ntype = n [ 1 ]
n = n . substr ( 2 )
match ntype :
' t ' : # tap
note_hits . append ( Note . NoteTap . new ( time , column ) )
' b ' : # break
note_hits . append ( Note . NoteTap . new ( time , column , true ) )
' h ' : # hold
var duration = float ( n )
note_hits . append ( Note . NoteHold . new ( time , column , duration ) )
' s ' : # slide star
var slide_type = n [ 0 ] # numeric digit, left as str just in case
var slide_id = int ( n . substr ( 1 ) )
# var note = Note.NoteSlide.new(time, column)
# if slide_id > 0:
# slide_ids[slide_id] = note
' e ' : # slide end
var slide_type = n [ 0 ] # numeric digit, left as str just in case
var slide_id = int ( n . substr ( 1 ) )
' x ' : # not sure
pass
if len ( note_hits ) > 1 :
pass # Set multihit on each one
return 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 ]
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 ) :
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
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